관심사의 분리란 한 객체가 너무 많은 역할과 책임을 지면 안된다는 의미로 객체지향 설계의 근간이 되는 철학중 하나이다.
갑자기 이 이야기를 왜 하는가?
우린 앞에서 JDBC
기술을 이용해 DB 와 연동을 하고 트랜잭션도 처리해보았다. 하지만 보다시피 코드는 try-catch
문들이 덕지덕지 붙어있고 중복된 코드들이 널려있으며 무엇보다 비즈니스 로직이 중심이 돼야하는 Service
객체에 DB연동과 트랜잭션을 위한 전처리 코드들이 대부분을 차지했다.
※ 비즈니스 로직이 Connection
까지 생성하는걸 확인 할 수 있다.
이런경우 만약 JDBC가 아니라 JPA로 트랜잭션을 처리하게된다면 비즈니스로직을 몽땅 수정해야하는 상황이 벌어진다.
어플리케이션의 비즈니스 로직은 특정 기술에 종속되어서는 안된다. 기술은 언제나 빠르게 바뀌지만 서비스는 그렇지 않기 때문이다. 그래서
DB를 바꾸더라도,
JDBC에서 JPA로 갈아타더라도,
템플릿이 바뀌더라도
최대한 비즈니스 로직엔 손을 댈 필요가 없어야한다.
즉,
Service
계층은 최대한 순수 자바 코드로 구현이 되어있어야한다.
이제 부터 Service
는 매니저를 하나 고용하여 이 친구한테 커넥션 생성/소멸, 커밋, 롤백을 시킬것이다.
이 녀석의 이름은 바로 PlatformTransactionManager
이다. 이 객체는 스프링에서 제공해주고 관리하는 객체로 자동으로 Bean
에 등록되어있다. 그래서 사용시 @Autowired
만 해주면 된다. (물론 정확한 구현체는 개발자가 지정해줘야한다. config
파일에서 지정해줘야한다. 다만 스프링부트를 사용하면 좀더 편리하게 설정가능하다)
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
PlatformTransactionManager
는 보다시피 인터페이스며
getTransaction()
,
commit()
,
rollback()
이 추상화 되어있다.
PlatformTransactionManager
인터페이스를 다양한 기술들로 구현이 되어있으며 추상화 덕분에 JDBC 와 JPA 를 동시에 사용하는것도 가능하다.
새 트랜잭션의 시작일 경우 새로운 커넥션을 하나 만들고,
TransactionSynchronizationManager
에 컨넥션을 전달해준다. -> 이건 곧바로 뒤에 설명이 나온다.
사실 Service
가 Connection
을 직접 생성했던데는 나름의 이유가 있었다. 원래는 커넥션생성의 책임은 Repository
가 가지고 있었다. 하지만 트랜잭션의 경우 하나의 커넥션에서 여럿의 업데이트가 동시에 수행되어야 한다.
그래서 만일 Repository
에서 이러한 사실을 모른채 그냥 한 번 Update
할 때마다 커넥션을 새로 생성해버린다면 트랜잭션이 실패하게 되버린다.
(사실 트랜잭션에 실패하면 롤백을 해버리면 되는데 이 경우 롤백을 하면 또 그것대로 문제가 생기기 때문에 '트랜잭션이 꼬여버린다~' 좀 더 어울린것 같기도하다.)
즉,
Service
의 관심사 분리를 위해선 트랜 잭션의 동기화를 담당하는 객체가 또 따로 필요한데 이 녀석이 바로TransactionSynchronizationManager
이다.
앞에서 잠깐 언급했듯이 트랜잭션매니저
는 새로운 커넥션을 생성한다음, 트랜잭션을 담아서 트랜잭션동기화매니저
에게 전달을 해주었다.
그러면 트랜잭션동기화매니저
는 전달받은 커넥션들을 고이 간직하고 있게되는데 문제는 DB는 알다시피 한 사람만 사용하지않는다. 동시에 여러사람들이 데이터를 저장하고 수정 하므로 멀티스레딩이 필요하다.
즉, 정확히 말하면 모든 커넥션들은 각자의 쓰레드를 할당받고 있고 쓰레드들이 저장되어 있는셈이다. 이때 트랜잭션동기화매니저
는 여러개의 쓰레드를 보관하고 있으므로 각각 커넥션마다 구분을 해줘야한다. 그래서 사용되는것이 TreadLocal
이다.
ThreadLocal
은 Map
으로 구현되어있다.
트랜잭션동기화매니저
에 보관된 커넥션들은 Repository
에서 꺼내 사용하게되는데 이때 꺼내는것은 DataSourceUtils
을 통해 꺼낸다.
물론 DataSourceUtils
는 트랜잭션동기화매니저
로 부터 전달받은 커넥션을 반환한다.
트랜잭션이 시작되면 트랜잭션매니저
가 새로운 Connection
을 하나 생성한다음 트랜잭션동기화매니저
에게 전달해준다.
트랜잭션동기화매니저
은 쓰레드로컬 에 커넥션을 저장한다.
트랜잭션이 시작 되고 Service
는 트랜잭션에 필요한 Reposiotry
의 메서드들을 호출한다.
이때 커넥션은 한 개의 커넥션으로 유지가 되며 그러기 위해서는 DataSourceUtils
로부터 커넥션을 받아서 사용해야한다.
트랜잭션이 종료된 경우
트랜잭션 동기화 매니저로부터 동기화된 커넥션을 다시 불러온다. (transactionStatus
에 저장되어있음) 그리고 커밋 혹은 롤백을 실행한다.
전체 리소스를 정리한다.
트랜잭션동기화매니저
는 쓰레드로컬을 clear 시키고,
커넥션
의 경우 다시 autoCommit 모드로 설정한다음 커넥션풀로 반환해준다.
얼추 관심사의 분리는 해결이 된것같아 보인다. 하지만 아직 코드의 중복이 해결되지 않았는데 이건 다음 장에 설명할 TransactionTemplate
에서 해결해준다.(과연?!)
본 포스트는
김영한의 스프링 DB 1편 - 데이터 접근 핵심 원리 를 보고 정리했습니다.