본문 바로가기
코드 스테이츠

트랜잭션(Transaction)

by 한휘용 2023. 6. 26.
728x90

트랜잭션(Transaction)

Java Spring에서 트랜잭션(Transaction)은 데이터베이스 작업 단위를 의미하며, 한 번에 수행되어야 하는 연산들의 논리적 단위입니다.

 

예를 들어, 여러 개의 데이터를 추가하거나 수정할 때, 모든 작업이 성공적으로 수행되거나 모두 실패하여 아무런 변경도 발생하지 않는 것을 보장합니다.

 

트랜잭션의 개념을 직관적으로 알수 있는 사례를 들어보겠습니다.

 

사례 1)
회원이 커피 주문 앱으로 카페라떼 두 잔을 선택하고 결제 버튼을 누른 후, 주문이 진행되는 중에 네트워크 오류로 인해 결제를 완료하는데 실패했습니다.

그런데, 회원이 주문한 주문이 정상적으로 데이터베이스에 등록이 되고, 주문한 커피 수만큼의 스탬프가 찍혔습니다.

이 경우, 카페를 운영하는 쪽에서는 판매 수익을 얻지 못하는 손해를 볼 것입니다. 반면에 회원은 공짜 커피를 마시게 되는 셈입니다.

사례 2)
반대로 회원이 주문한 커피에 대한 결제는 완료되었는데, 데이터베이스에 저장하는 중에 에러가 발생해서 회원이 주문한 커피 주문 정보가 데이터베이스에 정상적으로 등록이 되지 않았습니다.

이 경우, 회원은 커피도 마시지 못한 채 금전적인 손해만 볼 것입니다.

사례 3)
마지막으로 회원의 커피 주문은 정상적으로 데이터베이스에 등록이 되었는데, 주문한 커피 수만큼의 스탬프 횟수를 데이터베이스에 업데이트하는 중에 에러가 발생해서 커피 주문은 완료되었지만 나중에 확인해 보니 스탬프가 누적되지 않았습니다.

이 경우, 회원은 커피는 마시지만 아까운 스탬프 횟수를 잃게 될 것입니다.

위에서  제시한 세 가지 사례의 공통점은 두 개의 작업(커피 주문 결제, 스탬프 횟수)들이 마치 하나의 그룹처럼 묶여서 처리되는 중에 둘 중 하나라도 처리에 실패할 경우 애플리케이션의 신뢰성이 깨지는 상황이 발생하고 있습니다.

 

트랜잭션은 이처럼 여러 개의 작업들을 하나의 그룹으로 묶어서 처리하는 처리 단위인데, 앞서 본 사례처럼 애플리케이션의 신뢰성이 깨지는 상황이 발생한다면, 이는 트랜잭션이라고 부를 수 없습니다.

 

무조건 여러 개의 작업을 그룹으로 묶는다고 해서 트랜잭션이 아니라, 물리적으로는 여러 개의 작업이지만 논리적으로는 마치 하나의 작업으로 인식해서 전부 성공하든가 전부 실패하든가(All or Nothing)의 둘 중 하나로만 처리되어야 트랜잭션의 의미를 가집니다.

 

이러한 All or Nothing이라는 트랜잭션 처리 방식은 애플리케이션에서 사용하는 데이터의 무결성을 보장하는 핵심적인 역할을 합니다.

 

 

ACID 원칙

트랜잭션의 특징을 설명할 때 일반적으로 ACID라는 원칙을 이용합니다.

 

ACID 원칙은 데이터베이스에서 트랜잭션의 안전성과 일관성을 보장하기 위한 규칙들을 말합니다.

ACID는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)의 약자입니다.

 

1. 원자성(Atomicity)

트랜잭션의 원자성이란 작업을 더 이상 쪼갤 수 없음을 의미합니다.

어떤 작업은 처리되고, 어떤 작업은 처리하지 안하도 되는식으로 쪼개서 처리할 수 없습니다.

 

