💻 PROJECT/Team Projects

[ 🚚 ONESTOP ] Spring Boot 프로젝트 기술 선택 이유 정리

eunjiom 2026. 5. 13. 21:10

 

의존성 선택

JWT (jjwt 0.12.6)

인증/인가에 세션 대신 JWT를 씀

세션 방식은 서버가 상태를 저장해야 해서 서버 여러 대 운영할 때 세션 공유 문제가 생김. JWT는 토큰 자체에 userId, role 정보가 담겨 있어서 서버가 상태를 저장 안 해도 됨. BUYER / SELLER / ADMIN 세 가지 Role을 하나의 토큰으로 구분 가능

Spring Session + Redis도 고려했는데, Redis 장애 시 전체 인증이 터지는 리스크가 있어서 제외함


QueryDSL 5.1.0

상품 조회에서 카테고리, 키워드, 가격 범위, 정렬 조건이 동적으로 조합되는 쿼리가 필요했음

JPQL은 조건마다 분기 처리가 복잡하고 문자열을 이어붙이는 방식이라 타입 안전하지 않음. QueryDSL은 Java 코드로 쿼리를 조립하니까 컴파일 시점에 오류를 잡을 수 있고 조건을 동적으로 추가/제거하기 쉬움

JPA Specification도 있는데, 복잡한 조건 조합에서 가독성이 떨어지고 조인 많아지면 한계가 있어서 제외함


Redisson 3.27.2 (분산락)

쿠폰 선착순 발급에서 수천 명 동시 요청 시 중복 발급 문제를 막기 위해 분산락이 필요했음

기본 Redis 클라이언트인 Lettuce로도 구현 가능하지만 락 획득 실패 시 재시도 로직을 직접 짜야 함. Redisson은 분산락이 내장되어 있고 tryLock으로 타임아웃, 대기 시간 설정이 간편함. Pub/Sub 기반 락 해제 알림을 지원해서 불필요한 폴링도 없음


Resilience4j 2.2.0 (Circuit Breaker)

AI 리뷰 요약이 장애 나도 상품 구매는 정상 동작해야 하는 요구사항이 있었음

AI API 타임아웃이나 Rate Limit 발생 시 요청이 계속 쌓이면 서버 전체에 영향을 줄 수 있어서 Circuit Breaker가 필요했음

Netflix Hystrix도 있는데 2018년부터 유지보수가 중단됨. Resilience4j는 Spring Boot 3.x 호환되고 Retry, Rate Limiter, Bulkhead도 함께 제공해서 선택


패키지 구조 — Feature-based

domain/
├── auth/
├── user/
├── product/
├── order/
├── delivery/
├── admin/
└── ai/

 

Layer-based(controller / service / repository 계층별 분리)가 아닌 Feature-based를 선택한 이유는 두 가지임

 

1. Git 충돌 최소화 팀원 5명이 각자 도메인을 맡아서 개발하는 구조인데, Layer-based면 같은 controller/ 폴더에 여러 명이 동시에 파일을 추가하게 됨. Feature-based는 각자 담당 도메인 폴더 안에서만 작업하니까 충돌이 거의 없음

 

2. MSA 전환 용이 나중에 MSA로 전환할 때 order/ 패키지 전체를 별도 서비스로 떼어낼 수 있음. Layer-based는 기능이 여러 레이어에 흩어져 있어서 분리가 어려움

global/은 Security, JWT, 공통 예외처리처럼 전 도메인에서 공통으로 쓰는 코드를 별도 분리함. infra/는 스케줄러, 모니터링처럼 특정 도메인에 속하지 않는 시스템 운영 코드를 분리함


공통 응답 형식 & 예외 처리

ApiResponse<T>

팀원들이 각자 개발하면 응답 형식이 제각각이 될 수 있어서 통일함

// 성공
{ "success": true, "data": { ... } }

// 실패
{ "success": false, "message": "에러 메시지" }

 

@JsonInclude(NON_NULL)을 전역 설정이 아닌 클래스에 직접 붙인 이유는, 전역으로 설정하면 의도적으로 null을 반환해야 하는 경우에 제어가 어려워지기 때문


