이미지 로딩 중...

TCP/IP와 네트워크 기초 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 24. · 4 Views

TCP/IP와 네트워크 기초 완벽 가이드

개발자라면 반드시 알아야 할 TCP/IP와 네트워크의 핵심 개념을 초급자도 이해할 수 있도록 쉽게 설명합니다. OSI 7계층부터 DNS, API 통신까지 실무에서 바로 활용할 수 있는 네트워크 지식을 담았습니다.


목차

  1. OSI 7계층과 TCP/IP 4계층
  2. TCP vs UDP 차이점
  3. 3-way handshake 동작 원리
  4. IP 주소와 포트 개념
  5. DNS와 도메인 이름 해석
  6. API 통신과 네트워크 계층

1. OSI 7계층과 TCP/IP 4계층

시작하며

여러분이 웹사이트를 개발하고 있는데, API 통신이 제대로 안 되는 문제를 겪어본 적 있나요? "네트워크 오류"라는 메시지만 보고 어디서부터 문제를 찾아야 할지 막막했던 경험 말이에요.

이런 문제는 실제 개발 현장에서 매일같이 발생합니다. 그런데 네트워크 계층 구조를 이해하지 못하면 문제의 원인이 물리적 연결인지, IP 주소 설정인지, 아니면 애플리케이션 코드 문제인지 구분할 수 없습니다.

바로 이럴 때 필요한 것이 OSI 7계층과 TCP/IP 4계층 모델입니다. 이 구조를 이해하면 네트워크 문제를 단계별로 체계적으로 분석하고 해결할 수 있습니다.

개요

간단히 말해서, 이 개념은 네트워크 통신이 일어나는 과정을 여러 계층으로 나누어 각 계층이 특정 역할을 담당하도록 만든 구조입니다. OSI 7계층은 이론적인 모델이고, TCP/IP 4계층은 실제로 인터넷에서 사용되는 모델입니다.

예를 들어, 여러분이 카카오톡으로 메시지를 보낼 때 애플리케이션 계층에서 시작된 데이터가 전송 계층, 네트워크 계층을 거쳐 물리적인 전파로 변환되어 상대방에게 전달됩니다. 기존에는 모든 네트워크 기능이 한 덩어리로 묶여 있었다면, 이제는 각 계층이 독립적으로 동작하면서 서로 협력할 수 있습니다.

계층화의 핵심 특징은 첫째, 각 계층이 독립적이라 한 계층을 수정해도 다른 계층에 영향을 주지 않는다는 점입니다. 둘째, 각 계층이 명확한 책임을 가지고 있어 문제 해결이 쉽습니다.

셋째, 표준화되어 있어 다양한 제조사의 장비가 함께 작동할 수 있습니다. 이러한 특징들이 오늘날 인터넷이 전 세계적으로 연결될 수 있는 기반이 되었습니다.

코드 예제

// 브라우저에서 서버로 HTTP 요청을 보내는 과정 (계층별 역할)

// 7계층 (애플리케이션): 사용자가 보는 데이터
const requestData = {
  url: 'https://api.example.com/users',
  method: 'GET',
  headers: { 'Content-Type': 'application/json' }
};

// 4계층 (전송): TCP 연결 설정 및 포트 지정
const socket = {
  sourcePort: 54321,  // 클라이언트 포트 (임의 할당)
  destPort: 443,      // 서버 포트 (HTTPS)
  protocol: 'TCP'
};

// 3계층 (네트워크): IP 주소로 경로 찾기
const ipPacket = {
  sourceIP: '192.168.1.10',
  destIP: '52.79.123.456',
  data: requestData
};

// 2계층 (데이터링크): MAC 주소로 물리적 전송
const frame = {
  sourceMac: 'AA:BB:CC:DD:EE:FF',
  destMac: 'Gateway MAC',
  payload: ipPacket
};

// 1계층 (물리): 전기 신호나 무선 신호로 변환되어 전송

설명

이것이 하는 일: 네트워크 계층 모델은 복잡한 네트워크 통신을 단계별로 나누어 각 단계가 명확한 책임을 갖도록 만듭니다. 마치 택배 배송 시스템처럼 포장-집하-배송-전달의 단계가 있는 것처럼요.

첫 번째로, 애플리케이션 계층(7계층)은 사용자가 실제로 보는 데이터를 다룹니다. 웹 브라우저에서 'api.example.com/users'로 요청을 보내면, 이 계층에서 HTTP 프로토콜로 요청을 만듭니다.

왜 이렇게 하느냐면, 사용자는 복잡한 네트워크 구조를 알 필요 없이 간단한 URL만으로 통신할 수 있어야 하기 때문입니다. 그 다음으로, 전송 계층(4계층)이 실행되면서 TCP 연결을 설정합니다.

여기서는 출발지 포트(54321)와 목적지 포트(443, HTTPS용)를 지정하고, 데이터를 안정적으로 전송하기 위한 순서 번호를 붙입니다. 내부에서는 데이터를 작은 세그먼트로 나누고, 각 세그먼트가 잘 도착했는지 확인하는 ACK 메시지를 주고받습니다.

네트워크 계층(3계층)에서는 IP 주소를 사용해 출발지(192.168.1.10)에서 목적지(52.79.123.456)까지 가는 최적의 경로를 찾습니다. 데이터링크 계층(2계층)은 같은 네트워크 내에서 MAC 주소를 사용해 다음 장비(보통 공유기나 게이트웨이)로 데이터를 전달하고, 물리 계층(1계층)이 이를 전기 신호나 빛, 무선 신호로 변환하여 케이블이나 공중을 통해 전송합니다.

여러분이 이 구조를 이해하면 네트워크 문제가 발생했을 때 "물리적 연결 확인 → IP 설정 확인 → 방화벽/포트 확인 → 애플리케이션 로직 확인" 순서로 체계적으로 접근할 수 있습니다. 또한 성능 최적화 시 어느 계층에서 병목이 발생하는지 파악할 수 있고, 보안 설정 시 각 계층별로 적절한 보안 조치를 취할 수 있습니다.

