본문 바로가기
우아한테크코스/Level3

저는 트랜잭션이 처음이라니까요?

by 조엘 2021. 8. 17.

안녕하세요! 조엘입니다!

 

"처음이라니까요" 시리즈 열 번째 토픽은 트랜잭션입니다. 🏆🏆

데이터를 영속화시키는 과정에서 트랜잭션이라는 용어는 빠지지 않고 나오는데요!

이게 대체 무엇인지, 이게 왜 중요한지에 대해서 알아보도록 해요 💪💪

 

 

*** 트랜잭션이란? ***

트랜잭션의 정의부터 알아보아요. 위키피디아에서 정의하는 트랜잭션은 다음과 같아요. 

 

A database transaction symbolizes a unit of work performed within a database management system (or similar system) against a database, and treated in a coherent and reliable way independent of other transactions. A transaction generally represents any change in a database. Transactions in a database environment have two main purposes:

 

  1. To provide reliable units of work that allow correct recovery from failures and keep a database consistent even in cases of system failure, when execution stops (completely or partially) and many operations upon a database remain uncompleted, with unclear status.

  2. To provide isolation between programs accessing a database concurrently. If this isolation is not provided, the programs' outcomes are possibly erroneous.

 

데이터베이스 트랜잭션은 일관성 있고 신뢰할 수 있는 방식으로 독립적이게 처리되는 작업 단위를 뜻한다.

데이터베이스 트랜잭션은 데이터베이스의 변화를 나타낸다. 데이터베이스 트랜잭션은 크게 두 가지의 목적을 가진다. 

 

1. 예상치 못한 에러가 발생해도 데이터베이스를 신뢰성 있는 상태로 만들 수 있도록 신뢰할 수 있는 작업 단위를 제공한다.

2. 데이터베이스에 동시에 접근하는 경우 프로그램 간에 격리를 제공하여 에러를 방지한다.

 

해당 정의에서 작업 단위하나의 로직을 뜻해요. 

하나의 로직은 하나의 연산 혹은 여러 개의 연산으로 이루어질 수 있어요. 

예시로 A가 B에게 10000원을 송금하는 과정은 다음과 같아요. 

  1. A가 10000원 이상이 있는지 확인한다.

  2. A의 계좌에서 10000원을 차감한다.

  3. B의 계좌에 10000원을 증가시킨다. 

 

작업 단위3가지 연산을 모두 포함해야 한다는 것을 알 수 있어요. 

사실 어느 연산까지 작업 단위로 묶을지는 개발자의 선택에 달려있어요. 

트랜잭션의 성질을 살펴보면, 어디까지를 작업 단위로 묶을 것인가에 대한 기준을 세울 수 있어요. 🎯🎯

 

 

*** 트랜잭션의 성질 ***

트랜잭션의 성질은 ACID로 대표되어요. 

Atomicity, Consistency, Isolation, Durability의 줄임말인데요. 하나씩 알아보아요. 

 

[ Atomicity - 원자성 ]

- 트랜잭션 내의 모든 연산은 완벽하게 수행되어야 한다.

- 어느 하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야 한다. 

- 따라서 하나의 트랜잭션 내의 연산들은 모두 반영되거나, 전혀 반영되지 않아야 한다.

- 모두 반영되는 것을 COMMIT, 반영되기 전의 상태로 돌아가는 것을 ROLLBACK이라 지칭한다. 

 

[ Consistency - 일관성 ]

- 트랜잭션은 허용된 규칙 안에서만 데이터를 변경할 수 있다. 

- PK/FK 등의 제약 조건, CASCADE, 트리거 등의 규칙들을 만족시켜야 한다. 

 

[ Isolation - 독립성 ]

- 트랜잭션 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장한다. 

- 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없다. 

- 트랜잭션 내의 연산들은 연속적으로 실행되어야 함을 의미한다.

- 아래 Isolation Level 파트에서 더 설명 예정 

 

[ Durability - 지속성 ] 

- 성공적으로 완료된 트랜잭션은 시스템 고장이 나더라도 영구적으로 반영되어야 한다. 

 

이제 ACID를 염두에 두고 작업 단위를 다음과 같이 정하는 것이 좋아요!

- 원자성: 하나의 로직 내에서 변화가 필요한 모든 연산들을 하나의 트랜잭션으로 묶기

- 일관성: 데이터베이스에 정의한 규칙을 준수하기

- 독립성: 해당 작업을 어느 수준까지 독립시킬 것인지 결정하기

- 지속성: 트랜잭션의 기록을 비휘발성 메모리에 저장하는 것으로, DB 벤더에서 신경 쓸 부분

 

 

*** 스프링에서의 트랜잭션 ***

스프링에서 트랜잭션을 사용하는 방법으로 가장 익숙한 것은 "@Transactional"을 활용하는 것인데요!

이를 AOP를 활용한 선언적 트랜잭션 방식이라고 불러요. 

핵심 비즈니스 로직에 영향을 주지 않으면서 트랜잭션 기능을 활용할 수 있어요. 

 

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

    @AliasFor("transactionManager")
    String value() default ""; // 빈으로 등록된 TransactionManager 지정

    @AliasFor("value")
    String transactionManager() default ""; // 빈으로 등록된 TransactionManager 지정
	
    String[] label() default {}; // 트랜잭션 설명 라벨
	
    Propagation propagation() default Propagation.REQUIRED; // 트랜잭션 전파 설정
	
    Isolation isolation() default Isolation.DEFAULT; // 트랜잭션 고립 수준
	
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; // 초 단위로 시간제한
	
    String timeoutString() default ""; // 초 단위로 시간제한
	
    boolean readOnly() default false; // 해당 트랜잭션에서 READ 연산만 허용
	
    Class<? extends Throwable>[] rollbackFor() default {}; // 해당 예외 발생 시 롤백
	
    String[] rollbackForClassName() default {}; // 해당 예외 발생 시 롤백
	
    Class<? extends Throwable>[] noRollbackFor() default {}; // 해당 예외 발생 시 롤백 X
	
    String[] noRollbackForClassName() default {}; // 해당 예외 발생 시 롤백 X
}

 

