음식점 예약 기능을 구현하던 중이었다.
예약을 담당하는 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에서 일어나는 프로세스 중 일부는 트랜잭션이 적용되어야 하는 부분이 있었더라면 어떻게 해야 했을까?
이 부분은 좀 더 공부해봐야겠다.
'Java > Spring' 카테고리의 다른 글
[Mybatis] Select시 NullPointerException ... org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible... (0) | 2021.01.18 |
---|---|
[Spring]WebClient 파라미터 인코딩 (1) | 2021.01.15 |
[Logback] Logback 설정 기록. 프로필에 따라, 패키지에 따라 분리. (0) | 2020.09.03 |
SpringBoot ViewResolver 설정, application.yml 읽지 못할 때 (0) | 2020.06.18 |
classpath 안에 있는 json 파일 읽어서 json 객체 만들기 (0) | 2020.06.18 |