← Back to Projects

01 · Backend Portfolio

ForPets

보호자와 시터를 양방향으로 매칭하는 반려동물 돌봄 플랫폼. 분산 락 기반 동시성 제어와 멀티 인스턴스 안정성이 핵심 과제.

ForPets(포펫츠) 로고
기간6주 · 2026.04 — 2026.06
백엔드 6명
역할
  • 프로필 · 반려동물 · 시터 · 예약 · 관리자 도메인 설계
  • 담당 기능 프론트엔드 구현
  • 예약 · 결제 동시성 제어 (Redisson 분산 락 + 낙관락 + AOP)
  • 만료 / 자동승인 Scheduler (ShedLock)
  • 테스트 작성 및 검증 — 단위 테스트 182건 · 통합(E2E) 테스트 105건
Links GitHub · 배포 사이트 ↗
01 · 서비스 소개

단방향이 아닌,
양방향 매칭 플로우.

일반 매칭 서비스와 달리 보호자 → 시터(정방향)시터 → 보호자(역방향) 두 흐름을 모두 지원. 두 플로우가 같은 시간 슬롯(TimeSlot)을 두고 경쟁하기 때문에 동시성 제어가 핵심 과제였습니다.

정방향 매칭CareRequest — 보호자가 특정 시터에게 직접 돌봄 요청
역방향 매칭Proposal — 시터가 보호자의 게시글(Post)에 먼저 제안
공통 자원TimeSlot — 두 플로우가 경쟁하는 시간 슬롯
확정 결과Reservation — 매칭 확정 후 생성되는 예약

기술 스택

LanguageJava 21
FrameworkSpring Boot 3.5
ORM / QuerySpring Data JPA · QueryDSL 5.1
DatabaseMySQL 8
Cache / LockRedis · Redisson 3.27 · ShedLock 5.16
PaymentPortOne (KG이니시스)
MonitoringPrometheus · Grafana · Micrometer
TestJUnit 5 · Mockito · k6
InfraDocker · AWS EC2 × 2 · ALB
02 · 시스템 아키텍처

EC2 × 2 · ALB 기반
Blue/Green 배포 구조.

ForPetsCare AWS Architecture — Cloudflare DNS · GitHub Actions CI/CD · EC2 Docker Host (Blue/Green Spring Boot) · RDS MySQL · ElastiCache Redis · Qdrant
ForPetsCare AWS Architecture — Blue/Green 배포, VPC 내 RDS·ElastiCache 분리

EC2 두 대를 ALB로 묶은 멀티 인스턴스 구성. 단일 인스턴스 락(synchronized, ReentrantLock)으로는 동시성 제어가 불가능하고 스케줄러도 양쪽 인스턴스에서 동시에 실행될 위험이 있어, Redisson 분산 락 + ShedLock 도입이 백엔드 설계의 출발점이었습니다.

03 · 담당 도메인

양방향 매칭 도메인 설계.

Post보호자가 올리는 돌봄 요청 게시글
CareRequest보호자가 특정 시터에게 직접 돌봄을 요청 (정방향)
Proposal시터가 보호자의 게시글에 돌봄을 제안 (역방향)
Reservation매칭 확정 후 생성되는 예약
PetSnapshot · TimeSlotInfo매칭 시점에 @Embeddable로 박제 — 원본이 바뀌어도 과거 예약 정합성 유지
HasTimeSlotInfoPost · CareRequest · Reservation이 공통으로 가진 시간 슬롯을 다형적으로 충돌 검사
ID-only 참조@SQLRestriction 기반 soft delete + ID-only 참조로 결합도 최소화
관측 (@TrackExecutionTime)커스텀 AOP로 락·트랜잭션·API 응답 시간을 Prometheus 지표로 노출
04.1 · 기술적 도전

Race Condition —
Lock과 Transaction의 순서 문제.

