안녕하세요, 고동수입니다.
← 전체 프로젝트

2025.11 – 2026.09 · 1인 개발 · 앱·서버 설계·구현·배포·운영

ImHere

위치를 확인해 도착·출발 알림을 문자와 푸시로 보내는 서비스

ImHere는 도착·출발을 감지해 지정한 사람에게 문자와 푸시 알림을 보내는 서비스입니다. 앱과 서버를 혼자 설계·구현하고 배포·운영했습니다. 알림 발송, 요청 제한, 친구 관계 모델을 다룬 세 가지 사례를 소개합니다.

01 · SMS 중복 발송을 막고 멈춘 알림 8건 복구

발송 상태 전이 · 강제 종료와 복구 경로
01 · 작업 생성PENDINGdedupeKey 유니크 제약
02 · 발송 권한 획득PROCESSING조건부 UPDATE
03 · 결과 저장SENTprovider_message_id 기록
PROCESSING 중 컨테이너 강제 종료수동 재시작 · 10분 지난 작업 복구8건 모두 SENT
장애 주입 실험에서는 실제 Solapi 대신 테스트 어댑터를 사용했습니다. 강제 종료 직후 PROCESSING에 남은 8건이 재시작 후 SENT가 된 것을 DB에서 확인했습니다.

문제

알림 발송을 비동기로 분리하면서 재시도나 재시작 때 같은 SMS가 다시 나갈 수 있었습니다. SMS는 건당 50원입니다. 공급사 호출과 DB 저장을 한 트랜잭션으로 묶을 수 없어 재시도만으로는 중복 과금을 막기 어려웠습니다.

해결

발송 건마다 dedupeKey를 두고 유니크 제약을 걸었습니다. 조건부 UPDATE로 한 실행 흐름에만 발송 권한을 줬습니다. 이벤트 전달 실패는 Spring Modulith의 Event Publication Registry가, 오래된 PROCESSING 발송 작업은 알림 복구 스케줄러가 각각 다시 처리합니다. 공급사 호출과 DB 저장 사이에 장애가 나면 실제 발송 여부를 확정할 수 없어 정확히 한 번 발송한다고 보장하지는 않습니다.

결과

발송 중 컨테이너를 강제 종료하고 수동으로 재시작했을 때 멈춘 8건이 모두 SENT 상태로 복구됐습니다. 검증 범위는 테스트 발송 어댑터를 사용한 단일 컨테이너 종료 시나리오입니다.

장애 주입 실험 기록

02 · 부하 실험으로 요청 제한 기준 정하기

k6 혼합 부하 실험 · 처리율별 응답시간 p95
두 구간 모두 실패율 0% · 부하 증가 구간에서 Hikari 커넥션 대기 증가
애플리케이션과 DB는 각각 EC2 t3.small(2 vCPU, 2 GiB)에서 실행했습니다. 서로 다른 부하 시나리오의 수치를 하나의 곡선으로 연결하지 않았습니다.

문제

발신자별 제한이 없어서 한 계정이 SMS를 대량으로 보낼 수 있었습니다. 정상적인 순간 사용량은 허용하면서 비용을 막을 기준이 필요했습니다. 당시 실제 사용자가 없어 별도 부하 실험으로 서버가 견디는 구간을 확인했습니다.

해결

운영과 분리한 EC2 t3.small 환경에서 100 RPS와 165 RPS를 실패 없이 처리했습니다. p95는 각각 24.13ms, 45.76ms였고, 부하가 늘 때 CPU보다 HikariCP 커넥션 대기가 먼저 증가했습니다. 공개된 혼합 부하 기록에서 확인한 최고 검증 구간은 165 RPS이며, 이를 서버의 한계치로 부르지는 않습니다. 운영 Nginx에는 IP당 10r/s·전체 100r/s, Bucket4j에는 발신자당 하루 수신자 4명을 설정했습니다. Nginx의 IP 제한은 한 사람의 순간 요청 최대 8건과 공유 IP 사용자 5명을 가정해 burst 30으로 두었습니다.

결과

단일 계정으로 400 RPS까지 올린 SMS 요청 실험에서 96%가 429로 차단됐고, 네트워크 오류는 881건에서 0건으로 줄었습니다. 검증 범위는 한 계정·한 수신자에게 요청을 집중한 별도 실험 환경입니다.

부하 실험과 운영 설정 · 요청 제한 재실험 기록

03 · 친구 관계를 한 타입으로 합쳐 조회 5회에서 2회로

친구 관계 모델 · 생성 규칙과 중복 제약
이전 · 3개 타입과 3개 테이블여러 조회 결과로 관계 상태 판단중복 관계 생성 가능 · 요청당 조회 5회
변경 후 · 단일 관계 타입사용자 쌍 정렬 + 생성 규칙자기 자신과의 관계 거부 · DB 유니크 제약
변경 결과122개 파일 재설계 · 관계 요청 조회 5회 → 2회
객체에서 관계 생성 규칙을 확인하고 DB 유니크 제약으로 중복 저장을 막았습니다.

문제

친구 관계가 3개 타입과 3개 테이블에 나뉘어 있었습니다. 두 사람의 관계를 확인할 때마다 여러 조회 결과를 조합해야 했고, 같은 관계가 중복 생성될 수도 있었습니다.

해결

두 사용자의 순서를 일정하게 정해 한 타입으로 관계를 표현했습니다. 객체 생성 시 자기 자신과의 관계를 거부하고, DB 유니크 제약으로 동일한 사용자 쌍이 다시 저장되지 않게 했습니다.

결과

관련 122개 파일을 재설계했습니다. 관계 요청 한 건당 조회는 5회에서 2회로 줄었고, 자기 자신과의 관계와 중복 관계도 각각 객체와 DB에서 막습니다.

함께 개선한 운영 구조

Redis·RabbitMQ 전용 서버를 Caffeine 로컬 캐시와 Spring 이벤트 처리로 대체해 운영 서버를 2대에서 1대로 줄였습니다. Prometheus·Grafana·Loki·Alloy로 로그와 지표를 수집하고 GitHub Actions로 배포했습니다.

코드와 서비스

사용 기술

Kotlin 2.3 · Spring Boot 4.1 · Spring Modulith 2.1 · Spring Security · JPA · QueryDSL 7.4 · MySQL 8.0 · Caffeine 3.1 · Bucket4j 8.19 · Firebase FCM · Solapi · Nginx · Docker · AWS EC2/ECR/CloudFormation · k6 · Prometheus · Grafana · Loki · Alloy · Flutter · Dart · Riverpod · SQLite