시작하며
안녕하세요, 카카오페이 결제 플랫폼 파티 테드입니다. 저는 결제 플랫폼 파티에서 결제 공통 부분을 담당하고 있습니다. 결제를 하기 위해 필요한 현금영수증 발행부터 시작해서 결제 증빙 제공, 환불, 결제수단 제공, 결제 통합 내역 관리, 리스크 관리 등에 이르기까지 말 그대로 결제와 관련된 다양한 업무를 맡고 있습니다.
업무를 하다 보면 예상하지 못했던 난관에 부딪히곤 하는데요, 저 역시 마찬가지였습니다. 이 글에서는 최근 개발 지원이 충분하지 않았던 A 업체와 연동하면서 있었던 여러 가지 난관과 해결 과정을 이야기를 해보고자 합니다. 본격적으로 이야기를 풀어가기 전에 배경을 말씀드리겠습니다. 이전에는 A 업체와의 기존 계약이 만료되는 시점에 수동으로 가맹점에 일일이 안내 메일을 보내야 했고, 저는 이를 자동화하여 업무 효율을 높이고 싶었습니다.
그런데, 막상 연동하려고 보니 예상하지 못했던 큰 난관에 부딪혔습니다. 가장 큰 난관은 이전에 경험하지 못했던 A 업체의 레거시한 개발 환경과 라이브러리를 적용하는 것이었습니다. 구체적으로 크게 망 분리 존에서의 통신, 설계도가 없는 API 개발, 물리 서버와 클라우드 서버의 차이, SNI (Server Name Indication)1 설정 등 여러 가지 난관이 있었습니다.
이 글에서는 프로젝트 진행 단계별로 이러한 난관을 어떻게 해결했는지 이야기해 보려고 합니다. 기존에 개발하던 환경이 아닌, 10년도 더 된 레거시 시스템과 연동하거나 서로 맞지 않는 환경에서 개발해야 하는 분들에게 도움되기를 바랍니다.
시스템 오픈까지 험난한 과정(고전 library jar 연동기)
카카오페이 개발 과정
우선 얘기하기에 앞서, 카카오페이에서의 간단한 개발 과정을 먼저 소개해 드리려고 합니다. 연동 과정에서 특정 부분에서만 문제가 발생한 게 아니라 신기하게도 하나씩 개발 과정을 진행할 때마다 새로운 문제점을 마주했기 때문에, 글의 이해를 돕기 위해 아래와 같이 개발 순서에 따라 글을 나누었습니다.
Step 1. A 업체와 통신환경 확인하기
Step 2. API 프로토콜 정의
Step 3. 개발 서버 테스트
Step 4. 리얼 서버 반영 및 정상 운영 확인
Step 1. A 업체와 통신환경 확인하기(저희는 추가 개발을 할 수가 없어요.)
제휴사와 연동하기 위해서는 전용선이 있는 상황이 아니라면, 아래처럼 직연동할 수는 없습니다.
제휴사를 카카오페이와 직접 연결하면 보안상 여러 가지 문제가 생길 수 있습니다. 때문에, 아래 그림과 같이 GW를 거쳐 연동해야 합니다. 통신 데이터를 적절한 암호화 수준(IPSecVPN, TLS 등)으로 보호하고, 그다음 방화벽으로 접근 제어를 합니다. 추가적으로 GW를 통해서 내부로의 접근을 보호하는 구조로 통신하게 됩니다.
GW 연동을 위해서는 먼저 토큰을 전달해서 해당 API를 호출하는 측이 제휴사가 맞는지 검증하게 되는데요, 헤더에 정해진 형식에 맞추어 카카오페이에서 발행하는 키값을 설정하는 간단한 작업입니다. 해당 작업을 통해서 기본적인 제휴사별 트래픽 관리 및 장애 발생 시 이 키값은 이슈 트래픽 등의 처리를 하는 기본적인 구분 값 역할을 하게 됩니다.
하지만 제휴사 측에서는 연동 라이브러리 유지 보수가 불가하여 추가 개발이 불가능한 상태였고, 이로 인해 기본적인 카카오페이 GW를 사용할 수 없는 상황이 생겼습니다. 전용선을 설치하는 방법도 있겠지만 전용선을 설치하게 된다면 추가 비용이 발생하기 때문에 제휴사 측에서 원하지 않는 방향이기도 했습니다.
그래서 별도의 GW를 구축하여, IPSec VPN으로 통신구간을 보호하고 nginx provisioning을 통해서 내부 서비스 간 데이터를 송수신하는 방법을 생각해냈습니다.
위의 그림처럼 제휴사는 GW를 호출을 하게 되지만 해당 GW는 기존의 다른 제휴사와 쓰던 GW 서버가 아닌, 신규로 발급받은 별도의 GW 서버입니다. 기존의 서버와 다른 점으로는 토큰 값 전달을 하지 않고, URI 분리만으로 카카오페이 내부망으로 들어올 수 있는 GW를 구성한 것입니다.
이렇게 구성하다 보면 몇 가지 보안 검토 사항이 필요했는데요.
- A사 → 카카오페이 통신 시 헤더키 인증 없이 URI 분리만으로 제휴사 구분
ㄴ VPN 통신 구간으로 라우팅 처리하고 URI를 A사 전용 URI로 구성하여 처리 - A사 TLS 1.0만 가능한 상황으로 TLS 1.0 허용 가능 여부
ㄴ 기본적으로 TLS 1.0은 보안 취약점이 있어 IPSec VPN 통해서 추가 암호화를 통한 보안 취약점 해결 - A사 통신 시 https 통신이 아닌 http 통신 허용 여부
다행히도 위의 보안 검토 사항이 모두 다 통과하여 (기술지원팀 감사합니다) A사 → GW 통신을 할 수 있게 되었습니다. 그 후에는 GW로 들어오는 request를 nginx provisioning을 통해서 제가 만든 서버로 직연동하게 하여 통신을 허용할 수 있게 되었습니다.
서비스 구성 절차
서비스 연동을 하기 위한 신청서
phase | 내용 | 상세 내용 |
---|---|---|
plan | 서비스 개선 요구사항 | 리스크 관리 자동화 구현 |
sandbox | 1. 서비스 구성 요청 | 신규 Gateway 구성 |
2. VIP 생성 | 제휴사 연동을 위한 공인 IP 구성 | |
3. 방화벽 정책 | VIP를 통한 접근제어 정책 구성 | |
4. Nginx Provisioning | 내부 서비스 맵핑 | |
real | 1. 서비스 구성 요청 | 신규 Gateway 구성 |
2. VIP 생성 | 제휴사 연동을 위한 공인 IP 구성 | |
3. GSLB 설정 | 서버/센터 이중화 구성을 위한 GSLB 설정 | |
4. CNAME 설정 | 도메인 생성 및 적용 | |
5. IPSec VPN | VPN 터널링 및 통신 설정 | |
6. 방화벽 정책 | VIP를 통한 접근제어 정책 구성 | |
7. Nginx Provisioning | 내부 서비스 맵핑 | |
8. SNI 설정 제거 | A 제휴사 전용 도메인 사용을 위한 설정 제거 |
Step 2. API 프로토콜 정의(API는 어떻게 만들면 될까요?)
이제 통신 단계가 막 끝나고, 데이터를 수신하기 위한 API를 만들어야 하는 시점에 또 다른 문제점이 나타나기 시작했습니다. A 제휴사에 API를 제공하기 위해 어떠한 API가 필요한지 필요한 스펙을 물어봤을 때, 명확한 스펙이 없는 상태에서 POST로 된 API url을 알려주면 호출할 수 있다는 답변을 받았습니다. 처음에는 “어떤 API를 만들어 달라고 요청을 하는 거지..?”라는 생각을 했습니다. API에 대한 스펙이 충분하지 않아 좀 더 질문을 해보았습니다. A사에 header, parameter, body 등과 같은 부분을 어떻게 담아서 보내줄 건지, 응답은 어떻게 내려주면 되는지에 대해서 물어봤지만 안타깝게도 내부 사정상 명확한 답변을 받지 못했습니다.
그러다가 문득 생각난 방법으로 “개발 환경에 우선 서버를 올린 다음에 해당 API로 들어오는 httpServletRequest를 전부다 로그로 찍어보는 건 어떨까?”라는 생각을 하게 되었고, 그렇게 하여 API에 대한 샘플 스펙을 알 수 있게 되었습니다. 그로 인해 알게 된 부분으로는, 해당 제휴사의 통신 방식이 REST 방식이 아닌 SOAP 통신 방식인 부분을 알게 되었고, 해당 프로토콜을 토대로 API를 개발하기 시작했습니다. 이제 API를 다 만들고 로컬 테스트도 완료되었기 때문에 개발 서버에 배포해서 테스트하면 마무리가 되겠다고 생각하고 있었습니다.
Step 3. 개발 서버 테스트(로컬과 K8s 개발 환경 차이로 인한 비극)
개발이 마무리되어 검증을 위해 개발 서버에서 테스트를 진행했는데요, 로컬 환경에서 잘 작동했던 API가 개발 서버 환경에서는 작동하지 않는 문제가 발생했습니다. A 제휴사에서 받은 환경 파일과 key 파일들을 resource 패키지/디렉토리에 넣어두고 사용하고 있었습니다. local 환경에서는 해당 값이 물리적인 파일 위치랑 일치하였기 때문에 문제가 되지 않았던 부분이, K8s로 서버를 올리면서부터 문제가 되기 시작했습니다. 물리 서버를 사용한다면 항상 배포되는 서버가 같은 서버이기 때문에 물리적인 위치에 고정으로 설정 파일들을 위치할 수 있었겠지만, 저희는 K8s 클라우드 환경에 서버를 올려두고 사용하다 보니 어떤 서버에 배포가 될지도 모르는 상황이었고, 그렇다고 해서 모든 서버에 설정 파일을 올려둘 수도 없는 상황이었습니다.
그리고 무엇보다도, 스프링 프레임워크에서 리소스 파일을 가져와서 읽기 위해서는 ClassPathResource를 이용해서 내용을 읽어와야 하지만, 제가 import 했던 jar 파일에서는 리소스 파일에서 읽는 방식이 아닌 물리적인 위치에서 파일을 읽는 방식이었기 때문에 해당 소스로 코드가 작업되어 있지 않았습니다. 그래서 K8s 환경에서는 해당 파일을 읽을 수 없는 너무 난감한 상황이 발생했습니다. 아무리 생각해봐도 jar로 제공받은 원래의 코드 수정은 불가능해 보였고, 방법이 없는 건가 싶었습니다.
그러던 중 문득 떠오른 생각은 “서버가 올라갈 때 Resource에 있는 파일들을 원하는 위치에 복사해 두고, 해당 파일을 읽도록 하면 어떨까?”였고 다행히 해당 방법으로 파일을 읽어올 수 있게 되었습니다.
샘플 코드를 살짝 보여드리자면 아래와 같습니다.@Configuration
class Config() {
private fun init() {
ConfigFilePath.values().forEach {
val file = ClassPathResource(it.path).inputStream // resource 파일 읽기
val tempFile = File.createTempFile(getPrefix(it), getFileName(it)) // 임시 파일 생성
FileUtils.copyInputStreamToFile(file, tempFile) // 읽은 inputStream 을 파일을 임시파일에 copy
val filePath = getFileName(it) // 파일 이름 가져오기 (123.xml, auth.key)
FileUtils.copy(tempFile, File(filePath)) // File 이름으로 파일 생성
FileUtils.moveFileToDirectory(File(filePath), File("${getMovePrefixLocation()}/${getPrefix(it)}"), false) // 지정된 위치로 파일 이동
tempFile.deleteOnExit() // 임시 파일 삭제
}
}
}
enum class ConfigFilePath(val path: String) {
A_FILE("/conf/kakao/123.xml"),
B_FILE("/conf/auth/auth.key");
abstract fun getFileName(): String
}
다행히 이 방식으로 개발 환경에서도 환경 파일과 key 파일 등을 읽을 수 있었습니다. 이렇게 우여곡절 끝에 개발 서버 환경에서 검증까지 완료를 할 수 있게 되었습니다.
Step 4. 리얼 서버 반영 및 정상 운영 확인(개발 서버 리얼 서버 환경 차이로 인한 연결 실패)
다음으로, 개발 환경과 리얼 환경의 망 차이로 인해 리얼 통신이 불가했던 상황에 대한 이야기입니다. 카카오페이 리얼 환경에서는 동일한 공인 IP에 여러 도메인이 호출하는 구조로서, 제휴사(클라이언트)에서 호출 시 SNI 설정으로 정확한 도메인으로 접근하는 정책으로 운영되고 있으며, 따라서 SNI 설정 지원이 가능해야 했습니다. SNI 값이 없으면, 어느 제휴사(클라이언트)가 호출하는지 알 수 없어서, 보안상 서비스 오픈이 불가능할 수도 있는 상황이 발생했습니다.
이후에 생각했던 방법은 새로운 VIP 생성(SNI 옵션 제거) 후 해당 서버가 이전에 만들어 두었던 GW를 호출하고 GW가 다시 제가 만든 서버를 호출하게 만드는 것이었습니다.
이후 제가 서버를 호출할 수 있도록 추가한 방법으로는 아래와 같습니다. Step 1에서 소개 드린 real 8번의 구성 방법입니다.
내용 |
---|
새로운 공인 IP 구성 |
새로운 도메인 설정 |
도메인과 IP 맵핑 |
SNI 기능 OFF |
이후 각종 방화벽 작업과 VPN 작업 |
Nginx 프로비저닝 재작업 |
위와 같은 작업을 진행한 이유로는 SNI 기능이 OFF된 새로운 공인 IP를 가진 GW를 만들어서 클라이언트를 통해서 request가 들어올 때에는 SNI 작업이 필요가 없지만, 다른 서버를 호출할 때에는 SNI 기능을 가진 서버를 추가로 필요했기 때문입니다. 쉽게 생각하면 비행기를 탈 때 직항으로 가는 방법도 있지만, 이번 같은 경우에는 직항으로 갈 수 있는 경로가 없으니… 경유를 통해서 목적지까지 도착하도록 설정한 방법이었습니다. 이후 무사히 연동하고 나서, 기존 데이터(연동 이전 데이터)들을 마이그레이션 하는 작업을 거치고, 실제 연동을 하고 나서 프로젝트를 마무리할 수 있게 되었습니다.
마치며
A 제휴사와 개발을 진행하면서 처음부터 쉽게 개발할 수 있을 거라고 생각하지는 않았습니다. 그렇지만, 생각지도 못한 부분들에서 문제가 하나둘씩 생기기 시작하고, 그로 인해 일정이 자꾸 뒤로 미뤄지면서 개인적으로 스트레스를 많이 받기도 했었습니다. 어려움이 있을 때마다 고민해 보고, 검색도 많이 해보고 기술지원팀의 도움도 많이 받았었습니다. 실제로 모든 문제들을 혼자 힘으로 전부다 해결하려고 했더라면 더 많은 시간을 소비했을 것 같습니다.
이번 프로젝트에서는 단순히 개발만 하는 개발자가 아닌 조금은 더 성장한 개발자가 되었다고 생각합니다. 실제 연동이 어떻게 이뤄지는지와 기존에 당연하다고 생각했던 부분들이 안될 수도 있다는 걸 배웠습니다. “vpn + nginx provisioning”을 했던 부분부터 시작해서, httpServletRequest를 좀 더 생각해 보게 되고, 당연히 쓸 수 있을 것 같았던 ClassPathResource를 사용할 수 없는 상황을 마주했습니다. 운영 환경에서 약간은 생소했던 SNI까지 다뤄야 했습니다. 어떻게 보면 너무도 당연하게 생각하고 있어서 생각지도 못한 문제점들을 하나씩 풀어나가야 했기에 더 많은 걸 배울 수 있었던 경험이었습니다.
이 글을 읽으시는 분들도 이런 문제뿐만 아니라 여러 가지 문제들로 개발의 난항을 겪고 계실 거라 생각됩니다. 이런 상황에서 조금은 멀리서 문제점을 바라보면서 해결 방법을 잘 찾아나간다면, 어떠한 문제점이 있더라도 잘 풀어나갈 수 있을 거라 생각됩니다. 저 또한 앞으로도 어떤 어려움이든 극복해서 고객의 편의와 더 나은 카카오페이를 위해 최선을 다하는 개발자가 되도록 노력하겠습니다. 긴 글 읽어주셔서 감사합니다!
참고 자료
Access a File from the Classpath in a Spring Application