[SB] 32. 문서를 만들 때

최재원's avatar
Jun 10, 2025
[SB] 32. 문서를 만들 때

문서를 제작할 때 유의점

notion image

위의 이미지를 보면 부족한 정보가 있다

  1. 호스트 주소을 알려줬는가 origin 주소
    1. http://naver.com
  1. insert 할 때 필드 값이 어떤 것이 있는지
    1. name, age, color 등
  1. 어떤 형식으로 데이터를 보내야 하는지
    1. Content-Type → json, form, text 등

각 방식의 장단점

💡

Swagger API

장점
  • 만들기 편함
단점
  • 기존 코드에 영향을 많이 줌 가독성 안 좋음
  • 신뢰성이 검증된 데이터가 아님

RestDoc

장점
  • 통합 테스트를 만들어야 만들 수 있음(장점이자 단점)
  • 실제 코드에 영향을 미치지 않음
  • 검증된 데이터를 사용해서 사용할 수 있음
단점
  • 통합 테스트 코드를 만들어야 함

Swagger API

스웨거는 개발자가 REST 웹 서비스를 설계, 빌드, 문서화, 소비하는 일을 도와주는 대형 도구 생태계의 지원을 받는 오픈 소스 소프트웨어 프레임워크이다.

swagger 설정

plugins { id 'java' id 'org.springframework.boot' version '3.2.2' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'shop.mtcoding' version = '1.0' java { sourceCompatibility = '21' }
  • 현재 프로젝트 버전
dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' }
  • 최신 버전 swagger 설치
    • 최신 버전은 상세 설정이 필요없다
  • commons-lang3 버전 3.5 이상
    • commons-lang3 버전 낮아서 Swagger 내부 메서드 실패할 수 있다

swagger 실행 확인

http://localhost:8080/swagger-ui/index.html
  • 프로젝트 실행 후 해당 주소로 접근
notion image

swagger 어노테이션 추가

☕UserController

package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.util.Resp; import java.util.Map; @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); } }

☕UserRequest

package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; 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

package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; 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(); } } }

swagger 실행 확인

User

notion image
로그인
notion image
회원가입
notion image
회원정보수정
notion image
유저네임중복체크
notion image
 
 

swagger의 단점

  • 코드의 가독성을 해친다
  • 많은 양의 어노테이션이 추가된다
    • notion image
      notion image
 

RestDoc 생성

  1. build.gradle 세팅
    1. 플러그인에 추가
    2. 라이브러리 추가
    3. 설정 추가
  1. 테스트 폴더에 MyRestDoc 클래스 생성
  1. 통합 테스트 코드 MyRestDoc 상속 받고 actions.andDo 추가
  1. ./gradlew clean build 실행
  1. http://localhost:8080/docs/api.html 접속

1. 설정 파일 변경

build.gradle

의존성
dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' }
  • 추가
  • 버전 숫자가 없으면 무조건 최신 버전
플러그인
plugins { id 'org.asciidoctor.jvm.convert' version '3.3.2' }
  • 추가
기타 설정
ext { set('snippetsDir', file("build/generated-snippets")) } tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() } tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test } tasks.register('copyRestDocs', Copy) { dependsOn tasks.named('asciidoctor') from "${asciidoctor.outputDir}" into "src/main/resources/static/docs" } tasks.named('bootJar') { dependsOn tasks.named('copyRestDocs') }
  • 나머지 추가

2. 추상 클래스 만들기

notion image

MyRestDoc

package shop.mtcoding.blog; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.operation.preprocess.Preprocessors; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; // 4. 문서 만들기 @AutoConfigureRestDocs(uriScheme = "http", uriHost = "localhost", uriPort = 8080) // build 폴더에 adoc 파일 생성 @AutoConfigureMockMvc // MockMvc 클래스가 IoC에 로드 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능 @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())); } }
  • @AutoConfigureRestDocs(uriScheme = "http", uriHost = "localhost", uriPort = 8080) → build 폴더에 adoc 파일 생성
    • notion image
  • @AutoConfigureMockMvc → MockMvc 클래스가 IoC에 로드 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능

UserControllerTest

package shop.mtcoding.blog.integre; @Transactional // 컨트롤러 통합 테스트 @AutoConfigureMockMvc // MockMvc 클래스가 IoC에 로드 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK -> 가짜 환경을 만들어 필요한 의존관계를 다 메모리에 올려서 테스트 public class UserControllerTest { @Autowired private ObjectMapper om; // json <-> java Object 변환 해주는 객체. IoC에 objectMapper가 이미 떠있음 @Autowired private MockMvc mvc; // 가짜 환경에서 fetch 요청하는 클래스 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능 @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")); } }
⬇ 다음과 같이 변경한다
package shop.mtcoding.blog.integre; // 컨트롤러 통합 테스트 @AutoConfigureRestDocs(uriScheme = "http", uriHost = "localhost", uriPort = 8080) @Transactional @AutoConfigureMockMvc // MockMvc 클래스가 IoC에 로드 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK -> 가짜 환경을 만들어 필요한 의존관계를 다 메모리에 올려서 테스트 public class UserControllerTest extends MyRestDoc { @Autowired private ObjectMapper om; // json <-> java Object 변환 해주는 객체. IoC에 objectMapper가 이미 떠있음 private String accessToken; @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); } }
  1. public class UserControllerTest extends MyRestDoc → 커스텀한 MyRestDoc을 상속 받는다
  1. @AutoConfigureRestDocs(uriScheme = "http", uriHost = "localhost", uriPort = 8080) → 이 어노테이션을 테스트 클래스 위에 추가한다
  1. @Autowired private MockMvc mvc; → 삭제한다. 상속 받은 클래스에 있는 mvc를 사용할 것이다
  1. actions.andDo(MockMvcResultHandlers.print()).andDo(document); → 테스트 메서드 맨 마지막 줄에 이 코드를 추가한다
      • 테스트의 결과를 출력하는데 출력물을 문서에 작성하라는 코드
      • 테스트 결과를 문서로 작성하기 위한 코드