실전 팁

💡 네트워크 문제 디버깅 시 하위 계층부터 확인하세요. 물리적 연결(케이블, Wi-Fi)이 안 되어 있으면 아무리 코드를 고쳐도 소용없습니다.

💡 API 개발 시 주로 7계층(애플리케이션)과 4계층(전송)만 신경 쓰면 됩니다. HTTP/HTTPS는 7계층, TCP/UDP는 4계층 프로토콜입니다.

💡 브라우저 개발자 도구의 Network 탭에서 각 요청의 계층별 정보를 확인할 수 있습니다. Headers(7계층), Status(4계층) 등을 보세요.

💡 Docker나 Kubernetes 사용 시 네트워크 격리는 주로 3계층(IP)과 4계층(포트)에서 이루어집니다. 이를 이해하면 컨테이너 간 통신 설정이 쉬워집니다.

💡 성능 최적화 시 CDN은 7계층에서, 로드 밸런서는 4계층 또는 7계층에서 동작합니다. 각 계층의 특성을 알면 적절한 도구를 선택할 수 있습니다.


2. TCP vs UDP 차이점

시작하며

여러분이 실시간 채팅 앱을 만들고 있는데, 메시지가 가끔 늦게 도착하거나 중복으로 오는 문제를 겪어본 적 있나요? 또는 동영상 스트리밍 서비스를 만들 때 화질은 조금 떨어져도 끊김 없이 재생되게 하고 싶었던 경험이 있나요?

이런 문제는 전송 계층 프로토콜 선택과 직결됩니다. 메시지 하나하나가 정확히 전달되어야 하는 경우와, 약간의 손실은 감수하더라도 빠른 전송이 중요한 경우는 다르게 접근해야 합니다.

바로 이럴 때 필요한 것이 TCP와 UDP의 차이를 이해하는 것입니다. 상황에 맞는 프로토콜을 선택하면 애플리케이션의 성능과 사용자 경험을 크게 향상시킬 수 있습니다.

개요

간단히 말해서, TCP는 "신뢰성 있는 배달 보증 택배"이고, UDP는 "빠른 일반 우편"입니다. TCP는 데이터가 순서대로 정확히 도착하는 것을 보장하고, UDP는 보장 없이 빠르게 보냅니다.

TCP가 필요한 경우는 웹 페이지 로딩, 파일 다운로드, 이메일 전송, 데이터베이스 쿼리처럼 데이터 정확성이 중요한 모든 상황입니다. 예를 들어, 은행 앱에서 송금할 때 금액 정보가 틀리면 큰일 나겠죠.

반면 UDP는 실시간 게임, 화상 통화, 라이브 스트리밍처럼 속도가 중요하고 약간의 데이터 손실은 괜찮은 경우에 사용됩니다. 기존에 모든 통신에 TCP를 사용했다면, 이제는 상황에 따라 UDP를 선택하여 지연 시간을 크게 줄일 수 있습니다.

TCP의 핵심 특징은 연결 지향(3-way handshake로 연결 설정), 순서 보장(패킷에 번호를 매김), 오류 검출 및 재전송(ACK 응답)입니다. UDP의 특징은 비연결성(연결 설정 없이 바로 전송), 순서 무보장, 오류 검출만 하고 재전송 안 함입니다.

이러한 차이가 각 프로토콜의 성능과 신뢰성을 결정합니다.

코드 예제

// Node.js에서 TCP 서버 (신뢰성 있는 통신)
const net = require('net');

// TCP 서버: 클라이언트와 안정적인 연결 유지
const tcpServer = net.createServer((socket) => {
  console.log('클라이언트 연결됨 (TCP)');

  socket.on('data', (data) => {
    // 데이터가 순서대로, 빠짐없이 도착함을 보장
    console.log('받은 데이터:', data.toString());
    socket.write('데이터 잘 받았습니다'); // 응답 보장됨
  });

  socket.on('end', () => {
    console.log('클라이언트 연결 종료');
  });
});

tcpServer.listen(3000);

// UDP 서버 (빠른 통신, 신뢰성 낮음)
const dgram = require('dgram');
const udpServer = dgram.createSocket('udp4');

udpServer.on('message', (msg, rinfo) => {
  // 메시지가 순서 없이, 때로는 누락되어 도착할 수 있음
  console.log(`UDP 메시지: ${msg} from ${rinfo.address}:${rinfo.port}`);
  // 응답을 보내도 도착 보장 안 됨
  udpServer.send('OK', rinfo.port, rinfo.address);
});

udpServer.bind(4000);

설명

이것이 하는 일: TCP와 UDP는 네트워크의 전송 계층에서 데이터를 주고받는 두 가지 방식입니다. TCP는 전화 통화처럼 연결을 먼저 맺고 대화하고, UDP는 엽서를 보내듯 그냥 던져 보냅니다.

첫 번째로, TCP 서버는 net.createServer()로 만들어지며, 클라이언트가 연결하면 소켓(socket) 객체가 생성됩니다. 이 소켓은 연결이 유지되는 동안 계속 존재하며, 데이터를 주고받을 수 있습니다.

왜 이렇게 하느냐면, 연결을 유지함으로써 패킷이 손실되었을 때 재전송을 요청하고, 순서가 뒤바뀐 패킷을 재정렬할 수 있기 때문입니다. TCP의 내부 동작을 보면, 클라이언트가 'Hello'라는 5바이트 데이터를 보내면 이것이 하나 이상의 TCP 세그먼트로 나뉩니다.

각 세그먼트에는 순서 번호(Sequence Number)가 붙고, 서버는 받으면 확인 응답(ACK)을 보냅니다. 만약 ACK가 일정 시간 내에 안 오면 클라이언트는 자동으로 재전송합니다.

