순수한 서비스 계층
핵심비지니스 로직이 들은 계층임으로 최대한 순수하게 유지해야한다.
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false);//트랜잭션 시작
//비즈니스 로직
bizLogic(con, fromId, toId, money);
con.commit(); //성공시 커밋
} catch (Exception e) {
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(con, fromId);
Member toMember = memberRepository.findById(con, toId);
memberRepository.update(con, fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(con, toId, toMember.getMoney() + money);
}
현재는 비지니스 로직보다 JDBC 트랜잭션 로직이 훨씬 길고 많다. JPA로 바꾸게 되면 다 바꿔야한다.
트랜잭션 문제 = JDBC 로직들을 DAO (Repository)에 다 모아 놓았지만 트랜잭션을 적용하면서 결국 서비스 계층에 JDBC 구현 기술의 누수가 발생했다.
또한 try,catch,finally 같은 반복이 많이 일어난다.
예외 누수문제 = SQL Exception 이 체크 예외라 DAO에서 처리하거나 서비스 까지 던져야한다.
또한 JDBC 종속적인 예외 클래스이다.
다양한 데이터 접근 기술이 데이터 접근 계층 말고도 서비스 계층까지 의존하고 있다.
jdbc 에서 jpa 로 변경하게 된다면 서비스 계층의 tx 로직까지 다 변경해야한다.
트랜잭션 추상화
실제 구현체가 변경이 있어도 서비스 레이어는 TxManager interface만 참조하고 있기에 코드 수정이 필요 없다.
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
스프링에서도 트랜잭션을 추상화를 위해 존재하는 interface가 존재한다.
이미 스프링에서 트랜잭션 인터페이스를 구현한 여러 종류의 데이터 접근 기술을 구현해놓았다.
트랜잭션 동기화
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다. 즉 같은 커넥션을 동기화 하면 된다. (지금까진 parameter로 넘겨주는 방식)
ThreadLocal에 커넥션을 보관하게 된다. 더 이상 parameter로 커넥션을 전달하지 않아도 된다.
동작방식
1. 트랜잭션 시작 요청 -> 트랜잭션 매니저가 Datasource를 통해 (setAutocommit , max.. 등등 설정) Connection을 얻어온후에 트랜잭션 시작후에 ThreadLocal에 보관하게 된다.
2. 리포지토리에서 실제 db에게 요청을 날릴때 자신이 속한 스레드로컬의 Connection 을 가지고 요청을 날리게 됨
3. 트랜잭션 종료 요청,commit,rollback -> 트랜잭션 매니저가 요청이 들어온 스레드의 스레드로컬의 Connection을 가지고 트랜잭션 종료 요청을 db에게 전달함(사실상 commit, rollback이 이런의미인듯) 이후 Connection.close()
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
resource 스레드 로컬에 보관하는 것을 확인할 수 있다.
예제 TM적용
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() throws SQLException {
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
TransactionSynchronizationManager를 사용하려면 DataSourceUtils를 통해 Connection 을 사용해야한다. 하지만 트랜잭션 시작 없이 시작하면
DAO에서 조회시 threadlocal이 비어있을 것임으로 새로운 커넥션을 JDBC로 붙어 얻어와서 동작하게 된다.
커넥션도 close를 직접하지 않고 DataSourceUtils #releaseConnection을 사용한다.
트랜잭션 내부에서 사용한 커넥션을 닫지 않고 추후 rollback, commit을 위해 스레드로컬을 위해 남겨둔다.
서비스 계층에서 넣어준 것이다. (요기서 tx를 시작했으니 , 시작과 동시에 Connection을 threadLocal에 넣어준다.) DAO 계층에서 Connection Close 하는것은 이상한 것이다.
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
TransactionManager에서 getTransaction을 하면 안에서 트랜잭션을 시작하게 된다.
TransactionManager는 생성자를 통해 주입받은 DataSource를 통해서 커넥션을 생성하고 TransactionSynchronizationManager 의 필드인 resource(ThreadLocal) 에 생성한 커넥션을 저장하게 된다.
예외 트랜잭션 탬플릿 적용
https://tonylim.tistory.com/352
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status) -> {
//비즈니스 로직
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
txTemplate에서 transaction begin, commit을 해주는 코드가 존재하고 이 내부에서 실제 호출할 비즈니스 로직을 주입받게 된다.
TransactionTemplate은 TransactionManager를 주입받게 된다.
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
받은 action을 , 즉 비지니스 로직을 실행 시키기만 한다.
하지만 여전히 Txtemplate도 transaction code이다. 이것조차 서비스 레이어에서 분리하고싶다.
@Transactional AOP
https://tonylim.tistory.com/358
https://tonylim.tistory.com/357
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
Advisor = BeanFactoryTransactionAttributeSourceAdvisor == (Pointcut = TransactionAttributeSourcePointcut) + (Advice = TransactionInterceptor)
이것들을 자동으로 빈으로 등록하여 제공해준다. 그러면 빈후처리기에서 @Transactional 이 있는지 reflection을 통해 확인하고 pointcut에 매칭이되면 Proxy bean을 컨테이너에 등록하게 된다.
나중에 실제 요청이 왔을 때 컨테이너에서 Advisor 빈들을 찾아보며 매칭되는 pointcut 에 advice를 적용시켜준다.
테스트를 돌릴시에 @SpringBootTest 를 해줘야 스프링을 이용해서 테스트를 진행 가능하다.
스프링부트에서는 application.properties에 적혀있는 정보를 가지고 DataSouce bean을 생성해서 주입해준다.
또한 PlatformTransactionManger 를 구현한 구체클래스를 bean으로 자동생성해서 주입해준다. 이때 뭘 등록할지 현재 등록된 라이브러리를 보고 판단한다.
'WEB > Spring' 카테고리의 다른 글
스프링 DB 2편 1) 스프링 트랜잭션 이해 (0) | 2022.06.20 |
---|---|
스프링 핵심 원리 - 고급편 9) 실전, 실무 주의 사항 (0) | 2022.06.13 |
스프링 핵심 원리 - 고급편 8) 포인트컷 (0) | 2022.06.13 |
스프링 핵심 원리 - 고급편 7) @Aspect AOP , 스프링 AOP 개념 (0) | 2022.06.12 |
스프링 핵심 원리 - 고급편 6) 빈 후처리기 (0) | 2022.06.09 |