프로그래밍/JPA

테스트 코드에서 영속성 컨텍스트를 주의하자!

승민아 2024. 8. 16. 23:04

테스트 코드를 작성하며 

Getter를 이용해 .get 메서드를 통해 개수나 엔티티를 조회할때

@OneToMany, @ManyToOne과 같은 연관 관계를 맺은 엔티티를 조회하고자할때

조회가 되지 않는 경우를 자주 겪었었다.

 

아마 이 글을 검색해서 들어온 여러분들도...

테스트 코드에서 왜 연관 관계 매핑을 제대로 했는데 원하는대로 테스트를 통과하지 못할까싶어서 들어왔을 것 같다.

 

간단하게 이해를 돕기위해 다음과 같은 예시 테스트 코드를 작성했다.

한번 읽어보자!

 

상황 파악!

member(회원)을 저장하고

member(회원)이 poster(게시글)를 작성한 상황이다.

이때 회원이 작성한 게시글의 개수를 조회하고자 한다.

@Test
@DisplayName("예시")
void test1() {

    // 회원 저장
    Member member = new Member();
    String loginId = "loginId";
    member.setLoginId(loginId);
    memberRepository.save(member);

    // 회원이 게시글 작성
    Poster poster = new Poster();
    poster.setMember(member);
    posterRepository.save(poster);
        
    // 회원 조회
    Member findMember = memberRepository.findMemberByLoginId(loginId)
        .orElseThrow(() -> new IllegalArgumentException());

    // 회원이 작성한 게시글 조회
    Assertions
        .assertThat(findMember.getPosters().size())
        .isEqualTo(1);
}

회원을 저장하고, 게시글도 작성자를 지정하고 저장했다.

 

findMember를 유심히 보자.

DB에서 findMember를 조회하고 findMember의 게시글은 연과 관계 매핑이 되어 있기 때문에

게시글 조회가 가능하다. 게시글의 개수는 1개가 나올 것이다.

 

테스트 결과

테스트 결과는 Fail...

게시글(poster)도 insert 쿼리로 들어갔는데

왜! 게시글의 개수가 조회가 안될까?

 

유심히 보면 회원이 작성한 게시글을 조회하는 SELECT 쿼리 또한 나가지 않은 것을 볼 수 있다.

 

왜 SELECT 쿼리가 안나간 것 일까?

 

문제 원인!

https://non-stop.tistory.com/482

 

[JPA] 영속성 컨텍스트, 엔티티의 생명주기

Entity ManagerFactory, Entity Manager 1. 엔티티 매니저 팩토리의 생성 비용은 매우 크므로 하나만 만들어 애플리케이션 전체에서 공유한다. 2. 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안

non-stop.tistory.com

이전에 작성한 영속성 컨텍스트를 보고 아하~ 했다.

역시 상황을 겪으며 배우는게 학습에 직빵이다.

 

Managed : 영속 상태

엔티티를 persist() 했다고해서 바로 DB에 저장되는 것이 아니다.

Managed로 영속 상태(영속성 컨텍스트에 저장된 상태)가 된다.

 

영속성 컨텍스트의 내용을 DB에 반영하고 싶다면

flush()를 통해 DB에 반영해야한다.

 

심지어 우리가 flush()를 통해 DB에 반영했고

바로 em.find()를 한다고해서 조회 쿼리를 날리는 것도 아니다..

 

게시글 내용 일부

1차 캐시 특징 때문에

영속성 컨텍스트에 있는 데이터를 가져온다.

 

자 그럼 해결 방법을 위한 개념들은 다 이해했으니 해결 해보자.!

 

해결 방법!

member와 findMember의 hashCode 값을 출력해보자.

System.out.println("member HashCode : " + member.hashCode());
Member findMember = memberRepository.findMemberByLoginId(loginId)
        .orElseThrow(() -> new IllegalArgumentException());
System.out.println("findMember HashCode : " + findMember.hashCode());

 

hashCode 출력 결과

member와 findMember의 hashCode 값이 똑같다!

기존 회원이랑 조회 쿼리를 날려서 얻은 회원이랑 똑같은 객체라는 말이다...

 

이것이 1차 캐시 특징 때문에

영속성 컨텍스트에 저장되어 있는 member를 그대로 반환 시켜준것이다.

 

기존 회원 객체는 DB를 통해 조회한 회원이 아니기에 Getter를 통해 생성된 get 메서드로 게시글을 꺼내봐도

게시글이 있을리가 없다.!

 

그러면 영속성 컨텍스트에 없어야 DB 조회를 통해 findMember를 얻을 수 있을 것이다.

 

이 문제는 em.clear()로 해결할 수 있다.

em.clear()는 영속성 컨텍스트를 비워주기에, 다음 조회시 DB를 통해 가져오게 된다.

 

이제 em.clear()를하고 hashCode 값을 보자!

em.clear();
System.out.println("member HashCode : " + member.hashCode());
Member findMember = memberRepository.findMemberByLoginId(loginId)
        .orElseThrow(() -> new IllegalArgumentException());
System.out.println("findMember HashCode : " + findMember.hashCode());

 

em.clear() 결과

드디어! 회원의 게시글을 조회하는 쿼리가 생성되고 hashCode 또한 달리 출력된다.

DB 조회를 통해 엔티티를 가져왔다는 것이다.

 

테스트 코드 통과

물론 이제 회원의 게시글도 조회를 잘 하기에

테스트 코드도 이제 정상적으로 통과한다.

 

결론!

@Test
@DisplayName("예제1")
void test1() {

    // given & when 부분
    // given, when 코드 작성

    // then 부분
    em.flush();
    em.clear();
    // Assertions 코드 작성
    
}

영속성 컨텍스트와 DB의 상태 차이 때문에 테스트 코드가 정상적으로 동작하지 않을 수 있기 때문에

테스트 코드 작성 후에 검증 코드 이전에

em.flush()로 영속성 컨텍스트의 내용을 DB에 반영하고,

em.clear()로 영속성 컨텍스트의 내용을 비워서

DB 내용을 통해 동작할 수 있도록 하자!