3. 문서 작성을 위한 설정

build.gradle

// 3. 문서 만들기 // 1. gradlew build or 서버실행(x -> 테스트 코드가 실행이 안됨) // gradlew build (test코드 실행 -> 결과 build/generated-snippets 에 copy 한다. *.adoc 으로 만들어준다) // set -> 변수 설정 set 은 file 주소다 ext { set('snippetsDir', file("build/generated-snippets")) } // 2. gradlew build (test코드 실행 -> 진짜로 1번의 결과가 실행됨) tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() } // 3. 테스트가 종료되면 asciidoctor 실행 - *.adoc 파일들을 가지고 하나의 .adoc 파일로 변경(미리 파일 만들어 놔야함 -> src/docs/asciidoc) 이곳에 만들어짐 tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test // test 를 실행 후에 실행(의존한다) } // 4. HTML 파일을 서버에서 열어볼 수 있게 Copy tasks.register('copyRestDocs', Copy) { dependsOn tasks.named('asciidoctor') from "${asciidoctor.outputDir}" // asciidoctor 결과 위치 into "src/main/resources/static/docs" } tasks.named('bootJar') { dependsOn tasks.named('copyRestDocs') }

build.gradle 설정 설명

ext { set('snippetsDir', file("build/generated-snippets")) } tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() } tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test } bootJar { dependsOn asciidoctor copy { // src/docs/asciidoc == from 경로 from "${asciidoctor.outputDir}" into 'src/main/resources/static/docs' // /static/docs로 복사! } } 야 결국 이거자나 build를 하면 test코드가 실행되는데 test코드가 실행된 결과를 build/generated-snippets 폴더에 파일로 저장하고 그 결과들을 static/docs 폴더에 html로 만들어줘 이 설정하는 거자나? 근데 왤케 복잡하게 되있어!! 그지같네 그치? ChatGPT의 말: 응 정확히 이해했어! 너가 요약한 게 딱 맞아: test를 실행하면 build/generated-snippets 폴더에 REST Docs 스니펫(.adoc 조각들)이 생기고, asciidoctor가 이걸 기반으로 HTML 문서를 생성하고, bootJar 시점에 이 HTML 결과물을 src/main/resources/static/docs로 복사해서 최종적으로
같은 경로에서 접근 가능하게 만드는 흐름이야. 근데 너 말대로 복잡해 보여도 이유는 있어. 요건 "빌드 프로세스 자동화"를 위해 필요한 의식 같은 거야 😅 요소 역할 snippetsDir 테스트에서 생성될 스니펫의 경로 정의 test.outputs.dir test task가 이 디렉토리를 출력물로 생성함을 명시 asciidoctor.inputs.dir AsciiDoctor가 어떤 파일을 기반으로 HTML 만들지를 정의 bootJar.copy HTML 결과물을 JAR에 포함시키기 위한 복사 작업 그럼 이걸 더 단순하게 만들 수 없냐? 가능은 해. 예를 들면, 아래처럼 Gradle Kotlin DSL로 쓰면 좀 더 명시적이면서 깔끔해: kotlin 복사 편집 val snippetsDir by extra { file("build/generated-snippets") } tasks.test { outputs.dir(snippetsDir) useJUnitPlatform() } tasks.asciidoctor { inputs.dir(snippetsDir) dependsOn(tasks.test) } tasks.bootJar { dependsOn(tasks.asciidoctor) doLast { copy { from("build/docs/asciidoc") into("src/main/resources/static/docs") } } } 결론 ✅ 네 말이 맞아. 결국 "test → snippets → asciidoctor → 복사" 이게 핵심인데, DSL 구조랑 자동화를 위한 규칙들 때문에 좀 장황하게 보이는 거야. 하지만 한 번 설정해두면, 문서 자동생성은 진짜 강력한 무기지. 필요하면 내가 이거 zip으로 자동 설정되게 구성해서 보내줄 수도 있어. 말만 해! 나의 말: 그리고 bootJar 시점도 별로야!! 그냥 서버실행하면 이자나 ChatGPT의 말: 맞아 ㅋㅋ 너 말 진짜 핵심 찔렀다. bootJar 시점에 복사하는 건 "배포용 jar 만들 때 문서 포함시키려고" 그런 건데, 우린 그냥 서버 실행(bootRun)만 해도 문서 나오면 되잖아? 서버 실행할 때 docs/index.html 바로 확인 가능한 게 더 현실적이고 빠르지. 굳이 jar 만들 때까지 기다릴 이유가 없어. ✅ 그러면 실시간 미리보기 + 서버실행에 맞는 구조는? 1. 테스트 → snippets 생성 2. asciidoctor → HTML 생성 3. HTML을 static/docs로 바로 복사 (서버 실행하면 바로 열람 가능) ✨ 이렇게 바꿔 쓰면 돼 (Gradle 기준) groovy 복사 편집 ext { snippetsDir = file('build/generated-snippets') docsOutputDir = file('src/main/resources/static/docs') // 여기에 바로 복사 } // 테스트하면 스니펫 생성 tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() } // 문서 빌드 시 스니펫 의존 tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test } // 문서 결과물을 바로 static/docs로 복사 tasks.register('copyDocs', Copy) { dependsOn tasks.named('asciidoctor') from file("build/docs/asciidoc") into docsOutputDir } // bootRun 전에 문서 복사 tasks.named('bootRun') { dependsOn tasks.named('copyDocs') } 📌 효과 ./gradlew bootRun만 하면: 테스트 돌고, 문서 뽑고, static/docs로 복사되고, 서버 켜지면 /docs/index.html 바로 열람 가능

각각의 설정 설명

// 1. gradlew build or 서버실행(x -> 테스트 코드가 실행이 안됨) // gradlew build (test코드 실행 -> 결과 build/generated-snippets 에 copy 한다. *.adoc 으로 만들어준다) // set -> 변수 설정 set 은 file 주소다 ext { set('snippetsDir', file("build/generated-snippets")) }
  1. gradlew build or 서버 실행(x -> 테스트 코드가 실행이 안됨)
      • gradlew build (test코드 실행 -> 결과 build/generated-snippets 에 copy 한다. *.adoc 으로 만들어준다)
      • set -> 변수 설정 set 은 file 주소다
      • 1번에선 주소를 변수 설정하는 것
// 2. gradlew build (test코드 실행 -> 진짜로 1번의 결과가 실행됨) tasks.named('test') { outputs.dir snippetsDir useJUnitPlatform() }
  1. gradlew build (test코드 실행 -> 진짜로 1번의 결과가 실행됨)
      • 여기서 테스트 코드 실행
// 3. 테스트가 종료되면 asciidoctor 실행 - *.adoc 파일들을 가지고 하나의 .adoc 파일로 변경(미리 파일 만들어 놔야함 -> src/docs/asciidoc) 이곳에 만들어짐 tasks.named('asciidoctor') { inputs.dir snippetsDir dependsOn test // test 를 실행 후에 실행(의존한다) }
  1. 테스트가 종료되면 asciidoctor 실행 - *.adoc 파일들을 가지고 하나의 .adoc 파일로 변경(미리 파일 만들어 놔야 함 -> src/docs/asciidoc) 이 문서에 합쳐진다
    1. api.adoc 이라는 파일을 만들어야 함. restdoc 문법임
      1. notion image
        api.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[]
        • 위 문서의 문법에 따라 만들어진 *.adoc 파일들을 가져와서 합쳐줌
          • notion image
        • 빌드 시 만들어지는 각각의 .adoc파일들
        • 작성한 .adoc 파일에 include 코드에 각 .adoc파일의 내용이 포함됨
// 4. HTML 파일을 서버에서 열어볼 수 있게 Copy tasks.register('copyRestDocs', Copy) { dependsOn tasks.named('asciidoctor') from "${asciidoctor.outputDir}" // asciidoctor 결과 위치 into "src/main/resources/static/docs" } tasks.named('bootJar') { dependsOn tasks.named('copyRestDocs') }
  1. 빌드시 만들어지는 docs/asciidoc/api.html 파일을 → static.docs/api.html 로 복사
    1. notion image
notion image
notion image

4. gradlew clean build 실행

notion image
  • build/generated-snippets 폴더 생성
  • 폴더 내부에 각각의 통합 테스트 클래스 이름의 폴더 생성
  • 테스트 이후에 만들어지는 폴더 이름은 기본적으로 _ 방식으로 만들어진다
  • 테스트 클래스 이름은 카멜케이스를 사용했기 때문에 자동으로 - 으로 변환된다
    • UserControllerTest → user-controller-test
  • 테스트 메서드 이름은 _ 방식으로 작성했기 때문에 그대로 만들어진다
    • join_test → join_test
  • 그러나 메서드를 카멜케이스로 작성했다면…
    • checkUsernameAvailable_test → check-username-available_test
    • 컨벤션이 망가진다
  • 따라서 메서드도 테스트 코드에선 _ 를 사용하자
    • check_username_available_test

5. asciidoctor 이 동작하면

notion image
notion image
  • 이처럼 각각의 .adoc 파일들이
notion image
  • 위 문법에 따라
notion image
  • 다음과 같이 html 파일로 합쳐진다

6. 결과 화면

notion image
 
Share article

jjack1