졸업 작품에 올인한지도 벌써 5달쯤 되었고, 1학기가 끝나기까지 약 한달이 남았다.
비동기 작업 성능을 높여보고
.. Swagger로 API 문서를 자동화
하고.. Redis를 도입
하는 등의 작업을 했지만..
사실상 이메일 발송
을 제외한 대부분의 기능들이 CRUD다!
따라서 새로운 기능이 도입되었으면 했고, 생각해 본 바로는 아래와 같다.
Spring Batch
를 이용해 대용량 데이터를 일괄처리하기
→ 휴먼 계정이 있다고 할 때, 일괄로 이메일을 보내 알려주는건 어떨까?
결제 시스템 만들어보기
→ 아보카도의 경우 예약 기능이 있는데, 소액의 예약금을 받는 식으로 넣어볼까?
Slack 챗봇 연동하기
우선, 2번의 결제 시스템을 먼저 도입해보기로 결정!
결제 시스템 설계 과정을 설명하기에 앞서 제대로 된 결제 시스템을 구현하려면, 사업자 등록을 우선시 해야된다.
하지만 심사가 통과될 때까지 시간이 오래 걸린다는 여러 의견들도 있었고,
'프로젝트 하나에 사업자 등록까지 해야 할까?'라는 의문이 함께 따라왔던 것 같다.따라서 이것저것 찾아보다 iamport 라는 플랫폼을 발견할 수 있었고, 모의 테스트 결제 API를 제공한다.
게다가 개발 가이드까지 정리가 잘 되어있어 이를 사용하기로 결정!추가로, 모든 전체 코드는 깃헙 링크를 통해 확인할 수 있습니다.
💡 결제 진행 흐름
- 클라이언트가 결제 요청시 프론트에서 결제창 호출
- Iamport 서비스에서 결제 진행
- 결제 완료시 응답을 백엔드 서버로 POST
- 서버는 해당 결제 내역을 저장하고, 해당 예약의 상태를 “결제 완료”로 바꿈
@PostMapping("/{id}")
public ResponseEntity<?> savePayment(
@RequestBody final Map<String, Object> model,
@PathVariable(value = "id") final Long appointmentId) throws JSONException, IOException {
//응답 header 생성
final HttpHeaders responseHeaders = makeHttpHeader();
final String impUid = (String) model.get("imp_uid");
final String merchantUid = (String) model.get("merchant_uid");
final boolean success = (boolean) model.get("success");
final String errorMsg = (String) model.get("error_msg");
if (!success) {
log.error(errorMsg);
return new ResponseEntity<>(errorMsg, responseHeaders, HttpStatus.OK);
}
try {
//해당 예약이 이미 결제 상태인지는 아닌지 확인
validateAppointmentPayStatus(appointmentId);
final var iamportClient = new IamportClient(API_KEY, API_SECRET);
final Payment payment = extractPayment(impUid, iamportClient);
return new ResponseEntity<>(paymentService.save(appointmentId, payment), responseHeaders, HttpStatus.OK);
} catch (IamportResponseException | IOException e) {
log.error("{}", e);
//예외 발생시 결제를 취소
//뒤에서 구현할 예정
return ResponseEntity.badRequest().build();
}
}
private Payment extractPayment(final String imp_uid, final IamportClient iamportClient) throws IamportResponseException, IOException {
return iamportClient.paymentByImpUid(imp_uid).getResponse();
}
private void validateAppointmentPayStatus(final Long appointmentId) {
final Appointment appointment = appointmentService.findById(appointmentId);
if (appointment.getPayStatus() == PayStatus.COMPLETED) {
throw new IllegalValueException("이미 결제된 예약입니다.", ErrorCode.ILLEGAL_STATE);
}
}
private HttpHeaders makeHttpHeader() {
final HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "application/json; charset=UTF-8");
responseHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
return responseHeaders;
}
@Transactional
public Long save(final Long appointmentId, final Payment payment) {
//예약 정보 조회
final Appointment appointment = appointmentService.findById(appointmentId);
//해당 예약건 결제
appointment.payment();
//결제 정보 생성(매핑) 후 저장
final PaymentEntity paymentEntity = paymentToPaymentEntity(payment);
return paymentRepository.save(paymentEntity).getId();
}
private PaymentEntity paymentToPaymentEntity(final Payment payment) {
return PaymentEntity.createPaymentEntity(
payment.getPgProvider(),
payment.getPayMethod(),
"아보카도 병원 예약금",
payment.getBuyerEmail(),
payment.getBuyerName());
}
kcp 결제 화면
카카오페이 결제 화면
💡 결제 취소가 되는 경우의 수
결제 정보 저장 및 예약 상태 변경
과정에서 예외가 발생한 경우우선, Iamport의 결제 취소 API는 토큰을 요구하므로 토큰을 먼저 발급받자.
@GetMapping("/token")
public ResponseEntity<String> getToken() throws IOException, JSONException {
final HttpURLConnection conn = getTokenConnection();
final JSONObject obj = getJsonObject();
sendRequest(conn, obj);
final int responseCode = getResponseCode(conn);
if (responseCode != 200) {
return ResponseEntity.badRequest().build();
}
final BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
final String accessToken = getResponse(br);
disconnect(conn, br);
return ResponseEntity.ok(accessToken);
}
@PostMapping("/cancel/{id}")
public ResponseEntity<String> refund(
final HttpServletRequest httpServletRequest,
@RequestBody final String merchantUid,
@PathVariable final Long appointmentId) {
final String token = extractor.extract(httpServletRequest, "Bearer");
final ResponseEntity<String> response
= executePaymentCancel(token, merchantUid, appointmentId);
return response;
}
try {
validateAppointmentPayStatus(appointmentId);
final var iamportClient = new IamportClient(API_KEY, API_SECRET);
final Payment payment = extractPayment(impUid, iamportClient);
return new ResponseEntity<>(paymentService.save(appointmentId, payment), responseHeaders, HttpStatus.OK);
} catch (IamportResponseException | IOException e) {
log.error("{}", e);
//예외 발생시 결제를 취소
final String token = refundController.getToken().getBody();
refundController.refundWithToken(token, merchantUid, appointmentId);
return ResponseEntity.badRequest().build();
}
우선은 초안으로 결제 기능과 환불 기능을 완료하였지만,
갑작스럽게 도입한 기능이다보니 아직 팀원과 상세 요구사항이 명세되지 않아 이를 맞추어 가야 할 것 같다.
뭐요