주제: 사용자 맞춤형 날씨 정보 제공 및 상호작용 플랫폼
대상: 날씨 정보 활용 및 상호작용에 관심이 있는 사용자
- 정확한 날씨 정보 제공: 기상청 데이터와 AI 분석을 통해 지역별 맞춤형 날씨 정보 전달
- 미션 수행과 커뮤니티 강화: 날씨 기반 미션과 커뮤니티 상호작용을 통해 사용자 참여 유도
- 긴급 상황 대응: 기상특보 채팅으로 실시간 정보 공유 및 대응
- ⚛️ React: 사용자 인터페이스(UI) 개발을 위한 라이브러리
- 🌱 Spring Boot: 강력한 백엔드 애플리케이션 개발 프레임워크
- 🐬 MySQL: 데이터 저장 및 관리에 사용된 관계형 데이터베이스
- 🚀 Redis: 캐싱 및 실시간 데이터 저장
- 💬 Kafka: 메시지 큐 및 데이터 스트리밍 도구로 실시간 채팅 및 알림 구현
- 🛠️ Jenkins: CI/CD 자동화를 위한 도구
- 📦 Docker: 애플리케이션 컨테이너화
- ☸️ Kubernetes: 컨테이너 오케스트레이션 및 관리
- 🐙 Argo: DevOps 워크플로우 관리
- ☁️ AWS: 클라우드 인프라 호스팅
- 🔀 Nginx: 리버스 프록시 및 웹 서버
- 🐍 Python: 데이터 처리 및 AI 모델 개발
- 🔗 LangChain: AI 어플리케이션 구축을 위한 프레임워크
- 🐙 GitHub: 버전 관리 및 소스 코드 관리
- 📝 Notion: 프로젝트 관리 및 문서화
- 🎮 Discord: 팀 커뮤니케이션
- 🎨 Figma: UI/UX 디자인
-
JWT 기반 사용자 인증
- 사용자 로그인/회원가입 시 JWT 토큰 발급 및 인증 관리.
- STOMP 헤더에 JWT 토큰을 포함해 실시간 채팅에서도 인증 처리.
- 토큰에서 사용자 ID를 추출하여 커뮤니티, 미션 등 다양한 기능에 활용.
-
회원가입 및 로그인
- 일반 회원가입 및 카카오 소셜 로그인 지원.
- 비밀번호 검증 및 닉네임 중복 방지 처리.
-
사용자 정보 관리
- 사용자 정보 수정, 닉네임 변경, 로그아웃 및 회원 탈퇴 기능 제공.
-
날씨 기반 맞춤형 미션 제공
- 기상 데이터를 활용하여 아침, 점심, 저녁 기준으로 최대 3개의 미션 생성.
- 예시 미션: "자전거를 타세요", "텀블러를 사용하세요".
- 각 미션마다 포인트 지급률 및 진행 상태 표시.
-
AI 기반 미션 인증
- 사용자가 미션에 맞는 사진을 첨부하면 AI가 해당 사진의 적합성을 검증.
- 인증 성공 시 포인트 지급 및 실패 시 재도전 가능.
-
포인트 및 랭킹 시스템
- 포인트를 기반으로 사용자 랭킹 제공.
- 순위 확인 및 검색 기능을 통해 사용자 경험 향상.
-
위치 기반 게시글
- 사용자의 현재 위치를 기준으로 반경 5km 내 게시글 표시.
- 위치 변경 시 카카오맵 API를 통해 새로운 위치 반영 및 게시글 업데이트.
-
게시글 작성
- 제목(20자) 및 본문(150자) 글자 수 제한을 통해 사용자 경험 최적화.
- 좋아요/싫어요 버튼으로 게시글 신뢰도 평가 가능.
-
내가 작성한 글 관리
- 사용자가 작성한 글의 리스트를 확인 및 삭제 가능.
- 기상특보에 따른 실시간 채팅방 생성
- 기상특보 발생 시 해당 지역의 사용자들을 위한 오픈 채팅방 자동 생성.
- 예: "강원도 건조/한파 경보 채팅방".
- 기상특보 데이터 업데이트:
- Spring Scheduler를 활용해 1시간마다 기상청 API 호출.
- 기상특보 발령 시 '시/도' 기준으로 실시간 단체 오픈채팅방 자동 생성.
- 실시간 채팅:
- Redis:
- 최신 채팅 메시지 100개를 캐싱하여 빠른 채팅 로드 제공.
- 사용자 간 실시간 메시지 교환 성능 향상.
- WebSocket + STOMP:
- 실시간 양방향 통신 구현.
- 채팅방 내 메시지 브로드캐스팅 및 실시간 알림 기능 제공.
- Redis:
- WebFlux와 R2DBC를 활용한 확장:
- WeatherWise-Server-Chatting 레포지토리에서 MSA 구조로 구현.
- WebFlux와 R2DBC를 사용해 비동기 처리로 확장성과 성능 향상.
문제 상황
- 커뮤니티 게시글에서 "도움돼요" / "도움 안 돼요" 변수들은 공유 자원이기 때문에 멀티스레드 환경에서 Race Condition 문제 가능성을 확인하고, ExecutorService를 활용한 멀티스레드 환경 테스트를 진행한 결과 요청 수만큼 카운팅되지 않는 문제 발생.
- Thread-1 이 addVoate() 를 실행하여 upVote 값을 읽는데, 이때 upVote 값은 0
- Thread-1 은 0 에서 1 을 더한 값을 계산하여 커밋을 하려고 준비
- Thread-1 이 아직 커밋을 완료하지 않은 상태에서, Thread-2 가 addVote() 를 실행
- Thread-2 역시 upVote 값을 읽는데 Thread-1 이 커밋을 하지 않았기 때문에 upVote 값은 여전히 0
- Thread-2 는 0 에서 1을 더한 값을 계산하여 자신의 트랜잭션에 저장하고 커밋을 준비
- 결과적으로 Thread-1 , Thread-2 모두 정상적으로 커밋이 되었음에도 불구하고, upVote 값은 두 번 증가하지 않고 1로 남는 문제가 발생 (Race Condtion)
문제 해결
- 간단하게 생각해보면 Thread-1 이 upVote 를 업데이트 하는 동안 다른 Thread 들은 접근하지 못 하도록 하면 된다.
- 비관적 락 , 낙관적 락 , 트랜잭션 격리 수준 , Kafka 들을 활용해 동시성 문제를 해결
첫 번쨰 문제 해결 방법 : MySQL 비관적 락
- 트랜잭션이 데이터를 읽거나 수정할 때, 다른 트랜잭션이 해당 데이터에 접근하려는 시도를 "비관적" 으로 바라보기 때문에 아예 다른 트랜잭션의 접근을 사전에 차단하는 방법
- 장점
- 다른 트랜잭션의 접근을 사전에 차단하기 때문에 즉각적인 충돌을 방지하여 데이터 정합성을 보장
- 단점
두 번쨰 문제 해결 방법 : Java ReentrantLock
- Java 에서 제공하는 synchronized 와 BLOCKED 상태를 통한 임계 영역 관리의 한계를 극복하기 위해 Lock 인터페이스와 ReentrantLock 구현체를 제공하는데, 이를 활용하여 동시성 문제를 해결
- 첫 번쨰 문제 해결 방법과 마찬가지로 Java ReentrantLock 은 비관적 락이지만, MySQL 비관적 락은 데이터베이스 레벨의 Lock 이고 Java ReentrantLock 은 애플리케이션 레벨의 Lock
- 장점
- lock() , unlock() 으로 Lock 을 명시적으로 제어할 수 있어 정교한 Lock 제어가 가능함
- ReentrantLock은 공정락(Fair Lock) 을 설정할 수 있기 때문에 스레드 기아 상태에 빠지는 것을 방지할 수 있음
- Condition 객체를 사용해 특정 조건을 만족할 때만 스레드를 꺠우거나 대기 상태로 전환할 수 있어, 동기화 로직을 더 세밀하게 구현할 수 있음
- 단점
- 첫 번째 문제 해결 방법과 같은 비관적 락 메커니즘을 사용하기 떄문에 높은 트래픽 환경에서는 성능 저하가 발생할 수 있음
- 직접 lock() , unlock() 을 명시해야 하기 때문에 코드 복잡성이 증가
세 번쨰 문제 해결 방법 : MySQL 낙관적 락
- 데이터에 대한 충돌 가능성이 낮다고 가정하고 락을 사용하지 않고 작업을 진행
- 데이터 변경 시점에 충돌 여부를 확인하여 충돌이 발생한 경우 트랜잭션을 롤백하고 재시도 하는 방식으로 동작
- 장점
- Lock 을 걸지 않기 떄문에 Lock 로 인한 CPU 와 메모리 오버헤드가 없음
- 데이터 충돌 가능성이 낮은 환경에서 높은 트래픽을 효율적으로 처리할 수 있음
- 충돌이 드물다면 비관적 락 보다 빠르고 효율적
- 단점
- 충돌이 많이 발생하게 된다면 트랜잭션을 롤백하고 재시도 해야 하기 때문에, 충돌 빈도가 높으면 성능이 저하
- 충돌 발생 시 트랜잭션을 어떻게 재시도 할지 설계해야 하므로 구현이 복잡
- 충돌로 인해 업데이트가 반복되면 데이터베이스에 불필요한 부하를 초래할 수 있음
네번쨰 문제 해결 방법 : Kafka 사용
- Kafka 는 메시지를 파티션 단위로 관리하며, 파티션 내 메시지는 순차적으로 처리 되는 것을 보장하기 때문에 "도움돼요" / "도움 안 돼요" 를 증가하거나 감소할 경우 그 이벤트를 Kafka 에 발행하여 비동기적으로 처리할 수 있도록 함
- 장점
- 비동기적으로 메시지를 처리하기 때문에 높은 트래픽에서도 안정적인 처리가 가능
- 단점
- Kafka 를 설정하고, 메시지 생산 및 소비 코드를 추가해야하기 때문에 초기 설계가 복잡할 수 있음
- Kafka 를 관리하기 위한 추가적인 인프라 비용이 발생
- 성능 관점에서는 TPS 가 100 이하인 경우에는 MySQL 낙관적 락을 사용
- TPS 100 ~ 300 에서는 MySQL 비관적 락 or Java ReentrantLock 을 사용
- 그 이상의 TPS 인 경우에는 Kafka 를 사용
- 물론 Kafka 를 사용하는 것이 모든 TPS 에서 유리하지만, Kafka 를 관리하기 위한 인프라 비용과 낮은 TPS 인 경우에 굳이 Kafka 를 사용할 필요성이 없다고 생각함
문제 상황
- 기존 모놀리식 구조에서는 Spring MVC를 사용하여 초당 최대 400명의 사용자 요청을 처리했으나, 이 이상의 트래픽이 발생하면 서버가 과부하로 인해 다운되는 문제가 발생.
- 응답 지연이 심해지고, 데이터의 정합성에도 문제가 생길 가능성이 높아짐.
1. 성능 병목 분석 및 원인 추론
- 단순 코드 최적화로는 문제 해결이 어려움을 인식하고, Prometheus와 Grafana를 도입해 실시간 메트릭 수집 및 병목 구간 분석.
- 분석 결과, Tomcat 워커 스레드가 최대값(200개)에 도달해 더 이상 요청을 처리하지 못하고 대기하는 현상 확인.
- 이때 CPU 사용률은 낮은 반면, 스레드는 과포화 상태였으며 이는 I/O 블로킹 구조로 인해 리소스가 비효율적으로 소비되고 있음을 의미한다고 판단.
- 즉, Spring MVC 기반의 동기 구조에서는 DB 접근, Redis 접근과 같은 I/O 작업 동안 스레드가 블로킹되어 다른 요청을 처리하지 못하는 구조적 한계가 병목의 근본 원인이었음.
2. MSA 도입과 WebFlux 기반 비동기 구조 전환
- 채팅 기능을 독립적인 MSA로 분리하고, Spring WebFlux 기반으로 리팩토링.
- WebFlux의 이벤트 루프 기반 아키텍처와 논블로킹 특성을 활용해 동시 처리 성능 대폭 향상.
- MVC: 요청당 스레드 1개 고정 → WebFlux: 이벤트 루프 내에서 스레드 재사용 가능.
- 같은 부하에서 Runnable 스레드 수: MVC 197개 → WebFlux 72개.
- 즉, WebFlux 구조는 적은 스레드로 더 많은 요청을 처리할 수 있어 리소스 효율성이 높음.
- Kafka 도입으로 메시지 I/O 처리 병렬화 및 서비스 간 결합도 감소.
- WebSocket → Kafka Producer → Kafka Consumer → Redis 캐시/DB 저장 → WebSocket Broadcast 흐름 구성.
- Kafka 파티션 분할을 통해 메시지 병렬 소비 구조 구성.
- R2DBC 도입으로 MySQL DB 접근의 블로킹 문제 해결.
- 기존 JDBC의 커넥션 대기/점유 문제 해소 → 비동기 처리로 저장 속도 개선.
- Redis 캐싱 최적화:
- 최신 메시지 100개를 캐싱하고, 요청 시 Redis에서 즉시 응답.
- 동시성 이슈 방지를 위해 락 기반 접근 구조 설계.
3. 데이터 정합성 보장
- Kafka를 통해 메시지를 유실 없이 소비하고 DB에 저장.
- 4만 건 이상의 메시지 전송 시에도 데이터 일치율 100% 확인.
- 비동기 구조에서도 메시지 순서 보장과 중복 방지 로직 설계.
4. 수치 기반 성과
- TPS 증가: 초당 메시지 처리량 400건 → 3,700건 이상 (약 9배 증가).
- 응답 지연 감소: 평균 응답 시간 5.52초 → 0.13초 (약 40배 단축).
- 사용자 처리량 증가: 초당 400명 수준 → 3700명 이상 동시 처리 가능.
- 스레드 효율성 향상: Runnable 스레드 수 약 60% 절감 (197개 → 72개).
- 데이터 정합성: 100% 메시지 저장 일치율 유지.
요약
- 단순 성능 개선을 넘어, 병목 원인을 메트릭 기반으로 분석하고, 구조적 한계를 해결하기 위한 아키텍처 리디자인을 수행함.
- 구조적 전환(WebFlux + R2DBC + Kafka)을 통해 높은 확장성과 안정성을 확보하고, 동시 접속자 증가에도 대응 가능한 실시간 채팅 시스템 구현.
기존 모놀리식 아키텍처 에서 마이크로서비스 아키텍처(MSA) 로 전환하는 과정을 설명합니다.
특히, 쿠폰 발급 서비스의 성능 문제를 해결하기 위해 Kafka를 활용한 비동기 메시지 처리 방식으로 전환한 사례를 중심으로 다룹니다.
- 특정 프로모션을 통해 사용자에게 쿠폰을 발급하는 이벤트입니다.
- 아래 발급 조건을 충족해야 쿠폰을 받을 수 있습니다.
- 한 사람당 한 개의 쿠폰만 가질 수 있다.
- 당일 미션 하나를 인증 받아야 한다.
- 이벤트 기간 동안, 매일 특정 시간에 오픈하며 총 지급 수량을 한정한다.
- 쿠폰 지급 수량은 당일 정해진 양을 초과할 수 없다.
모놀리식 아키텍처는 모든 기능이 단일 서비스 내에 통합되어 있어 아래와 같은 문제가 발생했습니다:
- 서버 마비: 트래픽이 급증하면 서버 자원이 고갈되어 기본 기능(예: 회원가입, 로그인)조차 정상적으로 작동하지 않음.
- TPS 한계: 평균 TPS 200에 도달 시 웹서버가 병목현상을 일으키며, 전체 서비스가 중단.
모놀리식 구조의 한계를 극복하고, 높은 트래픽 상황에서도 안정적인 서비스를 제공하기 위해 MSA를 도입했습니다.
- 각 기능을 독립적인 마이크로서비스로 분리.
- 특정 서비스 장애가 전체 시스템에 영향을 미치지 않도록 설계 가능.
- 초기에는 서비스 간 통신 방식으로 가장 익숙한 HTTP를 사용.
- 그러나 HTTP는 동기 요청 방식이기 때문에 모놀리식의 한계(서비스 간 결합도)와 장애 전파 문제를 해결하지 못함.
- 기존 HTTP 요청 방식의 결합도 높음과 장애 전파 위험을 해결하기 위해 Kafka를 도입.
- Kafka를 통해 비동기 메시지 기반 아키텍처로 전환.
- 비동기 처리
- 메시지를 큐에 저장하여 서비스 간 비동기적으로 데이터를 처리.
- 내결함성
- 메시지가 큐에 저장되므로 특정 서비스가 다운되더라도 메시지가 유지됨.
- 서비스 복구 후 메시지 재처리 가능.
- 높은 처리량
- Kafka는 높은 스루풋을 지원해 대량의 메시지를 효율적으로 처리.
- 확장성
- 메시지 브로커를 통해 서비스 간 결합도를 낮춰, 각 서비스를 독립적으로 확장 가능.
- 서비스 독립성 강화
- 서비스 간 직접적인 의존성을 제거하고, 메시지 브로커를 통한 간접적 통신으로 독립성 강화.
- 유연한 에러 처리
- 장애 발생 시 메시지를 큐에 저장하여 재처리하거나 다양한 방식으로 에러 처리 가능.
- 성능 향상
- 비동기 메시지 처리로 블로킹 없이 다수의 요청을 처리해 전체 시스템 성능 개선.
Kafka 기반 비동기 메시지 처리로 다음과 같은 성과를 달성했습니다:
- 성능 개선
- 트래픽 폭주 상황에서도 서비스 마비 없이 안정적 동작.
- 신뢰성 향상
- 서비스 다운 시에도 메시지가 큐에 저장되어 복구 후 처리가 가능.
- 확장성 강화
- 각 서비스를 독립적으로 스케일링 가능해 전체 시스템 유연성 증가.
- 데이터 정합성 유지
- 비동기 메시지 처리로 데이터 정합성을 유지하며, 고트래픽 상황에서도 안정적 데이터 처리 가능.
모놀리식 아키텍처에서 MSA로의 전환은 초기에는 복잡해 보일 수 있습니다. 그러나 높은 트래픽과 장애 상황에서 문제를 효과적으로 해결할 수 있는 강력한 방법입니다.
Kafka를 활용한 비동기 메시지 처리는:
- 서비스 간 결합도를 낮추고,
- 시스템 신뢰성과 확장성을 향상시키며,
- 안정적인 사용자 경험을 제공합니다.
이러한 전환을 통해 서비스 간 독립성을 확보하고, 높은 트래픽 상황에서도 확장성과 안정성을 겸비한 아키텍처를 구현할 수 있었습니다.



