Framework/Spring

@Transactional은 어떻게 쓰는것일까?

KOOCCI 2022. 8. 21. 00:02

목표 : @Transactional의 사용법을 설명할 수 있다.


Transaction을 유지하는 방법 정도로 알고 있는 Annotation에서 벗어나서, 조금 더 자세히 알아보도록 하자.

 

그동안은 어떻게 써왔는가?

일단 로직이 길 수 밖에 없는 상황에 써왔다. 다시 말해 쓸 수 밖에 없었다.

Transactional을 통해 묶어주어야, 2개 이상의 쿼리에 대해 DB Commit 과 Rolback이 가능하도록 관리된다고 알고 있었고, 연동 구간이 길 수 밖에 없는 로직, 예를 들면 기기제어, 각종 로그인, UI를 통한 컨텐츠 변경 등의 작업이 필요할 때마다 Service에서 사용하는 Method에 @Transactional를 설정하여 처리했다.

 

과연 난 잘 써왔을까?

 

Transaction

Transaction을 모르고 사용하는 사람은 없을 것이다.

그러니 한번 정도는 정리하고 가보자.

 

우선 Transaction상호작용의 단위이다.

데이터베이스 관리 시스팀 혹은 그와 유사한 시스템에서 사용하는 상호작용의 단위라고 할 수 있다.

ACID라고 하는 특징을 가진 변화에 있어서의 한 단위로 다시 말할 수 있다.

Transaction이라는 한 묶음은 ACID(Atomic, Consistency, Isolation, Durability)를 보장한다.

다시 말해, Transaction이라는 단위는 독립적이고, 원자성을 가지며, 일관성있게 영구적인 상태를 가지게 된다.

 

데이터는 변화에 민감해야 하며, 서로에게 영향을 행사할 수 없는 독립성(Isolation)을 가진다.

또한, 중간에 흐트러지지 않는, 어줍잖게 되고 있는 그 중간같은 값이 없이, 되거나 안되거나, 0 or 1 영역의 원자성(Atomic)을 가져야 한다.

이런 행위 전후에는 데이터베이스가 가지고 있는 제약이나 규칙에 위배되지 않고 일관성(Consistency)을 지켜야 한다.

마지막으로, 성공적인 수행 이후에는 계속 유지되는 지속성(Durability)이 있어야 한다.

 

Spring에서의 Transaction 설명

그럼 내부를 살펴보기 전에, Spring에서는 어떻게 설명하고 있는지 확인해보자.

 

Transaction Management

먼저, Spring에서의 Transaction 관리의 이점은 다음과 같다.

A consistent programming model across different transaction APIs, such as Java Transaction API (JTA), JDBC, Hibernate, and the Java Persistence API (JPA).
JTA(Java Transaction API), JDBC, Hibernate 및 JPA(Java Persistence API)와 같은 다양한 트랜잭션 API에서 일관된 프로그래밍 모델.
Support for declarative transaction management.
선언적 트랜잭션 관리를 지원
A simpler API for programmatic transaction management than complex transaction APIs, such as JTA.
JTA와 같은 복잡한 트랜잭션 API보다 프로그래밍 방식의 트랜잭션 관리를 위한 더 간단한 API
Excellent integration with Spring’s data access abstractions.
Spring의 데이터 접근 추상화와의 탁월한 통합.

 

Advantages of the Spring Framework’s Transaction Support Model

java EE 개발자는 Global Transaction이나 Local Transaction을 활용해 Transcation을 관리했다. 그러나, 둘 다 한계가 있다.

Local Transaction
쉽게 표현하면, 하나의 DB와의 연결이다. JDBC Connection 등 전용 리소스를 사용한다.
단점은, 여러 Transaction Resource인 상황에선 유효하지 못하다. Global Transaction 에서는 실행할 수 없다는 것이다.


Global Transaction

여러 개의 DB 작업을 하나의 트랜잭션으로 묶는다. Java는 이를 위해 JTA(Java Transaction API)를 제공한다.

Local Transaction이냐, Global Transaction이냐에 따라 코드도 다르고, 어떤 DB에 접근하냐에 따라 사용하는 트랜잭션 코드가 달라진다. 즉, Data Access 기술에 종속이 되어 버린다.

