🤔 시작하기 전에
개발 과정에서 만난 고민 builder?, Spring Data JPA?, DTO?
[SpringBoot + JPA 게시판 만들기] 개발 과정에서 만난 고민 builder?, Spring Data JPA?, DTO?
🤔 고민 강의, GPT, 구글링을 하며 혼자서 개발하다보니 계속해서 새로운 문제에 직면하게 되었다. 객체를 생성할때에는 어떤 방법이 더 좋지? 인터페이스를 만들고 구현체를 만들어 역할과 구
average1.tistory.com
1.
객체 생성시 Builder 패턴을 사용하여 가독성을 높이고 일관성과 불변성을 해칠 위험을 제거했습니다.
2.
Entity와 DTO의 범위를 Service계층 까지로 설정하였습니다, 즉 변환( Entity -> DTO, DTO -> Entity )이 서비스 레이어에서 일어나도록 제한 함으로 각 계층에 대한 결합도를 낮출 수 있습니다.
3.
Spring Data JPA를 도입하여 구현 클래스 없이 인터페이스 만으로 CRUD를 구현할 수 있게 하였습니다. 간단한 게시판 프로젝트이기 때문에 인터페이스 추상화 비용을 줄이고, 확장 가능성이 있을 때 인터페이스를 만드는 방향으로 정하게 되었습니다.
✔ 사용자 서비스
@Transactional
클래스 전체에 @Transactional(readOnly = true) 속성을 설정함으로써 클래스 전체에 읽기 전용 모드로 설정합니다.
트렌젝션 해당 메소드는 속성없는 @Transactional 어노테이션을 추가합니다.
읽기 전용 모드로 설정하는 이유는 성능 최적화에 있습니다.
쿼리 및 캐싱을 최적화할 수 있고, 데이터 변경이 일어나더라도 변경감지를 위한 스냅샷을 저장하는 동작을 하지 않기 때문에 성능향상을 기대할 수 있습니다.
ResMemberDto
추후에 Controller에 Entity를 리턴하게됨으로 응답객체를 새로 만들어 리턴 해주었습니다.
🔎 MemberService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
// 사용자 회원가입
@Transactional
public Long join(MemberDto memberDto){
Member member = Member.builder()
.name(memberDto.getName())
.email(memberDto.getEmail())
.address(memberDto.getAddress())
.gender(memberDto.getGender())
.password(passwordEncoder.encode(memberDto.getPassword()))
.build();
memberRepository.save(member);
return member.getId();
}
// 사용자 조회
public ResMemberDto findMemberById(Long id){
Member member = memberRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다."));
ResMemberDto resMemberDto = ResMemberDto.builder()
.id(member.getId())
.name(member.getName())
.address(member.getAddress())
.email(member.getEmail())
.gender(member.getGender())
.build();
return resMemberDto;
}
}
✔ 게시판 서비스
savePost
시작하기 전의 DTO와 Entity의 범위를 Service계층으로 설정했기 때문에 userId와 postFormDto를 받아 Member와 Post Entity로 변환하는 작업을 수행합니다.
updatePost
Post Entity는 builder패턴으로 생성되도록 설정하였기 때문에 객체가 생성된 후에 변경이 불가능합니다.
하지만 수정이 필요함으로 Post Entity 내부에서 update라는 메소드를 통해 변경할 수 있도록 메소드를 생성해 주었습니다.
JPA의 변경 감지(dirty checking)기능을 사용하여 Repository에 수정 명령을 직접 보내지 않고, Entity객체를 수정 함으로써 JPA는 commit 시점에 초기에 저장된 Entity의 스냅샷과 비교하여 변경된 내용이 있으면 자동으로 update쿼리를 보내줍니다.
🔎 PostService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {
private final PostRepository postRepository;
private final MemberRepository memberRepository;
// 게시글 등록
@Transactional
public Long savePost(Long userId, PostFormDto postFormDto){
Member member = memberRepository.findById(userId).orElseThrow(()-> new IllegalArgumentException("회원을 찾을 수 없습니다."));
Post post = Post.builder()
.title(postFormDto.getTitle())
.content(postFormDto.getContent())
.member(member)
.build();
postRepository.save(post);
return post.getId();
}
// 게시글 수정
@Transactional
public Long updatePost(Long postId, PostFormDto postFormDto){
Post post = postRepository.findById(postId).orElseThrow(()-> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
post.update(postFormDto.getTitle(), postFormDto.getContent());
return postId;
}
// 게시글 삭제
@Transactional
public void deletePost(Long postId){
postRepository.deleteById(postId);
}
// 게시글 조회
public ResPostDto findPostById(Long postId){
Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
return new ResPostDto(post.getId(), post.getTitle(), post.getContent());
}
}
✔ 테스트 코드
이번기회에 테스트 코드를 처음 작성해 보았습니다.
테스트 코드가 왜 필요고 어떤 테스트 코드가 좋은 코드인지에 대해 알 수 있었고 이전의 테스트 코드 없이 개발했을 때와
의 차이점을 분명히 느낄 수 있었습니다.
https://average1.tistory.com/35
테스트 코드 왜 작성할까요? (TDD?)
🤔 테스트 코드? 소프트웨어의 기능과 동작을 테스트하는 데 사용되는 코드입니다. 개발자가 작성한 코드를 실행하여 예상된 결과가 정상적으로 나오는지 확인하는 데 사용됩니다. E2E(End to End)
average1.tistory.com
🔎 MemberService Test
회원가입( joinMember )
회원가입 후 가입된 memberId를 통해 회원을 조회합니다.
가입한 회원의 데이터와 조회된 회원(findMember)의 데이터를 비교하여 회원 데이터가 데이터베이스에 저장 되었는지를 확인함으로써 회원가입 기능이 잘 작동하는지 테스트 합니다.
@SpringBootTest
public class MemberServiceTest {
@Autowired private MemberService memberService;
@Test
@DisplayName("멤버 회원가입")
public void joinMember() throws Exception {
//given
MemberDto memberDto = MemberDto.builder()
.name("Jang")
.email("test@test.com")
.gender(Gender.M)
.address(new Address("city","street","zipcode"))
.password("test1234")
.build();
//when
Long memberId = memberService.join(memberDto);
ResMemberDto findMember = memberService.findMemberById(memberId);
//then
Assertions.assertEquals(findMember.getId(), memberId);
Assertions.assertEquals(findMember.getName(), "Jang");
}
}
🔎 PostServiceTest
게시글 등록 ( savePost )
가입된 회원만 게시글을 등록할 수 있기 때문에 등록 전에 회원을 생성해 줍니다. ( createMember )
회원가입 테스트와 마찬가지로 게시글을 등록하고 등록한 게시글이 데이터베이스에 동일하게 추가 되었는지 비교함으로써 게시글 등록 기능이 작동하는지 테스트 합니다.
게시글 수정 ( updatePost )
게시글 수정 후 수정된 데이터가 잘 반영되었는지 확인하기 위해 조회된 데이터를 통해 수정 전 데이터와 비교하여 같지 않음을, 수정 후 데이터와 비교하여 같음을 확인함으로써 게시글 수정 기능이 작동하는지 테스트 합니다.
게시글 삭제 ( deletePost )
게시글 삭제시 저장된 데이터가 없어야 함으로 삭제된 게시글을 조회함으로써 Exception을 유도하여 게시글 삭제 기능을 테스트 합니다.
@SpringBootTest
public class PostServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Autowired PostService postService;
@Test
@DisplayName("게시글 등록")
void posting() {
//given
// 회원 생성
Long memberId = createMember();
// 게시글 생성
PostFormDto postFormDto = new PostFormDto("originalTitle", "originalContent");
//when
// 게시글 등록
Long postId = postService.savePost(memberId, postFormDto);
//then
// 등록된 게시글 조회
ResPostDto resPostDto = postService.findPostById(postId);
Assertions.assertEquals(resPostDto.getId(), postId);
Assertions.assertEquals(resPostDto.getTitle(), "originalTitle");
Assertions.assertEquals(resPostDto.getContent(), "originalContent");
}
@Test
@DisplayName("게시글 수정")
public void updatePost() {
//given
// 회원 생성
Long memberId = createMember();
// 게시글 생성
PostFormDto postFormDto = new PostFormDto("originalTitle", "originalContent");
// 게시글 등록
Long postId = postService.savePost(memberId, postFormDto);
// 수정할 게시글 생성
PostFormDto modifiedPostFormDto = new PostFormDto("modifiedTitle", "modifiedContent");
//when
// 게시글 수정
Long updatePostId = postService.updatePost(postId, modifiedPostFormDto);
//then
// 수정 후 게시글 조회
ResPostDto resPostDto = postService.findPostById(postId);
Assertions.assertNotEquals("originalTitle", resPostDto.getTitle());
Assertions.assertNotEquals("originalContent", resPostDto.getContent());
}
@Test
@DisplayName("게시글 삭제")
public void deletePost() {
//when
// 회원 생성
Long memberId = createMember();
// 게시글 생성
PostFormDto postFormDto = new PostFormDto("originalTitle", "originalContent");
// 게시글 등록
Long postId = postService.savePost(memberId, postFormDto);
//when
// 게시글 삭제
postService.deletePost(postId);
//then
// 삭제된 게시글 조회
Assertions.assertThrows(IllegalArgumentException.class, ()->{
postService.findPostById(postId);
});
}
public Long createMember(){
MemberDto memberDto = MemberDto.builder()
.name("test")
.email("test@test.com")
.gender(Gender.M)
.address(new Address("city", "street", "zipcode"))
.password("test1234")
.build();
return memberService.join(memberDto);
}
}
📌 전체 코드
GitHub - Rookie8294/project_board: SpringBoot + JPA 를 활용하여 CRUD 게시판 프로젝트
SpringBoot + JPA 를 활용하여 CRUD 게시판 프로젝트. Contribute to Rookie8294/project_board development by creating an account on GitHub.
github.com
'Project > SpringBoot+JPA 게시판' 카테고리의 다른 글
[SpringBoot + JPA 게시판 만들기] 게시판 페이징 및 N+1문제 최적화 ( Pageable, join fetch ) (0) | 2024.04.25 |
---|---|
[SpringBoot + JPA 게시판 만들기] 로그인, 로그아웃 기능 구현 및 타임리프 적용 (+스프링 시큐리티) (0) | 2024.03.31 |
[SpringBoot + JPA 게시판 만들기] 개발 과정에서 만난 고민 builder?, Spring Data JPA?, DTO? (0) | 2024.02.19 |
[SpringBoot + JPA 게시판 만들기] 프로젝트 생성 및 환경 설정 + 엔티티(Entity) 생성 - 2 (0) | 2024.02.13 |
[SpringBoot + JPA 게시판 만들기] 요구사항 및 프로젝트 설계 - 1 (0) | 2024.02.08 |