외부 연동에서 마주친 인프라
제휴 연동을 테스트하던 중 인증 API에서 Connection timed out이 발생했습니다. 우리 서버의 NAT IP가 파트너사 허용 목록에서 빠져 있었습니다.
저는 이 프로젝트에서 프론트엔드를 맡았고, 연동을 준비하는 동안 NAT IP, 방화벽, 포트, 테스트 환경 같은 조건이 계속 오갔습니다. 마침 사내 인프라 개발자분께 실제 AWS 구성을 배우고 있던 때라 이번 연동을 네트워크 관점에서 함께 정리해봤습니다. (사실 스터디라기보다 Scott이 알려주고 저는 옆에서 오...만 하고 있음. 늘 감사함)
초기에는 전용망을 포함해 여러 통신 방식이 검토됐습니다. 어떤 데이터를 왜 주고받는지에 따라 보안 요구사항이 정해졌고, 그에 맞춰 연결 방식도 결정됐어요.
#호출 방향에 따라 필요한 IP가 달랐다
연동 준비가 시작되면서 양쪽에서 IP와 포트 정보를 주고받았습니다. 파트너사가 우리를 호출할 때와 우리가 파트너사 API를 호출할 때 필요한 값은 달랐습니다. 우리가 외부 API를 호출할 때 파트너사에서 보는 출발지 IP는 서버의 사설 IP가 아니라 NAT를 거친 공인 IP입니다. 이 글에서는 이를 NAT IP라고 부르겠습니다.
NAT IP가 바뀌면 우리 내부 변경이 파트너사의 방화벽 정책 변경으로 이어질 수 있습니다. 허용 목록에 붙어 있던 /32도 같은 맥락이에요. IP 하나만 가리키는 프리픽스라, 정확히 이 출발지 IP 하나만 허용한다는 뜻이에요.
#연결은 경로만 있다고 되는 게 아니었다
라우팅 테이블은 목적지까지 어디로 보낼지 정합니다.
Destination Target
10.20.0.0/16 local
0.0.0.0/0 nat-...방화벽은 그 트래픽을 통과시킬지 판단합니다. 그래서 외부 통신이 되려면 둘 다 맞아야 합니다. 목적지까지 갈 경로가 있어야 하고, 그 경로의 트래픽이 허용돼 있어야 해요.
API가 연결되지 않을 때 "방화벽이 열렸는가"만 봐서는 부족합니다. 어느 방향의 요청인지, 출발지 IP가 무엇인지, 목적지와 포트가 맞는지, 목적지까지 갈 경로가 있는지를 함께 봐야 합니다. 프로젝트에서 오가던 긴 IP와 포트 목록도 단순한 서버 목록이 아니라, 두 시스템 사이에서 어떤 통신을 허용할지 정리한 목록이었어요.
연동 과정에서는 "양방향으로 방화벽을 열어달라"는 표현도 나왔습니다. 그런데 이 말만으로는 두 시스템이 각각 연결을 시작한다는 뜻인지, 하나의 연결에서 요청과 응답을 말하는 것인지 알 수 없습니다. stateful한 장비는 응답 트래픽을 연결 상태로 추적하지만, stateless한 정책은 반대 방향의 규칙도 따로 필요하다고 배웠어요. 그래서 '양방향'이라는 표현만으로 설정을 판단하기보다, 누가 연결을 시작하는지부터 확인하는 게 안전해 보여요.
양사의 테스트 환경 구성도 달랐습니다. 파트너사는 개발용 테스트 환경과 QA 환경이 나뉘어 있어서, 우리 쪽에서는 시점에 따라 호출 대상을 바꿔야 했습니다.
외부 연동에서 환경 전환은 호스트 변경만으로 끝나지 않았습니다. 목적지와 포트가 열려 있는지, 우리 NAT IP가 파트너사 허용 목록에 등록돼 있는지도 함께 맞아야 했어요.
#Connection timed out이면 어디부터 볼까
이 사례에서 Connection timed out이 발생한 지점을 요청 흐름에 놓고 보면 확인 순서를 좁힐 수 있습니다.
TCP 연결을 만드는 단계에서 timeout이 났다면 HTTP 요청 내용보다 앞선 구간부터 확인합니다.
- 목적지가 맞는가
- DNS가 정상적으로 해석되는가
- 목적지까지 갈 경로가 있는가
- 실제 출발지 IP는 무엇인가
- 우리 쪽 방화벽이 허용하고 있는가
- 파트너사의 허용 목록에 출발지 IP가 있는가
- 해당 포트에 연결할 수 있는가
#네트워크 문제는 중복 처리 문제로 이어질 수 있다
timeout은 발생한 단계에 따라 의미가 다릅니다. 요청이 서버에 전달된 뒤 응답을 기다리는 과정에서 timeout이 발생하면, 서버는 이미 요청을 처리했을 수도 있습니다. 호출자는 성공 여부를 알 수 없고, 재시도 과정에서 같은 이벤트가 다시 전달될 수 있습니다. 같은 프로젝트에서 논의했던 이벤트 식별자도 이 관점에서 이해할 수 있었어요.
TCP가 데이터를 안정적으로 전달한다고 해서 비즈니스 로직까지 한 번만 실행되는 것은 아닙니다. 그래서 사용자 식별자와 이벤트 식별자를 구분해야 합니다.
- 가입
hash(u1 + 가입)저장 - 가입 재시도 (timeout 후)
hash(u1 + 가입)버림 · 재시도 걸러짐 - 탈퇴
hash(u1 + 탈퇴)저장 - 재가입
hash(u1 + 가입)버림 · 새 이벤트인데
집계 가입 1 · 탈퇴 1 — 실제 가입 2 · 탈퇴 1 가입 1건 유실
재시도마다 새 식별자를 만드는 게 아니라, 같은 이벤트의 재시도에서는 같은 식별자를 유지해야 합니다. 결국 중요한 건 UUID냐 해시냐보다 무엇을 하나의 이벤트로 볼 것인지 정하는 일이었습니다.
#요청 하나를 끝까지 따라가기
같이 일하는 인프라 개발자분이 실제 AWS 구성을 놓고 하나씩 설명해 주셨는데, 가장 도움이 된 건 개념이 실제 시스템에서 어떻게 이어지는지 보는 과정이었어요.
이제 NAT IP, 방화벽, 환경 변경, timeout 같은 이야기가 각각 따로 보이지 않습니다. 문제가 생기면 코드부터 보기보다 요청이 어디까지 갔는지부터 확인합니다.
프론트엔드에서 시작한 요청도 브라우저에서 끝나지 않습니다. 서버와 네트워크를 지나 외부 시스템까지 이어집니다. 이번에 얻은 건 네트워크 용어 몇 개가 아니라, 그 요청을 끝까지 따라가며 문제 범위를 좁히는 기준이었습니다.
실제 값과 일부 구성은 일반화했습니다.