[우아한테크코스] 7월 21일 TIL

5 minute read

[Spring] 토비의 스프링 11장 트랜잭션

선언적 트랜잭션

선언적 트랜잭션 경계설정을 사용하면 코드 내에서 직접 트랜잭션을 관리하지 않아도 된다.
트랜잭션 스크립트 방식을 벗어날 수 있다.
트랜잭션 스크립트란 하나의 트랜잭션 안에서 동작해야 하는 코드를 한 군데 모아서 만드는 것이다.
트랜잭션 전파 기능 덕택에 의미 있는 작은 단위로 만들어진 메서드를 하나의 트랜잭션으로 동작하게 할 수도 있다.
예를 들어 A, B, C 각각의 메서드가 있을 때 A-B 코드를 하나의 트랜잭션으로 만들고 싶다면 전파 속성을 REQUIRED로 두고 A에서 B의 코드를 호출하게 하면 된다. 그럼 A에서 시작된 트랜잭션에 B가 자동으로 참가한다.

스프링의 선언적 트랜잭션은 기존에 JTA를 이용해야만 글로벌 트랜잭션을 적용할 수 있던 것을 간단한 어플리케이션과 JDBC, JPA, 하이버네이트 등과 같이 스프링이 지원하는 대부분의 데이터 액세스 기술에서도 적용할 수 있게 했다. 그와 동시에 JTA도 사용할 수 있다.

스프링이 제공하는 트랜잭션 서비스는 추상화와 동기화로 나눌 수 있다.

  • 추상화
    특정 기술과 플랫폼, 특정 트랜잭션 서비스, 특정 데이터 액세스 기술에 종속되지 않고 트랜잭션을 사용할 수 있도록 해주었다.
    핵심 인터페이스인 PlatformTransactionManager는 트랜잭션 경계 지정에 사용되어 메서드로는 getTransaction(속성에 따라 새로 시작하거나 진행 중에 참여하거나 새로 만들거나 무시하거나), commit, rollback을 가진다.
    TransactionDefinition은 트랜잭션의 네가지 속성을 나타내는 인터페이스로 TransactionStatus에 현재 참여하고 있ㄴ는 트랜잭션의 ID와 정보를 담는다. 커밋/롤백 시 사용된다.

  • 동기화
    트랜잭션을 일정 범위 안에서 유지해주고 어디서든 접근할 수 있게 해주었다.

트랜잭션 매니저의 종류

DB가 하나라면 트랜잭션 매니저도 하나만, 여러 개라면 Jta 트랜잭션 매니저 하나만 등록해야 한다.
두개를 완전히 독립적으로 사용한다면 괜찮다.

스프링이 제공하는 PlatformTransactionManager의 클래스

  • DataSourceTransactionManager
    Connection의 트랜잭션 API를 이용해 트랜잭션을 관리해주는 매니저이다.
    DataSource가 빈으로 있어야 하고 JDBC API를 이용한 곳에 적용할 수 있다.
    getConnection 시에 새로운 Connection을 돌려주어야 한다.
    JdbcTemplate의 내부에서 트랜잭션 매니저로부터 현재 진행 중인 트랜잭션을 가져올 때 DataSourceUtils 클래스의 getConnection(DataSource)를 사용해야 한다.

JdbcTemplate을 사용하지 않을 때 스프링 트랜잭션 매니저를 사용하고 싶다면?
Connection 가져올 때 모두 DataSourceUtils.getconnection()을 사용한다.
직접 DataSource의 getConnection()을 호출한다.

  • JpaTransactionManager
    JPA를 이용하는 DAO에 사용되는 매니저이다.
    JTA를 사용하는 경우에는 필요 없다.
    LocalContainerEntityManagerFactoryBean을 등록해야 한다.
    DataSourceTransactionManager가 제공하는 것을 모두 제공해 JDBC DAO 사용 시에도 사용할 수 있다.

  • HibernateTransactionManager, JmsTransactionManager, CciTransactionManager

  • JtaTrnasactionManager
    하나 이상의 DB나 트랜잭션 리소스가 참여하는 글로벌 트랜잭션에 사용된다.
    여러 개의 트랜잭션 리소스에 대한 작업을 하나의 트랜잭션으로 묶을 수도 있고, 여러 서버에 분산된 것도 묶을 수 있다.
    빈 등록을 해서 사용할 수 있다.

선언적 트랜잭션 경계설정의 방법

트랜잭션의 시작과 종료는 보통 서비스 계층 오브젝트의 메서드와 같다.

코드에 의한 경계 설정

public class MemberService {
  @Autowired
  private MemberDao memberDao;

  private TransactionTemplate transactionTemplate;

