본문으로 건너뛰기

Ch 16.4 UDP 소켓 프로그래밍

UDP(User Datagram Protocol) 소켓 프로그래밍은 연결을 설정하고 해제하는 과정 없이, 데이터를 데이터그램(Datagram) 이라는 패킷 단위로 전송하는 방식입니다.

우편물을 보낼 때처럼, 받는 사람이 실제로 받았는지 확인하지 않습니다. 신뢰성은 낮지만 오버헤드가 적어 전송 속도가 빠릅니다.

TCP vs UDP 핵심 차이
  • TCP: "전화 통화" - 연결 수립 → 통화 → 연결 종료, 순서/수신 보장
  • UDP: "우편 발송" - 그냥 보냄, 순서/수신 보장 없음, 하지만 빠름

1. UDP 소켓의 핵심 클래스

클래스역할
DatagramSocketUDP 소켓. 서버/클라이언트 구분 없이 동일하게 사용
DatagramPacket전송/수신 데이터를 담는 패킷 컨테이너

TCP와 달리 ServerSocket 같은 별도 서버 클래스가 없습니다. 둘 다 DatagramSocket을 사용하며, 서버는 포트 번호를 명시해서 생성합니다.

2. UDP 에코 서버/클라이언트 기본 구현

서버 (수신 측)

import java.net.*;
import java.nio.charset.StandardCharsets;

public class UdpEchoServer {
public static void main(String[] args) throws Exception {
int port = 8888;

// 포트를 지정하여 DatagramSocket 생성 (서버 역할)
try (DatagramSocket socket = new DatagramSocket(port)) {
System.out.println("UDP 에코 서버 시작. 포트: " + port);

byte[] buffer = new byte[1024];

while (true) {
// 수신용 빈 패킷 준비
DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);

// 데이터가 올 때까지 블로킹 대기
socket.receive(receivePacket);

// 수신된 데이터 추출
String message = new String(
receivePacket.getData(),
0,
receivePacket.getLength(),
StandardCharsets.UTF_8
);

InetAddress senderAddr = receivePacket.getAddress();
int senderPort = receivePacket.getPort();
System.out.printf("[수신] %s:%d → \"%s\"%n", senderAddr.getHostAddress(), senderPort, message);

if ("bye".equalsIgnoreCase(message.trim())) {
System.out.println("서버 종료 요청 수신");
break;
}

// 에코: 보낸 곳으로 그대로 반환
String echoMsg = "에코: " + message;
byte[] responseData = echoMsg.getBytes(StandardCharsets.UTF_8);
DatagramPacket sendPacket = new DatagramPacket(
responseData, responseData.length,
senderAddr, senderPort // 보낸 사람에게 응답
);
socket.send(sendPacket);
System.out.printf("[송신] → %s:%d \"%s\"%n", senderAddr.getHostAddress(), senderPort, echoMsg);
}
}
}
}

클라이언트 (송신 측)

import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class UdpEchoClient {
public static void main(String[] args) throws Exception {
String serverHost = "127.0.0.1";
int serverPort = 8888;

// 포트 지정 없이 생성하면 OS가 임의 포트 할당
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress serverAddr = InetAddress.getByName(serverHost);
System.out.println("UDP 클라이언트 시작. 서버: " + serverHost + ":" + serverPort);
System.out.println("메시지를 입력하세요 ('bye'로 종료):");

Scanner scanner = new Scanner(System.in);

while (true) {
System.out.print("> ");
String input = scanner.nextLine();

// 데이터를 바이트로 변환하여 패킷에 포장
byte[] sendData = input.getBytes(StandardCharsets.UTF_8);
DatagramPacket sendPacket = new DatagramPacket(
sendData, sendData.length,
serverAddr, serverPort
);

// 전송 (연결 없이 바로 발송)
socket.send(sendPacket);

if ("bye".equalsIgnoreCase(input.trim())) break;

// 응답 수신
byte[] buffer = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
socket.setSoTimeout(3000); // 3초 응답 없으면 타임아웃
socket.receive(receivePacket);

String response = new String(
receivePacket.getData(), 0,
receivePacket.getLength(),
StandardCharsets.UTF_8
);
System.out.println("서버 응답: " + response);
}
}
}
}

3. UDP 타임아웃과 재전송

UDP는 기본적으로 재전송 기능이 없습니다. 필요하면 애플리케이션 레벨에서 구현해야 합니다.

import java.net.*;
import java.nio.charset.StandardCharsets;

