티스토리 뷰

구매자들말고 판매자들이 자신들의 수익금을 정산받기 위한 계좌번호를 등록해야 하는데, 이 계좌번호와 예금주가 동일한지 확인을 해줘야 한다. 실수로 계좌번호를 잘못 입력할 수도 있고, 이름을 잘못 입력할 수도 있으니 수익금이 잘못 입금되거나 거절되면 골치아파지기 때문이다.

 

그러면 계좌실명조회는 어떻게 할 수 있을까? 이 부분은 토스페이먼츠에서 제공하지 않는다. 금융결제원에서 통합으로 제공하는데 오픈 API를 이용해서 진행할 수 있다.

 

https://developers.kftc.or.kr/dev/doc/open-banking

 

금융결제원 오픈API 개발자사이트

 

developers.kftc.or.kr

위 사이트를 들어가서 보면 

2.4.1. 계좌실명조회 API
설 명
이용기관이 특정 계좌의 계좌번호와 예금주 실명번호를 보유하고 있는 경우 해당 계좌의 유효성 및 예금주성명을 확인합니다.
계좌실명조회 API : 실명조회

계좌 실명 조회를 할 수 있는 API를 제공하는것을 확인할 수 있다.

 

참고로 이것은 TEST용 API로서 미리 mock데이터를 넣어논 것에 대해서만 응답을 해준다. 따라서 실제 서비스에 적용하기 위해서는 금융결제원 통합포털에 회원가입을 해야하는데, 이때 사업자 등록번호와 사업자 등록증이 필요하다. 이부분이 충족되면 그때 신청하기로 하고 일단 테스트 api를 이용해서 해보자.


계좌실명조회

https://developers.kftc.or.kr/dev/doc/open-banking

보면 Header에 Access_Token을 넣으라고 명시해주고 있다. 이때 금융결제원에서는 2가지 방법으로 accessToken을 발급해주는데
3-way 방식으로 사용자가 개입하는 방식과, 2-way 방식의 금융결제원과 우리쪽만의 통신으로 발급받는 방법이 있다.

 

나는 사용자의 개입없이 서비스측에서 AccessToken을 발급받고 -> 해당 토큰을 이용해서 계좌실명조회 API를 사용할 것이므로 두번째 방법으로 발급받도록 하겠다.

이용기관 Access Token 발급

아주 자세하게 HTTP 요청을 어떻게 보내야하는지 나와있다. 이를 토대로 요청을 보내도록 해보자.

 

client_id, client_secret

  • 위에서 말한 클라이언트 ID와 Secret은 마이페이지의 API Key 관리에 가면 확인할 수 있다.

scope

  • 고정값인 "oob"로 해준다.

grant_type

  • 고정값인 "client_credentials"로 해준다.

테스트 결과

