
์ด๋ฒ ๊ฒ์๊ธ์์ "Controller ๋ ์ด์ด์ ๋ฌธ์ํ (Swagger / OpenAPI) ์ด๋ ธํ ์ด์ ๋ถ๋ฆฌ ์ ๋ต"์ ๋ํด์ ๋งํ๋ ค๊ณ ํ๋ค.
์ปจํธ๋กค๋ฌ ํ์ผ์ @Operation, @Parameter, @ApiResponses ๋ฑ Swagger ๋ฌธ์์ฉ ์ด๋
ธํ
์ด์
์ด ๋ง์์ง๋ฉด,
๐ ๋น์ฆ๋์ค ๋ก์ง ์ฝ๋๋ณด๋ค ๋ฌธ์์ฉ ์ด๋
ธํ
์ด์
์ฝ๋๊ฐ ๋ ๋ง์์ง โ ๊ฐ๋
์ฑ์ด ๋จ์ด์ง
์ธํฐํ์ด์ค๋ก ๋ฌธ์ํ๋ฅผ ๋ฐ๋ก ๋ถ๋ฆฌํ๋ค๋ฉด???
์ปจํธ๋กค๋ฌ ํด๋์ค๋ โ ๋น์ฆ๋์ค ์ฒ๋ฆฌ์ ๋ผ์ฐํ
๋ด๋น
์ธํฐํ์ด์ค ํ์ผ์ โ API ๋ช
์ธ ๋ด๋น (Swagger์ฉ ์ด๋
ธํ
์ด์
๋ง ๋ชจ์)
๊ฐ์ API ๋ช ์ธ๋ฅผ ์ฌ๋ฌ ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅ (ex: BaseApi, CommonApi, AdminApi ๋ฑ)
ํ ์คํธ์์๋ interface ๊ธฐ๋ฐ์ผ๋ก ๋ฌธ์ ์๋ํ๋ฅผ ์ฝ๊ฒ ํ ์ ์๋ค.
API Spec ๋ณ๊ฒฝ์ โ Interface ํ์ผ๋ง ์์ ํ๋ฉด ๋จ
Controller ์ฝ๋์๋ ์ํฅ์ด ์ต์ํ๋์ด ์ ์ง๋ณด์์ ์ข๋ค!!
@SecurityRequirement(name = "bearerAuth")
@Tag(name = "Answer", description = "๋ต๋ณ ๊ด๋ฆฌ API")
public interface AnswerApi {
@Operation(summary = "๋ต๋ณ ์์ฑ", description = "์๋ก์ด ๋ต๋ณ์ ์์ฑํฉ๋๋ค.")
@PostMapping
ResponseEntity<AnswerDetailResponse> createAnswer(...);
@Operation(summary = "๋ต๋ณ ์ญ์ ", description = "๋ต๋ณ์ ์ญ์ ํฉ๋๋ค.")
@DeleteMapping("/{answerId}")
ResponseEntity<Void> deleteAnswer(@PathVariable Long answerId);
// ๊ธฐํ API...
}
๐ ๊ตฌํ์ ์์, API ์๊ทธ๋์ฒ + ๋ฌธ์ํ ์ด๋ ธํ ์ด์ ๋ง ์กด์ฌ
@RestController
@RequestMapping("/api/answers")
@RequiredArgsConstructor
public class AnswerController implements AnswerApi {
private final AnswerService answerService;
@Override
public ResponseEntity<AnswerDetailResponse> createAnswer(...) {
// ์๋น์ค ํธ์ถ
}
@Override
public ResponseEntity<Void> deleteAnswer(Long answerId) {
// ์๋น์ค ํธ์ถ
}
}
๐ implements AnswerApi โ ์ธํฐํ์ด์ค์์ ์ ์ํ ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋
๐ @RestController โ ์ค์ง์ ์ธ ์์ฒญ ์ฒ๋ฆฌ ๋ด๋น
๊ธฐ์กด ๋ฐฉ์ (@Operation ์ง์ ์ปจํธ๋กค๋ฌ์ ์์ฑ) | Interface ๋ถ๋ฆฌ ๋ฐฉ์ |
|---|---|
| ์ปจํธ๋กค๋ฌ ํ์ผ ๋ณต์กํด์ง | ์ปจํธ๋กค๋ฌ๋ ๋ก์ง๋ง ๋ด๋น, ๋ฌธ์๋ ๋ฐ๋ก ๊ด๋ฆฌ |
| ๋ฌธ์ ๋ณ๊ฒฝ ์ Controller๋ ์์ ํ์ | Interface๋ง ์์ |
| ํ ์คํธ / ๋ฆฌํฉํ ๋ง ์ ์ํฅ ์์ | ์ฑ ์ ๋ถ๋ฆฌ๋ก ์์ ์ |
| ๋ฌธ์์ ๋ก์ง์ด ์์ฌ ์์ | ๋ฆฌ๋ทฐ ์ ๋ก์ง๊ณผ ๋ฌธ์ ๋ถ๋ฆฌ๋ก ํธ์์ฑ โ |
@SecurityRequirement(name = "bearerAuth")
@Parameter(
in = ParameterIn.HEADER,
name = "Authorization", required = true,
schema = @Schema(type = "string"),
description = "Bearer [Access ํ ํฐ]"
)
public interface BaseApi {
}
๐ ๋๋ถ๋ถ ์๋น์ค์์๋ ๋ก๊ทธ์ธ์ ํตํ ์ธ์ฆ / ์ธ๊ฐ๊ฐ ์๊ธฐ ๋๋ฌธ์ ๊ณตํต๋ ํ ํฐ์ธ์ฆ๊ณผ ๊ด๋ จ๋ ๋ถ๋ถ์ ์์ ๊ฐ์ด BaseApi๋ก ๋ง๋ค์ด๋๋ค.
๊ทธ๋ฌ๋ฉด ํ์ Api์์ ๋ฐ๋ณต ์์ฑํ๋ Authorization ํค๋/๋ณด์ ์ค์ ์ ๊ฑฐ ๊ฐ๋ฅ!
@Tag(name = "Answer", description = "๋ต๋ณ ๊ด๋ฆฌ API")
public interface AnswerApi extends BaseApi {
@Operation(summary = "๋ต๋ณ ์์ฑ", description = "์๋ก์ด ๋ต๋ณ์ ์์ฑํฉ๋๋ค.")
@ApiResponse(responseCode = "201", description = "๋ต๋ณ ์์ฑ ์ฑ๊ณต",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = AnswerDetailResponse.class)))
@PostMapping(value = "/members/{memberId}/answers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<ApiResponse<AnswerDetailResponse>> createAnswer(
@PathVariable Long memberId,
@RequestPart(value = "imageFiles") MultipartFile imageFile,
@RequestPart(name = "request") @Valid AnswerCreateRequest request);
@Operation(summary = "๋ต๋ณ ์กฐํ", description = "๋ชจ๋ ๋ต๋ณ์ ํ์ด์ง๋ค์ด์
์ผ๋ก ์กฐํํฉ๋๋ค.")
@ApiResponse(responseCode = "200", description = "๋ต๋ณ ์กฐํ ์ฑ๊ณต",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Page.class)))
@GetMapping("/members/{memberId}/answers")
ResponseEntity<ApiResponse<Page<AnswerDetailResponse>>> getAllAnswers(
@PathVariable Long memberId,
@RequestParam(required = false) Long category,
Pageable pageable);
...
}
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "๋ต๋ณ ์์ฑ ์ฑ๊ณต",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = AnswerDetailResponse.class))),
@ApiResponse(responseCode = "400", description = "์๋ชป๋ ์์ฒญ",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class))),
@ApiResponse(responseCode = "401", description = "์ธ์ฆ ์คํจ"),
@ApiResponse(responseCode = "500", description = "์๋ฒ ์ค๋ฅ")
})
@AutoConfigureMockMvc
@SpringBootTest
@AutoConfigureRestDocs
class AnswerControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("๋ต๋ณ ์์ฑ API ํ
์คํธ")
void createAnswerTest() throws Exception {
MockMultipartFile file = new MockMultipartFile("imageFiles", "test.png", "image/png", "test".getBytes());
MockMultipartFile request = new MockMultipartFile("request", "", "application/json",
"{\"title\":\"ํ
์คํธ ์ ๋ชฉ\", \"content\":\"ํ
์คํธ ๋ด์ฉ\"}".getBytes());
mockMvc.perform(multipart("/api/answers")
.file(file)
.file(request)
.header("Authorization", "Bearer dummy_token"))
.andExpect(status().isCreated())
.andDo(document("answer-create",
requestHeaders(
headerWithName("Authorization").description("Access Token")
),
requestParts(
partWithName("imageFiles").description("์
๋ก๋ ํ์ผ"),
partWithName("request").description("๋ต๋ณ ์์ฑ ์์ฒญ ๋ฐ์ดํฐ")
),
responseFields(
fieldWithPath("id").description("์์ฑ๋ ๋ต๋ณ ID"),
fieldWithPath("title").description("๋ต๋ณ ์ ๋ชฉ"),
fieldWithPath("content").description("๋ต๋ณ ๋ด์ฉ")
)
));
}
}
์์ ๊ฐ์ด Mock์ ์ฌ์ฉํ์ฌ ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
๊ฐ๋ฐ์ ์งํํ๊ณ ํ์ต์ ์งํํ ์๋ก ํ
์คํธ ์ฝ๋์ ์ค์์ฑ์ ๋ํ์ฌ ๋๋ผ๊ณ ์๋ค.
์์ผ๋ก ํ
์คํธ ์ฝ๋์ ๋ํ ํ์ต์ ์งํ ํ ์ปจํธ๋กค๋ฌ ๊ธฐ๋ฐ, ๋ฉ์๋ ๊ธฐ๋ฐ ๋ฑ ์ธ๋ถ ํ
์คํธ ์ฝ๋ ์์ฑ์ ์ ์ฉํด ๋ณผ ๊ฒ์ด๋ค.
์ปจํธ๋กค๋ฌ์ ๋ฌธ์ํ ์ค๊ณ ์ต์ ํ ๊ตฌ์กฐ๋
๐ "๋ฌธ์ํ์ ๋น์ฆ๋์ค ๋ก์ง ๋ถ๋ฆฌ" ์ ๋ต์ ์ ์ง๋ณด์์ ํ ํ์ ์๋ ํฐ ๋์์ด ๋ ๊ฑฐ๋ผ ์๊ฐํ๋ฉฐ ์ฝ๋ ๊ฐ๋ ์ฑ, ์ ์ง๋ณด์์ฑ, ํ ์คํธ ์ฉ์ด์ฑ ๋ชจ๋์ ๋์์ด ๋๋ฏ๋ก ๋ค์ ํ๋ก์ ํธ์์ ์ง์ ์ ์ฉํด๋ณผ ๊ณํ์ด๋ค!!