Socket에서 Spring Boot까지: Java 네트워크 통신 이해하기
Source: Dev.to
위의 링크에 포함된 텍스트를 번역하려면 해당 내용을 제공해 주세요. 텍스트를 주시면 원본 형식과 마크다운을 유지하면서 한국어로 번역해 드리겠습니다.
Source: …
분산 시스템 간 통신
시스템 간 통신, 특히 분산 애플리케이션 컨텍스트에서 이야기할 때 우리는 종종 프레임워크와 고수준 추상화 뒤에 숨겨진 여러 개념을 다루게 됩니다. 이러한 기본 원리를 이해하면 API, 웹 서버, 통합이 실제로 “아래에서” 어떻게 동작하는지 더 잘 파악할 수 있습니다.
네트워크 프로토콜
실제로 한 시스템이 다른 시스템과 대화할 때 “마법”은 없습니다. 각각 특정 문제를 해결하는 프로토콜 스택이 존재합니다.
TCP/IP 의 역할
- TCP/IP 모델의 전송 계층.
- 한 지점에서 보낸 데이터가 정확히 다른 지점에 도착하도록 보장합니다.
TCP가 제공하는 것:
- 연결 기반 통신
- 신뢰성 있는 전송
- 데이터 순서 보장
- 오류 발생 시 자동 재전송
따라서 무결성과 일관성이 필수적인 상황, 예를 들어 HTTP 통신에서 널리 사용됩니다.
요약: TCP는 연결 기반 프로토콜로 두 컴퓨터 간에 신뢰할 수 있는 데이터 흐름을 제공합니다.
UDP 프로토콜
- 연결을 설정하지 않아 지연 시간이 감소합니다.
- 패킷 손실이 시스템에 큰 영향을 주지 않을 때 적합합니다(예: 오디오/비디오 스트리밍, 온라인 게임).
- 경우에 따라 TCP의 신뢰성 자체가 불필요한 오버헤드를 추가해 서비스를 오히려 방해할 수 있습니다.
포트와 주소 지정
- IP는 머신을 식별합니다.
- 포트는 그 머신 안의 애플리케이션을 식별합니다.
- TCP와 UDP는 포트를 사용해 데이터를 올바른 프로세스로 라우팅합니다.
엔드포인트 =
IP + 포트조합.
각 TCP 연결은 두 엔드포인트(클라이언트와 서버)로 고유하게 식별됩니다.
소켓(Socket) 이란?
Java에서는 java.net 패키지가 주요 추상화를 제공합니다:
| 클래스 | 기능 |
|---|---|
Socket | 클라이언트 측 |
ServerSocket | 서버 측 |
DatagramSocket | 클라이언트/서버 측 (UDP) |
DatagramPacket | 데이터 패킷 (UDP) |
- Java에서
Socket을 사용하면 이미 TCP(프로토콜)를 사용하고 있는 것입니다. - HTTP는 TCP 위에서 동작하는 애플리케이션 프로토콜이며 내부적으로 소켓을 사용합니다. 즉, HTTP는 소켓 TCP를 통해 전송되는 요청·응답 메시지 규약에 불과합니다.
브라우저가 웹 페이지에 접근할 때 HTTP가 “아래에서” 동작하는 방식
- 브라우저가 TCP 소켓을 생성합니다.
- 특정 포트(예: 80 또는 443)로 서버에 연결합니다.
- 소켓을 통해 HTTP 요청을 전송합니다.
- 서버는 원시 바이트 스트림을 받습니다.
- 서버가 HTTP 프로토콜을 해석합니다.
- 서버가 HTTP 응답을 생성해 소켓을 통해 전송합니다.
- 브라우저가 응답을 받아 해석하고 페이지를 렌더링합니다.
- 연결은 HTTP 버전 및 헤더에 따라 종료되거나(keep‑alive) 유지될 수 있습니다.
Java 네트워크 프로그래밍
높은 수준의 추상화
- URL을 통한 이미지 로드
- HTTP API 호출
- 인터넷 자원 다운로드
이 경우 Java는 소켓, TCP, 네트워크 세부 사항을 완전히 숨깁니다.
더 많은 제어가 필요할 때
- TCP:
Socket및ServerSocket - UDP:
DatagramSocket및DatagramPacket
소켓을 이용한 클라이언트‑서버 통신 (TCP 모델)
// 서버
ServerSocket serverSocket = new ServerSocket(4001);
Socket clientSocket = serverSocket.accept();
PrintStream out = new PrintStream(clientSocket.getOutputStream());
BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = reader.readLine()) != null) {
out.println("Server received: " + inputLine);
}
// 클라이언트
Socket socket = new Socket("localhost", 4001);
Scanner scanner = new Scanner(System.in);
PrintStream out =
new PrintStream(socket.getOutputStream());
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String message = scanner.nextLine();
out.println(message); // 서버에 전송
String response = reader.readLine(); // 응답을 읽음
System.out.println("Response: " + response);
다중 클라이언트 지원
while (true) {
Socket client = serverSocket.accept(); // 새로운 연결을 수락
new Thread(() -> handleClient(client)).start(); // 각 스레드가 하나의 소켓을 처리
}
애플리케이션 프로토콜
프로토콜을 사용하면 정의됩니다:
- 메시지 형식
- 상호작용 순서
- 가능한 상태
- 유효한 응답
실제 시스템에서는 프로토콜이 명령, 상태, 오류 및 비즈니스 흐름을 나타낼 수 있습니다.
소켓 통신에서 Spring Boot까지
우리가 Spring Boot와 같은 프레임워크를 사용하기 시작하면, 수많은 추상화 뒤에 여전히 네트워크를 통한 바이트 교환이라는 사실을 잊기 쉽습니다. 이 변천 과정을 이해하면 더 나은 코드를 작성할 수 있을 뿐 아니라, 보다 의식적인 아키텍처 결정을 내릴 수 있습니다.
1️⃣ 시작점: TCP와 Sockets
Java에서 TCP를 가장 직접적으로 사용하는 방법은 소켓을 이용하는 것입니다. 소켓을 직접 다룰 때 개발자는 모든 것을 스스로 관리해야 합니다:
- 연결 열기
- 바이트 읽고 쓰기
- 메시지 형식 정의
- 다중 연결 관리(스레드)
- 오류 및 연결 종료 처리
강력하지만, 이 모델은 금방 유지보수가 어려워집니다. 각 애플리케이션이 자체 “프로토콜”을 만들게 되고, 코드는 네트워크 인프라에 고도로 결합됩니다.
소켓을 사용한 서버와 클라이언트 전체 예제
// Server.java
import java.io.*;
import java.net.*;
public class Server {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(4001);
Socket clientSocket = serverSocket.accept();
PrintStream out = new PrintStream(clientSocket.getOutputStream());
BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()))) {
String inputLine;
while ((inputLine = reader.readLine()) != null) {
out.println("Server received: " + inputLine);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Client.java
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
try (Socket socket = new Socket("localhost", 4001);
Scanner scanner = new Scanner(System.in);
PrintStream out = new PrintStream(socket.getOutputStream());
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
System.out.print("Enter message: ");
String message = scanner.nextLine();
out.println(message); // 서버에 전송
String response = reader.readLine(); // 응답 읽기
System.out.println("Response: " + response);
} catch (IOException e) {
e.printStackTrace();
}
}
}
왜 Spring Boot와 같은 프레임워크로 마이그레이션할까?
- 저수준 추상화: Spring이 연결, 스레드‑풀, 타임아웃 등을 관리합니다.
- 표준화: 이미 검증된 REST, WebFlux, Spring MVC 등을 사용해 애플리케이션 레벨 프로토콜을 구현합니다.
- 테스트 용이성: Mock 및 통합 테스트가 더 간단합니다.
- 확장성: Tomcat, Jetty, Undertow 같은 서블릿 컨테이너와 Docker/Kubernetes와의 연동이 용이합니다.
TCP, UDP, 소켓을 이해하는 것은 시스템 간 통신의 기반을 이해하는 것입니다. Spring, Tomcat 같은 서버, HTTP 같은 프로토콜은 삶을 편하게 해 주기 위해 존재하지만, 이 모든 것이 기본 개념에 의존합니다. 소켓을 공부하면 HTTP가 해결하는 문제, 웹 서버가 존재하는 이유, 그리고 분산 시스템이 실제로 어떻게 대화하는지를 파악할 수 있습니다.
String response = reader.readLine(); // lê resposta do servidor
System.out.println("Resposta do servidor: " + response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2 – HTTP: 통신 표준화
- 메서드: GET, POST, PUT, PATCH, DELETE
- 헤더
- 상태 코드
- 요청 및 응답 형식
이렇게 하면 클라이언트와 서버가 “같은 언어를 말하게” 되고, 브라우저가 가능해지며 API가 일관되게 등장하기 시작합니다.
중요: HTTP는 소켓을 대체하는 것이 아니라, 소켓을 통해 전송되는 것을 정리해줄 뿐입니다.
3 – Servlet API: Java가 웹 세계에 진입
- 개발자는
HttpServletRequest,HttpServletResponse와doGet,doPost와 같은 메서드를 사용하게 된다. - 더 이상 소켓을 수동으로 열거나, HTTP 요청을 “손수” 해석하거나, 연결 동시성을 직접 다룰 필요가 없다.
- 초점은 네트워크가 아니라 HTTP 요청 자체가 된다.
4 – 서블릿 컨테이너: Tomcat이 들어가는 곳
아직도 필요한 것:
- 포트 청취
- 스레드 관리
- Servlet API 구현
- 애플리케이션 생명주기 제어
이것이 Servlet Containers(Tomcat, Jetty, Undertow 등)의 역할이다.
예를 들어 Tomcat
- TCP 소켓을 연다
- HTTP를 처리한다
- 서블릿을 인스턴스화한다
- 애플리케이션 코드에 실행을 위임한다
5 – 웹 서버: 대규모 연결 처리
주요 책임:
- TCP 및 TLS 연결 종료
- reverse proxy 역할 수행
- 고성능 정적 콘텐츠 제공
- 애플리케이션 컨테이너의 보호 및 부하 경감
이러한 서버는 애플리케이션의 비즈니스 로직을 해결하지 않으며; 인프라를 담당하고, 도메인을 담당하지 않는다.
6 – Java에서의 최초 웹 추상화
시간이 지나면서, 서블릿 안에 HTML을 직접 작성하는 것이 생산성이 낮다는 것이 드러났습니다. JSP와 JSF와 같은 기술이 등장했으며, 이는 프레젠테이션 계층을 더 잘 분리하는 데 도움을 주었지만 동시에 다음과 같은 문제도 도입했습니다:
- 복잡성
- 결합도
- 유지보수의 어려움
7 – Spring Framework: 탈결합 및 조직
Spring은 Java 개발의 구조적 문제를 해결하기 위해 탄생했습니다:
- 높은 결합도
- 테스트의 어려움
- 경직된 의존성
웹 컨텍스트(Sprint MVC)에서 개발자는
HttpServlet을 상속할 필요가 없습니다- controllers와 함께 작업합니다
- 어노테이션으로 라우트를 정의합니다
- 책임을 명확히 분리합니다
Spring은 Servlet API와 container(Tomcat 등) 위에서 계속 실행되지만, 이러한 세부 사항을 거의 완전히 추상화합니다.
8 – Spring Boot: 최대 생산성
Spring을 사용해도 설정이 여전히 방대했습니다. Spring Boot는 모든 것을 간소화하기 위해 등장했습니다:
- Auto‑configuration
- 내장 서버
- 간단한 초기화
- Boilerplate 감소
이제는, 이것만 하면 됩니다
java -jar app.jar
그리고 개발자가 눈치채지 못하더라도 Tomcat이 포함된 상태로 애플리케이션이 실행됩니다.
결론
통신 및 프로그래밍의 진화
TCP → Socket → HTTP → Servlet API → Spring → Spring Boot
인프라스트럭처의 진화
Web Servers (Apache/Nginx) → Servlet Containers (Tomcat/Jetty)
각 계층은 복잡성을 줄이고 생산성을 높이며 개발자를 저수준 세부 사항에서 멀어지게 하지만, 이러한 추상화 뒤에 무엇이 있는지를 이해하는 것은 단순히 프레임워크만 사용하는 사람과 실제로 시스템을 설계하는 사람을 구분합니다.
결국 모든 HTTP 요청은 여전히 socket TCP에서 시작하고 끝납니다, 비록 우리가 Spring Boot를 사용하고 있더라도.