개발/Spring DB

스프링 트랜잭션

Debin 2022. 6. 24.
반응형
본 게시글은 인프런 김영한 선생님 강의 스프링 DB 2편을 완강하고 배운 것을 남기고자 적은 포스팅입니다.
강의 링크는 아래와 같습니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard

 

스프링 DB 2편 - 데이터 접근 활용 기술 - 인프런 | 강의

백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의 소개 | 인

www.inflearn.com

들어가기전에 인텔리제이에서 @Transactional을 타자로 직접 치면 2가지가 나온다.
javax.transaction.Transactional과 import org.springframework.transaction.annotation.Transactional 이다.
참고로 javax는 자바 표준 확장 패키지라고 한다.
필자가 이해하기로는 자바 표준 확장 패키지의 @Transactional을 확장시킨것이 스프링의 @Transactional이다.
결국 우리는 스프링 @Transactional을 사용하면 된다.
더 많은 기능을 제공(예를 들어 속성)하고 스프링 프레임워크를 사용할 때 유리한 점이 있기 때문이다.
javax transactional vs spring transactional로 검색하면 영문으로 된 많은 내용들을 참고할 수 있다.

복습

  • 스프링은 추상화(PlatformTransactionManager)를 통해 기술 변경시에 트랜잭션 매니저 문제를 해결한 것을 이미 학습했다.
  • 스프링 부트는 어떤 기술을 사용하는지에 따라 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다.
  • @Transactional 을 통한 선언적 트랜잭션 관리 방식을 사용하게 되면 기본적으로 프록시 방식의 AOP가 적용된다.
  • 프록시 방식을 도입하면 프록시 객체에서는 트랜잭션 로직을 처리하고, 프록시 객체가 진짜 객체를 호출해 비즈니스 로직을 수행한다.

우리는 이제 애노테이션을 사용한 선언적 트랜잭션 관리에 대해 더 공부해보겠다.

스프링 트랜잭션

첫 번째 실습 (트랜잭션 적용 확인 및 프록시 개념 학습) 

  • TransactionSynchronizationManager.isActualTransactionActive()의 boolean 리턴값을 출력해 트랜잭션 적용을 확인했다.
  • @Transactional 을 메서드나 클래스에 붙이면 해당 객체는 트랜잭션 AOP 적용의 대상이 되고, 결과적으로 실제 객체 대신에 트랜잭션을 처리해주는 프록시 객체가 스프링 빈에 등록된다.
  • 그리고 주입을 받을 때도 실제 객체 대신에 프록시 객체가 주입된다. 프록시 객체도 직접 확인했다.
  • 프록시는 BasicService 를 상속해서 만들어지기 때문에 다형성을 활용할 수 있다.
  • 따라서 BasicService 대신에 프록시인 BasicService$$CGLIB 를 주입할 수 있다

스프링 DI 컨테이너에 트랜잭션 프록시 등록

여기서 중요한 부분이 있다. 바로 프록시 객체에서는 트랜잭션 적용에 관한 로직을 수행한다.

프로그램 로직, 비즈니스 로직 등 실제 로직은 실제 객채에서 행해진다.

두 번째 실습 (트랜잭션 적용 위치) 

  • 스프링의 우선 순위는 항상 더 구체적이고 자세한 것이 높은 우선순위를 가진다.
  • 인터페이스 타입 -> 인터페이스의 메서드 -> 클래스 타입 -> 클래스 메소드 순이다.
  • 웬만하면 인터페이스에 트랜잭션 애노테이션을 작성하지 말자. 구체 클래스에서 작성하자.

스프링 트랜잭션 주의사항

들으면서 아차 싶었고 흥미로웠던 부분이다. 아주 중요한 부분이니 잘기억하자!

결론부터 말하면 프록시를 거치지 않고 실제 객체를 직접 호출하면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다.

@Transactional 을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 객체를 호출해준다.

위 두 문장을 잘 기억하자.

AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다.
따라서 스프링은 의존관계 주입시에 항상 실제 객체 대신에 프록시 객체를 주입한다.
프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않는다.
하지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
이렇게 되면 @Transactional 이 있어도 트랜잭션이 적용되지 않는다.

내부 호출의 문제를 코드로 살펴보자.

@Slf4j
@SpringBootTest
public class InternalCallV1Test {

    @Autowired
    CallService callService;

    @Test
    void printProxy() {
        log.info("callService class={}", callService.getClass());
    }


    @Test
    void internalCall() {
        callService.internal();
    }
    @Test
    void externalCall() {
        callService.external();
    }

    @TestConfiguration
    static class InternalCallV1Config {
        @Bean
        CallService callService() {
            return new CallService();
        }
    }

    @Slf4j
    static class CallService {

        //트랜잭션이 필요하지 않다고 가정
        public void external() {
            log.info("call external");
            printTxInfo();
            internal();//여기가 중요하다.
        }

        //트랜잭션이 필요하다고 가정
        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

    private void printTxInfo() {
        boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
        log.info("tx active={}", txActive);
        }
    }
}

중요한 점은 external()은 트랜잭션이 없고, internal()은 @Transactional을 통해 트랜잭션을 적용한다는 것이다.

@Transactional 이 하나라도 있으면 트랜잭션 프록시 객체가 만들어진다.

그리고 callService 빈을 주입 받으면 트랜잭션 프록시 객체가 대신 주입된다.

 

