Spring/스프링 이론

[Spring] 트랜잭션과 @Transactional 이해하기

블로그 주인장 2023. 10. 24.

트랜잭션과 @Transactional 

트랜잭션과 스프링에서 사용하는 @Transactional 어노테이션에 관련하여 알아보겠습니다.


트랜잭션이란?

  • 데이터베이스의 상태를 변경하는 작업 또는 한번에 수행되어야하는 연산들을 의미한다.
  • 즉, 병행 제어 시 처리되는 작업의 논리적단위입니다.

트랜잭션의 연산

Transaction은 하나의 흐름으로 하나의 실행이 성공하거나, 실패하면 모든 연산을 동일하게 처리한다.

ex) A,B,C의 연산을 한 묶음이라고 할때, A는 정상 작동되었지만, B가 실패한다면 A의 작업 이력도 이전으로 돌린다.

 

  • 커밋 : 하나의 트랜잭션이 성공적으로 끝났으며, 데이터베이스가 일관성 있는 상태로 유지될 때, 트랜잭션이 마무리되었다는 것을 트랜잭션 관리자에게 알리기 위한 연산이다.
  • 롤백 : 트랜잭션 처리가 비정상적으로 종료된 경우, 트랜잭션을 다시 시작하거나, 트랜잭션의 부분적으로 연산한 결과를 취소시킨다.

트랜잭션의 특징

  • 원자성(Atomicity)
    1. 트랜잭션의 연산은 데이터베이스에 모두 반영되던지 아니면 모두 반영되지 않아야한다.
    2. 트랜잭션 내의 모든 명령은 반드시 완벽히 수행되어야하며, 하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야한다.
  • 일관성(Consistency)
    1. 트랜잭션을 성공적으로 완료하면 일관성 있는 데이터베이스 상태로 변환한다.
    2. 시스템이 가지고 있는 고정요소는 트랜잭션 수행 전과 트랜잭션 수행 완료 후의 상태가 같아야한다.
  • 독립성(Isolation)
    1. 둘 이상의 트랜잭션이 동시에 실행되는 경우 다른 트랜잭션의 연산이 끼어들 수 없다.
    2. 수행 중인 트랜잭션은 완전히 완료될 때까지, 다른 트랜잭션에서 수행 결과를 참조할 수 없다.
  • 지속성(Durability)
    1. 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야한다.

스프링에서 트랜잭션 적용

스프링에서는 어노테이션 방식으로 트랜잭션 처리를 지원한다. 

 

@Transactional을 선언하여 사용한다. 클래스 또는 메서드 위에 @Transactional을 붙이면, 트랜잭션 기능이 적용된 프록시 객체가 생성되며, 트랜잭션 성공 여부에 따라 Commit 또는 Rollback 작업이 이루어진다.

 

또 다른 방법으로는 트랜잭션 스크립트 방식인데, 하나의 트랜잭션 안에서 동작해야하는 코드를 한 군데 모아서 만드는 방식이다. 이러한 방식 때문에 보통 트랜잭션마다 하나의 메서드로 구성된다.


PlatformTransactionManager

스프링은 트랜잭션 추상화를 반영하였는데, 특정 기술에 종속되지 않는 일관된 방식으로 트랜잭션을 적용할 수 있다.

그 중, 스프링 트랜잭션 추상화의 핵심 인터페이스는 PlatformTransactionManger이다.

모든 스프링의 트랜잭션 기능과 코드는 이 인터페이스를 통해서 로우레벨의 트랜잭션 서비스를 이용할 수 있다.

트랜잭션을 관리할 매니저 객체를 추상형을 제작해두었기 때문에,

사용할 데이터베이스 및 데이터 엑세스 기술이 달라도 일관되게 형식으로 적용할 수 있다.

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager extends TransactionManager {

	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;

	void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;

}
  • getTransaction()
    1. 트랜잭션을 시작한다는 의미
    2. 트랜잭션 속성에 따라서 새로 시작하거나, 진행 중인 트랜잭션에 참여하거나, 진행중인 트랜잭션을 무시하고 새로운 트랜잭션을 만드는 식으로 상황에 따라 다르게 동작한다.
  • TransactionDefinition
    1. 트랜잭션 속성을 나타내는 인터페이스
    2. 현재 참여하고 있는 트랜잭션의 ID와 구분 정보를 담고 있다.
    3. 커밋 또는 롤백 시에 사용한다.

트랜잭션 매니저의 종류

  • DataSourceTransactionManager : Connection의 트랜잭션 API를 이용해서 트랜잭션을 관리해주는 트랜잭션 매니저
  • JpaTransactionManager : JPA를 이용하는 DAO에는 JpaTransactionManager를 사용한다. DataSourceTransactionManager가 제공하는 DataSource 레벨의 트랜잭션 관리 기능을 동시에 제공한다.
  • JmsTransactionManager, CciTransactionManager : 스프링에서는 DB 뿐만 아니라 트랜잭션이 지원되는 JMS와 CCI를 위해서도 트랜잭션 매니저를 제공한다.
  • JtaTransactionManager : 하나 이상의 DB 또는 트랜잭션 리소스가 참여하는 글로벌 트랜잭션을 적용하려면 JTA를 이용해야한다.

