[DB] Transaction 기초 개념 & Spring @Transactional 어노테이션

한호성·2024년 5월 18일
0

성능측정

목록 보기
5/7

Introduction

어플리케이션에서, 동시성 이슈를 막기 위해 DB Lock을 활용 해야 할 일이 있었습니다.
이 때, Spring에서 사용하는 @Transcational 이 적용된 코드를 개선하여 성능향상을 고려하였습니다.
기본 개념을 재 정립한 후, 성능을 향상 시킬 부분을 고민 하기로 하였습니다.
따라서 이 글은 Transcational 의 기본 개념과 Spring AOP를 활용한 @Transcational에 대한 글입니다.

Transactional 기본 개념

Transaction의 컴퓨터 과학분야에서의 의미는 “더 이상 분할이 불가능한 업무처리의 단위”를 의미합니다

  • ex) 많은 예시들이 은행의 일련의 과정을 듭니다.
    돈의 입금 및 출금의 과정은 한 단위로 생각해야 상식적으로 말이 됩니다.
    ( 누군가는 돈이 빠져나가고, 누군가는 돈을 받지 않는다면, 데이터 정합성의 문제가 있겠지요?)
    그렇기 때문에 돈의 입금,출금은 트랜잭션의 한 단위로 생각해야합니다.

간단하게 제가 자주 사용하는 postgreSQL에서 DB에서의 트랜잭션을 보겠습니다.

BEGIN TRANSACTION;

##작업1
select * from tb_order;

##작업2
update tb_order set client_name = 'ggg' where tb_order.id = 1;

COMMIT

위의 예시처럼 트랜잭션을 이용하여서 여러 작업을 모아서 하나의 단위로 보는 것이 트랜잭션의 개념입니다.

트랜잭셩 특징

트랜잭션의 특징으로 대게 ACID라고 불리는데, 조금 더 구체적으로 특징에 대해 알아보도록 하겠습니다.

  • 원자성 (Atomicity)
    • 트랜잭션이 데이터베이스에 모두 반영하던가, 모두 반영되지 않아야 한다.
    • 작업의 최소 단위라는 말이 위와같은 의미를 반영한 것이다.
      • 이것이 중요한 이유는 어플리케이션의 오류가 생겼을 때를 DB관점에서 생각해보면 좋다.
  • 일관성 (Consistency)
    • 트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다.
    • 트랜잭션이 진행되는 동안 DB의 변경점이 있더라도, 처음 트랜잭션이 시작할 때의 DB로 진행된다는 의미앋.
  • 독립성 (Isolation)
    • 독립성은 둘 이상의 트랜잭션이 동시에 실행되고 있을 경우, 다른 하나의 트랜잭션이 다른 트랜잭션의 연산에 끼어들 수 없다는 것을 의미한다.
  • 영구성 (Durability)
    • 트랜잭션이 성공적으로 완료됬을 경우, 결과는 영구적으로 반영되어야 한다는 점이다.

#cf) 트랜잭션의 롤백 명령의 경우, DDL문은 처리 대상이 아닙니다.

Transcational 격리 수준

격리수준은 동시에 여러 트랜잭션이 처리 될 때, 트랜잭션끼리 얼마나 격리되어 있는지를 나타내는 것을 의미한다.

즉 트랜잭션이 다른 트랜잭션에 변경한 데이터를 어느범위까지 볼 수 있게할지를 결정하는 것을 의미한다.

총 4가지의 격리수준이 있다.

  • READ UNCOMMITED
    • 어떤 트랜잭션의 변경내용이 commit 되기전에 이미 반영되서 보인다. ( 이렇게 사용할 일이 있을까..?)
  • READ COMMITED
    • 어떤 트랜잭션의 변경 내용이 commit 되어야 변경점을 다른 트랜잭션이 조회 가능하다.
      • 이 부분에도, 여러 db 동시에 데이터를 가져가서 update 치게 될 경우 정합성 이슈가 발생할 수 있다.
      • (Phantom read 발생: 같은 쿼리문에서도 다른 트랜잭션 commit된 내용에 의해 다른 데이터를 갖고 있음)
  • REPETABLE READ
    • 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준이다.
    • (Phantom Read 불허)
  • SERIALIZABLE
    • 가장 단순하고 엄격한 격리 수준, 한 트랜잭션이 작업할 때 사용한 모든 데이터들에 대해, 공유 잠금이 설정된다.
    • 그냥 순차적으로 작업이 실행된다고 보면된다. ( 성능저하가 당연히 예상된다)

