트랜잭션

유요한·2024년 4월 18일
0

Spring Boot

목록 보기
25/25
post-thumbnail

트랜잭션이란?

데이터를 저장할 때 단순히 파일을 저장해도 되는데, 데이터베이스에 저장하는 이유가 무엇일까?

여러가지 이유가 있지만 가장 대표적인 이유는 바로 데이터베이스는 트랜잭션이라는 개념을 지원하기 때문이다.

트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다. 그런데 하나의 거래를 안전하게 처리하려면 생각보다 고려해야 할 점이 많다. 예를들어, A의 5000원을 B에게 계좌이체한다고 생각해보면 A의 잔고에서 5000원을 감소하고 B의 잔고에서 5000원을 증가해야 한다. DB의 상태를 변경시키기 위해 수행하는 작업 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미한다.

계좌이체라는 거래는 2가지 작업이 하나의 작업처럼 동작해야 한다. 둘 중 하나의 작업에서 문제가 발생하면 심각한 문제가 발생한다. 데이터베이스의 트랜잭션 기능을 사용하면 둘 다 성공해야 저장하고 하나라도 실패하면 거래 전의 상태로 돌아갈 수 있다. 모든 작업이 성공해서 데이터베이스에 정상 반영하면 커밋(commit)이라고 하고 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백(Rollback)이라고 한다.

데이터베이스의 상태를 변화한다는 것은 SELECT, INSERT, DELETE 같은 SQL문을 이용해 데이터베이스에 접근 하는 것을 뜻한다.

트랜잭션 ACID

  • 원자성(Atomicity)
    트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.

  • 일관성(Consistency)
    모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어, 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.

    무결성 제약 조건

    무결성 제약조건이란 데이터베이스의 정확성, 일관성을 보장하기 위해 저장, 삭제, 수정 등을 제약하기 위한 조건을 뜻합니다. 그래서 데이터베이스를 일관성 있게 유지하기 위해서 입니다.

  • 격리성(Isolation)
    동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어, 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.

    트랜잭션 격리 수준 - Isolation level

    • READ UNCOMMITTED(커밋되지 않은 읽기)
    • READ COMMITTED(커밋된 읽기)
    • REPEATABLE READ(반복 가능한 읽기)
    • SERIALIZABLE(직렬화 가능)
  • 지속성(Durability)
    트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

트랜잭션 매너저와 트랜잭션 동기화 매니저

스프링은 트랜잭션 동기화 매니저를 제공한다. 이것은 쓰레드 로컬을 사용해서 커넥션을 동기화해준다. 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있다. 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.

트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다.

동작 방식 설명

  1. 트랜잭션을 시작하려면 커넥션이 필요하다. 트랜잭션 매니저는 데이터 소스를 통해 커넥션을 만들고 트랜잭션을 시작한다.

  2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션트랜잭션 동기화 매니저저장한다.

  3. 레포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 따라서 파라미터로 커넥션을 전달하지 않아도 된다.

  4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션도 닫는다.

트랜잭션 문제

트랜잭션을 시작하고 비즈니스 로직을 실행하고 성공하면 커밋하고 예외가 발생하면 실패해서 롤백한다. 다른 서비스에서 트랜잭션을 시작하려면 try, catch, finally를 포함한 성공하면 커밋, 실패하면 롤백 코드가 반복될 것이다. 이러한 코드가 각각의 서비스에서 반복된다. 여기서 달라지는 부분은 비즈니스 로직뿐이다.

이럴 때 탬플릿 콜백 패턴을 활용하면 이런 반복 문제를 해결할 수 있다. 하지만 이곳은 서비스 로직인데 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다. 애플리케이션을 구성하는 로직을 핵심 기능과 부가 기능으로 구분하자면 서비스 입장에서 비즈니스 로직은 핵심 기능이고 트랜잭션 부가 기능이다.

