본문 바로가기
Project/Project

[모두의 플리] 개인 개발 리포트

by boolynn 2026. 7. 29.

1. 프로젝트 개요

모두의 플리(ModuPly)는 대규모 트래픽을 고려한 글로벌 콘텐츠 큐레이션 소셜 플랫폼이다. 영화, 드라마, 스포츠 등 다양한 콘텐츠를 기반으로 플레이리스트를 생성하고 공유하며, 실시간 같이 보기 기능을 통해 다른 사용자와 함께 콘텐츠를 즐길 수 있다. 핵심 기능으로는 플레이리스트 큐레이션, 리뷰 및 평점, 실시간 알림(SSE), 팔로우 기반 소셜 기능, DM이 있다.


2. 담당한 작업

플레이리스트, 리뷰, 알림, SSE 도메인 전체와 ELK 스택(Logstash, Kibana) 구성을 담당했다.

플레이리스트

  • CRUD, 콘텐츠 추가/삭제, 구독/구독취소 구현
  • 구독자 수(subscribeCount) 기준 정렬 추가
  • Caffeine 캐싱 적용(PLAYLIST_DETAIL) 및 수정/삭제/구독 시 캐시 무효화
  • N+1 쿼리 개선: 소유자/구독자/콘텐츠 정보를 일괄 조회로 최적화

리뷰

  • 생성/수정/삭제/목록 조회 구현
  • rating:createdAt 복합 커서 기반 페이지네이션 구현
  • 작성자 프로필 이미지 URL 생성(BinaryContentService.generateUrl) 연동

알림

  • 플레이리스트 구독·콘텐츠 추가·팔로우·DM 수신·권한 변경 알림 발송
  • 커서 기반 알림 목록 조회(읽지 않은 알림만 반환), 읽음 처리 구현
  • NotificationType enum에 title/messageTemplate 필드 추가로 하드코딩 제거

SSE

  • 실시간 알림 전송을 위한 SSE 연결 관리(SseEmitterManager)
  • Redis Pub/Sub 기반 다중 서버 브로드캐스트 구현(SseRedisPublisher, SseRedisSubscriber)
  • createdAt:id 복합 커서 기반 Last-Event-ID 유실 알림 재전송 구현

ELK

  • Logstash 파이프라인 구성(grok 파싱, MDC 필드 추출)
  • Filebeat 로그 수집 설정
  • Kibana 대시보드 구성(레벨별 로그 건수, 시간대별 추이, HTTP 메서드별 분포)

3. 기술적 성과

기술 스택

  • Java 17, Spring Boot, Spring Security, Spring Batch, Spring Data JPA, QueryDSL, PostgreSQL, Redis
  • WebSocket(STOMP), SSE, Redis Pub/Sub
  • AWS(ECS, ECR), Docker Compose, GitHub Actions, Codecov, CodeRabbit
  • TMDB, The Sports DB
  • ELK Stack(Elasticsearch, Logstash, Kibana), Filebeat
  • k6

 

주요 구현

  • 복합 커서 페이지네이션: 리뷰 목록 조회에서 rating:createdAt 복합 커서를 설계하여 rating 정렬 시에도 정확한 페이지네이션 구현
  • Redis Pub/Sub SSE: 다중 서버 환경에서 SSE 이벤트가 모든 인스턴스에 전달되도록 Redis Pub/Sub 기반 브로드캐스트 구현
  • Last-Event-ID 재전송: SSE 재연결 시 createdAt:id 복합 커서 기반으로 유실된 알림을 DB에서 조회하여 재전송
  • N+1 쿼리 개선: 플레이리스트 목록 조회에서 findAllById, countByPlaylistIds 등 일괄 조회로 최적화
  • ELK 로그 모니터링: Logstash grok 파싱으로 MDC 필드를 구조화된 필드로 추출하고 Kibana 대시보드로 시각화

4. 문제점 및 해결 과정

4-1. Redis Pub/Sub 메시지 역직렬화 타입 불일치로 인한 SSE Last-Event-ID 설정 실패

✭ Situation

다중 서버 환경에서 SSE 알림 전달을 위해 Redis Pub/Sub을 도입했다. Redis에서 메시지를 수신한 후 SSE로 전달할 때 Last-Event-ID 기반 유실 알림 재전송이 동작하지 않는 문제가 발생했다.

 

Task

SSE 이벤트에 id가 설정되지 않아 클라이언트 재연결 시 Last-Event-ID 헤더가 전달되지 않고, 결과적으로 유실 알림 재전송이 불가능한 상황을 해결해야 했다.

Action

원인을 분석한 결과, ObjectMapper.readValue(..., Object.class)가 JSON 객체를 LinkedHashMap으로 역직렬화하여 instanceof NotificationDto 체크가 항상 false를 반환하는 것을 발견했다. Object.class 대신 NotificationDto.class로 명시적 타입을 지정하여 해결했다.

// 수정 전
Object data = objectMapper.readValue(message.getBody(), Object.class);

// 수정 후
NotificationDto data = objectMapper.readValue(message.getBody(), NotificationDto.class);

 

Result
SSE 이벤트에 createdAt:id 형식의 id가 정상적으로 설정되어 클라이언트 재연결 시 유실 알림 재전송이 동작하게 되었다.


4-2. SSE 통합 테스트로 인한 다른 테스트 컨텍스트 오염

