MemberRepository의 코드 수정 (V3)
파라미터로 커넥션을 넘기는 부분을 모두 지우고
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 {
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection = {}, class = {}", con, con.getClass());
return con;
}
이렇게 수정했다.
- DataSourceUtils.getConnection()
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 그 커넥션을 반환한다.
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없으면 새로운 커넥션을 생성해서 반환한다.
- DataSourceUtils.releaseConnection()
- 커넥션을 con.close()로 직접 닫으면 커넥션이 유지되지 않는다. 커넥션은 트랜잭션을 종료할 때까지 유지되어야 한다.
- DataSourceUtils.releaseConnection()은 커넥션을 바로 닫는 것이 아니라, 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다. 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다.
MemberService 코드 수정(V3_1)
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_1 {
//transactionManager를 주입 받기
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryWithDBV3 memberRepository;
//커뮤니티 포인트 전송
public void pointTransfer(String fromNickname, String toNickname, int point) throws SQLException {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(fromNickname, toNickname, point);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new IllegalStateException(e);
}
}
private void bizLogic(String fromNickname, String toNickname, int point) throws SQLException {
Member fromMember = memberRepository.findByNickname(fromNickname);
Member toMember = memberRepository.findByNickname(toNickname);
validation(toMember);
memberRepository.updatePoint(fromNickname, fromMember.getPoint() - point);
memberRepository.updatePoint(toNickname, toMember.getPoint() + point);
}
private void validation(Member toMember) {
if(toMember.getNickname().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
transactionManager를 주입 받아 getTransaction()으로 트랜잭션을 시작한다.
DefaultTransactionDefinition은 트랜잭션과 관련된 옵션을 지정하는 것이다.
테스트 코드
테스트 코드는 이전과 같고 달라진 점은
@BeforeEach
void before() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
USERNAME, PASSWORD);
//JDBC용 트랜잭션 매니저인 DataSourceTransactionManager를 서비스에 주입한다.
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberRepository = new MemberRepositoryWithDBV2(dataSource);
memberService = new MemberServiceV2(memberRepository, dataSource);
}
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
이 코드를 통해 서비스에 JDBC용 트랜잭션 매니저인 DataSourceTransactionManager를 주입하는 것이다.
트랜잭션 매니저의 동작 흐름
- 서비스 계층에서 transactionManager.getTransaction()으로 트랜잭션 시작
- 트랜잭션을 시작하려면 데이터베이스 커넥션이 필요하므로, 트랜잭션 매니저는 내부에서 데이터 소스를 사용해서 커넥션 생성
- 수동 커밋 모드로 변경
- 커넥션을 트랜잭션 동기화 매니저에 보관
- 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관하여 멀티 쓰레드 환경에서 안전하게 커넥션 보관 가능
- 서비스는 비즈니스 로직 수행
- Repository의 메서드들은 트랜잭션이 시작된 커넥션이 필요한데 이것을 DataSourceUtils.getConnection()으로 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 이렇게 같은 커넥션을 사용하게 되어 트랜잭션이 유지된다.
- 획득한 커넥션으로 SQL을 데이터베이스에 전달해서 실행한다.
- 비즈니스 로직이 끝나고 .commit()이나 .rollback()을 호출한다.
- 트랜잭션을 종료하려면 동기화된 커넥션이 필요한데, 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
- 획득한 커넥션을 통해 데이터 베이스에 트랜잭션을 커밋하거나 롤백한다.
- 트랜잭션 동기화 매니저를 정리하고, 자동 커밋 모드로 되돌리고, con.close()로 커넥션을 종료하거나 반환한다.
트랜잭션 try, catch, finally가 반복되는 문제 해결
스프링의 TransactionTemplate으로 문제를 해결할 수 있다.
MemberService 수정 (V3_2)
/**
* 트랜잭션 - 트랜잭션 템플릿
*/
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_2 {
//transactionManager를 주입 받기
private final TransactionTemplate txTemplate;
private final MemberRepositoryWithDBV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryWithDBV3 memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
//커뮤니티 포인트 전송
public void pointTransfer(String fromNickname, String toNickname, int point) throws SQLException {
txTemplate.executeWithoutResult((staus) -> {
try {
bizLogic(fromNickname, toNickname, point);
} catch (Exception e) {
throw new IllegalStateException(e);
}
});
}
private void bizLogic(String fromNickname, String toNickname, int point) throws SQLException {
Member fromMember = memberRepository.findByNickname(fromNickname);
Member toMember = memberRepository.findByNickname(toNickname);
memberRepository.updatePoint(fromNickname, fromMember.getPoint() - point);
validation(toMember);
memberRepository.updatePoint(toNickname, toMember.getPoint() + point);
}
private void validation(Member toMember) {
if(toMember.getNickname().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
TransactionTemplate를 사용하려면 transactionManager가 필요해서 생성자에서 transactionManager를 주입받으면서 TransactionTemplate를 생성했다.
트랜잭션 템플릿 사용 로직
public void pointTransfer(String fromNickname, String toNickname, int point) throws SQLException {
txTemplate.executeWithoutResult((staus) -> {
try {
bizLogic(fromNickname, toNickname, point);
} catch (Exception e) {
throw new IllegalStateException(e);
}
});
}
- 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드가 사라졌다.
- 기본 동작
- 비즈니스 로직이 정상 수행되면 커밋
- 언체크 예외가 발생하면 롤백, 그 외는 커밋 (체크 예외도 커밋)
- bizLogic()에서 SQLException을 던져서 처리해주기 위해 try~catch로 체크 예외를 언체크 예외로 바꿔서 던지게 함.
트랜잭션 문제 해결 - 트랜잭션 AOP
아직 서비스 계층에 순수한 비즈니스 로직만 남겨두지는 못했다. @Transactional을 사용하면 스프링이 AOP를 사용해서 트랜잭션을 편리하게 처리해준다.
AOP: 관점 지향 프로그래밍
어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각을 모듈화하는 것. 코드들을 부분적으로 나누어서 모듈화한다는 것이다. 스프링 AOP는 프록시를 이용한다.
트랜잭션이 필요한 곳이 @Transactional 애노테이션만 붙이면 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.
MemberService 수정 (V3_3)
/**
* 트랜잭션 - @Transactional AOP
*/
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_3 {
private final MemberRepositoryWithDBV3 memberRepository;
//커뮤니티 포인트 전송
@Transactional
public void pointTransfer(String fromNickname, String toNickname, int point) throws SQLException {
bizLogic(fromNickname,toNickname, point);
}
private void bizLogic(String fromNickname, String toNickname, int point) throws SQLException {
Member fromMember = memberRepository.findByNickname(fromNickname);
Member toMember = memberRepository.findByNickname(toNickname);
memberRepository.updatePoint(fromNickname, fromMember.getPoint() - point);
validation(toMember);
memberRepository.updatePoint(toNickname, toMember.getPoint() + point);
}
private void validation(Member toMember) {
if(toMember.getNickname().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
@Transactional 애노테이션만 붙이고, 트랜잭션 코드는 모두 지워 순수한 비즈니스 로직만 남게 되었다. @Transactional 애노테이션은 메서드에 붙여도 되고, 클래스에 붙여도 된다.
테스트 코드
@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 MemberRepositoryWithDBV3 memberRepository;
@Autowired
private MemberServiceV3_3 memberService;
@AfterEach
void after() throws SQLException {
memberRepository.deleteByNickname("memberA");
memberRepository.deleteByNickname("memberB");
memberRepository.deleteByNickname("ex");
}
@TestConfiguration
static class TestConfig {
@Bean
DataSource dataSource() {
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
MemberRepositoryWithDBV3 memberRepositoryWithDBV3() {
return new MemberRepositoryWithDBV3(dataSource());
}
@Bean
MemberServiceV3_3 memberServiceV3_3() {
return new MemberServiceV3_3(memberRepositoryWithDBV3());
}
}
@Test
void AopCheck() {
log.info("memberService class = {}", memberService.getClass());
log.info("memberRepository class = {}", memberRepository.getClass());
Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
}
나머지 비즈니스 로직 테스트 코드는 앞과 동일하다.
스프링 AOP를 적용하려면 스프링 컨테이너가 필요해서 이전과 다르게 설정이 필요하다.
'Spring' 카테고리의 다른 글
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 6. 스프링과 문제 해결 - 예외 처리, 반복 (2) | 2024.03.09 |
---|---|
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 5. 자바 예외 이해 (4) | 2024.03.02 |
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 4. 스프링과 문제 해결 - 트랜잭션 - 1 (0) | 2024.02.29 |
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 3. 트랜잭션 이해 (3) | 2024.02.27 |
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 2. 커넥션 풀과 데이터소스 이해 (0) | 2024.02.23 |