예를 들어, 은행 계좌 이체를 트랜잭션으로 처리한다고 가정해봅시다. 이체 작업은 출금과 입금 두 단계로 이루어집니다. 원자성을 보장하기 위해서는 출금과 입금이 항상 함께 수행되어야 합니다. 만약 출금은 성공했지만 입금이 실패하면, 출금된 금액은 다시 복구되어야 합니다. 마찬가지로, 입금은 성공했지만 출금이 실패하면 입금된 금액도 복구되어야 합니다.

 

 

2. 일관성(Consistency)

트랜잭션의 일관성이란 트랜잭션이 에러 없이 성공적으로 종료될 경우,

비즈니스 로직에서 의도하는 대로 일관성 있게 저장되거나 변경되는 것을 의미합니다.

 

예를 들어, 트랜잭션을 통해 어떤 계좌에서 돈을 출금하고 다른 계좌로 입금하는 작업을 수행한다고 가정해봅시다.

이 작업이 트랜잭션으로 묶여 있을 때, 데이터베이스는 출금 계좌의 잔액을 올바르게 감소시키고 입금 계좌의 잔액을 올바르게 증가시켜야 합니다. 이러한 변경은 일관된 상태를 유지하여 데이터의 정확성을 보장해야 합니다.

 

3. 격리성(Isolation)

트랜잭션의 격리성이란 각 작업이 다른 작업에게 영향을 주지 않고 독립적으로 실행되는 것을 의미합니다.

이는 동시에 여러 작업이 수행되더라도 각 작업은 다른 작업의 결과를 알거나 영향을 받지 않습니다.

 

예를 들어, 은행에서 여러 사람들이 동시에 입금과 출금 작업을 수행한다고 가정해봅시다.

독립성이 보장되지 않는다면 한 사람의 입금 작업이 다른 사람의 출금 작업에 영향을 줄 수 있습니다.

그러나 독립성이 보장된다면 각 작업은 다른 작업에게 영향을 주지 않고 독립적으로 처리됩니다.

즉, 한 사람의 입금 작업은 다른 사람의 출금 작업에 영향을 미치지 않고 독립적으로 실행됩니다.

 

4. 지속성(Durability)

트랜잭션의 지속성이란 작업이 완료되고 커밋되면 해당 작업의 결과가 영구적으로 저장되는 것을 의미합니다.

즉, 트랜잭션이 성공적으로 완료되면 그 결과는 데이터베이스에 영구적으로 저장되어야 합니다.

 

예를 들어, 은행 거래를 통해사용자 A가 자신의 계좌에서 100달러를 인출하는 거래를 진행한다고 가정해봅시다. 이 거래는 트랜잭션으로서 실행됩니다. 트랜잭션이 성공적으로 완료되고 커밋되면 A의 계좌에서 100달러가 인출되어야 하고, 이 결과는 데이터베이스에 영구적으로 저장되어야 합니다. 따라서 A의 계좌 잔액은 영구적으로 100달러가 감소되어야 합니다.

 

 

트랜잭션 커밋(commit)과 롤백(rollback)

트랜잭션에서 커밋(commit)과 롤백(rollback)은 중요한 개념입니다.

커밋(commit)과 롤백(rollback)은 데이터베이스에서 사용되는 명령어로 지금부터 간단하게 살펴보겠습니다.

 

 

커밋(commit)

커밋(commit)은 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어로 커밋(commit)명령을 수행하면 변경된 내용이 데이터베이스에 영구적으로 저장됩니다.

만약 작업이 끝난후 커밋(commit)명령을 수행하지 않으면 결과가 데이터베이스에 최종적으로 반영되지 않습니다.

 

성공적인 커밋은 데이터 변경을 영구적으로 반영하고, 다른 작업에서도 변경된 데이터를 정확히 조회할 수 있게 합니다.

커밋은 트랜잭션의 완료를 의미합니다.

 

롤백(rollback)