문제
동일 Reservation에 confirm / cancel / webhook이 동시 진입 시 Payment 상태가 중복 갱신.
원인
Lock이 Transaction 안쪽에 걸려 수명이 역전 → 커밋 전에 락이 해제되면서 짧은 구간에 다른 스레드가 같은 데이터에 접근 가능. 락의 의미가 사실상 사라짐.
해결
  1. Lock이 Transaction을 감싸도록 구조 리팩토링 (@Order(HIGHEST_PRECEDENCE)@Transactional보다 바깥 보장)
  2. @DistributedLock AOP + Redisson 추상화, SpEL 동적 키 적용
  3. webhook처럼 파라미터에 reservationId가 없는 케이스는 LockService(Supplier)로 선조회 후 락
  4. 락 통과 후 stale read/write 방어를 위해 Payment · ReservationPayment에 @Version 낙관락 추가
결과
예약 확정·취소·결제 콜백 동시 발생 시 상태 중복 처리를 구조적으로 차단. 회귀 방지 통합 테스트 확보.
설계 결정
분산 락은 트랜잭션 바깥에 두는 것이 원칙. AOP Order를 명시적으로 지정하지 않으면 @Transactional보다 안쪽에 걸릴 수 있다는 걸 체감했습니다. SpEL 동적 키를 AOP 체인 중간에 바인딩하는 과정에서는 Reflection으로 파라미터를 재추출해야 했습니다.
04.2 · 기술적 도전

PG 부분환불 미지원
→ Settlement 구조 분리로 우회.

문제
예약 취소 시 위약금을 제외한 금액만 환불해야 하는데, PortOne(KG이니시스) 부분환불 API 호출이 실패 (pgCode=500503).
원인
PortOne(KG이니시스)가 부분환불을 지원하지 않음 → PG 레벨에서는 해결 불가로 판단.
해결
  1. PG 호출로 푸는 방향을 포기, 정산(Settlement) 구조 변경으로 우회하기로 결정
  2. 위약금을 제외한 금액을 별도 Settlement로 생성
  3. SettlementType enum 확장 → 부분환불 시 정산 2건(환불분 / 정산분)으로 분리
  4. PaymentErrorCode 신설로 외부 PG 실패 분기를 표준화
결과
부분환불 시나리오 실패 케이스 0건.
설계 결정
외부 시스템 한계를 마주쳤을 때 PG를 바꾸는 대신 도메인 모델로 우회. 결제 이력과 정산 이력을 분리해두었기에 가능했던 선택.
04.3 · 기술적 도전

멀티 인스턴스 스케줄러
중복 실행 방지 — ShedLock.

ShedLock 다이어그램 — app1·app2·app3 중 하나만 락을 획득해 스케줄러 실행
ShedLock — 여러 인스턴스가 동시에 잡을 시도해도 락을 획득한 하나만 실행
문제
EC2 두 대에서 동일 스케줄러가 양쪽에서 실행되면서 같은 레코드를 두 번 처리.
해결
기존 스케줄러에 @SchedulerLock 부착만으로 다중 인스턴스 단일 실행 보장. Quartz 클러스터링 대신 ShedLock(Redis provider)을 선택한 이유는 기존 코드 변경 최소화. 스케줄러(잡 단위) + Redis SETNX(엔티티 단위) 두 층으로 락을 걸었습니다.
적용 스케줄러
  • Post / CareRequest 만료 처리
  • UNAVOIDABLE 사유 취소 자동 승인
  • Reservation 만료 처리
설계 결정
배치 시스템 도입 없이 기존 @Scheduled 위에 어노테이션 하나로 확장. "코드 최소 변경"이 우선 순위였고 ShedLock이 그 조건에 맞았습니다.
05 · 회고

기술 선택은
데이터로 남긴다.

네 가지 락 전략을 직접 만들고 비교해보고 나서야 "동시성 제어는 결국 자원 보호의 문제"라는 말이 와닿았습니다. 다음 프로젝트에서도 기술을 도입할 때 이런 비교 데이터를 먼저 남기려 합니다.