이렇게 비즈니스 로직과 트랜잭션을 처리하는 기술 로직이 한 곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다. 결과적으로 유지보수하기 힘들다. 서비스 로직은 가급적 비즈니스 로직만 있어야 한다. 하지만 트랜잭션 기술을 사용하려면 트랜잭션 코드가 나와야 한다. 어떻게 이 문제를 해결할 수 있을까?

이럴 때 AOP를 통해 프록시를 도입하면 문제를 깔끔하게 해결한다. 프록시를 도입하면 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다.

@Transactional

@Transactional은 스프링 프레임워크에서 트랜잭션을 지원하는 데 사용되는 어노테이션입니다. 이 어노테이션을 사용하면 해당 메서드 또는 클래스의 메서드들이 트랜잭션 경계 내에서 실행됩니다. 즉, 여러 개의 연관된 데이터베이스 작업이 하나의 트랜잭션으로 묶여서 실행되거나, 전체가 성공 또는 실패할 때 롤백되는 등의 트랜잭션 처리를 할 수 있습니다. 메서드가 정상적으로 완료되면 트랜잭션은 커밋되고, 예외가 발생하면 롤백됩니다.

@Transactional 어노테이션에서 readOnly 속성을 사용하는 경우가 있는데, 이 속성을 true로 설정하면 해당 트랜잭션이 읽기 전용으로 동작하게 됩니다. 즉, 데이터를 조회하는 쿼리만 수행하고 데이터를 변경하는 쓰기 작업은 허용되지 않습니다. 이렇게 하면 트랜잭션의 격리 수준을 낮출 수 있어 일부 성능 향상을 기대할 수 있습니다.

readOnly와 비슷한 개념인 Hint는 스냅샷을 만들지 않기 때문에 메모리가 절약됩니다. 즉, 읽기 전용입니다. @Transaction(readOnly=true)는 트랜잭션 커밋 시점에 flush를 하지 않기 때문에 이로 인한 dirty checking 비용이 들지 않습니다. 따라서 cpu가 절약됩니다. 스프링 5.1 버전 이후를 사용하시면 @Transaction(readOnly=true)로 설정하시면, @QueryHint의 readOnly까지 모두 동작합니다. 즉, @Transaction(readOnly=true)로 대체해서 사용하면 됩니다.

테스트 환경에서 @Transactional를 추가하면 rollback을 시켜준다. 하지만 일반적으로 @Transactional를 구성하면 메소드를 실행할 때 오류 발생 시 전체 실행 내용을 롤백 시켜준다.

@Transactional은 어떻게 동작할까?

이를 알기 위해서는, SpringBoot의 핵심 개념인 AOP프록시 패턴에 대해서 알아야한다. 트랜잭션은 Spring AOP를 통해 구현되어있다. 더 정확하게 말하면, 어노테이션 기반 AOP를 통해 구현되어있다.

AOP

AOP관점 지향 프로그래밍으로, 애플리케이션의 공통 관심사를 애플리케이션의 핵심 로직으로부터 분리해서 모듈화하는 프로그래밍 패러다임이다. SpringBoot에서는 AOP를 이용하여 선언적 트랜잭션 관리, 즉 @Transactional을 구현한다.

SpringBoot는 AOP를 구현하기 위해 주로 프록시 패턴을 사용한다. 프록시 패턴은 특정 객체에 대한 접근을 제어하거나 추가적인 기능을 제공하기 위해, 해당 객체의 대리인 역할을 하는 객체를 두는 디자인 패턴이다. @Transactional이 붙은 클래스나 메서드를 호출할 때, SpringBoot는 실제 객체 대신 프록시 객체를 사용하여 호출을 가로챈다.

프록시

프록시 객체는 원본 객체를 대신해서 호출될 객체로, 원본 객체를 감싸서 클라이언트의 요청을 처리하는 중간 단계에 위치합니다. 프록시 객체는 원본 객체와 같은 인터페이스를 구현하고 있어서, 클라이언트는 프록시 객체를 호출하는 것으로 인해 원본 객체의 메소드를 호출하는 것과 같은 효과를 얻을 수 있습니다. 프록시 객체를 사용하는 이유는, 프록시 객체를 통해 원본 객체에 대한 접근을 제어하거나, 부가적인 기능을 제공하기 위해서입니다.

