배경
트랜잭션을 사용하는 것과 사용하지 않는 것의 장단점을 따져보고, 상황에 맞게 적절하게 사용하도록 해보자.
트랜잭션 트레이드 오프
Spring에서 영속성을 관리하는 방법에는 두 가지가 있다.
1.
트랜잭션 사용(dirty-check를 통한 변경감지 방식)
2.
트랜잭션 없이 사용(repository의 메서드 직접 호출)
각 방식의 CRUD에 대한 트레이드 오프를 따져보자.
데이터 생성
Spring에서 데이터의 생성 시에는 영속화가 되어있지 않기 때문에, 트랜잭션을 걸어두고 repository.save(entity)를 호출하지 않으면 테이블에 데이터를 저장하지 않는다. 때문에 반드시 save를 호출해야 하므로 트레이드 오프 비교에서 생략한다.
데이터 조회
1.
트랜잭션 사용
•
장점
◦
트랜잭션 격리 수준에 따른 장단점 반영된다.(ex. Repeatable Read → 트랜잭션 내 데이터 조회의 일관성 보장 / 커밋 이전 시점의 데이터만 조회 가능)
◦
트랜잭션의 범위를 명시적으로 나타낼 수 있다.
◦
readOnly 옵션을 통해 데이터베이스나 ORM 상의 약간의 성능 최적화가 가능하다.
◦
읽기 전용 트랜잭션에 대해 1차 캐시를 최적화 가능하다.(Lazy Fetch)
•
단점
◦
트랜잭션을 시작하고 닫는 과정에서 약간의 오버헤드가 발생한다.(애플리케이션에서 관리하는 트랜잭션)
◦
트랜잭션 관리를 위한 추가적인 리소스(메모리, DB 락, 로그)가 사용된다.
2.
트랜잭션 없이 사용
•
장점
◦
트랜잭션 시작/종료 오버헤드 없음, 리소스(메모리, 락, 로그) 최소화 등 성능상 이점이 있다.
◦
애플리케이션 트랜잭션을 걸지 않으면 DB 연결 시 기본적으로 auto-commit 모드로 동작하는데, 트랜잭션을 열고 조회 쿼리 수행 후 트랜잭션을 닫아 트랜잭션 범위를 최소화 할 수 있다.
•
단점
◦
데이터 일관성이 보장되지 않는다.(앞선 조회와 나중의 조회 결과가 값이 달라질 수 있음 / Join이나 복잡한 쿼리에서의 데이터 일관성 보장 안됨)
◦
엔티티 그래프 탐색을 사용할 수 없다(LazyInitializationException 발생)
◦
데이터베이스나 ORM의 읽기 전용 최적화를 활용할 수 없다.
데이터 수정
1.
트랜잭션 사용
•
장점
◦
Dirty-check를 통한 성능 최적화가 가능하다(트랜잭션 종료 시 일괄 update)
◦
Dirty-check를 통해 변경 사항만 update 하는게 가능하다.
◦
데이터 일관성을 보장할 수 있다.(다중 update 중 트랜잭션 실패 시 이전 update 내역까지 롤백)
◦
트랜잭션의 범위를 명시적으로 나타낼 수 있다.
◦
데이터베이스에 변경되는 데이터만 전달하기 때문에 리소스 사용이 적다.
•
단점
◦
애플리케이션 차원에서 해당 메서드가 끝날 때까지 트랜잭션을 유지하고 있어야한다.(리소스 사용)
2.
트랜잭션 없이 사용
•
장점
◦
save 메서드를 직접 호출하여 명시적으로 해당 지점에서 엔티티의 변경이 저장됨을 나타낼 수 있다.
◦
save 메서드 호출 시 즉시 DB에 반영된다.
•
단점
◦
JpaRepository의 경우 내부적으로 병합을 사용하여 변경되지 않은 사항들도 전부 update 된다.(ex. User - age, name 중 age만 변경 → age, name 둘 다 저장)
◦
병합을 사용하기 때문에 엉뚱한 값이 반영되거나 다른 트랜잭션에서 수정한 값을 덮어쓰는 update 부정합이 발생할 수 있다.
◦
병합을 사용하기 때문에 수정되지 않은 모든 필드도 데이터베이스에 전달되어 추가적으로 리소스를 사용하게 된다.
◦
각 save 메서드 호출이 독립적인 트랜잭션으로 실행되기 때문에, 트랜잭션 간 격리가 보장되지 않는다.
◦
트랜잭션 실패 시 그 전까지 발생한 update에 대해 롤백이 불가능하다.
◦
각 save 메서드 호출마다 DB 트랜잭션을 시작하고 종료하기 때문에 오버헤드가 발생한다.
데이터 삭제
1.
트랜잭션 사용
•
장점
◦
트랜잭션을 통해 데이터 일관성 보장할 수 있다.(트랜잭션 실패 시 삭제된 엔티티도 복구)
•
단점
◦
영속성 컨텍스트 동기화 문제가 있을 수 있다.(delete 호출 후 트랜잭션 내에서 다시 조회 시, 여전히 존재)
◦
위의 단점으로 인해 트랜잭션 내 삭제 후 flush가 필요할 수 있다.
2.
트랜잭션 없이 사용
•
장점
◦
엔티티 삭제를 DB에 즉시 반영 가능하다.(경우에 따라 단점이 될 수도 있음)
•
단점
◦
롤백 불가능
session-per-*** 패턴
session-per-operation
session-per-operation은 메서드마다 트랜잭션을 걸어서 단일 스레드에서 메서드 실행 시마다 데이터베이스에 session이 열고 닫는 것이 반복되는 패턴을 말한다. 이는 안티 패턴으로, 이와 같이 소규모 트랜잭션이 반복되는 것은 하나의 트랜잭션으로 묶인 작업보다 잘 수행될 가능성이 낮고 유지보수 및 확장성이 좋지 않다.
session-per-operation 패턴은 auto-commit 모드에서의 쿼리 수행과 유사한 동작이다. Spring에서 트랜잭션을 걸지 않고 여러 엔티티를 반복적으로 조회할 때도 이와 같은 형태로 수행된다.
session-per-request
session-per-request 패턴은 가장 일반적인 트랜잭션 패턴으로 하나의 요청당 하나의 session을 열고 트랜잭션을 시작하여, 전체 작업을 하나로 묶어 처리한 후 session을 닫는 패턴을 말한다. 여기서의 request는 애플리케이션에서 하나의 기능 단위로 해석할 수 있다.
session-per-application
session-per-application 패턴은 session이 애플리케이션에 바인딩되어, 애플리케이션이 실행되는 동안 지속적으로 session이 유지되는 패턴을 말한다. 이 역시 안티 패턴으로, 하나의 session이 여러 스레드에 공유되어 경쟁 상태를 유발하기도 하고 session이 곧 애플리케이션이기 때문에 session을 닫고 롤백하는게 불가능하다. 또한 오랫동안 session을 열어두고 엔티티를 캐싱하므로 메모리 사용량이 지속적으로 증가하게 된다.
결론
데이터 조회 시
1.
트랜잭션 사용
•
여러 번의 조회가 일관된 결과를 보장해야할 때(데이터 일관성이 중요할 때)
•
복잡한 조회 로직이 있고 트랜잭션 중에 읽어온 데이터의 변경을 막아야 할 때
•
많은 엔티티들을 읽거나 여러번 읽어야 할 때(개별 조회 시 작은 트랜잭션 반복 방지 + 읽기 전용 최적화)
•
트랜잭션 격리 수준의 장점을 살려야 할 때
2.
트랜잭션 없이 사용
•
단일 혹은 적은 엔티티의 간단한 조회
•
최신 데이터만 필요하고 일관성이 크게 중요하지 않을 때
•
성능이 굉장히 중요할 때
데이터 수정 시
트랜잭션을 사용하지 않는 것은 단점이 매우 많고 ACID를 보장하지 못하기 때문에, 일반적으로 트랜잭션을 사용하여 처리하는 것이 안전하고 효율적이다.
다만, update를 딱 한 번만 사용하는 경우나 같은 엔티티 여러 개를 update 시 벌크 연산으로 처리한다면 큰 문제 없이 사용하는 것도 가능하다. 하지만 이후 해당 로직에서 추가적인 엔티티 수정이 발생하는 경우 트랜잭션을 까먹고 반영하지 않을 확률이 높기 때문에 가급적 트랜잭션을 통해 처리하는 것이 권장된다.
repository.save() 메서드는 엔티티 생성 시, detached 상태의 엔티티 영속화 시에만 사용하자.
데이터 삭제 시
일반적으로 데이터를 삭제하는 경우는 거의 없고 soft delete 방식을 많이 사용하기 때문에 잘 사용되지는 않는다. 하지만 데이터 삭제하는 경우가 있다면, 트랜잭션의 롤백 기능을 통해 데이터 일관성을 보장하는게 중요하다. 때문에 데이터 삭제의 경우 항상 트랜잭션 내에서 수행되는 것이 권장된다.