위에서 아래로 내려갈수록 트랜잭션 간 고립 정도가 높아지며 성능이 떨어진다.

( trade off 관계라 생각하면 된다.)

내가 사용하는 PostgreSQL 기본값을 뭐일까? ⇒ READ COMMITED 이다


자 여기까지 기본적인 Transcation의 개념을 알아보았다.
이제 우리가 Spirng에서 사용하는 DB접근방식 통해 어떻게 Transaction을 관리하는지 알아보자

Spring Transcation

Spring에서의 DB접근 방법 중 JDBC각 제일 Low Level에 존재한다 이것들을 추상화해서 여러개의 접근방법이 존재하는데 대표적으로 Mybatis, JPA, JDBC Template 등이 있다.

이 때, Transcation 관련된 코드도 각각 다른데, 이것을 해결하기 위해 Spring은 트랜잭션을 관리하는 코드도 추상화해서 제공한다.

이 때, 사용되는 것이 PlatfromTranscationManager이고, 각 접근방법에 따라 구현체들도 제공한다. 거기다가, 코드들도 전부 AOP를 활용해 실제 비지니스 로직만 작성하면 되게끔 만들어 놓았다. (얼마나 편한가.. 제대로 알기만 하면, 우리는 가져다가 쓰기만 하면된다.)

@Transcational

김영한 선생님 강의에서 나온 사진을 통해 트랜잭션의 작동순서가 어떻게 되는지 정리해보자.

  1. annotation에 의해 생성된 프록시 객체의 메소드를 호출 한다.
  2. 추상화된 TranscationManager가 Datasource로부터 커넥션을 가져와서 트랜잭션을 시작한다.
  3. 이후 트랜잭션 동기화 매니저에 보관한다
  4. 실제 서비스 동작한다.
  5. 데이터 접근할 때, 트랜잭션 동기화 매니저에서 커넥션을 가져와 작업을 수행한다.
  6. 메소드가 마무리 될 때, 트랜잭션도 같이 끝나게 된다.

#cf) 트랜잭션 동기화 Thread에서 공유되는 ThreadLocal이란 곳에 저장해두고 사용한다고 하는데.. 이후에 자세히 알아보도록 하자.

트랜잭션 AOP 주의사항 - 프록시 내부 호출

특정 클래스나 메소드에 하나라도 @Transcational 어노테이션이 있으면, 트랜잭션 AOP는 해당 클래스를 상속받아서 프록시 객체를 만들어서 스프링 컨테이너에 등록한다.

이렇게 되면 어떤 class를 DI를 통해 주입될 때, Spring DI를 통해 프록시 객체가 주입되므로, 메소드를 호출할 때, 프록시 객체가 먼저 실행되고 이후에 작동하게 된다.

프록시를 거치지 않고 대상 객체를 직접 호출하는 경우, AOP가 적용되지 않는다.

즉 class 내부의 메소드에 의해 호출하는 메소드에 @Transcational이 있더라도, 적용이 되지 않는다.

왜? → class 외부에서 맨 처음 class 접근할 때, aop가 적용되지 않은 상태에서 호출하게 되면, 상속받은 class(xxxx$$CGLIB) 에서 → class(xxxx) 로 호출해서 로직이 실행되고 그 안에서, 내부 메소드가 실행되기 때문에, 내부메소드에 aop를 달아놔도 spring aop가 처리된 상속된 class를 실행시키지 않는다.

트랜잭션 AOP 주의사항 - 스프링 초기화시

  • bean을 초기화하는 시점에서는 @Transcational은 작동하지 않는다.
    • 초기화 코드가 먼저 되고, 이후에 동작하기 때문에
      • 해결 법 @EventListener (ApplicationReadyEvent.class)