그리고 스프링은 트랜잭션 기술의 공통점을 모아, 추상화를 제공한다.

 

Spring Framework’s Consistent Programming Model

스프링은 일관성 있는 프로그래밍 모델을 지원한다. 즉, Global Transaction과 Local Transaction의 단점들을 해소해준다.

결과적으로 각기 다른 환경에서 다른 트랜잭션 관리 전략을 쓰더라도, 코드는 한 번만 작성하면 된다.

스프링에서는 Declarative & programmatic transaction management를 둘다 지원한다.

보통, Declarative transaction management를 선호하며 권장한다.

 

이렇게 추상화된 상태라면, 트랜잭션 관리와 관련된 코드를 아예 작성하지 않거나 매우 조금만 참여하며, 어플리케이션 개발에 집중할 수 있다.

 

Understanding the Spring Framework Transaction Abstraction

그럼 스프링에서 어떻게 추상화 했는지 알아보자.

스프링에서는 the notion of a transaction strategy, 트랜잭션 전략이라는 개념이 들어간다. 

트랜잭션 전략은 TransactionManager, 그 중에서도 imperative(명령형) TransactionManager인 PlatformTransactionManager 와 reactive(반응형) transaction management인 ReactiveTransactionManager가 있다.

 

public interface PlatformTransactionManager {
   TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
   void commit(TransactionStatus status) throws TransactionException;
   void rollback(TransactionStatus status) throws TransactionException;
}

여기서 TransactionException은 모두 Unchecked Exception (RuntimeException을 상속)한 것이다.

트랜잭션 인프라의 장애는 거의 예외없이 치명적이고, 드물게 트랜잭션 실패를 App에서 복구한다면, TransactionException을 잡아서 처리할수도 있다. 즉, 개발자가 예외를 처리하도록 강요하지 않는다.

 

다시 말해, Spring에서는 Checked Exception에 대해서는 Rollback이 되지 않도록 Default로 설정되었다. (rollbackFor로 수정할 수 있다)

Checked Exception복구가 가능하다는 매커니즘을 갖고 있기 때문이다.

예를 들면, 파일이 없을 때는 Default이미지를 catch에서 선택하는 등의 복구전략이 있다는 것이 전재이므로, Rollback을 처리하지 않는게 Default이다.

 

getTransaction() 메서드는 TransactionDefinition 파라미터에 따라, TransactionStatus를 반환한다.

 

TransactionDefinition

public interface TransactionDefinition {
   int PROPAGATION_REQUIRED = 0;
   int PROPAGATION_SUPPORTS = 1;
   int PROPAGATION_MANDATORY = 2;
   int PROPAGATION_REQUIRES_NEW = 3;
   int PROPAGATION_NOT_SUPPORTED = 4;
   int PROPAGATION_NEVER = 5;
   int PROPAGATION_NESTED = 6;
   int ISOLATION_DEFAULT = -1;
   int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
   int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
   int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
   int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
   int TIMEOUT_DEFAULT = -1;
   int getPropagationBehavior();
   int getIsolationLevel();
   int getTimeout();
   boolean isReadOnly();
   @Nullable
   String getName();
}

많은 사항들이 있고, 주석으로 설명이 있지만, 간략하게 특수한 용어들만 먼저 정리해보자

Propagation(전파) : 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지를 지정
기본적으로 트랜잭션 범위 내에 있는 모든 코드는 해당 트랜잭션에서 실행된다. 단, 트랜잭션 컨텍스트가 이미 있는 상태에서 트랜잭션 메소드를 실행하는 경우엔 동작 방식을 지정할 수 있다. 예를 들어 기존 트랜잭션에서 코드를 계속 실행하거나 (일반적), 기존 트랜잭션을 일시 중단하고 새 트랜잭션을 만들 수 있다.
Isolation(고립) : 이 트랜잭션 작업과 얼마나 격리할 것인지를 나타내는 척도, 일관성없는 데이터 허용 수준 (격리 수준)
예를 들어, 현재 트랜잭션은 다른 트랜잭션에서 커밋하지 않는 쓰기를 볼 수 있는가?
TimeOut(타임 아웃) : 잭션의 실행 시간으로, 타임 아웃 시, Rollback
Read-Only(읽기 전용) : 데이터를 수정하지 않고, 읽기만 하는 상태

 

