@Transactional

텐저린티·2023년 8월 2일
0

Spring

목록 보기
5/6

개념

  • 트랜잭션 처리에 필요한 begin, commit 처리를 분리해, 서비스 로직 관심사에 집중할 수 있도록 하는 어노테이션
  • Spring AOP 가 @Transactional을 선언한 클래스에 대해서 프록시를 만들어줌
  • 트랜잭션은 ACID 원칙을 준수해야함.
    • Atomicity (원자성) : 모두 처리되거나, 모두 롤백되거나
    • Consistency(일관성) : 트랜잭션 성공 시 모든 데이터는 일관성 유지
    • Isolation (고립성) : 트랜잭션 처리 시 외부 간섭 없어야 함.
    • Durability (지속성) : 트랜잭션 처리 시 결과가 영속적이어야 함.

트랜잭션 전파

  • 트랜잭션 처리 중 다른 트랜잭션 처리 발생하는 거
  • 전파 종류
    • REQUIRED
      • 기본값
      • 트랜잭션 필요
      • 진행중인 트랜잭션 존재 → 트랜잭션 사용
      • 진행중인 트랜잭션 부재 → 새로운 트랜잭션 시작
    • MANDATORY
      • 호출 전 반드시 진행중인 트랜잭션 존재해야 함
    • REQUIRED_NEW
      • 항상 새로운 트랜잭션 시작
      • 진행중인 트랜잭션은 잠시 중단되고 새로 시작한 트랜잭션 종료 후 재개됨
    • SUPPORTS
      • 진행중인 트랜잭션이 있는 경우 해당 트랜잭션 사용
    • NOT_SUPPORTED
      • 트랜잭션 불필요
      • 진행중인 트랜잭션 존재 시 중단하고 다른 트랜잭션 종료 후 재개
    • NEVER
      • 진행중인 트랜잭션 있는 경우 예외
    • NESTED
      • 진행중인 트랜잭션 존재 시 중첩된 트랜잭션에서 실행
      • 중첩된 트랜잭션은 서로 독립적
      • 진행중인 트랜잭션 부재 시 REQUIRED와 동일 동작

트랜잭션 격리

  • 트랜잭션이 복수 개 존재할 가능성이 있는 REQUIRED, REQUIRED_NEW 전파인 경우에만 사용 가능

  • 더티 리드
    • 커밋되지 않은 값도 조회 가능한 상태
  • 반복 불가한 조회
    • 한 트랜잭션 내에서 같은 쿼리가 두 번 실행될 때 다른 결과가 나오는 상태
    • 같은 데이터가 변했을 가능성이 있음
  • 팬텀리드
    • 같은 쿼리가 두 번 실행될 때 다른 결과가 나오는 상태
    • 다른 데이터에 접근할 가능성 있음

메소드

  • value()
    • transactionManager 별칭 반환

      String value() default "";
  • transactionManager()
    • 구체적인 TransactionManager 빈 정의를 통해 사용하려고 하는 transaction manager 사용하기 위함

    • Qualifier value 사용

      String transactionManager() default "";
  • label()
    • 각 transaction manager를 구분하는 0 이상의 라벨값

    • 미리 지정된 값을 사용하는 방식

      String[] label() default {};
  • propagation()
    • 트랜잭션 전파 타입

    • 기본값은 Propagation.REQUIRED

      Propagation propagation() default Propagation.REQUIRED;
  • isolation()
    • 새로 시작된 트랜잭션에만 적용

    • Propagation.REQUIRED, Propagation.REQUIRES_NEW 와만 사용 가능

      Isolation isolation() default Isolation.DEFAULT;
  • timeout()
    • 트랜잭션 제한 시간
    • 새로 시작된 트랜잭션에만 적용
    • Propagation.REQUIRED, Propagation.REQUIRES_NEW 트랜잭션 전파 환경에서만 사용 가능
  • timeoutString()
    int timeout() default TransactionDefination.TIMEOUT_DEFAULT;
    String timeoutString() default "";
  • readOnly()
    • 읽기 전용인 경우 true 로 설정 (기본값 false)

    • 런타임에 최적화

    • 읽기 전용으로 설정되어도 반드시 쓰기 엑세스가 실패하는 건 아님

      boolean readOnly() default false;
  • rollbackFor() / rollbackForClassName()
    • 트랜잭션 롤백 유발하는 예외유형 명시
      - 트랜잭션은 기본적으로 RuntimeException (unchecked-exception)에서는 롤백됨
      - Checked-exception에서는 롤백되지 않음
      - checked-exception을 명시해서 롤백하도록 하는 역할

      Class<? extends Throwable>[] rollbackFor() default {};
      String[] rollbackForClassName() default {};
  • noRollbackFor() / noRollbackForClassName()
    • 트랜잭션 롤백 유발하지 않는 예외 명시

    • checked-exception을 명시

      Class<? extends Throwable>[] noRollbackFor() default {};
      String[] noRollbackForClassName() default {};