금융결제원 테스트시 전송 주소는 {https://testapi.openbanking.or.kr/oauth/2.0} 으로 라이브 전송 주소의 open과 다름에 유의하자.

정상적으로 AccessToken을 발급 받았으니 이 토큰값을 이용해서 요청을 보내보자.

 

응답정보 관리 Mock 데이터 추가

포스트맨을 이용해 API  테스트

  • 금융결제원에 응답정보관리에 추가한 값이 그대로 조회되어 반환되는 것을 확인할 수 있다.

응답 전문

{
    "api_tran_id": "d8b434c2-410a-4a4f-878c-6249c128ae40",
    "rsp_code": "A0000",
    "rsp_message": "",
    "api_tran_dtm": "20220321155503170",
    "bank_tran_id": "M202200352U987654323",
    "bank_tran_date": "20190101",
    "bank_code_tran": "097",
    "bank_rsp_code": "000",
    "bank_rsp_message": "",
    "bank_code_std": "004",
    "bank_code_sub": "0970001",
    "bank_name": "KB국민은행",
    "account_num": "_계좌_번호_",
    "account_holder_info_type": "",
    "account_holder_info": "_실명_번호_",
    "account_holder_name": "김바보",
    "account_type": "1",
    "savings_bank_name": "",
    "account_seq": "001"
}

위에서 Postman을 통해 API가 정삭적으로 작동되는것을 확인했으니 이제 실제 코드로 작성해보자.

엑세스 토큰 발급 구현

OpenApi Controller

@Api(tags = "14. 금융결제원 OpenAPI")
@RestController
@RequestMapping("/v1/api/openapi")
@RequiredArgsConstructor
public class OpenApiController {

	private final OpenApiService openApiService;
	private final ResponseService responseService;
	private final int FAIL = -1;

	@PostMapping("/token")
	@ApiOperation(value = "금융결제원 AccessToken 발급")
	public CommonResult requestOpenApiAccessToken() {

		try {
			openApiService.requestOpenApiAccessToken();
			return responseService.getSuccessResult();
		} catch (Exception e) {
			return responseService.getFailResult(
					FAIL,
					e.getMessage()
			);
		}
	}
}
  • 엑세스 토큰을 발급받는다.

OpenApi Service

@Service
@RequiredArgsConstructor
public class OpenApiService {

	private final AccessTokenRepository accessTokenRepository;

	@Value("${openapi.client_id}")
	String clientId;

	@Value("${openapi.client_secret}")
	String clientSecret;

	@Transactional
	public void requestOpenApiAccessToken() {

		RestTemplate rest = new RestTemplate();

		URI uri = URI.create("https://testapi.openbanking.or.kr/oauth/2.0/token");

		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

		MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
		param.add("client_id", clientId);
		param.add("client_secret", clientSecret);
		param.add("scope", "oob");
		param.add("grant_type", "client_credentials");

		String now = LocalDateTime.now(ZoneId.of("Asia/Seoul")).toString();

		if (accessTokenRepository.findFirstByExpireDateAfter(now).isEmpty()) {
			OpenApiAccessTokenDto newAccessTokenRes;
			try {
				newAccessTokenRes = rest.postForObject(
						uri,
						new HttpEntity<>(param, headers),
						OpenApiAccessTokenDto.class
				);
			} catch (Exception e) {
				throw new BussinessException(e.getMessage());
			}
			accessTokenRepository.save(newAccessTokenRes.toEntity());
		}
	}
}
  • DB에 저장한 엑세스 토큰의 만료시간이 남아있다면 놔두고, 만료되었다면 새로 발급받는다.

OpenApi DTO

@Data
public class OpenApiAccessTokenDto {
	String access_token;	// 오픈뱅킹에서 발급해준 엑세스 토큰
	String token_type;		// 토큰유형 Bearer
	String expires_in;		// 토큰 만료 기간 (초)
	String scope;			// 토큰 권한 범위 oob
	String client_use_code;	// 이용기관코드

	public OpenApiAccessToken toEntity() {
		LocalDateTime expireDate = LocalDateTime
				.now(ZoneId.of("Asia/Seoul"))
				.plusSeconds(Integer.parseInt(expires_in));

		return OpenApiAccessToken.builder()
				.accessToken(access_token)
				.tokenType(token_type)
				.expireDate(expireDate.toString())
				.scope(scope)
				.clientUseCode(client_use_code)
				.build();
	}
}
  • 응답값을 받고 처리하는 DTO
  • 엔티티로 저장할 때 만료까지 남은 초를 이용해서 만료날짜를 구해서 저장한다.

OpenApi Entity

public class OpenApiAccessToken {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "seq", nullable = false)
	private Long seq;

	@Setter
	@Column(nullable = false, length = 400)
	private String accessToken;		// 엑세스 토큰

	@Column(nullable = false)
	String tokenType;				// 토큰유형 Bearer

	@Setter
	@Column(nullable = false)
	String expireDate;				// 토큰 만료 날짜

	@Column(nullable = false)
	String scope;					// 토큰 권한 범위 oob

	@Column(nullable = false)
	String clientUseCode;			// 이용기관코드
}
  • 실제 DB에 저장되는 엑세스 토큰 객체이다.
  • 토큰의 길이가 최대 400자이므로 컬럼에 length = 400을 꼭 명시해줘야 한다. (디폴트 길이는 255)

