[JPA,SpringBoot] KnockKnock 개발일지 - 0102

오늘의 목표

  1. ✔ transaction, commit, flush의 정의와 차이 관계 명확히 공부
  2. ✔ EntityManager의 역할과 사용방법 명확히 정리하기
  3. ✔ @Transactional 이 동작하지 않았던 원인 찾기
  4. ✔ 포스트 삭제됨에 따라 댓글도 같이 삭제되는 기능 코드 구현 및 테스트
  5. ✔ 회원가입과 로그인 CRUD 및 핵심 기능 코드 구현
  6. ✔ 회원가입과 로그인 CRUD 및 핵심 기능 코드 테스트

오늘의 이슈

  1. [TIL📚] flush도 commit이 안되었을 뿐 영속성 컨텍스트의 내용을 DB에 동기화 시킨다 (롤백이 가능한 상태로 동기화 시킴)
  2. @Transactional 어노테이션이 선언된 메소드가 테스트 코드에서 제대로 작동되지 않았던 이유
  3. 회원가입 id 중복 Exception 처리하기
  4. @Before 어노테이션으로 테스트 이전 반복되는 초기 데이터 설정 한 번에 하기!
  5. 💥모든 Service class 에는 @Transactional(readOnly = true) 붙이기

1. flush도 commit이 안되었을 뿐 영속성 컨텍스트의 내용을 DB에 동기화 시킨다

나는 여태까지 바보같이 flush는 query만 날려주고 그게 DB에 동기화를 시키지는 않는 줄 알았다... 근데 그게 아니였다
이번 일을 계기로 flush 와 트랜잭션 commit에 대해 명확히 알게 되어서 다행인 것 같다.
해당 부분은 여기에 자세히 정리했으니 참고하길..!!

2. @Transactional 어노테이션이 선언된 메소드가 테스트 코드에서 제대로 작동되지 않았던 이유

자.. 이게 이번 글의 메인이 되지 않을까 싶다.
이 원인을 찾지 못해서 엄청나게 헤맸는데 결국은 아주 기초적인 개념에 기반된 문제였다.
결론부터 얘기하자면 Test 클래스가 @Transactional 이고,
그 안의 테스트 메소드 안에서 또 하나의 @Transactional 선언된 메소드가 실행되니
두 개의 Transactional이 전파되어 해당 메소드 트랜잭션이 Test 클래스 트랜잭션으로 합류
되었기 때문이다.
다시 말해 작동되지 않았던 @Transactional 어노테이션이 선언된 메소드의 트랜잭션은 Test 클래스에 합류되어 Test가 끝나는 시점에서 함께 트랜잭션되었다는 소리다.

테스트 코드와 @Transactional 어노테이션이 선언되었던 Repository의 메소드를 살펴보며 좀 더 깊게 이해해보자.

@Repository
@RequiredArgsConstructor
public class CommentRepository {
	
     private final EntityManager em;
     
    @Transactional
    public void remove(Comment comment){
        em.remove(comment);
    }

}

@Transactional 어노테이션이 붙었지만 트랜잭션 커밋이 되지 않았던 문제의 메소드

@Service
@RequiredArgsConstructor
public class CommentService {

