1. 프로젝트 개요
Monew는 사용자가 관심사 키워드를 등록하면 Naver API와 RSS를 통해 관련 뉴스를 자동 수집·요약해서 제공하는 뉴스 큐레이션 플랫폼입니다. 댓글, 좋아요 등 소셜 기능도 함께 제공해 사용자들이 뉴스에 대한 의견을 나눌 수 있도록 설계했습니다.
Spring Boot 기반 REST API 서버로, PostgreSQL과 MongoDB를 함께 사용하고 AWS 인프라 위에서 운영합니다.
2. 담당한 작업
댓글 도메인 개발 (Comment / CommentLike)
프로젝트에서 댓글 도메인 전체를 담당했습니다.
- 댓글 등록 / 수정 / 논리 삭제 / 물리 삭제 API 구현
- 댓글 좋아요 / 좋아요 취소 API 구현
- QueryDSL 기반 커서 페이지네이션 구현 (createdAt / likeCount 정렬)
- 이벤트 기반 알림 발행 구조 설계 (ApplicationEventPublisher, @TransactionalEventListener)
커서 페이지네이션은 offset 방식의 성능 한계를 피하기 위해 선택했습니다. likeCount + createdAt 복합 커서로 정렬 일관성을 유지하도록 설계했고, 이 과정에서 QueryDSL의 동적 조건 분기를 타입 안전하게 다루는 방법을 깊게 익혔습니다.
QA 및 버그 수정
- 전체 도메인 코드 리뷰 및 버그 파악
- 댓글 좋아요 알림 수신자 오류 수정
- 불필요한 COUNT 쿼리 제거 및 DB 인덱스 추가
통합 테스트 환경 구축
- Testcontainers 기반 통합 테스트 공통 설정(BaseIntegrationsTest) 구축
- 댓글 / 댓글 좋아요 통합 테스트 작성
API 문서화 & 발표 자료
- Swagger 문서화
- 발표 자료 제작을 담당했습니다!
3. 기술적 성과
기술 스택
| 백엔드 | Spring Boot 3.5, Spring Data JPA, QueryDSL, Spring Batch |
| 데이터베이스 | PostgreSQL 16, MongoDB Atlas |
| 인프라 | AWS ECS, RDS, S3, ECR, GitHub Actions |
주요 구현
QueryDSL 복합 커서 페이지네이션
likeCount + createdAt 조합 커서를 사용해 정렬 일관성을 유지했습니다. 단순 offset 페이지네이션은 데이터가 많아질수록 응답이 느려지는 문제가 있는데, 커서 방식은 이전 페이지 마지막 항목을 기준으로 다음 데이터를 조회하기 때문에 데이터 양과 무관하게 일정한 성능을 유지할 수 있습니다.
이벤트 기반 아키텍처
댓글 생성/수정/삭제 시 ApplicationEventPublisher로 이벤트를 발행하고, @TransactionalEventListener(AFTER_COMMIT)으로 MongoDB 사용자 활동 내역을 반영하는 구조를 설계했습니다. 도메인 로직과 부가 작업(알림, 활동 내역)을 명확하게 분리해 코드 응집도를 높였습니다.
복합 인덱스 추가
댓글 목록 조회 쿼리에서 Full Table Scan이 발생하는 것을 확인하고, (article_id, is_deleted, created_at) / (article_id, is_deleted, like_count) 복합 인덱스를 추가해 조회 성능을 개선했습니다.
4. 문제점 및 해결 과정
[문제 1] 통합 테스트 환경에서 Spring 컨텍스트 캐싱 충돌
상황
@Testcontainers + @Container를 추상 클래스의 static 필드에 적용했을 때, 첫 번째 테스트 클래스 실행 후 컨테이너가 종료됐지만 Spring 컨텍스트는 기존 DB URL을 캐싱하고 있어 이후 테스트에서 DB 연결 실패가 발생했습니다.
원인
Spring 컨텍스트는 테스트 간 재사용을 위해 캐싱되는데, Testcontainers가 각 테스트 클래스마다 컨테이너를 새로 띄우고 내리는 생명주기와 충돌했습니다.
해결
static 블록에서 컨테이너를 수동으로 기동하는 방식으로 변경해 컨테이너 생명주기를 JVM 생명주기에 맞춰 일치시켰습니다.
static method() {
postgres.start();
mongo.start();
}
컨테이너가 JVM 종료 시점까지 유지되므로, 여러 테스트 클래스가 같은 컨테이너 인스턴스를 공유하게 되어 Spring 컨텍스트 캐싱과 충돌 없이 동작합니다.
[문제 2] 댓글 좋아요 알림이 잘못된 사용자에게 발송
상황
CommentLikeNotificationEvent에 댓글 작성자가 아닌 좋아요를 누른 사람(liker)의 ID가 전달되어, 알림 수신자가 잘못 지정되는 버그가 있었습니다.
원인
이벤트 생성 시 수신자를 지정하는 코드에서 comment.getUser()가 아닌 현재 요청 사용자를 참조하고 있었습니다. 이때 알림 도메인 담당하신 분과 소통을 잘 했어야 됐을 것 같습니다. 알림은 commentLike의 user를 댓글 주인으로 파악했었고, 저는 댓글 좋아요를 누른 사람으로 파악했습니다.
해결
bulk update 이후 재조회한 refreshed.getUser()(댓글 작성자)를 이벤트에 전달하도록 수정했습니다. 이 과정에서 @Modifying 쿼리 실행 후 JPA 1차 캐시와 DB 값의 동기화 문제도 함께 정리했습니다.
[문제 3] 댓글 목록 조회 시 매 페이지 불필요한 COUNT 쿼리 실행
상황
커서 페이지네이션을 사용하고 있음에도 매 요청마다 전체 COUNT 쿼리가 실행되어 불필요한 DB 부하가 발생하고 있었습니다.
해결
첫 페이지 요청(cursor, after 모두 null)일 때만 COUNT 쿼리를 실행하고, 이후 페이지 요청에서는 -1L을 반환하도록 수정했습니다. 커서 페이지네이션에서는 전체 개수가 의미 없는 경우가 대부분이라 이 방식이 자연스럽게 맞아떨어졌습니다.
[문제 4] @TransactionalEventListener로 인한 HTTP 응답 지연
상황
AFTER_COMMIT 단계에서 MongoDB 쓰기 작업이 동기적으로 실행되어 HTTP 응답이 지연되는 문제가 있었습니다.
원인
AFTER_COMMIT은 트랜잭션 커밋 이후에 실행되지만 같은 스레드에서 동기적으로 동작합니다. 따라서 MongoDB 쓰기 작업이 끝날 때까지 응답이 반환되지 않았습니다.
해결
Outbox 패턴을 도입했습니다. 도메인 데이터 변경과 Outbox 이벤트 저장을 같은 RDB 트랜잭션으로 묶고, 별도 스케줄러가 PENDING 상태의 이벤트를 읽어 MongoDB에 비동기로 반영하도록 구조를 개선했습니다. PostgreSQL과 MongoDB 간 정합성 문제도 함께 해결되었습니다.
5. 코드 품질 및 최적화
- 코드 스타일 통일: Google Java Style Guide 기반 포매터를 팀 전체에 적용해 리뷰 시 스타일 논쟁 없이 로직에 집중할 수 있도록 했습니다.
- 계층 분리 원칙 준수: Controller → Service → Repository(QueryDSL) → DB 흐름을 일관되게 유지했습니다.
- 커버리지 관리: JaCoCo 설정에서 DTO, 설정 클래스, 매퍼 등 실질적인 테스트 대상이 아닌 클래스를 제외해 의미 있는 커버리지 수치를 관리했습니다.
6. 향후 개선 사항
Redis 캐시 도입
자주 조회되는 관심사 목록, 뉴스 기사 목록 등에 Redis 캐시를 적용하면 DB 부하를 줄이고 응답 속도를 개선할 수 있습니다. 현재 구조에서 캐시 레이어를 추가하는 것은 크게 어렵지 않을 것으로 보입니다.
Spring Batch 구조 개선
현재 뉴스 수집/백업 배치가 단순 Tasklet 방식으로 구성되어 있습니다. 데이터 규모가 커질 경우 Chunk 기반으로 전환하면 안정성과 재처리 효율을 높일 수 있습니다.
'Project > Monew' 카테고리의 다른 글
| [Monew] 복기 - 일정·개발 측면 회고 (0) | 2026.05.10 |
|---|---|
| [Monew] 개인 회고록 - 기술보다 어려웠던 것들 (0) | 2026.04.27 |