  @Autowired
  public void init(PlatformTransactionManager tm) { //트랜잭션 종류 상관 없이 경계설정 가능 
    this.transactionTemplaate = new TransactionTemplate(tm);  //TransactionDefinition을 만들어 속성 변경 가능
  }

  public void add(final Member member) {
    this.transactionTemplate.execute(new TransactionCallback {
      public Object doInTransaction(TransactionStatus status) {   //트랜잭션 안에서 동작하는 것, 
        memberDao.add(member);
      }
      return null;  //커밋, 진행 중인 트랜잭션이 있다면 모두 끝나고 커밋, 예외 발생 시 롤백
    });
  }
}

AOP를 이용한 선언적인 방법

간단한 설정으로 특정 부가기능을 임의의 타겟 오브젝트에 줄 수 있는 프록시 AOP 사용

  • AOP와 tx 네임스페이스
    어떤 부가기능을 사용할지(어드바이스) 부가 기능을 담은 트랜잭션 어드바이스 정의
    ```java
어떤 대상에게 부여할지(포인트컷) 
```java
<aop:pointcut id="txPointcut" expression="execution(* *..MemberDao.*(..))" />

포인트컷은 기본적으로 인터페이스에 적용된다.

트랜잭션 어드바이스와 포인트컷을 결합해 하나의 AOP 모듈로 정의하는 것을 어드바이저라고 한다.

<aop:config>
  <aop:pointcut id="txPointcut" expression="execution(* *..MemberDao.*(..))" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
</aop:config>

어드바이저는 참조될 일이 없어 id를 넣지 않는다.

코드에는 영향을 주지 않고 일괄적으로 트랜잭션을 적용하고 변경할 수 있다.

  • @Transactional
    설정파일에 따로 명시할 필요 없이 어노테이션이 지정된 트랜잭션을 대상으로 속성을 제공한다.
    클래스의 메서드, 클래스, 인터페이스의 메서드, 인터페이스 순으로 우선순위가 적용된다.
    불규칙하게 혼용하지 말고 적용 대상을 미리 지정하고 통일하는 것이 좋다.

코드에 일일이 붙이기 번거롭지만 세밀한 설정을 할 수 있다.
특정 메서드만 변경하는 것도 용이하다.

proxy-targer-class 속성을 true로 주면 클래스 프록시 설정도 할 수 있다.
어노테이션은 클래스에 부여해야 클래스 프록시를 적용할 수 있다.
클래스 프록시는 final 클래스를 적용할 수 없다.
클래스 프록시를 적용하면 모든 public 메서드에 트랜잭션이 적용되어 불필요한 메서드에도 트랜잭션이 적용될 수 있음을 알아야 한다.

트랜잭션 속성

선언적 트랜잭션은 여섯가지 속성을 가지고 있다.

  1. 트랜잭션 전파(propagation)
    트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법을 결정하는 속성이다.
    • REQUIRED
      디폴트 속성으로 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
    • SUPPORTS
      이미 시작된 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 진행한다.
    • MANDATORY
      이미 시작된 트랜잭션이 있으면 참여하고 없으면 예외가 발생된다. 혼자서는 트랜잭션이 실행될 수 없다.
    • REQUIRES_NEW
      항상 새로운 트랜잭션을 시작한다.
      JTA 트랜잭션 매니저를 사용하면 트랜잭션 보류가 가능하도록 한다.
    • NOT_SUPPORTED
      이미 시작된 트랜잭션이 있으면 보류하고 트랜잭션을 사용하지 않는다.
    • NEVER
      트랜잭션을 사용하지 않도록 강제한다. 이미 있으면 예외가 발생된다.
    • NESTED
      이미 진행 중인 트랜잭션이 있으면 그 안에 새로운 트랜잭션을 만드는 것이다. 중첩된 트랜잭션은 부모의 커밋 롤백에는 영향을 받지만 자신의 커밋 롤백은 부모에게 영향을 끼치지 않는다.
  2. 트랜잭션 격리수준(isolation)
    동시에 여러 트랜잭션이 실행될 때 트랜잭션의 작업 결과를 다른 트랜잭션에게 어떻게 노출할지 결정하는 것이다.
    • DEFAULT
      db나 데이터 액세스 기술의 기본 설정을 따른다.
      대부분의 DB는 READ_COMMITTED를 기본 격리수준으로 갖는다.
    • READ_UNCOMMITTED
      가장 낮은 격리수준으로 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출된다.
    • READ_COMMITTED
      가장 많이 사용되는 격리수준으로 커밋되지 않은 정보는 읽을 수 없다. 하지만 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있어 다시 읽으면 다른 정보일 수 있다.
    • REPEATABLE_READ
      하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없다. 하지만 새로운 로우를 추가하는 것은 제한되지 않아 새로운 로우가 발견될 수 있다.
    • SERIALIZABLE
      가장 강력한 격리수준으로 동시에 같은 테이블의 정보를 접근할 수 없다. 하지만 성능이 떨어져 극단적인 상황에만 사용해야 한다.
  3. 트랜잭션 제한시간(timeout)
    트랜잭션에 제한시간을 초 단위로 지정할 수 있다.
    이 기능을 지원하지 않는 일부 트랜잭션 매니저는 예외가 생길 수도 있다.

  4. 읽기전용 트랜잭션
    트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 방지하기 위해 사용한다.
    트랜잭션을 준비하면서 읽기전용 속성이 트랜잭션 매니저에 전달되면 그에 따라 매니저가 적절한 작업을 수행하는데 일부 매니저는 속성을 외시하기도 하므로 주의해야 한다.

  5. 트랜잭션 롤백 예외
    런타임 예외가 발생하면 롤백한다. 체크예외나 그냥 넘어가면 커밋한다.
    체크 예외를 롤백 대상으로 삼고 싶으면 특정 exception을 지정해 사용할 수 있다.

  6. 트랜잭션 커밋 예외 롤백 대상인 런타임 예외를 트랜잭션 커밋 대상으로 지정해줄 수 있다.
  • ORM과 비ORM DAO를 함께 사용할 때
    영속성 컨텍스트를 사용하기 때문에 발생하는 문제가 있다. JPA의 캐시에만 있고 DB에는 반영되지 않은 결과가 존재하는 것이다.
    이렇게 JPA의 1차 캐시에 저장됐지만 DB에는 반영되지 않은 엔티티가 있다면 바로 JDBC를 이용해서는 안된다. flush를 하고 진행해야 한다.

