실험일지2 - @Transactional 어디까지 사용해야할까?
프로젝트를 진행하면서 팀원들과 오랜 이야기를 한 적이 있었다.바로 @Transactional 사용이다.
습관적으로 우리는 @Transactional을. 붙여서 사용해왔다. 읽기모드로 사용하는 경우는 readOnly로 사용하고,
그렇지 않은 경우는 메소드마다 @Transactional을 붙인다.
하지만, 서비스 레이어 앞에서 붙여주는건 하나의 '비용'이라고 생각하는 의견이 있었다.
이렇게 메서드에 붙이지 않아도 트랜잭션은 작동한다는 의견이었다. 그리고 실제로 이 의견은 맞다.
JpaRepository의 실제 구현체는 SimpleJpaRepository이다.
SimpleJpaRepository를 보면,
실제로 이렇게 readOnly True인 트랜잭션이 달려있다.
save메소드는 Transactional이 달려있다. 당연히 트랜잭션이 보장되어있을 것이다.
그러면 논쟁의 쟁점은 이 이야기였다.
Transactional을 구현 계층부터 시작할 것인가? Repository부터 시작할 것인가?
그렇다면 문제가 없나 한번 따져보자
의혹 1. 혹시 결과가 다르게 저장되지 않을까?
하지만 그럴리가 없을거다 객체도 정상적으로 생성되었고 저장도 Jpa의 의도에 맞게 잘 저장되었다.
의혹 2. 저장되기 전 후의 객체 주소값이 다르지 않을까?
Transactional을 붙여주지 않았다면 JpaRepository의 save 전에 생성된 Inventory의 객체 주소와 save후의 Inventory객체 주소 값이 다르지 않을까 의심해볼 수 있다.
하지만 결과는 같다.
com.programmers.lime.domains.inventory.domain.Inventory@46ac7792
com.programmers.lime.domains.inventory.domain.Inventory@46ac7792
하지만 다음과 같은 경우는 객체 주소값이 다를 수 있다.
"한 메소드 내에서 save와 findById가 사용된 경우 + osiv 꺼져있음"
com.programmers.lime.domains.inventory.domain.Inventory@420fb8
com.programmers.lime.domains.inventory.domain.Inventory@6a1b92da
왜냐하면 활용되는 JPA의 영속성컨텍스트는 트랜잭션 단위로 객체의 주소가 사용된다. 지금 반환된 두 번째 객체는 준영속 상태이다.
만약 트랜잭션이 켜져있다면, 구현 계층부터 repository의 transaction까지 전파가 되었을 것이다.
인지해야할 내용은 스프링은 싱글톤이지만 동시성에 대한 처리가 잘 되어있다는 것이다. 동시에 요청이 오더라도 스레드마다 각각 다른 트랜잭션을 할당한다. 즉, 접근하는 영속성 컨텍스트도 트랜잭션마다 다르다. 기회가 되면 별도로 다뤄봐야겠다.
그러면 기본적으로는 transactional을 구현계층에서 제거한다고 해서 큰 문제는 없어보인다.
그러면 만일의 상황을 가정해보자. 이번에는 수정하는 요구상황이다.
1. 값을 변경한다.
2. 변경된 객체를 저장한다. (변경감지로)
1번에서 객체의 값을 변경한다. 그리고 1번에서 문제가 발생하더라도 데이터에는 전혀 영향이 없다.
데이터는 애초에 값이 아무것도 변경되지 않았기 때문이다.
그렇다면, 이 생각은 어떨까?
1-1 값을 변경한다. 수정되는 값이 다른 엔티티와 연관관계를 갖고있다.
이 경우에는 다른 객체에게 영향을 미칠수도 있으므로, db에 저장되기 전에 문제가 발생하였고, 저장되는 경우에도 문제가 발생한다.
트랜잭션은 여러 테이블의 정합성을 보장해주기 위해서 존재하기 때문에, 잘못 저장이되어 원자성을 보장하지 못하는 경우에는 구현 계층에 transactional을 붙여주는게 더 낫다.
2번의 경우에는 이전에도 테스트해보았듯이 JPA의 경우 어차피 트랜잭션이 걸려있으므로 문제가 발생해도 롤백이된다.
지금까지의 결론으로는 그냥 구현계층에 직접 transactional을 사용하는게 좋아보인다.
하지만, 트랜잭션은 비용이 큰 기술이다. 그러니 적절히 비용을 고민해봐야 한다.
그 비용은 JPA와 관련이 있다.
엔티티 매니저는 기본적으로 DB에 접근하는 커넥션 풀을 점유하고 있다.
만약 트랜잭션이 오래 유지가 되면, DB와의 커넥션도 그만큼 유지되는 것이다.
하나의 예시를 들어보자면, 한 트랜잭션에 외부 API를 사용하는 경우가 있다면, 외부 API의 동작에 의해 트랜잭션처리가 지연되고, 그만큼 DB연결시간도 영향을 받는것이다. 즉 기술의 의존도가 커지는 상황이 있다.
정리해보자면,
1. JPA내부에는 이미 Transactional이 걸려있다. 기술적 검토를 통해 단순한 로직이라면 서비스 단에서 transactional을 사용하지 않아도 된다.
2. 연관관계가 걸려있는 경우, 객체의 상태를 변경시키고 DB에 저장되는 정보에도 영향을 미치는 상황인지 검토해보고 transactional을 사용한다.
3. transactional과 커넥션 풀과의 관계를 생각해야 한다. 앞선 로직에서 지연이 발생하면 그만큼 DB커넥션을 점유하고 있는 시간으로 이어져 기술적 의존도를 고려한 의사결정을 해야한다.
추가 설명
+ 4. 동시성 상황을 고려해야 한다.
JPA를 사용하면서 우리는 락의 범위를 인지해야 한다. 락을 건다는 것은 락을 건 만큼 데이터의 정합성을 보장하기 위해서 사용한다. 그리고 그 범위는 트랜잭션 내부에서 동작해야만 한다.
JpaRepository 인터페이스를 사용하는 경우 entityManager.getTransaction().begin() 메서드를 사용하지 못한다.
그렇지 않으면 TransactionRequiredException에러를 만나게 된다. 이 역시도 기술을 고려한 개발을 생각해야 하는 이유중 하나다.