[CS] Transaction과 스프링에서의 활용

김상현·2024년 1월 28일
1

CS

목록 보기
10/10
post-thumbnail

트랜잭션이란?

우리의 일상 생활에서 트랜잭션을 가장 쉽게 이해할 수 있는 상황은 바로 계좌 입출금 상황이 있다. 계좌 입출금의 과정을 살펴보면 다음과 같다.

입출금 과정

1. A가 B에게 10000원을 송금
2. A의 계좌에 10000원을 차감
3. B의 계좌에 10000원을 증가

만약 앞선 입출금 과정에서 10000원을 차감하는 단계 혹은 10000원을 증가하는 단계에서 오류가 발생하여 부분 업데이트 현상이 발생한다면 이 세상에 존재하던 10000원이 사라지거나 이 세상에 존재하지 않던 10000원이 발생하는 문제가 발생할 것이다. 따라서 논리적인 작업 셋을 하나의 단위로 묶어서 처리하는 것에 대한 필요성이 발생하였고 해결 방법으로 트랜잭션이 개발되었다.

트랜잭션은 작업의 완전성을 보장해 주는 것이다. 즉 논리적인 작업 셋을 모두 완벽하게 처리하거나 처리하지 못할 경우에는 원 상태로 복구해서 작업의 일부만 적용되는 현상(Partial update)이 발생하지 않게 만들어주는 기능이다.

트랜잭션은 꼭 여러개의 변경 작업을 수행하는 쿼리가 조합됐을 때만 의미 있는 개념은 아니다. 트랜잭션은 하나의 논리적인 작업 셋에 하나의 쿼리가 있든 두 개 이상의 쿼리가 있든 관계없이 논리적인 작업 셋 자체가 100% 적용되거나(Commit을 실행했을 때) 아무것도 적용되지 않아야(ROLLBACK 또는 트랜잭션을 ROLLBACK 시키는 오류가 발생했을 때)함을 보장해 주는 것이다.


트랜잭션의 4가지 성질

  • 원자성(Atomicity)
    • 트랜잭션이 데이터베이스에 모두 반영되던가(Commit), 아니면 전혀 반영되지 않아야 한다(Rollback).
  • 일관성(Consistency)
    • 트랜잭션의 작업 처리 결과가 항상 일관성이 있어야 한다.
    • 데이터베이스 내의 계층 관계, 컬럼의 속성 등이 항상 일관되게 유지되어야 한다.
  • 독립성(Isolation)
    • 둘 이상의 트랜잭션이 동시에 실행되고 있을 경우 어떤 하나의 트랜잭션이라도, 다른 트랜재션의 연산에 끼어들 수 없다.
  • 지속성(Durability)
    • 트랜잭션이 성공적으로 완료됬을 경우, 결과는 영구적으로 반영되어야 한다.
    • 모든 트랜잭션은 로그로 남겨져 어떠한 장애에도 대비할 수 있도록 한다.

DBMS의 트랜잭션

데이터베이스에 작업이 들어왔을 때 모든 작업의 독립성을 보장해 하나씩 순차적으로 진행하게 된다면 CPU는 DBMS보다 인풋 아웃풋 작업을 빈번히 수행하기 때문에 트랜잭션을 순차적으로 수행하면 CPU는 응답을 기다리는 시간이 길어져 프로그램이 비효율적으로 동작하는 문제가 발생할 수 있다. 이처럼 데이터베이스에 저장된 데이터의 무결성과 동시성의 성능을 지키기 위해 트랜잭션의 설정이 중요하다.

데이터베이스에서는 각각의 명령을 하나의 트랜잭션으로 보고 보장해주기 때문에 여려 명령을 하나의 트랜잭션으로 묶고 싶은 경우 개발자가 직접 트랜잭션의 경계설정을 통해 트랜잭션을 명시하는 일이 필요하다.


스프링에서 사용하는 트랜잭션

Annotation을 기반으로 트랜잭션을 설정하는 방안

@Transactional(readOnly = true)
public class Service {
	@Transactional	
	public void save(Entity e) {
		...
	}
}

트랜잭션 어노테이션은 메소드, 클래스, 인터페이스 등에 적용할 수 있다. 클래스 상단에 적용된 어노테이션에 대해서는 해당 클래스에 존재하는 모든 메서드에 어노테이션이 적용된다.

중첩되어 존재하는 경우에는 클래스 메서드, 클래스, 인터페이스 메서드 인터페이스 순으로 우선순위(priority)를 갖고 적용된다.

어노테이션이 적용된 메서드는 메서드 시작부터 트랜잭션이 시작되고 메서드를 성공적으로 끝마치면 트랜잭션 커밋, 도중에 문제가 발생하면 롤백하는 과정이 진행된다. 어노테이션은 데이터베이스에 여러번 접근하면서 하나의 작업을 수행하는 서비스 계층 메서드에 붙이는 것이 일반적이다.


@Transactional 속성