TransactionStatus

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    @Override
    boolean isNewTransaction(); // 새로운 트랜잭션 존재 여부
    boolean hasSavepoint(); // 현재 일치하는 트랜잭션이 존재하는가
    @Override
    void setRollbackOnly(); // rolbakc이 가능한가?
    @Override
    boolean isRollbackOnly(); //rollback이 되었는가?
    void flush(); // 실제 DB에 동기화
    @Override
    boolean isCompleted(); // 트랜잭션 완료 여부
}

TransactionStatus를 통해, 간단한 코드로 트랜잭션 실행을 제어하고, 상태를 확인할 수 있다.

Declarative Transaction Management

스프링 프레임워크 사용자 대부분이 Declarative Transaction Management(선언적 트랜잭션 관리)를 선택한다.

Application 코드에 미치는 영향이 가장 적기 때문에, 이상에 알맞다.

 

스프링에서 Declarative Transaction management는 AOP덕분에 가능한 방법이다.

트랜잭션 지원은 AOP Proxy를 통해 활성화되며, 트랜잭션 advice는 메타데이터(XML 혹은 Annotation)로 구동된다는 점이다. 트랜잭션 메타데이터를 가지고 만든 AOP Proxy는 TransactionInterceptor와 적당한 TransactionManager 구현체를 사용해 메소드 호출을 둘러싸고 트랜잭션을 실행한다.

 

Transactional Proxy

 

Using @Transactional

// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    Foo getFoo(String fooName) {
        // ...
    }

    Foo getFoo(String fooName, String barName) {
        // ...
    }

    void insertFoo(Foo foo) {
        // ...
    }

    void updateFoo(Foo foo) {
        // ...
    }
}

클래스 레벨에 사용하는 Annotation은, 선언하는 클래스(하위 클래스도)의 모든 메소드에 적용할 기본값을 나타낸다.

물론 메소드마다 개별로 Annotation을 달아도 된다. 다만, 클래스 계층 구조 상 위에 있는 클래스엔 적용되지 않으며, 상속된 메서드는 subclass 레벨의 Anntation에도 포함되려면 재정의(redeclared)되어야 한다.

 

이제 큰 2가지 사항에 대해 확인해볼 것이다.

  • public 메서드에만 적용해야 한다.
  • 같은 객체에 있는 다른 Method를 호출하면, 트랜잭션이 적용이 되지 않는다.

Transactional Proxy를 사용할 때는 public 메서드에만 적용해야 한다.

protected, private에 선언해도 에러는 아니지만, 정상적인 동작을 하지 않는다. (내부 메서드 호출과 동일한 이유)

만약 필요하다면, AspectJ를 사용하는 것이 좋다.

 

@Transactional Annotation인터페이스 정의나 인터페이스의 메서드, 클래스 정의, 클래스 public 메서드에 적용할 수 있다.

다만, 인터페이스 대신 구체적인 클래스(구체적인 클래스의 메서드)에 선언하기를 권장한다.

인터페이스에 적용할 경우, Interface-based Proxy를 사용할 때만 의도대로 동작한다.

만약, class-based Proxy를 사용하거나 weaving-based aspect를 사용한다면, 트랜잭션 세팅은 proxy와 weaving infrastructure에게 인지되지 않고, 객체는 transactional proxy에 감싸지지 않을 것이다.

 

위 설명을 다시 정리해보면, Spring AOP는 Proxy 기반으로 만들어져, Target Method가 호출되는 시점에 부가기능을 추가할 메소드를 자체적으로 판단하여, 부가기능을 주입한다. 즉, Runtime에 Weaving한다고 하여, Runtime Weaving이라고 한다.

 

이 Runtime Weaving 기능을 Spring 에서는 CGLib ProxyJDK Proxy를 상황에 따라 번갈아가며 사용한다.

SPring AOP Proxy

JDK Dynamic Proxy인터페이스를 구현하여 Proxy를 생성해주고, CGLib(Code Generator Library) Proxy클래스를 상속받아 Proxy를 생성한다.