테스트 및 결과

요런식으로 DB에 저장된다.


앞에서 열심히 구현한 것은 결국 계좌실명조회 요청을 보낼 때 헤더에 포함할 AccessToken을 발급받기 위해 한 것이였다. 이제 이 토큰을 가지고 계좌실명조회를 구현해보도록 하자.

계좌실명조회 구현

OpenApi Controller

@PostMapping("/realname")
@ApiOperation(value = "계좌번호 실명 조회 후 등록", notes = "해당 계좌번호와 예금주명이 일치하는지 확인 후 등록합니다.")
public SingleResult<Boolean> requestMatchAccountRealName(
        @ApiParam(value = "코디 번호", required = true) @RequestParam Long crdiSeq,
        @ApiParam(value = "은행 코드", required = true) @RequestParam BANK_CODE bankCode,
        @ApiParam(value = "계좌 번호", required = true) @RequestParam String bankAccount,
        @ApiParam(value = "예금주 성함", required = true) @RequestParam String realName,
        @ApiParam(value = "예금주 생년월일 yyMMdd", required = true) @RequestParam String birthday
) {
    try {
        boolean result = openApiService.requestMatchAccountRealName(crdiSeq, bankCode.getBankCode(), bankAccount, realName, birthday);
        return responseService.getSingleResult(result);
    } catch (Exception e) {
        e.printStackTrace();
        throw new BussinessException(e.getMessage());
    }
}
  • 은행별로 코드가 존재하므로 이에 맞게 은행기관을 전달받는다.
  • 비교할 예금주 성함을 받고, 금융결제원에 요청보낼 때 필요한 생년월일, 은행코드, 계좌번호를 받는다.

참고) 은행코드 Enum 클래스로 생성

@RequiredArgsConstructor
@Getter
public enum BANK_CODE {
	KDB산업은행("002"), SC제일은행("023"), 전북은행("037"),
	IBK기업은행("003"), 한국씨티은행("027"), 경남은행("039"),
	KB국민은행("004"), 대구은행("031"), 하나은행("081"),
	수협은행("007"), 부산은행("032"), 신한은행("088"),
	NH농협은행("011"), 광주은행("034"), 케이뱅크("089"),
	우리은행("020"), 제주은행("035"), 카카오뱅크("090");

	private final String bankCode;
}

