이 글에서는 인터페이스를 사용해 Controller에 포함된 Swagger 코드를 줄인 내용과 불필요한 응답 및 정확한 행위 표현을 위한 HTTP 상태 코드 적용에 대한 내용을 다룹니다.
1. Controller - Swagger interface 분리
전시로그는 API 문서화를 위해 Swagger를 사용하고 있습니다. Swagger는 코드에 작성하면 문서가 자동으로 만들어진다는 장점이 있지만, 그만큼 코드가 지저분해진다는 단점이 있습니다. 따라서 코드 가독성을 위해 이를 정리하고자 Controller에 있던 Swagger 코드를 Interface로 분리하여 구현하는 방식으로 변경하기로 했습니다.
[변경 전]
InterestController.java
@Tag(name = "Interest API", description = "Interest 관련 API입니다.")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/interest")
public class InterestController {
private final InterestService interestService;
@Operation(summary = "즐겨찾기 등록", description = "전시회 id를 이용하여 즐겨찾기를 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "등록 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = InterestResponseDto.InterestRes.class))}),
@ApiResponse(responseCode = "400", description = "등록 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@PostMapping("/{exhibitionId}")
public ResponseEntity<?> registerInterest(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "전시회 id를 입력해주세요.", required = true) @PathVariable Long exhibitionId
) {
return interestService.registerInterest(userPrincipal, exhibitionId);
}
@Operation(summary = "즐겨찾기 해제", description = "전시회 id를 이용하여 즐겨찾기를 해제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "해제 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))}),
@ApiResponse(responseCode = "400", description = "해제 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@DeleteMapping("/{exhibitionId}")
public ResponseEntity<?> deleteInterest(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "전시회 id를 입력해주세요.", required = true) @PathVariable Long exhibitionId
) {
return interestService.deleteInterest(userPrincipal, exhibitionId);
}
@Operation(summary = "즐겨찾기 목록 조회", description = "Access Token을 이용하여 즐겨찾기 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = InterestResponseDto.InterestListResWithPaging.class))}),
@ApiResponse(responseCode = "400", description = "조회 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@GetMapping
public ResponseEntity<?> deleteInterest(
@Parameter(description = "즐겨찾기 목록을 페이지별로 조회합니다. **Page는 0부터 시작합니다!**", required = true) @RequestParam(value = "page") Integer page,
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal
) {
return interestService.getInterestList(page, userPrincipal);
}
}
Swagger를 위한 @Tag, @Operation, @ApiResponses와 같은 어노테이션과 각 API의 추가 설명이 Controller 코드에 포함되어 있습니다. 함수와 변수, 매개변수 모두가 한 눈에 잘 읽히지 않는 것이 불편했습니다.
[변경 후]
InterestController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/interest")
public class InterestController implements InterestApi {
private final InterestService interestService;
@PostMapping("/{exhibitionId}")
public ResponseEntity<?> registerInterest(
@CurrentUser UserPrincipal userPrincipal,
@PathVariable Long exhibitionId
) {
return interestService.registerInterest(userPrincipal, exhibitionId);
}
@DeleteMapping("/{exhibitionId}")
public ResponseEntity<Void> deleteInterest(
@CurrentUser UserPrincipal userPrincipal,
@PathVariable Long exhibitionId
) {
interestService.deleteInterest(userPrincipal, exhibitionId);
return ResponseEntity.noContent().build();
}
@GetMapping
public ResponseEntity<?> deleteInterest(
@CurrentUser UserPrincipal userPrincipal,
@RequestParam(value = "page") Integer page
) {
return interestService.getInterestList(page, userPrincipal);
}
}
Swagger 어노테이션들을 제거하고, 필요한 코드만 남겨두어 클래스 전체의 길이가 짧아지고 가독성이 높아졌습니다.
달라진 점은 InterestApi를 implements하고 있다는 점인데, InterestApi에 Swagger 코드를 넣어놓고 구현하여 사용하고 있습니다.
InterestApi.java
@Tag(name = "Interest API", description = "Interest 관련 API입니다.")
public interface InterestApi {
@Operation(summary = "즐겨찾기 등록", description = "전시회 id를 이용하여 즐겨찾기를 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "등록 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = InterestResponseDto.InterestRes.class))}),
@ApiResponse(responseCode = "400", description = "등록 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@PostMapping("/{exhibitionId}")
ResponseEntity<?> registerInterest(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "전시회 id를 입력해주세요.", required = true) @PathVariable Long exhibitionId
);
@Operation(summary = "즐겨찾기 해제", description = "전시회 id를 이용하여 즐겨찾기를 해제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "해제 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Void.class))}),
@ApiResponse(responseCode = "400", description = "해제 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@DeleteMapping("/{exhibitionId}")
ResponseEntity<Void> deleteInterest(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "전시회 id를 입력해주세요.", required = true) @PathVariable Long exhibitionId
);
@Operation(summary = "즐겨찾기 목록 조회", description = "Access Token을 이용하여 즐겨찾기 목록을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = InterestResponseDto.InterestListResWithPaging.class))}),
@ApiResponse(responseCode = "400", description = "조회 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@GetMapping
ResponseEntity<?> deleteInterest(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "즐겨찾기 목록을 페이지별로 조회합니다. **Page는 0부터 시작합니다!**", required = true) @RequestParam(value = "page") Integer page
);
}
2. 응답 성공 HTTP 상태 코드 반영
HTTP 상태 코드는 100번대부터 500번대까지 다양하게 존재합니다. 100번대는 정보, 200번대는 성공, 300번대는 리다이렉션, 400번대는 클라이언트 에러, 500번대는 서버 에러 응답을 위해 사용됩니다.
기존 전시로그는 200 ok로 모든 성공 응답을 반환했으며, 일부 생성, 수정, 삭제에서 응답 값이 필요하지 않은 경우에도 Message를 생성하여 응답에 포함했습니다. 하지만, 안드로이드 팀원들도 Message를 사용하지 않는다고 했고, 코드를 짤 때 마음대로 작성하는 메세지는 불필요함이 컸기 때문에 없애는 방향으로 결정했습니다.
이를 위해 응답 성공 시 사용되는 200번대 HTTP 상태 코드 중 200 OK, 201 Created, 204 No-Content를 활용하여 불필요한 Message 응답을 줄이고, 상태 코드로 행위의 결과를 나타낼 수 있도록 변경하기로 했습니다.
[변경 전]
CalendarService.java
@Transactional
public ResponseEntity<?> uploadPoster(UserPrincipal userPrincipal, CalendarRequestDto.UploadPosterReq uploadPosterReq) {
User findUser = userService.validateUserByToken(userPrincipal);
Optional<Calendar> checkCalendar = calendarRepository.findByUserAndPhotoDate(findUser, uploadPosterReq.getDate());
if (checkCalendar.isPresent()) {
checkCalendar.get().updateImage(uploadPosterReq.getImgUrl());
} else {
Calendar calendar = CalendarConverter.toCalendar(findUser, uploadPosterReq.getDate(), uploadPosterReq.getImgUrl(), uploadPosterReq.getCaption());
calendarRepository.save(calendar);
}
ApiResponse apiResponse = ApiResponse.toApiResponse(
Message.builder().message("포토캘린더에 이미지를 업로드했습니다.").build());
return ResponseEntity.ok(apiResponse);
}
@Transactional
public ResponseEntity<?> deleteCalendar(UserPrincipal userPrincipal, CalendarRequestDto.UploadImageReq deleteImageReq) {
User findUser = userService.validateUserByToken(userPrincipal);
Optional<Calendar> findCalendar = calendarRepository.findByUserAndPhotoDate(findUser, deleteImageReq.getDate());
DefaultAssert.isTrue(findCalendar.isPresent(), "해당 날짜에 이미지가 없습니다.");
Calendar calendar = findCalendar.get();
s3Uploader.deleteImage(DIRNAME, calendar.getImageUrl());
calendarRepository.delete(calendar);
ApiResponse apiResponse = ApiResponse.toApiResponse(
Message.builder().message("이미지를 삭제했습니다.").build());
return ResponseEntity.ok(apiResponse);
}
CalendarController.java
@Operation(summary = "전시회 포스터에서 이미지 업로드", description = "전시회 포스터를 불러와서 업로드 합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "업로드 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))}),
@ApiResponse(responseCode = "400", description = "업로드 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@PostMapping(value = "/exhibition")
public ResponseEntity<?> uploadPoster(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "Schemas의 UploadPosterReq 를 확인해주세요.", required = true) @RequestBody CalendarRequestDto.UploadPosterReq uploadPosterReq
) {
return calendarService.uploadPoster(userPrincipal, uploadPosterReq);
}
@Operation(summary = "이미지 삭제", description = "해당 날짜의 이미지를 삭제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))}),
@ApiResponse(responseCode = "400", description = "삭제 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))}),
})
@DeleteMapping
public ResponseEntity<?> deleteCalendar(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "Schemas의 UploadImageReq 를 확인해주세요.", required = true) @RequestBody CalendarRequestDto.UploadImageReq deleteImageReq
) {
return calendarService.deleteCalendar(userPrincipal, deleteImageReq);
}
기존 Controller에서는 서비스에서 내려주는 Message를 포함한 ResponseEntity를 그대로 반환했습니다.
[변경 후]
CalendarService.java
@Transactional
public void uploadPoster(UserPrincipal userPrincipal, CalendarRequestDto.UploadPosterReq uploadPosterReq) {
User findUser = userService.validateUserByToken(userPrincipal);
Optional<Calendar> checkCalendar = calendarRepository.findByUserAndPhotoDate(findUser, uploadPosterReq.getDate());
if (checkCalendar.isPresent()) {
checkCalendar.get().updateImage(uploadPosterReq.getImgUrl());
} else {
Calendar calendar = CalendarConverter.toCalendar(findUser, uploadPosterReq.getDate(), uploadPosterReq.getImgUrl(), uploadPosterReq.getCaption());
calendarRepository.save(calendar);
}
}
@Transactional
public void deleteCalendar(UserPrincipal userPrincipal, CalendarRequestDto.UploadImageReq deleteImageReq) {
User findUser = userService.validateUserByToken(userPrincipal);
Optional<Calendar> findCalendar = calendarRepository.findByUserAndPhotoDate(findUser, deleteImageReq.getDate());
DefaultAssert.isTrue(findCalendar.isPresent(), "해당 날짜에 이미지가 없습니다.");
Calendar calendar = findCalendar.get();
s3Uploader.deleteImage(DIRNAME, calendar.getImageUrl());
calendarRepository.delete(calendar);
}
CalendarController.java
@PostMapping(value = "/exhibition")
public ResponseEntity<Void> uploadPoster(
@CurrentUser UserPrincipal userPrincipal,
@RequestBody CalendarRequestDto.UploadPosterReq uploadPosterReq
) {
calendarService.uploadPoster(userPrincipal, uploadPosterReq);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@DeleteMapping
public ResponseEntity<Void> deleteCalendar(
@CurrentUser UserPrincipal userPrincipal,
@RequestBody CalendarRequestDto.UploadImageReq deleteImageReq
) {
calendarService.deleteCalendar(userPrincipal, deleteImageReq);
return ResponseEntity.noContent().build();
}
.created()는 URL를 반드시 포함해야 되는데, 필요하지 않은 경우에는 위와 같이 status로 상태 코드를 입력하고 build()하여 응답합니다.
Service에서 불필요한 코드가 줄어들었으며, 필요한 응답만을 전송하여 상태 코드를 통해 행위가 성공했는지, 해야 할 일을 정할 수 있게 되었습니다.
'Project' 카테고리의 다른 글
| Bulk 연산으로 Write 작업 개선하기 (0) | 2025.03.13 |
|---|---|
| 외부 API 호출 시 고려할 것 (0) | 2025.03.10 |
| 배치 프로세스 리팩토링 (0) | 2025.03.03 |
| 효과적인 파일 업로드 방법 (0) | 2025.01.19 |
| 파일 업로드 로직 변경 (0) | 2024.11.20 |