Docker 기반 Spring 다중 서버 + Nginx 로드 밸런싱 환경 구축
backend1,backend2,backend3: 동일한 Spring 애플리케이션 컨테이너 3개 생성- 각 컨테이너에
PROJECT_NAME환경변수로 서버 식별 값 주입 nginx.conf를 통해 라운드로빈 방식으로 요청 분산 처리- Nginx 컨테이너에서 3개 백엔드 컨테이너를
upstream으로 연결 - MySQL 컨테이너 구성 및
volume설정 완료 (/volumes/mysql-data) - Spring 컨트롤러에서
/api엔드포인트로 응답값 출력 docker-compose.yml을 사용해 전체 구성 자동화
- 브라우저에서
http://localhost접속 시 backend 서버가 순차적으로 응답하는 구조 확인 완료
STOMP 기반 WebSocket 채팅 서버 및 Nginx WebSocket 프록시 구성
- Spring WebSocket + STOMP 설정 완료 (
/ws-chat엔드포인트 사용) - 프론트엔드에서
nickname파라미터 기반 WebSocket 연결 구현 (/ws-chat?nickname=xxx) @MessageMapping,@SendTo로직으로 STOMP 메시지 처리 구성- Nginx에서
/ws-chat요청을 WebSocket 업그레이드와 함께 백엔드로 프록시 처리 nginx.conf에proxy_set_header및Upgrade헤더 설정으로 WebSocket 연결 유지- backend1 ~ backend3 컨테이너에 동일한 WebSocket 서버 적용
- MySQL과 연결되는 Spring backend 컨테이너 정상 기동 확인
- Redis Pub/Sub 없이도 WebSocket 연결 및 메시지 송수신 확인
- 브라우저에서
http://localhost접속 후 WebSocket 연결 및 채팅 메시지 송수신 정상 작동
Redis Pub/Sub 기반 WebSocket 채팅 메시지 분산 처리 구현
- Redis 컨테이너 추가 및
StringRedisTemplate기반 Pub/Sub 구조 도입 - 각 Spring 서버가 Redis를 통해 채팅 메시지를 publish → subscribe 하도록 구성
RedisPublisher클래스: 채팅 메시지를 Redis 채널(room.*)로 발행RedisSubscriber클래스: Redis로부터 메시지를 수신해 WebSocket 구독자에게 전달RedisMessageListenerContainer설정으로room.*,user.*채널 패턴 구독 처리- 서버 간 메시지 일관성을 위해 단일 서버가 아닌 모든 서버가 동일한 메시지를 수신하도록 설계
- 귓속말 기능을 위해
/user/queue/private경로 활용 (WebSocket STOMP)
backend1,backend2,backend3중 어떤 서버로 접속해도 채팅방 메시지가 모든 서버에 동기화됨- Redis가 메시지를 중계하여 멀티 서버 환경에서 실시간 채팅 일관성 보장
- 클라이언트 간 귓속말 메시지 또한 정상 수신됨 (
/user/queue/private)
AI 자동응답 기능 연동 및 WebSocket 프록시 문제 해결
GPTService.java를 통해 OpenAI GPT-4.1 API 연동 구현- 클라이언트 메시지를 OpenAI API로 전송하고 응답을 다시 채팅방에 전송
- Spring STOMP 채팅 흐름에 AI 챗봇 자동응답 기능 추가
- 메시지를 수신하면 GPT 응답을 자동으로 브로드캐스트
HttpClient를 사용해 OpenAI API 호출 및 JSON 응답 파싱 로직 작성
- 실수로 커밋된 API Key 노출 문제 해결
- GitHub Push Protection으로 인한
GH013에러 발생 - 해당 API Key가 포함된 파일을 삭제하고 커밋 내역을 정리한 뒤
--force push로 재업로드
- GitHub Push Protection으로 인한
- Nginx에서 WebSocket 연결 실패 및 프록시 문제 디버깅
proxy_pass,Upgrade,Connection등 헤더를 재설정하여 WebSocket 정상 연결/ws-chat경로의 WebSocket 요청이 백엔드로 올바르게 전달되도록nginx.conf수정
- 브라우저 콘솔(F12) 및
docker logs명령어를 통한 실시간 로그 확인
- 클라이언트가 일반 채팅 메시지를 보내면, GPT-4.1 응답이 자동으로 수신되어 실시간 전송됨
- 브라우저에서 WebSocket 연결 및 AI 응답 동작 모두 정상 작동
- 민감 정보가 GitHub에 포함되지 않도록 토큰을 제거한 커밋으로 정리 완료
이 프로젝트는 docker-compose 구성 파일을 단일 파일 방식에서
**data(데이터 계층)와 backend(애플리케이션 계층)**으로 분리하여
보다 유연하고 유지보수하기 쉬운 구조로 개선
- 모든 서비스(MySQL, Redis, 백엔드, Nginx)가 한 파일에 정의됨
- 실행, 중지, 재시작을 모두 함께 수행해야 함
- 백엔드 코드를 수정해도 DB까지 영향을 받음
- 테스트나 배포 유연성이 낮음
- MySQL, Redis 등 데이터 서비스만 정의
- 외부 네트워크(
prod_server)를 통해 백엔드와 통신
- Spring 백엔드 서버 3개 + Nginx 로드밸런서 정의
- DB와 Redis는 연결만 하고 직접 실행하지 않음
| 구분 | 장점 |
|---|---|
| 독립 실행 | DB와 Redis를 따로 실행/종료/유지보수할 수 있음 |
| 빠른 빌드 | 백엔드만 변경 시, 데이터 서비스 재시작 없이 빠른 빌드 가능 |
| 재사용성 | data 구성은 다른 프로젝트에서도 재사용 가능 |
| 배포 유연성 | 운영 환경에서 외부 DB/Redis 연결에도 쉽게 대응 가능 |
| 장애 격리 | 데이터 서비스 장애가 있어도 백엔드를 따로 유지 가능 |
| 로컬 테스트 최적화 | backend만 여러 번 재기동하며 개발/테스트하기 쉬움 |
모든 서비스는 prod_server라는 공통 Docker 네트워크에 연결되어 있으며,
이를 통해 서로 다른 디렉토리의 Compose 파일에서도 컨테이너 간 통신이 가능
docker network create prod_serverSpring Data JPA 기반 회원가입/로그인 기능 구현 및 ERD 정리
- Spring Data JPA를 기반으로 회원가입 → 로그인 흐름 구현
- 회원가입 시 입력한 정보는
user,user_profile테이블에 저장 - 로그인 시 입력한
userid,password가 DB 정보와 일치하는지 검증 - 로그인 성공 시 세션 기반 인증 처리 (
HttpSession사용) - 로그인/회원가입 페이지 HTML 폼 작성 및 연동 완료
- 사용자(
user)와 사용자 프로필(user_profile) 분리 설계 - 게시글(
board), 댓글(comment), 인증(auth)까지 포함한 전체 구조 기반으로 기능 설계
- 브라우저에서 회원가입 → 로그인 → 세션 생성까지 흐름 정상 작동
- 로그인 성공 후 세션을 통해 인증 상태 유지 가능
게시판 기능 구현 (CRUD, 검색, 페이징) 및 대용량 배치 저장 처리
Board엔티티 기반으로 게시판 CRUD 기능 구현- 글쓰기:
/boards에POST요청으로 게시글 등록
→user_id를 통해 작성자User와 연관관계 설정 - 게시글 상세 조회:
/boards/{id}GET요청 - 게시글 수정:
/boards/{id}PUT요청 - 게시글 삭제:
/boards/{id}DELETE요청
- 글쓰기:
Board는User와 다대일(@ManyToOne) 관계이며, 댓글(Comment)과는 일대다(@OneToMany)로 매핑됨- 모든 작업은
BoardDTO를 통해 클라이언트와 데이터 송수신 처리
- 페이징된 전체 조회:
/boards?page={번호}&size={개수}로 요청 시Page<BoardDTO>응답
→findAllPaging(Pageable)JPQL 사용 - 키워드 검색:
/boards/search?keyword={text}로 제목 또는 내용에서 키워드 포함된 게시글 검색
→searchKeywordPaging(String, Pageable)JPQL 사용
→ 대소문자 구분 없이LIKE검색 수행 - 검색 + 페이징 동시 지원
BoardController의/boards/batchInsertAPI를 통해List<BoardDTO>단위로 MySQL에 배치 저장- 내부적으로
JdbcTemplate.batchUpdate()사용 - SQL 문:
INSERT INTO board (title, content, user_id, created_date, updated_date, batchkey) VALUES (?, ?, ?, ?, ?, ?) - 1000개 단위로 자른 후 반복 저장하며
batchkey(UUID)를 지정해 동일 배치 내 데이터 식별
- 내부적으로
/boards/jpaBatchInsertAPI는List<Board>를EntityManager.persist()방식으로 저장- 1000개마다
flush()및clear()호출하여 메모리 사용량 제어
- 1000개마다
- CRUD, 검색, 페이징 기능이 모두 정상 동작함
- 수천 개의 게시글도
/batchInsert,/jpaBatchInsertAPI를 통해 안정적으로 저장됨 username,user_id,created_date,updated_date정보가BoardDTO에 포함되어 응답으로 제공됨
Spring Security + JWT 기반 인증 시스템 적용 및 게시판 권한 연동
- 기존 단순 로그인 방식을 Spring Security 기반 구조로 전환
- 로그인 시 Access Token / Refresh Token 발급 및 DB(
auth테이블)에 저장- Access Token: 짧은 유효시간의 인증 토큰
- Refresh Token: 재발급을 위한 장기 토큰 (쿠키 or Authorization 헤더에서 수신)
- JWT 토큰은
JwtTokenProvider에서 생성/검증하며,JwtTokenFilter에서 요청마다 인증 수행
- 사용자는
/api/auth/loginSecurity로 로그인 요청
→ 유효한 경우 JWT 토큰 발급 및 응답 - 이후 클라이언트는 요청 시
Authorization: Bearer <access_token>헤더 포함 - 백엔드는
JwtTokenFilter를 통해 토큰을 검증하고SecurityContext에 사용자 정보 저장
- 게시글 작성 시
@AuthenticationPrincipal을 통해 현재 로그인한 사용자 정보를 획득
→ 해당 사용자의user_id를 자동으로 게시글에 연관 설정 - 토큰 없이 게시글 작성 시
403 Forbidden응답
/api/auth/refresh엔드포인트로 Refresh Token 전달 시, 새 Access Token 발급- Refresh Token은 쿠키 또는
Authorization헤더에서 자동 추출됨 - 유효성 검사 후 새로운 토큰으로 갱신 및
auth테이블 업데이트
- JWT 기반 로그인/인증 흐름이 정상 작동하며, 모든 요청에서
SecurityContext를 통해 사용자 정보 활용 가능 - 게시판 글 작성 시 현재 로그인한 사용자의 ID가 자동 연결됨
- 로그인 후 토큰 없이 요청 시 접근 차단(403) 확인 완료
- Refresh Token을 통한 Access Token 갱신 기능 정상 작동
ElasticSearch + Logstash + Kibana 구축
prod_server네트워크 기반으로 ElasticSearch, Logstash, Kibana 컨테이너 구축 및 연동backend1 ~ backend3애플리케이션 로그를 Logstash → ElasticSearch로 전달 후 Kibana 시각화logback-spring.xml을 사용해 ELK 연동용 파일 로그 출력 구조 적용
- 로그 경로:
logs/app.log - 출력 패턴:
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-3level] %logger{5} - %msg%n - RollingFileAppender 사용:
- 일자별 파일 자동 분리(
app-YYYY-MM-DD.log) - 10일치까지만 보관 후 자동 삭제
- 일자별 파일 자동 분리(
- 콘솔 비동기(ASYNC) + 파일 로그 동시 출력,
root level="INFO"설정
- input:
/logs/app.log,start_position => "beginning",sincedb_path => "/dev/null" - filter:
grok으로timestamp,thread,level,logger,log추출AOP_LOG→log_type: aop,OAuth2_LOG→log_type: OAuth2date필터로@timestamp변환,Asia/Seoul타임존 적용
- output:
- ElasticSearch(
http://elasticsearch:9200), 인덱스:spring-logs-%{+YYYY.MM.dd} stdout { codec => rubydebug }(디버깅용)
- ElasticSearch(
-
ElasticSearch
- 이미지:
docker.elastic.co/elasticsearch/elasticsearch:8.12.0 - 포트:
9200,9300 - 환경:
xpack.security.enabled=false - 볼륨:
./volumes/esdata:/usr/share/elasticsearch/data
- 이미지:
-
Kibana
- 이미지:
docker.elastic.co/kibana/kibana:8.12.0 - 포트:
5601 - 환경:
ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - 볼륨:
./volumes/kibana-data:/usr/share/kibana/data
- 이미지:
-
Logstash
- 이미지:
docker.elastic.co/logstash/logstash:8.12.0 - 포트:
5044,5000 - 볼륨:
./logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf./logstash/logstash.yml:/usr/share/logstash/config/logstash.yml:ro../../logs:/logs
- 네트워크:
prod_server
- 이미지:
✅ ElasticSearch 상태 확인
curl -X GET "localhost:9200/_cat/indices?v"
- ElasticSearch 기반 게시판 검색 기능 구현
- 접두어, 초성, 중간 글자, 오타 허용 검색 기능 구현
- MySQL과 ElasticSearch 분리 관리 가능하도록 구성
- 접두어 검색:
autocomplete_analyzer사용 - 중간 글자 검색:
ngram_analyzer사용 - 초성 검색:
chosung_analyzer사용 - 오타 허용 검색:
fuzziness: "AUTO"옵션 (3자 이상부터 적용)
-
인덱스:
board-indexautocomplete_analyzer,ngram_analyzer,chosung_analyzer적용
-
BoardEsDocument
- ElasticSearch 전용 DTO
title,content,username,userId,created_date,updated_date필드 저장
-
BoardEsService
search(keyword, page, size)메서드- 접두어 검색:
PrefixQuery - 중간 글자 검색:
MatchQuery - 초성 검색:
PrefixQuery - 오타 허용 검색:
MatchQuery+fuzziness: "AUTO"
- 접두어 검색:
bulkIndexInsert메서드를 통한 대용량 색인 가능
-
BoardEsController
- 검색 API 경로:
/boards/elasticsearch?keyword={keyword}&page={page}&size={size}
- 검색 API 경로:
-
BoardEsRepository
ElasticsearchRepository<BoardEsDocument, String>기반 CRUD 지원
- Postman 및 브라우저를 통한 검색 API 정상 동작 확인
- Kibana Dev Tools:
GET board-index/_search { "query": { "match_all": {} } }
- ElasticSearch 인덱스 상태 확인:
curl -X GET "localhost:9200/_cat/indices?v" - 대용량 색인 이후에도 검색 속도 및 정확도 정상 유지
Kafka 기반 검색 이벤트 처리 및 Elasticsearch 검색어 통계 적재
- Kafka를 이용하여 검색 이벤트 비동기 전송 처리
- 검색 시 검색어 이벤트를 Kafka Topic에 발행
- Kafka Consumer에서 수신한 검색 이벤트를 Elasticsearch에 저장
- Elasticsearch에 검색어, 사용자 ID, 검색 시간 저장
- Docker 기반 Kafka, Zookeeper, Elasticsearch 컨테이너 실행 및 연동 확인
- 검색 → Kafka 발행 → Consumer → Elasticsearch 저장 단일 플로우 테스트 완료
-
Kafka Producer/Consumer
SearchLogMessageDTO 사용- Kafka Topic:
search-log - Consumer Group:
search-log-group - 수신 메시지를
SearchLogDocument로 변환하여 Elasticsearch에 저장
-
Elasticsearch Repository
SearchLogEsRepository를 통해SearchLogDocumentCRUD 처리SearchLogEsService의save()메서드로 데이터 저장
- Kafka, Zookeeper: Kafka Topic 발행 및 Consumer 수신
- Elasticsearch, Kibana, Logstash: 검색어 통계 및 로그 분석 처리
- Prometheus, Grafana: 서버 상태 모니터링
- Kafka 기반 검색 이벤트가 비동기 처리되어 Elasticsearch에 적재됨
- Kibana에서 검색어 통계를 시각적으로 확인 가능
- Docker Compose를 통해 Kafka, Zookeeper, Elasticsearch, Kibana, Logstash, Prometheus, Grafana 환경을 한 번에 구성하여 연동 완료