이 모든 과정이 자동으로 이루어지므로 개발자는 socket.write()만 호출하면 됩니다. 반면 UDP 서버는 dgram.createSocket()로 만들며, 연결 개념이 없습니다.

각 메시지는 독립적이고, 누가 보냈는지 정보(rinfo)와 함께 도착합니다. UDP는 메시지를 보내면 그냥 네트워크에 던지고 끝입니다.

재전송도, 순서 보장도 없습니다. 하지만 이 덕분에 TCP의 3-way handshake(연결 설정에 3번의 패킷 교환)가 없어서 지연 시간이 획기적으로 줄어듭니다.

여러분이 실시간 게임을 만든다면 UDP를 선택하세요. 플레이어의 위치 정보가 0.1초 전 데이터로 재전송되는 것보다, 약간 끊겨도 최신 위치를 받는 게 낫습니다.

하지만 채팅 메시지나 결제 정보는 반드시 TCP를 사용해야 합니다. WebSocket(실시간 채팅)은 내부적으로 TCP를 사용하고, WebRTC(화상 통화)는 UDP를 사용합니다.

HTTP/3는 QUIC이라는 UDP 기반 프로토콜을 사용해 TCP의 신뢰성과 UDP의 속도를 모두 잡으려 합니다.

실전 팁

💡 웹 개발자라면 대부분 TCP(HTTP/HTTPS)만 사용하게 됩니다. UDP는 WebRTC로 화상 통화나 P2P 기능을 만들 때 내부적으로 사용됩니다.

💡 AWS Lambda나 클라우드 함수는 기본적으로 HTTP(TCP)만 지원합니다. UDP가 필요하면 EC2나 컨테이너를 직접 띄워야 합니다.

💡 방화벽 설정 시 TCP와 UDP는 별도로 설정해야 합니다. 포트 3000을 열 때 "TCP 3000"인지 "UDP 3000"인지 명시하세요.

💡 모바일 앱에서 실시간 위치 추적이나 IoT 센서 데이터 전송은 UDP가 배터리를 덜 소모합니다. 연결 유지 오버헤드가 없기 때문입니다.

💡 로컬 네트워크에서 서비스 디스커버리(서버 자동 찾기)는 UDP 브로드캐스트를 많이 사용합니다. 모든 장치에 동시에 메시지를 보낼 수 있기 때문입니다.


3. 3-way handshake 동작 원리

시작하며

여러분이 API 서버에 요청을 보냈는데 "Connection timeout" 오류가 나는 경우를 겪어본 적 있나요? 또는 서버 로그에는 아무 기록도 없는데 클라이언트는 연결이 안 된다고 하는 상황 말이에요.

이런 문제는 TCP 연결 과정에서 발생하는 경우가 많습니다. 클라이언트와 서버가 통신하기 전에 "악수"를 나누는데, 이 과정에서 문제가 생기면 데이터 전송 자체가 시작되지 않습니다.

바로 이럴 때 필요한 것이 3-way handshake 원리입니다. 이것을 이해하면 연결 문제를 빠르게 진단하고, 타임아웃 설정을 적절히 조정하며, DDoS 공격(SYN Flooding)도 이해할 수 있습니다.

개요

간단히 말해서, 3-way handshake는 TCP 연결을 시작할 때 클라이언트와 서버가 세 번의 메시지를 주고받아 "이제부터 데이터 주고받을 준비가 되었다"고 확인하는 과정입니다. 왜 이 과정이 필요한지 실무 관점에서 보면, 서버가 살아있는지 확인하고, 서버가 요청을 받을 준비가 되었는지 검증하며, 양쪽 모두 데이터 송수신 능력이 있는지 확인하기 위해서입니다.

예를 들어, 서버가 너무 바빠서 새 연결을 받을 수 없다면 handshake 단계에서 거부하여 불필요한 데이터 전송을 막을 수 있습니다. 기존에는 그냥 데이터를 보내고 응답을 기다렸다면, 이제는 연결부터 확실히 맺고 시작합니다.

3-way handshake의 핵심은 SYN(연결 요청), SYN-ACK(요청 수락 및 역방향 연결 요청), ACK(최종 확인)의 세 단계입니다. 각 단계에서 순서 번호(Sequence Number)를 교환하여 이후 데이터 패킷의 순서를 관리할 수 있게 합니다.

이 과정이 완료되어야 비로소 실제 데이터를 주고받을 수 있습니다.

코드 예제

// Node.js에서 TCP 연결 시 3-way handshake 과정 (내부 동작)
const net = require('net');

// 클라이언트: 서버에 연결 시도
const client = net.createConnection({ port: 3000, host: 'localhost' }, () => {
  // 이 콜백이 실행되는 시점 = 3-way handshake 완료된 시점
  console.log('연결 성공! (3-way handshake 완료)');
  client.write('이제 데이터를 보낼 수 있습니다');
});

// 내부적으로 일어나는 일:
// 1단계 (클라이언트 -> 서버): SYN
//   클라이언트: "연결하고 싶어요. 내 초기 순서번호는 1000입니다"
//   TCP 플래그: SYN=1, seq=1000

// 2단계 (서버 -> 클라이언트): SYN-ACK
//   서버: "OK, 연결 받아들입니다. 내 초기 순서번호는 5000입니다. 당신의 1000도 받았어요"
//   TCP 플래그: SYN=1, ACK=1, seq=5000, ack=1001

// 3단계 (클라이언트 -> 서버): ACK
//   클라이언트: "당신의 5000도 받았습니다. 이제 데이터 보낼게요"
//   TCP 플래그: ACK=1, seq=1001, ack=5001

client.on('error', (err) => {
  // handshake 실패 시 발생 (서버 응답 없음, 타임아웃 등)
  console.error('연결 실패:', err.message);
  // ECONNREFUSED: 서버가 안 떠있음 (2단계에서 RST 받음)
  // ETIMEDOUT: 서버 응답 없음 (방화벽 차단 등)
});

설명

