배경
운영 환경의 EC2 인스턴스들은 Private Subnet에 있다. 사내에서 특정 서버에 직접 붙어야 할 일이 생겨 보안 그룹 인바운드에 내 공인 IP와 포트를 열었다. 그런데 접속이 되지 않았다. 거부(refused)도 아니고 타임아웃이었다.
처음엔 보안 그룹을 잘못 넣었나 싶어 몇 번을 다시 확인했다. 규칙은 맞았다. 원인은 요청이 아니라 응답이 나가는 길에 있었다.
증상: 요청은 도착하는데 응답이 안 온다
접속이 안 될 때 거부와 타임아웃은 의미가 다르다.
| 증상 | 대체로 의미하는 것 |
|---|---|
| Connection refused | 패킷은 도착했고, 그 포트에서 듣는 프로세스가 없다 |
| Connection timeout | 응답이 돌아오지 않는다. 차단됐거나, 길이 없거나, 길이 다르다 |
타임아웃이었으니 후자였다. 인스턴스 안에서 패킷을 직접 확인해보면 상황이 분명해진다. 들어오는 SYN은 찍히고, 인스턴스도 SYN-ACK을 내보낸다. 그런데 클라이언트에는 그 응답이 도착하지 않는다.
[클라이언트] --- SYN ---> [EC2] 도착한다
[클라이언트] <--- ??? --- [EC2] 내보내는데 안 온다
서버는 자기 할 일을 했다고 생각하고, 클라이언트는 아무것도 못 받은 채 기다린다. 이 상태에서 보안 그룹만 다시 들여다보면 답이 안 나온다.
현재 구조
Private Subnet의 라우트 테이블 구성이다.
| 목적지 | 다음 홉 | 설명 |
|---|---|---|
| VPC CIDR | local | VPC 내부 통신 |
0.0.0.0/0 | NAT Gateway | 기본 외부 통신 |
[my-ip-1]/32 | IGW | 특정 IP (수동 추가) |
[my-ip-2]/32 | IGW | 특정 IP (수동 추가) |
Dev 환경도 동일한 패턴으로 구성되어 있다.

왜 라우트 추가가 필요한가
들어오는 길과 나가는 길은 따로 정해진다
외부에서 EC2로 요청을 보내는 것 자체는 문제가 없다. 요청은 인터넷에서 IGW를 지나 Elastic IP를 가진 EC2까지 정상적으로 도착한다.
문제는 EC2가 응답을 돌려보낼 때다. 응답 패킷의 목적지는 요청자의 공인 IP이고, 이 패킷이 어디로 나갈지는 라우트 테이블이 정한다. 요청이 IGW로 들어왔다는 사실은 여기에 아무 영향을 주지 않는다.
- 해당 IP 라우트가 없으면:
0.0.0.0/0 → NAT Gateway에 매칭된다 - 해당 IP 라우트가 있으면:
[my-ip]/32 → IGW에 매칭된다

들어올 때와 나갈 때 경로가 달라지는 이 상태를 비대칭 라우팅이라고 부른다. 상태를 추적하는 장비나 프로토콜이 중간에 있으면 대개 여기서 끊긴다.
NAT Gateway를 타면 왜 안 되는가
NAT Gateway로 응답이 나가면 출발지 IP가 EC2의 Elastic IP가 아니라 NAT Gateway의 IP로 변환(SNAT)된다. 요청자 입장에서는 A에게 요청했는데 B가 대답하는 셈이라, TCP 세션과 매칭되지 않아 응답이 버려진다.
NAT Gateway 쪽에서도 이 패킷은 낯설다. NAT는 자기가 내보낸 요청에 대한 응답을 되돌려 보내기 위해 세션 테이블을 유지하는데, 이 응답 패킷은 그 테이블에 없는 흐름이다. 안에서 밖으로 나가는 통신을 처리하도록 만들어진 장치라서, 밖에서 시작된 통신의 뒷부분만 떠맡을 수 없다.
IGW를 타면 왜 되는가
IGW는 Elastic IP와 Private IP 사이의 1:1 NAT만 수행한다. 응답이 IGW로 나가면 출발지 IP가 EC2의 Elastic IP로 유지되고, 요청자는 자기가 요청한 그 주소에서 온 응답으로 인식한다. 그래서 세션이 성립한다.

