졸업 프로젝트(하루멍록)에서 좋아요 알림 설계 및 개발 경험을 정리한 게시글입니다.
🚀 프로젝트 배경
하루멍록 프로젝트를 진행하면서 좋아요 알림 기능을 구현하게 되었다. 처음에는 가장 단순한 동기 방식으로 시작했지만, 점차 개선이 필요하다는 걸 느꼈다.
📈 Phase 1: 동기 → Outbox 패턴 도입
동기 방식의 문제점
초기에는 좋아요 API 요청이 들어오면 동기적으로 FCM 알림까지 처리하는 방식이었다. 하지만 이 방식은 두 가지 문제가 있었다.
- FCM 전송 실패 시 전체 트랜잭션이 롤백되어 좋아요 자체가 저장되지 않음 (정합성 문제)
- FCM 전송 시간만큼 사용자가 응답을 기다려야 함 (사용성 문제)
Outbox 패턴으로 개선
이를 해결하기 위해 Outbox 패턴을 도입했다. 좋아요 저장과 알림 이벤트를 동일 트랜잭션에서 Outbox 테이블에 기록하고, 별도 폴링 스케줄러가 주기적으로 이벤트를 읽어 FCM을 전송하는 방식이다.
이를 통해 정합성을 보장하면서도 사용자에게는 빠른 응답을 줄 수 있었다.

🔄 Phase 2: Outbox → Fire-and-Forget 비동기 방식 전환
Outbox 패턴의 한계
Outbox 패턴으로 정합성과 사용성 문제는 해결했지만, 실제 운영하면서 새로운 문제가 보였다.
- Outbox 테이블과 폴링 스케줄러 추가로 시스템 복잡도 증가
- 최대 10초(폴링 주기)의 알림 지연 발생
PM과 해당 기능에 대해 다시 이야기를 나누어본 결과, 좋아요 알림은 일부 유실이 발생하더라도 "정합성"보다 "실시간성"이 더 중요하다는 결론에 도달했다.
비동기 Fire-and-Forget 방식으로 전환
Outbox 패턴 대신 비동기 fire-and-forget 방식으로 전환했다. 좋아요 저장 후 즉시 응답을 반환하고, FCM 전송은 별도 스레드에서 비동기로 처리하되 결과를 기다리지 않는 방식이다.
비동기 구현 과정에서의 고민
비동기로 구현하다 보니 단순히 @Async만 붙인다고 끝나는 게 아니라는 걸 깨달았다. 이번에 공부하며 메모리, 장애 측면에서 여러 점들을 고려해야한다는 것을 알았다.
스레드 풀 크기 설정
FCM은 I/O bound 작업이라 스레드를 많이 할당할 수 있지만, 메인 API와 리소스를 공유하는 환경이었기에 무분별하게 스레드를 늘리면 OOM이나 CPU 크레딧 고갈이 발생할 수 있어, 적절한 스레드 풀 크기(min 2, max 6)를 설정했다.
장애 모니터링
비동기 작업은 실패해도 메인 플로우에 영향을 주지 않기 때문에, 오히려 장애를 발견하기 어렵다는 문제가 있었다. 이를 위해 FCM 실패 시 디바이스 토큰을 비활성화하고, Discord Webhook으로 실시간 장애 알림을 받도록 구성했다.
무한 블로킹 방지
Discord Webhook 호출 자체가 블로킹되면 스레드가 계속 점유되는 문제가 있어, 타임아웃을 설정해 무기한 블로킹을 방지했다.
Lazy 로딩 예외 처리
비동기 작업이 별도 스레드에서 실행되면서 영속성 컨텍스트가 분리되어 Lazy 로딩 예외가 발생할 수 있었다. 이를 해결하기 위해 엔티티를 직접 전달하는 대신 필요한 데이터만 담은 DTO를 비동기 작업에 전달했다.
📊 성과
알림 지연 시간을 최대 10초에서 0.1초 이내로 단축했고, Outbox 테이블과 폴링 스케줄러를 제거하여 시스템 복잡도를 크게 감소시켰다. 무엇보다 실시간 알림으로 사용자 경험이 개선되었다는 점이 가장 큰 성과였다.
💡 배운 점
동기 → Outbox → 비동기로 진화하는 과정을 통해 "기술은 비즈니스 요구사항에 맞춰 선택해야 한다"는 교훈을 얻었다. Outbox 패턴 자체는 훌륭한 패턴이지만, 모든 상황에 적합한 것은 아니다. PM과 소통하며 핵심 요구사항(정합성 vs 실시간성)을 명확히 하고, 그에 맞는 아키텍처를 선택하는 것이 중요하다는 걸 체감했다.
또한 비동기 처리는 단순히 성능 개선만이 아니라, 스레드 관리, 장애 모니터링, 예외 처리 등 여러 측면에서 깊이 있는 고민이 필요하다는 것도 배웠다.
'개발기' 카테고리의 다른 글
| AI Native 서버 개발 with Rust - YAPP 27기 Server 후기 (0) | 2026.03.09 |
|---|---|
| 10기 오픈소스 기여모임 후기 — Spring Tools & Spring Framework에 처음으로 기여한 이야기 (1) | 2026.02.12 |
| [LangChain] 복잡한 SQL, AI/LLM으로 해결하기: Text-to-SQL 4가지 방식 성능/비용 비교 (1) | 2025.12.09 |
| 서버비 0원으로 월 8,000 트래픽 감당하기: Cloudflare Workers + Notion 도입기 (0) | 2025.12.03 |