프로그래밍 언어/JAVA

Java로 TCP 프록시 서버 구현하기 - 라운드 로빈 로드밸런싱을 활용한 백엔드 서버 연결

코딩금융치료 2025. 1. 2. 11:22

소개

멀티스레드 환경에서 다수의 TCP 요청을 처리하고, 이를 백엔드 서버로 효율적으로 분배하는 TCP 프록시 서버를 구축하는 방법을 소개합니다. 이 프로젝트는 Java의 강력한 네트워크 라이브러리와 라운드 로빈 로드밸런싱 알고리즘을 활용해 간단하고 효율적인 로드밸런싱을 구현합니다.
 

1. 프로젝트 개요

목표

  1. TCP 요청을 처리하는 프록시 서버를 구축.
  2. 백엔드 서버 그룹으로 요청을 분배.
  3. 라운드 로빈 알고리즘을 통해 요청을 균등하게 분배.
  4. 멀티스레드를 활용해 여러 클라이언트 요청을 동시에 처리.

구성

  • 클라이언트: TCP 요청을 보냄.
  • 프록시 서버: TCP 요청을 수신하고 백엔드 서버로 전달.
  • 백엔드 서버 그룹: 요청을 처리하고 결과를 반환.

2. 라운드 로빈 로드밸런싱

로드밸런싱이란?

로드밸런싱은 다수의 서버에 작업을 균등하게 분배하여 시스템 성능과 가용성을 최적화하는 기술입니다.

라운드 로빈 방식

라운드 로빈은 각 서버에 순차적으로 요청을 보내는 간단한 로드밸런싱 방식입니다.

  • 첫 번째 요청은 서버 A로 전달.
  • 두 번째 요청은 서버 B로 전달.
  • 모든 서버가 요청을 한 번씩 처리한 후 다시 처음으로 돌아감.

3. 구현 코드

3.1 서버 소켓 설정

Java의 ServerSocket 클래스를 활용하여 클라이언트 요청을 수신합니다. 백엔드 서버 연결은 소켓을 사용합니다.
 

import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class TcpProxyServer {
    private static final String[] BACKEND_SERVERS = {"192.168.10.1:9577", "192.168.10.2:9577"};
    private static final AtomicInteger INDEX = new AtomicInteger(0); // 라운드 로빈 인덱스

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10); // 스레드 풀 생성

        try (ServerSocket serverSocket = new ServerSocket(9577)) {
            System.out.println("Proxy server listening on port 9577...");

            while (true) {
                Socket clientSocket = serverSocket.accept(); // 클라이언트 연결 대기
                executor.submit(() -> handleClient(clientSocket)); // 요청 처리
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleClient(Socket clientSocket) {
        try (Socket backendSocket = connectToBackend();
             InputStream clientIn = clientSocket.getInputStream();
             OutputStream clientOut = clientSocket.getOutputStream();
             InputStream backendIn = backendSocket.getInputStream();
             OutputStream backendOut = backendSocket.getOutputStream()) {

            // 클라이언트 -> 백엔드 데이터 전달
            Thread clientToBackend = new Thread(() -> forwardData(clientIn, backendOut));
            clientToBackend.start();

            // 백엔드 -> 클라이언트 데이터 전달
            Thread backendToClient = new Thread(() -> forwardData(backendIn, clientOut));
            backendToClient.start();

            clientToBackend.join();
            backendToClient.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Socket connectToBackend() throws IOException {
        // 라운드 로빈으로 백엔드 서버 선택
        int targetIndex = INDEX.getAndUpdate(i -> (i + 1) % BACKEND_SERVERS.length);
        String[] backend = BACKEND_SERVERS[targetIndex].split(":");
        String host = backend[0];
        int port = Integer.parseInt(backend[1]);

        System.out.println("Forwarding to backend: " + host + ":" + port);
        return new Socket(host, port);
    }

    private static void forwardData(InputStream in, OutputStream out) {
        try {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
                out.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 코드 설명

  1. 프록시 서버 설정:
    • ServerSocket은 9577 포트를 열어 클라이언트 요청을 수신.
    • 요청이 들어올 때마다 스레드 풀에서 작업을 처리.
  2. 백엔드 연결:
    • AtomicInteger를 활용해 라운드 로빈 방식으로 백엔드 서버를 선택.
    • 선택된 서버로 TCP 연결을 생성.
  3. 데이터 전달:
    • 양방향 데이터 전달: 클라이언트 -> 백엔드, 백엔드 -> 클라이언트 데이터를 각각 다른 스레드에서 처리.
  4. 스레드 풀:
    • ExecutorService를 사용해 동시에 여러 클라이언트 요청을 처리.
    • 스레드 풀 크기는 고정(10개)으로 설정하여 자원 사용 제한.

5. 실행 방법

  1. 백엔드 서버 설정:
    • "192.168.10.1:9577" 및 "192.168.10.2:9577" 로 백엔드 서버를 실행합니다.
  2. 프록시 서버 실행:
    • 위 코드를 컴파일 및 실행합니다:
       
  3. 클라이언트 요청 테스트:
    • 클라이언트에서 127.0.0.1:9577로 요청을 보냅니다.
    • 요청이 백엔드 서버로 순차적으로 전달되는지 확인합니다.
javac TcpProxyServer.java
java TcpProxyServer

   
   4.Telnet을 사용해 프록시 서버에 요청을 보냅니다.

telnet 127.0.0.1 9577

 결과:

  • 첫 번째 요청:  Backend 1 응답
  • 두 번째 요청:  Backend 2 응답

6. 로드밸런싱 결과 확인

위 테스트를 통해 각 요청이 라운드 로빈 방식으로 백엔드 서버에 분배되는 것을 확인할 수 있습니다:

  1. 백엔드 서버 1 (192.168.10.1:9577)과 백엔드 서버 2 (192.168.10.2:9577)의 터미널에서 클라이언트의 연결 로그가 번갈아 출력됩니다.
  2. 클라이언트 응답 메시지가 두 서버에서 번갈아 반환됩니다.

7. 라운드 로빈 로드밸런싱의 장점

  • 균등 분배: 각 서버가 동일한 양의 요청을 처리.
  • 구현 간단: 복잡한 로직 없이 배열과 인덱스를 활용.
  • 스레드 안전: AtomicInteger를 사용해 스레드 간 충돌 방지.

 
 

'프로그래밍 언어 > JAVA' 카테고리의 다른 글

Java 직렬화와 NotSerializableException 문제  (0) 2025.01.02