개요
회사 면접을 준비하다가 HAProxy 라는 키워드를 접하게 되었다. 단순히 개념으로만 이해하기보다는, 직접 동작을 확인해보고 싶어서
로컬 환경에서 간단한 Spring Boot 서버 두 개를 띄우고 HAProxy를 이용해 트래픽을 분산시키는 실습을 진행했다.
이를 통해 “리버스 프록시”가 실제로 어떤 역할을 하는지 직접 체감해볼 수 있었다.
리버스 프록시란
리버스 프록시란 클라이언트와 웹 서버 간의 중개자 역할을 하는 서버로,
클라이언트로부터의 요청을 대신 받아 웹 서버에 전달하고, 웹 서버의 응답을 클라이언트에게 전달하는 역할을 한다.
이를 통해 리버스 프록시는 웹 서버의 부하를 분산시키고, 보안을 강화하는 등 다양한 기능을 수행할 수 있다.
기본 작동 원리는 클라이언트가 리버스 프록시에 요청을 보내면, 리버스 프록시는 요청을 웹 서버에 전달하고, 웹 서버는 요청된 데이터를 처리한 후 리버스 프록시에게 응답을 보낸다.
그리고 리버스 프록시는 웹 서버로부터 받은 응답을 클라이언트에게 전달하는 방식으로 동작한다.
구현 목표
- Spring Boot 서버 2개 실행 (8090, 8095)
- HAProxy를 8080 포트에서 프록시로 실행
- 동일한 API 요청을 두 서버로 번갈아 전달
- 콘솔 로그로 트래픽 분배 확인
구조도

전체 구성은 Spring Boot 서버 2개 + HAProxy 1개로 이루어져 있으며,
각각의 서버는 동일한 API를 제공하고 HAProxy가 8080 포트에서 요청을 받아 내부적으로 두 서버(8090, 8095)로 트래픽을 분배한다.
프로젝트 구성
| 구성 | 내용 |
| Spring Boot 1 (8090) | 예매 API 서버 1 |
| Spring Boot 2 (8095) | 예매 API 서버 2 |
| HAProxy (8080) | 트래픽 분산 및 프록시 서버 |
트래픽 분산의 핵심은 네트워크 계층이지만, 실제 서버에서 돌아가는 API가 있어야 요청 분배를 확인할 수 있기 때문에
간단한 예매 시스템 형태의 엔티티를 구성했다.
| Entity | ||
| Event | id, title, date | 공연 정보 |
| Seat | id, seatNumber, isReserved | 좌석 정보 |
| Reservation | id, userId, seat, status, reservedAt | 예매 정보 |
| Status | Enum(CONFIRMED, CANCELED) | 예약 상태 |
각 서버는 동일한 코드로 구성되며, 예매 생성/조회/취소 API를 제공한다.
HAProxy 설정
HAProxy는 하나의 포트(8080)에서 들어오는 요청을 받아 내부의 두 서버(8090, 8095)로 트래픽을 분산시킨다.
global
maxconn 4096
log stdout format raw local0
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_front
bind *:8080
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /actuator/health
server app1 127.0.0.1:8090 check
server app2 127.0.0.1:8095 check
위 설정은 HAProxy가 트래픽을 어떻게 처리할지 정의하는 구성 파일이다.
각 블록은 역할이 다르며, 전체적으로 다음과 같은 의미를 가진다.
global — 전역 설정
- maxconn 4096 : 동시에 처리할 수 있는 최대 연결(Connection) 수를 지정한다.
- log stdout format raw local0 : 로그를 표준 출력(stdout)으로 표시하여 콘솔에서 실시간으로 확인할 수 있게 한다.
HAProxy 프로세스의 동작 환경을 정의 하는 영역이다.
defaults — 기본 동작 정책
- mode http : 트래픽을 HTTP 레벨(L7)에서 처리하도록 지정한다.
- timeout connect 5s : 백엔드 서버로 연결 시도 시 5초 안에 연결되지 않으면 실패로 간주한다.
- timeout client 30s : 클라이언트 요청이 30초 이상 지연되면 연결을 종료한다.
- timeout server 30s : 서버 응답이 30초 안에 오지 않으면 연결을 종료한다.
이 영역은 모든 요청에 공통으로 적용되는 정책이다.즉, “HTTP로 통신하며 일정 시간 이상 지연 시 연결을 끊는다”는 기본 룰을 정의한다.
frontend — 클라이언트 요청을 받는 입구
- bind *:8080 : 클라이언트가 접속할 포트를 지정한다. (8080으로 요청을 받는다)
- default_backend app_servers : 들어온 요청을 어느 서버 그룹(backend)으로 넘길지 지정한다.
즉, 클라이언트는 localhost:8080으로만 요청을 보내며, HAProxy가 그 요청을 내부의 서버 그룹(app_servers)으로 전달한다.
이 부분이 바로 리버스 프록시의 입구 역할이다.
backend — 실제 트래픽을 처리하는 서버 그룹
- balance roundrobin : 요청을 순서대로 서버에 분배한다.
- option httpchk GET /actuator/health : /actuator/health 엔드포인트로 헬스체크를 수행해 서버 상태를 확인한다.
- server app1 127.0.0.1:8090 check : 첫 번째 Spring 서버 등록 (헬스체크 포함)
- server app2 127.0.0.1:8095 check : 두 번째 Spring 서버 등록 (헬스체크 포함)
이 영역이 실제 트래픽 분산의 핵심이다. balance roundrobin을 통해 요청이 8090 → 8095 → 8090 순서로 번갈아 전달된다.
만약 특정 서버가 /actuator/health 요청에 실패(404/500 등)하면,
HAProxy는 자동으로 해당 서버를 DOWN 상태로 제외하고 나머지 서버로만 트래픽을 보낸다.
실행방법
서버 두개 실행
./gradlew bootRun --args='--server.port=8090'
./gradlew bootRun --args='--server.port=8095'