🔊 Tip

DB가 하나라면 트랜잭션 매니저 또한 하나만 등록되어야한다.

여러 개라도, JTA를 이용한 글로벌 트랜잭션을 적용할 것이라면 JtaTransactionManager 하나만 등록되어야한다.

독립된 2개 이상의 DB라면 DataSource도 2개가 등록되므로, DB 수에 따른 트랜잭션 매니저를 등록할 수 있다.


Transaction 경계 설정

  • 트랜잭션 경계를 지정한다는 의미는 트랜잭션이 어디서 시작하고 종료하는지 결정하는 것을 의미한다.
  • 그에 따라 종료 시에 정상 종료(commit)이나 비정상 종료(rollback)인지 결정한다.
  • 스프링에서는 선언적 트랜잭션 설정방식으로 유연하고 편리하게 설정한다.

 

선언적 Transaction

선언적 Transaction 을 이용하면 코드에는 전혀 영향을 주지 않으며,

특정 메소드 실행 전 후에 트랜잭션이 시작되고 종료되거나 기존 트랜잭션에 참여할 수 있도록 만들 수 있다.

선언적 Transaction은 선언형 설정 방식으로, Transaction Template과 달리 트랜잭션 처리를 코드에서 직접 수행하지 않는다.

 

Transaction Template(Transaction Script)

  • 하나의 트랜잭션 안에서 동작해야 하는 코드를 한 군데에서 모아서 만드는 방식
  • 보통 '1개의 트랜잭션 = 1개의 메소드' 로 구성이 되는데 아래와 같은 형식을 가진다.
(메소드의 앞부분) DB 연결
        ⬇
트랜잭션 시작 코드
        ⬇
(트랜잭션 내) DB를 액세스하는 코드
혹은 그 결과를 가지고 비즈니스 로직을 적용하는 코드

@Aspect

애플리케이션 기능은 크게 핵심 기능과 부가 기능으로 나눌 수 있다.

  • 핵심 기능 : 해당 객체가 제공하는 고유한 기능(ex: accountService - 계좌 로직)
  • 부가 기능 : 핵심 기능을 보조하는 기능(ex : 로그 추적, 트랜잭션 처리 등)

하나의 부가 기능은 여러 곳에서 동일하게 사용되는 경우가 많다.

이러한 부가기능을 한 번에 관리하기 위해서, Aspect를 사용한다.

 

Aspect : 부가 기능을 분리하고, 이 기능을 어디에 적용할지 선택하는 기능을 합쳐놓은 하나의 모듈로 만든 것이다.

우리말로는 '관점' 이라는 뜻인데, 애플리케이션을 바라볼 때 핵심 기능만 바라볼 수 있도록 도와준다.


@Transactional

트랜잭션이 적용될 인터페이스나 클래스, 메소드 등에 @Transactional 어노테이션을 부여한다.

트랜잭션 대상으로 지정하고 트랜잭션 속성을 제공한다.

@Target(value={TYPE,METHOD})
@Retention(value=RUNTIME)
@Inherited
@Documented
public @interface Transactional {
	//...
}

 @Transactional 어노테이션의 Target은 TYPEMETHOD 이다.

class, interface, enum 등의 (TYPE)이나, Method(METHOD)에 적용할 수 있다는 의미이다.

 

@Transactional 동작 구조

스프링의 트랜잭션 AOP는 @Transactional을 인식하여 트랜잭션 프록시를 적용한다.

프록시는 '대리인' 이라는 뜻인데, Aspect와 @Transactional을 적용한 클래스나 메서드인 Target을 연결해 주는 역할을 한다.

  1. Client가 API 호출
  2. 프록시 실행
  3. 트랜잭션 코드 실행
  4. 비즈니스 로직 실행
  5. 트랜잭션 코드 실행(commit /  rollback)


Transaction 경계 설정

트랜잭션의 경계를 설정할 때는 Propagation, Isolation, Timeout, ReadOnly 등 추가 속성을 지정할 수 있다.


Propagation

트랜잭션을 시작하거나, 기존 트랜잭션에 참여하는 방법을 결정하는 속성이다.

 

✔️ REQUIRED

Default 속성이며, 모든 트랜잭션 매니저가 지원한다. 보통 이 속성으로 많이 사용한다. 미리 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션을 생성한다. 간단한 트랜잭션 전파 방식이지만 사용해보면 매우 강력하고 유용한 속성이라는 사실을 알 수 있다.

 

✔️ SUPPORTS

