Spring Boot Boilerplate 설계 가이드
이 문서는 보일러플레이트의 핵심 설계 원칙과 패턴을 정리한 가이드입니다.
📐 아키텍처 원칙
Layer Architecture
Controller → Service → Repository
| 계층 | 역할 | 규칙 |
|---|---|---|
| Controller | 요청/응답 처리 | Repository 직접 사용 ❌, Entity 사용 ❌ |
| Service | 비즈니스 로직 | Entity ↔ DTO 변환 담당 |
| Repository | 데이터 접근 | QueryDSL에서 DTO 직접 반환 가능 |
API 설계 규칙 (CSR)
- 1 API = 1 Controller 메소드 = 1 Service 메소드
- 프론트엔드(JS)에서 진행 흐름을 받고 API 여러번 호출하는 방식
- API 여러번 호출이 부담될 때만 Facade 패턴 고려
- Service 메소드가 비슷하면 → private 공통 메소드로 추출
- SELECT만 하는 경우 → QueryDSL에서 DTO 직접 반환 권장
- DB조회나 해당 domain에 관한 로직은 Service, 파일이나 날짜 등 공통기능은 Util로 이름짓기
📁 파일 업로드
설계 원칙
- 파일 업로드는 별도 API로 분리 (게시판 API + 파일들 API)
- 파일 공통 관리 →
RefTypeEnum으로 용도 구분 - 트랜잭션 불일치(파일만 업로드됨) → 배치로 해결
환경별 저장 전략 (Strategy Pattern)
// 로컬 환경: LocalFileStorage (컴퓨터 경로에 저장)
// 운영 환경: SupabaseFileStorage (클라우드에 저장)
| 환경 | 저장소 | 설정 |
|——|——–|——|
| 개발 | 로컬 파일시스템 | supabase.enabled: false |
| 운영 | Supabase Storage | supabase.enabled: true |
yml 설정에 따라 LocalFileStorage 또는 SupabaseFileStorage 빈이 주입됨
🗄️ JPA 설계
연관관계 원칙
- 양방향 지양 → 프론트에서 API 분리 요청 (글 API + 댓글 API + 파일 API)
- 부모글 삭제 시 고아 데이터 → 배치 or orphanRemoval or Cascade or service에서 자식들도 같이 삭제 or 프론트에서 직접 자식 삭제요청
- 메인 데이터 여러개 연관관계 조회 (댓글목록 한번에 조회 + 각 댓글에 user username이 필요한상황)
- 방법 1: 메인 쿼리 실행 후 서브쿼리 id 모아서
IN쿼리로 한번에 처리 - 방법 2: 조인 - JPQL, @EntityGraph 등으로 한번에 쿼리 실행
- 상황에 맞게 선택. 이후 성능 문제가 있다면 개선
- 방법 1,2 둘다 repository(queryDSL)에서 처리. 연관관계가 너무 많을 때만 service에서 처리 고려
- 방법 1: 메인 쿼리 실행 후 서브쿼리 id 모아서
Entity vs DTO
// 변환메소드는 DTO측에서
// DTO → Entity
Entity entity = dto.toEntity();
// Entity → DTO
DTO dto = DTO.from(entity);
- Entity — DB 매핑 + 비즈니스 편의 메소드 (상태 변경). 편의메소드에서 exception 발생시켜도 공통처리 됨
- DTO — 변환 메소드 + API 요청/응답. 기본적으로 공통DTO 쓰다가 새로운 DTO 필요하면 그때그때 만들기
Repository 규칙
- 연관관계 필요 →
findByEntity() - 단순 조회 →
findByEntityId()(ID만으로 조회)
Paging Controller 바인딩
- Pageable → Controller 파라미터에서 직접 바인딩 (시작이 0,1인건 클라이언트가 처리)
📤 응답 표준화
성공 응답
ResponseEntity<ApiResponse<T>>
// 사용: return ResponseEntity.ok(ApiResponse.success("조회 성공", data));
페이징 응답
PageResponse<T> // Page<>의 필요한 필드만 추출
에러 응답
ErrorResponse // 모든 에러 상황에서 일관된 JSON 구조
// { success: false, message: "...", errorCode: "...", timestamp: "..." }
HTTP 상태 코드
| 코드 | 사용 | 예외 클래스 |
|——|——|————-|
| 400 | 비즈니스 규칙 위반 | BusinessRuleException |
| 401 | 인증 필요 / 토큰 만료 | SecurityConfig authenticationEntryPoint |
| 403 | 권한 없음 (작성자 아님) | AccessDeniedException |
| 404 | 리소스 없음 | EntityNotFoundException |
| 409 | 중복 리소스 | DuplicateResourceException |
전역 예외 처리 (GlobalExceptionHandler)
BusinessException하위 클래스들 → 각각의 HTTP 상태 코드로 자동 응답@Valid검증 실패 →VALIDATION_ERROR(400)- JSON 파싱 실패 →
INVALID_JSON(400) - 예상치 못한 예외 →
INTERNAL_SERVER_ERROR(500)
🔐 Security — JWT + OAuth2 (쿠키 방식)
인증 구조
브라우저: JWT를 HttpOnly 쿠키에 저장 (JS에서 접근 불가 → XSS 안전)
앱: JWT를 JSON 응답으로 받아 앱 내부 저장소에 저장
JWT 구조
| 토큰 | 만료 | 저장 | 용도 |
|——|——|——|——|
| Access Token | 30분 | 쿠키 access_token | API 인증 |
| Refresh Token | 4시간 | 쿠키 refresh_token + DB | Access Token 재발급 |
로그인 정보 사용
@AuthenticationPrincipal CustomUserAccount userAccount
userAccount.getUsername(); // 사용자 ID
userAccount.getUserDTO(); // 전체 사용자 정보
CORS 설정 주의사항
⚠️ Spring Security 사용 시 반드시
CorsConfigurationSourceBean으로 등록!WebMvcConfigurer방식은 Security 필터보다 늦게 동작해서 무용지물!
💬 실시간 채팅 (STOMP WebSocket)
구조
브라우저 ←→ SockJS(/ws-chat) ←→ STOMP 프로토콜 ←→ SimpleBroker
인증 흐름 (쿠키 기반)
1. SockJS 연결 요청 → HttpHandshakeInterceptor: 쿠키에서 JWT 추출 → 세션에 username 저장
2. STOMP CONNECT → JwtChannelInterceptor: ① Authorization 헤더 확인 (앱) ② 세션 username 확인 (웹)
메시지 흐름
클라이언트 send → /pub/room/{roomId} → ChatController → /sub/room/{roomId} → 구독자 전원
🔧 환경 분리
| 환경 | 파일 | 특징 |
|---|---|---|
| 개발 | application.yml |
ddl-auto: create, SQL 초기화, H2/MariaDB |
| 운영 | application-prod.yml |
ddl-auto: update, SQL 초기화 비활성화 |
📝 로그
- logback-spring.xml 사용
- 콘솔 + 파일(일별 롤링) + 에러 분리
- 30일 보관, 용량 제한
✅ 구현 현황
| 기능 | 상태 | 파일/패키지 |
|---|---|---|
| Layer Architecture | ✅ | Controller/Service/Repository 분리 |
| API 표준 응답 | ✅ | ApiResponse, PageResponse, ErrorResponse |
| 전역 예외 처리 | ✅ | GlobalExceptionHandler |
| JWT 인증 | ✅ | jwt/ 패키지 |
| OAuth2 로그인 | ✅ | 카카오, 구글 (보안 개선 적용) |
| 파일 업로드 | ✅ | file/ 패키지, Strategy Pattern |
| 게시판 (CRUD) | ✅ | community/ 패키지 |
| 댓글 (CRUD) | ✅ | community/comment/ 패키지 |
| 실시간 채팅 | ✅ | stomp/ 패키지 (STOMP + SockJS + JWT 인증) |
| 로그 설정 | ✅ | logback-spring.xml |
| 환경 분리 | ✅ | application.yml / application-prod.yml |
| Docker | ✅ | Dockerfile |
| Swagger | ✅ | /swagger-ui.html |
| Actuator | ✅ | /actuator/health |
| CORS | ✅ | CorsConfig.java (CorsConfigurationSource Bean) |
| QueryDSL | ✅ | 게시판 검색/페이징 |
🚀 추가 고려사항 (선택)
현재 미적용 (필요시 추가)
| 기능 | 설명 | 언제 필요? | |——|——|———–| | Redis | 세션/캐시/토큰 블랙리스트, WebSocket 세션 공유 | 서버 다중화 시 | | 채팅 메시지 DB 저장 | 현재 메시지는 실시간 전송만 (저장 안 됨) | 채팅 이력 필요 시 | | 이메일 발송 | 비밀번호 찾기, 알림 | 사용자 알림 필요 시 | | 스케줄러 | @Scheduled 배치 작업 | 고아 파일 정리, 통계 집계 | | 캐싱 | @Cacheable | 자주 조회되는 데이터 | | Rate Limiting | API 호출 제한 | 악용 방지 | | Access Token 블랙리스트 | JWT 즉시 무효화 (Redis) | 강제 로그아웃 필요 시 |
프론트엔드 분리 시 (React/Vue/Flutter 등)
templates/폴더,HomeController.java삭제- CORS 설정 확인 (
CorsConfig.java에 프론트 도메인 추가) - 앱은
Authorization: Bearer {token}헤더 방식 사용 - WebSocket: STOMP CONNECT 시
Authorization헤더로 JWT 전달
📚 참고 문서
설계/개념/- CORS, CSRF, Swagger 설명설계/동작설명용/- API 흐름, OAuth2 동작 설명설계/배포/- Docker, Railway 배포 가이드