스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션10. 스프링 트랜잭션 전파1 - 기본
CS/김영한 스프링 강의

스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션10. 스프링 트랜잭션 전파1 - 기본

여기선 트랜잭션 안에 트랜잭션이 있을 때, 트랜잭션이 2개일때 등 복잡한걸 해볼거다.

일단 status를 해서 트랜잭션 직접 시작하고 종료하는 것부터 해보자.

 

@Slf4j
@SpringBootTest
public class BasicTxTest {

    @Autowired
    PlatformTransactionManager txManager;

    @TestConfiguration
    static class Config {
        @Bean
        public PlatformTransactionManager transactionManager(DataSource datasource) {
            return new DataSourceTransactionManager(datasource);
        }
    }

    @Test
    void commit() {
        log.info("트랜잭션 시작");
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());

        log.info("트랜잭션 커밋 시작");
        txManager.commit(status);
        log.info("트랜잭션 커밋 완료");
    }

    @Test
    void rollback() {
        log.info("트랜잭션 시작");
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());

        log.info("트랜잭션 커밋 시작");
        txManager.rollback(status);
        log.info("트랜잭션 커밋 완료");
    }

}

배웠던 대로 hikari 커넥션 풀에서 커넥션 하나 빌려와서 새로운 트랜잭션을 가져올 때 시작하고 커밋 및 롤백도 잘 된다.

 

트랜잭션을 2개이상 쓰지만 겹치지 않고 따로 했을때도 보자.

각자 다른 트랜잭션으로 커밋하고 있다. 근데도 잘 보면 같은 커넥션 conn0을 다 사용하는걸 볼 수 있다. 하지만 주소값은 다르다.

이게 어떻게 된 거냐면 hikari 풀에서 커넥션을 빌려오는데 이미 트랜젝션이 끝나 작업이 완료된 커넥션이기 때문에 같은 커넥션을 재활용하는 것이다. 다만 그러면 진짜 같은 작업인지 알 수 없기 때문에 hikari에서 자체적으로 구분하기 위해 같은 커넥션이라도 주소값을 매변 다르게 지정한다. 이걸 보고 커넥션이 같아도 다른 작업이라는걸 알 수 있다.

 

롤백도 마찬가지다.

 

 

트랜잭션 전파라는 개념은 트랜잭션 안에 트랜잭션이 있을 때 어떻게 처리할 것인지를 얘기하는 것이며, 지금부터는 기본 옵션인 REQUIRED를 기준으로 한다고 함.

트랜잭션이 아직 안 끝나고 실행중인데 내부에 다른 트랜잭션이 수행될 때 상대적으로 밖에 있는걸 외부, 아닌걸 내부로 나눠서 부른다.

스프링의 경우 기본 옵션으로 이 두 트랜잭션을 하나의 트랜잭션으로 묶어서 만들어준다. 또 이해를 위해 이걸 물리 트랜잭션, 논리 트랜잭션으로 나눠서 정의한다.

기본적인 원칙은 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋되고, 하나라도 롤백되면 전부 롤백되는 것.

 

코드로 예시를 보자.

 

외부 트랜잭션에서 먼저 시작하고 아직 commit을 해서 트랜잭션을 끝마치치 않았는데 한번 더 트랜잭션을 실행한다. 코드 상 외부 내부가 구분 되니까 이렇게 부르는 듯.

그럼 위에서 설명한 개념대로 둘 다 커밋해야 실제 커밋되는거고 한다고 했는데 이게 어떻게 가능한가? 를 설명하려 함.

일단 얘를 실행해보면

뭔가 이상함을 느낄 수 있는데 처음 트랜잭션 시작할 땐 실제 db랑 주고받으며 autocommit false로 하고 커넥션 연결하고 하는 등 실제로 시작을 하는데 그 다음 내부 트랜잭션을 시작할 땐 그냥 참여했다는 말만하고 실제로는 아무것도 안한다. 말 그대로 스프링이 논리적으로 나눠서 부를 뿐이지 실제로는 아무것도 안하는 것. 또 사실 한번 커밋하면 물리적으로 끝나버리는데 어떻게 두번 할까. 왜 이럴까

 

사실은 내부 트랜잭션은 한다고 해놓고 아무것도 안해야 위에서 말한 방식을 수행할 수 있다. 그래야 내부에서 커밋을 하더라도 뭐 있는게 없으니 커밋이 안되서 물리적으로 변화는 없고, 외부에서 해야 아까 내부에서 적용한 내용들까지 같이 적용되며 커밋 되야 하는게 맞다. 그래서 가상으로 트랜잭션이 수행되는 것처럼 속인 것. 자기가 외부인지 아닌지는 isNewTransaction()으로 알 수 있다. 매니저가 실제 트랜잭션 동기화 매니저랑 커넥션 요청 해본다음 이미 누가 점유하고 있는지 여부로 기억하는 것.

 

 

만약 외부에서 롤백이 일어난 경우, 내부로써 가상으로 트랙잭션을 가지고 있는 애가 아무리 커밋을 해도 가짜라 어차피 물리적 트랜잭션을 가지고 있는 애가 롤백을 하니까 안의 것 까지 전부 롤백된다.

 

어려운건 외부가 아닌 내부에서 롤백되었을 경우 어떻게 되는가.

 

외부에서 롤백하면 에러없이 잘 되는것과 달리 내부가 롤백되고 외부에서 커밋하면 에러가 뜨는걸 볼 수 있음. 왜 이렇게 설계했냐면 사실 개발자는 커밋했는데 그대로 진행되는게 더 심각한 위험이기 때문에 에러를 발생시킨다.

 

만약 내부에서 하나라도 롤백이 나왔을 경우, 트랜잭션 동기화 매니저에서 rollbackOnly를 true로 설정하고 계속 진행한다. 애초에 롤백이 나왔다는건 안에서 문제가 발생했으므로 실제 db에 반영이 되면 안되므로 그걸 막는것이다. 그래서 외부에서 커밋을 했을 때 트랜잭션 동기화 매니저 확인 후 이미 rollback only로 설정되어 있어서 커밋할 수 없다고 에러를 발생 시키는 것.

 

하나의 커넥션 가지고 하는게 아니라 진짜 따로따로 하고 싶을 때도 있을텐데 이건 requires_new 옵션을 사용하면 된다. 이러면 내부에서 롤백해도 외부에선 커밋되는 등 따로따로 작용한다.

보면 트랜잭션을 가져오는 척 하는게 아니라 외부 트랜잭션을 미루고 새로운 커넥션을 가져오며 진짜 트랜잭션을 새로 만드는걸 볼 수 있음.

얘도 새로 만드는거라 isNewTransaction()이 true이고, 이게 true이기 때문에 커밋이 실제로 동작하는 것이다.

 

주의할 점은 실제로 커넥션 2개를 물고 있다는 것. 커넥션 풀이 고갈되지 않도록..

 

나머지 옵션도 있긴한데 사실 대부분 REQUIRED만 사용하고 아주 가끔 REQUIRED_NEW를 사용하는 정도라.. 그냥 있다는 것만 알아두고 필요할 때 찾아보면 된다.