이것이 하는 일: 3-way handshake는 TCP 연결의 시작 의식입니다. 마치 전화를 걸 때 "여보세요" - "네, 듣고 있어요" - "네, 그럼 말씀하세요"와 같은 절차입니다.

첫 번째로, 클라이언트가 net.createConnection()을 호출하면 운영체제의 TCP 스택이 SYN 패킷을 서버로 보냅니다. 이 패킷에는 "나는 1000번부터 데이터를 보낼 거야"라는 초기 순서 번호(ISN: Initial Sequence Number)가 포함됩니다.

왜 이렇게 하느냐면, 이후 데이터를 여러 패킷으로 나누어 보낼 때 순서를 추적하기 위해서입니다. 이 번호는 보안을 위해 랜덤하게 생성됩니다.

두 번째 단계에서, 서버는 SYN 패킷을 받으면 "OK, 네가 1000번부터 보낸다는 거 알았어(ACK=1001). 나는 5000번부터 보낼게(SYN, seq=5000)"라는 의미의 SYN-ACK 패킷을 보냅니다.

내부적으로 서버는 이 연결을 위한 메모리와 리소스를 할당합니다. 만약 서버가 너무 바빠서 새 연결을 받을 수 없다면 이 단계에서 RST(Reset) 패킷을 보내 거부할 수 있습니다.

세 번째이자 마지막 단계에서, 클라이언트는 서버의 SYN-ACK를 받고 "네 5000번도 잘 받았어(ACK=5001)"라는 ACK 패킷을 보냅니다. 이 패킷에는 이미 실제 데이터를 포함시킬 수도 있습니다(TCP Fast Open 기능).

서버가 이 ACK를 받으면 연결이 ESTABLISHED 상태가 되고, 양쪽 모두 데이터를 자유롭게 주고받을 수 있습니다. Node.js에서는 이 시점에 createConnection의 콜백이 실행됩니다.

여러분이 API 클라이언트를 만들 때 타임아웃을 설정하는데, 이 타임아웃은 주로 3-way handshake가 완료되는 시간을 의미합니다. 만약 방화벽이 SYN 패킷을 차단하면 클라이언트는 SYN-ACK를 못 받아서 계속 재시도하다가 타임아웃이 발생합니다.

서버 모니터링에서 "SYN_RECEIVED" 상태가 많이 보인다면 2단계에서 멈춘 연결들이며, 이는 SYN Flooding 공격의 징후일 수 있습니다. 성능 최적화 관점에서는 HTTP/2나 HTTP/3, WebSocket이 한 번의 handshake로 여러 요청을 처리하기 때문에 훨씬 효율적입니다.

실전 팁

💡 curl 명령어에 -v 옵션을 주면 3-way handshake 과정을 볼 수 있습니다. "Connected to"가 나오는 시점이 handshake 완료 시점입니다.

💡 AWS ELB나 Nginx의 connection timeout 설정은 이 handshake 완료까지의 시간입니다. 너무 짧게 설정하면 느린 네트워크의 사용자가 연결하지 못합니다.

💡 Docker 컨테이너나 Kubernetes Pod가 시작될 때 readiness probe가 실패한다면, TCP handshake는 성공하지만 애플리케이션이 준비 안 된 상태일 수 있습니다.

💡 로드 밸런서의 health check는 주기적으로 3-way handshake를 시도합니다. 서버가 살아있지만 바쁘다면 handshake는 성공해도 이후 HTTP 응답이 느릴 수 있습니다.

💡 HTTPS의 경우 3-way handshake 후 TLS handshake가 추가로 2-3번의 왕복이 더 필요합니다. 이것이 첫 연결이 느린 이유입니다. 해결책은 connection reuse와 TLS session resumption입니다.


4. IP 주소와 포트 개념

시작하며

여러분이 서버를 배포했는데 "localhost에서는 되는데 외부에서 접속이 안 돼요"라는 문제를 겪어본 적 있나요? 또는 Docker 컨테이너를 실행했는데 포트 매핑을 잘못해서 서비스가 안 뜨는 경험 말이에요.

이런 문제는 IP 주소와 포트를 제대로 이해하지 못해서 발생합니다. 서버를 0.0.0.0에 바인딩해야 하는지 127.0.0.1에 해야 하는지, 포트 3000과 80의 차이가 무엇인지 모르면 배포 단계에서 막힙니다.

바로 이럴 때 필요한 것이 IP 주소와 포트의 개념입니다. 이것들을 정확히 이해하면 네트워크 설정, 방화벽 규칙, 컨테이너 배포를 자신 있게 할 수 있습니다.

개요

간단히 말해서, IP 주소는 네트워크상에서 컴퓨터의 주소이고, 포트는 그 컴퓨터 안에서 실행 중인 특정 프로그램을 찾아가는 번호입니다. IP 주소가 아파트 동/호수라면, 포트는 그 집의 방 번호라고 생각하면 됩니다.

실무에서 IP 주소는 두 종류를 주로 사용합니다. 사설 IP(Private IP)는 192.168.x.x나 10.x.x.x 같은 것으로 집이나 회사 내부 네트워크에서만 쓰이고, 공인 IP(Public IP)는 전 세계에서 유일한 인터넷 주소입니다.

예를 들어, 여러분의 노트북은 집 공유기로부터 192.168.0.5 같은 사설 IP를 받지만, 인터넷에 나갈 때는 공유기의 공인 IP를 사용합니다. 포트는 0부터 65535까지 있는데, 0-1023은 잘 알려진 포트(Well-known ports)로 HTTP(80), HTTPS(443), SSH(22) 같은 표준 서비스가 사용합니다.

1024-49151은 등록된 포트로 특정 애플리케이션이 사용하고, 49152-65535는 동적 포트로 클라이언트가 임시로 사용합니다. 기존에는 "localhost:3000"이라는 주소의 의미를 몰랐다면, 이제는 이것이 "내 컴퓨터(127.0.0.1)의 3000번 포트에서 실행 중인 프로그램"을 뜻한다는 것을 알 수 있습니다.