public class UdpReliableSender {
static final int MAX_RETRIES = 3;
static final int TIMEOUT_MS = 2000;

/**
* UDP 전송 후 응답을 기다림. 응답 없으면 재전송 (최대 3회)
*/
static String sendWithRetry(DatagramSocket socket, String message,
InetAddress addr, int port) throws Exception {
byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
socket.setSoTimeout(TIMEOUT_MS);

for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
// 전송
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, addr, port);
socket.send(sendPacket);
System.out.printf("[시도 %d] 전송: %s%n", attempt, message);

// 응답 수신 시도
byte[] buffer = new byte[1024];
DatagramPacket recvPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(recvPacket); // 타임아웃까지 대기

return new String(recvPacket.getData(), 0, recvPacket.getLength(), StandardCharsets.UTF_8);

} catch (SocketTimeoutException e) {
System.out.println("[시도 " + attempt + "] 타임아웃. " +
(attempt < MAX_RETRIES ? "재전송..." : "최대 재시도 횟수 초과"));
}
}

throw new Exception("서버 응답 없음 (최대 " + MAX_RETRIES + "회 재시도 후 실패)");
}

public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress addr = InetAddress.getByName("127.0.0.1");
String response = sendWithRetry(socket, "Hello, UDP!", addr, 8888);
System.out.println("응답: " + response);
} catch (Exception e) {
System.out.println("실패: " + e.getMessage());
}
}
}

4. 브로드캐스트 전송

동일 네트워크의 모든 호스트에게 동시에 메시지를 보냅니다.

import java.net.*;
import java.nio.charset.StandardCharsets;

public class UdpBroadcast {
static final int BROADCAST_PORT = 9999;
static final String BROADCAST_ADDR = "255.255.255.255";

// 브로드캐스트 수신 서버
public static void startReceiver() throws Exception {
DatagramSocket socket = new DatagramSocket(BROADCAST_PORT);
socket.setBroadcast(true); // 브로드캐스트 수신 허용

System.out.println("브로드캐스트 수신 대기. 포트: " + BROADCAST_PORT);
byte[] buffer = new byte[1024];

while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);

String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.printf("[브로드캐스트 수신] %s → \"%s\"%n",
packet.getAddress().getHostAddress(), msg);
}
}

// 브로드캐스트 발신자
public static void sendBroadcast(String message) throws Exception {
DatagramSocket socket = new DatagramSocket();
socket.setBroadcast(true); // 브로드캐스트 활성화

byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
InetAddress broadcastAddr = InetAddress.getByName(BROADCAST_ADDR);

DatagramPacket packet = new DatagramPacket(
sendData, sendData.length, broadcastAddr, BROADCAST_PORT
);

socket.send(packet);
System.out.println("브로드캐스트 전송: " + message);
socket.close();
}

public static void main(String[] args) throws Exception {
// 실제 실행: 수신자와 발신자를 별도 프로세스로 실행해야 함
System.out.println("브로드캐스트 예제 (LAN 내 모든 호스트에 전송)");
System.out.println("수신자: startReceiver() 실행");
System.out.println("발신자: sendBroadcast(\"공지사항\") 실행");
}
}

5. 멀티캐스트

특정 그룹에 가입한 호스트들에게만 데이터를 전송합니다 (브로드캐스트보다 효율적).

import java.net.*;
import java.nio.charset.StandardCharsets;

public class UdpMulticast {
// 멀티캐스트 그룹 주소 범위: 224.0.0.0 ~ 239.255.255.255
static final String MULTICAST_GROUP = "224.0.0.1";
static final int MULTICAST_PORT = 7890;

// 멀티캐스트 수신 (그룹 가입)
static void joinAndReceive() throws Exception {
MulticastSocket socket = new MulticastSocket(MULTICAST_PORT);
InetAddress group = InetAddress.getByName(MULTICAST_GROUP);

// 멀티캐스트 그룹에 가입
socket.joinGroup(group);
System.out.println("멀티캐스트 그룹 가입: " + MULTICAST_GROUP);

byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);

String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("수신: " + msg);

// 그룹 탈퇴
socket.leaveGroup(group);
socket.close();
}

// 멀티캐스트 송신
static void sendToGroup(String message) throws Exception {
MulticastSocket socket = new MulticastSocket();
InetAddress group = InetAddress.getByName(MULTICAST_GROUP);

byte[] sendData = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(
sendData, sendData.length, group, MULTICAST_PORT
);

socket.setTimeToLive(1); // TTL: 1 = 같은 서브넷 내에서만 유효
socket.send(packet);
System.out.println("멀티캐스트 전송: " + message);
socket.close();
}

public static void main(String[] args) {
System.out.println("멀티캐스트 예제");
System.out.println("IPTV, 증권 시세 전송 등에 활용");
}
}

6. UDP vs TCP 선택 기준