Situation
SSE 연결/전송 검증을 위한 통합 테스트를 작성했는데, 해당 테스트 이후 WatchingSessionApiIntegrationTest, UserRepositoryTest 등 다른 테스트가 DataIntegrityViolationException으로 실패하는 문제가 발생했다.

 

Task
SSE 통합 테스트가 다른 테스트에 영향을 주지 않도록 해결해야 했다.

 

Action
원인 분석 결과, SSE 테스트에서 mockMvc.perform(get("/api/sse"))를 호출하면 비동기 연결이 열린 채로 대기 상태가 되고, 테스트 종료 후에도 SseEmitter가 남아있어 DB 연결 풀을 선점하는 것을 발견했다. removeAllEmitters() 메서드 추가, @DirtiesContext 적용 등을 시도했으나 해결되지 않아 SSE 통합 테스트를 제거하고 단위 테스트로 핵심 로직을 커버하는 방향으로 결정했다.

 

Result
SseEmitterManagerTest에서 연결, 재연결, Heartbeat, 타임아웃 등 핵심 로직을 단위 테스트로 커버하여 CI 테스트가 안정적으로 통과하게 되었다.


4-3. SseEmitterManager 순환 의존성 문제

Situation
Last-Event-ID 유실 알림 재전송 구현을 위해 SseEmitterManager에 NotificationService를 주입하려 했으나 순환 의존성 문제가 발생했다.

 

Task
순환 의존성을 해결하면서 유실 알림 재전송 로직을 구현해야 했다.

 

Action
@Lazy 어노테이션으로 임시 해결할 수 있었으나, 근본적인 해결을 위해 SseMissedNotificationSender라는 별도 클래스로 재전송 로직을 분리했다.

 

Result
단일 책임 원칙을 지키면서 순환 의존성 문제를 해결했고, 각 클래스의 역할이 명확해졌다.


5. 협업 및 피드백

5인 팀 프로젝트를 진행하며 각자 도메인을 나누어 개발했다. 알림 이벤트 발행 시점과 방식을 각 도메인 담당자와 협의하여 충돌 없이 구현했고, 특히 팔로우, DM 도메인 담당자와 이벤트 인터페이스를 맞추는 과정에서 사전 소통의 중요성을 느꼈다.

 

코드래빗을 통해 자동 코드 리뷰를 적극 반영했다. 동시성 버그, N+1 쿼리, 예외 처리 누락 등 미처 발견하지 못한 문제들을 발견하는 데 도움이 되었으며, 자동화된 코드 리뷰 툴이 실제 개발 품질 향상에 기여한다는 것을 경험했다.

 

팀원의 코드 리뷰를 통해 findAllByPlaylist를 프로젝션 쿼리(findSubscriberIdsByPlaylistId)로 개선하는 등 성능 최적화 아이디어를 얻었다. 단순히 오류를 찾는 것을 넘어 왜 이렇게 설계했는지를 함께 고민하는 시간이었다.


6. 코드 품질 및 최적화

  • 테스트: 서비스/컨트롤러 단위 테스트, QueryDSL 레포지토리 통합 테스트를 작성했다. SseMissedNotificationSender, SseRedisSubscriber 등 새로 추가된 클래스에 대한 테스트도 함께 작성하여 회귀를 방지했다.
  • 캐싱 전략: 플레이리스트 단건 조회에 PLAYLIST_DETAIL 캐시를 적용했다. 캐시 키를 playlistId:currentUserId로 설계하여 사용자별 구독 상태가 다르게 캐싱되도록 했고, 수정/삭제/구독 시 allEntries = true로 무효화했다.
  • 방어 코드: Logstash grok 파싱 실패 시 message 필드가 삭제되는 문제를 _grokparsefailure 태그 체크로 방어하여 원본 로그 유실을 방지했다.
  • 단일 책임 원칙: SseEmitterManager에서 알림 재전송 로직을 SseMissedNotificationSender로 분리하고, 알림 이벤트 발행을 SseRedisPublisher로 분리하여 각 클래스의 역할을 명확히 했다.

7. 향후 개선 사항 및 제안

  • SSE 구독자 수 커서 페이지네이션: 현재 subscribeCount 정렬은 첫 페이지만 지원하며, 복합 커서 구현이 필요하다.
  • Redis Pub/Sub → Kafka 전환: 현재 Redis Pub/Sub은 메시지 유실 가능성이 있어 Kafka 도입 시 SSE 알림 전달의 신뢰성을 높일 수 있다.
  • ELK Kibana 알림 설정: ERROR 로그 급증 시 Slack 알림 연동으로 운영 모니터링을 강화할 수 있다.
  • 캐시 무효화 전략 개선: 현재 allEntries = true로 전체 캐시를 비우고 있어 불필요한 캐시 미스가 발생할 수 있다. 향후에는 변경된 데이터와 관련된 캐시만 선택적으로 무효화하는 전략을 적용하면 더욱 효율적인 캐싱이 가능할 것이다.
  • 프로필 이미지 캐싱: presigned URL 만료(10분)마다 재생성하는 비용을 줄이기 위해 URL 캐싱 전략 도입을 검토할 수 있다.

8. 링크

'Project > Project' 카테고리의 다른 글

[모뉴] 개인 개발 리포트  (2) 2026.05.08
[Findex] 개인 개발 리포트  (2) 2026.03.20