우선 internal() 메서드의 흐름을 살펴보자.

  1. callsService.internal()이 호출되는데 여기서 callService는 프록시 객체다.
  2. 그럼 트랜잭션 프록시를 호출한 것이므로 internal() 메서드에 @Transactional이 붙어 있는 것을 보고 트랜잭션을 적용한다.
  3. 이제 트랜잭션이 적용 됐으면 실제 객체를 호출해서 비즈니스 로직을 수행한다.
  4. 처리를 완료하면 트랜잭션 프록시로 돌아오고, 트랜잭션 프록시는 트랜잭션을 완료한다.
  5. 트랜잭션을 적용한 것을 확인했다. 결론적으로 잘 돌아간다!!

문제는 바로 external() 메서드를 호출하는 것이다. external()은 internal()을 내부 호출한다. 흐름을 살펴보자.

  1. callsService.external()이 호출되는데 여기서 callService는 프록시 객체다.
  2. 그럼 트랜잭션 프록시를 호출한 것이다.
    그러나 external()의 메서드는 @Transaction이 붙어 있지 않아 트랜잭션을 적용하지 않는다.
  3. 트랜잭션을 적용하지 않고, 실제 객체를 통해 external()을 호출한다.
  4. external()은 내부에서 internal()을 호출한다. 여기서 문제가 발생한다.

문제 발생!!!

자바에서는 메서드 앞에 참조가 없으면 this가 생략된 것이다. 즉 this.internal()을 호출한 것이다.

그런데 우리가 학습하기로는 트랜잭션은 오직 트랜잭션 프록시를 거쳐서 시작한다.

그러나 생각해보면 실제 객체 external()에서 실제 객체 internal(), 즉 this.internal()을 호출한 것이 되어버린다.

따라서 트랜잭션이 걸리지 않는다. 정말 재밌는 내용이었다.

결론은 프록시를 거치지 않아서 트랜잭션이 안걸렸다!!!

 

그렇다면 해결책을 알아봐야 하는데 보통 해결책은 internal()같은 메서드를 별도의 클래스로 분리해서 사용한다고 한다.

이러면 내부 호출을 외부 호출로 변경해 문제를 해결할 수 있다. 프록시를 거쳐 문제를 해결할 수 있었다.

역시 자바 기초가 중요하다. this에 관한 부분을 잘 짚고 넘어가자.

 

참고

  1. 스프링의 트랜잭션 AOP 기능은 public 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어있다.
    따라서 public 이 아닌곳에 @Transactional 이 붙어 있으면 예외가 발생하지는 않고, 트랜잭션 적용만 무시된다.
  2. 초기화가 코드가 먼저 호출되고, 트랜잭션 AOP가 적용되므로, 초기화 시점에는 메서드에서 트랜잭션을 획득할 수 없다. 
    @PostConstruct을 이용해 트랜잭션 확인이 불가능!
  3. 2번의 문제는 @EventListener(value = ApplicationReadyEvent.class)를 통해 해소할 수 있다.

스프링 트랜잭션 옵션

트랜잭션 매니저를 지정할 수 있는 옵션, 트랜잭션 전파 , 트랜잭션 격리 수준 등 많은 내용을 학습했다. 

그중에서도 제일 인상 깊은 롤백에 관한 옵션에 대해 정리해보겠다. 다른 옵션에 대해서는 강의를 확인하면 좋을 것 같다.

rollbackFor 옵션

예외 발생시 스프링 트랜잭션의 기본 정책은 다음과 같다.

  • 언체크 예외인 런타임 예외 계층과, Error과 그 하위 예외가 발생하면 롤백한다.
  • 체크 예외와 그 하위 예외들은 커밋한다.
  • 아래 옵션을 사용해 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할지 지정할 수 있다.
@Transactional(rollbackFor = Exception.class)

예를 들어서 이렇게 지정하면 체크 예외인 Exception 이 발생해도 롤백하게 된다. (하위 예외들도 대상에 포함된다.)

예외와 트랜잭션 커밋, 롤백

예외가 발생했는데, 내부에서 예외를 처리하지 못하고, 트랜잭션 범위 밖으로 예외를 던지면 아래 그림과 같을 것이다.

예외 발생

  • 예외 발생시 스프링 트랜잭션 AOP는 예외 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
  • 언체크 예외 계열은 롤백, 체크 예외 계열은 커밋한다.
  • 물론 개발자가 규칙을 수정할 수도 있다. 실제로 테스트를 통해 이렇게 동작하는 것을 확인했다.

그럼 이제 의문이 생긴다. 왜 체크 예외는 커밋하고 언체크 예외는 롤백할까?

스프링은 아래와 같이 가정한다.

  • 체크 예외는 비즈니스 의미(비즈니스 예외)가 있을 때 사용한다.
  • 언체크 예외는 복구 불가능한 예외로 가정한다.
  • 물론 이런 정책을 꼭 지킬 필요는 없다. 

비즈니스 예외는 무엇일까?? 강의에서는 예시로 상품을 주문시 결제 비용이 부족하면 이것은 비즈니스 예외라고 했다.

'주문시 잔고가 부족하면 주문 데이터를 저장하고 ,결제 상태를 대기로 처리한다.' 라는 방식으로 비즈니스 예외를 처리했다.

비즈니스 예외는 매우 중요하고, 반드시 처리해야 하는 경우가 많으므로 체크 예외를 고려할 수 있다.

시스템 예외도 있다. 시스템에 문제가 있어서 발생하는 예외!

 

예외는 생각할 부분이 많다. 그리고 이번에 공부를 하면서 아래 영상이 생각났다.

https://www.youtube.com/watch?v=_WkMhytqoCc&t=280s 

물론 스프링이 정해준 규칙이 있지만, 개발자끼리, 팀끼리 잘 정의해서 예외 처리 방식을 정하면 좋을 것 같다.

 

이상으로 포스팅을 마칩니다. 감사합니다.

반응형

댓글