사용 사례프로토콜이유
HTTP/HTTPS 웹 통신TCP데이터 무결성 필수
파일 전송 (FTP)TCP순서/무결성 필수
이메일 (SMTP/IMAP)TCP신뢰성 필수
DNS 쿼리UDP단순 요청-응답, 빠름
온라인 게임 (위치 업데이트)UDP약간 유실되어도 무방, 속도 중요
실시간 음성/화상 통화 (VoIP)UDP지연 최소화, 재전송보다 다음 패킷
비디오 스트리밍UDP버퍼링 최소화 (최신: QUIC = UDP 기반)
증권 시세 멀티캐스트UDP동시에 다수 수신자에게 빠른 전달
NTP (시간 동기화)UDP단순 요청-응답
DHCP (IP 자동 할당)UDP브로드캐스트 기반

7. 실전 예제: UDP 기반 간단한 로그 수집기

import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.*;

/**
* UDP 로그 수집기
* 여러 서비스에서 UDP로 로그를 보내면 중앙에서 수집
* 로그는 유실 가능성이 있지만 성능이 중요한 경우 UDP 사용
*/
public class UdpLogCollector {
static final int LOG_PORT = 5140; // Syslog 스타일
static final BlockingQueue<String> logQueue = new LinkedBlockingQueue<>(10000);

// 로그 수신 서버
static void startLogServer() throws Exception {
try (DatagramSocket socket = new DatagramSocket(LOG_PORT)) {
System.out.println("UDP 로그 수집기 시작. 포트: " + LOG_PORT);
byte[] buffer = new byte[4096];

while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);

String logEntry = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String fullLog = "[" + timestamp + "] [" + packet.getAddress().getHostAddress() + "] " + logEntry;

logQueue.offer(fullLog); // 큐에 넣기 (가득 차면 무시)
}
}
}

// 로그 처리 워커 (파일 저장, DB 저장 등)
static void processLogs() {
while (true) {
try {
String log = logQueue.take(); // 블로킹 대기
System.out.println("[LOG] " + log);
// 실제로는 파일 저장, Elasticsearch 전송 등
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}

// 로그 전송 클라이언트
static void sendLog(String serviceName, String level, String message) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
String logMsg = String.format("[%s][%s] %s", serviceName, level, message);
byte[] data = logMsg.getBytes(StandardCharsets.UTF_8);

DatagramPacket packet = new DatagramPacket(
data, data.length,
InetAddress.getByName("127.0.0.1"),
LOG_PORT
);
socket.send(packet);
}
}

public static void main(String[] args) throws Exception {
// 로그 처리 워커 시작
Thread worker = new Thread(UdpLogCollector::processLogs);
worker.setDaemon(true);
worker.start();

// 서버를 별도 스레드로 실행
Thread server = new Thread(() -> {
try { startLogServer(); }
catch (Exception e) { System.err.println("서버 오류: " + e.getMessage()); }
});
server.setDaemon(true);
server.start();

// 잠시 대기 후 로그 전송 시뮬레이션
Thread.sleep(500);

sendLog("UserService", "INFO", "사용자 로그인: alice@example.com");
sendLog("OrderService", "WARN", "재고 부족: productId=123");
sendLog("PaymentService", "ERROR", "결제 실패: transactionId=TXN-456");
sendLog("ApiGateway", "INFO", "요청 처리 완료: GET /api/users 200 45ms");

Thread.sleep(1000); // 로그 처리 대기
System.out.println("로그 수집 예제 완료");
}
}

8. DatagramSocket 주요 메서드 정리

import java.net.*;

public class DatagramSocketMethods {
public static void main(String[] args) throws Exception {
// 소켓 생성
DatagramSocket socket = new DatagramSocket(8888); // 포트 지정 (서버)
DatagramSocket clientSocket = new DatagramSocket(); // 임의 포트 (클라이언트)

// 주요 설정 메서드
socket.setSoTimeout(5000); // 읽기 타임아웃 (ms)
socket.setReceiveBufferSize(65536); // 수신 버퍼 크기
socket.setSendBufferSize(65536); // 송신 버퍼 크기
socket.setBroadcast(true); // 브로드캐스트 허용

// 상태 조회
System.out.println("바인딩 포트: " + socket.getLocalPort());
System.out.println("로컬 주소: " + socket.getLocalAddress());
System.out.println("닫혔나: " + socket.isClosed());
System.out.println("바인딩됐나: " + socket.isBound());
System.out.println("소켓 타임아웃: " + socket.getSoTimeout());

socket.close();
clientSocket.close();
}
}
고수 팁: UDP 성능 최적화
  1. 버퍼 크기 조정: setReceiveBufferSize(), setSendBufferSize()로 OS 버퍼 크기 설정
  2. 패킷 크기 제한: MTU(1500바이트)를 초과하면 단편화 발생 → 1400바이트 이하 권장
  3. 비동기 처리: DatagramChannel(NIO)로 논블로킹 UDP 구현
  4. QUIC 프로토콜: HTTP/3는 UDP 기반 QUIC를 사용 (TCP보다 빠른 연결 수립)