토비의 스프링

4 minute read

[5장] 서비스 추상화

[5.2] 트랜잭션 서비스 추상화

트랜잭션은 더 이상 나눌 수 없는 단위 작업
Atomic 원자성, Consistency 일관성, Isolation 독립성, Durability 지속성
중간에 문제가 발생해 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 돌아가야 한다.

  • 트랜잭션 경계설정 DB는 자체적으로 트랜잭션을 지원한다. 다중 로우의 수정이나 삭제 요청을 했을 때 일부만 되고 일부는 안되는 경우는 없다. 따라서 하나의 SQL 명령을 실행하면 DB가 트랜잭션을 보장해준다.
    하지만 여러 SQL 명령을 하나의 트랜잭션으로 보고싶은 경우는 어찌해야 할까?
    출금이 일어날때 내 계좌에서 출금하고 상대 계좌에 입금하는 일이 하나의 일로 일어나지만 두개의 SQL 명령이 일어나는 것과 같다.

이렇게 두개의 명령을 하나의 트랜잭션으로 보고 싶을 때는 두번째 SQL 실행 시에 실패했다 하더라도 첫번째 SQL에 실행한 내용을 모두 원래대로 되돌려야 한다. 이것을 트랜잭션 롤백이라고 한다. 반대로 두개의 명령을 모두 성공적으로 수행했다면 트랜잭션 커밋이라고 하여 DB에 반영한다.

  • JDBC 트랜잭션의 트랜잭션 경계설정
    어플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 지정하는 것, 즉, 하나의 논리적인 단위의 크기를 지정하는 것이 트랜잭션 경계 설정이다. 이는 아주 중요한 작업이다.
    JDBC에서 트랜잭션은 다음과 같이 사용된다.
    ```java Connection c = datasource.getConnection(); //DB 연결

c.setAutoCommit(false); //트랜잭션 시작 try { PreparedStatement st1 = c.prepareStatement(“update users …”); st1.executeUpdate();

PreparedStatement st2 = c.prepareStatement("delete users ...");
st2.executeUpdate();

c.commit(); //트랜잭션 커밋 } catch(Exception e) {   c.rollback(); //트랜잭션 롤백 }

c.close(); //DB 연결 끊기


JDBC에서는 트랜잭션은 자동커밋 옵션을 false로 바꿔주면서 설정하고 실행한다.  
commit이나 rollback이 실행될 때까지가 하나의 트랜잭션이 된다.  
이렇게 하나의 DB connection 안에서 만들어지는 트랜잭션을 **로컬트랜잭션**이라고 한다.  
JdbcTemplate도 템플릿 메서드가 실행될 때마다 템플릿 메서드 안에서 connection을 설정하고 그 안에서 트랜잭션을 시작하고 모든 작업을 마치면 트랜잭션을 커밋/롤백하고  connection을 닫는 과정으로 실행된다.  

<img width="342" alt="스크린샷 2021-07-24 오후 10 58 03" src="https://user-images.githubusercontent.com/43775108/126870718-82533c48-f80e-4c76-8a8b-f7621d8a23eb.png">

하지만 세번의 update을 하나의 트랜잭션으로 보고싶은 경우에도 하나의 jdbc 템플릿마다 하나의 트랜잭션이 설정되기 때문에 db의 결과도 똑같이 남는다. 이 경우는 어떻게 해결해야 할까?  
- 비즈니스 로직에서의 트랜잭션 경계설정  
DB를 직접 다루는 DAO 메서드 안으로 세번의 update를 실행하는 메서드의 내용을 옮겨서 이를 해결할 수 있다.  
하지만 비즈니스 로직과 db 로직이 한데 묶이는 것은 옳지 않다.  
기껏 jdbc template을 사용해 분리한 비즈니스 로직을 한데 섞는 것이나 다름 없다.  

이것은 DAO로직을 사용하는 SERVICE 비즈니스 로직으로 트랜잭션의 경계설정을 옮겨와 트랜잭션을 다시 설정할 수 있다.  
dao의 로직에 파라미터로 connection을 전달해주어야 한다.  
`update(Connection c, User user)`  
하지만 JdbcTemplate을 사용할 수 없고 service와 dao 메서드가 완벽 분리될 수 없다는 단점이 있다. 어떻게 해결할 수 있을까?  

- 트랜잭션 동기화  
스프링은 트랜잭션 동기화를 통해 이 문제를 해결할 것을 제안한다.  
이는 connection을 특정 장소에 보관해두고 dao 메서드가 가져다 쓰도록 하는 것이다.  
트랜잭션이 종료되면 동기화를 마친다. 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 connection 오브젝트를 저장하고 관리하므로 멀티스레드 환경에서도 충돌이 나지 않는다.  
<img width="343" alt="스크린샷 2021-07-24 오후 10 58 48" src="https://user-images.githubusercontent.com/43775108/126870738-e742ae61-b8fb-4698-ac52-d32d10579c67.png">  

이 트랜잭션 동기화 저장소는 어떻게 적용할 수 있을까?  

```java
private Datasource dataSource;

