기술스택
- Spring Boot 3.3.5
- Java 17
- H2 Database
1. 개요
문제점
전 포스팅에 만들었던 코드를 보자. 전체 내용은 여기 를 클릭하면 된다.
MemberServiceV2.java
@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(); // 실패시 롤백
e.printStackTrace();
throw new IllegalStateException(e);
}finally {
release(con);
}
}
private static void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); // 커넥션 풀 고려
con.close();
}catch (Exception e) {
log.info("error", e);
}
}
}
private static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")){
throw new IllegalStateException("이체중 예외 발생");
}
}
}
현재 서비스 계층인 MemberServiceV2.java 에서 DataSource, Connection, SQLException과 같은 JDBC 기술에 의존하고 있다. 만약 JDBC에서 JPA와 같은 다른 기술로 바꾸게 된다면 어떻게 될까? 바로 서비스 코드를 모두 함께 변경해야 한다.
서비스 코드가 한개라면 상관이 없겠지만 수백 수천개가 된다면....
문제 정리
지금 이코드의 문제를 정리하자면 크게 세가지로 정리할 수 있다.
- 트랜잭션 문제
- 예외 누수 문제
- JDBC 반복 문제
1. 트랜잭션 문제
- 서비스 계층 코드에 JDBC 구현 기술이 서비스 계층에 누수되는 문제
- 트랜잭션 동기화 문제
- 코드 반복이 많은 트랜잭션 적용 반복 문제
2. 예외 누수
- JDBC 구현 기술 예외가 서비스 계층으로 전파되는 문제
- SQLException은 JDBC 전용 기술이다. 향후 다른 데이터 접근 기술을 사용하면 그에 맞는 다른 예외로 변경해야 한다.
3. JDBC 반복 문제
- try, catch, finally와 같은 유사한 코드 반복이 많은 문제
스프링은 서비스 계층을 순수하게 유지하면서, 이러한 문제들을 해결할 수 있는 기술들을 제공한다.
2. 트랜잭션 추상화
트랜잭션 추상화 개요
이 문제를 해결할려면 트랜잭션 기능을 추상화 하면된다. 단순하게 생각하면 다음과 같은 인터페이스를 만들어서 사용하면 된다.
트랜잭션 추상화 인터페이스
public interface TxManager {
begin();
commit();
rollback();
}
그리고 다음과 같이 TxManager 인터페이스 기반으로 각각의 기술에 맞는 구현체를 만들면 된다.
스프링의 트랜잭션 추상화
스프링은 이미 이런 고민을 다 해두었다. 우리는 굳이 인터페이스와 구현 클래스를 만들 필요 없이 스프링이 제공하는 트랜잭션 추상화 기술을 사용하면 된다. 심지어 데이터 접근 기술에 따른 구현체도 대부분 만들어두어서 가져다 사용하기만 하면 된다.
스프링의 트랜잭션 추상화 핵심은 PlatformTransactionManager 이다.
PlatformTransactionManager.java
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
3. 트랜잭션 동기화
스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 한다.
- 트랜잭션 추상화
- 리소스 동기화
스프링은 트랜잭션 동기화 매니저를 제공한다. 이것은 쓰레드 로컬(ThreadLocal)을 사용해서 커넥션을 동기화 해준다. 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다.
트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있다. 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.
4. 트랜잭션 문제 해결 - 트랜잭션 AOP 이해
프록시를 도입하기전에는 서비스의 로직에서 트랜잭션을 직접 시작한다.
서비스 계층의 트랜잭션 사용 코드 예시
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new
DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
프록시를 사용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.
트랜잭션 프록시 적용 후 서비스 코드 예시
public class Service {
public void logic() {
//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음
bizLogic(fromId, toId, money);
}
}
스프링이 제공하는 트랜잭션 AOP
스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할 수 있다. 스프링 부트를 사용함녀 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다. 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.
@Transactional를 사용한 MemberServiceV3_3.java
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
// 이하 생략...
}
이제 테스트를 진행 해보자.
@Slf4j
@SpringBootTest
class MemberServiceV3_3Test {
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
@Autowired
private MemberServiceV3_3 memberService;
@Autowired
private MemberRepositoryV3 memberRepository;
@TestConfiguration
static class TestConfig{
@Bean
DataSource dataSource(){
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource());
}
@Bean
MemberRepositoryV3 memberRepositoryV3(){
return new MemberRepositoryV3(dataSource());
}
@Bean
MemberServiceV3_3 memberServiceV3_3(){
return new MemberServiceV3_3(memberRepositoryV3());
}
}
@Test
@DisplayName("이체중 예외 발생")
void accountTransferEx() throws SQLException {
// given
Member memberA = new Member(MEMBER_A, 10000);
Member memberEx = new Member(MEMBER_EX, 10000);
memberRepository.save(memberA);
memberRepository.save(memberEx);
// when
assertThatThrownBy(()-> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
.isInstanceOf(IllegalStateException.class);
// then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberEx.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(10000);
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
}
테스트가 성공하는 것을 볼 수 있다.
5. 트랜잭션 문제 해결 - 트랜잭션 AOP 정리
- 프로세스 호출: 클라이언트, 테스트 케이스 또는 인터페이스 호출이 발생.
- 트랜잭션 매니저: 스프링 컨테이너에 등록된 트랜잭션 매니저가 동작.
- 트랜잭션 시작: transactionManager.getTransaction()을 통해 트랜잭션 매니저를 통해 트랜잭션이 시작.
- 데이터소스 연결 생성: 데이터소스를 통해 커넥션이 생성되고, 자동 커밋을 비활성화 (con.setAutoCommit(false)).
- 커넥션 보관: 커넥션이 보관됨.
- 트랜잭션 동기화 매니저: 트랜잭션 동기화를 위한 커넥션이 관리.
- 보관된 커넥션 획득: 동기화된 트랜잭션 커넥션을 통해 커넥션을 획득.
- 실제 서비스 호출: 서비스 로직이 호출되며, 트랜잭션 처리 로직을 통해 데이터 액세스 로직에 전달.
- 트랜잭션 처리 종료: 트랜잭션 처리 완료 후 AOP 프록시를 통해 트랜잭션이 종료.
'JDBC' 카테고리의 다른 글
[JDBC] 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2024.11.12 |
---|---|
[JDBC] 자바 예외 이해 (1) | 2024.11.09 |
[JDBC] 트랜잭션 이해 (0) | 2024.11.08 |
[JDBC] 커넥션풀과 데이터소스 이해 (0) | 2024.11.07 |
[JDBC] JDBC 이해 (0) | 2024.11.07 |