동현 유
척척석사
동현 유
전체 방문자
오늘
어제
  • 분류 전체보기 (181)
    • BlockChain (48)
      • [paper] Consensus (13)
      • [paper] Execution (19)
      • [paper] Storage (5)
      • [paper] ZKP (1)
      • [paper] Oracle (1)
      • Blockchains (9)
    • Java (19)
      • Java의 정석 (13)
      • Java 파헤치기 (5)
    • Python (20)
      • Python 뜯어보기 (6)
      • 데이터 분석 기초 (5)
      • Python 기초 강의 (6)
      • Python 기초 강의 부록 (3)
    • Golang (0)
    • MySQL (3)
      • programmers (2)
      • 기본 문법 (0)
    • 웹 프로젝트 (IBAS) (36)
      • Django 레거시 (14)
      • SpringBoot api 개편 (14)
      • Infra (3)
      • 서버 장애 기록 (4)
      • 신입팀원 교육 자료 (1)
    • CS (30)
      • Operating System (22)
      • Computer Security (3)
      • Network (4)
      • DBMS (1)
    • 책 (10)
      • 도메인 주도 설계 철저 입문 (9)
      • Real MySQL 8.0 (1)
    • BOJ 문제 풀이 (3)
    • 이러쿵저러쿵 (10)
    • 회고 (1)

인기 글

최근 댓글

최근 글

hELLO · Designed By 정상우.
동현 유

척척석사

웹 프로젝트 (IBAS)/SpringBoot api 개편

[Spring Boot] Jpa 테스트 작성 시 영속성 컨텍스트 관련 주의사항

2022. 8. 25. 02:18
테스트용 엔티티를 돌려쓰지 말 것!

테스트 코드를 작성하다보면 Mock 객체를 매번 생성해주어야한다.

@DataJpaTest 를 통해 트랜잭션 테스트를 진행하다보면

유독 많이 사용되는 객체가 존재한다.

 

예로 회원서비스는 대부분의 서비스가 의존하고 있기 때문에 회원 엔티티를 Mocking 하는 경우가 아주 많다.

 

만약 @BeforeAll 과 같은 메서드를 사용하거나, 테스트 클래스 변수 선언을 통해,

Member 엔티티를 단 하나 생성하고, 테스트 메서드간 돌려 쓴다면,

테스트가 의도치 않게 실패할 수 있다.

또 테스트가 실행되는 순서에 따라 서로 논리적인 의존성이 발생하기도 한다.


그 이유는 다음과 같다.

@DataJpaTest 어노테이션은 내부에 @Transactional 이 붙어서 테스트가 하나 끝나면 롤백이 되도록 설정되어 있다.

다만 여기서 롤백의 대상이 무엇인지 아는게 중요하다.  테스트 db 에 커밋되었던 레코드를 롤백한다. 

 

테스트 케이스가 실패하는 주요 원인은 영속성 컨텍스트도 초기화가 될 것이라 생각하기 때문이다.

영속성 컨텍스트는 일종의 캐시역할을 한다.

트랜잭션이 열려 있을 때, 엔티티에 대한 수정사항이 생기면 캐시를 수정하고, 수정할 때마다 엔티티에 새로운 버전을 부여해서 관리한다. 트랜잭션에 오류가 없이 잘 마무리되면 마지막에 db 에 변경사항을 반영(commit)하고 트랜잭션을 닫는다. 영속성 컨텍스트는 트랜잭션과 생명주기가 동일하므로 트랜잭션과 같이 종료된다. 영속성 컨텍스트에서 관리되던 엔티티들은 detach 상태로 표시되게 된다.

 

또 다른 트랜잭션 테스트가 실행되면, 트랜잭션이 생성되고, 새로운 영속성 컨텍스트가 생성된다. 이 때 전에 사용하던 detach 된 엔티티를 아무생각 없이 재사용하게 되면, 실제로는 비지니스 로직에 문제가 없어도 테스트에 실패할 수 있다. detach 된 엔티티는 merge를 명시적으로 호출하여 새로운 영속성 컨테스트가 관리하도록 해주어야하기 때문이다.

 


코드를 통해 살펴보자

class TestMember {