propagation

  • 이미 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 옵션
  • REQUIRED : 디폴트 설정값. 트랜잭션 내부에 트랜잭션이 존재할 경우 두 개의 트랜잭션이 모두 성공할 경우에만 커밋을 수행하고 둘 중 하나라도 실패할 경우 두 개의 트랜잭션이 모두 롤백된다.
  • SUPPORTS : 기존의 트랜잭션이 존재하는 경우 해당 트랜잭션에 참여하고 존재하지 않을 경우 트랜잭션 없이 메서드가 실행된다.
  • MANDATORY : 기존의 트랜잭션이 존재하는 경우 해당 트랜잭션에 참여하고 존재하지 않을 경우 IllegalTransactionStateException 예외가 발생합니다. 즉, 기존의 트랜잭션이 존재하지 않는다면 실행될 수 없고, 혼자서도 메서드를 실행할 수 없다.
  • REQUIRES_NEW : 기존의 트랜잭션이 존재해도 항상 새로운 트랜잭션을 시작한다. 기존의 트랜잭션이 존재하는 경우 기존 트랜잭션을 잠시 멈추고 새로운 트랜잭션이 독립적으로 실행된다.
  • NOT_SUPPORTED : 기존의 트랜잭션이 존재한다면 보류하고 트랜잭션을 사용하지 않고 자신의 메소드를 실행하는 방식이다.
  • NEVER : 트랜잭션을 사용하지 않도록 강제한다. 기존의 트랜잭션이 존재한다면 IllegalTransactionStateException ****예외가 발생한다.
  • NESTED : 기존의 트랜잭션이 존재한다면 그 안에 새로운 자식 트랜잭션을 만드는 설정이다. 만약 기존(부모) 트랜잭션에 문제가 발생한다면 새로운(자식) 트랜잭션은 롤백 처리되지만, 새로운(자식) 트랜잭션에 문제가 발생해도 기존(부모) 트랜잭션에는 롤백이 발생하지 않는다.

isolation(격리수준)

  • 동시에 여러 트랜잭션이 실행될 때 다른 트랜잭션에게 보여줄지 말지 결정하는 것으로 가능한 많은 트랜잭션을 동시에 진행하면서도 문제가 생기지 않도록 하려는 설정이다.
  • READ_UNCOMMITTED : 가장 낮은 격리수준으로 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있는 설정이다. 해당 설정은 동시성 문제가 발생할 수 있다는 단점이 있다.
  • READ_COMMITTED : 가장 많이 사용되는 두 번째로 낮은 격리 수준으로 커밋되지 않은 정보는 읽을 수 없다. READ_UNCOMMITTED 의 문제를 해결하기 위해 존재하지만 3개의 트랜잭션이 동시에 발생했을 때 마찬가지로 동시성 문제가 발생할 수 있다는 단점이 있다. 대부분의 DB는 READ_COMMITTED 를 기본 격리수준으로 사용한다.
  • REPEATABLE_READ : 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없게 한다. 하지만 새로운 로우를 추가하는 것은 제한하지 않기 때문에 다시 조회했을 때 발견하지 못한 새로운 로우가 발생할 수 있다는 문제가 있다.
  • SERIALIZABLE : 동시에 같은 테이블의 정보를 접근할 수 없다. 하지만 트랜잭션을 동시에 사용하지 못하고 순차적으로 수행하는 것과 다를 바 없어서 성능이 매우 떨어지니 극단적인 상황에만 사용하도록 해야한다.

timeout

  • 트랜잭션에 초 단위로 제한시간을 지정할 수 있다.
  • 트랜잭션의 실행 시간이 지정해둔 값을 초과하게 된다면 IllegalTransactionStateException 예외가 발생하며 롤백된다.

readOnly

  • 읽기전용 트랜잭션을 위한 속성
  • 해당 속성을 true 로 설정할 경우 트랜잭션 작업 안에서 update, insert, delete 작업이 수행되는 것을 방지한다.
  • flush 모드가 manual로 설정되어 사용자가 수동으로 flush 를 수행해야 하기 때문에 JPA의 더티체킹 기능을 무시할 수 있기 때문에 성능향상에 도움이 된다.

rollbackFor

  • 트랜잭션 기본적으로 RuntimeExceptionError 가 발생했을 때만 롤백을 수행한다. 따라서 이외에 Exception을 롤백 대상으로 삼고 싶다면 특정 Exception을 클래스로 전달해 사용할 수 있다.

noRollbackFor

  • RuntimeExceptionError 가 발생하여도 커밋을 수행할 수 있도록 하는 설정이다. IOException 을 설정값으로 두면 롤백 대상인 IOException 이 발생해도 롤백하지 않고 커밋을 진행한다.

주의사항

부분 업데이트 현상이 발생하면 실패한 쿼리로 인해 남은 레코드를 다시 삭제하는 재처리 작업이 필요할 수 있다. 실행하는 쿼리가 하나뿐이라면 재처리 작업은 간단할 것이다. 하지만 2개 이상의 쿼리가 실행되는 경우라면 실패에 대한 재처리 작업은 다음 예제와 같이 상당한 고민거리가 될 것이다. 만약 트랜잭션을 적용하지 않은 부분 업데이트 현상이 발생한다면 부분 업데이트 결과로 쓰레기 데이터가 테이블에 남아 있을 가능성이 있다.

트랜잭션은 꼭 필요한 최소의 코드에만 적용하는 것이 좋다. 이는 프로그램 코드에서 트랜잭션의 범위를 최소화하라는 의미다. 다음은 트랜잭션 처리시 주의해야할 리스트다.

  • DBMS의 기능이 필요가 없는 작업은 트랜잭션에 포함시킬 필요는 없다.
  • 메일 전송이나 FTP 파일 전송 작업 또는 네트워크를 통해 원격 서버와 통신하는 등과 같은 작업은 어떻게 해서든 DBMS의 트랜잭션 내에서 제거하는 것이 좋다.
  • 반드시 하나의 트랜잭션으로 묶어야할 작업 셋과 작업의 성격이 달라 별도의 트랜잭션으로 분리하는 것이 좋은 상황을 잘 판단해야 한다.
profile
목적 있는 글쓰기

0개의 댓글