코드 예제

// Express 서버의 IP 주소와 포트 설정
const express = require('express');
const app = express();

// 기본 라우트
app.get('/', (req, res) => {
  res.send('서버 실행 중!');
});

// 잘못된 방법: localhost만 바인딩 (외부 접속 불가)
// app.listen(3000, 'localhost'); // 또는 '127.0.0.1'
// 이렇게 하면 같은 컴퓨터에서만 접속 가능

// 올바른 방법: 모든 네트워크 인터페이스에 바인딩
app.listen(3000, '0.0.0.0', () => {
  console.log('서버가 모든 IP에서 3000번 포트로 실행 중');
  // localhost:3000 ✓ (127.0.0.1:3000)
  // 192.168.0.10:3000 ✓ (내 컴퓨터의 사설 IP)
  // 외부 공인 IP:3000 ✓ (방화벽/공유기 설정 시)
});

// 여러 포트에서 동시에 실행하기
const httpServer = app.listen(80, '0.0.0.0');   // HTTP (권한 필요)
const httpsServer = app.listen(443, '0.0.0.0'); // HTTPS (권한 필요)
const devServer = app.listen(3000, '0.0.0.0');  // 개발용

// Docker 환경에서 포트 매핑
// Dockerfile: EXPOSE 3000
// 실행: docker run -p 8080:3000 myapp
// 의미: 호스트의 8080포트 -> 컨테이너의 3000포트

설명

이것이 하는 일: IP 주소와 포트는 네트워크 통신의 목적지를 정확히 지정합니다. 마치 "서울시 강남구 테헤란로 123, 5층 501호"처럼 단계적으로 찾아가는 것입니다.

첫 번째로, IP 주소가 결정합니다 "어느 컴퓨터로 갈 것인가". '0.0.0.0'은 특별한 주소로, 서버가 listen할 때 사용하면 "이 컴퓨터의 모든 네트워크 인터페이스에서 듣겠다"는 의미입니다.

왜 이렇게 하느냐면, 컴퓨터는 보통 여러 IP를 가지고 있기 때문입니다(localhost용 127.0.0.1, 내부 네트워크용 192.168.0.10, VPN용 10.0.0.5 등). '0.0.0.0'에 바인딩하면 이 모든 IP로 들어오는 요청을 받을 수 있습니다.

두 번째로, 포트 번호가 "그 컴퓨터의 어느 프로그램"을 지정합니다. 운영체제는 포트 번호를 보고 패킷을 적절한 프로세스에게 전달합니다.

예를 들어, 여러분의 컴퓨터에서 웹 브라우저가 3000번 포트로 요청을 보내면, 운영체제는 3000번 포트를 listen하고 있는 Node.js 프로세스에게 그 데이터를 전달합니다. 내부적으로는 (IP 주소, 포트, 프로토콜)의 조합이 하나의 소켓을 식별합니다.

실제 클라이언트가 서버에 연결할 때의 흐름을 보면, 클라이언트는 api.example.com:443에 연결하려고 합니다. 먼저 DNS가 api.example.com을 IP 주소 52.79.123.456으로 변환합니다.

그 다음 클라이언트는 임의의 포트(예: 54321)를 자신의 출발지 포트로 선택하고, 목적지는 52.79.123.456:443으로 설정하여 패킷을 보냅니다. 서버는 443 포트에서 이 패킷을 받고, 응답할 때는 출발지 52.79.123.456:443, 목적지 (클라이언트IP):54321로 보냅니다.

여러분이 Docker를 사용할 때 -p 8080:3000이라고 하면, 이것은 "호스트 컴퓨터의 8080 포트로 들어오는 요청을 컨테이너 안의 3000 포트로 전달하라"는 뜻입니다. 컨테이너 안의 앱은 3000번으로 실행되지만, 외부에서는 8080으로 접속하는 것입니다.

AWS 보안 그룹이나 방화벽 설정에서 "80, 443 포트 열기"는 이 포트들로 들어오는 트래픽만 허용한다는 의미입니다. 개발할 때 "이미 사용 중인 포트"라는 오류가 나면 다른 프로그램이 그 포트를 점유하고 있는 것이며, lsof -i :3000(Mac/Linux) 또는 netstat -ano | findstr :3000(Windows)로 확인할 수 있습니다.

실전 팁

💡 개발 환경에서는 localhost(127.0.0.1)에 바인딩하고, 프로덕션에서는 0.0.0.0에 바인딩하세요. 보안을 위해 필요한 인터페이스만 열어야 합니다.

💡 1024 이하의 포트(80, 443 등)는 관리자 권한이 필요합니다. 개발 시 3000, 8080 같은 높은 포트를 쓰고, 프로덕션에서는 Nginx가 80/443을 받아 내부 포트로 프록시하세요.

💡 AWS EC2나 Azure VM의 보안 그룹/NSG 설정을 확인하세요. 서버가 0.0.0.0:3000으로 떠있어도 방화벽이 막으면 접속 안 됩니다.

💡 Kubernetes에서 Service의 port는 서비스가 노출하는 포트, targetPort는 Pod 내부 컨테이너의 포트입니다. 이 둘을 다르게 설정할 수 있습니다.

💡 IPv6 환경에서는 ::이 IPv6의 0.0.0.0입니다. Node.js의 listen(3000)은 기본적으로 IPv6와 IPv4를 모두 지원하려 시도합니다.


5. DNS와 도메인 이름 해석

시작하며

여러분이 "www.google.com은 되는데 우리 회사 API 도메인은 'DNS lookup failed' 오류가 나요"라는 버그 리포트를 받아본 적 있나요? 또는 도메인을 새로 구매해서 설정했는데 "아직 안 열려요"라는 불만을 듣는 상황 말이에요.

이런 문제는 DNS(Domain Name System)의 동작 원리를 모르면 해결하기 어렵습니다. 도메인 이름이 어떻게 IP 주소로 변환되는지, 왜 변경사항이 즉시 반영되지 않는지, 어떻게 최적화할 수 있는지 이해해야 합니다.