public void upgrade() throws Exception {
  TransactionSynchronizationManager.initSynchronization(); //트랜잭션 동기화 작업 초기화
  Connection c = DatasourceUtils.getConnection(dataSource);  
  c.setAutoCommit(false); //DB 커넥션 생성과 동기화를 하는 유틸 메서드를 통해 트랜잭션 시작

  try {
    update(user);
    c.commit();
  } catch(Exception e) {
    c.rollback();
    throw e;
  } finally {
    DataSourceUtils.releaseConnection(c, dataSource); //DB 연결 닫기
    TransactionSynchronizationManager.unbindResource(this.dataSource);
    TransactionSynchronizationManager.clearSynchronization(); //동기화 종료
  }
}

dataSource에서 직접 connection을 가지고 와도 되지만 유틸 메서드는 Connection을 생성해주고 트랜잭션 동기화에 사용할 수 있도록 저장소에 바인딩도 해주기 때문에 사용된다.
DAO를 사용하는 모든 JDBC 작업은 메서드에서 만든 Connection 오브젝트를 사용하고 같은 트랜잭션에 참여하게 된다. 작업을 마치면 커밋하고 커넥션을 닫는 것이다.

  • JdbcTemplate과 트랜잭션 동기화
    JdbcTemplate은 메서드가 호출되면 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB Connection이나 트랜잭션이 없는 경우엔 직접 Connection을 생성하고 종료한다.
    위와 같이 트랜잭션 동기화를 시작해놓았다면 DB Connection을 만들지 않고 트랜잭션 동기화 저장소에 있는 Connection을 가져와 사용한다.

트랜잭션이 필요없다면 바로 호출해도 되고, 위 코드처럼 외부에서 만들고 트랜잭션 동기화를 해줄 수도 있다.
하지만 하나의 트랜잭션에서 여러 개의 DB에 데이터 삽입이 필요하다면?

  • 트랜잭션 서비스 추상화
    로컬 트랜잭션은 하나의 DB에 종속되므로 글로벌 트랜잭션을 이용해야 한다.
    글로벌 트랜잭션은 트랜잭션 매니저를 통해 여러 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다.
    그래서 자바는 JDBC 외에도 JTA(Java Transaction API)를 제공한다.
    애플리케이션에서는 기존의 방법대로 DB는 JDBC가 관리하고 트랜잭션을 JTA에게 위임하는 것이다.
    스크린샷 2021-07-24 오후 10 59 43 JTA를 이용한 트랜잭션의 코드는 다음과 같다.
InitialContext ctx = new InitialContext();
UserTransaction tx = (UserTransactiona)ctx.lookup(USER_TX_JNDI_NAME); //JNDI를 이용해 userTransaction 오브젝트 가리킴  

tx.begin();
Connection c = dataSource.getConnection();
try {
  user.update();
  tx.commit();
} catch(Exception e) {
  tx.rollback();
  throw e;
} finally {
  c.close();
}

하지만 이또한 트랜잭션 API에 너무 의존적이다. 뿐만 아니라 하이버네이트와 같이 독자적인 트랜잭션 관리를 하는 dao로의 전환이 어렵다. OCP 위배
이는 트랜잭션 서비스의 추상화를 통해 해결할 수 있다.
트랜잭션 추상화는 다음과 같이 할 수 있다.
그림 5-6

public void upgrade() {
  PlatformTransactionManager transactionManager = new DataSourceTransactionManager(datasource);

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

  try {
    update(user);
    transactionManager.commit(status);
  } catch(Exception e) {
    transactionManager.rollback(status);
    throw e;
  }
}

스프링이 제공하는 트랜잭션 추상 인터페이스는 PlatformTransactionManager이고, 로컬 트랜잭션은 DataSourceTransactionManager를 사용할 수 있다.

JDBC와 달리 getTransaction() 메서드만 호출하면 트랜잭션을 가져오는 것으로 Connection 생성 없이 트랜잭션을 시작할 수 있다.
파라미터로 넘기는 DefaultTransactionDefinition은 트랜잭션 속성을 가지고 있다.
이렇게 시작된 트랜잭션은 TransactionStatus 타입의 변수에 저장된다.

JTA를 사용할 때는 PlatformTransactionManager txManager = new JTATransactionManager(), JPA를 사용할 때는 JPATransactionManager를 사용할 수 있다.

트랜잭션 매니저를 당연히 빈으로 분리할 수도 있다.
빈으로 등록한 것을 설정 파일에 추가할 수도, DI로 주입받을 수도 있다.
이렇게 함으로써 SRP도 철저히 지킬 수 있게 되었다.
그림 5-7 계층과 책임의 분리