티스토리 뷰
예제 코드
Member.class
updateUsername()을 통해 사용자의 이름을 변경할 수 있다.
@Entity
@Setter
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "product_id"))
private List<Product> products = new ArrayList<>();
public void updateUsername(String username) {
this.username = username;
}
}
MemberServiceTest.class
save() 시점에 insert 쿼리문이 발생하고, updateUsername()에서 update 쿼리문이 발생할 것으로 예상할 수 있다.
@SpringBootTest
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager entityManager;
@Transactional
@DisplayName("사용자의 이름을 변경할 수 있다.")
@Test
void updateUsername() {
Member member = new Member();
member.setUsername("희망");
Member savedMember = memberRepository.save(member);
entityManager.flush();
entityManager.clear();
memberService.updateUsername(savedMember.getId(), "이희망");
}
}
그러나 테스트 코드를 실행하면 update 쿼리문이 발생하지 않고 종료된다.
save() 시점에 member 객체가 1차 캐시에 저장되고, updateUsername()에 의해 1차 캐시에 저장된 member 객체가 수정되면서 더티 체킹(dirty checking)이 발생할 것으로 예상했다.
더티 체킹(dirty checking)
JPA의 더티 체킹(dirty checking)이란 1차 캐시(메모리)에 저장된 엔티티의 상태 변화를 자동으로 감지하여 DB에 반영하는 기능이다.
트랜잭션을 처리하는 과정에서 DB에서 조회된 엔티티를 1차 캐시에 저장과 동시에 스냅샷을 저장한다. 스냅샷이란 최초로 조회한 엔티티의 상태이다. 트랜잭션이 커밋되는 시점에 스냅샷과 엔티티를 비교하여 변경 사항이 존재한다면 DB에 반영 후 트랜잭션을 종료한다.
@Transactional는 롤백 테스트를 지원한다.
스프링에서 테스트 코드에 @Transactional을 사용하면 해당 테스트 메서드는 트랜잭션 범위 내에서 실행된다. 테스트가 끝날 때 트랜잭션은 기본적으로 롤백되기 때문에 테스트 코드에서 수정한 내용은 실제로 DB에 반영되지 않고 트랜잭션 종료 시점에 롤백된다. 롤백되는 이유는 테스트 코드가 실제 DB에 영향을 주지 않고 반복 가능한 상태를 만들기 위해서다.
JPA의 더티 체킹은 트랜잭션이 커밋되는 시점에 1차 캐시에 저장된 엔티티의 변경사항을 DB에 반영한다. 따라서 커밋이 발생해야 하는데 테스트 코드에서 @Transactional은 기본적으로 롤백을 하기 때문에 더티 체킹이 발생하지 않는다. 이러한 이유에서 update 쿼리문이 발생하지 않았던 것이다.
그렇다면 테스트 코드에서 @Transactional을 사용하면 안 되는 것인가?
@Transactional을 사용하지 않는다는 것은 @Transactiona이 가져다주는 이점들을 포기하겠다는 것이다. 개발의 편리성을 위해 제공되는 애너테이션을 굳이 사용하지 않을 필요가 있을까?
@Transactional이 가져다주는 이점들은 다음과 같다.
- 테스트에서 사용된 데이터를 자동으로 롤백한다.
- 테스트에서 사용된 데이터가 데이터베이스에 영향을 미치지 않는다.
- 하나의 테스트가 다른 테스트에 영향을 주지 않는다.
- 더티 체킹 및 지연 로딩
- 트랜잭션이 커밋될 때 더티 체킹에의해 변경된 사항이 DB에 반영된다.
- 트랜잭션 내에서 엔티티를 조회할 때 연관된 엔티티들을 지연 로딩으로 가져올 수 있다.
- 테스트 코드에서 @Trasnactional을 사용하지 않으면 불편한 점이 많다.
- 테스트가 종료되면 테스트 데이터를 수동으로 롤백해야 한다.
- @BeforeEach, @AfterEach 등을 사용하여 롤백해야 하는데, 이때 롤백해야할 데이터를 롤백하지 않는 실수를 할 수 있다.
- 롤백에 대한 고민때문에 테스트 코드를 작성하는 것이 지연된다.
- 테스트 코드도 작성해야 하고 롤백해야 하는 코드도 작성해야하기 때문에 어려움이 있다.
- 테스트가 종료되면 테스트 데이터를 수동으로 롤백해야 한다.
테스트 코드에서 @Trasnactional을 사용할 때 주의할 점
@Transactional을 사용하되 1차 캐시의 변경 사항을 명시적으로 flush하자. 예제 코드에서 member 엔티티의 변경 사항이 DB에 반영되지 않은 이유는 1차 캐시에 저장된 변경 사항이 롤백되면서 적용되지 않았기 때문이다. 따라서 트랜잭션이 종료되기 전에 강제로 1차 캐시의 변경 사항을 flush하여 DB에 반영될 수 있도록 한다.
@SpringBootTest
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager entityManager;
@Transactional
@DisplayName("사용자의 이름을 변경할 수 있다.")
@Test
void updateUsername() {
Member member = new Member();
member.setUsername("희망");
Member savedMember = memberRepository.save(member);
entityManager.flush();
entityManager.clear();
memberService.updateUsername(savedMember.getId(), "이희망");
entityManager.flush();
}
}
1차 캐시에 저장된 변경 사항을 강제로 DB에 flush 했기 때문에 update 쿼리문이 발생하는 것을 확인할 수 있다.