바로 이럴 때 필요한 것이 DNS와 도메인 이름 해석 과정입니다. 이것을 알면 도메인 설정, CDN 연결, 로드 밸런싱, 장애 대응을 자신 있게 할 수 있습니다.

개요

간단히 말해서, DNS는 인터넷의 전화번호부입니다. 사람이 기억하기 쉬운 이름(www.naver.com)을 컴퓨터가 사용하는 숫자(223.130.195.95)로 바꿔줍니다.

왜 필요한지 보면, IP 주소는 외우기 어렵고 자주 바뀔 수 있기 때문입니다. 예를 들어, 네이버 서버의 IP가 바뀌어도 도메인 이름은 그대로여서 사용자는 아무 영향을 받지 않습니다.

또한 하나의 도메인이 여러 IP로 연결되어 로드 밸런싱할 수도 있고, 지역별로 가장 가까운 서버로 연결(GeoDNS)할 수도 있습니다. 기존에는 hosts 파일에 모든 도메인과 IP를 수동으로 적었다면, 이제는 DNS 서버가 분산된 데이터베이스 형태로 자동으로 관리합니다.

DNS의 핵심 구성요소는 도메인 등록 기관(가비아, Route53 등), 네임서버(도메인 정보를 저장하는 서버), DNS 레코드(A, AAAA, CNAME, MX 등), TTL(Time To Live, 캐시 유지 시간)입니다. 이러한 요소들이 협력하여 전 세계 어디서든 빠르게 도메인을 IP로 변환합니다.

코드 예제

// Node.js에서 DNS 조회하기
const dns = require('dns');
const util = require('util');
const lookup = util.promisify(dns.lookup);
const resolve4 = util.promisify(dns.resolve4);

// 방법 1: 운영체제의 DNS 설정 사용 (캐시 포함)
async function simpleLookup() {
  try {
    const result = await lookup('www.google.com');
    console.log('IP 주소:', result.address);
    console.log('IP 버전:', result.family); // 4 = IPv4, 6 = IPv6
  } catch (err) {
    console.error('DNS 조회 실패:', err.message);
    // ENOTFOUND: 도메인이 존재하지 않음
    // ESERVFAIL: DNS 서버 오류
  }
}

// 방법 2: 직접 DNS 서버에 질의 (캐시 우회)
async function detailedLookup() {
  try {
    // A 레코드 조회 (도메인 -> IPv4)
    const addresses = await resolve4('www.google.com');
    console.log('모든 IP 주소들:', addresses);
    // ['142.250.207.36', '142.250.207.68'] (로드 밸런싱용 여러 IP)

    // CNAME 조회 (별칭)
    const cnames = await util.promisify(dns.resolveCname)('www.github.com');
    console.log('실제 도메인:', cnames); // ['github.com']

    // MX 레코드 조회 (이메일 서버)
    const mx = await util.promisify(dns.resolveMx)('gmail.com');
    console.log('메일 서버:', mx);
    // [{ exchange: 'gmail-smtp-in.l.google.com', priority: 5 }]
  } catch (err) {
    console.error('DNS 조회 실패:', err);
  }
}

simpleLookup();
detailedLookup();

설명

이것이 하는 일: DNS는 계층적 분산 데이터베이스 시스템으로, 도메인 이름을 여러 단계로 나누어 각 단계를 책임지는 서버가 관리합니다. 마치 우편물이 국가 → 시/도 → 구/군 → 동/리로 분류되어 배달되는 것과 비슷합니다.

첫 번째로, 브라우저나 애플리케이션이 'api.example.com'에 접속하려 하면 먼저 로컬 캐시를 확인합니다. 운영체제는 최근에 조회한 DNS 결과를 일정 시간(TTL) 동안 저장해둡니다.

왜 이렇게 하느냐면, 같은 도메인을 반복 조회할 때마다 DNS 서버에 물어보면 느리기 때문입니다. 캐시에 없으면 다음 단계로 진행합니다.

두 번째로, 시스템에 설정된 DNS 리졸버(보통 ISP의 DNS 서버나 8.8.8.8 같은 공개 DNS)에 질의합니다. 이 리졸버도 자체 캐시를 가지고 있어서 빠르게 응답할 수 있습니다.

캐시에 없으면 리졸버가 대신 조회를 시작합니다. 먼저 루트 네임서버(전 세계에 13개 그룹)에 물어보면 ".com을 관리하는 서버는 여기야"라고 알려줍니다.

그 다음 .com 네임서버에 물어보면 "example.com을 관리하는 서버는 여기야"라고 알려주고, 최종적으로 example.com의 네임서버가 "api.example.com의 IP는 52.79.123.456이야"라고 응답합니다. 내부적으로 DNS 레코드는 여러 타입이 있습니다.

A 레코드는 도메인을 IPv4 주소로, AAAA는 IPv6 주소로, CNAME은 다른 도메인의 별칭으로 매핑합니다. 예를 들어 'www.github.com'은 CNAME으로 'github.com'을 가리키므로, 실제 IP는 'github.com'의 A 레코드에서 가져옵니다.

MX 레코드는 이메일 서버를, TXT 레코드는 SPF(이메일 인증)나 도메인 소유권 검증에 사용됩니다. 여러분이 새 도메인을 설정하거나 변경할 때 "전파(propagation)"에 시간이 걸리는 이유는 TTL 때문입니다.

이전 IP가 TTL 3600초(1시간)로 캐시되어 있다면, 최대 1시간까지는 사람들이 옛날 IP로 접속할 수 있습니다. 그래서 도메인 변경 전에 TTL을 낮게(300초 등) 설정하고, 변경 후 다시 높이는 전략을 씁니다.

