JUnit 테스트에서 EntityManager를 이용해보자.

영석·2025년 8월 16일

Spring

목록 보기
4/4
post-thumbnail

🧐 JUnit 테스트코드를 작성하면서

한정판매 플랫폼 Sunpick을 개발과 함께 테스트코드도 작성하려고 한다..

개발 + 테스트코드작성 과정을 하나로 묶어서 작업하는 습관을 들이려고 한다..
프로젝트하면서 시간에 쫓기다 보니 신경을 못썼던 점 반성하고 고쳐보자

SunPick은 현재 JPA로만 DB에 접근하도록 설계하고 있습니다.
JPA 매핑을 위해 테이블 별 Entity를 작성했고, 이를 테스트하는 절차가 필요하다 생각했다.

실제 DB에서 잘 작동이 되는지를 판단하는 테스트코드를 작성해 보자

💻 JPA - Persistence Context

먼저 영속성 컨텍스트가 어떤 역할을 하는지 알아야 제대로 된 테스트 코드를 작성할 수 있다.
다양한 역할을 하지만 현재 포스팅에선 두 가지만 알아두고 넘어가 보자.

✅ 1차 캐시

영속성 컨택스트 내부에 존재하는 엔티티를 잠시 저장해 두는 메모리 공간입니다.
같은 트랜잭션 내에서 엔티티를 캐싱하여 DB 접근을 최소화할 수 있습니다.

✅ 쓰기 지연 (Write - Behind)

Insert, Update, Delete 같은 작업들을 바로 DB에 보내지 않고 Query을 모아두었다가
트랜잭션이 커밋되는 시점에 저장된 모든 Query를 보내는 기능입니다.

네트워크 통신을 최소화하여 성능 최적화 가능합니다.
Query를 한 번에 전송한다면, 문제가 생겼을 때 전송되지 않기 때문에, 원자성을 지원합니다.

🚨 문제발생

Entity Mapping이 잘되었는지 판단하려면 어떻게 테스트를 해야 할까?
당연하게도 실제 DB에 넣어보고, 가져와서 값을 검증하면 될 것 같다.

하지만 Persistence Context를 이해해지 못한 채로 테스트 코드를 작성하면
전혀 다른 테스트가 될 수 있다.

처음 의도한 테스트 설계

Member 엔티티 생성 -> DB 저장 -> DB 조회 -> 데이터 검증

JPA를 이용하여 DB CRUD 테스트 (EntityManager 미사용)

Member 엔티티 생성 -> save 호출 -> 1차 캐시 적재 -> findById() -> 캐시 적중 후 조회 -> 데이터 검증 -> Commit

처음 설계한 테스트 목적은 DB에 저장 후 조회 했을때 데이터 검증이다.
하지만 1차 캐시가 테스트에 포함되어버린다면

1차 캐시를 검증하는 테스트가 아님에도, 처음 의도와 전혀 다른 테스트가 된다

즉 초기 테스트 설계를 지키려면, JPA 1차 캐시를 테스트코드에서 제외해야 했다.

💻 검증

🚨 EntityManager를 사용하지 않고 1차 캐시가 개입된 경우

  • 캐싱이 된 객체를 반환하기 때문에 생성된 Member 엔티티와, findById()를 통해 조회한 Member 엔티티의 참조주소가 같을 것이다.

로그를 찍어본 결과 예상대로 참조주소가 같다, 그리고
캐싱된 객체을 가져왔기 때문에 Select 관련 Query는 DB로 전송되지 않았다.

✅ EntityManager를 사용하여 1차 캐시를 제외한 경우

  • 캐시사용 X, 즉 생성된 Member 엔티티와, findById()를 통해 조회한 Member 엔티티와 전혀 다른 객체 일 것이다.

캐시를 이용하지 않았기 때문에 각각 다른 객체로 생성되어 참조주소가 다르다는 것을 알 수 있다.
추가로 캐시 적중 실패로, Select Query가 DB로 전송된 것도 확인가능하다.

☑️ 해결

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
@Import(JpaConfig.class)
public class MemberJpaMappingTest {

    @PersistenceContext
    private EntityManager em;

    @Autowired
    private MemberRepository memberRepository;

    @Test
    void 멤버_엔티티_필드_매핑_정상작동_테스트() {

        //given
        Member member = TestEntityFactory.testMember();
        Member savedMember = memberRepository.save(member);
        em.flush(); // 모든 Query문 DB에 전송
        em.clear(); // 1차 캐시 정리

        //when
        Member selectedMember = memberRepository.findById(savedMember.getId()).orElseThrow();

        //then
        assertSoftly(as -> {
            as.assertThat(selectedMember.getName()).isEqualTo("이영석");
            as.assertThat(selectedMember.getEmail()).isEqualTo("ssafy@naver.com");
            as.assertThat(selectedMember.getPassword()).isEqualTo("password123");
            as.assertThat(selectedMember.getBirthDate()).isEqualTo(LocalDate.of(1999, 1, 15));
            as.assertThat(selectedMember.getWithdrawnAt()).isNull();
            as.assertThat(selectedMember.getCreatedAt()).isNotNull();
            as.assertThat(selectedMember.getModifiedAt()).isNotNull();
        });
    }
}

em.flush()를 통해 즉시 DB에 Query를 전송하고, em.clear()를 통해 1차 캐시를 정리를 해주자.

이렇게 되면 em.flush() 시점에 DB에 실제로 저장되고,
em.clear()을 통해 when절의 findById()에서 1차 캐시가 아닌 DB에서 데이터를 조회한다.

profile
느리게 갱신되는 개발실력 - >_0

0개의 댓글