Skip to content

Architecture

TAEHEON KIM edited this page Jun 26, 2026 · 3 revisions

Architecture

전체 구조

클라이언트 요청은 Nginx가 받아 App1 / App2 두 인스턴스에 Round Robin으로 분산합니다. 모든 서비스는 단일 AWS EC2 t3.large 위에서 Docker Compose로 구동됩니다.

Client
  -> Nginx (SSL, Load Balancer, gzip)
    -> App1 / App2 (Spring Boot, Kotlin, Virtual Thread)
      -> MySQL / Redis / Kafka / Elasticsearch

멀티 모듈 구조

헥사고날 아키텍처 기반으로 레이어를 모듈로 분리했습니다.

모듈 역할
domain 도메인 엔티티, Repository 인터페이스, 도메인 이벤트
application UseCase, Service, Kafka Consumer/Producer, Event Handler
infrastructure JPA/jOOQ 구현체, Kafka/Redis/ES 설정, 외부 어댑터
presentation Controller, Filter, Interceptor
bootstrap 애플리케이션 진입점, 환경별 설정 파일

의존성 방향: presentation -> application -> domain <- infrastructure


Java -> Kotlin 마이그레이션 이유

  • Null 안전성: nullable(?)과 non-null을 컴파일 타임에 구분해 NPE 원천 차단. Java의 Optional, @NonNull은 런타임까지 보장되지 않음
  • 간결한 문법: data class, 확장 함수, when 표현식으로 보일러플레이트 제거. 엔티티·DTO 변환 코드량이 눈에 띄게 감소

주요 흐름

1. 인증

Client -> Nginx -> App
App -> Kakao OAuth 2.0 (인가 코드 교환)
App -> JWT Access Token (30분) 발급
App -> Refresh Token -> Redis 저장 (7일)

2. 실시간 알림

App -> Outbox DB 저장 -> Kafka 발행
  -> NotificationKafkaConsumer
  -> 알림 DB 저장 + Redis Pub/Sub 발행
  -> RedisNotificationSubscriber
  -> SSE -> Client

Kafka를 거치는 이유: Outbox 패턴으로 DB에 먼저 저장 후 Kafka 발행. 발행 실패 시 OutboxRetryScheduler가 재시도해 유실 방지.

Redis Pub/Sub이 중간에 있는 이유: Kafka Consumer가 App1에서 실행되더라도 SSE 연결은 App2에 있을 수 있음. Redis Pub/Sub으로 전 인스턴스 브로드캐스트 -> 어느 인스턴스에 연결된 클라이언트도 알림 수신.

3. 실시간 채팅

Client <-> WebSocket STOMP <-> App
App -> Redis Pub/Sub 발행
  -> App1, App2 모두 수신
  -> 각자 연결된 Client에게 전달

알림과 달리 Kafka 없이 Redis Pub/Sub 직행. 유실보다 지연 최소화가 우선이기 때문.

4. 게시글 ES 동기화

App -> Outbox -> Kafka (post-sync topic)
  -> PostSyncKafkaConsumer
  -> Elasticsearch 인덱싱 (CREATE / UPDATE / DELETE)

게시글 저장 API 응답과 ES 인덱싱을 분리해 API 응답 지연 방지.


인프라 컴포넌트

컴포넌트 역할
MySQL 8.0 주 데이터 저장소. JPA + jOOQ, HikariCP max 50
Redis 7 게시글 캐시, Pub/Sub (알림·채팅), RateLimit, Idempotency, Refresh Token
Kafka KRaft 알림·ES 동기화 비동기 처리. DLT로 실패 메시지 격리
Elasticsearch 9.0.2 게시글 검색 (nori 분석기, 동의어 필터) + 로그 저장
Logstash App Logback JSON -> TCP 5044 수신 -> ES 인덱싱
Kibana ES 로그 시각화 (:5601), requestId 기반 요청 추적
Nginx SSL 종료, Round Robin 로드밸런싱, WebSocket 업그레이드, gzip

공통 적용 사항

항목 내용
Virtual Thread spring.threads.virtual.enabled: true 전체 앱 적용
Circuit Breaker Resilience4j — ES/Kafka 장애 시 빠른 실패 (실패율 50% 초과 시 OPEN)
Graceful Shutdown 배포 시 처리 중 요청 완료 후 종료 (timeout 30s)
MDC 로깅 모든 요청에 requestId, userId 자동 주입 -> Kibana 추적
Outbox Pattern Kafka 발행 전 DB 저장 -> 실패 시 스케줄러 재시도

Clone this wiki locally