Java / Spring Boot 기반으로 JWT 인증·인가, 권한 분기, 예외 처리, 그리고 Docker 기반 CI/CD 배포까지 실무 기준으로 설계·구현한 백엔드 중심 개인 프로젝트
- JWT Access / Refresh Token 인증 구조 직접 설계
- Spring Security 필터 ↔ Service 계층 책임 분리
- 인증 권한과 비즈니스 권한 분리 설계
- 실무 기준 예외 처리 · 트랜잭션 패턴 적용
- Docker + GitHub Actions 기반 자동 배포 환경 구축
- 최소 UI를 통한 인증·권한 정책 실제 동작 검증
👉 UI 구현이 아닌 백엔드 설계와 운영 환경 검증에 초점을 둔 프로젝트입니다.
- EC2 + Nginx + HTTPS 적용
- 실제 배포 환경에서 인증 흐름 확인 가능
- Java 17
- Spring Boot 3.x
- Spring Security
- JPA (Hibernate)
- MariaDB, Flyway
- JWT (Access / Refresh Token)
- AWS EC2 (Ubuntu)
- Docker / Docker Compose
- Nginx (Reverse Proxy)
- GitHub Actions
- GitHub Container Registry (GHCR)
- Self-hosted GitHub Actions Runner
- IntelliJ HTTP Client (auth.http)
- React (Minimal UI, 검증 목적)
Client
↓
Nginx (Reverse Proxy)
↓
Spring Boot API (Docker)
↓
MariaDB (Docker)
Controller → Service → Repository
- Service 계층:
- 비즈니스 로직
- 권한 검사
- 트랜잭션 관리 담당
- Controller 계층:
- 요청/응답 역할에 집중
- 인증/인가: JWT + Spring Security
- 비즈니스 예외: BusinessException + ErrorCode
- 응답 포맷: ApiResponse
- 전역 예외 처리: GlobalExceptionHandler
- Stateless 구조로 서버 확장성 확보
- API 중심 서비스 구조에 적합
- 프론트엔드 / 모바일 환경과 호환성 우수
- Access Token: 짧은 수명 → 탈취 피해 최소화
- Refresh Token:
- DB에 저장
- 강제 로그아웃 및 재발급 제어 가능
세션 기반 인증 방식도 검토했으나, 서버 수평 확장 시 세션 공유 비용을 줄이기 위해 JWT 방식을 선택했습니다.
POST /api/v1/users/login- AccessToken + RefreshToken 발급
- RefreshToken은 DB 저장 (유저당 1개)
Authorization: Bearer {accessToken}- JwtAuthenticationFilter에서 검증 후 SecurityContext에 사용자 정보 저장
POST /api/v1/users/reissue- RefreshToken 검증 후 AccessToken만 재발급
- Refresh Token이 유효하지 않으면 재로그인 유도
POST /api/v1/users/logout- RefreshToken DB 삭제
- 기존 AccessToken은 만료 시까지 유효 (Stateless)
- Spring Security 필터 단계에서 처리
- 인증 실패 시 401 반환
- Service 계층에서 직접 검사
- 작성자 불일치 시 AccessDeniedException 발생
- GlobalExceptionHandler에서 403으로 응답
- Service 계층에서 BusinessException
- @Valid 실패 시 MethodArgumentNotValidException
- Service 계층:
AccessDeniedException - Security 필터 단계:
AuthenticationEntryPointAccessDeniedHandler
Security 필터 단계 예외와 서비스 계층 예외를 분리하여 예외 흐름을 명확히 했습니다.
| 상태 코드 | 의미 |
|---|---|
| 400 | 잘못된 요청 |
| 401 | 인증 실패 |
| 403 | 권한 없음 |
| 404 | 리소스 없음 |
| 409 | 중복 데이터 |
| 500 | 서버 오류 |
- 트랜잭션은 Service 계층에서 관리
- 조회 로직은
@Transactional(readOnly = true)적용 - 쓰기 / 읽기 책임 분리
설계 이유
- 불필요한 변경 감지 비용 감소
- 실무에서 사용하는 트랜잭션 범위 기준 적용
- POST /api/v1/users/signup
- POST /api/v1/users/login
- GET /api/v1/users/me
- POST /api/v1/users/reissue
- POST /api/v1/users/logout
- POST /api/v1/posts
- GET /api/v1/posts/{id}
- GET /api/v1/posts?page=0&size=10
- PUT /api/v1/posts/{id}
- DELETE /api/v1/posts/{id}
- POST /api/v1/posts/{postId}/comments
- GET /api/v1/posts/{postId}/comments
- PUT /api/v1/posts/{postId}/comments/{commentId}
- DELETE /api/v1/posts/{postId}/comments/{commentId}
SSH 없이, main 브랜치 push만으로도 자동 배포
git push (main)
↓
GitHub Actions
- Docker Image Build
- GHCR Push
↓
Self-hosted Runner (EC2)
- docker compose pull
- docker compose up -d --force-recreate
↓
Production 반영
- scp / ssh 기반 수동 배포 제거
- 이미지 단위 배포로 서버 상태 일관성 확보
- 컨테이너 재생성(--force-recreate)으로 배포 반영 보장
- Spring Boot Actuator /actuator/health 로 배포 성공 여부 검증
curl -I http://127.0.0.1:8080/actuator/health
# HTTP/1.1 200 OK
운영 환경에서 ddl-auto=create/update를 사용하지 않고,
Flyway 기반 마이그레이션으로 DB 스키마를 관리합니다.
spring.jpa.hibernate.ddl-auto=validate고정- 스키마 변경은 Flyway 마이그레이션 파일로만 수행
- Docker Compose 운영 환경에서도 앱 기동 시 자동 migrate
- IntelliJ HTTP Client(auth.http)로 인증·권한·CRUD 시나리오 검증
- 인증 실패(401), 권한 실패(403) 케이스 포함
- 요청 시나리오를 코드처럼 관리
- 목적:
- JWT 인증 흐름 검증
- 권한 제어 정책 실제 동작 확인
- 모든 권한 판단은 백엔드에서 수행
- 프론트엔드는 UX 목적의 조건부 렌더링만 담당
- 좋아요 기능 추가 및 사용자 반응 데이터 관리
- 검색 및 페이징 고도화
- 운영 환경 고도화
- Refresh Token 저장소 확장 시 캐시 도입 검토 (트래픽 증가 대비)
- 무중단 배포 구조(Blue/Green) 적용