Transactional 어노테이션은 위와 같은 속성 값들을 지정할 수 있어요. 

이 중 전파 설정(propagation)고립 수준(isolation)이 뭔지 잘 모르겠는데요! 이를 조금 더 공부해봅시다! 👀👀

 

 

*** 트랜잭션의 전파 설정 ***

트랜잭션의 전파 설정은 진행 중인 트랜잭션에서 다른 트랜잭션이 호출될 때 어떻게 처리할지를 정하는 것이에요. 

다음과 같은 설정 옵션들 중에서 지정할 수 있어요. 

상황에 따라 알맞은 트랜잭션 전파 설정으로 지정해서 사용하면 유용할 것이에요. 💦💦

 

[ REQUIRED (기본값) ] 

- 부모 트랜잭션이 존재한다면 부모 트랜잭션으로 합류

- 부모 트랜잭션이 없다면 새로운 트랜잭션 생성

- 하나의 트랜잭션으로 묶이기 때문에 롤백 시 모든 진행 상황 롤백됨

 

[ REQUIRES_NEW ]

- 무조건 새로운 트랜잭션 생성

- 각각의 트랜잭션이 롤백되더라도 서로 영향 X

 

[ MANDATORY ]

- 부모 트랜잭션에 합류함

- 부모 트랜잭션 없다면 예외 발생

 

[ NESTED ]

- 부모 트랜잭션이 존재한다면 중첩 트랜잭션 생성

- 부모 트랜잭션이 없다면 새로운 트랜잭션 생성

- 중첩된 트랜잭션 내부에서 롤백 발생 시 중첩 트랜잭션의 시작 지점까지만 롤백

- 중첩 트랜잭션은 부모 트랜잭션이 커밋될 때 함께 커밋

 

[ NEVER ]

- 트랜잭션 생성하지 않음

- 부모 트랜잭션 존재 시 예외 발생

 

[ SUPPORTS ] 

- 부모 트랜잭션이 있다면 합류

- 부모 트랜잭션이 없다면 트랜잭션을 생성하지 않음

 

[ NOT_SUPPORTED ] 

- 부모 트랜잭션이 있다면 보류시킴

- 부모 트랜잭션이 없다면 트랜잭션을 생성하지 않음

 

 

*** 트랜잭션의 고립 수준 ***

사실 "동시에 많은 DB 요청을 처리하는 것""트랜잭션의 독립성을 보장하는 것"은 공생하기 힘들어요. 

트랜잭션 1이 데이터 A에 작업을 하는 도중에, 트랜잭션 2가 데이터 A에 접근하면 어떻게 해야 할까요? 

트랜잭션 1이 모두 완료되고 트랜잭션 2가 처리되는 것이 안전하겠지만, 이는 성능에 문제가 생길 것이에요.

병렬적으로 요청을 처리하지 못하고, 한 번에 하나씩만 처리하니까요. 

 

결국 우리는 성능독립성 사이에서 합의를 봐야 해요. 

 

트랜잭션의 고립 수준은 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 고립되어 있는지를 정의해요. 

다음과 같은 고립 수준들 중에서 지정할 수 있어요. 

 

[ DEFAULT (기본값)

- DB 벤더의 Isolation Level 따름

    - Oracle: READ_COMMITTED

    - MySQL: REPEATABLE_READ

 

[ READ_UNCOMMITTED ]

- 트랜잭션 1이 커밋하지 않은 데이터에 대해, 트랜잭션 2가 읽는 것을 허용

 

[ READ_COMMITTED ]

- 트랜잭션 1이 커밋한 데이터에 대해, 트랜잭션 2가 읽는 것을 허용

 

[ REPEATABLE_READ ]

- 트랜잭션 1이 읽은 데이터에 대해, 트랜잭션 1이 종료될 때까지, 다른 트랜잭션에서 수정/삭제 허용하지 않음

    - 다른 트랜잭션에서 삽입은 허용

 

[ SERIALIZABLE ]

- 트랜잭션 1이 읽은 데이터에 대해, 트랜잭션 1이 종료될 때까지, 다른 트랜잭션에서 수정/삭제/삽입 허용하지 않음

 

각각의 고립 수준 별로 발생할 수 있는 문제 상황들이 있는데,

이는 이미 설명을 잘해주신 아래 참조의 블로그 링크로 설명을 대신할게요!

읽어보시는 것을 추천합니다! 📜📜 

 

여기까지 읽어주셔서 감사합니다!! 다음에 또 만나용 👍👍

 

 

참고

- 샐리의 트랜잭션 테코톡: https://www.youtube.com/watch?v=aX9c7z9l_u8

- 에이든의 트랜잭션 메커니즘 테코톡: https://www.youtube.com/watch?v=ImvYNlF_saE 

- 전파 설정: https://deveric.tistory.com/86

- 고립 수준 1: https://it-license.tistory.com/25

- 고립 수준 2: https://joont92.github.io/db/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-isolation-level/

- 위키피디아: https://en.wikipedia.org/wiki/Database_transaction

 

 

 

반응형

댓글6