트랜잭션 옵션소개

@Transcational 어노테이션에서 사용할 수 있는 옵션들

1 TrasncationManager

  • Transcational을 사용할 때 필요한 TranscationalManager를 지정할 수 있다.
  • ex) @Transcational(value="xxxTxmanager”) # 빈이름을 쓰는것

2 rollbackfor / rollbackForClassName / noRollbackFor /

  • 보통 언체크 예외가 터질 때, 롤백을 하는데, 그것을 원하는대로 바꾸고 싶을 때가 있다.
  • ex) @Trasncational(rollbackfor = Exception.class)
    • 체크예외도 롤백하게 된다.

3 isolation

  • 트랜잭션 격리 수준을 지정하는데 사용된다.
    • 기본값 데이터베이스에서 설정한 격리 수준을 따른다.
      • READ_UNCOMMITTED
      • READ_COMMITTED (보통 이거)
      • REPEATABLE_READ
      • SERIALIZE

4 timeout

  • 트랜잭션 수행 시간에 대한 타임아웃을 초로 지정한다.

5 readOnly

  • 트랜잭션 중 읽기전용 트랜잭션이 생성된다.
    • 성능최적화 때문에 사용
  • JPA 같은 경우
    • flush 작동 x
    • dirtycheck를 위한 스냅샷을 찍지 않음

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

  • default 설정으로 unchecked 예외에서는 롤백하고 checked 예외에서는 롤백이 안된다.

왜? 이런 규칙을 default로 설정하고, 필요에 따라서 내가 설정을 하게끔 했을끼?

스프링은 기본적으로

  • 체크예외는 → 비지니스 의미가 있을 때 사용하고
  • 언체크예외 → 복구 불가능한 예외로 생각

위의 이유로 @Transcational의 기본값이 이렇게 된 것이다..

→ 어떤 시스템을 구성할 때, 예외를 2가지로 나누어서 생각해야한다.

  • 시스템 예외
  • 비즈니스 예외

트랜잭션 전파

트랜잭션이 둘 이상 있을 때, 어떻게 작동할까??

트랜잭션 전파 기본

스프링은 다양한 트랜잭션 전파 옵션이 있다.

default == REQUIRED 일 경우

외부 트랜잭션 수행중인데, 내부 트랜잭션이 추가로 수행되는 경우

  • 외부 트랜잭션 + 내부 트랜잭션을 하나의 트랜잭션으로 묶여서 사용된다.
  • 물리 트랜잭션, 논리 트랜잭션
    • 물리 트랜잭션이란
      • db에서의 동작을 의미한다.
      • set Auto Commit False - 작업 - commit 이 한단위
    • 논리 트랜잭션이란
      • 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위 ( 조금 더 추상화된 개념.)
      • 기본 설정의 경우, (내부,외부) 구분하지 않는다.

원칙 : 하나의 트랜잭션으로 생각한다. ( 아래의 룰을 읽어보면 이해된다.)

→ 모든 논리트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.

→ 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백 된다.

내부에 트랜잭션 실행하더라도, 가장 외부의 트랜잭션이 물리 트랜잭션을 컨트롤하게끔 제어한다.

다른 옵션

  • REQUIRES_NEW 을 사용하게 되면, 다른 트랜잭션을 만들고, 별도의 트랜잭션 동작을 하게된다.

  • → 커넥션을 2개 들고 사용한다고 생각하면된다..
    ( 한 요청의, 커넥션을 2개들고 있는 문제…db connection 개수가 부족해질 수 있다…)

  • 옵션 보통 REQUIRED , REQUIRED_NEW 두가지를 가장 많이 사용한다.

정리

평소에 사용하던 Transcational을 다시 한번 정리해보았다. 기존에 대략적으로 알고 있던 내용들을 다시 한번 정리한 계기가 되었다.
이제 성능 향상을 위해 어떤 것들을 할지 생각해보도록 하자.

Reference

(김영한 선생님의 DB 강의)
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard

(기타 블로그)
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/

profile
개발자 지망생입니다.

0개의 댓글