Tech/mysql | DB모델링 | JPA

Spring 트랜잭션과 db커넥션 생애주기

glqdlt 2025. 9. 10. 22:32

기본적으로 DataSource 커넥션은 @Transactional이 선언된 메서드가 호출될 때 획득되고, 메서드 실행이 끝나서 트랜잭션이 commit 또는 rollback 되면 커넥션도 반환된다. 그래서 일반적으로는 트랜잭션의 생애주기와 커넥션의 생애주기가 동일하게 간다고 봐도 무관하다.

하지만 경계가 명확히 다른 두 트랜잭션이더라도, 같은 스레드 안에서 동일한 트랜잭션 매니저를 사용하고 전파 속성의 기본값( Propagation.REQUIRED)을 사용중이라면 상황이 달라진다. 이 경우 두 트랜잭션은 분명히 각각 커밋이 되더라도, 이미 ThreadLocal에 바인딩된 커넥션이 있다면 새로 열지 않고 커넥션을 그대로 재사용한다. 나는 이를 모른 체로 디버깅을 하다가 datasource.getConnection()이 한번만 호출되는 것을 겪고는 당혹했던 적이 있다. 

예를 들어, 아래와 같은 구조를 생각해보자.

public class ParentService {

    @Autowired
    private AService serviceA;

    @Autowired
    private BService serviceB;

    public void start() {
        List<?> someData = serviceA.search(); // 데이터 조회 readonly=true
        serviceB.save(someData);              // 데이터 저장 readonly=false
    }
}
@Service
public class AService {

    @Transactional(readOnly = true)
    public List<?> search() {
        // DB에서 데이터 조회
    }
}
@Service
public class BService {

    @Transactional
    public void save(List<?> data) {
        // DB에 데이터 저장
    }
}

위 코드에서 serviceA.search() 와 serviceB.save() 는 서로 별개의 트랜잭션으로 동작할 것처럼 보여진다. 실제로 트랜잭션 경계가 구분이 되어있기 때문에 트랜잭션 자체는 별개로 처리가 된다. 그러나 문제는 serviceA에서 열린 트랜잭션에서 사용된 readonly db커넥션을 serviceB.save() 호출에서도 재사용하면서 readonly=true 상태에서 insert 를 하려고 시도하니 실패하게 된다.

당연하게도 개발자는 트랜잭션 단위가 명확히 나뉘었으니 커넥션도 다른 커넥션을 사용할 것이라고 기대할테다. 하지만 실제 동작은 그렇지 않다. Spring의 트랜잭션 매니저는 ThreadLocal에 커넥션을 보관하기 때문에 하나의 물리 커넥션에서 이전 트랜잭션의 설정(readOnly 등)이 그대로 이어질 수 있다. 

여기서 오해하면 안 되는 것이, 트랜잭션 경계 설정에서 Propagation 속성이 매우 중요한 역할을 한다는 점이다. 만약 Propagation.REQUIRES_NEW라면, 이미 ThreadLocal에 커넥션이 담겨 있더라도 반드시 새로운 커넥션을 열어 새로운 트랜잭션을 시작한다.

사실 이러한 것은 어찌보면 당연하다. DB 세션 단에서 1세션 = 1트랜잭션 원칙이 요구되기 때문이다. 1개의 db세션당 1개의 트랜잭션만 열수 있기 때문에 매우 당연한 이야기다. 단, 부분 롤백을 위한 savepoint 같은 고급 기능은 예외로 두지만, 기본적으로는 하나의 세션은 동시에 두 개의 독립된 트랜잭션을 유지할 수 없다.

여담이지만 hikari cp 에서는 cp 풀의 size 에 대한 계산식을 cpu 와 1개의 thread 에서 사용되는 db 커넥션 수 를 비례해서 계산하길 권장한다. 처음엔 그냥 단순한 공식이겠거니 했지만, 이 포스트에서 다루는 1개의 thread에서 커넥션을 공유할 수 있다는 것이 전제 되지 않는다면 이러한 계산식을 적용할 수 없다는 말이 될 수 있다. 왜냐면 만약 처음에 예상했던 것처럼 트랜잭션 경계마다 db커넥션을 사용한다는 것이었다면 트랜잭션이 정의된 로직마다 커넥션 갯수로 잡아야한다는 것이 계산식에 포함되어있엇을 테니 말이다.

트랜잭션 매니저와 ThreadLocal

작성중