[JPA] 트랜잭션

ACID

  • 원자성: 트랜잭션 내에서는 하나의 작업인 것처럼
  • 일관성: 일관성 있는 데이터베이스 상태
  • 격리성: 동시 실행되는 트랜잭션 간 서로 영향을 미치지 않도록
  • 지속성: 트랜잭션이 성공적으로 끝나면 ㅕㄹ과 영원히 기록

격리수준

격리성을 보장하려면 트랜잭션을 거의 차례대로 실행해야 하는데 그럼 동시성 처리의 성능이 나빠지므로 격리수준을 4단계로 나누어 정의했다.

  • READ UNCOMMITTED: DIRTY READ, NON-REPEATABLE READ, PHANTOM READ
  • READ COMMITTED: NON-REPEATABLE READ, PHANTOM READ
  • REPEATABLE READ: PHANTOM READ
  • SERIALIZABLE

격리 수준에 따른 문제는 세가지가 있다.

  • DIRTY READ
    트랜잭션1이 데이터를 수정하고 있는데 커밋하지 않아도 트랜잭션2가 수정 중인 데이터를 조회할 수 있다.
  • NON-REPEATABLE READ(반복 불가능한 읽기)
    트랜잭션1이 데이터를 조회하는데 트랜잭션2가 데이터를 수정하고 다시 조회하면 수정된 데이터가 조회된다.
  • PHANTOM READ
    트랜잭션1이 조회했을 땐 하나였는데 트랜잭션2가 추가해서 트랜잭션1이 다시 조회하면 데이터가 하나 추가되어 조회된다.

낙관적 락과 비관적 락

  • 낙관적 락
    트랜잭션 대부분은 충돌하지 않는다고 낙관적으로 가정하는 것
    DB의 락 기능이 아니라 JPA의 버전 관리 기능을 사용한다.
    트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.
    LockModeType: OPTIMISTIC, OPTIMISTIC_FORCE_INCREMENT
  • 비관적 락
    트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 거는 것
    DB의 락 기능을 사용한다. (select for update)
    LockModeType: PESSIMISTIC_READ, PESSIMISTRIC_WRITE, PESSIMISTIC_FORCE_INCREMENT

  • NONE
    락을 걸지 않는다.
  • READ(=OPTIMISTIC)
  • WRITE(=OPTIMISTIC_FORCE_INCREMENT)

[Database] Transaction

트랜잭션이란?

  • 왜, 어떤 상황에 필요한지
  • 트랜잭션 작동방법은 에이든, 격리수준은 예지니어스

스프링에서 트랜잭션의 적용 방법

  • JDBC에서
  • JPA에서
  • 경계 설정을 어떻게 해야 하는가

  • 트랜잭션 매니저
  • XML 등록
  • 어노테이션 @Transactional 동작 방법
  • 테스트에서의 트랜잭션