이번 4일 간 프로젝트를 집중 있게 하느라 블로그 글 작성도 못하고 코테 준비도 제대로 못했다 ㅜㅜ
프로젝트 초기에는 속도에 대한 반성을 많이 했었는데, 시간이 지나고 프로젝트의 틀이 잡힐수록 그 반성은 큰 오산이라는 것을 알게 되었다. 퀄리티 있고, 클린 한 코드를 작성하려면 꽤나 공들이며 시간을 투자해야 한다는 것을 깨달았다. 즉, 더 효율적인 시간 분배가 관건이다.
이제 프로젝트에서 계획한 기능은 거의 구성이 된 상황이다. 거진 1달이 걸렸다. 계획한 것에 비해 늦었다고 생각하지만 본격적인 리팩터링을 적용해볼 생각이다. 처음으로 혼자서 제대로 된 프로젝트를 하기 때문에 더 애착이 가기도 한다. 이제 반도 안 왔고, 갈 길이 험난할 테지만 이뤄보자~
지난 4일 간 구현 목록
- 카카오 페이 REST API를 이용한 단건 결제, 장바구니 결제, 결제 조회, 결제 취소
- 결제, 카드 정보, 결제 금액 테이블 생성
- 결제 프로세스에서 트래픽 몰렸을 때의 대처 (레코드 락 이용)
- git ignore 수정 (yml 파일)
▶ 결제 조회
카카오 페이 API를 이용하면 결제한 내역의 상세 내용을 조회할 수 있다. API를 통해 받은 데이터중 내게 필요한 데이터를 추려서 해당 테이블들에 저장을 했다.
https://developers.kakao.com/docs/latest/ko/kakaopay/payment-detail
컨트롤러 레이어
@GetMapping("/{userId}")
public Message getDBOrderInfo(@PathVariable String userId) {
return new Message
.Builder(orderService.getOrderInfoList(userId))
.message(MEMBER_ORDER_LIST)
.httpStatus(HttpStatus.OK)
.mediaType(MediaType.APPLICATION_JSON)
.build();
}
@GetMapping("/detail")
public Message getOrderDetail(@RequestParam String tid, @RequestParam String cid) {
return new Message
.Builder(kakaoPayService.getOrderDetail(tid, cid))
.message(ORDER_INFO)
.httpStatus(HttpStatus.OK)
.mediaType(MediaType.APPLICATION_JSON)
.build();
}
- 첫 번째 핸들러는 DB에 저장된 추려진 주문 데이터를 Select 하는 동작이다. 서비스 레이어를 통해 매핑된 SQL을 실행해 주문 목록을 가져온다.
- 두 번째 핸들러는 카카오페이 API를 통해 직접 데이터를 얻는 동작이다. JSON 데이터를 객체로 받아 반환한다.
서비스 레이어
@Transactional(readOnly = true)
public List<OrderInfoDTO> getOrderInfoList(String userId) {
return orderMapper.selectOrderList(userId).stream()
.map(a -> modelMapper.map(a, OrderInfoDTO.class))
.collect(Collectors.toList());
}
- DB를 통해 select를 하는 서비스이다.
@Transactional
public OrderInfoDTO getOrderDetail(String tid, String cid) {
/* 서버로 요청할 헤더*/
HttpHeaders headers = new HttpHeaders();
setHeaders(headers);
/* 서버로 요청할 Body */
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("cid", cid);
params.add("tid", tid);
HttpEntity<MultiValueMap<String, String>> body = new HttpEntity<>(params, headers);
try {
return restTemplate.postForObject(HOST + KAKAP_PAY_ORDER, body, OrderInfoDTO.class);
} catch (RestClientException e) {
log.error(e.getMessage());
}
return null;
}
- kakao api를 통해 직접 요청 후 조회를 한다.
▶ 주문 취소
주문 취소도 Kakao API를 이용한 요청을 통해 쉽게 진행할 수 있다. 요청을 할 경우 주문 취소에 대한 데이터를 여러 JSON으로 반환한다. 이를 객체로 받아 활용하면 될 것이다. 필요한 데이터를 받아 설계한 테이블에 UPDATE를 했다.
https://developers.kakao.com/docs/latest/ko/kakaopay/cancellation
컨트롤러 레이어
@PostMapping("/cancel")
public Message payCancel(@RequestBody Map<String, Object> map){
return new Message
.Builder(kakaoPayService.cancelKakaoPay(map))
.mediaType(MediaType.APPLICATION_JSON)
.httpStatus(HttpStatus.OK)
.message(CANCEL_PAY)
.build();
}
- 필요한 Json 데이터를 Map 객체로 받아 서비스 레이어로 전달한다.
서비스 레이어
@Transactional
public OrderCancelDTO cancelKakaoPay(Map<String, Object> map) {
/* 서버로 요청할 헤더*/
HttpHeaders headers = new HttpHeaders();
setHeaders(headers);
/* 서버로 요청할 Body */
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("tid", String.valueOf(map.get("tid")));
params.add("cancel_amount", String.valueOf(map.get("cancelAmount")));
params.add("cancel_tax_free_amount", String.valueOf(map.get("cancelTaxFreeAmount")));
params.add("cid", TEST_CID);
HttpEntity<MultiValueMap<String, String>> body = new HttpEntity<>(params, headers);
try {
OrderCancelDTO responseDTO =
restTemplate.postForObject(HOST + KAKAO_PAY_CANCEL, body, OrderCancelDTO.class);
orderMapper.updateOrder(responseDTO.getTid());
return responseDTO;
} catch (RestClientException e) {
log.error(e.getMessage());
}
return null;
}
- map 객체를 통해 얻어온 파라미터를 가지고 API에 요청을 해 결제 취소 데이터를 객체로 받아온다.
그 후 해당 레코드를 업데이트한다.
▶ 테이블 생성
결제에 필요한 데이터를 DB에 저장하기 위해 테이블을 설계했다.
최대한 필요한 데이터를 추려서 설계해보았다. 결제 금액과 카드 정보는 주문 테이블과 1:1로 매칭 되고, 주문과 상품의 관계를 주지 않은 이유는 여러 개 주문이 있을 경우가 있기 때문이다. ERD 모델을 설계해야 하기 때문에 1:1 관계를 주었고, 실제로 FK를 설정하지 않고 인덱스를 정의했다.
CREATE TABLE CARDINFO
(
tid VARCHAR(30) NOT NULL,
issuer_corp VARCHAR(40) NULL,
issuer_corp_code VARCHAR(30) NULL,
bin VARCHAR(30) NULL,
card_type VARCHAR(5) NULL,
install_month VARCHAR(5) NULL,
interest_free_install VARCHAR(2) NULL,
INDEX `idx_tid` USING BTREE (tid) COMMENT '결제 고유 번호 인덱스' VISIBLE
)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci
COMMENT = '카드 정보 테이블';
CREATE TABLE amount
(
tid VARCHAR(30) NOT NULL,
total INT(10) NULL,
tax_free INT(10) NULL,
vat INT(10) NULL,
point INT(10) NULL,
discount INT(10) NULL,
INDEX `idx_tid` USING BTREE (tid) COMMENT '결제 고유 번호 인덱스' VISIBLE
)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci
COMMENT = '결제 금액 테이블';
SQL과 인덱스 설정
▶ 트래픽
앞선 포스팅에서 정리했다시피 레코드에 락을 거는 방법을 택했다. 하지만 이 같은 방법은 속도가 현저히 느려진다. 실제 서비스에서는 성능이 매우 중요하다 들었기 때문에, 다른 방법을 모색해봐야겠다.
레코드 자체에 락을 거는 것 대신 큐를 통해 주문 요청 데이터를 쌓아둔 후 순차적으로 처리하는 방법도 있을 것 같다. 실행해보자!!
▶ 이메일 서비스
회원가입 시 이메일로 인증번호를 전송하고, redis에 저장하며 인증번호를 검증하는 작업을 진행했다. 하지만 MSA 환경에서 이 같은 작업은 데이터 정합성에 문제를 일으킬 수 있다. 느슨한 결합으로 분리되어 있는 앱이므로, 잘 생각을 해봐야 하는 문제이다. 계속 알아보고 생각을 한번 더 해보며 개선해보자
진행중인 프로젝트
https://github.com/GroovyArea/MyChickenBreastShop
'📕 Spring Framework > Spring Project' 카테고리의 다른 글
OutBox Pattern & Saga Pattern & Transaction (0) | 2022.06.13 |
---|---|
「OutBox Pattern」 활용 (0) | 2022.06.10 |
2022.06.02 「DB 동시성 문제」 (0) | 2022.06.02 |
2022.06.01 「결제 API - Ver.2」 (2) | 2022.06.01 |
2022.05.29 「결제 API」 (0) | 2022.05.29 |