Skip to content

Latest commit

 

History

History
232 lines (188 loc) · 10.2 KB

CLAUDE.md

File metadata and controls

232 lines (188 loc) · 10.2 KB

Billilge Backend

대학교 학생회 물품 대여 관리 시스템 백엔드

기술 스택

  • Spring Boot 3.4.1 / Kotlin 1.9.25 / JDK 21
  • JPA + QueryDSL (MySQL)
  • Spring Security (JWT + Google OAuth2)
  • Firebase Cloud Messaging (푸시 알림)
  • AWS S3 (이미지 업로드)
  • Apache POI (Excel 생성)

빌드 & 실행

./gradlew compileKotlin   # 컴파일 확인
./gradlew build           # 전체 빌드
./gradlew bootRun         # 실행

아키텍처

Controller → Facade → Service → Repository
레이어 역할 DTO 참조
Controller HTTP 요청/응답, @AuthenticationPrincipal로 인증 정보 추출 Request/Response DTO
Facade Request DTO 분해, 크로스 도메인 조합, Response DTO 생성 Request/Response DTO
Service 비즈니스 로직, 자기 도메인 Repository만 사용 Entity/primitives만
Repository 데이터 접근 (JPA + QueryDSL) Entity/Query DTO만

핵심 규칙

  • Service는 Request/Response DTO를 참조하지 않는다 — Entity, primitives, 글로벌 DTO(PageableCondition, SearchCondition)만 사용
  • Service는 타 도메인 Repository를 직접 의존하지 않는다 — 타 도메인 Service를 통해 접근 (예외: PayerService → MemberRepository 순환 의존 방지)
  • 크로스 도메인 조합은 Facade에서 수행한다 — Facade가 여러 Service를 호출해 엔티티를 조합 후 Service에 전달
  • Facade에서 트랜잭션 필요 시 @Transactional 명시 — 여러 서비스 호출을 하나의 persistence context로 묶어야 할 때

트랜잭션 패턴

  • Service 클래스에 @Transactional(readOnly = true) 기본 적용
  • 쓰기 메서드만 @Transactional로 오버라이드
  • JPA dirty checking 활용 — repository.save() 없이 엔티티 필드 변경으로 자동 반영

도메인 구조

domain/
├── item/           # 물품 관리
├── member/         # 회원, 인증
├── notification/   # 알림 (FCM 푸시)
├── payer/          # 회비 납부자 관리
└── rental/         # 대여/반납 관리

각 도메인 패키지 구조:

domain/{name}/
├── controller/     # API 컨트롤러 + Api 인터페이스 (Swagger)
├── facade/         # Facade (DTO 변환, 크로스 도메인 조합)
├── service/        # 비즈니스 로직
├── repository/     # JPA Repository + Custom(QueryDSL)
├── entity/         # JPA Entity
├── dto/            # request/, response/
├── enums/          # 도메인 열거형
└── exception/      # 도메인 에러 코드

서비스 의존성

ItemService            → ItemRepository, S3Service
MemberService          → MemberRepository, TokenProvider, PayerService
NotificationService           → NotificationRepository, NotificationPushOutboxService
NotificationPushOutboxService → NotificationPushOutboxRepository
PushNotificationSender        → FCMService, MemberService, NotificationPushOutboxService
PayerService                  → PayerRepository, MemberRepository, ExcelGenerator
RentalService                 → RentalRepository, ApplicationEventPublisher

알림 발송 구조

RentalService가 이벤트를 발행하면 NotificationEventHandler가 커밋 이후 비동기로 받아 처리한다.

RentalService → 이벤트 발행
  └ NotificationEventHandler (@Async + AFTER_COMMIT, 트랜잭션 없음)
      ├ NotificationService.createNotification()   # 알림 + 아웃박스 저장 (한 트랜잭션, 즉시 커밋)
      └ PushNotificationSender.dispatch()          # 트랜잭션 밖에서 FCM 호출

PushRetryScheduler (@Scheduled, 30초 간격)
  └ PushNotificationSender.dispatch()              # 미발송 건 재시도 (같은 경로)
  • 푸시 발송은 트랜잭션 밖에서 수행한다 — 네트워크 I/O가 DB 커넥션을 점유하지 않도록, 그리고 푸시 실패가 저장된 알림을 롤백시키지 않도록 분리
  • PushNotificationSender는 예외를 전파하지 않는다 — 한 수신자의 실패가 다른 수신자에게 영향을 주면 안 됨
  • FCM 실패는 PushResult로 구분한다 — InvalidToken(토큰 제거) / Retryable(재시도 대상) / Permanent(재시도 무의미)
  • @Async는 알림 전용 실행기(notificationTaskExecutor)를 사용한다 — AsyncConfig에 정의

푸시 재시도 (아웃박스)

발송 대상을 notification_push_outbox에 수신자 단위 row로 남긴다. 알림과 같은 트랜잭션에서 저장되므로 프로세스가 재시작돼도 발송 대상이 남는다.