    public static Member WRITER = new MEMBER(12171652, "홍길동");  // 테스트용 엔티티
    
}
@DataJpaTest
class BoardRepositoryTest {

    @Autowired
    private BoardRepository boardRepository;
    @Autowired
    private TestEntityManager em;
    
    private Member writer = TestMember.WRITER;  // static 변수를 그대로 대입. 

    @DisplayName("게시글을 수정한다.")
    @Test
    public void update() {
    
        //given
        Board board = new Board
                        .id(1)
                        .title("제목입니다.")
                        .contents("내용입니다.")
                        .writtenBy(writer)
                        .build();
        boardRepository.save(board);

        //when
        board.modify("제목이 수정되었습니다.", "내용이 수정되었습니다.", writer.getId());
        boardRepository.save(board);

        //then
        Board updatedBoard = boardRepository.findById(FREE_BOARD.getId())
                .orElseThrow(EntityNotFoundException::new);
        assertThat(updatedBoard.getContents()).isEqualTo("내용이 수정되었습니다.");
        assertThat(updatedBoard.getTitle()).isEqualTo("제목이 수정되었습니다.");
        assertThat(updatedBoard.getWriterId()).isEqualTo(writer.getId());
    }
   
}

 

위와 같은 경우에 게시글 수정 테스트만 단건 실행시킨다면 당연히 테스트는 통과할것이다.

하지만 만약 다음과 같은 테스트가 존재한다고 가정해보자.

@DataJpaTest
class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;


    @DisplayName("회원 이름을 성공적으로 변경한다.")
    @Test
    public updateMemberNameTest() {
    
        //given
        String oldName = TestMember.WRITER.getName(); // "홍길동"
        String newName = "고길동";
        
        //when
        TestMember.WRITER.setName(newName);
        memberRespository.save(WRITER);
        
        //then
        Member updatedMember = memberRepository.findById(TestMember.WRITER.getId());
        assertThat(updatedMember.getName()).isEqualTo(newName);
    }
}

회원 이름을 변경하는 테스트코드가 성공적으로 통과하고 나서,

전체 테스트를 돌릴 때,

회원테스트가 먼저 실행된 뒤에, 게시글 테스트가 실행된다고 해보자.

 

회원 테스트가 성공적으로 통과하고 나면 TestMember.WRITER 는 detached 된 상태의 엔티티이다.

그 후에 게시글 테스트에서 해당 엔티티를 다시 사용하여 save 를 하게 되면 해당 엔티티를 merge 하지 않았기 때문에 오류가 발생한다. 

 

그리고 여러 테스트를 거칠수록 TestMember.WRITER 의 상태와 내부 변수 값이 계속 변하기 때문에, 테스트 실행 순서에 따라 다른 결과값들을 얻게 된다. (여러 테스트를 한꺼번에 실행시킬 때, 그 순서가 일정하게 보장되지 않는다.)


이를 방지하기 위해서는 팩토리 메서드를 사용하는 방법이 있다.

class TestMember {

    // public static Member WRITER = new MEMBER(12171652, "홍길동");  // 변경 전
    
    public static Member WRITER() {
        return new MEMBER(12171652, "홍길동");  // 변경 후
    }
}

위와 같이 사용하면 값은 같지만 엔티티는 매번 달라지므로 언급했던 문제를 회피할 수 있다.

 

'웹 프로젝트 (IBAS) > SpringBoot api 개편' 카테고리의 다른 글

[SpringBoot] 9. OSIV 설정을 통한 쿼리 최적화 방안 고찰  (0) 2022.08.02
[SpringBoot] 8. 하이버네이트 원격서버 암호화 연결 (SSH tunneling 설정)  (4) 2022.07.30
[SpringBoot] 7. SpringSecurity 인증 모듈 개발 (OAuth2, jwt, 소셜로그인)  (4) 2022.07.02
DDD, 어그리게이트 분리를 위한 리팩토링  (0) 2022.06.27
OAuth2 naver 회원 id 형식 문제 (네아로)  (0) 2022.05.14
    동현 유
    동현 유
    Fault Tolerant System Researcher for more Trustful World and Better Lives. (LinkedIn: https://www.linkedin.com/in/donghyeon-ryu-526b8a276/)

    티스토리툴바