네트워크(Network)는 net이라는 그물과 work 일이라는 단어가 합쳐서 만들어진 단어로, IT 분야에서는 "두 개 이상의 노드(장치)가 링크(연결 매체)를 통해 서로 데이터를 공유하고 통신할 수 있도록 연결된 체계"라고 할 수 있습니다.
컴퓨터 네트워크 관점에서는 단순히 케이블을 연결한 물리적 상태만을 뜻하지 않으며, 송수신 장치가 서로 신호를 이해하고 데이터를 오류 없이 안전하게 주고받기 위해 합의된 규칙(프로토콜)과 소프트웨어 인프라까지를 모두 포함하는 광범위한 개념으로 확장할 수 잇습니다.
OSI 7계층은 컴퓨터 시스템 간의 원활한 통신을 위해 국제표준화기구(ISO)에서 제정한 네트워크 표준 모델입니다. 복잡한 네트워크 통신 단계를 7개로 쪼개어 표준화한 이유는 "역할 분담과 유지보수의 용이성" 때문입니다. 특정 계층의 문제가 발생하면 다른 계층의 하드웨어나 소프트웨어를 건드리지 않고 해당 계층만 수정하여 문제를 해결할 수 있습니다.
Java 기반 백엔드 애플리케이션을 개발할 때 우리는 주로 상위 계층인 L7(애플리케이션 계층), L6(표현 계층), 그리고 L4(전송 계층)와 깊은 연관을 맺습니다. 하위 계층(L1~L3)은 주로 운영체제(OS Kernel)와 네트워크 장비(라우터, 스위치 등)가 알아서 처리하므로, Java 코드 레벨에서는 추상화된 소켓 인터페이스를 통해 간접적으로 제어하게 됩니다.
Socket, ServerSocket)이 이 계층에 직접 대응하며, 포트(Port) 번호를 할당하여 프로세스 간 통신 통로를 확보합니다.각 연관 계층이 Java 코드에서 실제로 어떻게 사용되는지 예시와 함께 설명해보겠습니다.
OS 커널의 TCP/IP 프로토콜 스택과 직접 상호작용하기 위해 Java가 제공하는 로우 레벨 API를 사용한 예시입니다. 포트 번호를 지정하여 수신 대기 상태를 만들고 클라이언트 연결을 허용합니다.
package transport;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServerSocketExample {
public static void main(String[] args) {
int port = 8080; // L4 계층의 포트 번호 지정
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("L4 Server가 포트 " + port + "에서 클라이언트 연결을 대기 중입니다.");
while (true) {
// 클라이언트와의 3-Way Handshake가 백그라운드(OS)에서 완료되면 연결 소켓을 반환받음
try (Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
System.out.println("클라이언트가 연결되었습니다: " + clientSocket.getRemoteSocketAddress());
// 스트림을 통한 데이터 읽기
String clientMessage = in.readLine();
System.out.println("수신된 메시지: " + clientMessage);
// 클라이언트에 응답 전송
out.println("서버에서 보내는 응답: " + clientMessage);
} catch (Exception e) {
System.err.println("클라이언트 처리 중 오류 발생: " + e.getMessage());
}
}
} catch (Exception e) {
System.err.println("서버 소켓 오픈 실패: " + e.getMessage());
}
}
}Java 내부에서 다루는 객체(메모리 상의 인스턴스)를 네트워크 통신에 적합한 표준 규격(JSON)으로 변환(직렬화)하는 표현 계층의 역할 예시입니다. 실무에서는 json 형태가 제일 많이 사용되어Jackson 라이브러리의 사용방법을 예시로 들었습니다.
package presentation;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SerializationExample {
// 전송할 도메인 객체 정의
public static class UserDto {
private String name;
private int age;
public UserDto() {} // 기본 생성자 필수
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
public static void main(String[] args) {
ObjectMapper objectMapper = new ObjectMapper();
try {
UserDto user = new UserDto("홍길동", 30);
// 1. Serialization (Java 객체 -> JSON 문자열) : 네트워크 송신을 위한 준비
String jsonString = objectMapper.writeValueAsString(user);
System.out.println("L6 직렬화 결과 (네트워크로 전송할 데이터 포맷):");
System.out.println(jsonString);
// 2. Deserialization (JSON 문자열 -> Java 객체) : 수신한 데이터 해석
UserDto deserializedUser = objectMapper.readValue(jsonString, UserDto.class);
System.out.println("\nL6 역직렬화 결과 (애플리케이션이 이해할 수 있는 객체 포맷):");
System.out.println("이름: " + deserializedUser.getName() + ", 나이: " + deserializedUser.getAge());
} catch (Exception e) {
System.err.println("직렬화/역직렬화 실패: " + e.getMessage());
}
}
}L4 소켓 제어와 L6 데이터 포맷 지정을 상위 수준에서 모두 추상화하여, 개발자가 순수하게 비즈니스 프로토콜(HTTP) 및 자원에 집중할 수 있게 해주는 계층입니다. Java 11부터 내장된 HttpClient를 사용하여 REST API를 호출하는 예시입니다.
package application;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.duration.Duration;
public class HttpClientExample {
public static void main(String[] args) {
// HTTP 통신을 수행할 클라이언트 인스턴스 생성
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
// HTTP GET 요청 명세 작성 (URI, Method 등 애플리케이션 규약 정의)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("[https://jsonplaceholder.typicode.com/posts/1](https://jsonplaceholder.typicode.com/posts/1)"))
.header("Accept", "application/json")
.GET()
.build();
try {
System.out.println("L7 HTTP GET 요청을 전송합니다.");
// 내부적으로 L4 소켓이 생성되고, L6 수준의 헤더/바디 포맷팅을 거쳐 비동기/동기 통신이 수행됨
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("응답 상태 코드: " + response.statusCode());
System.out.println("응답 바디 데이터:");
System.out.println(response.body());
} catch (Exception e) {
System.err.println("HTTP 통신 오류 발생: " + e.getMessage());
}
}
}백엔드 애플리케이션을 지탱하는 웹 서버(톰캣, 네티 등)와 데이터베이스, 마이크로서비스 간의 통신은 모두 네트워크 위에서 작동합니다. 실무에서 신뢰성 있고 성능이 뛰어난 아키텍처를 설계하기 위해 백엔드 개발자는 다음과 같은 관점을 견지해야 합니다.
실무에서는 스프링 프레임워크가 제공하는 선언적 기능(예: @GetMapping, OpenFeign 등)을 주로 사용하기 때문에 자칫 네트워크 동작을 과하게 단순화하여 생각하기 쉽습니다. 하지만 대규모 트래픽 환경이나 장애 상황을 마주했을 때, 그 밑바닥에 흐르는 저수준 지식의 유무가 개발자의 역량을 가릅니다.
모든 통신에 HTTP(REST API)가 만능은 아닙니다.
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.