팀 프로젝트를 진행하며 결제, 정산 구현을 담당하게 되었다.
글을 사고 팔 수 있는 중개 플랫폼으로,
위와 같은 기능을 구현하기 위해 포트원(PortOne) 에서 제공하는 1) 결제
, 2) 예금주 조회
기능을 사용하고자 했다. 그 중에서도 결제 기능은 다소 순조롭게 구현할 수 있었지만.. 정산 정보 등록시 계좌 검증을 하기 위해 예금주 조회 API를 이용하는 것이 마음대로 되지 않았다.
이를 해결하였던 과정을 정리해볼 겸 글을 작성해 보려고 한다. ✍🏻
💡 예금주 조회 API (V1)
https://developers.portone.io/api/rest-v1/vbank#get%20%2Fvbanks%2Fholder
예금주 조회 API 호출 순서
/users/getToken
)/vbank/holder
)POST /users/getToken
Request Bodyimp_key
,imp_secret
포트원 API를 사용하기 위해서는 인증 토큰(access token)을 매 요청시 마다 Authroization
헤더에 포함하여 사용자 인증을 진행해야 한다. 발급 받은 인증 토큰(access token)은 30분간 유효하며 그 이후에는 재발급이 필요하다.
토큰 발급을 위해서 /user/getToken
을 호출해야 하는데, 이 때 필요한 imp_secret
, imp_key
값을 얻기 위해서 아래 경로로 이동한다.
imp_key
: REST API Keyimp_secret
: REST API Secret 토큰 발급에 필요한 요청 정보인 Secret과 Key는 외부에 노출되면 안되는 값이므로 보안을 위해 서버측에서 처리하기로 했다.
build.gradle
// 포트원에서 제공하는 SDK를 의존성에 추가
repositories {
mavenCentral()
maven { url 'https://jitpack.io' } // 추가
}
dependencies {
// 추가
implementation 'com.github.iamport:iamport-rest-client-java:0.1.6'
}
application.yml
portone:
api:
key: {REST_API_KEY}
secret: {REST_API_SECRET}
PortOneService.java
@Service
public class PortOneService {
private final IamportClient iamportClient;
@Autowired
public PortOneService(@Value("${portone.api.key}") String key,
@Value("${portone.api.secret}") String secret) {
this.iamportClient = new IamportClient(key, secret);
}
public String issueToken() {
return iamportClient.getAuth().getResponse().getToken();
}
PortOneApiController.java
@Controller
@RequestMapping("/api/portone")
@RequiredArgsConstructor
public class PortOneApiController {
private final PortOneService portOneService;
@GetMapping("/issueToken")
public ResponseEntity<String> issueToken() {
return ResponseEntity.ok(portOneService.issueToken());
}
GET /vbanks/holder
Request HeaderAuthorization: Bearer {ACCESS_TOKEN}
Request Querybank_code
,bank_num
문제는 여기서 발생했다. javascript fetch를 통해 access token을 발급 받고, 다시 한번 fetch로 아래의 코드를 이용해 예금주 조회 API를 호출했지만 돌아오는 것은 빈 화면뿐..
fetch('https://api.iamport.kr/vbanks/holder?bank_code={은행코드}&bank_num={계좌번호}', {
method: 'get',
headers: {
'Content-Type': 'application/json',
'Authorization': 'bearer {ACCESS_TOKEN}'
}
})
.then(response => response.json())
.then(json => console.log(json));
개발자 도구를 확인해봐도 설정을 제대로 안해놨었는지 (특히 콘솔 탭 설정?) 오류가 제대로 뜨지 않아서 뭐지.. 뭐가 잘못된거지? 고민하다가 Postman
으로 호출해보았다.
근데 잘만 된다. ㅠ__ㅠ
이래 저래 헛짓을 하다가 콘솔 설정을 다시 하고 보니 오류 메시지가 아래와 같이 나타났다.
CORS..? 그게 뭔데..? 왜 내 앞길을 막아
이에 관련해서 좀 더 찾아보던 찰나에 포트원 Github Issue에서 아래와 같은 문구를 발견하게 된다.
결국, 자바스크립트로 호출하면 안된다는 것 이었다. Secret과 Key를 클라이언트를 통해 전달하면 노출될 수 있으니 클라이언트에서 호출하는 것을 막아 놓은 것 같다.
하지만 ... 나의 삽질은 계속 되었다 ^___^ㅎ..
클라이언트
에서 하면 되는거 아니야? CORS
에러를 봤으면서 왜 이런 생각을 했을까?;;클라이언트
호출을 시도 했던 것 같은데.서버
측에서 호출하도록 설정이 되어있었던 것이 아닐까 싶음! (아마도...)아무튼 이번에는 서버
측에서 예금주 조회 API를 호출해보자!
아쉽게도 포트원 SDK(IamportClient
)에서 예금주 조회 API 기능을 지원하지 않는 듯 하여, RestTemplate
을 이용해 요청하는 방식을 이용하였다.
PortOneService.java
@Service
public class PortOneService {
private final RestTemplate restTemplate;
private final IamportClient iamportClient;
@Autowired
public PortOneService(@Value("${portone.api.key}") String key,
@Value("${portone.api.secret}") String secret,
RestTemplateBuilder restTemplateBuilder) {
this.iamportClient = new IamportClient(key, secret);
this.restTemplate = restTemplateBuilder.build();
}
public String issueToken() {
return iamportClient.getAuth().getResponse().getToken();
}
public CommonResponse getAccountHolder(String bankCode, String bankNum) throws URISyntaxException {
try {
String accessToken = issueToken();
String fullUrl = UriComponentsBuilder.fromHttpUrl(IamportClient.API_URL + "/vbanks/holder")
.queryParam("bank_code", bankCode)
.queryParam("bank_num", bankNum)
.toUriString();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);
RequestEntity<Object> requestEntity = new RequestEntity<>(headers, HttpMethod.GET, new URI(fullUrl));
IamportResponse iamportResponse = restTemplate.exchange(requestEntity, IamportResponse.class).getBody();
return new CommonResponse(true, null, iamportResponse.getResponse());
} catch (HttpClientErrorException e) {
return new CommonResponse(false, "계좌 검증 실패", null);
}
}
}
RestTemplate
을 생성자 주입getAccountHolder()
: 검증 정보를 받아 올바른 계좌 정보인 경우 계좌 소유자를 반환함bankCode
는 금융결제원 표준코드 3자리PortOneApiController.java
@RestController
@RequestMapping("/api/portone")
@RequiredArgsConstructor
public class PortOneApiController {
private final PortOneService portOneService;
@RequestMapping("/vbank")
public ResponseEntity<CommonResponse> isAccountValid(@RequestBody UserSettlementDTO userInfo) throws URISyntaxException {
return ResponseEntity.ok(portOneService.getAccountHolder(userInfo.getBankCode(), userInfo.getAccountNumber()));
}
}
isAccountValid()
: Service의 return 값을 응답CommonResponse.java
@Getter
@Setter
public class CommonResponse {
private Boolean success;
private String message;
private Object response;
public CommonResponse(Boolean success) {
this(success, null, null);
}
public CommonResponse(Boolean success, String message) {
this(success, message, null);
}
public CommonResponse(Boolean success, Object response) {
this(success, null, response);
}
public CommonResponse(Boolean success, String message, Object response) {
this.success = success;
this.message = message;
this.response = response;
}
}
RestTemplate
을 이용하여 서버
측에서 직접 HTTP 요청을 날려서 예금주 조회 API를 응답받아 해당 결과를 다시 클라이언트
측에 전달하는 방식으로 구현하게 되었다.
더 나은 코드나 예제가 있을 수도 있겠지만 우선 다시 한번 정리하고자 하는 마음으로 기록한다!🤣