domain/{domain}/{sub}/api 패키지에 위치하는 Controller, 요청/응답 DTO, Mapper 작성 규칙입니다.
이 프로젝트는 과거 v1(레거시)/v2(표준) API 가 공존했지만, v1 은 전부 제거되어 현재는 v2 단일 버전만 존재합니다. 경로/디렉터리는 향후 확장을 고려해 여전히 api/v2/... 로 버저닝되어 있습니다.
| 경로 prefix | DTO 위치 | 특징 |
|---|---|---|
/api/v2/... |
api/v2/dto/{request,response}/ |
ApiResponse<T> 래퍼 사용. request/response 디렉터리 분리 |
대표 패턴 (예: community/post/api/v2/controller/PostController):
@RestController
@RequiredArgsConstructor
@Tag(name = "Post Public v2", description = "게시글 관련 API")
@RequestMapping("/api/v2/posts")
public class PostController {
private final PostService postService;
private final PostDtoMapper postDtoMapper;
}규칙:
@RestController+@RequestMapping("/api/v2/{리소스명}")조합- 클래스 이름:
{Entity}Controller(예:PostController) - 관리자 전용 Controller 는
{Entity}AdminController패턴이 발견됨 (community/report/api/v2/controller/ReportAdminController등) — 별도 권한 검증 적용 - Swagger 태그:
@Tag(name = "{도메인} Public v2", description = "...") - 의존성은 모두
private final+@RequiredArgsConstructor로 생성자 주입
@PostMapping(value = "/{id}/like")
@ResponseStatus(value = HttpStatus.CREATED)
@Operation(summary = "게시글 좋아요 저장 API", description = "...")
public ApiResponse<Void> likePost(
@PathVariable("id") String id,
@AuthenticationPrincipal CustomUserDetails userDetails) {
this.likePostService.likePost(userDetails.getUser().getId(), id);
return ApiResponse.success();
}규칙:
- 모든 응답은
ApiResponse<T>로 래핑 (성공/실패 모두) - 성공 시
ApiResponse.success(data)또는 데이터가 없을 때ApiResponse.success() - HTTP 상태 코드는
@ResponseStatus로 명시 (기본값 200 에 의존하지 않음) - 인증된 사용자 정보는
@AuthenticationPrincipal CustomUserDetails로 받음 - Swagger 문서를 위해
@Operation(summary = ..., description = ...)추가 - 입력 검증은
@Valid와 Jakarta Bean Validation 어노테이션 활용
이미지 / 파일 업로드는 multipart 로 받습니다.
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<PostCreateResponse> createPost(
@RequestPart("request") @Valid PostCreateRequest request,
@RequestPart(value = "images", required = false) List<MultipartFile> images,
@AuthenticationPrincipal CustomUserDetails userDetails) { ... }- JSON 메타데이터는
request파트, 파일은 별도 파트 - 파일 메타데이터(이미지 순서 등)는 request DTO 내부에
images[].fileIndex식으로 매핑
위치: shared/dto/ApiResponse
{
"code": "S000",
"message": "요청 처리 성공",
"data": { ... }
}- 모든 응답은 이 구조를 따릅니다 (
@JsonInclude(JsonInclude.Include.NON_NULL)로 null 필드는 응답에서 제외). - 성공 코드/메시지는
shared/exception/ResponseCodeenum 의SUCCESS값을 사용 (S000) - 실패:
code는 도메인별*ErrorCodeenum 의 값(예:USER_404_001),message는 사람이 읽을 수 있는 한국어 메시지 - 페이징은
shared/dto/PageResponse활용
예외 응답 흐름: exception.md.
| 종류 | 위치 | 명명 |
|---|---|---|
| Request DTO | api/v2/dto/request/ |
{Action}{Entity}Request (예: PostCreateRequest) |
| Response DTO | api/v2/dto/response/ |
{Entity}Response, {Action}{Entity}Response (예: PostResponse, PostCreateResponse) |
| Service 계층 DTO | service/dto/ |
{Entity}{Action}Query, {Entity}{Action}Result, {Entity}{Action}Command |
신규 코드 작성 시:
.gemini/styleguide.mdRule 68 — "불변 DTO 는record를 기본으로 사용한다" 를 따릅니다. 신규 Request/Response/Service DTO 는 record 를 우선 검토하세요.
신규 권장: record
public record PostCreateRequest(
@NotBlank String content,
@NotNull String boardId,
Boolean isAnonymous,
List<ImageMetadata> images
) {}신규 코드에서 record 가 채택되고 있는 위치는 주로 community/report 의 service Command/Result DTO (PostReportCreateCommand, PostReportCreateResult 등) 와 일부 도메인 이벤트.
현재 코드 다수의 패턴: Lombok class
대부분의 기존 DTO 는 아직 Lombok 기반입니다. 기존 코드를 수정할 때는 일관성을 위해 같은 패턴을 유지하되, 신규 추가는 가능한 한 record 로 작성합니다.
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class PostCreateRequest {
@NotBlank
private String content;
@NotNull
private String boardId;
private Boolean isAnonymous;
private List<ImageMetadata> images;
}- 검증:
@NotNull,@NotBlank,@Size,@Pattern등 Jakarta validation - 기본 생성자는
PROTECTED또는PRIVATE로 외부 직접 생성 차단 @Builder활용.@AllArgsConstructor는PRIVATE명시- DTO 내부 nested 클래스는 static class 로 정의
위치: api/v2/mapper/, 명명: {Entity}DtoMapper
@Mapper(componentModel = "spring")
public interface PostDtoMapper {
PostCreateCommand toCreateCommand(PostCreateRequest request, String writerId);
@Mapping(target = "boardName", source = "board.name")
PostResponse toResponse(PostDetailResult result);
}규칙:
@Mapper(componentModel = "spring")으로 Spring Bean 등록- Controller ↔ Service 사이 변환은 Mapper 가 책임
- 복잡한 변환은
default메서드 또는@AfterMapping
MapStruct 1.4.x 는 최신 기능 일부가 빠져 있으니, 새 어노테이션을 도입하기 전에 컴파일 가능 여부 확인이 필요합니다.
메서드 시큐리티가 활성화되어 있어 (WebSecurityConfig 의 @EnableMethodSecurity) Controller 메서드에 직접 사용 가능합니다.
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/...")
public ApiResponse<...> adminApi(...) { ... }- 역할(Role):
domain/user/account/enums/user/Roleenum 의 값들 (예:ADMIN,PRESIDENT,VICE_PRESIDENT,LEADER_*,COMMON) - 역할 그룹:
RoleGroupenum - 가능한 한 Controller 메서드에 명시 (서비스 레벨 검증은 별도 Validator 패턴 활용)
@AuthenticationPrincipal CustomUserDetails userDetails
User user = userDetails.getUser();위치: domain/user/auth/userdetails/CustomUserDetails
Controller 에서는 별도 예외를 잡지 않습니다. 모든 예외는 shared/exception/GlobalV2ExceptionHandler (단일 글로벌 핸들러) 가 통합 처리.
@Valid위반 →MethodArgumentNotValidException→ 400 BAD_REQUEST (GlobalErrorCode.BAD_REQUEST)- 비즈니스 예외 →
*ErrorCode.toBaseException()throw → Handler 가 적절한 HTTP status 로 변환
자세한 예외 처리 흐름: exception.md.
springdoc-openapi-starter-webmvc-ui사용- 클래스 레벨:
@Tag(name, description) - 메서드 레벨:
@Operation(summary, description) - 응답 스키마:
@Schema(description, example)(DTO 필드에 추가) - 보안:
core/security/SwaggerSecurityConfig가 Swagger 경로 접근 권한 처리
-
api/v2/controller/{Entity}Controller생성,/api/v2/{리소스}매핑 -
@Tag+@Operation추가 - 요청 DTO 는
api/v2/dto/request/, 응답 DTO 는api/v2/dto/response/ - 인증 필요 시
@AuthenticationPrincipal CustomUserDetails활용 - 역할 기반 인가는
@PreAuthorize - 응답은
ApiResponse.success(...)또는ApiResponse.success()래핑 -
@ResponseStatus명시 - 입력 검증은
@Valid+ Bean Validation 어노테이션 - Mapper(
{Entity}DtoMapper) 통해 Service 계층 DTO 로 변환 후 호출