1. 프로젝트 개요
- 흩어져 있는 지수 데이터를 한눈에 보고, 성과를 랭킹화하여 투자 인사이트를 제공하는 대시보드
- 목적: 파편화된 다양한 금융 지수(Index) 데이터를 한곳에 모아 시계열 차트로 시각화하고, 기간별 성과 랭킹을 제공하여 사용자에게 직관적인 투자 인사이트를 제공합니다.
- 핵심 기능: 외부 연동 작업을 통해 일정 시간마다 최신 지수 정보 및 지수 데이터를 자동으로 동기화하여 정확도 높은 금융 시계열 데이터를 제공합니다.
- 일/주/월/년 등 특정 기간 대비 지수 성과(등락률) 계산 및 Top 10 랭킹 산출
- 다중 필드 정렬을 지원하는 지수 데이터 커서 기반(Cursor-based) 페이징 조회
- 대시보드 시각화를 위한 이동평균선(5일, 20일) 적용 차트 데이터 제공
2. 담당한 작업
- 역할: Backend Developer
- 기여 부분: IndexData 및 IndexInfo 도메인을 핵심으로 하는 금융 대시보드 API 설계 및 구현을 전담했습니다.
- 지수 성과 대시보드 구현: 지수별 기준가 대비 기간별 등락률과 대비(Versus) 수치를 계산하여 랭킹화하는 핵심 비즈니스 로직을 구현했습니다.
- 차트 데이터 제공: 단순 종가 나열을 넘어, 슬라이딩 윈도우 알고리즘을 적용한 이동평균선 데이터를 실시간으로 산출하여 대시보드 차트용 데이터를 제공했습니다.
- 관심 지수 성과 대시보드 구현: 즐겨찾기로 등록한 지수 지수별 기준가 대비 기간별 등락률과 대비(Versus) 수치를 계산하여 한눈에 볼 수 있는 대쉬모드를 구현했습니다
- 데이터 Export 및 필터링: In-Memory 기반의 대용량 CSV 파일 다운로드 기능을 개발했습니다.
3. 기술적 성과
- 기술 스택: Java, Spring Boot 3, Spring Data JPA, QueryDSL, H2 Database (Test), PostgreSQL (Prod), MapStruct
- 주요 성과:
- 메모리 기반 CSV 다운로드 아키텍처 구현: 로컬 디스크 I/O를 발생시키는 기존의 파일 스토리지 저장 방식 대신, 자바 메모리에서 StringBuilder로 데이터를 조립하여 byte[] 스트림으로 직접 응답하는 방식을 채택했습니다. 이를 통해 서버 디스크 부하를 0으로 만들고 응답 속도를 획기적으로 개선했으며, BOM(
\\ufeff) 적용으로 엑셀 인코딩 문제까지 사전 차단했습니다.
4. 문제점 및 해결 과정
- 이슈 1: MapStruct 객체 매핑 덮어쓰기(Target Collision) 방어
- 상황 (Situation): 지수 성과 랭킹 대시보드 API에서 사용자가 조회 기간(
periodType)을 변경해도 등락률과 대비 값이 업데이트되지 않고, 항상 전일 데이터로만 고정 출력되는 심각한 논리적 버그를 발견했습니다.
- 과제 (Task): 디버깅 결과 비즈니스 로직의 데이터 연산은 정확했지만, DTO로 변환하는 매핑 단계에서 데이터가 유실되고 있었습니다. 원인을 파악하여 올바른 연산 결과가 프론트엔드로 전달되도록 수정해야 했습니다.
- 행동 (Action): MapStruct 라이브러리의 컴파일된 구현체를 뜯어보며 동작 원리를 분석했습니다. 그 결과, 명시적으로 넘겨준 매개변수보다 '매핑 타겟 필드와 동일한 이름을 가진 엔티티 객체의 필드값'을 우선적으로 덮어씌운다는 라이브러리의 암묵적 동작 방식을 알아냈습니다. 즉시
@Mapping 어노테이션을 활용하여 source와 target을 명확하게 1:1로 매칭하도록 코드를 수정했습니다.
- 결과 (Result): 의도치 않은 데이터 덮어쓰기를 완벽하게 방어하여 기간별 등락률 데이터가 대시보드에 정확하게 표출되었습니다. 이를 통해 편리한 라이브러리라도 내 부의 동작 원리와 규칙을 정확히 파악하고 사용하는 '방어적 코딩'의 중요성을 깨달았습니다.
- 이슈 2: 현실 시간과 비즈니스 시간의 불일치 (주말/공휴일 버그 해결)
- 상황 (Situation): 성과 랭킹 산출 로직의 기준일을 현실 시간인
LocalDate.now()로 설정해 두었는데, 주말이나 공휴일에 API를 호출하면 데이터 정합성이 깨져 등락률이 무조건 0%로 계산되는 도메인 특화 오류가 발생했습니다.
- 과제 (Task): 주식 및 지수 시장이 열리지 않는 휴일에도 대시보드가 정상적으로 최신 기준의 수익률을 계산하여 보여주도록 시간 기준 아키텍처를 재설정해야 했습니다.
- 행동 (Action): 금융 도메인에서는 현실 시간이 아닌 '비즈니스 시간(영업일)'을 기준으로 삼아야 함을 깨달았습니다. 이에 DB에 저장된 해당 지수의
MAX(baseDate)를 조회하여 가장 최신 영업일을 반환하는 헬퍼 메서드를 Repository에 추가하고, 대시보드의 모든 비즈니스 로직 기준일을 해당 영업일로 전면 교체했습니다.
- 결과 (Result): 주말 및 공휴일에도 버그 없이 정확한 지수 성과 데이터가 제공되었습니다. 단순한 기능 구현을 넘어, 금융 도메인의 특성을 코드에 녹여내는 도메인 주도적 시야를 넓힐 수 있었습니다.
- 이슈 3: QueryDSL 서브쿼리 조인 충돌 및 스코프 분리
- 상황 (Situation): QueryDSL을 활용해 각 지수별 최신 날짜 데이터를 뽑아오는 복잡한 동적 쿼리를 작성하던 중, 메인 쿼리와 서브쿼리가 같은 엔티티(
IndexData)를 참조하면서 쿼리가 비정상적으로 생성되고 실행되는 문제가 발생했습니다.
- 과제 (Task): 서브쿼리가 메인 쿼리의 엔티티와 충돌하지 않도록 쿼리의 스코프(Scope)를 명확히 분리하여, 문법적으로 완벽한 SQL이 생성되도록 만들어야 했습니다.
- 행동 (Action):
JPAExpressions를 사용한 서브쿼리 내부에 new QIndexData("sub") 구문을 도입했습니다. 이를 통해 메인 쿼리의 Q클래스와는 완전히 독립적인 별도의 Alias(별칭)를 가진 인스턴스를 명시적으로 생성하여 서브쿼리의 조인 조건에 부여했습니다.
- 결과 (Result): 메인 쿼리와 서브쿼리 간의 엔티티 참조 충돌이 완벽히 해결되어 의도한 대로 쿼리가 실행되었습니다. QueryDSL 내부에서 Q클래스의 생명주기와 별칭이 실제 SQL 생성에 미치는 영향을 깊이 이해하게 되었습니다.
- 이슈 4: 클라이언트 파라미터 바인딩 에러 (프론트엔드 연동 트러블슈팅)
- 상황 (Situation): 특정 지수 조회 API 테스트 중 서버에서 데이터를 반환하지 못하는 현상이 발생했습니다. 초기에는 백엔드의 JPQL로직 오류를 의심했습니다.
- 과제 (Task): 백엔드 단독 테스트(Postman) 시에는 쿼리가 정상 작동함을 확인했습니다. 프론트엔드와 백엔드 사이의 네트워크 통신 과정 중 어느 지점에서 데이터가 변형되었는지 병목을 찾아야 했습니다.
- 행동 (Action): 브라우저의 Network 탭을 열어 요청 흐름을 분석했습니다. 프론트엔드에서 UUID 문자열을 파싱하는 과정에 자바스크립트 오류가 생겨
?indexInfoId=NaN 형태로 서버에 요청이 들어오는 것을 포착했습니다. 이로 인해 Spring의 @RequestParam이 Type Mismatch를 일으켜 컨트롤러 진입 전 400 Bad Request로 요청을 차단하고 있음을 파악한 후, 강사님께 원인을 공유하여 로직 수정을 요청했습니다.
- 결과 (Result): 백엔드 코드만 파고드는 것이 아니라, 클라이언트 단의 네트워크 로그 분석과 교차 검증을 통해 신속하게 이슈를 해결해 내는 실무적인 크로스 플랫폼 트러블슈팅 역량을 길렀습니다.
- 이슈 5: 지수 랭킹 전체 조회 스트림 제어 논리 오류 해결
- 상황 (Situation): 지수 성과 랭킹 조회 API에서 '전체 조회'를 요청했음에도 불구하고, 계속해서 단 1개의 지수 데이터만 응답 리스트에 담겨 반환되는 논리 오류를 겪었습니다.
- 과제 (Task): 전체 조회가 요청되었을 때, 컨트롤러에서 넘겨받은
limit 매개변수만큼 정상적으로 리스트가 페이징되어 반환되도록 서비스 로직을 수정해야 했습니다.
- 행동 (Action): 서비스 로직 내에서
IndexDataPerformanceDto 리스트를 Java Stream의 .limit() 연산자로 제한하는 구간을 디버깅했습니다. 그 결과, 스트림 내부의 삼항 연산자 조건이 indexInfoId != null ? limit : 1로 잘못 작성되어 있어, 전체 조회(ID가 null인 상황) 시 무조건 1개만 조회되도록 강제되고 있음을 발견했습니다. 이를 indexInfoId == null ? limit : 1로 논리에 맞게 수정했습니다.
- 결과 (Result): 조건문 수정 후 전체 지수 랭킹이 설정한
limit 개수만큼 정확하게 출력되었습니다. 사소한 조건문 실수 하나가 API 전체의 동작을 망칠 수 있다는 점을 체감하고, 꼼꼼한 단위 테스트와 엣지 케이스 점검의 중요성을 다시금 상기하게 되었습니다.
5. 협업 및 피드백
- 안전한 환경 변수 공유 문화 정착: 깃허브에 DB 비밀번호 등 민감 정보가 유출되는 것을 막기 위해
.gitignore에 .env를 등록
- 올바른 Git 병합 절차 가이드 제공: 협업 중 잦은 머지 충돌(Merge Conflict)로 인해 개발 속도가 지연되자, 팀원들에게 올바른 PR 병합 절차(dev 브랜치 Pull ➔ 로컬 기능 브랜치와 병합 ➔ 로컬에서 충돌 해결 ➔ 원격에 Push)를 문서화하여 공유함으로써 형상 관리의 안정성을 높였습니다.
- 적극적인 PR 코드 리뷰: 코드 병합 전 팀원들과 PR(Pull Request) 기반의 적극적인 코드 리뷰를 진행하여, 비즈니스 로직의 결함을 사전에 캐치하고 코드 컨벤션을 통일했습니다.