비연결성 - 대상 서버가 패킷을 받을 수 있는 상태인지 알 수 없다. - 패킷을 받을 대상이 없거나 서비스 불능 상태여도 패킷을 전송한다.
비신뢰성 - 중간에 패킷이 사라져서 손실이 발생할 수 있다. - 패킷이 순서대로 오지 않을 수 있다.
프로그램 구분 - 같은 IP를 사용하는 서버에서 통신하는 애플리케이션이 둘 이상이라면? IP만으로는 어떤 애플리케이션에 요청을 한 건지 전혀 알 수 없다는 문제가 발생한다.
이러한 문제점을 해결해 줄 수 있는 게 바로 전송 제어 프로토콜(TCP; Transmission Control Protocol)이다.
인터넷 프로토콜 스택의 4 계층
인터넷 프로토콜 스택 4 계층이란 인터넷에서 컴퓨터들이 서로 정보를 주고받는 데 쓰이는 프로토콜의 모음이다.
용도에 따라 4개의 계층으로 나뉘어있다.
애플리케이션 계층은 프로그램 간 통신을 위한 계층이다.
전송 계층은 송신자와 수신자를 연결하는 서비스에 대한 정보를 담는 계층이다.
인터넷 계층은 패킷을 목적지로 전송하기 위한 정보를 담는 계층이다.
네트워크 인터페이스 계층은 LAN 드라이버, LAN 장비 등 물리적 전송을 위한 계층이다.
실제 통신 예시는 다음과 같은 과정이 이루어진다.
TCP는 데이터를 세그먼트 단위로 나눠서 전송한다. 세그먼트는 패킷의 일부로서 데이터를 포함하며, TCP 헤더와 함께 전송된다.
MSS (Maximum Segment Size): TCP는 MSS라는 최대 세그먼트 크기를 사용하여 데이터를 나눈다. 이는 통신하는 양쪽의 호스트 간에 협상되며, 일반적으로 MTU (Maximum Transmission Unit) 크기에 맞추어 설정된다.
패킷 분할 (Packet Fragmentation): TCP는 패킷을 세그먼트로 나누어 전송하지만, 네트워크에서는 패킷 크기에 제한이 있을 수 있다. 이 경우, 패킷 분할이 발생할 수 있다. 라우터나 중간 장비에서 패킷이 네트워크 MTU보다 크다면, 패킷은 작은 조각으로 나눠져서 전송되며, 이를 패킷 분할이라고 한다.
TCP(전송 제어 프로토콜)의 특징
연결지향 - TCP 3 Way handshake (가상 연결)
데이터 전달 보증
순서 보장
신뢰성 있는 프로토콜
TCP는 다음과 같은 단계로 연결을 수행한다.
계속 연결되어 있는 상태는 아니고, 해당 연결 후 데이터를 받은 후에는 연결을 끊는다.
사용자 데이터그램 프로토콜(UDP; User Datagram Protocol)
UDP는 TCP와 반대로 기능이 거의 없다. TCP처럼 연결 지향적이지도 않고, 데이터의 전달이나 순서도 보장할 수 없다.
그렇지만 단순하고 빠르다는 장점이 있다.
IP(인터넷 프로토콜)과 유사하지만 포트와 체크섬(checksum) 정도가 추가된다.
기존에는 실시간 비디오 스트리밍이나 사용자가 직접 프로토콜을 제어하고자 할 때 많이 사용했다. 요새는 스트리밍이더라도 TCP를 사용하는 경우도 많다고 한다. 참고로 최근에 발표된 HTTP/3의 경우 UDP 기반의 QUIC 프로토콜을 사용한다.
PORT (포트)
포트는 같은 IP 내에서 프로세스를 구분하기 위한 역할을 한다. TCP와 UDP는 둘 다 포트를 사용하는 프로토콜이다. 각 포트는 번호로 구별되며, 이를 포트 번호라고 한다. 포트 번호 중에서 자주 쓰이는 포트를 well-known Port라고 한다.
포트 번호는 0 ~ 65535까지 할당 가능하며, 0 ~ 1023 포트 번호는 위에서 말한 well-known Port에 해당하여 사용하지 않는 편이 좋다.
대표적인 well-known port는 다음과 같다.
20,21 - FTP
23 - TELNET
22 - SSH
HTTP - 80
HTTPS - 443
도메인 네임 시스템(DNS; Domain Name System)
우리는 클라이언트와 서버가 IP를 통해서 인터넷 망에서 통신한다고 배웠다. 하지만 IP는 너무 기억하기 어렵다는 단점이 있다. 또한 IP는 고정이 아니다 바뀔 수 있다.
이러한 문제점을 해결해 줄 수 있는 게 도메인 네임 시스템(DNS)이다.
DNS는 도메인 명을 IP 주소로 변환한다.
예를 들어 우리가 구글에 접속한다면 다음과 같은 과정이 이루어진다.
1. 클라이언트는 구글의 도메인명을 브라우저에 입력한다.
2. DNS 서버에서 구글의 도메인 명에 대한 서버의 실제 IP 주소로 변환해 준다.
3. 클라이언트는 서버 IP 주소로 통신한다.
DNS를 사용하면 위에서 말한 IP의 단점인 기억하기 어려운 점과 IP가 유동적으로 바뀌는 문제를 해결할 수 있다.
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestUrl;
public void setRequestUrl(String requestUrl) {
this.requestUrl = requestUrl;
}
public void log(String message){
System.out.println("[uuid" +uuid+ "]["+requestUrl+"]" + message);
}
@PostConstruct
public void init(){
uuid = UUID.randomUUID().toString();
System.out.println(this);
}
@PreDestroy
public void close(){
System.out.println(this);
}
}
@Scope(value = "request")를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해 둔다.
이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해 두면 다른 HTTP 요청과 구분할 수 있다.
이 빈이 소멸되는 시점에 @PreDestroy를 사용해서 종료 메시지를 남긴다.
requestURL은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
테스트용 컨트롤러와 서비스를 작성한다.
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
System.out.println(myLogger.getClass());
myLogger.setRequestUrl(requestURL);
myLogger.log("controller");
logDemoService.logic("testId");
return "OK";
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id){
myLogger.log("service Id = " + id);
}
}
실행 시 다음과 같이 에러가 발생한다.
Error creating bean with name 'myLogger':
Scope 'request' is not active for the current thread;
consider defining a scoped proxy for this bean
if you intend to refer to it from a singleton;
싱글톤 빈은 스프링 애플리케이션을 실행 시에 빈을 생성하고 의존 관계를 주입한다. 하지만 request 스코프 빈은 위에서 http 요청이 들어와야 빈을 생성한다고 했다. 따라서 생성되지 않은 빈을 조회하려고 하여 에러가 발생했다. 이 문제를 해결하기 위해서는 객체를 조회할 때까지 빈 생성을 지연해야 한다.
해당 문제를 해결하기 위해서는 두 가지 방안이 있다.
하나는 ObjectProvider를 사용하는 방법이다.
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
System.out.println(myLogger.getClass());
myLogger.setRequestUrl(requestURL);
myLogger.log("controller");
logDemoService.logic("testId");
return "OK";
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id){
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service Id = " + id);
}
}
ObjectProvider를 사용하면 빈의 생성을 myLoggerProvider.getObject()가 호출될 때까지 지연시킬 수 있다.
스프링 애플리케이션을 실행하면 정상적으로 실행되고, api를 호출해 보면 다음과 같은 결과를 얻을 수 있다.
한 가지 방법이 더 있는데 프락시 방식이다.
수정한 ObjectProvider를 원래대로 돌린 뒤 스코프 애노테이션에 해당 내용을 추가한다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {