반응형

✅ Registration

 

로드밸런서의 첫번째 기능인 Registration을 구현하고자 한다.

 

각각의 조건 살펴보면

  • 각각의 서버는 로드밸런서에 연결되어야 한다
  • 그리고나서, 로드밸런서는 listen할 프로토콜과 포트를 등록한다.
  • 로드밸런서는 listen할 프로토콜과 포트를 bind한다.
  • 그리고나서 로드밸런서는 트래픽을 알맞는 서버에 분배한다.

 

위의 조건을 기반으로 과정을 더 자세히 살펴보겠다.

 

🔎 Register 프로세스 : 서버와 로드밸런서간의 통신

 

 

🧾등록 과정 (Control Channel)

  • 각 서버는 로드밸런서에 연결된 후 자신이 제공할 서비스(프로토콜, 포트)를 등록한다.
  • JSON 형식의 메시지를 통해 서버가 자신을 등록하며, 로드밸런서는 이 정보를 바탕으로 트래픽을 분배한다.

✏️1. 서버가 로드밸런서에게 등록 요청

  • 서버는 TCP 프로토콜을 사용하여 로드밸런서에 다음과 같은 메시지를 전송한다.

            
{"cmd": "register", "protocol": "tcp", "port": 80}
  • 이 메시지는 서버가 어떤 프로토콜포트로 클라이언트 요청을 처리할 것인지 알려준다.

 

✏️2. 로드밸런서의 응답 (ACK)

  • 서버가 정상적으로 등록되면 로드밸런서는 다음과 같은 성공 메시지(ACK)를 반환한다.

            
{"ack": "successful"}
  • 만약 등록이 실패하면 로드밸런서는 실패 이유를 담아 반환합니다

            
{"ack": "failed", "msg": "Invalid port"}

 

 

🧾바인딩 (binding)

 

  • ✏️ 서버 등록 후 바인딩(Binding) 및 수신(Listen) 시작:
    • 로드밸런서는 서버가 등록한 프로토콜과 포트를 기반으로 해당 서비스에 대한 트래픽을 수신.
      • 예를 들어, TCP 80번 포트로 들어오는 HTTP 요청을 API 서버에 분배
  • ✏️ 클라이언트 요청의 트래픽 분배:
    • 로드밸런서는 등록된 서버 목록을 기반으로 라운드 로빈(Round-Robin) 방식 등으로 요청을 분배.
      • 예: API 서버 1번과 2번에 순차적으로 트래픽을 분배.
      • 만약 TCP 웹 서버가 여러 개일 경우, 클라이언트 요청은 이들 서버로 고르게 분배.

이 과정에서 로드밸런서는 동적으로 작동한다 

 

동적 로드밸런싱

  • 로드밸런서는 동적(Dynamic)으로 동작하며, 서버가 등록되면 자동으로 해당 서버에 트래픽을 분배.
  • 서버가 추가되거나 삭제되더라도 로드밸런서는 이를 즉시 반영하여 새로운 요청을 처리.

✅ Server Handler 구현하기

Server는 위에서 봤던 것처럼 크게 전송 계층의 프로토콜인 TCPUDP로 나누어 처리할 것이다.

 


            
private final ExecutorService threadPool = Executors.newFixedThreadPool(10);

이때의 핵심은, TCP와 UDP를 다루는 Handler를 구현할 때

쓰레드 풀을 정의하여 멀티 쓰레드로 구현할 것이다.

 

멀티쓰레드로 구현하였을 때

  • 여러 클라이언트들으 요청을 동시에 처리할 수 있다.
  • 병렬처리를 통해 응답시간을 줄인다.

와 같은 이점을 살릴 수 있다.

 

📌 Server 패키지 설계하기

 

전체적인 구조는 위와 같다

 

ServerHandler라는 부모 인터페이스를 두고,

TCPServerHandlerUDPServerHandler는 상속받는 구조를 가진다

 

그리고 Handler를 생성하기 위한 ServerHandlerFactory를 두고,

 

ServerHandlerFactory는 클라이언트로부터 들어온 서버에 대한 정보를 다루는 객체인 ServerInfo로부터

Handler를 생성하는 구조이다.

 

🔎 ServerHandler.java


            
package org.loadBalancer.model;
public abstract class ServerHandler {
protected final String ip;
protected final int port;
public ServerHandler(String ip, int port) {
this.ip = ip;
this.port = port;
}
public abstract void handleRequest(); // protocol의 종류마다 다른 handler 적용
}

 

TCP Handler와 UDP Handler 모두

Ip와 Port를 가지고 handler하는 메서드를 가진다는 공통점이 있다.

 

이에 Server라는 부모클래스를 두어 상속받도록하여 유지보수에 용이하도록 구현하였다.

 

🔎 TCPServerHandler.java / UDPServerHandler.java


            
package org.loadBalancer.model;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TCPServerHandler extends ServerHandler {
private final ExecutorService threadPool = Executors.newFixedThreadPool(10);
public TCPServerHandler(String ip, int port) {
super(ip, port);
}
@Override
public void handleRequest() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("TCP ServerHandler running on " + ip + ":" + port);
while (true) {
// 클라이언트 연결 대기 (blocking)
Socket clientSocket = serverSocket.accept();
System.out.println("Accepted connection from " + clientSocket.getInetAddress());
// 스레드 풀에서 각 클라이언트 요청을 처리
threadPool.execute(() -> handleClient(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 각 클라이언트의 요청을 처리하는 메서드
private void handleClient(Socket clientSocket) {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
out.write("Echo: " + inputLine + "\n");
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
System.out.println("Connection closed.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

            
package org.loadBalancer.model;
import java.net.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UDPServerHandler extends ServerHandler {
private final ExecutorService threadPool = Executors.newFixedThreadPool(10);
public UDPServerHandler(String ip, int port) {
super(ip, port);
}
@Override
public void handleRequest() {
try (DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName(ip))) {
System.out.println("UDP ServerHandler running on " + ip + ":" + port);
byte[] buffer = new byte[1024];
// 무한 루프를 돌며 UDP 패킷을 수신 대기
while (true) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 클라이언트의 UDP 요청 수신
// 각 패킷을 별도의 스레드에서 처리
threadPool.execute(() -> handlePacket(socket, packet));
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 수신한 패킷을 처리하는 메서드
private void handlePacket(DatagramSocket socket, DatagramPacket packet) {
try {
// 수신된 데이터 읽기
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received: " + received);
// 응답 데이터 생성
String response = "Echo: " + received;
byte[] responseData = response.getBytes();
// 응답 패킷 생성 및 전송
DatagramPacket responsePacket = new DatagramPacket(
responseData, responseData.length, packet.getAddress(), packet.getPort());
socket.send(responsePacket);
System.out.println("Response sent to " + packet.getAddress() + ":" + packet.getPort());
} catch (Exception e) {
e.printStackTrace();
}
}
}

 

ServerHandler를 상속받는 TCPServerHandler와 UDPServerHandler를 구현한다.

 

🔎TCPServerHandler


            
Socket clientSocket = serverSocket.accept();
  • TCP는 클라이언트와 서버 간 연결을 맺은 후 데이터를 주고 받는다
    • 즉, 서버와 클라이언트간의 소켓 연결을 진행한다.
  • TCP에서는 3-way-handshake를 통해 연결을 설정한다
    • 클라이언트 SYN
    • 서버 SYN-ACK
    • 클라이언트 ACK
  • 네트워크 지연 중에도 서버는 블로킹 상태로 클라이언트 요청을 수신 대기

            
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
  • TCP는 신뢰성 있는 데이터를 전송한다
    • 패킷 손실, 중복, 순서 오류를 감지하고 재전송을 통해 정확한 수신을 보장한다
    • 입출력 스트림을 통해 순차적 데이터 전송을 지원한다.

            
finally {
try {
clientSocket.close();
System.out.println("Connection closed.");
} catch (IOException e) {
e.printStackTrace();
}
}
  • 작업이 완료되면 명시적으로 연결을 종료한다
    • 소켓을 닫아 연결을 종료한다.
    • TCP에서는 4-way handshake를 통해 연결을 종료한다.

🔎UDPServerHandler


            
DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName(ip));
  • UDP는 데이터를 전송하기 전에 클라이언트와 서버 간의 연결 설정 없이 패킷을 바로 전송
    • DatagramSocket : 연결을 설정하지 않고 바로 데이터 수신 및 전송한다.
    • 서버는 클라이언트의 요청을 받을 때마다 개별적인 패킷으로 처리한다.

            
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 클라이언트의 UDP 요청 수신
  • UDP는 헤더 크기가 작고, 연결 유지가 필요 없기 때문에 TCP보다 빠르다.
    • 데이터가 작은 크기의 패킷으로 전송되며, 단순히 수신과 응답만 수행
    • 빠르게 패킷을 수신하고 즉시 처리하는 구조로 설계

            
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received: " + received);
  • 단순히 패킷을 수신하고 응답합니다. 만약 패킷이 손실되거나 순서가 뒤바뀌어 도착해도 복구하지 않는다.
  • 각 패킷이 독립적으로 전송되기 때문에 순서가 보장되지 않는다.

 

🔎 ServerInfo.java


            
package org.loadBalancer.model;
public class ServerInfo { // 서버의 주요 정보를 담고 있는 객체
private String ip;
private Integer port;
private String protocol;
public ServerInfo(){}
public ServerInfo(String ip, Integer port, String protocol){
this.ip = ip;
this.port = port;
this.protocol = protocol;
}
public String getIp(){
return this.ip;
}
public Integer getPort(){
return this.port;
}
public String getProtocol(){
return this.protocol;
}
}

 

json으로 들어오는 메시지 중 ip와 port 그리고 protocol값을 입력 받는 객체를 정의한다.

🔎 ServerFactory.java


            
package org.loadBalancer.model;
public class ServerHandlerFactory {
public static ServerHandler createServer(ServerInfo info) {
switch (info.getProtocol().toUpperCase()) {
case "TCP":
return new TCPServerHandler(info.getIp(), info.getPort());
case "UDP":
return new UDPServerHandler(info.getIp(), info.getPort());
default:
throw new IllegalArgumentException("Unsupported protocol: " + info.getProtocol());
}
}
}

마지막으로 Factory패턴으로 구현하여

진입점에서 ServerInfo에 대한 객체 정보를 바탕으로 각각의 server에 맞는 Handler를 생성할 수 있도록 구현한다.

반응형
J_hzlo