 public void delete(Long id){
        Comment comment = commentRepository.findOne(id);
        commentRepository.remove(comment);
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class CommentServiceTest {

    @Test
    public void 댓글삭제(){
        //given
        List<Post> posts = postRepository.findByTag("영어");
        Post post = posts.get(0);
        CommentRequest commentRequest = new CommentRequest();
        commentRequest.setComment("this comment should be deleted");
        commentRequest.setPostId(post.getId());
        Comment thisComment = commentService.save(commentRequest);
        //when
        // 🎈 문제의 부분!! delete 쿼리가 나가지 않았었다
        commentService.delete(thisComment.getId());
        //then
        List<Post> posts2 = postRepository.findByTag("영어");
        Post post2 = posts2.get(0);
        List<Comment> comments = post2.getPostcomments();
        Assertions.assertThat(comments.size()).isEqualTo(1);
    }

}

이 코드의 문제는 바로 Test의 @Transactional에 remove 메소드의 @Transactional 이 합류되어 버렸다는 것이다.
댓글삭제 테스트 메소드가 실행될 때 remove의 메소드는 결국 Test가 Commit 되면 같이 Commit되고 Test가 rollback 되면 이 메소드도 함께 Rollback 되어 버리게 되는 것이다.

따라서 Test가 Rollback이 자동으로 되기 때문에 당연히 remove 메소드도 그에 따라 rollback이 되었을 것이며 따라서 delete 쿼리가 나가지 못한 채 테스트 코드가 종료되어 버리게 된 것이다.

그렇다면 Test 클래스의 트랜잭션이 무조건 롤백을 하지 않으면 어떨까!!!!

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback(value=false)
public class CommentServiceTest {
...
}

다음과 같이 Test 클래스에 @Rollback(value=false) 선언을 추가해봤다.

짠🤸‍♀️ 다음과 같이 delete 쿼리가 나가는 것을 확인할 수 있다.

하.지.만 그렇다고 이 테스트 코드가 통과하는 것은 아니다.
왜냐면 여전히 Test 클래스에 @Transactional 어노테이션이 살아있고 따라서 remove메소드의 트랜잭션이 Test 클래스의 트랜잭션과 운명을 같이하고 있기 때문에
Test 트랜잭션이 commit되는 시점에서 remove 메소드의 delete쿼리도 commit되게 되며
그것은 이미 //then 부분의 코드가 모두 실행된 이후에 일어나기 때문에
//then 부분에서는 DB상에 delete 쿼리가 반영되지 않은 내용이 실행되고 검증되게 된다.

당연히 DB 상으로 테스트 코드가 끝난 후에 delete 쿼리도 나갔고 rollback도 이루어지지 않았기 때문에 해당 comment 내용은 지워져있다.
그저 테스트코드 검증이 안되었을 뿐,,,

따라서 해당 @Transactional 코드에서는 그냥 강제로 flush를 해주는 것이 가장 깔끔하다.

 @Transactional
    public void remove(Comment comment){
        em.remove(comment);
        em.flush();
    }

3. 회원가입 ID 중복 Exception 처리하기

Exception 처리는 다음과 같이 간단히 할 수 있다.
나중에 기능 구현하다보면 API에 맞게 Exception을 변경해줘야 하겠지만 그건 나중에 생각하는걸로 ㅠㅠ
너무 어렵다 개발.... 흑흑 생각해야할 게 왜이리 많은지

 //회원가입
    @Transactional(readOnly = false)
    public Member signIn(SignInRequest signInRequest){
        String userId = signInRequest.getId();
        validateSameIdExsist(userId);
        String userPassword = signInRequest.getPassword();
        String nickname = signInRequest.getNickname();

        Member member = new Member();
        member.setUserId(userId);
        member.setUserPassword(userPassword);
        member.setNickName(nickname);

        memberRepository.save(member);
        return member;
    }

    //회원가입시 동일한 id의 회원이 있는지 검증
    private void validateSameIdExsist(String id){
        if(memberRepository.findByUserId(id)!= null){
            throw new IllegalStateException("이미 존재하는 id입니다.");
        }
    }
  • Exception을 기대하는 test 코드 작성
 @Test(expected = IllegalStateException.class)
    public void 중복회원가입안됨(){
        //given
        SignInRequest signInRequest = new SignInRequest();
        signInRequest.setId("testmember1");
        signInRequest.setPassword("1234");
        signInRequest.setNickname("테스트");
        //when
        Member member = memberService.signIn(signInRequest);
        //then
        Assertions.fail("예외 발생 안됨");
    }

4. @Before 어노테이션으로 테스트 이전 반복되는 초기 데이터 설정 한 번에 하기

 @Before
    public void 초기데이터설정(){
        SignInRequest signInRequest1 = new SignInRequest();
        signInRequest1.setId("testmember1");
        signInRequest1.setPassword("1234");
        signInRequest1.setNickname("테스트1");
        Member member1 = memberService.signIn(signInRequest1);

        SignInRequest signInRequest2 = new SignInRequest();
        signInRequest2.setId("testmember2");
        signInRequest2.setPassword("1234");
        signInRequest2.setNickname("테스트2");
        Member member2 = memberService.signIn(signInRequest2);
    }

다음과 같이 test 초반 데이터를 한번에 설정해줄 수 있다.
검색해보니까 juint5 에서 @BeforeEach @AfterEach 요론 기능들을 제공해주던데
따로 의존성을 넣어주어야 하는 건지.. 어째서인지 내 코드에서는 해당 어노테이션이 작동하지 않았다.
이 부분은 나중에 기회 되면 더 찾아봐야겠다.

5. 💥모든 Service class 에는 @Transactional(readOnly = true) 붙이기

아이패드 필기 정리본 보다가 뒤늦게 알게 된 사실..ㅎㅎㅎㅎ
만약에 데이터 조회가 아닌 등록이나 수정, 삭제가 있는 메소드에서는 따로 @Transactional을 붙여서 readOnly 옵션을 해제하면 된다.

왜 그렇게 하는지는 이해가 잘 안가서 ㅠㅠ 아직 트랜잭션 모르는게 너무 많다...
내일 김영한 인프런 강의 그쪽 다시 돌려보면서 이유를 찾아서 정리해봐야겠다.

다음 개발 계획

  1. 왜 모든 Service 클래스에 Transactional (readOnly = true) 붙이는지 공부하기
  2. 회원 계정 정보와 post, comment 데이터 연결하기 (postwriter, commentwriter)
  3. 회원 계정 report(신고제) 관련 핵심기능 구현하기
  4. 회원 계정 정보와 post, comment 데이터 연결 기능 테스트 코드 작성하기
  5. 회원 계정 report(신고제) 관련 핵심 기능 테스트 코드 작성하기

좋은 웹페이지 즐겨찾기