롤백(rollback)은 작업 중 문제가 발생했을 때, 트랜잭션 내에서 수행된 작업들을 취소하여 이전 상태로 되돌리는 것을 말합니다.

 

트랜잭션 도중에 문제가 발생하거나 예외가 발생한 경우,

롤백을 실행하여 이전에 수행한 작업을 모두 취소하고 데이터베이스를 원래의 일관된 상태로 복구합니다.

롤백은 트랜잭션을 취소하고, 데이터의 일관성을 유지하는 중요한 메커니즘입니다.

 

 

트랜잭션의 진정한 의미

트랜잭션의 의미를 이해하기 위해 데이터베이스에서 작업을 처리하는 상황을 예로 들었지만 트랜잭션은 사실 데이터베이스에만 한정해서 사용하는 의미는 아닙니다.

 

예를 들어 어떤 데이터를 로컬 데이터베이스에도 저장하고, 그 결과를 푸시 알림(push notification)으로 클라이언트에게 전송하는 기능이 있다면 데이터베이스 저장과 푸시 알림 전송이라는 두 개의 작업이 하나의 트랜잭션으로 묶여서 둘 중에 하나라도 실패할 경우 롤백(rollback)이 되어야 할 수도 있습니다.(정책적으로 이 두 개의 작업을 별개로 보는 경우도 있습니다.)

 

이처럼 전혀 다른 타입의 리소스(데이터베이스, 파일, 메시지 등)를 하나의 작업 단위로 묶어서 처리해야 되는 상황에서는 어떤 식으로 트랜잭션을 적용하면 좋을지에 대해 고민해봐야 합니다.

 

 

Spring Boot에서 트랜잭션 설정 - 선언형 방식의 트랜잭션 적용 

Spring에서 선언형 방식으로 트랜잭션을 적용하는 방법은 크게 2가지 입니다.

 

첫 번째는 작성한 비즈니스 로직에 애너테이션을 추가하는 방식이고, 다른 하나는 AOP 방식을 이용해서 비즈니스 로직에서 아예 트랜잭션 적용 코드 자체를 감추는 방식입니다.

 

애너테이션 방식의 트랜잭션 적용

spring에서 트랜잭션을 적용하는 가장 간단한 방법은 @Transactional 이라는 애너테이션을 트랜잭션이 필요한 영역에 추가해 주는 것입니다.

 

클래스 레벨에 @Transaction 애너테이션 적용 예)

import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional   // (1)
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());

        return memberRepository.save(member);
    }
		
		...
		...
}

위의 예시 코드와 같이 클래스 레벨에서 @Transactional 애너테이션을 추가하면 기본적으로 해당 클래스에서 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션이 적용된다.

 

모든 메서드에 트랜잭션이 적용되는 이유는 MemberRepository가 데이터베이스와 직접적으로 연관되는 클래스이기 때문이다.

 

 

JPA 로그 레벨 설정

애플리케이션을 실행시키기 전에 트랜잭션이 어떻게 적용되는지 로그로 확인할 수 있도록 JPA의 로글 레벨을 아래와 같이 application.yml에 추가한다.

 

JPA 로그 레벨 설정 예)

spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    url: jdbc:h2:mem:test
  jpa:
    hibernate:
...
...

logging:         # (1) 로그 레벨 설정
  level:
    org:
      springframework:
        orm:
          jpa: DEBUG

위 예시 코드와 같이 로그레벨을 ‘DEBUG’ 레벨로 설정하면 JPA 내부에서 ‘DEBUG’ 로그 레벨을 지정한 부분의 로그를 확인할 수 있다. 

 

728x90

'코드 스테이츠' 카테고리의 다른 글

Spring Security - 보안  (0) 2023.07.10
테스팅(Testing) - 단위 테스트  (0) 2023.06.28
페이지네이션(Pagination)  (0) 2023.06.20
DDD(Domain Driven Design),애그리거트(Aggregate)  (0) 2023.06.19
Spring Data JDBC  (0) 2023.06.18