트랜잭션 AOP를 적용해 보겠다 (프록시)
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if(toMember.getMemberId().equals("ex")){
throw new IllegalStateException("이체 중 예외 발생");
}
}
}
정말 간단해졌다.
그냥 트랜잭션 할 곳에 @Transactional 이것만 붙여주면 된다.
물론 클래스에다가 붙이면 그 클래스의 모든 메소드가 트랜잭션 적용이 된다.
@Slf4j
@SpringBootTest
public class MemberServiceV3_3Test {
private static final String MEMBER_A = "memberA";
private static final String MEMBER_B = "memberB";
private static final String MEMBER_EX = "ex";
@Autowired
private MemberRepositoryV3 memberRepository;
@Autowired
private MemberServiceV3_3 memberService;
@TestConfiguration
static class TestConfig{
@Bean
DataSource dataSource(){
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource());
}
@Bean
MemberRepositoryV3 memberRepository(){
return new MemberRepositoryV3(dataSource());
}
@Bean
MemberServiceV3_3 memberSerivce(){
return new MemberServiceV3_3(memberRepository());
}
}
@AfterEach
private void after() throws SQLException {
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete(MEMBER_EX);
}
@Test
@DisplayName("AOP 체크")
public void aopCheck(){
assertThat(AopUtils.isAopProxy(memberService)).isTrue();
assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
}
@Test
@DisplayName("정상 이체")
void accountTransfer() throws SQLException {
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
int transferMoney = 2000;
memberRepository.save(memberA);
memberRepository.save(memberB);
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney);
Member findedMemberA = memberRepository.findById(memberA.getMemberId());
Member findedMemberB = memberRepository.findById(memberB.getMemberId());
assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney() - transferMoney);
assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney() + transferMoney);
}
@Test
@DisplayName("이체 중 예외 발생")
void accountTransferEx() throws SQLException {
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_EX, 10000);
int transferMoney = 2000;
memberRepository.save(memberA);
memberRepository.save(memberB);
assertThatThrownBy(()->memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney))
.isInstanceOf(IllegalStateException.class);
Member findedMemberA = memberRepository.findById(memberA.getMemberId());
Member findedMemberB = memberRepository.findById(memberB.getMemberId());
assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney());
assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney());
}
}
테스트 코드이다. 뭔가 덕지덕지 많이 붙었다.
우선 @SpringBootTest.
프록시는 스프링에서 제공해주는 기능이다. 따라서, 스프링이 올라와 있는 환경에서 써야 한다.
@SpringBootTest는 이렇게 테스트환경에서 스프링을 띄워주는 역할을 한다. 물론 필요한 기본 빈 들을 자동등록 해 주고.
@AutoWired로 자동 의존주입으로 지정해 주고,
@TestConfiguration 이거는 테스트에 관한 설정을 해 주는 곳인데, 주로 빈 등록할 때 쓰는 것 같다. 아니면 딱 빈 등록할 때 쓰는건가?
참고로, DataSourceTransactionManager는 우리 눈에 보이기에는 어떤 곳에도 의존관계를 주입시키는 곳이 보이지 않지만,
우리가 사용했던 @Transactional 이 프록시 기능은 결국 스프링 빈에 등록된 트랜잭션 매니저를 찾아서 사용하기 때문에 우리가 등록 해 주어야 한다.
그 외에는 다 비슷하고,
AopCheck() 테스트 보면,
이게 프록시 객체인지 아닌 지 확인하는데,
이게 프록시가 어떤 구조냐면,
이런 구조가 아니라,
어..
추상적으로 이런 구조와 유사하다.
그러니까, 스프링이 저 본 클래스 코드를 보고 @Transactional 이게 프록시이므로 프록시인걸 확인 하면,
본 클래스를 상속받은 프록시 + 본클래스인 클래스를 만든다.
그래서 저번에 얘기했던
try{
본로직
commit()
}catch (e){
rollback()
}
이런 것 처럼 감싸서 사용하는거다. 뭐 주위에 감싸는 내용은 프록시의 내용에 따라 당연히 다르다.
그래서 저 프록시 클래스를 출력해보면,
EnhancerBySpringCGLIB$$코드
직역해보면 강화된 스프링에 의해 CGLIB? 이런거고 코드는 아마 자동으로 저렇게 생성되는거니 중복을 피하기 위해서 한 것인듯?
CGLIB는 Code Generator Library
의 약자로, 직역하면 코드 생성기 라이브러리이고,
클래스의 바이트 코드를 조작하여 프록시 객체를 생성하도록 도와주는 라이브러리라고 한다.
바이트코드... ㅋㅋㅋㅋ
여튼 뭐 정상작동 된다.
전체 흐름이다. 클라이언트가 서버에 요청을 하면 요청에 따라 우리의 구현에 따라 그게 프록시 일 수도 있다. 그게 트랜잭션이라고 생각하고,
그럼 클라이언트 -> 서버 -> 컨트롤러 -> 서비스(프록시인) 이렇게 오게 되는데,
본 클래스를 상속받은 프록시 + 본 클래스의 로직을 실행한다. 내부는 프록시에 따라 프록시 로직 사이에 본 클래스의 로직이 적절하게 잘 들어가 있을 것이다. 이번 경우 트랜잭션이므로 트랜잭션의 로직 사이에 적절하게 본 클래스의 로직이 잘 들어가 있다.
여튼 @Transactional이므로 맨 처음 서버 자체를 실행 했을 때 저 @Transactional을 보고 프록시 + 본 클래스인 클래스를 만들어 빈에 등록해 놨을 것이다.
여튼 본클래스를 한번 감싼 그 프록시 + 본 클래스를 사용하게 되고, 적절히 잘 Transaction + 비즈니스 로직이 알아서 잘 실행이 된다.
클라이언트 요청이 오면 스프링 컨테이너에서 트랜잭션 매니저 빈을 획득하고,
그 트랜잭션 매니저를 이용해 (트랜잭션 매니저 생성할 때는 커넥션 생성을 담당하는 dataSource도 들어간다.)
커넥션을 생성하고,
오토커밋모드를 꺼주고,
트랜잭션 동기화 매니저(쓰레드 로컬)에 그 커넥션을 보관해 준다.
이러면 이제 트랜잭션 할 준비가 끝난 것이다.
그 다음 실제 비즈니스 로직을 호출 하는데,
우리가 이때 커넥션 가져오는 걸
public Connection getConnection() throws SQLException {
Connection con = DataSourceUtils.getConnection(dataSource);
return con;
}
이걸로 해 놔서 DataSourceUtils.getConnection()은 쓰레드 로컬을 뒤져보고 거기서 커넥션을 가져오는 거기 때문에, 서로 간 커넥션을 공유할 수 있다. (마치 쫌 내부의 휘발성 DB비슷한 존재일려나.. )
비즈니스 로직에 따라, 리포지토리를 호출하여 그 리포지토리가 DB의 커넥션을 얻어 DB와 소통할 때, 이 커넥션은 쓰레드 로컬에서 가져오는 것 이고,
여기 트랜잭션 동기화 매니저에서 가져온 DB는
private void close(Connection con, Statement stmt, ResultSet rs){
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, dataSource);
}
DB close를 이렇게 해놨는데,
DataSourceUtils.releaseConnection()은 트랜잭션 동기화 매니저에서 가져온 DB는 close() 하지 않고 그냥 넘어간다.
다른 DB들은 그냥 close하던지, 풀에 반환한다.
그래서, 그냥 넘어가기 때문에 누군가는 close(혹은 풀에 반환)를 해 줘야 한다.
그걸 AOP 프록시에서
commit혹은 rollback 했을 시 close(혹은 반환) 한다.
에러없이 잘 수행됐으면 commit,
만약에 에러를 어디선가 내뿜었으면 catch로 에러를 잡고, rollback 수행.
'스프링 > 5. 스프링 DB-1' 카테고리의 다른 글
30. 자바 예외 (0) | 2023.10.02 |
---|---|
29. 스프링 부트 자동 리소스 등록 (0) | 2023.10.01 |
27. 트랜잭션 AOP (0) | 2023.10.01 |
26. 트랜잭션 템플릿 (0) | 2023.10.01 |
25. 트랜잭션 매니저 (0) | 2023.10.01 |