ErrorCode Enum

HTTP 상태코드, 에러 코드, 메시지를 하나의 Enum으로 묶어서 관리함

throw new CustomException(ErrorCode.USER_NOT_FOUND);
// → 404, "MEMBER_001", "존재하지 않는 회원입니다."

 

문자열 하드코딩은 중복과 오타 위험이 있고, 상수 클래스(static final)는 세 가지 정보를 묶어 관리하기 어려워서 Enum을 선택함. 컴파일 시점에 오류를 잡을 수 있는 것도 장점


GlobalExceptionHandler

@RestControllerAdvice로 전역 예외 핸들러를 만들어서 예외 처리 로직을 한 곳에 모음

처리하는 예외는 세 가지

  • CustomException — 비즈니스 로직 예외
  • MethodArgumentNotValidException — @Valid 유효성 검증 실패
  • Exception — 그 외 예상치 못한 예외 (500)

@RestControllerAdvice를 선택한 이유는 @ControllerAdvice + @ResponseBody가 합쳐진 것으로, REST API 서버라 뷰 없이 모든 응답이 JSON이기 때문


Spring Security + JWT 설정

BCryptPasswordEncoder

  • 단방향 암호화 방식 비교
방식 문제점
MD5 / SHA-256 솔트 없이 동일한 비밀번호가 항상 같은 해시값 → 레인보우 테이블, 브루트포스 취약
PBKDF2 반복 횟수 직접 설정 필요, Spring Security 통합 번거로움
BCrypt 매번 랜덤 솔트 자동 생성, 연산 느리게 설계, cost factor 조절 가능

 

> Spring Security 공식 권장 방식이라 BCrypt를 선택함


SessionCreationPolicy.STATELESS

세션 기반 인증은 서버가 여러 대일 때 세션 공유 문제가 생기고 Redis 같은 별도 저장소가 필요해짐. JWT는 토큰 서명만 검증하면 되니까 서버가 상태를 저장할 필요가 없음

SessionCreationPolicy.STATELESS 설정으로 Spring Security가 세션을 아예 생성하지 않게 해서 불필요한 메모리 사용도 없앰


authenticationEntryPoint / accessDeniedHandler 직접 구현

Spring Security 기본 동작은 인증 실패 시 302 리다이렉트인데, REST API에서 302를 받으면 클라이언트 처리가 어려움

두 상황을 분리해서 JSON으로 응답하도록 직접 구현함

  • authenticationEntryPoint → 토큰 없거나 유효하지 않을 때 (401, AUTH_007)
  • accessDeniedHandler → 로그인은 됐지만 권한이 없을 때 (403, AUTH_011)

프론트에서 에러 코드만 보고 401이면 로그인 페이지로, 403이면 권한 없음 메시지를 표시할 수 있음


Access Token / Refresh Token 분리

토큰 하나만 쓰면 만료 시간을 짧게 설정하면 사용자가 자주 재로그인해야 하고, 길게 설정하면 탈취됐을 때 오래 악용될 수 있음

  • Access Token: 15분 (탈취 시 피해 최소화)
  • Refresh Token: 14일 (사용자 편의성 유지)

@PostConstruct로 SecretKey를 초기화한 이유는, @Value로 주입받은 값은 빈 생성 시점에 주입되기 때문에 생성자에서 바로 쓰면 null이 됨. @PostConstruct는 빈이 완전히 초기화된 후 실행되니까 정상적으로 SecretKey를 생성할 수 있음


아직 구현 안 한 것들

  • JwtAuthFilter (OncePerRequestFilter) — User 엔티티 완성 후 구현 예정. 요청마다 JWT 검증하고 SecurityContext에 인증 정보 저장하는 역할
  • CustomUserDetails / CustomUserDetailsService — DB에서 사용자 조회해서 Security 컨텍스트에 올려주는 역할
  • Redis Refresh Token 저장 — 현재는 강제 로그아웃(계정 정지, 다른 기기 로그인 차단)이 불가능한 상태. refresh:{userId} 키로 Redis에 저장하면 서버에서 강제 만료 처리 가능