CDN(CloudFlare, CloudFront)을 사용하면 DNS 수준에서 가장 가까운 엣지 서버로 연결되어 속도가 빨라집니다. 로드 밸런서를 쓸 때도 DNS Round Robin(하나의 도메인에 여러 IP 등록)이나 AWS Route53의 가중치 기반 라우팅을 활용할 수 있습니다.

실전 팁

💡 nslookup 또는 dig 명령어로 DNS 문제를 디버깅하세요. "dig www.example.com"은 전체 조회 과정과 TTL을 보여줍니다.

💡 프로덕션 배포 전 TTL을 낮추세요(300초 권장). 배포 후 문제 생기면 빠르게 롤백할 수 있습니다. 안정화되면 TTL을 다시 높여(3600초) 성능을 올립니다.

💡 Docker/Kubernetes 환경에서 DNS 캐싱 문제를 겪으면 /etc/resolv.conf 설정과 CoreDNS 설정을 확인하세요.

💡 개발 환경에서 hosts 파일(/etc/hosts, C:\Windows\System32\drivers\etc\hosts)을 수정하면 DNS 없이 도메인을 테스트할 수 있습니다.

💡 AWS Route53, CloudFlare 같은 DNS 서비스는 일반 DNS보다 빠르고 DDoS 방어 기능이 있습니다. 무료로 시작할 수 있으니 활용하세요.


6. API 통신과 네트워크 계층

시작하며

여러분이 프론트엔드에서 백엔드 API를 호출했는데 "CORS 오류가 나요", "요청은 보냈는데 응답이 안 와요", "Postman에서는 되는데 브라우저에서는 안 돼요" 같은 문제를 겪어본 적 있나요? 이런 문제는 API 통신이 실제로 어떤 네트워크 계층들을 거쳐 일어나는지 모르면 해결하기 어렵습니다.

HTTP 요청 하나가 실제로는 DNS 조회, TCP 연결, TLS 핸드셰이크, HTTP 헤더 처리 등 여러 단계를 거칩니다. 바로 이럴 때 필요한 것이 API 통신과 네트워크 계층의 관계입니다.

이것을 이해하면 API 오류를 빠르게 진단하고, 성능을 최적화하며, 보안 문제를 예방할 수 있습니다.

개요

간단히 말해서, API 통신은 네트워크의 모든 계층을 사용하는 종합적인 과정입니다. HTTP/REST API 요청 하나가 애플리케이션 계층부터 물리 계층까지 모든 단계를 거쳐 실행됩니다.

실무 관점에서 보면, 프론트엔드 개발자가 fetch('https://api.example.com/users')를 호출하면 이것이 내부적으로 수십 개의 네트워크 작업으로 변환됩니다. 예를 들어, DNS 조회(도메인 → IP), TCP 3-way handshake(연결 설정), TLS 핸드셰이크(HTTPS 암호화), HTTP 요청 전송, 응답 수신, 연결 종료 또는 재사용 등의 과정이 순차적으로 일어납니다.

기존에는 API 호출을 단순히 "요청 보내고 응답 받기"로만 생각했다면, 이제는 각 계층에서 무슨 일이 일어나는지 이해하고 최적화할 수 있습니다. API 통신의 핵심 요소는 HTTP/HTTPS 프로토콜(7계층), TCP 연결 관리(4계층), IP 라우팅(3계층), 헤더와 바디 구조, 상태 코드(200, 404, 500 등), CORS와 보안 정책입니다.

이러한 요소들이 함께 작동하여 클라이언트와 서버 간 데이터를 안전하고 효율적으로 주고받습니다.

코드 예제

// JavaScript에서 API 호출 시 네트워크 계층별 동작
async function fetchUserData() {
  try {
    // 7계층 (애플리케이션): HTTP 요청 생성
    const response = await fetch('https://api.example.com/users/123', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token-here'
      }
    });

    // 내부에서 일어나는 일:
    // 1. DNS 조회: 'api.example.com' -> IP 주소 (캐시되어 있으면 생략)
    // 2. TCP 3-way handshake: 연결 설정 (이미 열려있으면 재사용)
    // 3. TLS handshake: HTTPS 암호화 협상 (1.5-2 RTT 소요)
    // 4. HTTP 요청 전송:
    //    GET /users/123 HTTP/1.1
    //    Host: api.example.com
    //    Content-Type: application/json
    //    Authorization: Bearer your-token-here
    // 5. 서버 처리 및 응답 대기
    // 6. HTTP 응답 수신:
    //    HTTP/1.1 200 OK
    //    Content-Type: application/json
    //    Content-Length: 156
    //    {"id":123,"name":"John"}

    // 응답 처리
    if (!response.ok) {
      // 4xx: 클라이언트 오류 (잘못된 요청, 인증 실패 등)
      // 5xx: 서버 오류 (서버 다운, 데이터베이스 오류 등)
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();
    return data;

  } catch (error) {
    // 네트워크 계층별 오류 처리
    if (error.message.includes('Failed to fetch')) {
      console.error('네트워크 오류: DNS 실패, TCP 연결 실패, 또는 CORS 문제');
    } else if (error.message.includes('timeout')) {
      console.error('타임아웃: 서버 응답이 너무 느림');
    } else {
      console.error('HTTP 오류:', error.message);
    }
    throw error;
  }
}

// Node.js 서버에서 CORS 설정 (7계층 문제 해결)
const express = require('express');
const app = express();

