본문 바로가기
Java/Spring

[Spring][Transaction] 트랜잭션 제외하기, 트랜잭션 제외 안 될 때

by 오늘의개발부 2020. 9. 7.
반응형

음식점 예약 기능을 구현하던 중이었다.

 

예약을 담당하는 ReservationService에서 makeReservation 메소드를 통해 손님의 예약 정보를 저장한다. 예약 확인 문자가 정상적으로 전송되지 않았을 때는 예약 자체가 안 된 것으로 처리해야했으므로 예약 정보 저장이 롤백되어야 했다. 그래서 @Transactional 어노테이션을 달아 트랜잭션을 관리했다.

 

아래는 간략화한 코드이다.

public class ReservationService{

    @Transactional //1. 예약
    public void makeRservation(ReservationInfo info) throws ReservationException, SMSException{
        saveRerservationInfo(info); //2. 예약 정보 저장
        smsService.sendSMS(info); //3. 예약 확인 SMS 전송
    }
}


public class SMSService{

    public void sendSMS(ReservationInfo info) throws SMSException{
        APIToken token = getToken(); //4. 토큰 받아옴
        sendSMSRequest(token, info);  //9. SMS 전송 요청
    }

    public APIToken getToken(){
        APIToken token = findToken(); //5. 토큰 조회
        if(token == null || isExpired(token)){ //6. 없거나 만료되었는지 체크
            token = sendTokenRequest(); //7. 새 토큰 받아옴
            saveToken(token); //8. 새 토큰을 DB에 저장
        }
        return token;
    }
}

 

하지만 SMSService의 sendSMS 메소드에서 예외가 발생해 전체 롤백이 필요한 상황에서도, 토큰이 만료되어서 토큰을 새로 받아와 저장했다면 그것은 롤백에서 배제되어야 했다.

 

1. 예약 정보 저장 => 예외 발생시 롤백

2. SMS

2.1 토큰 조회 (있으면 가져오고, 없거나 만료되었으면 새로 받아와 저장) => 롤백 제외

2.2 SMS 발송 => 예외 발생시 롤백

 

이렇게 되어야 하는 것이다.

 

 

public class ReservationService{

    @Transactional //1. 예약
    public void makeRservation(ReservationInfo info) throws ReservationException, SMSException{
        saveRerservationInfo(info); //2. 예약 정보 저장
        smsService.sendSMS(info); //3. 예약 확인 SMS 전송
    }
}


public class SMSService{

    public void sendSMS(ReservationInfo info) throws SMSException{
        APIToken token = getToken(); //4. 토큰 받아옴
        sendSMSRequest(token, info);  //9. SMS 전송 요청
    }

    @Transactional(propagation = Propagation.NOT_SUPPORTED) <<<<
    public APIToken getToken(){
        APIToken token = findToken(); //5. 토큰 조회
        if(token == null || isExpired(token)){ //6. 없거나 만료되었는지 체크
            token = sendTokenRequest(); //7. 새 토큰 받아옴
            saveToken(token); //8. 새 토큰을 DB에 저장
        }
        return token;
    }
}

그래서 제외 대상이 되는 getToken 메소드에 @Transactional(propagation = Propagation.NOT_SUPPORTED) 를 붙여주었다.

 


다시 테스트 해보니 sendSMS()에서 예외가 발생했을 때 함께 롤백이 되었다. 즉 Propagation.NOT_SUPPORTED가 적용되지 않았다.

 

구글링을 해봐도 뭐가 문제인지 알 수 없던 차에 생각났다.

 

프록시를 이용한 트랜잭션은 외부에서 메소드가 호출될 때 바이트코드를 삽입해준다.

ReservationService.makeRservation에서는 SMSService.sendSMS를 호출하지만,
정작 @Transaction이 달린 getToken는 SMSService 내부에서 호출하기 때문에 프록시로 감싸져있지 않았다.

 

 

public class ReservationService{

    @Transactional //1. 예약
    public void makeRservation(ReservationInfo info) throws ReservationException, SMSException{
        saveRerservationInfo(info); //2. 예약 정보 저장
        smsService.sendSMS(info); //3. 예약 확인 SMS 전송
    }
}


public class SMSService{

    @Transactional(propagation = Propagation.NOT_SUPPORTED) <<<<
    public void sendSMS(ReservationInfo info) throws SMSException{
        APIToken token = getToken(); //4. 토큰 받아옴
        sendSMSRequest(token, info);  //9. SMS 전송 요청
    }


    public APIToken getToken(){
        APIToken token = findToken(); //5. 토큰 조회
        if(token == null || isExpired(token)){ //6. 없거나 만료되었는지 체크
            token = sendTokenRequest(); //7. 새 토큰 받아옴
            saveToken(token); //8. 새 토큰을 DB에 저장
        }
        return token;
    }
}

SMSService.sendSMS에 트랜잭션을 붙여주자 정상적으로 적용되었다.

 

하지만 sendSMS에서 일어나는 프로세스 중 일부는 트랜잭션이 적용되어야 하는 부분이 있었더라면 어떻게 해야 했을까?
이 부분은 좀 더 공부해봐야겠다.

반응형