Go, Vantage point
가까운 곳을 걷지 않고 서는 먼 곳을 갈 수 없다.
Github | https://github.com/overnew/
Blog | https://everenew.tistory.com/
티스토리 뷰
* 김영한님의 스프링 DB 1편 강좌를 수강하며 정리한 글입니다. *
이전 글에서는 트랜젝션과 DB 락에 대해서 공부하였다.
애플리케이션 3 계층 구조
1. 프리젠테이션 계층
클라이언트의 진입점으로 UI와 관련된 처리 한다.
클라이언트의 요청을 검증하고 응답한다.
특정 기술에 의존한다.
2. 비즈니스(서비스) 계층
비즈니스 로직을 담당한다.
특정 프레임워크 기술에 종속적이지 않아야 프레젠테이션과 데이터 계층의 기술 변경에도 코드가 유지될 수 있다.
3. 데이터 계층
DB 서버에 접근을 담당한다.
특정 구현 기술에 의존한다.
이전 글에서 계좌 이체의 서비스 로직은 커넥션과 트랜젝션의 생성, 유지하는 코드와는 분리를 시켰다.
하지만 여전히 전체 비즈니스 계층에는 트랜잭션을 위한 JDBC 코드가 누수되고 있다.
이처럼 비즈니스 로직과 JDBC 기술이 섞여있으면 변화에 대처하기 힘들고 유지보수도 까다롭다.
또한 같은 트랜젝션의 유지를 위해 Connection을 매개변수로 넘기면서 유지하고 있다.
거기에 트랜젝션의 시작과 끝에 예외처리의 코드가 반복된다.
CRUD 구현 메서드들도 순수 JDBC 처리를 위해 많은 코드가 반복되고 있다.
이런 모든 문제들을 스프링으로 해결해보자.
트랜잭션 추상화
트랜잭션은 구현 기술마다 제공하는 방법이 다르다.
우리가 사용한 JDBC는 자동 커밋 취소를 위해서 con.setAutoCommit(false)를 호출했지만,
JPA는 tarnsaction.begin()을 사용한다.
이처럼 구현 기술의 변경은 트랜잭션 처리 코드를 모두 바꿔야 한다.
그러므로 스프링의 트랜잭션 추상화 인터페이스 사용해서 원하는 구현체를 DI를 주입하는 스프링의 기능을 이용하자.
스프링의 트랜잭션 추상화 인터페이스는 PlatformTransactionManager이다.
각 기술들의 구현체 또한 이미 제공된다.
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;
}
스프링은 트랜잭션 동기화 매니저를 통해 트랜잭션의 종료까지 커낵션을 동기화(유지) 해주는 기능을 제공해준다.
쓰레드는 프로세스의 공유 데이터를 자유롭게 접근하면서 내부에는 자신만의 로컬 스택(저장소)를 갖는다.
이는 스프링 빈의 싱글톤 인스턴스들과 다르게 공유되지 않는 각자의 값을 가지는데, 이를 쓰레드 로컬이라 한다.
이는 멀티 쓰레드 환경에서 동시성 문제로 부터 안전하게 값을 저장할 수 있다.
동기화 매니저는 이 쓰레드 로컬을 사용하여 커낵션을 저장하기 때문에 커낵션을 동기화한다.
동작의 순서는 다음과 같다.
비즈니스 로직 -> 트랜잭션 매니저가 DataSource로 커낵션을 만들어 트랜잭션 시작 -> 커낵션을 동기화 매니저에 저장 -> 데이터 계층은 커낵션을 동기화 매니저에서 꺼내 사용 -> 트랜잭션 종료 시, 매니저를 통해 커낵션 종료
JDBC의 PlatformTransactionManager의 구현체는 DataSourceTransactionManager(dataSource)이다.
이제 transactionManager.getTransection()을 통해 트랜잭션을 획득할 수 있다.
이 메서드가 호출되면 트랜잭션 매니저가 커낵션을 DataSource를 통해 획득하고 동기화 매니저에 저장한다.
트랜잭션 매니저를 사용해 커낵션을 이용하려면 DataSourceUtils를 사용하면 된다.
DataSourceUtils의 getConnection()은 트랜잭션 동기화 매니저를 사용한다.
만약 관리 중인 커낵션이 있다면 이를 반환하고, 없다면 새로운 커낵션을 생성해 반환한다.
기존의 커낵션은 비즈니스 로직에서 생성되었지만 데이터 계층에서 닫을 위험이 있었다.
DataSourceUtils의 releaseConnection()은 트랜잭션이 동기화 매니저에 의해 관리되고 있다면 종료하지 않는다.
(트랜잭션이 끝나면 매니저를 통해 종료될 것임)
만약 매니저의 관리하가 데이터 계층에서 생성된 것이므로 종료시킨다.
트랜잭션의 시작과 종료가 구현체에서 자동으로 처리되어 더 이상 오토커밋 설정과 커낵션 풀로의 반납과 같은 release처리를 직접 할 필요가 없다.
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//트랜잭션의 시작 오토커밋 false설정, 상태정보를 받아옴
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(toId, money, fromId);
transactionManager.commit(status); //세션에 commit 명령 전달, 커낵션을 통해 commit 실행
}catch (Exception e){
transactionManager.rollback(status); //전체 리소스 정리, 오토커밋 true로, 커낵션 종료 및 반납
throw new IllegalStateException(e);
}
}
각각의 CRUD로직은 이제 DataSourceUtils을 사용해 동기화 매니저의 커낵션을 이용한다.
private void close(Connection con, Statement stmt, ResultSet rs){
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
//트랜잭션 동기화를 위해 DataSourceUtils
DataSourceUtils.releaseConnection(con,dataSource);
//JdbcUtils.closeConnection(con); 데이터 로직이 닫으면 안됨
}
private Connection getConnection() throws SQLException {
//트랜잭션 동기화 사용을 위해 DataSourceUtils
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get Connection={}, class={}",con, con.getClass());
return con;
}
이제 CRUD는 획득한 커낵션을 통해 SQL로 데이터 로직을 실행하면 된다.
트랜잭션 추상화를 통해 구현체를 JPA나 하이퍼네이트 등으로 바꾸어도 비즈니스 로직은 영향을 받지 않는다.
트랜잭션마다 비즈니스 로직 수행을 위해 트랜잭션 실행, commit, rollback과 같은 반복된 작업들을 필요한다.
이를 템플릿 콜백 패턴으로 해결해보자.
스프링은 TransactionTemplate 라는 템플릿 클래스로 해당 기능을 제공한다.
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
//PlatformTransactionManager로 생성할 수 있음
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status) -> {
try {
bizLogic(toId, money, fromId); //비즈니스 로직 전달
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
이제 TransactionTemplate의 executeWithoutResult()를 통해 비즈니스 로직을 전달하면,
트랜잭션의 생성, commit과 rollback을 모두 정상 수행해준다.
단, 람다 함수에서는 예외를 throw할 수 없기 때문에 catch하여 다시 던져 주었다.
하지만, 트랜잭션 처리 코드와 핵심인 비즈니스 로직이 한 곳에서 사용되어 두 가지 역할을 하게 된다.
비즈니스 계층에는 순수한 비즈니스 로직만을 남기는 방법이 있을까?
AOP와 프록시의 도입
프록시는 서버와 클라이언트 간의 매개체 역할을 한다.
우리가 사용할 트랜잭션 프록시도 이와 같은 역할을 한다.
트랜잭션의 시작, 비즈니스 로직 호출, commit과 rollback 처리까지 대신 수행해 준다.
스프링의 트랜잭션 AOP를 사용하면 이를 편리하게 적용할 수 있다.
무려 @Transactional 애노테이션만 붙여도, 스프링 AOP가 인식하여 트랜잭션 프록시를 적용해준다.
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(toId, money, fromId);
}
단, 스프링이 동작해야 하므로 사용하는 DataSource와 transactionManager등을 스프링 빈으로 등록하여 주입해야 한다.
transactionManager을 등록하면 스프링의 트랜잭션 AOP가 이를 자동으로 주입받아 사용한다.
프록시의 동작방법은 @Transactional로 등록된 비즈니스 로직을 상속받아 오버라이드 하여 트랜잭션 코드를 만들어낸다. 트랜잭션 코드의 내부에서 비즈니스 로직을 호출하여 실행하는 식이다.
@Transactional 애노테이션을 선언하는 것으로 트랜잭션을 적용하는 것을 선언적 트랜잭션 관리,
이전처럼 트랜잭션 매니저나 탬플릿으로 통해 직접 관련 코드를 작성하는 것이 프로그래밍적 트랜잭션 방식 이라 한다.
사실상, 선언적 트랜잭션이 워낙 간편하기 때문이 거의 다 이를 사용한다.
DataSource와 transactionManager는 스프링 부트의 등장 이전까지는 직접 빈으로 등록해야 했지만, 스프링 부트는 개발자가 등록하지 않는다면 자동으로 등록해준다.
기본적으로 스프링 부트는 HikariDataSource를 커넥션 풀로 제공한다.
설정을 위해서는 application.properties에 설정 정보를 등록하면 된다.
트랜잭션 매니저의 구현체는 등록된 라이브러리를 토대로 자동 등록해준다.
'개발 > Spring DataBase' 카테고리의 다른 글
[Spring DB] 6. 프로필과 JdbcTemplate (0) | 2022.07.24 |
---|---|
[Spring DB] 5. 런타임 예외의 활용 (0) | 2022.07.21 |
[Spring DB] 3. 트랜잭션과 ACID, DB Lock (0) | 2022.07.18 |
[Spring DB] 2. 커넥션 풀과 DataSource (0) | 2022.07.16 |
[Spring DB] 1. JDBC의 등장 (0) | 2022.07.15 |