그리고, Spring은 CGLib Proxy 를 기본으로 권장하는 것이다.

 

또 다른 한가지, 같은 객체에 있는 다른 Method를 호출하면, 트랜잭션이 적용이 되지 않는다.

이는 Proxy의 특징으로, Transaction에 의한 AOP Proxy를 예로 들면, Target Object 메서드 호출 전에, Transaction이 시작하고 호출 후에 commit or rollback된다.

즉, Proxy는 Client가 Target Object를 호출하는 과정에만 동작한다.

그 말은, Target Object에서, 자신의 다른 Method를 호출하면 프록시는 정상적으로 동작하지 않는다.

프록시를 통하지 않고 다른 Method가 호출이 되며, @Transactional Annotation이 있더라도 동작하지 않는다.

 

다음 예시를 보자.

출처: https://springsource.tistory.com/135 [Rednics Blog:티스토리]

@Transactional
public class MemberService {
  
  @Transactional(propagation=Propagation.REQUIRES_NEW)
  public void add(Member m) {...}
  
  public void complex() {
    ...
    this.add(new Member(...));
  }
  ...
}

클래스 전체에 Transactional이 걸려있고, 내부에 method에도 걸려있다.

이 때, Client로부터 complex()라는 게 호출된다면, 그전에 Proxy를 먼저 타게 될 것이고, Class에 적용된 Annotation에 따라 Proxy에서 트랜잭션이 시작된다.

문제는, 그 안에 있는 this.add 메서드의 실행이다.

이미 Proxy를 통과해서 들어온 상황이라, add 메서드는 트랜잭션 속성이 반영되지 못한다.

즉, add 메서드는 호출이 되었지만, 새로운 트랜잭션이 생성되는 것이 아니고, complex() 에서 시작된 트랜잭션에 그냥 참여될 뿐이다.

 

이에 대한 해결방법은 모드를 Proxy가 아닌 AspectJ 모드를 사용하는 방법이 있다. 이 방법은 프록시 없이 Weaving을 통해(바이트 코드를 수정해서) @Transactional 을 Target Class에 있는 모든 메소드의 런타임 동작으로 전환한다.

 

@Transactional Settings

 

Property Type Description
value String 사용할 트랜잭션을 지정하는 생략 가능한 한정자 (qualifier)
transactionManager String value의 Alias
label   Transaction에 표현적인 설명을 추가하기 위한 문자열 라벨의 배열
propagation enum: Propagation 생략 가능한 전파 설정
isolation enum: Isolation 생략 가능한 고립 수준.
propagation이 REQUIRED나 REQUIRES_NEW일 때만 적용할 것.
timeout int (in seconds of granularity) 생략 가능한 트랜잭션 타임 아웃.
propagation이 REQUIRED나 REQUIRES_NEW일 때만 적용할 것.
timeoutString String (in seconds of granularity) 타임 아웃을 String 값(EX> placeholder)으로 대신 사용할 수 있음.
readOnly boolean 읽기/쓰기 versus 읽기 전용 트랜잭션.
propagation이 REQUIRED나 REQUIRES_NEW일 때만 적용할 것.
rollbackFor Array of Class objects, which must be derived from Throwable. 롤백을 유발해야 하는 exception 클래스 배열.
생략 가능
rollbackForClassName Array of exception name patterns. 롤백을 유발해야 하는 exception 클래스 이름의 배열.
생략 가능
noRollbackFor Array of Class objects, which must be derived from Throwable. 롤백을 유발하지 않을 exception 클래스 배열.
생략 가능
noRollbackForClassName Array of exception name patterns. 롤백을 유발하지 않을 exception 클래스 이름의 배열.
생략 가능

propagation

앞서, Propagation(전파) 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지를 지정이라고 정리했다.

그 설정값들을 정리해보자.

 

REQUIRED : default 값. 현재 활성화된 트랜잭션이 존재하면, 해당 트랜잭션에 비즈니스 로직을 Append, 없다면 새로운 트랜잭션 생성

SUPPORTS : 활성화된 트랜잭션이 있으면 해당 트랜잭션을 사용, 없다면 트랜잭션 없이 실행

