diff --git a/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java b/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java index 0f99e59b..8a986b4d 100644 --- a/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java +++ b/apps/user-service/src/main/java/site/icebang/common/dto/ApiResponse.java @@ -4,15 +4,72 @@ import lombok.Data; +/** + * 공통 APi 응답 DTO 클래스입니다. + * + *

REST API의 응답 형식을 표준화하기 위해 사용됩니다. 모든 응답은 성공 여부({@link #success}), 응답 데이터({@link #data}), 응답 + * 메시지({@link #message}), 그리고 HTTP 상태 코드({@link #status})를 포함합니다. + * + *

사용 예시: + * + *

{@code
+ * // 성공 응답 생성
+ * ApiResponse response = ApiResponse.success(userDto);
+ *
+ * // 메시지를 포함한 성공 응답
+ * ApiResponse response = ApiResponse.success(userDto, "회원 조회 성공");
+ *
+ * // 오류 응답 생성
+ * ApiResponse errorResponse = ApiResponse.error("잘못된 요청입니다.", HttpStatus.BAD_REQUEST);
+ * }
+ * + * @param 응답 데이터의 타입 + * @author jys01012@gmail.com + * @since v0.0.1-alpha + * @see HttpStatus + * @see lombok.Data + */ @Data public class ApiResponse { + /** + * 요청 처리 성공 여부. + * + *

true: 요청이 정상적으로 처리됨 false: 요청 처리 중 오류 발생 + */ private boolean success; + + /** + * 실제 응답 데이터(payload). + * + *

요청이 성공적으로 처리되었을 경우 반환되는 데이터이며, 실패 시에는 {@code null}일 수 있습니다. + */ private T data; + + /** + * 응답 메세지. + * + *

성공 또는 오류 상황을 설명하는 메시지를 담습니다. 클라이언트에서 사용자에게 직접 표시할 수도 있습니다. + */ private String message; + + /** + * HTTP 상태 코드. + * + *

Spring의 {@link HttpStatus} 열거형을 사용합니다. + */ private HttpStatus status; // HttpStatus로 변경 + /** 기본 생성자입니다. 모든 필드가 기본값으로 초기화됩니다. */ public ApiResponse() {} + /** + * 모든 필드를 초기화하는 생성자. + * + * @param success 요청 성공 여부 + * @param data 응답 데이터 + * @param message 응답 메시지 + * @param status HTTP 상태 코드 + */ public ApiResponse(boolean success, T data, String message, HttpStatus status) { this.success = success; this.data = data; @@ -20,18 +77,50 @@ public ApiResponse(boolean success, T data, String message, HttpStatus status) { this.status = status; } + /** + * 성공 응답을 생성합니다. (기본 메시지: "OK", 상태: 200 OK) + * + * @param data 응답 데이터 + * @param 데이터 타입 + * @return 성공 응답 객체 + */ public static ApiResponse success(T data) { return new ApiResponse<>(true, data, "OK", HttpStatus.OK); } + /** + * 성공 응답을 생성합니다. (상태: 200 OK) + * + * @param data 응답 데이터 + * @param message 사용자 정의 메시지 + * @param 데이터 타입 + * @return 성공 응답 객체 + */ public static ApiResponse success(T data, String message) { return new ApiResponse<>(true, data, message, HttpStatus.OK); } + /** + * 성공 응답을 생성합니다. + * + * @param data 응답 데이터 + * @param message 사용자 정의 메시지 + * @param status 사용자 정의 상태 코드 + * @param 데이터 타입 + * @return 성공 응답 객체 + */ public static ApiResponse success(T data, String message, HttpStatus status) { return new ApiResponse<>(true, data, message, status); } + /** + * 오류 응답을 생성합니다. + * + * @param message 오류 메시지 + * @param status HTTP 상태 코드 + * @param 데이터 타입 + * @return 오류 응답 객체 + */ public static ApiResponse error(String message, HttpStatus status) { return new ApiResponse<>(false, null, message, status); } diff --git a/apps/user-service/src/main/java/site/icebang/common/dto/PageParams.java b/apps/user-service/src/main/java/site/icebang/common/dto/PageParams.java index 5f2f0d30..6083bc43 100644 --- a/apps/user-service/src/main/java/site/icebang/common/dto/PageParams.java +++ b/apps/user-service/src/main/java/site/icebang/common/dto/PageParams.java @@ -2,23 +2,90 @@ import lombok.Data; +/** + * 페이징, 검색, 정렬, 필터링을 위한 공통 매개변수 클래스입니다. + * + *

목록 조회 API에서 공통적으로 사용되는 요청 파라미터를 정의합니다. 현재 페이지 번호({@link #current}), 페이지 크기({@link #pageSize}), + * 검색어({@link #search}), 정렬 조건({@link #sorters}), 필터 조건({@link #filters})를 포함합니다. + * + *

사용 예시: + * + *

{@code
+ * PageParams params = new PageParams();
+ * params.setCurrent(2);
+ * params.setPageSize(20);
+ * params.setSearch("회원");
+ *
+ * int offset = params.getOffset(); // 20
+ * boolean searchable = params.hasSearch(); // true
+ * }
+ * + * @author jys01012@gmail.com + * @since v0.0.1-alpha + * @see lombok.Data + */ @Data public class PageParams { + /** + * 현재 페이지 번호 (1부터 시작). + * + *

1부터 시작하며, 기본값은 1입니다. 0 이하의 값은 유효하지 않습니다. + */ private int current = 1; + + /** + * 한 펭지에 표시할 데이터 개수. + * + *

한 페이지에 표시할 항목의 개수를 지정합니다. 기본값은 10개이며, 일반적으로 10, 20, 50, 100 등의 값을 사용합니다. + */ private int pageSize = 10; + + /** + * 검색어. + * + *

목록에서 특정 조건으로 검색할 때 사용되는 키워드입니다. {@code null}이거나 빈 문자열인 경우 검색 조건이 적용되지 않습니다. + */ private String search; + + /** + * 정렬 조건 배열. + * + *

예: {@code ["name:asc", "createdAt:desc"]} API 설계에 따라 "필드명:정렬방향" 형식을 권장합니다. {@code null}이거나 빈 + * 배열인 경우 기본 정렬이 적용됩니다. + */ private String[] sorters; + + /** + * 필터링 조건 배열. + * + *

예: {@code ["status:active", "role:admin"]} 각 요소는 특정 필드에 대한 필터링 조건을 나타냅니다. 형태는 구현에 따라 다를 수 + * 있습니다. + */ private String[] filters; - // 계산된 offset + /** + * 페이징 처리를 위한 offset(시작 위치)을 계산합니다. + * + * @return (current - 1) * pageSize + */ public int getOffset() { return (current - 1) * pageSize; } + /** + * 검색어가 유효하게 존재하는지 확인합니다. + * + * @return 검색어가 null이 아니고 공백이 아닌 경우 true + */ public boolean hasSearch() { return search != null && !search.trim().isEmpty(); } + /** + * 정렬 조건이 존재하는지 확인합니다. + * + * @return 정렬 조건 배열이 null이 아니고, 1개 이상 있는 경우 true + */ public boolean hasSorters() { return sorters != null && sorters.length > 0; } diff --git a/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java b/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java index 4a2a8bfa..0982be0a 100644 --- a/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java +++ b/apps/user-service/src/main/java/site/icebang/common/dto/PageResult.java @@ -6,17 +6,67 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * 페이징 처리된 결과 DTO 클래스. + * + *

목록 조회 API에서 페이징된 데이터를 반환할 때 사용됩니다. 실제 데이터 목록({@link #data}), 전체 개수({@link #total}), 현재 페이지 + * 번호({@link #current}), 페이지 크기({@link #pageSize}), 전체 페이지 수({@link #totalPages}), 다음/이전 페이지 여부를 + * 포함합니다. + * + *

사용 예시: + * + *

{@code
+ * PageParams params = new PageParams();
+ * params.setCurrent(2);
+ * params.setPageSize(10);
+ *
+ * // Repository나 Mapper에서 데이터를 가져와 PageResult 생성
+ * PageResult pageResult = PageResult.from(
+ *     params,
+ *     () -> userRepository.findUsers(params.getOffset(), params.getPageSize()),
+ *     () -> userRepository.countUsers()
+ * );
+ *
+ * boolean hasNext = pageResult.isHasNext(); // true or false
+ * }
+ * + * @param 데이터 타입 + * @author jys01012@gmail.com + * @since v0.0.1-alpha + */ @Data @NoArgsConstructor public class PageResult { + + /** 현재 페이지에 포함된 데이터 목록. */ private List data; + + /** 전체 데이터 개수. */ private int total; + + /** 현재 페이지 번호 (1부터 시작). */ private int current; + + /** 한 페이지에 포함되는 데이터 개수. */ private int pageSize; + + /** 전체 페이지 수. */ private int totalPages; + + /** 다음 페이지가 존재하는지 여부. */ private boolean hasNext; + + /** 이전 페이지가 존재하는지 여부. */ private boolean hasPrevious; + /** + * 생성자. + * + * @param data 현재 페이지 데이터 + * @param total 전체 데이터 개수 + * @param current 현재 페이지 번호 + * @param pageSize 페이지 크기 + */ public PageResult(List data, int total, int current, int pageSize) { this.data = data; this.total = total; @@ -25,24 +75,55 @@ public PageResult(List data, int total, int current, int pageSize) { calculatePagination(); } - // 페이징 계산 로직 분리 + /** + * 페이징 관련 필드를 계산합니다. + * + *

totalPages, hasNext, hasPrevious 값을 설정합니다. + */ private void calculatePagination() { this.totalPages = total > 0 ? (int) Math.ceil((double) total / pageSize) : 0; this.hasNext = current < totalPages; this.hasPrevious = current > 1; } - // 기존 of 메서드 + /** + * PageResult 객체를 생성합니다. + * + * @param data 현재 페이지 데이터 + * @param total 전체 데이터 개수 + * @param current 현재 페이지 번호 + * @param pageSize 페이지 크기 + * @param 데이터 타입 + * @return PageResult 객체 + */ public static PageResult of(List data, int total, int current, int pageSize) { return new PageResult<>(data, total, current, pageSize); } - // PageParams를 받는 of 메서드 + /** + * PageParams를 기반으로 PageResult 객체를 생성합니다. + * + * @param data 현재 페이지 데이터 + * @param total 전체 데이터 개수 + * @param pageParams 요청 파라미터 ({@link PageParams}) + * @param 데이터 타입 + * @return PageResult 객체 + */ public static PageResult of(List data, int total, PageParams pageParams) { return new PageResult<>(data, total, pageParams.getCurrent(), pageParams.getPageSize()); } - // 함수형 인터페이스를 활용한 from 메서드 (트랜잭션 내에서 실행) + /** + * 함수형 인터페이스를 활용해 PageResult를 생성합니다. + * + *

데이터 조회와 카운트 조회를 별도의 Supplier로 받아 트랜잭션 내에서 실행할 수 있습니다. + * + * @param pageParams 요청 파라미터 ({@link PageParams}) + * @param dataSupplier 데이터 조회 함수 + * @param countSupplier 전체 개수 조회 함수 + * @param 데이터 타입 + * @return PageResult 객체 + */ public static PageResult from( PageParams pageParams, Supplier> dataSupplier, Supplier countSupplier) { List data = dataSupplier.get(); @@ -50,27 +131,50 @@ public static PageResult from( return new PageResult<>(data, total, pageParams.getCurrent(), pageParams.getPageSize()); } - // 빈 페이지 결과 생성 + /** + * 비어 있는 페이지 결과를 생성합니다. + * + * @param pageParams 요청 파라미터 ({@link PageParams}) + * @param 데이터 타입 + * @return 빈 PageResult 객체 + */ public static PageResult empty(PageParams pageParams) { return new PageResult<>(List.of(), 0, pageParams.getCurrent(), pageParams.getPageSize()); } - // 빈 페이지 결과 생성 (기본값) + /** + * 기본값(1페이지, 10개)으로 비어 있는 페이지 결과를 생성합니다. + * + * @param 데이터 타입 + * @return 빈 PageResult 객체 + */ public static PageResult empty() { return new PageResult<>(List.of(), 0, 1, 10); } - // 데이터가 있는지 확인 + /** + * 현재 페이지에 데이터가 있는지 확인합니다. + * + * @return 데이터가 존재하면 true + */ public boolean hasData() { return data != null && !data.isEmpty(); } - // 첫 번째 페이지인지 확인 + /** + * 현재 페이지가 첫 번째 페이지인지 확인합니다. + * + * @return 첫 번째 페이지면 true + */ public boolean isFirstPage() { return current == 1; } - // 마지막 페이지인지 확인 + /** + * 현재 페이지가 마지막 페이지인지 확인합니다. + * + * @return 마지막 페이지면 true + */ public boolean isLastPage() { return current == totalPages; } diff --git a/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java b/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java index 25d41d29..da3ae215 100644 --- a/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java +++ b/apps/user-service/src/main/java/site/icebang/common/service/PageableService.java @@ -3,6 +3,42 @@ import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; +/** + * 페이징 가능한 서비스 인터페이스. + * + *

엔티티나 DTO 목록을 페이징 처리하여 반환해야 하는 서비스에서 구현합니다. 공통적으로 {@link PageParams} 요청 파라미터를 받아 {@link + * PageResult} 응답을 제공합니다. + * + *

사용 예시: + * + *

{@code
+ * @Service
+ * public class UserService implements PageableService {
+ *
+ *     private final UserRepository userRepository;
+ *
+ *     @Override
+ *     public PageResult getPagedResult(PageParams pageParams) {
+ *         List users = userRepository.findUsers(
+ *             pageParams.getOffset(), pageParams.getPageSize()
+ *         );
+ *         int total = userRepository.countUsers();
+ *         return PageResult.of(users, total, pageParams);
+ *     }
+ * }
+ * }
+ * + * @param 페이징 처리할 데이터 타입 + * @author jys01012@gmail.com + * @since v0.0.1-alpha + */ public interface PageableService { + + /** + * 페이징 처리된 결과를 반환합니다. + * + * @param pageParams 페이징 및 검색/정렬 요청 파라미터 + * @return 페이징 처리된 결과 객체 + */ PageResult getPagedResult(PageParams pageParams); } diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java index 8243acde..4e34fab7 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -14,9 +14,35 @@ import site.icebang.common.dto.ApiResponse; import site.icebang.common.exception.DuplicateDataException; +/** + * 전역 예외 처리기 (Global Exception Handler). + * + *

이 클래스는 애플리케이션 전역에서 발생하는 예외를 {@link ApiResponse} 형태로 변환하여 클라이언트에게 반환합니다. 예외 유형에 따라 적절한 {@link + * HttpStatus} 코드를 설정하며, 공통적인 예외 처리 로직을 중앙화합니다. + * + *

처리되는 주요 예외는 다음과 같습니다: + * + *

    + *
  • {@link MethodArgumentNotValidException} - 요청 데이터 유효성 검증 실패 + *
  • {@link NoResourceFoundException} - 존재하지 않는 리소스 접근 + *
  • {@link AuthenticationException} - 인증 실패 + *
  • {@link AccessDeniedException} - 인가 실패 + *
  • {@link DuplicateDataException} - 중복 데이터 발생 + *
  • {@link Exception} - 그 외 처리되지 않은 일반 예외 + *
+ * + *

모든 응답은 {@code ApiResponse.error(...)} 메서드를 통해 생성되며, 에러 메시지와 HTTP 상태 코드가 포함됩니다. + */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { + + /** + * 요청 데이터 유효성 검증 실패 시 발생하는 예외를 처리합니다. + * + * @param ex 발생한 {@link MethodArgumentNotValidException} + * @return {@link ApiResponse} - 검증 실패 메시지와 {@link HttpStatus#BAD_REQUEST} + */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse handleValidation(MethodArgumentNotValidException ex) { @@ -24,6 +50,12 @@ public ApiResponse handleValidation(MethodArgumentNotValidException ex) return ApiResponse.error("Validation failed: " + detail, HttpStatus.BAD_REQUEST); } + /** + * 처리되지 않은 모든 일반 예외를 처리합니다. 서버 내부 오류로 간주되며, 에러 로그를 남깁니다. + * + * @param ex 발생한 {@link Exception} + * @return {@link ApiResponse} - 내부 오류 메시지와 {@link HttpStatus#INTERNAL_SERVER_ERROR} + */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse handleGeneric(Exception ex) { @@ -31,24 +63,48 @@ public ApiResponse handleGeneric(Exception ex) { return ApiResponse.error("Internal error: ", HttpStatus.INTERNAL_SERVER_ERROR); } + /** + * 존재하지 않는 리소스 접근 시 발생하는 예외를 처리합니다. + * + * @param ex 발생한 {@link NoResourceFoundException} + * @return {@link ApiResponse} - 리소스 없음 메시지와 {@link HttpStatus#NOT_FOUND} + */ @ExceptionHandler(NoResourceFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiResponse handleNotFound(NoResourceFoundException ex) { return ApiResponse.error("Notfound: " + ex.getMessage(), HttpStatus.NOT_FOUND); } + /** + * 인증 실패 시 발생하는 예외를 처리합니다. + * + * @param ex 발생한 {@link AuthenticationException} + * @return {@link ApiResponse} - 인증 실패 메시지와 {@link HttpStatus#UNAUTHORIZED} + */ @ExceptionHandler(AuthenticationException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ApiResponse handleAuthentication(AuthenticationException ex) { return ApiResponse.error("Authentication failed: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); } + /** + * 인가(권한) 실패 시 발생하는 예외를 처리합니다. + * + * @param ex 발생한 {@link AccessDeniedException} + * @return {@link ApiResponse} - 접근 거부 메시지와 {@link HttpStatus#FORBIDDEN} + */ @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ApiResponse handleAccessDenied(AccessDeniedException ex) { return ApiResponse.error("Access denied: " + ex.getMessage(), HttpStatus.FORBIDDEN); } + /** + * 중복 데이터 발생 시 발생하는 예외를 처리합니다. + * + * @param ex 발생한 {@link DuplicateDataException} + * @return {@link ApiResponse} - 중복 데이터 메시지와 {@link HttpStatus#CONFLICT} + */ @ExceptionHandler(DuplicateDataException.class) @ResponseStatus(HttpStatus.CONFLICT) public ApiResponse handleDuplicateData(DuplicateDataException ex) { diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java index efeffde1..9e6672f3 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAccessDeniedHandler.java @@ -15,11 +15,37 @@ import site.icebang.common.dto.ApiResponse; +/** + * 접근 거부 처리기 (REST 전용 AccessDeniedHandler). + * + *

Spring Security에서 인가(Authorization) 실패 시 호출됩니다. 사용자가 필요한 권한 없이 보호된 리소스에 접근하려고 하면 이 핸들러가 실행되어 + * JSON 형식의 에러 응답을 반환합니다. + * + *

주요 특징: + * + *

    + *
  • HTTP 상태 코드: {@link HttpStatus#FORBIDDEN} (403) + *
  • 응답 본문: {@link ApiResponse} 형식의 에러 메시지 + *
  • 응답 Content-Type: {@code application/json;charset=UTF-8} + *
+ * + *

이 핸들러는 기본 HTML 오류 페이지 대신, REST API 클라이언트에 JSON 기반의 표준 에러 응답을 제공하기 위해 사용됩니다. + */ @Component @RequiredArgsConstructor public class RestAccessDeniedHandler implements AccessDeniedHandler { + + /** JSON 직렬화를 위한 ObjectMapper */ private final ObjectMapper objectMapper; + /** + * 인가되지 않은 요청이 들어왔을 때 실행됩니다. + * + * @param request 현재 요청 + * @param response 응답 객체 + * @param ex 발생한 {@link AccessDeniedException} + * @throws IOException 응답 스트림 처리 중 오류 발생 시 + */ @Override public void handle( HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java index b7c50d76..9d3ec7b5 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/RestAuthenticationEntryPoint.java @@ -15,11 +15,37 @@ import site.icebang.common.dto.ApiResponse; +/** + * 인증 진입점 처리기 (REST 전용 AuthenticationEntryPoint). + * + *

Spring Security에서 인증(Authentication) 실패 시 호출됩니다. 인증되지 않은 사용자가 보호된 리소스에 접근하려고 하면 이 핸들러가 실행되어 + * JSON 형식의 에러 응답을 반환합니다. + * + *

주요 특징: + * + *

    + *
  • HTTP 상태 코드: {@link HttpStatus#UNAUTHORIZED} (401) + *
  • 응답 본문: {@link ApiResponse} 형식의 에러 메시지 + *
  • 응답 Content-Type: {@code application/json;charset=UTF-8} + *
+ * + *

이 핸들러는 기본 로그인 페이지 리다이렉트 대신, REST API 클라이언트에 JSON 기반의 표준 에러 응답을 제공하기 위해 사용됩니다. + */ @Component @RequiredArgsConstructor public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + /** JSON 직렬화를 위한 ObjectMapper */ private final ObjectMapper objectMapper; + /** + * 인증되지 않은 요청이 들어왔을 때 실행됩니다. + * + * @param request 현재 요청 + * @param response 응답 객체 + * @param ex 발생한 {@link AuthenticationException} + * @throws IOException 응답 스트림 처리 중 오류 발생 시 + */ @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)