스프링 부트 3.2.2 버전을 기준으로 작성됨
H2 데이터베이스 Version 2.2.224 (2023-09-17)
개념 위주로 정리 실질적인 실습은 강의 자료 참조
애플리케이션 구조
비지니스 로직이 들어있는 서비스 계층이 가장 중요하다.
비지니스 로직은 최대한 변경없이 유지되어야한다.
그렇기에 특정 기술에 종속적이지 않게 개발해야하고,
이로 인하여 비지니스 로직의 유지보수와 테스트가 쉬워진다.
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
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);
}
}
트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작하는 것이 좋다.
문제는 트랜잭션을 사용하기 위해서 DataSource
, Connection
, SQLException
같은 JDBC 기술에 의존해야한다는 점이다.
JDBC -> JPA 기술로 바꾸면 서비스 코드도 모두 변경해야하고 비지니스 로직과 JDBC 기술이 섞여 있어 유지보수가 어렵다.
구현 기술마다 트랜잭션 사용 방법이 다르므로
트랜잭션을 추상화하여 해당 인터페이스를 의존하게 한다.
스프링은 이미 제공하고 있다.
PlatformTransactionManager
인터페이스다.
package org.springframework.transaction;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
getTransaction()
: 트랜잭션 시작commit()
: 트랜잭션 커밋rollback()
: 트랜잭션 롤백참고
JdbcTransactionManager
도 있다.
둘의 기능 차이는 크지 않다.
스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할
쓰레드 로컬(ThreadLocal)을 사용해서 커넥션을 동기화해준다. 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용
트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션 동기화 가능하고 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면되기에 파라미터로 커넥션 전달은 필요없다.
DataSourceUtils.getConnection()
)참고
쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여
따라서 해당 쓰레드만 해당 데이터에 접근 가능
DataSourceUtils.getConnection()
DataSourceUtils.releaseConnection()
트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지
트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫음
private final PlatformTransactionManager transactionManager
트랜잭션 매니저를 주입받는다.
DataSourceTransactionManager
구현체를 주입JpaTransactionManager
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());
getTransaction()
TransactionStatus status
을 반환 (현재 트랜잭션의 상태 정보가 포함되어 있고 커밋, 롤백할 때 필요)new DefaultTransactionDefinition()
commit(status)
rollback(status)
java.sql.SQLException
의 경우 예외파트로//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
해당 부분이 반복된다. 달라지는 부분은 비지니스 로직 뿐
템플릿 콜백 패턴을 활용하면 해결된다.
템플릿 콜백 패턴에 대한 공부는 따로 해야한다.
스프링은 TransactionTemplate
라는 템플릿 클래스 제공
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
// 응답 값이 있을 때 사용
public <T> T execute(TransactionCallback<T> action){..}
// 응답 값이 없을 때 사용
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
code
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
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);
}
});
}
비지니스 로직이 정상 수행되면 커밋
언체크 예외가 발생하면 롤백 , 그 외에는 커밋
스프링 AOP를 통해 프록시 도입하면 위의 문제를 해결 가능
프록시로 인해 트랜잭션 처리 객체와 비지니스 로직 처리 서비스 객체를 명확하게 분리 가능
스프링에서 트랜잭션 AOP @Transactional
애노테이션 제공
스프링 AOP에 대해서는 공부가 필요하다. 추후에 공부할 것
@Transactional
public void accountTransfer(String fromId, String toId, int money){
bizLogic(fromId, toId, money);
}
이로써 순수한 비지니스 로직만 남게 되었다.
@Transactional
애노테이션은 메서드나 클래스에 붙여도 된다. 클래스에 붙이면 외부에서 호출 가능한 public
메서드가 AOP 적용 대상이 된다.
// test에서 AOP 프록시 적용 확인
Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
// 결과
memberService class=class hello.jdbc.service.MemberServiceV3_3$
$EnhancerBySpringCGLIB$$.. //(프록시(CGLIB))
@Transactional
애노테이션 하나만 선언하여 편리하게 트랜잭션을 적용하는 것@Bean
DataSource dataSource() {
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
기존에는 데이터소스와 트랜잭션 매니저를 직접 스프링 빈으로 등록했어야 했다. -> 현재) 스프링 부트의 자동화
스프링 부트는 데이터소스를 스프링 빈에 자동으로 등록
자동으로 등록되는 스프링 빈 이름 : dataSource
당연하지만 개발자가 직접 등록하면 스프링부트는 자동 등록하지 않는다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
스프링 부트가 기본으로 생성하는 데이터소스는 커넥션 풀을 제공하는 HikariDataSource
이다. 커넥션 풀과 관련된 설정도 application.properties
를 통해서 지정 가능
spring.datasource.url
속성이 없으면 내장 데이터베이스(메모리 DB)를 생성하려고 시도
스프링 부트는 적절한 트랜잭션 매니저 (PlatformTransactionManager) 를 자동으로 스프링 빈에 등록
자동으로 등록되는 스프링 빈 이름 : transactionManager
트랜잭션 매니저 선택 기준은 현재 등록된 라이브러리
🔖 학습내용 출처