AOP를 구현하기 위해서 프록시 객체를 이용하는 이유?

원본 객체에 대한 코드 수정 없이, 런타임 시점에서 프록시 객체를 통해 부가적인 처리를 추가하기 위해서입니다.

프록시 객체는 원본 객체를 감싸고, 클라이언트가 프록시 객체를 호출할 때 원본 객체의 메소드를 호출하기 전에 추가적인 로직을 수행할 수 있도록 도와줍니다. 이렇게 하면 클라이언트는 원본 객체를 호출하는 것처럼 보이지만, 실제로는 프록시 객체가 원본 객체를 감싸고 부가적인 로직을 수행하게 됩니다.

Spring에서 프록시 객체를 생성하기 위해서는 프록시 객체를 생성할 인터페이스를 구현하는 클래스를 만들고, 이 클래스에 부가적인 처리를 수행하는 Advice를 정의한 뒤, Spring AOP가 프록시 객체를 생성하여 클라이언트에게 제공하게 됩니다. 이렇게 함으로써 원본 객체에 대한 수정 없이 부가적인 처리를 적용할 수 있으며, 코드의 재사용성과 유지보수성을 높일 수 있습니다.

@Transactional 동작 과정

  1. @Transactional이 붙은 클래스나 메서드가 있을 때, SpringBoot는 해당 클래스나 메서드를 호출 할 때 트랜잭션을 관리하는 프록시 객체를 생성한다.

  2. Spring AOP를 사용하여 실제 객체의 프록시를 생성한다.

  3. 프록시 객체는 @Transactional이 붙은 메서드가 호출될 때, 트랜잭션 매니저를 사용하여 새로운 트랜잭션을 시작하거나, 이미 진행 중인 트랜잭션에 참여한다.

  4. 메서드 실행 중에 예외가 발생하면, 프록시 객체는 트랜잭션 매니저에 ROLLBACK을 요청하고, 만약 성공적으로 완료되면 COMMIT을 요청한다.

특징

  • 클래스, 메소드에 @Transactional이 선언되면 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성

  • 프록시 객체는 @Transactional이 포함된 메서드가 호출될 경우, 트랜잭션을 시작하고 Commit or Rollback을 수행

  • CheckedException or 예외가 없을 때는 Commit

  • UncheckedException이 발생하면 Rollback

주의점

  • 우선순위

    • @Transactional은 우선순위를 가지고 있다. 클래스 메서드에 선언된 트랜잭션의 우선순위가 가장 높고, 인터페이스에 선언된 트랜잭션의 우선순위가 가장 낮다.

      따라서 공통적인 트랜잭션 규칙은 클래스에, 특별한 규칙은 메서드에 선언하는 식으로 구성할 수 있다. 또한, 인터페이스 보다는 클래스에 적용하는 것을 권고한다.
    • 인터페이스나 인터페이스의 메서드에 적용할 수 있다. 하지만, 인터페이스 기반 프록시에서만 유효한 트랜잭션 설정이 된다.
    • 자바 어노테이션은 인터페이스로부터 상속되지 않기 때문에 클래스 기반 프록시 or AspectJ 기반에서 트랜잭션 설정을 인식 할 수 없다.
  • 트랜잭션의 모드
    @Transactional은 Proxy ModeAspectJ Mode가 있는데 Proxy Mode가 Default로 설정되어있다. Proxy Mode는 다음과 같은 경우 동작하지 않는다.

    • 반드시 public 메서드에 적용되어야한다.
    • Protected, Private Method에서는 선언되어도 에러가 발생하지는 않지만, 동작하지도 않는다.
    • Non-Public 메서드에 적용하고 싶으면 AspectJ Mode를 고려해야한다.
  • @Transactional이 적용되지 않은 Public Method에서 @Transactional이 적용된 Public Method를 호출할 경우, 트랜잭션이 동작하지 않는다.

profile
발전하기 위한 공부

0개의 댓글