
들어가며
Rust 소켓 프로그래밍을 진행하면서 TCP와 UDP의 차이점에 대해 이론적으로만 알고 있었던 것을 실제 코드 구현과 에러 메시지를 통해 생생하게 경험하게 되었습니다.
특히 다음과 같은 TCP 클라이언트 에러 메시지는 두 프로토콜의 본질적인 차이를 확인할 수 있었습니다.

UDP 클라이언트와 서버, TCP 클라이언트와 서버 각각을 rust로 프로그래밍 하는 것이 과제였는데, UDP 클라이언트는 서버가 실행 중이지 않아도 문제 없이 실행 됐었는데 TCP 클라이언트는 서버가 실행 중이지 않은 상태에서 실행될 경우 위와 같은 에러 메시지를 발생시켰습니다.
이 에러를 통해 TCP와 UDP의 근본적인 차이점과 각 프로토콜이 실제 애플리케이션에 미치는 영향에 대해 생각해보게 되었습니다.
TCP vs UDP: 연결 모델의 차이
UDP: 비연결형(Connectionless) 프로토콜
UDP 클라이언트를 구현하면서 가장 놀라웠던 점은 서버의 존재 여부와 상관없이 클라이언트가 독립적으로 실행된다는 것이었습니다.
- UDP는 연결 개념 없이 데이터를 전송합니다
- send_to() 메서드로 데이터를 보내는 시점에 서버가 없어도 메서드 자체는 성공적으로 실행됩니다
- 데이터는 네트워크로 전송되지만, 서버가 없으면 데이터는 그냥 유실됩니다
- 로컬 포트 바인딩은 서버 상태와 무관하게 클라이언트 측 OS에서 처리합니다
따라서 UDP 클라이언트는 서버가 실행되지 않더라도 프로그램이 시작되고 메뉴를 보여주는 등의 기본 기능이 작동합니다. 문제가 발생하는 시점은 실제로 데이터를 보내고 응답을 기다릴 때뿐입니다.
TCP: 연결형(Connection-Oriented) 프로토콜
실제 문제가 발생한 부분을 같이 보면서 TCP의 특성을 좀 더 알아보겠습니다.
먼저 Rust 클라이언트 코드에서 아래 부분에서 TCP 연결을 설정할 때 문제가 발생했습니다.
let mut stream = TcpStream::connect(&server_addr)
.expect("Failed to set up Tcp stream");
이 코드가 실행되면 서버에 연결을 시도하고, 연결이 거부되면 "Failed to set up Tcp stream" 메시지와 함께 프로그램이 종료됩니다. TCP 클라이언트는 서버가 실행 중이지 않으면 더 이상 진행할 수 없었습니다.
- 데이터 교환 전에 반드시 서버와 연결(Connection)을 설정해야 합니다.
- TcpStream::connect(서버주소) 함수는 호출되는 즉시 실제로 서버에 연결을 시도합니다(TCP 3-way handshake)
- 서버가 실행 중이지 않거나 해당 포트에서 요청을 받고 있지 않으면, 서버 측 OS(<-소켓의 주인)는 연결 요청을 명시적으로 거부합니다.
- 이 거부는 ConnectionRefused 에러로 클라이언트에게 반환되며, .expect()를 사용하면 프로그램이 즉시 종료됩니다
웹 개발에서의 TCP 연결 처리
그런데 문득, 웹 환경에서 프론트엔드와 백엔드 통신에서 백엔드가 실행중이지 않아도 프론트엔드 단독으로 개발할 수 있던 것이 떠올랐습니다. 대부분의 웹 환경이면 HTTP/2를 사용할테고, 이는 TCP 기반으로 동작합니다.
React와 같은 프론트엔드 프레임워크도 내부적으로 TCP 연결을 사용하는데, 어떻게 백엔드 서버가 없어도 UI가 실행될 수 있을까요? (TCP 클라이언트는 서버가 없어서 연결이 안되면 더 이상 아무것도 할 수 없는데 말이죠)
브라우저에서는
- 실행 환경:
- Rust TCP 클라이언트: 운영체제 위에서 직접 실행됨
- React: 브라우저 내부의 자바스크립트 엔진 위에서 실행됨
- 네트워크 통신 방식:
- 프론트엔드 자바스크립트는 TCP 소켓을 직접 다루지 않음
- 대신 HTTP(S) 요청(fetch API) 또는 WebSocket API 사용
- 브라우저가 저수준 네트워크 통신을 추상화하여 처리
- 오류 처리 방식:
- 웹 애플리케이션에서 fetch API 호출이 실패해도 전체 애플리케이션은 종료되지 않음
- .catch() 블록이나 try...catch 구문으로 오류를 우아하게 처리
- 사용자에게 "데이터를 불러올 수 없습니다" 같은 메시지 표시 가능
// 웹 프론트엔드에서의 네트워크 요청 예시
fetch('/api/data')
.then(response => response.json())
.then(data => {
// 데이터 처리
})
.catch(error => {
// 서버 연결 실패해도 UI는 계속 작동
console.error('서버 연결 실패:', error);
showErrorMessage('데이터를 불러올 수 없습니다');
});
따라서, 웹 프론트엔드 환경에서는 네트워크 스택의 추상화 수준이 높고 계층 간 격리가 잘 되어 있어, 하위 계층의 문제가 애플리케이션 실행에 직접적인 영향을 미치지 않는 것입니다.
반면 Rust 클라이언트는
- 전송 계층(4계층)에 직접 접근
- TcpStream::connect()와 같은 API로 OS의 소켓에 직접 접근
- 연결 실패는 애플리케이션 코드로 바로 전파
- 오류 처리가 핸들링되지 않는다.
- 오류를 명시적으로 처리하지 않으면(.expect() 사용) Rust에서 패닉을 일으키고 프로그램이 종료
결국 우리가 지금까지 사용하던 웹페이지는 네트워크의 Layering 덕분에 Transport Layer의 에러가 Application Layer까지 문제를 일으키지 않기 때문에 네트워크 에러가 발생해도 자바스크립트가 작동하고 UI를 조작하는데는 전혀 문제가 없었던 것이죠!!
결론
TCP 클라이언트를 실행할 때 UDP 클라이언트를 개발할 때와는 달리 연결을 설정하지 못하면 프로그램이 그 즉시 뻗는 것을 보고, 교과서적인 지식을 넘어서 각 프로토콜의 설계 철학과 실제 애플리케이션 동작에 미치는 영향을 깊이 이해할 수 있었습니다.
TCP는 안정적인 데이터 전송을 보장하기 위해 연결 설정을 필수적으로 요구하므로 서버 없이는 클라이언트도 제대로 동작할 수 없습니다. 반면 UDP는 빠른 전송을 위해 연결 개념을 포기하여 독립적인 동작이 가능하지만, 데이터 전송의 신뢰성은 낮아집니다.
'컴퓨터 > CS' 카테고리의 다른 글
| [소프트웨어공학] 인터페이스 개념과 연결되는 스프링 DI (0) | 2025.04.15 |
|---|---|
| [운영체제] "파이프는 공유 메모리인가?" 리눅스의 통신 방식 (0) | 2025.04.09 |
| [네트워크] TCP 소켓 프로그래밍: read() 함수와 연결 종료 감지의 이해 (0) | 2025.04.05 |