-
Notifications
You must be signed in to change notification settings - Fork 0
Tech Decisions
Java 코드베이스에서 마이그레이션. null 안전성과 간결한 문법이 주된 이유.
-
Null 안전성: 타입 시스템이
nullable(?)과 non-null을 컴파일 타임에 구분해 NPE 원천 차단. Java에서는Optional이나@NonNull으로 방어해도 런타임까지 보장되지 않음 -
간결한 문법:
data class, 확장 함수,when표현식으로 보일러플레이트 제거. 엔티티 <-> DTO 변환 코드가 눈에 띄게 줄어듦
트레이드오프: Java 대비 컴파일 속도 느림. 팀원이 Kotlin에 익숙하지 않으면 학습 곡선 존재.
자체 회원가입/로그인을 구현하면 비밀번호 암호화, 이메일 인증, 찾기 기능까지 구현해야 함. OAuth 2.0으로 인증을 카카오에 위임해 보안 관련 책임을 줄이고 핵심 도메인 개발에 집중.
트레이드오프: 카카오 서비스 장애 시 로그인 불가. 카카오 계정이 없는 사용자는 가입 불가.
Stateless 인증으로 서버가 세션을 저장할 필요 없음. Access Token (30분) + Refresh Token (7일, Redis) 조합으로 탈취 피해 최소화. RTR 방식으로 재발급 시 기존 토큰 덮어쓰기.
트레이드오프: Access Token은 만료 전까지 서버에서 강제 무효화 불가. 토큰 크기가 세션 ID보다 커서 매 요청 헤더 크기 증가.
유저-게시글-지원-팀-채팅-알림 간 관계가 복잡하고 트랜잭션이 중요한 도메인. 관계형 모델과 ACID 보장이 필요해 RDB 선택.
트레이드오프: 수평 스케일아웃이 NoSQL보다 어렵고 비용이 높음.
Command(쓰기) 레이어에서 도메인 엔티티 중심 개발. 연관관계 매핑, 변경 감지, 트랜잭션 관리를 프레임워크에 위임. 복잡한 조회는 jOOQ로 분리(CQRS).
트레이드오프: 연관관계가 복잡해지면 N+1 문제 발생 가능. 복잡한 쿼리는 JPQL 한계로 jOOQ나 네이티브 쿼리로 우회 필요.
QueryDSL은 JPA 엔티티 기반 Q클래스를 생성해 Query 레이어가 JPA 엔티티 구조에 종속됨. CQRS로 분리해도 형식적인 수준에 그침.
- JPA 독립성: DB 스키마 기반 코드 생성으로 Query 레이어가 JPA 엔티티에 의존하지 않음
- 타입 안전성: 컬럼명 오타, 타입 불일치를 컴파일 타임에 감지
- SQL 표현력: DB 특정 함수, 복잡한 JOIN을 SQL에 가깝게 표현 가능
트레이드오프: DB 스키마 변경 시 코드 재생성 필요.
CI 환경에서 DB 없이 generateJooq 실행 불가 -> -x generateJooq 스킵으로 해결.
자주 조회되는 컬럼 조합에 복합 인덱스 적용. 정렬 기준 컬럼을 인덱스에 포함시켜 정렬 연산 비용 제거.
(status, deadline) — 모집 중 필터링 + 마감임박순 정렬을 인덱스 스캔만으로 처리.
트레이드오프: 인덱스 추가 시 쓰기 성능 저하. 인덱스 수가 많아질수록 INSERT/UPDATE 시 인덱스 갱신 비용 증가.
지원은 여러 사용자가 동시에 시도하는 액션. 낙관적 락은 충돌 시 예외를 던지고 재시도를 클라이언트에게 위임하는데 지원 같은 경우 재시도가 사용자 경험을 해침. 비관적 락으로 행을 선점해 먼저 들어온 요청이 처리 완료 후 다음 요청을 처리하도록 순서 보장.
트레이드오프: 락 대기 중 다른 요청이 블로킹되어 처리량 감소. 락 타임아웃 3초로 무한 대기를 방지했지만 타임아웃 초과 요청은 실패 처리됨.
@Async만으로는 애플리케이션이 죽으면 이벤트가 사라짐.
Kafka는 메시지를 디스크에 영속 보관해 Consumer 재시작 후 재처리 가능.
Outbox 패턴과 결합해 발행 실패 시 재시도. DLT로 실패 메시지 격리.
트레이드오프: 운영 복잡도 증가. 단순 비동기 처리 대비 인프라 비용이 높음. 단일 인스턴스 환경에서는 오버엔지니어링일 수 있음.
Kafka 발행과 DB 저장을 같은 트랜잭션으로 묶을 수 없어 발행 실패 시 유실 발생.
DB에 Outbox 테이블로 먼저 저장 후 Kafka 발행.
발행 성공 시 SENT 처리, 실패 시 스케줄러가 PENDING 상태를 감지해 재시도.
트레이드오프: Outbox 테이블 write로 DB 부하 증가. 스케줄러 (30초 주기) 기반 재시도라 발행까지 최대 30초 지연 가능.
알림 처리를 API 응답 경로에서 분리.
댓글 저장 API가 알림 DB 저장 + Kafka 발행까지 동기로 처리하면 커넥션 홀딩 시간이 길어져 커넥션 풀 고갈.
@Async로 알림을 백그라운드에서 처리해 API는 즉시 응답.
트레이드오프: DiscardPolicy 적용으로 풀 포화 시 알림이 조용히 버려짐.
알림 유실 가능성을 의도적으로 수용한 결정.
단순 @Scheduled + 루프는 하나의 트랜잭션으로 전체를 처리해 메모리 부담과 롤백 범위가 커짐.
JpaCursorItemReader로 스트리밍 조회, Chunk 1,000건씩 커밋해 메모리 효율적으로 대량 처리.
트레이드오프: 배치 설정 복잡도 증가. 현재 데이터 규모에서는 단순 스케줄러로도 충분할 수 있음.
인메모리 저장소로 응답 속도가 빠름. TTL 지원으로 캐시 만료 관리 편리. Pub/Sub 내장으로 별도 브로커 없이 멀티 인스턴스 동기화 가능. Rate Limiting, Idempotency, Refresh Token, 게시글 캐시를 단일 Redis로 처리.
트레이드오프: 인메모리라 Redis 장애 시 캐시 데이터 유실.
RedisCacheErrorHandler로 장애 시 DB 폴백 처리했지만 폴백 구간 동안 DB 부하 증가.
멀티 인스턴스 환경에서 WebSocket/SSE 세션은 인스턴스별로 독립적. Redis Pub/Sub으로 발행하면 구독 중인 모든 인스턴스가 수신해 각자 연결된 클라이언트에게 전달.
트레이드오프: 메시지 영속성 없음. 구독자가 없는 순간 발행된 메시지는 유실됨. 알림은 Kafka Outbox로 보완, 채팅은 유실보다 지연 최소화가 우선이라 수용.
채팅은 서버가 먼저 클라이언트에게 메시지를 보내야 함. HTTP 폴링 대비 불필요한 요청 없이 낮은 지연. STOMP는 채널(topic/queue) 개념과 구조화된 메시징을 제공해 Spring 메시지 브로커와 자연스럽게 통합.
트레이드오프: 연결 유지 비용으로 서버 메모리 사용 증가. 연결이 많아질수록 메모리 압박.
LIKE '%스프링%'은 앞에 %가 붙으면 인덱스를 타지 않아 풀 스캔 발생.
MySQL FULLTEXT INDEX는 한국어 형태소 분석 지원이 제한적.
nori 분석기로 형태소 단위 토크나이징, 동의어 필터로 한/영 혼용 검색 처리.
트레이드오프: 운영 복잡도 증가. 색인 지연(Near Real-Time)으로 게시글 저장 직후 검색 결과에 즉시 반영되지 않을 수 있음. DB와 ES 간 데이터 정합성 관리 필요.
동일 사용자의 반복 요청으로 인한 DB 부하와 어뷰징 방지.
@RateLimit 어노테이션으로 API별 선택적 적용.
Redis Sorted Set + Lua Script로 Sliding Window를 원자적으로 처리해 분산 환경에서도 정확한 제한 보장.
트레이드오프: 임계값 설정이 어려움. 너무 낮으면 정상 사용자도 차단, 너무 높으면 어뷰징 방어 효과 감소.
네트워크 재시도나 더블클릭으로 인한 중복 생성 방지.
동일한 Idempotency-Key로 재시도 시 캐시된 응답 반환해 DB 처리 없이 동일한 결과 보장.
트레이드오프: 키 생성 책임이 클라이언트에 있음. 클라이언트가 키를 매번 다르게 생성하면 보호 효과 없음.
SSL 종료를 Nginx에서 처리해 앱 서버 CPU 부담 감소. Round Robin 로드밸런싱, gzip 압축, WebSocket 업그레이드를 한 곳에서 담당.
트레이드오프: Nginx 단일 장애점(SPOF). Nginx가 죽으면 전체 서비스 불가.
인스턴스 하나가 죽어도 서비스 유지. 두 인스턴스로 부하 분산.
트레이드오프: 멀티 인스턴스로 인해 WebSocket/SSE 세션 동기화 문제 발생 -> Redis Pub/Sub으로 해결했지만 구현 복잡도 증가.
로컬/운영 환경 동일화. 7개 서비스를 docker-compose.yml 하나로 정의해 환경 재현성 확보.
트레이드오프: 단일 EC2에 7개 서비스를 모두 올려 리소스 경합 발생. 실서비스라면 서비스별 인스턴스 분리가 필요.
로그를 중앙 수집해 Kibana에서 requestId로 전체 흐름 추적 가능.
트레이드오프: Elasticsearch, Logstash, Kibana 3개 서비스가 메모리를 많이 사용해 단일 EC2에서 다른 서비스와 리소스 경합.
requestId, userId 등을 스레드 로컬에 저장해 해당 요청의 모든 로그에 자동 포함.
비동기 스레드에는 TaskDecorator로 컨텍스트 전파.
트레이드오프: MDC는 ThreadLocal 기반이라 비동기 스레드에 자동 전파되지 않음.
TaskDecorator로 해결했지만 비동기 실행 경로마다 명시적 설정 필요.
Spring Boot 3.2+ 공식 지원. I/O 대기 중 플랫폼 스레드를 블록하지 않아 동일 하드웨어로 더 많은 동시 요청 처리 가능. 설정 하나로 전체 앱에 적용.
트레이드오프: CPU 바운드 작업에는 이점 없음. ThreadLocal 사용 시 Virtual Thread 특성상 예상치 못한 동작 가능.
ES나 Kafka 장애 시 요청이 타임아웃까지 대기하며 스레드를 점유. 실패율 50% 초과 시 즉시 실패를 반환해 연쇄 장애 방지.
트레이드오프: 오탐(false positive) 발생 시 정상 서비스도 차단됨. OPEN 상태 30초 동안 해당 기능 사용 불가.
재배포 시 처리 중인 요청을 완료 후 종료. 30초 타임아웃.
트레이드오프: 배포 시 최대 30초 지연. 처리 중인 요청이 30초 이상 걸리면 강제 종료됨.
Mock은 인터페이스 시그니처만 맞으면 통과해 실제 로직이 틀려도 테스트가 통과하는 경우가 존재. Fake는 실제 인메모리 구현체라 서비스 로직이 데이터를 올바르게 저장, 조회, 검증하는지 결과 기반으로 검증.
트레이드오프: 인터페이스 변경 시 Fake도 함께 수정 필요. Mock 대비 유지보수 비용 높음.
@WebMvcTest로 Controller 슬라이스만 로딩. HTTP 매핑, 인증, 요청 유효성 검증에 집중.
UseCase는 Mock 처리해 테스트 범위를 HTTP 레이어로 한정.
트레이드오프: UseCase를 Mock 처리해 실제 비즈니스 로직은 검증되지 않음. HTTP 레이어와 서비스 레이어를 연결한 통합 흐름은 인수 테스트에서 별도 검증 필요.
H2는 MySQL과 SQL 방언이 달라 H2에서 통과한 쿼리가 운영에서 깨지는 경우 발생. 특히 jOOQ는 DB 스키마 기반이라 방언 차이에 민감. MySQL 8.0.36, Redis 7.4 실제 컨테이너로 운영 환경과 동일하게 검증.
트레이드오프: 컨테이너 기동 시간으로 테스트 실행 속도 느림.
AbstractIntegrationTest에서 컨테이너를 한 번만 시작해 전체 테스트에서 공유로 최소화.
단위 테스트로는 실제 부하 상황의 병목을 알 수 없음. 10,000 VUs, 100,000건 요청으로 커넥션 풀 고갈, p(95) 응답시간, 에러율 측정. 최적화 전후 수치 비교로 개선 효과를 정량적으로 검증.
트레이드오프: 스크립트가 레포에 없어 CI 자동화 어려움. 수동 실행이라 코드 변경 후 회귀 자동 감지 불가.