LRA 주요 사항 설명
본 Section에서는 LRA 분산 트랜잭션 개발 시 주요한 사항에 대해서 설명한다.
1. LRA ID
LRA ID는 분산 트랜잭션을 추적하고 관리하기 위한 고유 식별자이며, 최초 LRA 분산 트랜잭션 실행 시 LRA Coordinator에 의해 획득되며, 분산 트랜잭션을 관리하는 데 사용된다.
(참고로 LRA ID는 기존 BXM 에서의 GUID 개념으로 이해하면 된다.)
LRA ID는 Http Header에 LRA_HTTP_CONTEXT_HEADER name으로 저장되어 있으며, @RequestHeader Annotation을 이용해서 메소드의 파라미터로 전달 받을 수 있으며, 보상 트랜잭션 처리 시 사용될 수 있다.
다음은 LRA ID를 파라미터로 전달 받아 처리하는 예이다.
@LRA(value= LRA.Type.REQUIRED)
public ResponseEntity<String> updateEmployee(@RequestHeader(name = LRA_HTTP_CONTEXT_HEADER, required = true) String lraId) {
// 업무 로직 구현
}
@Complete
public ResponseEntity<String> completeEmployee(@RequestHeader(name= LRA_HTTP_CONTEXT_HEADER, required= true) String lraId, @RequestBody Map<Object, Object> terminationData) {
// 완료 (Commit) 처리
}
@Compensate
public ResponseEntity<String> cancelEmployee(@RequestHeader(name= LRA_HTTP_CONTEXT_HEADER, required= true) String lraId, @RequestBody Map<Object, Object> terminationData) {
// 보상에 대한 Rollback 처리
}
@LRA(value= LRA.Type.REQUIRES_NEW) 로 정의되어 있는 경우에는 분산 트랜잭션을 새롭게 시작한다는 의미이며 , 해당 서비스가 호출 되기 전에 분산 트랜잭션이 시작되고 있는 경우에는 기존의 LRA ID는 Parent LRA ID가 되고, 새롭게 LRA ID를 채번한다. |
2. 복원 데이터 처리
기본적으로 분산 트랜잭션을 수행할 때, 업무 로직에서는 일반적으로 LRA ID를 사용하여 트랜잭션 상태를 추적하고 관리 할 수 있다.
하지만 보상 작업이 필요한 상황에서는 단순히 LRA ID만으로는 충분하지 않을 수 있다. 이는 보상 작업을 수행하는 동안 추가적인 정보나 상태를 필요로 할 수 있기 때문이며, 이러한 사항으로 인하여 프레임워크에서는 복원 정보를 저장하고 관리할 수 있는 API를 제공한다.
-
복원 데이터 저장 API
Map<Object, Object> terminationData = new HashMap<>();
// terminationData 에 복원 데이터 저장
LRAContext.storeTerminationData(terminationData); // terminationData 저장
3. LRA Local 테스트
일반적으로 Local 환경에서 Local 테스트를 수행하는 것은 불가능하다. 이는 분산 트랜잭션의 관리자인 Coordinator가 Kubernetes 클러스터 환경에 구성되어 있고, 보상 처리를 위해 Coordinator에 저장된 보상 처리 URL을 호출해야 하기 때문이다.
따라서 Local에서 직접 Coordinator를 호출하여 보상 처리를 테스트하는 것은 불가능하다.
Local 환경에서 테스트를 수행하기 위해서는 Coordinator와 통합된 서비스 모두를 Local 환경에 띄우고, 테스트용 Coordinator를 구성하여 보상 처리를 수행해야 하지만, 환경 구성이 쉽지 않기 때문에 실질적으로 불가능하다고 볼 수 있다.
따라서 Local에서 테스트를 수행할 때에는 Coordinator와 연동된 서비스가 아닌, 각 서비스의 동작을 로컬에서 개별적으로 테스트를 할 수 있도록 해야 한다.
다음은 Local에서 JUnit Mock테스트를 이용한 예제이다.
@ExtendWith( SpringExtension.class)
@ContextConfiguration(classes = {SmpLraCalleeController.class})
public class SmpLraCalleeControllerMockTest {
private Logger logger= LoggerFactory.getLogger(getClass());
@Autowired
private SmpLraCalleeController smpLraCalleeController;
@MockBean
private SmpLraCalleeService smpLraCalleeService;
/**
* Sample LRA Callee Controller
*
* {@link sample.online.controller.SmpLraCalleeController#addEmployee}
*/
@Test
@DisplayName("Sample LRA Callee Controller 테스트")
public void testAddEmployee(){
logger.debug("[addEmployee] 메소드의 테스트를 시작 합니다.");
// Static Class에 대한 Mock 정의
MockedStatic<DefaultApplicationContext> defaultApplicationContext = mockStatic(DefaultApplicationContext.class);
BDDMockito.given(DefaultApplicationContext.getBean(ArgumentMatchers.any()
, ArgumentMatchers.eq(SmpLraCalleeService.class))).willReturn(smpLraCalleeService);
MockedStatic<LRAContext> lraContext = mockStatic(LRAContext.class); // void 메소드는 따로 BDDMockito로 작성하지 않는다.
// LRA Controller
/*
* TODO 테스트할 메소드의 입력 값을 지정 하십시오.
*/
String lraId = UUID.randomUUID().toString();
SmpLraCalleeController01InDto input= new SmpLraCalleeController01InDto();
// SmpLraCalleeService Mock 정의
BDDMockito.given(smpLraCalleeService.addEmployeeLRA(input, lraId)).willReturn(1);
// Controller 호출
String result = smpLraCalleeController.addEmployee(lraId, input);
logger.info("result : {}", result);
assertEquals("Y", result, "Response failed");
// Complete
// SmpLraCalleeService Mock 정의
BDDMockito.given(smpLraCalleeService.modifyEmpIoyeeLRAStatus(lraId, "COMPLETED")).willReturn(1);
//BDDMockito.doNothing().when(smpLraCalleeService).modifyEmpIoyeeLRAStatus(lraId, "COMPLETED"); // void
Map<Object, Object> terminationData = new HashMap<>();
smpLraCalleeController.complete(lraId, terminationData);
// Compensate
// SmpLraCalleeService Mock 정의
BDDMockito.given(smpLraCalleeService.modifyEmpIoyeeLRAStatus(lraId, "FAILED")).willReturn(1);
// Controller 호출
smpLraCalleeController.cancel(lraId, terminationData);
defaultApplicationContext.close();
lraContext.close();
logger.debug("[addEmployee] 메소드의 테스트가 종료 되었습니다.");
}
}
4. LRA 분산 트랜잭션 작성 Controller 예제
본 Section에서는 간단하게 이체(출금 → 입금) 서비스에 대하여 간단하게 예제를 구성하였다.
해당 예제는 LRA 분산 트랜잭션에 대한 개발에 유용한 참고 Sample로, LRA의 동작 방식과 기본적인 개념을 이해하는데에 있다. 예제는 Controller만을 구성하고 있으므로, 실제로 LRA 분산 트랜잭션을 어떻게 구현하는지에 대한 상세한 내용은 제공하지 않는다. 따라서 이 예제는 개발자들이 LRA를 사용하여 분산 트랜잭션을 구현하는 방법을 확인하기 위한 참고용으로 만 사용되는 것을 권장한다. |
다음은 예제의 간단한 구성도이다.
1. 이체 컨트롤러
/**
* LRA 이체 컨트롤러
*
* @author sysadmin
*/
@RestController
@RequestMapping("/lra/transfer")
@BxmCategory(logicalName = "LRA 이체 컨트롤러", description = "LRA 이체 컨트롤러")
public class LRATransferController {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* LRA - 이체 처리
* @param in 이체정보
* @return 이체 처리 결과
*/
@RequestMapping(method = RequestMethod.POST)
@LRA(value = LRA.Type.REQUIRED, timeLimit = 10, timeUnit = ChronoUnit.SECONDS)
@BxmCategory(logicalName = "LRA 이체 처리", description = "LRA 이체 처리")
public ResponseEntity<LRATransferOut> transfer(@RequestBody LRATransferIn in) {
SystemHeader systemHeader = RequestContextTrace.getRequestSystemHeader().orElse(null);
// 출금
WithdrawIn withdrawIn = new WithdrawIn();
withdrawIn.setCustId(in.getCustId());
withdrawIn.setAcctNum(in.getWithdrawAcctNum());
withdrawIn.setSummary(in.getSummary());
withdrawIn.setWithdrawAmt(in.getTransferAmt());
try {
PrependedSystemHeader header = PrependedSystemHeader.prepend(systemHeader);
// 출금 서비스 호출
String withdrawResult = DefaultWebClient.postForObject(header, "http://withdraw-app:8080/lra/withdraw", withdrawIn, String.class);
} catch (Exception e) {
logger.error("Failed to withdraw...", e);
throw new DefaultApplicationException("sample.poc.transfer.failed");
}
// 입금
DepositIn depositIn = new DepositIn();
depositIn.setCustId(in.getCustId());
depositIn.setAcctNum(in.getDepositAcctNum());
depositIn.setSummary(in.getSummary());
depositIn.setDepositAmt(in.getTransferAmt());
try {
PrependedSystemHeader header = PrependedSystemHeader.prepend(systemHeader);
// 입금 서비스 호출
String depositResult = DefaultWebClient.postForObject(header, "http://deposit-app:8080/lra/deposit", depositIn, String.class);
} catch (Exception e) {
logger.error("Failed to deposit...", e);
throw new DefaultApplicationException("sample.poc.transfer.failed", "Failed to transfer");
}
LRATransferOut result = new LRATransferOut();
result.setSuccessYn("Y");
return ResponseEntity.ok(result);
}
/**
* LRA - 이체 성공 처리
* @param terminationData 처리 데이터
* @return 결과
*/
@RequestMapping(method = RequestMethod.PUT, path = "/complete")
@Complete
@BxmCategory(logicalName = "LRA 이체 성공", description = "LRA 이체 성공")
public ResponseEntity<String> complete(@RequestBody Map<Object, Object> terminationData) {
return ResponseEntity.ok(ParticipantStatus.Completed.name());
}
/**
* LRA - 이체 보상 처리
* @param terminationData 처리 데이터
* @return 결과
*/
@RequestMapping(method = RequestMethod.PUT, path = "/compensate")
@Compensate
@BxmCategory(logicalName = "LRA 이체 실패", description = "LRA 이체 실패")
public ResponseEntity<String> compensate(@RequestBody Map<Object, Object> terminationData) {
return ResponseEntity.ok(ParticipantStatus.Compensated.name());
}
}
2. 출금 컨트롤러
/**
* LRA 출금 컨트롤러
*
* @author sysadmin
*/
@RestController
@RequestMapping("/lra/withdraw")
@BxmCategory(logicalName = "LRA 출금 컨트롤러", description = "LRA 출금 컨트롤러")
public class LRAWithdrawController {
private Logger logger = LoggerFactory.getLogger(getClass());
private WithdrawService withdrawService;
/**
* LRA - 출금 처리
* @param in 출금정보
* @return 출금처리 결과
*/
@RequestMapping(method = RequestMethod.POST)
@LRA(value = LRA.Type.REQUIRED, timeLimit = 10, timeUnit = ChronoUnit.SECONDS)
@Transactional
@BxmCategory(logicalName = "LRA 출금 처리", description = "LRA 출금 처리")
public ResponseEntity<String> withdraw(@RequestBody WithdrawIn in) {
withdrawService = DefaultApplicationContext.getBean(withdrawService, WithdrawService.class);
// 출금 처리
withdrawService.withdraw(in, "이체");
// 이체에 실패할 경우 보상 처리를 위해 terminataionData에 출금정보를 저장
Map<Object, Object> terminationData = new HashMap<>();
terminationData.put("withdrawCustId", in.getCustId());
terminationData.put("withdrawAcctNum", in.getAcctNum());
terminationData.put("withdrawAmt", in.getWithdrawAmt().toString());
LRAContext.storeTerminationData(terminationData);
return ResponseEntity.ok("completed");
}
/**
* LRA - 출금 성공 처리
* @param terminationData 처리 데이터
* @return 결과
*/
@RequestMapping(method = RequestMethod.PUT, path = "/withdraw/complete")
@Complete
@BxmCategory(logicalName = "LRA 출금 성공", description = "LRA 출금 성공")
public ResponseEntity<String> complete(@RequestBody Map<Object, Object> terminationData) {
logger.info("Complete withdraw.");
return ResponseEntity.ok(ParticipantStatus.Completed.name());
}
/**
* LRA - 출금 보상 처리
* @param terminationData
* @return
*/
@RequestMapping(method = RequestMethod.PUT, path = "/withdraw/compensate")
@Compensate
@BxmCategory(logicalName = "LRA 출금 보상 처리", description = "LRA 출금 보상 처리")
public ResponseEntity<String> compensate(@RequestBody Map<Object, Object> terminationData) {
logger.info("Failed to withdraw. Compensate for withdraw.");
withdrawService = DefaultApplicationContext.getBean(withdrawService, WithdrawService.class);
// terminationData에 출금 정보가 있을 경우 보상 처리 수행
if (terminationData.containsKey("withdrawCustId")) {
String custId = (String) terminationData.get("withdrawCustId");
String acctNum = (String) terminationData.get("withdrawAcctNum");
BigDecimal depositAmt = new BigDecimal((String) terminationData.get("withdrawAmt"));
DepositIn depositIn = new DepositIn();
depositIn.setCustId(custId);
depositIn.setAcctNum(acctNum);
depositIn.setSummary("출금 취소");
depositIn.setDepositAmt(depositAmt);
// 출금 Rollback
withdrawService.depositError(depositIn);
}
return ResponseEntity.ok(ParticipantStatus.Compensated.name());
}
}
3. 입금 컨트롤러
/**
* LRA 입금 컨트롤러
*
* @author sysadmin
*/
@RestController
@RequestMapping("/lra/deposit")
@BxmCategory(logicalName = "LRA 입금 컨트롤러", description = "LRA 입금 컨트롤러")
public class LRADepositController {
private Logger logger = LoggerFactory.getLogger(getClass());
private DepositService depositService;
/**
* LRA - 입금 처리
* @param in 입금정보
* @return 입금 처리 결과
*/
@RequestMapping(method = RequestMethod.POST)
@LRA(value = LRA.Type.REQUIRED, timeLimit = 10, timeUnit = ChronoUnit.SECONDS)
@Transactional
@BxmCategory(logicalName = "LRA 입금", description = "LRA 입금")
public ResponseEntity<String> deposit(@RequestBody DepositIn in) {
logger.info("Start to deposit.");
depositService = DefaultApplicationContext.getBean(depositService, DepositService.class);
// 입금 처리
depositService.deposit(in, "이체");
// 이체에 실패할 경우 보상 처리를 위해 terminataionData에 입금정보를 저장
Map<Object, Object> terminationData = new HashMap<>();
terminationData.put("depositCustId", in.getCustId());
terminationData.put("depositAcctNum", in.getAcctNum());
terminationData.put("depositAmt", in.getWithdrawAmt().toString());
LRAContext.storeTerminationData(terminationData);
return ResponseEntity.ok("completed");
}
/**
* LRA - 입금 성공 처리
* @param terminationData 처리 데이터
* @return 결과
*/
@RequestMapping(method = RequestMethod.PUT, path = "/complete")
@Complete
@BxmCategory(logicalName = "LRA 입금 성공", description = "LRA 입금 성공")
public ResponseEntity<String> complete(@RequestBody Map<Object, Object> terminationData) {
logger.info("Complete deposit.");
return ResponseEntity.ok(ParticipantStatus.Completed.name());
}
/**
* LRA - 입금 보상 처리
* @param terminationData 처리 데이터
* @return 결과
*/
@RequestMapping(method = RequestMethod.PUT, path = "/compensate")
@Compensate
@BxmCategory(logicalName = "LRA 입금 보상처리", description = "LRA 입금 보상처리")
public ResponseEntity<String> compensate(@RequestBody Map<Object, Object> terminationData) {
logger.info("Failed to deposit. Compensate for withdraw.");
depositService = DefaultApplicationContext.getBean(depositService, DepositService.class);
// terminationData에 입금 정보가 있을 경우 보상 처리 수행
if (terminationData.containsKey("depositCustId")) {
String custId = (String) terminationData.get("depositCustId");
String acctNum = (String) terminationData.get("depositAcctNum");
BigDecimal depositAmt = new BigDecimal((String) terminationData.get("depositAmt"));
WithdrawIn withdrawIn = new WithdrawIn();
withdrawIn.setCustId(custId);
withdrawIn.setAcctNum(acctNum);
withdrawIn.setSummary("입금 취소");
withdrawIn.setDepositAmt(depositAmt);
// 입금 Rollback
depositService.withdrawError(withdrawIn);
}
return ResponseEntity.ok(ParticipantStatus.Compensated.name());
}
}