DB에서 조회해온 엔티티에 값을 변경하였다. 이 메서드가 끝난 후에 DB에 값이 변경되어있을 것으로 예상했으나, 실제로는 변경이 반영되지 않았고, 해당 메서드에 @Transactional
을 적용해 엔티티 값 변경을 DB에 정상 반영할 수 있었다.
따라서 이후 엔티티에 값을 수정할때마다
@Transactional
을 사용하였다.
@Transactional
는 인스턴스 값 변경을 감지해주고 DB에 쿼리를 날리는 어노테이션일까?
@Transactional
은 적용 메서드가 호출될때, 트랜잭션을 시작하고 메서드를 종료할때 트랜잭션을 커밋하고, 예외가 발생했을 때 트랜잭션을 롤백한다.
위의 설명을 보면 변경을 감지하고 변경사항을 반영하기 위해 사용한다는 언급은 따로 없다. 그런데 유심히 보면 ✌️커밋✌️이라는 표현이 있다.
이 개념을 이해하기 위해서는 엔티티 매니저의 동작과 엔티티 생명주기를 이해하고 있어야한다. (이전글 참고)
트랜잭션이 커밋된다는 것은 데이터 베이스의 변경 사항을 영구적으로 반영한다는 것이다.
그런데 어떻게하면 순수한 자바코드의 변경을 데이터 베이스의 변경사항으로 옮길 수 있을까?
당연히 영속성 컨텍스트의 내용을 직접 flush
로 데이터 베이스에 반영해야한다.
그런데 이런 방식의 경우는 개발자의 실수가 크게 작용할 여지와, 주된 관심사 외의 추가적인 코드가 필요하게 된다. 따라서 JPA는 트랜잭션을 커밋할 때, flush
를 자동으로 호출한다.
결국
@Transactional
어노테이션으로 시작된 트랜잭션과 호출 종료시 자동으로 플러시 되기 때문에 인스턴스의 변경이 DB에 반영되는 것이다.
@Transactional
없이도 동작하는 이유는 뭘까?하지만 객체 생성시 save 메서드 호출을 하면@Transactional
을 사용허지 않아도 정상 반영이 된다.
이것도 저번에 다뤘는데 JPQL은 쿼리가 실행될때마다 flush가 일어나 데이터베이스와의 반영이 일어나기 때문이다.
메서드 쿼리는 따로 업데이트 메서드 쿼리를 사용하지 않는다.
@Transactional
을 사용하지 않고 저장하고 싶다면 save를 사용하면 된다.
트랜잭션은 데이터 베이스의 상태를 변화시키기 위해 수행하는 단위를 말한다.
이때 작업단위는 쿼리하나가 아닌 논리적인 하나의 작업단위를 뜻한다.
@Transactional
으로 묶인 메서드는 스프링이 프록시 객체를 만들어 메서드를 제정의할때, 앞뒤로 커밋 시작과, 예외 발생시 데이터 베이스의 반영사항을 롤백한다.
이때 자바 객체까지 원래대로 되돌려주는 것은 아니라는 것을 기억하자.
스프링 컨테이너는 트랜잭션 범위와 영속성 컨텍스트의 생존 범위를 동일하게 가져간다. 하나의 트랜잭션이 시작될 때, 영속성 컨텍스트를 생성하고 트랜잭션이 종료될때 영속성 컨텍스트를 종료한다. 따라서 @Transactional
의 범위가 영속성 컨텍스트의 범위와 동일하기 때문에, 영속성 컨텍스트를 사용해야한다면 주의가 필요하다.
특히
@Transactional
이 걸린 서비스 메서드를 테스트 코드 내에서 호출할 때 영속성 컨텍스트의 주기때문에 예상치 못한 결과를 만날 수 있다.!
@Transactional
이 있을때 = 영속성 컨텍스트가 동일할 때public class Service {
@Transactional
public Entity getEntity() {
Entity entity = entityRepository.findById(엔티티id)
return Entity;
}
}
public class ServiceTest {
@Transactional
public Entity getEntityTest() {
service.getEntity() == 예상값
}
}
@Transactional
이 있고 테스트 코드에도 @Transactional
이 있다.시작된 트랜잭션이 존재하는 경우, 해당 트랜잭션을 그대로 사용하고, 없는 경우에 새로 시작한다.
위의 경우에는 이미 테스트 코드에서 트랜잭션이 시작되었기 때문에 새로운 트랜잭션이 시작되지 않고 기존의 영속성 컨텍스트를 사용한다.
해당 영속성 컨텍스트가 테스트 코드의 예상값을 검사하는 로직까지 살아있기 때문에, 동일한 인스턴스 값으로 해당 테스트 코드가 통과한다.
@Transactional
가 없을때 = 영속성 컨텍스트가 다를 때public class Service {
@Transactional
public Entity getEntity() {
Entity entity = entityRepository.findById(엔티티id)
return Entity;
}
}
public class ServiceTest {
public Entity getEntityTest() {
service.getEntity() == 예상값
}
}
서비스 메서드 호출시 시작된 트랜잭션이 없기 때문에 새롭게 트랜잭션을 시작하고, 영속성 컨텍스트도 만든다. 이렇게 만들어진 영속성 컨텍스트는 서비스 메서드 호출이 끝나면서 함께 사라지기 때문에 테스트 코드에서는 영속성 컨텍스트가 죽어있다.
따라서 서비스메서드의 반환값과 테스트 메서드내 예상값은 다른 주소에 저장되어있는 다른 인스턴스이다. ==
연산시 해당 테스트는 실패한다.
@Transactional
을 사용하지 않고 테스트를 진행하고 싶다면 따라서 재정의한 epuals()
를 이용하자. 테스트 코드에서 비교하는 두개의 객체는 다른 주소에 있지만 해당 값들은 동등한 값이다. 따라서 재정의한 epuals()
를 사용해 테스트를 진행할 수 있다.
public class ServiceTest {
public Entity getEntityTest() {
service.getEntity().epuals(예상값)
}
}
==
epuals()
@Id
식별자 동일