상황 : 한 로직에 DB insert를 실행한 후 HttpClient를 이용해 restAPI를 실행시킨 후 return 값으로 추가 로직을 처리해야 하는 상황 중 HttpClient method 실행 중 잘못된 api를 실행시키면 ClientProtocolException이 발생한다. Exception이 발생하면 트랜잭션 중 insert 상태를 rollback해야 하는데 ClientProtocolException은 checked Exception이기 때문에 rollback이 자동으로 일어나지 않는다.
요약: checked Exception이 발생했을 때 rollback 하는 방법
첫번째로 시도한 것은 @Transaction이다.
Transaction annotation은 checked Exception에서 자동으로 rollback해주지 않는다.
두번째 시도 @Transaction(rollbackFor)를 한다.
@Transactional(rollbackFor = { ClientProtocolException.class, ProtocolException.class })
실패....
세번째 시도 강제로 rollback을 시켜주기
@Autowired
private PlatformTransactionManager transactionManager;
@PostMapping("/test")
@ResponseBody
@Transactional public void test(@RequestBody ReqMap
reqMap, @AuthenticationPrincipal AdminUser user) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
try {
throw new ClientProtocolException();
} catch (ClientProtocolException e) {
transactionManager.rollback(status); // 롤백
}
}
성공 그러나 return 값으로 ResMap.valueOf(ResultCode.SUCCESS);
코드 값을 넘겨야 하는데,
return 실행 후 proxy에서 추가 작업을 통해 resultMap이 덮어씌어짐.
대안으로 작성한 게 ClientProtocolException 발생 시 RuntimeException을 강제로 실행시켰음.
try{}
catch (ClientProtocolException e) {
throw new RuntimeException();
}
/**
* 프로젝트 실행
*/
@PostMapping("/run")
@ResponseBody
public ResMap runProject(@RequestBody ReqMap reqMap, @AuthenticationPrincipal AdminUser user) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
try {
projectService.runProject(reqMap, user);
return ResMap.valueOf(ResultCode.SUCCESS);
}catch(RuntimeException e) {
return postRollbackActions();
} catch (Exception e) {
return ResMap.valueOf(ResultCode.ERR_ETC);
}
}
GPT한테 물어보니 좋은 방법은 아니라고 함.
해결 방법이 아니었음. 결국 안되고 있었음.
의심을 한 건 트랜잭션 단위를 내가 잘못알고 있나였음.
client에서 server로 응답값을 보낼때 controller에서 응답값을 받고 return 해줄때 까지를 transction이라고 생각했음.
그리고 transactional의 단위를 확인하니
트랜잭션은 메소드 단위었음.
그리고 transactional은 service 계층에서 부여함.
/**
* 프로젝트 실행
*
* @throws IOException
* @throws ClientProtocolException
*/
@Transactional(rollbackFor = {ClientProtocolException.class})
public void runProject(ReqMap reqMap, AdminUser user) throws Exception {
List<KngMap> keyList = reqMap.getContentList(); // 프로젝트일련번호 리스트
for (KngMap key : keyList) {
if (key.size() < 1) throw new Exception("등록된 API가 없습니다.");
List<CpaiApiMng> apiList = apiManageDAO.getApiList(key); // API관리 목록 조회
CapiHistoryDetail projHist = insertRunHistory(key, user); // 프로젝트 실행이력 INSERT
executeApi(apiList, projHist, user); // api 실행
}
}
결국 service계층에서
@Transactional(rollbackFor = {ClientProtocolException.class})를 사용해서 해결
컨트롤러에서 @transactional을 선언할 경우
spring AOP는 다이나믹 Proxy기법을 사용한다.
controller는 다이나믹 proxy기법을 사용하는 interface를 가지고 있지 않기 때문에 @transaction은 controller에서 작동하지 않는다.
application context 파일에서 다음과 같은 설정을 하면 사용 가능하다.
<annotation-driven>proxy-target-class="true"/></annotation-driven>
일반적으로 controller까지 transcation을 가지고 있으면 비효율적이므로 서비스 계층에서 transactional을 걸어준다.