테스트 구조 최적화를 통한 CI 피드백 루프 개선
컨텍스트 재기동 병목을 정량 측정하고, 테스트 의미를 보존하면서 cleanup 전략으로 전환
Backend Developer
"읽기 쉬운 코드와 검증 가능한 구조를 통해
팀이 빠르게 이해하고 안전하게 수정할 수 있는
시스템을 만드는 2년차 백엔드 개발자입니다."
테스트 실행 시간 단축
검색 성능 개선
헥사고날 아키텍처
포트/어댑터 설계
개발 경력
ABOUT
오버엔지니어링을 지양하고 의도와 책임이 분명한 구조를 지향하며, 팀이 빠르게 이해하고 수정할 수 있는 설계를 중요하게 생각합니다.
동작만 하는 코드보다 흐름과 의도가 자연스럽게 읽히는 코드를 중요하게 생각하며, 가독성과 협업 효율을 함께 높이려 합니다.
AI Agent를 학습, 탐색, 구현, 정리 과정에 적극 활용해 반복 업무를 줄이고, 더 빠르게 검토·실행할 수 있는 개발 방식을 지향합니다.
SKILLS
CAREER
PROJECTS
컨텍스트 재기동 병목을 정량 측정하고, 테스트 의미를 보존하면서 cleanup 전략으로 전환
2,260만 건 LIKE+OFFSET 구조 한계를 역색인 기반 하이브리드 아키텍처로 전환
WAS 부하 분리를 위해 Java–Node.js–Redis–PostgreSQL 분리형 집계 구조를 설계하고 장애 복구까지 고려한 파이프라인 구축
조회 빈도 높고 변경 빈도 낮은 데이터 특성을 분석하여 로컬 캐시 전략 도입
폴링 한계를 해소하기 위해 WebSocket + STOMP로 배틀방 단위 양방향 통신 구현
클릭하여 상세 보기EDUCATION & CERTIFICATIONS
삼성 청년 SW 아카데미 (SSAFY) 11기
웹 기술 트랙 수료
경상국립대학교
도시공학과 전공 / 스마트도시건설 복수전공
정보처리기사
삼성 역량 평가 PRO (B형)
공통 프로젝트 우수상
CONTACT
전체 테스트 실행 시간 429초 중 실제 테스트 로직 비용은 미미했고, 병목은 @DirtiesContext로 인한 Spring 컨텍스트 재기동이었습니다. baseline 측정 스크립트를 만들어 모듈별 비용을 정량화한 후, 가장 비싼 모듈부터 순차 최적화하는 전략을 선택했습니다.
테스트 삭제나 assertion 약화가 아닌, "같은 의미를 더 싸게 유지"하는 것을 원칙으로 세웠습니다. mock 기반 치환으로 bootstrap 의미가 사라지는 것도 금지했습니다.
문제: @SpringBootTest + Testcontainers + @DirtiesContext(BEFORE_CLASS) 조합으로 테스트마다 컨텍스트가 재기동되어 207초 소요.
원인: JUnit 인스턴스 수가 아니라 컨텍스트 재기동 횟수가 진짜 비용. 매 테스트 클래스마다 Spring Boot + Testcontainers가 새로 뜨고 있었음.
해결: @DirtiesContext를 무작정 제거하지 않고, 먼저 cleanup 경로를 구축. Redis 상태는 clearRedisState() helper로 session/profile/lock 정리, DB write 테스트는 snapshot restore로 복구. maxParallelForks=1로 컨텍스트 캐시 보호.
문제: 외부 연동 테스트가 기본 test 태스크에 섞여 있어, 불필요한 외부 호출 + 컨텍스트 재기동 발생.
해결: 테스트 타입을 unit/component/bootstrap/external integration 4단계로 분류. 외부 실연동 테스트는 삭제하지 않되 전용 태스크와 태그로 분리하여 fast suite에서 제외.
약 2,260만 건의 차량 등록 데이터에서 LIKE '%검색어%' 쿼리가 B-Tree 인덱스를 무력화시키고, OFFSET 999990이 사실상 전체 데이터를 읽고 버리는 구조였습니다.
PostgreSQL의 GIN 인덱스 + Full Text Search도 검토했으나, OFFSET 기반 페이지네이션의 구조적 한계(뒤 페이지로 갈수록 선형 성능 저하)는 해결 불가하다고 판단. 역색인(Inverted Index) 기반으로 데이터 접근 방식 자체를 전환하기 위해 Elasticsearch를 선택했습니다.
데이터 정합성이 중요한 원본은 PostgreSQL에 유지하고, 검색은 Elasticsearch가 전담하는 하이브리드 아키텍처를 설계했습니다.
문제: 대용량 데이터를 한번에 Elasticsearch로 이관 시 메모리 초과 및 타임아웃 발생.
해결: 배치 기반 마이그레이션 프로세스를 구축하여 5만 건씩 청크 단위로 안정적 이관. 비효율적인 LIKE 쿼리는 Bool Query + Wildcard Query 네이티브 쿼리로 대체.
문제: Elasticsearch의 max_result_window 기본값(10,000) 제한으로 전체 페이지 접근 불가.
해결: max_result_window 제한 해제 + 인덱스 설정 최적화로 Deep Pagination 지원.
기존 통계 조회는 원본 테이블을 매번 조인하는 방식으로, 데이터가 누적될수록 조회 쿼리가 20초 이상 소요되었습니다. 매 액션마다 DB에 직접 INSERT하면 WAS 부하가 급증하는 문제도 있었습니다.
Kafka/RabbitMQ 도입도 검토했으나, 현재 규모에서는 오버엔지니어링이라 판단. 대신 기존에 사용 중인 Redis를 버퍼로 활용하여 쓰기 성능을 확보하고, 별도 Node.js 서버로 기존 WAS에 영향 없이 부하를 분리하는 구조를 선택했습니다.
Redis의 HINCRBY/ZADD NX 원자적 연산으로 동시성 문제를 자연스럽게 해결하고, 키 패턴을 PostgreSQL PK와 일치시켜 이관 시 파싱 비용을 최소화했습니다.
문제: 집계 결과의 실시간 조회와 컬럼별 정렬을 동시에 구현해야 했으나, Redis만으로는 SQL ORDER BY 같은 정렬이 불가능.
해결: 배치(매일 새벽) + 실시간 마이그레이션(탭 클릭 시) 이중 구조를 설계. 관리자가 통계 탭 클릭 시 Redis → PostgreSQL 즉시 이관 후 SQL로 조회하는 방식으로 두 요구사항을 모두 충족.
문제: 여러 관리자가 동시에 탭 클릭 시 마이그레이션이 중복 실행되어 데이터 중복 집계 발생.
해결: Redis SET NX EX 기반 분산 락 적용. Redlock 같은 복잡한 알고리즘 대신, 관리자 통계 특성(동시 요청 빈도 낮음, 실패 시 스킵해도 치명적이지 않음)을 고려해 단순한 구조를 선택.
문제: Java → Node.js HTTP 전송 실패 시 이벤트 데이터 유실 가능.
해결: 실패 시 로컬 파일 백업(/mount/stats-backup/) → 자정 배치에서 자동 복구하는 경로를 설계. 메시지 큐 없이도 파일 시스템 기반으로 신뢰성 확보.
기존 원본 테이블 조인 방식에서 사전 집계 테이블 조회로 전환하여 통계 페이지 응답 시간 대폭 단축. WAS와 집계 로직을 완전 분리하여 기존 서비스 안정성 유지. 장애 시에도 파일 백업 → 배치 복구 경로로 데이터 정합성 보장.
퀴즈 목록/상세 조회 API가 반복적으로 DB에 접근하여 성능 저하가 발생했습니다. 특히 인기 퀴즈와 데일리 퀴즈는 조회 빈도가 높지만 변경 빈도가 낮은 특성을 가지고 있었습니다.
분산 캐시(Redis)는 이미 세션 관리에 사용 중이었고, 조회 전용 데이터에 네트워크 hop을 추가할 필요가 없다고 판단. 단일 인스턴스 환경에서 로컬 캐시가 적합하다고 결론 내리고 Caffeine을 선택했습니다.
데이터 변경 시 @CacheEvict로 캐시를 무효화하고, k6 성능 테스트로 적용 전후를 정량적으로 검증했습니다.
문제: 퀴즈 데이터 수정/삭제 시 캐시에 이전 데이터가 남아 불일치 발생 가능.
해결: 변경 빈도가 낮은 데이터에 @Cacheable 적용, 데이터 변경 시 @CacheEvict로 관련 캐시를 즉시 무효화하여 일관성 보장.
사용자 간 실시간 상호작용(배틀 참가/퇴장, 준비 상태, 정답 제출, 점수 변화)을 동기화해야 했습니다. HTTP 폴링 방식은 불필요한 트래픽 유발 + 실시간성 한계가 명확했습니다.
SSE(Server-Sent Events)도 검토했으나, 클라이언트→서버 방향 통신이 필요한 양방향 특성상 WebSocket이 적합하다고 판단. STOMP 프로토콜을 도입하여 배틀방 단위의 구독/발행 패턴으로 메시지 격리를 구현했습니다.
문제: 여러 사용자가 동시에 정답을 제출할 때 점수/순위 계산에서 race condition 발생.
해결: 배틀방 상태 변경 로직에 @Synchronized 처리를 적용하고, 배틀 상태를 BattleRoomStatus Enum으로 관리. 추후 Redis 분산 락 활용으로 확장 가능하도록 설계.
문제: 전체 브로드캐스팅 시 다른 배틀방 참가자에게도 메시지가 전달되는 문제.
해결: /sub/battle/room/{roomId} 경로로 배틀방별 구독 채널을 분리. Simple Message Broker를 활용하여 특정 roomId를 구독 중인 클라이언트에게만 상태 변경 메시지 전송.
실시간 퀴즈 배틀 기능 구현 완료. 폴링 대비 서버/네트워크 부하 감소 및 실시간성 보장. WebSocket + STOMP 기반 메시징 아키텍처 구축 경험 확보.
여러 모듈에 걸친 비즈니스 로직(사용자 가입 → 프로필 생성 → 환영 알림)에서 단일 ACID 트랜잭션으로는 데이터 정합성을 보장할 수 없었습니다.
2PC(Two-Phase Commit)는 분산 환경에서 단일 장애점 문제와 가용성 저하가 우려되어 제외. 대신 Saga 패턴(Orchestration 기반)을 선택하여 각 로컬 트랜잭션을 단계별로 실행하고 실패 시 보상 트랜잭션으로 최종 일관성을 보장하는 구조를 설계했습니다.
로컬 트랜잭션 커밋과 이벤트 발행의 원자성 문제를 해결하기 위해 Outbox 패턴 + Debezium 조합을 도입. outbox_events 테이블에 이벤트를 DB 트랜잭션으로 함께 저장하고, Debezium이 폴링하여 Kafka로 발행하는 At-Least-Once 보장 구조를 구현했습니다.
특정 모듈 장애가 전체 시스템의 데이터 불일치로 확산되는 것을 방지하여 회복탄력성(Resilience) 향상. 모듈 간 결합도를 낮추어 유지보수성 및 확장성 확보.