1. Swagger
- Annotation을 이용해서 코드를 문서화시킬 수 있는 라이브러리
- build.gradle
- commons-lang3 버전도 호환될 수 있도록 일치
dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
}
- UserRequest
- Schema (컬럼 설명) 추가
public class UserRequest {
@Data
public static class UpdateDTO {
@Schema(description = "비밀번호 (4~20자)", example = "1234")
@Size(min = 4, max = 20)
private String password;
@Schema(description = "이메일 주소", example = "user@example.com")
@Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요")
private String email;
}
@Data
public static class JoinDTO {
@Schema(description = "유저네임 (2~20자, 특수문자/한글 불가)", example = "metacoding")
@Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다")
private String username;
@Schema(description = "비밀번호 (4~20자)", example = "1234")
@Size(min = 4, max = 20)
private String password;
@Schema(description = "이메일 주소", example = "user@example.com")
@Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요")
private String email;
public User toEntity() {
return User.builder()
.username(username)
.password(password)
.email(email)
.build();
}
}
@Data
public static class LoginDTO {
@Schema(description = "유저네임 (2~20자)", example = "metacoding")
@Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다")
private String username;
@Schema(description = "비밀번호 (4~20자)", example = "1234")
@Size(min = 4, max = 20)
private String password;
@Schema(description = "자동 로그인 여부 (체크시 'on')", example = "on", nullable = true)
private String rememberMe; // check되면 on, 안되면 null
}
}
- UserResponse
- Schema(컬럼 설명) 추가
public class UserResponse {
@Data
public static class TokenDTO {
@Schema(description = "엑세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI...")
private String accessToken;
@Schema(description = "리프레시 토큰", example = "dGhpc0lzUmVmcmVzaFRva2Vu")
private String refreshToken;
@Builder
public TokenDTO(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
@Data
public static class DTO {
@Schema(description = "유저 ID", example = "1")
private Integer id;
@Schema(description = "유저 이름", example = "cos")
private String username;
@Schema(description = "이메일 주소", example = "cos@nate.com")
private String email;
@Schema(description = "생성일시", example = "2024-05-16T10:00:00")
private String createdAt;
public DTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
this.createdAt = user.getCreatedAt().toString();
}
}
}
- UserController
- Tag : 해당 컨트롤러 기능 설명
- Operation : 해당 메서드 기능 설명
- APIResponse : 리턴되는 값 매핑 및 구조 설명 (Resp에 묶여있어서 자동 매핑이 불가)
@Slf4j
@Tag(name = "User API", description = "회원가입, 로그인, 회원정보 수정 등 사용자 관련 API")
@RequiredArgsConstructor
@RestController
public class UserController {
private final UserService userService;
private final HttpSession session;
@Operation(summary = "회원정보 수정", description = "로그인한 사용자의 비밀번호와 이메일을 수정합니다.")
@ApiResponse(responseCode = "200", description = "회원정보 수정 성공",
content = @Content(schema = @Schema(implementation = UserResponse.DTO.class)))
@PutMapping("/s/api/user")
public ResponseEntity<?> update(@Valid @RequestBody UserRequest.UpdateDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
UserResponse.DTO respDTO = userService.회원정보수정(reqDTO, sessionUser.getId());
return Resp.ok(respDTO);
}
@Operation(summary = "유저네임 중복체크", description = "해당 유저네임이 이미 사용 중인지 확인합니다.")
@ApiResponse(responseCode = "200", description = "중복 여부 반환",
content = @Content(schema = @Schema(implementation = Map.class)))
@GetMapping("/api/check-username-available/{username}")
public ResponseEntity<?> checkUsernameAvailable(
@Parameter(description = "확인할 유저네임", example = "metacoding") @PathVariable("username") String username) {
Map<String, Object> respDTO = userService.유저네임중복체크(username);
return Resp.ok(respDTO);
}
@Operation(summary = "회원가입", description = "유저네임, 비밀번호, 이메일을 받아 회원가입을 진행합니다.")
@ApiResponse(responseCode = "200", description = "회원가입 성공",
content = @Content(schema = @Schema(implementation = UserResponse.DTO.class)))
@PostMapping("/join")
public ResponseEntity<?> join(
@Valid @RequestBody UserRequest.JoinDTO reqDTO,
Errors errors,
HttpServletResponse response,
HttpServletRequest request) {
log.debug(reqDTO.toString());
log.trace("트레이스ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ");
log.debug("디버그---------");
log.info("인포ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ");
log.warn("워닝ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ");
log.error("에러ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ");
String hello = request.getHeader("X-Key");
System.out.println("X-good : " + hello);
response.setHeader("Authorization", "jooho");
UserResponse.DTO respDTO = userService.회원가입(reqDTO);
return Resp.ok(respDTO);
}
@Operation(summary = "로그인", description = "유저네임과 비밀번호를 이용하여 로그인합니다.")
@ApiResponse(responseCode = "200", description = "로그인 성공",
content = @Content(schema = @Schema(implementation = UserResponse.TokenDTO.class)))
@PostMapping("/login")
public ResponseEntity<?> login(
@Valid @RequestBody UserRequest.LoginDTO loginDTO,
Errors errors,
HttpServletResponse response) {
UserResponse.TokenDTO respDTO = userService.로그인(loginDTO);
return Resp.ok(respDTO);
}
}
결과 확인
- localhost:8080/swagger-ui/index.html 접속

- 해당 기능 설명 및 Request, Response 정보 추가

장점
- 간편하게 설정 가능
- 별도의 추가 설정이 필요 없음
단점
- 코드의 가독성을 많이 해침
- example로 값을 적기 때문에 예시 값의 신뢰성이 떨어짐
2. REST Docs
- 테스트 코드에 @AutoConfigureRestDocs 어노테이션을 추가하여 문서를 만들 수 있는 라이브러리
- build.gradle
- commons-lang3 버전도 호환될 수 있도록 일치
- resource 폴더 내에 static/docs 폴더 생성 후 adoc 파일 하나 만들어둘 것 (html 변환용)
plugins {
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
}
ext {
snippetsDir = file('build/generated-snippets')
}
test {
useJUnitPlatform()
outputs.dir snippetsDir
systemProperty 'file.encoding', 'UTF-8'
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
outputDir = file("${buildDir}/docs/asciidoc")
}
task copyDocs(type: Copy) {
dependsOn asciidoctor
from("${asciidoctor.outputDir}")
into("src/main/resources/static/docs")
}
- api.adoc
- resource/static/docs 안에 미리 만들어둘 것
- :별칭: → 폴더명
- include::{snippets}/{별칭}/메서드명/http-request.adoc[]
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]
:user: user-controller-test
:board: board-controller-test
:reply: reply-controller-test
:love: love-controller-test
= REST API
:toc: left
:toclevels: 2
:source-highlighter: highlightjs
== 회원
=== 1. 회원가입 유저네임 중복 실패
===== Request Example
include::{snippets}/{user}/join_username_uk_fail_test/http-request.adoc[]
===== Response Example
include::{snippets}/{user}/join_username_uk_fail_test/http-response.adoc[]
=== 2. 회원가입
===== Request Example
include::{snippets}/{user}/join_test/http-request.adoc[]
===== Response Example
include::{snippets}/{user}/join_test/http-response.adoc[]
=== 3. 로그인
===== Request Example
include::{snippets}/{user}/login_test/http-request.adoc[]
===== Response Example
include::{snippets}/{user}/login_test/http-response.adoc[]
=== 4. 회원정보수정
===== Request Example
include::{snippets}/{user}/update_test/http-request.adoc[]
===== Response Example
include::{snippets}/{user}/update_test/http-response.adoc[]
=== 4. 회원정보수정
===== Request Example
include::{snippets}/{user}/update_test/http-request.adoc[]
===== Response Example
include::{snippets}/{user}/update_test/http-response.adoc[]
=== 5. 유저네임중복확인
===== Request Example
include::{snippets}/{user}/check_username_available_test/http-request.adoc[]
===== Response Example
include::{snippets}/{user}/check_username_available_test/http-response.adoc[]
== 게시글
=== 1. 게시글등록
===== Request Example
include::{snippets}/{board}/list_test/http-request.adoc[]
===== Response Example
include::{snippets}/{board}/list_test/http-response.adoc[]
- MyRestDoc : 문서화용 추상 메서드
- Autowired를 통해 기존의 필터를 사용 가능
- AutoConfigureRestDocs : mvc에서 uri 정보를 추가하여 REST Docs 관련 설정을 자동 구성
@AutoConfigureRestDocs(uriScheme = "http", uriHost = "localhost", uriPort = 8080)
@AutoConfigureMockMvc
@ExtendWith({SpringExtension.class, RestDocumentationExtension.class})
public abstract class MyRestDoc {
@Autowired
protected MockMvc mvc;
protected RestDocumentationResultHandler document;
@BeforeEach
public void documentSetUp() {
this.document = MockMvcRestDocumentation.document("{class-name}/{method-name}",
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(Preprocessors.prettyPrint()));
}
}
- Test 클래스 : 문서화 기능 추가
- 중복되는 Annotation 삭제
- action.andDo 메서드로 문서화 기능 추가 (*.adoc 파일 생성)
// 컨트롤러 통합 테스트
@Transactional
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK -> 가짜 환경을 만들어 필요한 의존관계를 다 메모리에 올려서 테스트
public class UserControllerTest extends MyRestDoc {
@Autowired
private ObjectMapper om; // json <-> java Object 변환 해주는 객체. IoC에 objectMapper가 이미 떠있음
private String accessToken;
@BeforeEach
public void setUp() {
// 테스트 시작 전에 실행할 코드
System.out.println("setUp");
User ssar = User.builder()
.id(1)
.username("ssar")
.build();
accessToken = JwtUtil.create(ssar);
}
@AfterEach
public void tearDown() { // 끝나고 나서 마무리 함수
// 테스트 후 정리할 코드
System.out.println("tearDown");
}
@Test
public void join_username_uk_fail_test() throws Exception { // 이 메서드를 호출한 주체에게 예외 위임 -> 지금은 jvm 이다
// given -> 가짜 데이터
UserRequest.JoinDTO reqDTO = new UserRequest.JoinDTO();
reqDTO.setEmail("ssar@nate.com");
reqDTO.setPassword("1234");
reqDTO.setUsername("ssar");
String requestBody = om.writeValueAsString(reqDTO);
// System.out.println(requestBody); // {"username":"haha","password":"1234","email":"haha@nate.com"}
// when -> 테스트 실행
ResultActions actions = mvc.perform( // 주소가 틀리면 터지고, json 아닌거 넣으면 터지고, 타입이 달라도 터지고. 따라서 미리 터진다고 알려줌
MockMvcRequestBuilders
.post("/join")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
);
// eye -> 결과 눈으로 검증
String responseBody = actions.andReturn().getResponse().getContentAsString();
//System.out.println(responseBody); // {"status":200,"msg":"성공","body":{"id":4,"username":"haha","email":"haha@nate.com","createdAt":"2025-05-13 11:45:23.604577"}}
// then -> 결과를 코드로 검증 // json의 최상위 객체를 $ 표기한다
actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(400));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("중복된 유저네임이 존재합니다"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body").value(Matchers.nullValue()));
actions.andDo(MockMvcResultHandlers.print()).andDo(document);
}
@Test
public void join_test() throws Exception { // 이 메서드를 호출한 주체에게 예외 위임 -> 지금은 jvm 이다
// given -> 가짜 데이터
UserRequest.JoinDTO reqDTO = new UserRequest.JoinDTO();
reqDTO.setEmail("haha@nate.com");
reqDTO.setPassword("1234");
reqDTO.setUsername("haha");
String requestBody = om.writeValueAsString(reqDTO);
// System.out.println(requestBody); // {"username":"haha","password":"1234","email":"haha@nate.com"}
// when -> 테스트 실행
ResultActions actions = mvc.perform( // 주소가 틀리면 터지고, json 아닌거 넣으면 터지고, 타입이 달라도 터지고. 따라서 미리 터진다고 알려줌
MockMvcRequestBuilders
.post("/join")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
);
// eye -> 결과 눈으로 검증
String responseBody = actions.andReturn().getResponse().getContentAsString();
//System.out.println(responseBody); // {"status":200,"msg":"성공","body":{"id":4,"username":"haha","email":"haha@nate.com","createdAt":"2025-05-13 11:45:23.604577"}}
// then -> 결과를 코드로 검증 // json의 최상위 객체를 $ 표기한다
actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(4));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.username").value("haha"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.email").value("haha@nate.com"));
actions.andDo(MockMvcResultHandlers.print()).andDo(document);
}
@Test
public void login_test() throws Exception {
// given
UserRequest.LoginDTO reqDTO = new UserRequest.LoginDTO();
reqDTO.setUsername("ssar");
reqDTO.setPassword("1234");
String requestBody = om.writeValueAsString(reqDTO);
// System.out.println(requestBody);
// when
ResultActions actions = mvc.perform(
MockMvcRequestBuilders
.post("/login")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
);
// eye
String responseBody = actions.andReturn().getResponse().getContentAsString();
System.out.println(responseBody);
// then (jwt 길이만 검증) -> 길이 변환 가능. 패턴만 확인
actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.accessToken",
matchesPattern("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$")));
actions.andDo(MockMvcResultHandlers.print()).andDo(document);
}
@Test
public void update_test() throws Exception {
// given
UserRequest.UpdateDTO reqDTO = new UserRequest.UpdateDTO();
reqDTO.setEmail("ssar@gmail.com");
reqDTO.setPassword("1234");
String requestBody = om.writeValueAsString(reqDTO);
// System.out.println(requestBody);
// when
ResultActions actions = mvc.perform(
MockMvcRequestBuilders
.put("/s/api/user")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + accessToken)
);
// eye
String responseBody = actions.andReturn().getResponse().getContentAsString();
System.out.println(responseBody);
// then
actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(1));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.username").value("ssar"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.email").value("ssar@gmail.com"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.createdAt",
matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+")));
actions.andDo(MockMvcResultHandlers.print()).andDo(document);
}
@Test
public void check_username_available_test() throws Exception {
// given
String username = "ssar";
// when
ResultActions actions = mvc.perform(
MockMvcRequestBuilders
.get("/api/check-username-available/{username}", username)
);
// eye
String responseBody = actions.andReturn().getResponse().getContentAsString();
System.out.println(responseBody);
// then
actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공"));
actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.available").value(false));
actions.andDo(MockMvcResultHandlers.print()).andDo(document);
}
}
결과 확인
- 테스팅을 통해 빌드를 수행하면 api 문서가 생성됨


- api.html
- 일부만 작성됨
- 서버 실행 후 /docs/api.html로 접속하면 문서가 화면에 출력됨

장점
- 기존 코드를 수정하지 않음
- URI, Body, Header 등의 요구 사항을 자세하게 작성 가능
- 통합 테스트를 통해 통과된 코드만 사용하므로 예시 값도 신뢰할 수 있음
단점
- 통합 테스트 코드가 있어야 사용 가능
- 초기 설정이 많아 복잡함
Share article