OpenApi Service

	@Transactional
	public boolean requestMatchAccountRealName(Long crdiSeq, String bankCode, String bankAccount, String realName, String birthday) {
		if (birthday.length() != 6 || bankAccount.length() > 16) return false;

		RestTemplate rest = new RestTemplate();

		URI uri = URI.create("https://testapi.openbanking.or.kr/v2.0/inquiry/real_name");

		HttpHeaders headers = new HttpHeaders();
		String accessToken = accessTokenRepository.findFirstByExpireDateAfter(LocalDateTime.now(ZoneId.of("Asia/Seoul")).toString())
				.orElseThrow(() -> new BussinessException(ExMessage.ACCESS_TOKEN_ERROR_NOT_FOUND))
				.getAccessToken();
		headers.setContentType(MediaType.APPLICATION_JSON);
		headers.setBearerAuth(accessToken);
		headers.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
		headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

		JSONObject param = new JSONObject();
		String uniqueNum = String.valueOf(System.currentTimeMillis() % 1000000000);
		param.put("bank_tran_id", agentCode + "U" + uniqueNum);
		param.put("bank_code_std", bankCode);
		param.put("account_num", bankAccount);
		param.put("account_holder_info_type", "");
		param.put("account_holder_info", birthday);
		param.put("tran_dtime", LocalDateTime.now(ZoneId.of("Asia/Seoul")).format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));

		OpenApiAccountRealNameDto realNameDto;
		try {
			realNameDto = rest.postForObject(
					uri,
					new HttpEntity<>(param.toJSONString(), headers),
					OpenApiAccountRealNameDto.class
			);
		} catch (Exception e) {
			throw new BussinessException(e.getMessage());
		}

		if (realNameDto == null) {
			throw new BussinessException(ExMessage.UNDEFINED_ERROR);
		}

		if (!realNameDto.getBank_code_std().equals(bankCode)) {
			System.out.println("bankCode = " + bankCode);
			throw new BussinessException(ExMessage.ACCOUNT_ERROR_WRONG_BANK);
		}

		if (!realNameDto.getAccount_holder_name().equals(realName)) {
			throw new BussinessException(ExMessage.ACCOUNT_ERROR_WRONG_NAME);
		}

		if (!realNameDto.getAccount_holder_info().equals(birthday)) {
			throw new BussinessException(ExMessage.ACCOUNT_ERROR_WRONG_BIRTHDAY);
		}

		accountInfoRepository.findByCrdiSeq(crdiSeq)
				.ifPresentOrElse(
						accountInfo -> {
							accountInfo.setAccountNum(realNameDto.getAccount_num());
							accountInfo.setAccountRealName(realNameDto.getAccount_holder_name());
							accountInfo.setBankName(realNameDto.getBank_name());
							accountInfo.setBankCode(realNameDto.getBank_code_std());
							accountInfo.setBirthDay(realNameDto.getAccount_holder_info());
						}, () -> accountInfoRepository.save(
								AccountInfo.builder()
										.crdiSeq(crdiSeq)
										.accountNum(realNameDto.getAccount_num())
										.accountRealName(realNameDto.getAccount_holder_name())
										.bankCode(realNameDto.getBank_code_std())
										.bankName(realNameDto.getBank_name())
										.birthDay(realNameDto.getAccount_holder_info())
										.build()));
		return true;
	}
  • 금융결제원의 계좌실명조회 API를 이용해서 조회하면 

위와과 같은 응답값을 받게 된다. 이를 DTO를 통해 처리하고, 요청 시 받은 정보와 금융결제원에서 반환해준 정보와 비교하여 예금주와 동일한지 여부를 확인한다.

  • 예금주의 정보와 요청 정보에 이상이 없다면 해당 정보를 DB에 저장한다. 이때, 이미 존재하는 경우 정보만 변경하고 없다면 새로 저장하게 하였다.

등록 계좌 정보 조회 컨트롤러 및 서비스 메서드 추가

@GetMapping("/account")
public SingleResult<OpenApiAccountInfoRes> getCrdiAccountInfo(
        @ApiParam(value = "번호", required = true) @RequestParam Long crdiSeq
) {
    try {
        return responseService.getSingleResult(openApiService.getCrdiAccountInfo(crdiSeq));
    } catch (Exception e) {
        e.printStackTrace();
        throw new BussinessException(e.getMessage());
    }
}
@Transactional(readOnly = true)
public OpenApiAccountInfoRes getCrdiAccountInfo(Long crdiSeq) {
    return accountInfoRepository.findByCrdiSeq(crdiSeq)
            .stream().map(AccountInfo::toDto)
            .findFirst()
            .orElseThrow(() -> new BussinessException(ExMessage.MEMBER_ERROR_NOT_FOUND));
}

 

테스트 및 결과

  • 등록 후 조회 결과가 잘 나오고 있다.
참고

해당 테스트들은 전부 금융결제원에서 미리 등록한 Mock 데이터가 반환되는 것들이다. 실제 본인들이 갖고 있는 계좌로 테스트를 하고 싶으면 통합포털사이트에 가서 서비스 실제 등록을 해야 가능하다.

 

금융 결제원을 통해 계좌실명조회를 성공적으로 요청하였고 이를 DB에 정상적으로 저장 후 조회하는 것까지 완료하였다.

 

 


Reference

https://developers.kftc.or.kr/dev/doc/open-banking

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday