입문 프로젝트 배달 주문 플랫폼을 약 2주간 진행하여 드디어 마무리되었다. 아쉬운 점이 많기에 그냥 흘려보내지 않기 위해 회고록을 작성해보고자 한다.
이번 프로젝트에서 인증/인가 시스템, 회원 및 배송지 도메인 구현, 공통 응답및 전역 처리기 구축, CI/CD 파이프라인 구축을 담당하였다.
응답 표준화: ApiResponse 클래스를 도입하여 상태 코드, 메시지, 데이터를 포함한 공통 응답 구조를 설계, API 일관성 유지하도록 하였다.
예외 중앙 관리: @RestControllerAdvice 기반의 전역 예외 처리기(GlobalExceptionHandler)를 구축하여 시스템 내 모든 예외를 ErrorCode 기반으로 통합 핸들링하고자 하였다.
CI/CD는 GitHub Actions로 배포 자동화를 위해 사용하였다. master 브랜치에 push되면 파이프라인이 트리거되며, Gradle로 JAR를 빌드(-x test)한 뒤 Docker 이미지를 빌드하여 Amazon ECR에 Push한다. 이후 docker-compose.yml과 Nginx 설정 파일을 SCP로 EC2에 전송하고, SSH로 접속하여 ECR에서 이미지를 Pull한 뒤 docker compose up -d --force-recreate로 컨테이너를 재기동한다. 환경변수는 GitHub Secrets의 ENV_FILE로 관리하여 민감 정보를 코드에서 분리하였다.
JWT 기반 인증: JwtTokenProvider로 액세스 토큰(8분)과 리프레시 토큰을 발급하며, HMAC-SHA256 알고리즘으로 서명한다. JwtAuthenticationFilter가 매 요청마다 Authorization: Bearer {token} 헤더를 검증하고 SecurityContext에 인증 정보를 등록한다.
Redis기반 토큰 관리: 로그아웃 시 액세스 토큰을 SHA-256으로 해싱하여 Redis 블랙리스트(auth:blacklist:)에 남은 유효 시간을 TTL로 저장함으로써 토큰 재사용을 방지한다. 리프레시 토큰은 auth:refresh:{username} 키로 Redis에 저장하며, 재발급 요청 시 저장된 값과 비교하여 유효성을 검증한다.
로그인 Rate Limit: LoginRateLimiter를 통해 IP당 60초 내 최대 10회 로그인 시도를 허용하며, 초과 시 RATE_LIMIT_EXCEEDED(429) 에러를 반환한다. X-Forwarded-For 헤더를 우선 확인하여 프록시 환경에서도 실제 클라이언트 IP를 정확히 식별한다.
역할 기반 접근 제어: UserRole enum으로 CUSTOMER, OWNER, MASTER 세 가지 권한을 구분하며, MASTER 권한은 회원가입 시 선택할 수 없도록 제한한다. SecurityConfig에서 공개 엔드포인트(회원가입, 로그인, 토큰 재발급, Swagger)를 제외한 모든 요청에 인증을 요구하며, 세션을 사용하지 않는 STATELESS 정책을 적용하였다.
회원 관리 API: MASTER는 전체 회원 목록 조회 및 모든 회원 정보 접근이 가능하고, 일반 사용자는 본인 정보만 조회·수정·탈퇴할 수 있도록 권한을 분리하였다. 비밀번호 변경은 현재 비밀번호 확인 후 새 비밀번호로 변경하며, MASTER도 타인의 비밀번호는 변경할 수 없다. MASTER만 타 사용자의 역할을 변경할 수 있으며, 자기 자신의 역할은 변경할 수 없도록 제한하였다.
소프트 삭제 및 감사 추적: BaseAuditEntity를 상속하여 생성·수정·삭제 시각과 처리자를 자동으로 기록한다. 탈퇴는 deletedAt을 설정하는 소프트 삭제 방식으로 처리하며, 탈퇴 회원의 로그인을 차단한다.
배송지 CRUD: 회원 1명이 여러 배송지를 등록할 수 있으며(ManyToOne 관계), 생성·조회·수정·삭제 API를 제공한다. 삭제는 소프트 삭제 방식이며, 페이지네이션을 지원한다. MASTER는 전체 배송지를 조회할 수 있고, 일반 사용자는 본인 배송지에만 접근 가능하다.
기본 배송지 관리: PATCH /{addressId}/default 엔드포인트로 기본 배송지를 지정할 수 있으며, 새로운 기본 배송지 설정 시 기존 기본 배송지를 자동으로 해제하여 사용자 당 기본 배송지가 항상 하나만 유지되도록 보장한다.
데일리 스크럼의 힘: 매일 짧게 진행한 스크럼을 통해 서로의 진행 상황을 투명하게 공유했는데 이 시간이 팀원 간의 속도를 맞추고 병목 현상을 조기에 발견하는 데 큰 도움이 되었다.
코드 리뷰를 통한 성장: "내 코드를 짜는 것보다 리뷰하는 것이 더 큰 공부가 된다"는 것을 느낄 수 있었다.
설계 고민의 부재: 기능 구현 일정에 쫓기다 보니 객체지향적인 설계나 확장성에 대한 깊은 고민이 부족했다. "일단 돌아가게 만들기"에 급급했던 점이 가장 아쉬웠다.
성능 최적화와 모니터링: 배포 후 헬스 체크나 장애 대응 시나리오가 느꼈다. 향후에는 DB 쿼리 튜닝 등 성능 이슈를 고민하고, 개선 결과를 수치화(지표화)하여 객관적인 성능 최적화 경험을 쌓고 싶다.
프로젝트를 진행하며 겪었던 트러블 슈팅 중 하나를 소개해보고 느낀점을 정리해보자면 로그아웃 기능을 구현하며 직면한 문제이다.
문제 상황: JWT는 Stateless한 특성 때문에 서버에서 특정 토큰을 강제로 무효화할 방법이 없었다.
해결 방안: Redis를 도입하여 로그아웃 요청 시 해당 Access Token을 블랙리스트(Blacklist)로 등록하는 방법을 선택했다. 이후 요청마다 필터에서 Redis에 해당 토큰이 있는지 확인하여 접근을 차단하는 방식으로 해결했다.
반성 및 개선 사항: 해결 후 다른 팀의 발표를 보며 '토큰 버전 관리' 등 다양한 대안이 있음을 알게 되었다. "가장 먼저 찾은 방법이 최선의 방법은 아닐 수 있다"는 점을 깨달았고, 앞으로는 문제 해결 시 여러 선택지를 비교 분석하는 습관을 지니기로 다짐했다.
기능 구현을 넘어 '운영'과 '확장'의 관점에서 시스템을 바라봐야 한다는 것을 느낀 프로젝트였다. 아쉬움이 많은 만큼 제가 채워야 할 영역이 명확해졌고, 다음 프로젝트에서는 "왜 이 기술인가?"에 대해 더 명확한 답을 내놓을 수 있도록 고민하는 과정을 중요하게 생각해 봐야겠다.