주의사항

@Transactional 적용

  • 클래스 레벨에서 선언 시, 해당 클래스와 그 서브클래스의 모든 메서드에 기본값으로 적용.
    • 조상 클래스들에는 적용 X
    • @Transaction 선언 클래스를 상속받은 서브클래스에서도 트랜잭션 적용을 위해선 서브클래스에도 따로 @Transactional 선언 필요
  • 우선순위 (1등일 수록 우선순위가 높음)
    1. 클래스 메소드
    2. 클래스
    3. 인터페이스 메소드
    4. 인터페이스
  • 테스트 메소드에 @Transactional 선언 시 테스트 종료될 때 자동으로 롤백
    • 테스트 용 DB에 테스트 내용이 남지 않음

롤백

  • WebEnvironmentRANDOM_PORT, DEFINED_PORT 사용 시 롤백 X
    • 테스트가 별도 스레드에서 실행되므로, 트랜잭션 롤백 X
  • id (식별자) 롤백 X
    • Auto-Increment 옵션은 트랜잭션 범위 밖에서 동작
    • 트랜잭션 롤백되어도, 증가된 식별자 값은 줄어들지 않.
    • 동시성 문제 때문

접근제어자

  • spring AOP는 public 메소드에만 적용
    • Spring AOP의 기본 프록시인 CGLIB 프록시는 동적으로 상속을 통해 프록시 생성
  • private 메소드는 상속이 불가 → 프록시 생성 X
    • 마찬가지로 final 메소드도 상속불가므로, 프록시 생성 X
  • protected 메소드도 AOP 적용되지 않음
    • CGLIB프록시 말고 JDK 동적 프록시 경우 인터페이스 기반으로 동작
    • protected 메소드에서 프록시 동작 불가
    • spring에서는 일관된 AOP 적용을 위해서 protected 메소드도 트랜잭션이 걸리지 않도록 함.

Exception 관리

  • rollbackFor, noRollbackFor 메소드를 이용해 Exception 관리
  • 트랜잭션은 기본적으로 unchecked-exception만 관리
  • 해당 메소드로 checked-exception을 명시해서 롤백 처리
  • 그래도 checked-exception의 경우 try-catch 블록으로 Exception을 발생시켜 롤백하는게 권장된다고 함.

트랜잭션 내부 호출

  • 결론

    • 프록시를 거치지 않고 대상 객체를 직접 호출하면 AOP 적용 안 됨.
    • 따라서 트랜잭션도 적용 안 됨.
  • 예제

    public class AppleSeller {
    
    		public void packAll(List<Apple> apples) {
    				apples.forEach(apple -> pack(apple));
    		}
    		
    		@Transactional
    		public void pack(Apple apple) {
    				bookRepository.save(apple);
    		}
    }
    • 이렇게 되어야 pack() 메소드 호출 시 트랜잭션이 적용된다.

    • 내부 호출된 상황에서는 pack() 메소드를 호출해도 트랜잭션이 적용되지 않는다.

  • 원인

    • Spring AOP는 Target 객체를 Bean 으로 등록하는게 아니라, Proxy를 스프링 Bean으로 등록
    • Target 객체 내에서 Target 객체 내 다른 메소드 호출 시 프록시 거치지 않고 대상 객체 직접 호출
      • 프록시 빈 내부에서 실제 Target 객체 메소드를 호출했다는 의미
      • 프록시를 거치지 않고, 실제 객체 메소드를 직접 호출했다는 것
    • 프록시를 거치지 않았으므로 인터셉트할 수 없음
      • AOP 적용 안 됨.
      • 트랜잭션은 AOP로 적용되기 때문에 트랜잭션도 적용 안 됨.
  • 해결법

    • 내부 호출을 외부 호출로 바꿔줘야함.
    • 내부 호출하는 메소드를 별도 클래스로 분리
    • 내부 호출하는 메소드에 @Transactional선언
    • AopContext 활용 방법
      • AopContext.currentProxy() 메소드로 현재 AOP 프록시를 반환받아, 메소드를 호출 → 트랜잭션 동작 O
    • 셀프 호출 방법
      • 프록시 내부에서 프록시 호출하면 인터셉터 작동 X
      • 프록시 외부에서 호출하는 방식으로 작동되게 하는 것
      • 근데 옛날 방식의 해결법이라고 함.
profile
개발하고 말테야

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

좋은 글 감사합니다. 자주 올게요 :)

답글 달기