JPA의 EntityManager와 트랜잭션 개념을 공부하던 중, 하나의 의문이 생겼다.
EntityManager는 애초에 데이터베이스에 대한 CRUD 작업을 객체지향적으로 처리하기 위해 설계된 것인데,
정작 DB에 데이터를 쓰는 persist(), remove() 같은 작업을 수행할 때 트랜잭션이 없으면 정상적으로 동작하지 않거나 에러가 발생한다는 것이다.
하지만 트랜잭션은 본래 DB와는 무관하게, 어떤 작업 단위를 원자적으로(One or Nothing) 처리하기 위해 존재하는 개념 아닌가?
그렇다면 EntityManager로 DB 작업을 하면서 트랜잭션이 없으면 안 된다는 것이 설계 관점에서 자연스러운 것일까, 아니면 비정상적인 강제성일까?
이런 구조가 단지 구현상의 제약인지, 아니면 JPA의 의도된 철학인지에 대한 궁금증이 생겼다.
이 의문을 해결하기 위해 JPA와 트랜잭션이 실제로 어떤 구조 안에서 동작하는지를 더 깊이 살펴보게 되었다. 결론부터 말하자면, JPA에서 EntityManager를 통해 DB 쓰기 작업을 수행할 때 트랜잭션이 필수적인 이유는 단순한 기술적인 제약이 아니라, JPA가 의도한 설계 철학 때문이다. 이 설계는 단지 DB에 insert를 하기 위한 도구를 제공하는 것을 넘어, 객체지향적인 상태 관리를 통해 변경을 모아서 일괄적으로 처리함으로써 성능과 정합성을 동시에 확보하려는 전략을 내포하고 있다.
JPA의 EntityManager는 단순한 JDBC의 대체제가 아니다. 우리가 persist(), remove(), merge() 등을 호출할 때 JPA는 곧바로 SQL을 실행하지 않는다. 그 대신, 해당 엔티티 객체를 영속성 컨텍스트라는 1차 캐시에 등록하고, 실제 SQL은 나중에 flush() 시점에 한꺼번에 실행되도록 보류한다. 이 방식을 쓰기 지연 전략(write-behind)이라고 한다. 예를 들어, em.persist(student)를 호출했다고 해서 바로 insert 쿼리가 DB에 날아가는 것이 아니다. 이 시점에서 JPA는 "아, 이 객체는 저장 대상이구나" 정도로만 인식하고, 실제 SQL 실행은 하지 않는다. 그 이유는 변경 사항을 여러 개 모아서 트랜잭션 종료 직전에 한 번에 처리함으로써 DB에 대한 I/O를 줄이고, 네트워크 왕복 비용까지 최소화할 수 있기 때문이다.
문제는 여기서 트랜잭션이 없으면 flush 타이밍을 제어하거나 보장할 수 없다는 점이다. JPA는 기본적으로 트랜잭션 커밋 직전에 자동으로 flush를 수행하여 DB 반영을 하게 설계되어 있다. 그런데 트랜잭션이 없다면 flush가 언제 호출될지, 심지어 flush가 필요한지도 JPA가 판단하기 어렵다. 예를 들어, 우리가 여러 개의 엔티티를 persist()로 등록하고 나중에 한꺼번에 commit()을 기대한다고 하자. 이때 트랜잭션이 없다면 각각의 persist()는 제각각 DB에 반영될 수도 있고, 어떤 것은 DB에 반영되지 못한 채 프로그램이 종료될 수도 있다. 결과적으로 데이터 정합성과 무결성, 그리고 일관성이 심각하게 깨질 수 있는 상황이 발생하는 것이다.
게다가 일부 DBMS(MySQL 등)는 트랜잭션이 명시되지 않은 상태에서 flush를 시도하면 예외를 발생시킬 수 있다. 이는 JPA가 flush 타이밍을 개발자에게 맡기지 않고, 트랜잭션 커밋이라는 명확한 지점에서 안전하게 DB와 동기화를 하도록 강제하는 이유이기도 하다. 트랜잭션 없이 작업을 처리하려 한다면, 복합적인 작업(예: insert → update → delete)이 중간에 실패했을 때 이를 롤백할 수 있는 수단이 없으므로, DB 무결성이 깨지거나 쓰레기 데이터가 쌓이는 결과로 이어질 수 있다.
이처럼, JPA의 트랜잭션 강제는 단순한 제약이나 구현상의 한계가 아니라, 철저히 의도된 전략적 선택이다. JDBC처럼 하나의 명령마다 즉시 DB에 반영하는 방식은 직관적일 수 있지만, 성능상 불리하고, 복잡한 애플리케이션에서는 데이터 정합성을 스스로 모두 책임져야 하는 부담이 크다. 반면 JPA는 객체 상태를 조용히 추적하고 있다가, 트랜잭션이라는 울타리 안에서 필요한 SQL만 한꺼번에 실행하여 일괄 처리한다. 이 방식은 단순히 편리함을 넘어서, 안정성과 성능을 동시에 확보하는 방식이다.
따라서 "EntityManager가 DB 작업을 하는데 왜 트랜잭션이 필요할까?"라는 의문은, "왜 JPA는 객체의 변경을 트랜잭션 안에서만 추적하려고 할까?"라는 더 근본적인 질문으로 이어진다. 그 이유는 JPA가 단순한 데이터 삽입 도구가 아니라, 객체지향적인 상태 변경 추적기이며, 이 변경들을 안전하게 DB에 반영하기 위해 트랜잭션이 반드시 필요하기 때문이다. 즉, JPA에서의 트랜잭션은 단지 DB의 commit/rollback을 위한 수단이 아니라, JPA의 전체 동작 흐름을 결정하는 핵심적인 역할을 수행한다.
이런 관점에서 보면, JPA가 트랜잭션 없는 DB 쓰기를 허용하지 않는 이유는 매우 설계적으로 정당하며, 실제 대규모 애플리케이션에서의 데이터 정합성과 퍼포먼스를 유지하기 위한 현명한 선택임을 이해할 수 있다.
앞으로 JPA를 사용할 때는 단순히 트랜잭션이 있어야 하니까 붙이는 것이 아니라, 트랜잭션이 있어야만 JPA가 제 역할을 할 수 있다는 구조적 이해를 바탕으로 설계하는 것이 필요하다고 느꼈다.
'SPRING (SPRINGBOOT)' 카테고리의 다른 글
@Transactional(readOnly = true) 트랜잭션 내에서 DB에 쓰기를 수행하면 어떤 문제가 발생할까? (0) | 2025.05.06 |
---|