PENDING ─┬─ 발송 성공 ──────────────→ SENT
         ├─ Retryable 실패 ─ 백오프 → PENDING (재시도 횟수 소진 시 FAILED)
         ├─ InvalidToken/Permanent → FAILED
         └─ 생성 후 1시간 경과 ─────→ EXPIRED
  • 유효 시간(TTL)은 10분 — 사용자 알림은 지금 과방에 갈지를 결정하는 정보고, 관리자 알림도 학생이 기다리는 상태에서 처리해야 하는 일이라 둘 다 실시간성이 중요하다. 지나면 EXPIRED로 포기하며, 인앱 알림은 이미 저장돼 있으므로 정보가 사라지는 것은 아니다
  • 백오프는 30초 → 2분 → 5분, 최대 3회 — 누적 7분 30초로 유효 시간 안에 들어온다
  • 다음 재시도 시각이 TTL을 넘기면 예약하지 않고 즉시 포기한다 — 어차피 만료될 시도를 기다리지 않는다
  • 즉시 발송과 재시도가 같은 경로를 탄다 — 새 row의 nextRetryAt은 60초 뒤로 잡혀, 즉시 시도와 폴러가 겹치지 않는다
  • 메시지 본문은 저장하지 않는다 — 연결된 Notification의 status와 formatValues로 재구성
  • 인스턴스를 여러 대로 늘리면 조회에 잠금(FOR UPDATE SKIP LOCKED)이나 ShedLock이 필요하다

보관 정책 — PushOutboxPurgeScheduler가 매일 새벽 4시(KST)에 정리한다.

상태 보존 기간
SENT 7일
FAILED, EXPIRED 30일 (실패 원인 확인용)
PENDING 삭제하지 않음
  • 배치(500건)로 나눠 삭제하고 배치마다 트랜잭션을 끊는다 — 락 구간을 짧게 유지
  • 한 회 처리량 상한(20배치)에 도달하면 경고 로그를 남긴다 — 남은 건이 있다는 사실이 묻히지 않도록

대여 상태 머신

[사용자 신청]  PENDING → CONFIRMED → RENTAL → RETURN_PENDING → RETURN_CONFIRMED → RETURNED
                      → REJECTED
               PENDING → CANCEL (사용자 취소)

[관리자 생성]  대여물품: → RENTAL (바로 대여중)
              소모품:   → RETURNED (즉시 반납 처리)
  • CONFIRMED: 재고 차감, 담당자(worker) 배정
  • RETURNED: 재고 복원 (소모품 제외)
  • 소모품(CONSUMPTION): RENTAL 상태 요청 시 자동으로 RETURNED 처리

대여 비즈니스 규칙

  • 회비 납부자(isFeePaid)만 대여 가능
  • 동일 물품 중복 대여 불가 (ignoreDuplicate로 우회 가능)
  • 시험 기간 대여 불가 (exam-period.start-date / end-date)
  • 주말 대여 불가
  • 과거 시간 대여 불가
  • 10시~17시만 대여 가능
  • Dev 모드(/rentals/dev): 시간 검증 생략, ADMIN 역할 필요

API 엔드포인트

인증 (Public)

Method Path 설명
POST /auth/sign-up 회원가입
POST /auth/admin-login 관리자 로그인

물품 (Public)

Method Path 설명
GET /items 물품 검색

회원 (JWT 필요)

Method Path 설명
POST /members/me/fcm-token FCM 토큰 등록

알림 (JWT 필요)

Method Path 설명
GET /notifications 알림 목록
GET /notifications/count 안읽은 알림 수
PATCH /notifications/{id} 알림 읽음
PATCH /notifications/all 전체 읽음

대여 (JWT 필요)

Method Path 설명
POST /rentals 대여 신청
POST /rentals/dev 개발용 대여 (시간 검증 생략)
GET /rentals 대여 이력 조회
PATCH /rentals/{id} 대여 취소
PATCH /rentals/return/{id} 반납 신청
GET /rentals/return-required 반납 필요 목록

관리자 (JWT + @OnlyAdmin)

Method Path 설명
GET /admin/items 물품 목록 (대여자 수 포함)
POST /admin/items 물품 추가
PUT /admin/items/{id} 물품 수정
GET /admin/items/{id} 물품 상세
DELETE /admin/items/{id} 물품 삭제
GET /admin/members 회원 목록
GET /admin/members/admins 관리자 목록
POST /admin/members/admins 관리자 등록
DELETE /admin/members/admins 관리자 해제
GET /admin/members/payers 납부자 목록
POST /admin/members/payers 납부자 등록
DELETE /admin/members/payers 납부자 삭제
GET /admin/members/payers/excel 납부자 엑셀 다운로드
GET /admin/notifications 관리자 알림
GET /admin/rentals/dashboard 대시보드
GET /admin/rentals 대여 이력
PATCH /admin/rentals/{id} 대여 상태 변경
POST /admin/rentals 관리자 대여 생성
DELETE /admin/rentals/{id} 대여 이력 삭제

Global 패키지

global/
├── annotation/     # @OnlyAdmin
├── config/         # SecurityConfig, CorsConfig, SwaggerConfig, QueryDslConfig, AsyncConfig
├── dto/            # PageableCondition, SearchCondition, PageableResponse
├── exception/      # ApiException, ErrorCode, GlobalExceptionHandler
├── external/
│   ├── fcm/        # FCMConfig, FCMService
│   └── s3/         # S3Config, S3Service
├── logging/        # LoggingFilter
├── security/
│   ├── jwt/        # TokenProvider, TokenAuthenticationFilter
│   └── oauth2/     # Google OAuth2 핸들러, UserAuthInfo
└── utils/          # DateUtils(isWeekend), ExcelGenerator