Longest Prefix Match 원리
라우트 테이블은 가장 구체적인 경로를 우선 적용한다.
/32는 단일 IP를 의미한다 (가장 구체적)/0는 모든 IP를 의미한다 (가장 포괄적)
따라서 [my-ip]/32 → IGW가 0.0.0.0/0 → NAT GW보다 항상 우선한다. 해당 IP와의 통신만 IGW를 타고, 나머지 트래픽은 그대로 NAT Gateway를 통해 나간다.
이 성질 덕분에 이 방식이 안전하다. 서브넷 전체의 기본 경로를 바꾸지 않고 특정 IP 하나에만 예외를 만드는 것이라서, 나머지 통신에는 영향이 없다.
외부에서 접근하기 위한 작업 순서
- 라우트 테이블에 IP 추가: 대상 Private RT에
[접근할 IP]/32 → IGW라우트를 추가한다 - 보안 그룹에 IP 허용: 해당 EC2의 보안 그룹 인바운드에 IP와 포트(SSH 22, RDP 3389 등)를 추가한다
- IP 변경 시 양쪽 모두 갱신: ISP에 의해 공인 IP가 바뀌면 라우트 테이블과 보안 그룹을 함께 고쳐야 한다
둘 중 하나만 하면 증상이 다르게 나타난다. 보안 그룹만 열면 이 글의 타임아웃이 되고, 라우트만 추가하면 보안 그룹에서 막혀 역시 타임아웃이 된다. 증상이 같아서 헷갈리기 쉬우니, 접속이 안 되면 두 가지를 같이 확인하는 편이 빠르다.
이 방식의 한계
동작하지만 관리 비용이 붙는다는 점은 분명히 해두는 게 좋다.
- 공인 IP는 바뀐다. 바뀔 때마다 라우트 테이블과 보안 그룹 두 곳을 갱신해야 하고, 갱신을 잊으면 다시 타임아웃을 만난다.
- 접근해야 하는 사람이 늘면
/32라우트가 계속 쌓인다. 누가 왜 추가했는지 적어두지 않으면 나중에 지우지 못하는 항목이 된다. - Private Subnet에 둔 이유가 외부 노출을 줄이기 위해서였는데, 구멍을 하나씩 내는 방식이라 원래 의도와 반대 방향으로 간다.
그래서 상시 접근이 필요하다면 SSM Session Manager처럼 인바운드를 열지 않고 붙는 방법이나, Bastion 또는 VPN으로 진입점을 하나로 모으는 쪽이 낫다. 이 글의 방식은 임시로 열어야 할 때, 혹은 고정 IP를 가진 특정 대상에만 쓰는 예외 수단으로 보는 게 맞다.
참고
- Public Subnet의 라우트 테이블은 기본 라우트가 이미
0.0.0.0/0 → IGW이므로 별도 IP 추가가 필요 없다 - DB Subnet의 라우트 테이블은 NAT Gateway만 연결되어 있고 개별 IP 라우트가 없다 (외부 직접 접근 불가 구조)
배운 점
허용과 경로는 다른 문제였다. 보안 그룹만 열어두고 접속이 안 될 때 한참을 헤맸던 이유는 둘을 같은 것으로 생각했기 때문이다. 보안 그룹은 들어오는 것을 허용할 뿐이고, 응답이 어느 길로 나가는지는 라우트 테이블이 정한다. 요청은 들어왔는데 응답이 NAT로 나가면 연결은 성립하지 않는다.
그래서 네트워크 문제는 방향을 나눠서 본다. 들어오는 경로와 나가는 경로는 별개이고, 둘 다 맞아야 통신이 된다. 타임아웃이라는 증상 앞에서 "막혔나"만 묻지 않고 "돌아오는 길이 있나"를 같이 묻게 된 게 이 일의 소득이다.
경로는 동작만이 아니라 비용과 보안도 결정한다. 같은 원리가 비용으로 드러난 사례가 S3 VPC Gateway Endpoint 점검이고, 기본값으로 잡힌 경로가 통신을 끊은 사례가 Docker 네트워크가 사내 네트워크를 망가뜨렸던 날이다. 세 글 모두 결론은 같다. 트래픽이 어느 길로 가는지 모르면 설계가 끝난 게 아니다.
이어 읽기
- S3 VPC Gateway Endpoint 점검 및 네트워크 비용 최적화: 경로가 비용을 결정한 사례
- Docker 네트워크가 사내 네트워크를 망가뜨렸던 날: 라우팅이 꼬여 통신이 끊긴 사례
- NLB Target Group Unhealthy 원인 분석 및 해결: 통신은 되는데 실패로 보이던 다른 경우