이렇게 서버를 두개 띄워놓고
haproxy -f haproxy.cfg -db
를 통해 HAProxy를 실행해 PostMan요청으로 요청을 연속으로 하면
POST http://localhost:8080/api/reservations/1
Content-Type: application/json
{
"userId": "userA"
}

동일한 요청을 8080 포트로 여러 번 보냈을 때, 콘솔 로그에서 8090과 8095 서버가 번갈아 요청을 처리하는 것을 확인할 수 있었다.
즉, 하나의 프록시 서버(HAProxy)가 클라이언트의 요청을 받아
내부의 여러 Spring Boot 서버로 트래픽을 분산(Round Robin 방식) 하고 있다는 뜻이다.
이번 구현을 통해 “리버스 프록시”가 하는 역할과 “트래픽 분산”이 실제로 어떻게 동작하는지를 직접 눈으로 확인할 수 있었다.
Haproxy 로드벨런싱 알고리즘
HAProxy는 여러 로드 밸런싱 알고리즘을 지원하므로 애플리케이션의 특정 요구 사항과 동작을 기반으로 적절한 알고리즘을 선택할 수 있다.
| 알고리즘 | 설명 | 특징 / 사용 예시 |
| Round Robin (roundrobin) | 가장 단순하고 기본적인 방식. 요청을 서버 목록 순서대로 하나씩 분배하고, 마지막 서버까지 갔으면 다시 처음부터 반복한다. | 서버 성능이 비슷하고 요청이 균등할 때 적합 |
| Least Connections (leastconn) | 현재 연결(Connection) 수가 가장 적은 서버로 새 요청을 보낸다. | 세션 처리 시간이 다양하거나 부하가 불균등할 때 유리 |
| Source (source) | 클라이언트의 IP 주소를 해싱해서 서버를 결정한다. 동일한 IP의 요청은 항상 같은 서버로 전달된다. | 세션 유지를 위해 Sticky Session이 필요한 경우 |
| URI (uri) | 요청의 URI를 해싱해 서버를 결정한다. | 특정 리소스별 캐시 일관성이 필요할 때 |
| Header (hdr) | 요청 헤더의 특정 값을 해싱해 서버를 선택한다. | 특정 헤더 값이 있는 요청인 경우 |
| Weighted Round Robin (roundrobin + weight) | 각 서버에 가중치(weight)를 부여하여 성능이 높은 서버로 더 많은 요청을 보낸다. | 서버 스펙이 다를 때 (예: 2core vs 8core) |
| Random (random) | 요청을 임의(Random) 서버로 보낸다. | 테스트 환경이나 균등 트래픽이 큰 의미 없는 상황에서 사용 |
| Consistent Hash (hash-type consistent) | 요청 키(세션, 사용자 ID, 쿠키 등)를 기반으로 해시를 계산해 동일한 서버로 라우팅한다. | 캐시 서버, 채팅 서버 등에서 세션 일관성 유지 필요할 때 |
| First (first) | 순서상 첫 번째로 가용한 서버에 요청을 전달한다. | 서버 수가 적고 요청이 단순한 경우 (테스트용) |
정리
HAProxy는 단순히 여러 서버로 요청을 분산하는 도구가 아니라, 서비스의 특성과 트래픽 패턴에 맞춰 다양한 알고리즘을 적용할 수 있는 리버스 프록시다.
이번 실습에서는 트래픽 분산의 기본 원리를 확인하기 위해 Round Robin 방식을 사용했지만, 실제 서비스 환경에서는 LeastConn, Weighted Round Robin, Source 등의 방식을 트래픽 형태와 세션 구조에 맞춰 함께 사용하는 경우가 많다고 한다.(외쳐 갓PT)
단 한 줄의 설정(balance roundrobin → balance leastconn)만으로도 로드 밸런싱 정책이 완전히 달라지는 것을 보며,
HAProxy가 단순하면서도 유연한 구조라는 걸 체감할 수 있었다.
이번 실습을 통해 리버스 프록시의 개념과 트래픽 분산 구조의 기본 원리를 이해할 수 있었다.
'Java > 기술회고' 카테고리의 다른 글
| Kafka Consumer Lag 600을 0으로 만든 방법 (k6 + Prometheus +Grafana) (0) | 2026.01.28 |
|---|---|
| Thread.sleep()은 왜 위험할까? Kafka DLQ로 안전하게 재시도 처리하기 (0) | 2025.12.15 |
| Kafka 기반 실시간 경매 시스템 부하 테스트 및 병목 개선 (k6 + Prometheus + Grafana) (0) | 2025.10.18 |
| Copilot + JUnit 테스트 코드 자동 생성 환경 구성 (0) | 2025.10.16 |
| XSLT로 XML 데이터를 HTML로 변환해 JSP로 출력 해보기 (0) | 2025.08.26 |