이미 시작된 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 진행한다. 트랜잭션이 없긴 하지만 해당 경계 안에서 Connection이나 Hibernate Session 등을 공유할 수 있다.

 

✔️ MANDATORY

REQUIRED와 비슷하며, 이미 시작된 트랜잭션이 있으면 참여한다. 하지만 트랜잭션이 없다면 생성하는 것이 아니라 예외를 발생시킨다. 독립적으로 트랜잭션을 실행하면 안되는 경우에 사용한다.

 

✔️ REQUIRES_NEW

항상 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있다면, 트랜잭션을 보류시킨다.

 

✔️ NOT_SUPPORTED

트랜잭션을 사용하지 않게 한다. 이미 진행 중인 트랜잭션이 있으면 보류시킨다.

 

✔️ NEVER

트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션도 존재하면 안되며, 트랜잭션이 있다면 예외를 발생시킨다.

 

✔️ NESTED

이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션을 시작하고, 부모 트랜잭션에 영향을 주지 않는다.

중첩 트랜잭션은 말 그대로 트랜잭션 안에 트랜잭션을 만드는 것이다. 독립적인 트랜잭션을 만드는 REQUIRES_NEW와는 다르다.

 

중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만, 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.

// AOP - Advice 등록
DefaultTransactionAttribute defaultAttribute = new DefaultTransactionAttribute();
defaultAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

// @Transactional
@Transactional(propagation = Propagation.PROPAGATION_REQUIRED)

Isolation

트랜잭션 격리 수준은 동시에 여러 트랜잭션이 진행될 때에 트랜잭션의 작업 결과를 다른 트랜잭션에게 어떻게 노출할 것인지를 결정하는 기준이다.

 

✔️ DEFAULT

사용하는 데이터 엑세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다.

대부분의 DB는 READ_COMMITTED를 기본 격리 수준으로 갖지만, 데이터 엑세스 기술이나 DB마다 다를 수 있기 때문에 DB나 드라이버 문서를 살펴볼 필요가 있다.

 

✔️ READ_UNCOMMITTED (Level 0)

: 커밋되지 않는 데이터에 대한 읽기 허용

Dirty Read가 발생 가능

가장 낮은 격리 수준이다. 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제가 있다.

하지만 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용할 수 있다.

 

✔️ READ_COMMITTED (level 1)

: 커밋된 데이터에 대한 읽기 허용

-Dirty Read 방지
가장 많이 사용되는 격리 수준으로, 커밋되지 않은 정보는 읽을 수 없습니다.

대신 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있습니다.

따라서, 트랜잭션이 같은 로우를 다시 읽었을 때 다른 내용일 수 있습니다.

 

✔️ REPEATABLE_READ (level 2)

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

Non-Repeatable Read 방지
하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막지만, 새로운 로우를 추가하는 것은 제한하지 않습니다.

따라서 SELECT로 조건에 맞는 로우를 전부 가져오는 경우, 트랜잭션이 끝나기 전에 새로 추가된 로우가 발견될 수 있습니다.

 

✔️ SERIALIZABLE (level 3)

읽기 작업에도 공유 잠금을 설정하게 되고, 다른 트랜잭션에서 변경하지 못함

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

가장 안전한 격리수준이지만 가장 성능이 떨어지기 때문에 극단적으로 안전한 작업이 필요한 경우가 아니라면 자주 사용되지 않습니다.


Timeout

트랜잭션 격리 수준은 동시에 여러 트랜잭션이 진행될 때에 트랜잭션의 작업 결과를 다른 트랜잭션에게 어떻게 노출할 것인지를 결정하는 기준이다.


ReadOnly

트랜잭션을 읽기 전용으로 설정할 수 있다.

성능을 최적화하기 위해 사용할 수도 있고, 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용할 수 도 있다.

 

하지만 일부 트랜잭션 매니저의 경우 읽기 전용 속성을 무시할 수도 있으니 주의해야 합니다. 일반적으로 INSERT, UPDATE, DELETE 같은 쓰기 작업이 진행되면 예외가 발생합니다.


RollbackFor

선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백한다. 반면에 예외가 전혀 발생하지 않거나 체크 예외가 발생하면 커밋한다. 체크 예외를 커밋 대상으로 삼은 이유는 체크 예외가 예외적인 상황에서 사용되기보다는 리턴 값을 대신해서 비즈니스적인 의미를 담은 결과를 돌려주는 용도로 많이 사용되기 때문이다.

@Transactional(propagation = Propagation.NESTED, rollbackFor = NotFoundAuthIdException.class)
    public void save(String message, String token) {
        String authority = jwtUtil.extractAuthorityFromToken(token);
        ...
}

noRollbackFor

rollbackFor 속성과는 반대로 기본적으로 롤백 대상인 런타임 예외를 트랜잭션 커밋 대상으로 지정한다. 사용 방법은 위와 동일하다.

 

반응형

댓글