MANDATORY : 활성화된 트랜잭션이 있으면 해당 트랜잭션을 사용, 없다면 예외 발생

NEVER : 활성화된 트랜잭션이 존재하면 예외 발생, 없다면 트랜잭션을 사용하지 않음.

NOT_SUPPORTED : 활성화된 트랜잭션이 존재하면 보류, 비즈니스 로직을 트랜잭션 없이 실행.

REQUIRES_NEW : 항상 새로운 트랜잭션을 시작. 이미 진행중인 트랜잭션이 존재하면, 해당 트랜잭션을 잠시 보류.

NESTED : 활성화된 트랜잭션이 존재하면, 해당 트랜잭션의 세이브 포인트를 기록. 중첩 트랜잭션을 만들어 진행하는데, 비즈니스 로직에 예외가 발생하면 세이브 포인트로 롤백.

isolation

isolation(고립)은 ACID의 한 요소로, 이 트랜잭션 작업과 얼마나 격리할 것인지를 나타내는 척도, 일관성없는 데이터 허용 수준 (격리 수준)이라고 정리했다.

이 때, 그 정도에 따라 발생할 수 있는 동시성의(concurrency) side effects로, 3가지가 존재한다.

 

Dirty Read

 A 트랜잭션에서 완전히 반영되지 않은 데이터를 B 트랜잭션이 읽을 수 있다.

만약, A 트랜잭션이 롤백되면, 데이터 불일치가 나온다.

 

Non-repeatable Read

A 트랜잭션이 조회중인 정보를 트랜잭션 B에서 수정하고 커밋하면, A 트랜잭션 내에서 재조회했을 때, 수정된 데이터가 조회된다 (이전 데이터는 다시 조회할 수 없다). 즉, 복해서 같은 데이터를 읽을 수 없는 경우 (트랜잭션 내에서 결과가 일관성을 가지지 못한다)

 

Phantom Read

외부에서 수행되는 삽입/삭제로 인해, 트랜잭션 내에서의 동일한 쿼리가 다른 값을 반환

 

위 Side Effect로 인해, 격리 수준을 4단계로 제공하고, 그 설정값이 다음과 같다.

 

DEFAULT : 기본 격리 수준 (DB 설정에 종속한다.)

READ_UNCOMMITED(Level 0) : 커밋되지 않는 데이터에 대한 읽기를 허용한다.

- Dirty Read가 발생할 수 있다.

READ_COMMITED(Level 1) : 커밋된 데이터에 대해서만 읽기 허용한다. (데이터 변경 중에는 접근 불가)

- Dirty Read 방지

REPEATEABLE_READ(Level 2) : 동일 필드에 대해 다중 접근 시 모두 동일한 결과를 보장

- 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 Shared Lock이 걸리므로 다른 사용자는 그 영역에 해당하는 데이터에 대한 수정이 불가

- 선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신하거나 삭제가 불가능하기 때문에 같은 데이터를 두번 쿼리했을 때, 일관성을 가진다.

- Non-Repeatable Read 방지

SERIALIZABLE(Level 3) : 가장 높은 격리, 성능 저하의 우려가 있음

- 데이터의 일관성 및 동시성을 위해 MVCC(Multi Version Concurrency Control)을 사용하지 않음.

- 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정 및 입력이 불가능

- Phantom Read 방지

MVCC : 다중 사용자 데이터베이스 성능을 위한 기술로 데이터 조회 시, LOCK을 사용하지 않고 데이터의 버전을 관리해 데이터의 일관성 및 동시성을 높이는 기술)

따라서, 격리 수준에 따라 문제점 발생 도표는 다음과 같다.

 

Isolation Level Dirty Read Non-Repeabtable Read Phantom Read
Read Uncommitted O O O
Read Committed - O O
Repeatable Read - - O
Serializable - - -

이제 Annotation을 보자.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

   @AliasFor("transactionManager")
   String value() default "";

   @AliasFor("value")
   String transactionManager() default "";

   Propagation propagation() default Propagation.REQUIRED;

   Isolation isolation() default Isolation.DEFAULT;

   int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

   boolean readOnly() default false;

   Class<? extends Throwable>[] rollbackFor() default {};

   String[] rollbackForClassName() default {};

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

   String[] noRollbackForClassName() default {};

}

 