app.use((req, res, next) => {
  // CORS 헤더 설정 (브라우저 보안 정책)
  res.header('Access-Control-Allow-Origin', 'https://yourfrontend.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'John' });
});

app.listen(443, '0.0.0.0'); // 4계층: 모든 IP의 443 포트에서 대기

설명

이것이 하는 일: API 통신은 클라이언트와 서버 간 데이터를 주고받는 전체 과정으로, 네트워크 스택의 모든 계층이 협력하여 작동합니다. 마치 국제 택배가 발송지 집 → 지역 물류센터 → 공항 → 목적지 공항 → 지역 물류센터 → 수취인 집으로 이동하는 것처럼 단계적입니다.

첫 번째로, 브라우저나 앱에서 fetch()를 호출하면 URL을 파싱합니다. 'https://api.example.com/users/123'에서 프로토콜(https), 도메인(api.example.com), 경로(/users/123)를 추출하고, HTTPS이므로 포트는 기본값 443을 사용합니다.

왜 이렇게 하느냐면, 각 정보가 다른 계층에서 사용되기 때문입니다(도메인은 DNS 조회, 포트는 TCP 연결, 경로는 HTTP 요청에 사용). 두 번째로, 실제 전송 전에 준비 작업이 일어납니다.

DNS 조회로 도메인을 IP로 변환하고(보통 10-100ms, 캐시되면 0ms), TCP 3-way handshake로 연결을 맺습니다(1 RTT, 약 20-100ms). HTTPS이므로 TLS 핸드셰이크가 추가로 필요한데, 이것이 1.5-2 RTT(약 30-200ms)를 더 소비합니다.

이 모든 것이 첫 요청에서는 필수이지만, HTTP/2나 Keep-Alive로 연결을 재사용하면 두 번째 요청부터는 생략됩니다. 세 번째로, 실제 HTTP 요청이 전송됩니다.

브라우저는 HTTP 메시지를 만들고(요청 라인, 헤더, 바디), 이것이 TCP 세그먼트로 분할되며, 각 세그먼트는 IP 패킷으로 캡슐화되고, 최종적으로 이더넷 프레임으로 물리적 전송됩니다. 서버까지 가는 경로에 있는 각 라우터는 IP 헤더를 보고 다음 홉을 결정하지만, HTTP 헤더나 바디는 보지 않습니다(캡슐화 덕분).

서버는 요청을 받으면 역순으로 계층을 올라가며 처리합니다. 물리 계층에서 전기 신호를 수신하고, 데이터링크 계층에서 프레임을 확인하며, 네트워크 계층에서 IP 패킷을 처리하고, 전송 계층에서 TCP 세그먼트를 재조립한 뒤, 마침내 애플리케이션 계층에서 HTTP 요청을 파싱하여 Express 핸들러가 실행됩니다.

응답도 동일한 과정을 역순으로 거칩니다. 여러분이 API 성능을 최적화할 때는 각 계층을 고려해야 합니다.

DNS는 캐싱과 CDN으로(CloudFlare DNS), TCP/TLS 연결은 재사용과 HTTP/2로(Keep-Alive, Connection pooling), HTTP는 압축과 캐싱으로(gzip, ETag, Cache-Control) 최적화합니다. 브라우저 개발자 도구의 Timing 탭에서 "DNS Lookup", "Initial Connection", "SSL", "Waiting(TTFB)" 등을 보면 각 계층의 소요 시간을 확인할 수 있습니다.

CORS 오류는 7계층(애플리케이션)의 보안 정책 문제이고, "Connection refused"는 4계층(TCP)의 연결 실패, "Host not found"는 3계층(DNS) 문제입니다.

실전 팁

💡 브라우저 개발자 도구 Network 탭의 Timing을 꼭 보세요. 병목이 DNS인지, SSL인지, TTFB(서버 처리)인지 한눈에 알 수 있습니다.

💡 API 클라이언트(axios, fetch)에 타임아웃을 반드시 설정하세요. DNS 조회 타임아웃, 연결 타임아웃, 응답 타임아웃을 각각 설정하면 더 정확합니다.

💡 HTTPS를 항상 사용하세요. HTTP는 중간에 내용이 노출되고 변조될 수 있습니다(중간자 공격). Let's Encrypt로 무료 SSL 인증서를 받으세요.

💡 Connection pooling을 활용하세요. Node.js의 http.AgentaxioshttpAgent 옵션으로 연결을 재사용하면 성능이 크게 향상됩니다.

💡 GraphQL이나 gRPC는 HTTP를 다르게 사용합니다. GraphQL은 HTTP POST를 항상 쓰고, gRPC는 HTTP/2의 스트리밍을 활용합니다. 각각의 특성을 이해하고 적절히 선택하세요.


#Network#TCP/IP#API#DNS#HTTP#API,네트워크,인프라

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

WebSocket과 Server-Sent Events 실시간 통신 완벽 가이드

웹 애플리케이션에서 실시간 데이터 통신을 구현하는 핵심 기술인 WebSocket과 Server-Sent Events를 다룹니다. 채팅, 알림, 실시간 업데이트 등 현대 웹 서비스의 필수 기능을 구현하는 방법을 배워봅니다.

API 테스트 전략과 자동화 완벽 가이드

API 개발에서 필수적인 테스트 전략을 단계별로 알아봅니다. 단위 테스트부터 부하 테스트까지, 실무에서 바로 적용할 수 있는 자동화 기법을 익혀보세요.

효과적인 API 문서 작성법 완벽 가이드

API 문서는 개발자와 개발자 사이의 가장 중요한 소통 수단입니다. 이 가이드에서는 좋은 API 문서가 갖춰야 할 조건부터 Getting Started, 엔드포인트 설명, 에러 코드 문서화, 인증 가이드, 변경 이력 관리까지 체계적으로 배워봅니다.

API 캐싱과 성능 최적화 완벽 가이드

웹 서비스의 응답 속도를 획기적으로 개선하는 캐싱 전략과 성능 최적화 기법을 다룹니다. HTTP 캐싱부터 Redis, 데이터베이스 최적화, CDN까지 실무에서 바로 적용할 수 있는 핵심 기술을 초급자 눈높이에서 설명합니다.

OAuth 2.0과 소셜 로그인 완벽 가이드

OAuth 2.0의 핵심 개념부터 구글, 카카오 소셜 로그인 구현까지 초급 개발자를 위해 쉽게 설명합니다. 인증과 인가의 차이점, 다양한 Flow의 특징, 그리고 보안 고려사항까지 실무에 바로 적용할 수 있는 내용을 다룹니다.