대부분의 설정값을 이해할 수 있을 것이며, 이제 최종적으로 정리해보자.

 

Wrap Up

아주 길게 Transactional에 대해 정리해보았다.

생각보다 양이 굉장히 많아서 놀랬다.

 

우선 하나씩 다시 정리해보자.

 

Transaction상호작용의 단위이며, ACID를 보장하는 한 변화의 단위이다.

ACID는 Atomic, Consistency, Isolation, Durability 이며, 원자성, 일관성, 격리, 지속성을 말한다.

Spring에서 Transaction은 추상화를 통해, Global Transaction과 Local Transaction으로 나누어 처리하던 로직을 간소화하였다.

선언적 Transaction Management에서 PlatformTransactionManager 이라는 인터페이스를 Transaction 전략으로 사용하였다.

이 때, getTransaction, commit, rollback 3개의 메서드를 가지고 있으며, Exception은 TransactionException으로 unchecked Exception을 관리한다.

getTransaction() 메서드는 트랜잭션을 시작하는 것과 동일하고, TransactionDefinition 파라미터에 따라, 간단한 코드로 트랜잭션 실행을 제어하고, 상태를 확인할 수 있도록 TransactionStatus를 반환한다.

Spring에서 Declarative Transaction management는 AOP Proxy로 관리된다. Proxy 패턴으로 Target 전에 Transaction이 실행된다.

@Transactional Annotation은 클래스나 인터페이스 혹은 그 메서드에 선언될 수 있으나, 인터페이스보다는 클래스나 그 메서드에 선언되는 것을 권장한다.

 JDK Dynamic Proxy의 경우, Interface 기반의 Transaction 설정으로 되었을 때 동작하지만, 클래스나 그 메서드에 선언되어 있을 때는 동작하지 않는다.

따라서, CGLib Proxy를 통해, Runtime Weaving 될 때, 바이트 코드를 만들어 버리는 형태로 진행해야 클래스나 그 메서드에서 정상적으로 동작할 수 있다.

@Transactionalpublic 메서드에만 적용해야 하며, 같은 객체에 있는 다른 Method를 호출하면, 트랜잭션이 적용이 되지 않는다.

Proxy로 적용되는 흐름 상, 내부 메서드 호출은 Proxy를 타지 않기 때문이며, 외부 노출이 안되면 실행이 안되기 떄문이다.

@Transactional에는 여러 설정이 있고, propagation, Isolation, timeout, RollbackFor 등이 있다.

propagation은 한 트랜잭션에서 다른 트랜잭션이 실행될 때, 어떻게 전파되는지를 설정한다.

Default로 REQUIRED 이며 이미 진행되는 트랜잭션이 있으면, 해당 트랜잭션에 소속되며 아니면 새롭게 생성된다.

isolation은 ACID에도 포함되는 격리성이다.

격리 레벨에 따라 Side Effect로 3가지, Dirty Read, Non-repeatable Read, phantom Read 가 나올 수 있다.

격리 레벨은 총 4개 (default 포함 5개)로 구분되며 각각 Side Effect가 나올 수 있는지 여부가 판단된다.

DEFAULT, READ_UNCOMMITED, READ_COMMITED, REPEATABLE_READ, SEARIALIZABLE 이다.

 

그럼 나는 @Transactional을 잘 쓰고 있었을까?

용도는 맞았을 지도 모르겠다. 그러나 그냥 남들 쓰는 걸 보고 썼다.

@Transactional(value="transactionManager", propagation = Propagation.REQUIRED, rollbackFor = Exception.class)

거의 대부분의 코드에 쓰인 내용이다.

rollbackFor 정도를 제외하면 설정이 없어도 될 정도다.

rollbackFor에 Exception.class를 적용한 이유는 아마, 반드시 처리되어야 할 Checked Exception이 발생했을 때, 이를 unChecked Exception으로 바꾸어 던지기보다, 간편했기 때문일 것이다.

쉽지 않은 작업이지만, 구체적으로 Throw를 날릴 수 있도록 연습해야할 것이다.