반응형
이전글 : [Kotlin / Spring] Spring Security 도입기 [1] : 의존성 설치하기

 

[1] CustomFilter 구현

Custom Filter를 구현하고자 하는 이유는 다음과 같다.

1. Basic 이 아닌 JWT 기반 Bearer 토큰을 사용할 것이다.

2. token에 대한 검증 이후 서비스이용 가능 여부를 검증하기 위한 밑작업을 할 것이다.

 

이를 위해서 한개 호출당 1회 실행되는 설정으로 OncePerRequestFilter() 를 상속받아 Filter class 를 작성하였다.

package project.config.filter

import project.model.AuthUserInfo
import project.model.JwtToken
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter

class CustomFilter : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
        try {
            val authHeader = request.getHeader("Authorization")
            if (authHeader?.startsWith("Bearer ") == true) {
                val token = authHeader.removePrefix("Bearer ").trim()
                val jwtToken = JwtToken(accessToken = token)
                val tokenPayload = jwtToken.verifyAccessToken()
                val authUser = AuthUserInfo.of(userId = tokenPayload["userId"], userType = tokenPayload["userType"])
                val auth = UsernamePasswordAuthenticationToken(authUser, null, authUser.authorities)
                SecurityContextHolder.getContext().authentication = auth
                filterChain.doFilter(request, response)
            } else {
                throw Exception("wrong authorization")
            }
        } catch (e: Exception) {
            throw Exception(e)
        }
    }
}

 

1. 비즈니스 로직

1) Authorization 형식 확인

`token`

먼저 header 의 Authorization 에 담긴 값이 Bearer 로 시작하는지 검증한다. 안타깝게도 spring security 에서는 `Bearer ` 에 대한 내용을 자동으로 검증해주는 함수가 없어서 문자열로 직접 검증 해야 한다고 한다.

 

2) accessToken 검증

`jwtToken` & `tokenPayload`

이후로 accessToken 에 대한 검증을 진행한다. 이때 내가 만든 함수를 호출하여 검증하도록 한다.

해당 함수에는

- 만료된 토큰 여부

- 토큰의 payload 에 필수정보가 포함되어 있는지

등등에 대해서 검증하고 있다.

 

3) 인증된 object 생성

`authUser`

data class로 `AuthUserInfo` 를 생성하여 해당 클래스에 인증된 유저의 정보를 담아 객체를 생성한다.

 

4) SecurityContext 에 사용자 설정

`auth`

authUser 로 Authentication 객체를 만들고 이를 SecurityContextHolder에 authentication 값으로 갱신한다.

 

5) filter 통과

해당 필터 로직을 종료한다. (다음 필터가 있다면 그것진행. 없으면 이후 프로세스 진행됨)

 

2. Filter 에서의 Exception 처리

먼저 spring 으로 들어온 요청에 대한 영역을 구분하면 아래와 같다.

그림에서처럼 Filter 는 스프링 영역 밖에 존재한다. 이 점으로 인해서 Filter 에서 발생된 Exception 은 Interceptor나 Controller, Service 등과 달리 @ControllerAdvice 에 도달하지 않고 Servlet 수준에서 처리 된다.

 

위의 코드처럼 exception을 처리하면 Web Application 에 정의된 폼 대로 응답이 출력된다.

 

statusCode는 500이다.

{
    "timestamp": "2025-05-08T06:20:20.443+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/auth/token/verify"
}

 

만약, 이미 가공한 형태의 에러응답이 있고 이를 사용하고 싶다면 직접 문자열로 작성하면 된다. 로깅도 잊지말자!

 

private fun processError(
    response: HttpServletResponse,
    errorCode: String,
    message: String,
    exceptionMessage: String,
) {
    response.status = 400
    response.contentType = "application/json"
    response.characterEncoding = "UTF-8"
    val body =
        """
        {
            "success": false,
            "response": null,
            "message":  "$message",
            "error": {
                        "code": "$errorCode",
                        "message": "$exceptionMessage"
                    },
        }
        """.trimIndent()
    response.writer.write(body)
}

해당 함수를 Filter class 안에 작성하고 에러발생시 해당 함수를 호출하도록 한다. 

catch (e: JwtCustomException) {
        this.processError(response, "EXPIRED_TOKEN", "만료된 토큰" e.message)
        return
}

 

그럼 응답이 아래와 같이 나오며 statusCode는 400으로 출력된다.

{
    "success": false,
    "response": null,
    "message": "만료된 토큰",
    "error": {
        "code": "EXPIRED_TOKEN",
        "message": "JWT expired 72022520 milliseconds ago at 2025-05-07T10:28:21.000Z. Current time: 2025-05-08T06:28:43.520Z. Allowed clock skew: 0 milliseconds."
    },
}

 


다음으로는 이렇게 만든 CustomFilter 를 사용해 SecurityConfig 를 작성하고 인증시킬 조건에 대해 설정해보자

반응형
반응형
language : kotlin "2.1.20"
framework : spring boot "3.4.4"
Build tool : Gradle "8.13"

[Spring Security]

Spring Security는 Spring Framework 기반 애플리케이션에 보안 기능을 추가하기 위한 프레임워크주로 인증(Authentication)인가(Authorization) 기능을 제공하며, 세션 관리, CSRF 보호, 암호화, HTTP 요청 보안 설정 등 다양한 보안 관련 기능을 제공한다.

 

- 인증 (Authentication)

> 접근하는 자가 "인증" 된 사용자인가?

사용자가 누구인지 확인하는 과정이다. 인증되지 않는 사용자의 접근을 제한한다.

 

- 인가 (Authorization)

> 접근한 자가 "인가" 된 사용자인가?

접근한 사용자가 해당 기능에 권한이 있는지 확인하는 과정이다. 인증된 사용자이나 인가되지않은 사용자일 경우 접근이 제한된다.

 


[적용하기]

1. 의존성 설치하기 (Gradle)

https://docs.spring.io/spring-security/reference/getting-spring-security.html#getting-gradle-boot

 

Getting Spring Security :: Spring Security

As most open source projects, Spring Security deploys its dependencies as Maven artifacts, which makes them compatible with both Maven and Gradle. The following sections demonstrate how to integrate Spring Security with these build tools, with examples for

docs.spring.io

위 공식문서를 참고한다. 

dependencies {
	implementation "org.springframework.boot:spring-boot-starter-security"
    	testImplementation("org.springframework.security:spring-security-test")
}

 

이상태에서 application 을 실행하면 콘솔에 아래와 같은 문구가 나온다.

[2025-05-02 16:27:18.174] [] 23605 [main] [WARN ] UserDetailsServiceAutoConfiguration [getOrDeducePassword:90] 

Using generated security password: 97a0****-****-****-****-************

This generated password is for development use only. Your security configuration must be updated before running your application in production.
   
[2025-05-02 16:27:18.391] [] 23605 [main] [INFO ] MyProjectApplicationKt [logStarted:59] Started MyProjectApplicationKt in 6.119 seconds (process running for 6.422)

 

security password 를 따로 설정하지 않아 임의로 생성이 되었고, 이것은 개발용으로만 사용해야한다는 경고성 문구가 확인되며 운영환경에서는 UserDetailsService 또는 SecurityFilterChain으로 명시적 설정이 필요함을 알려준다.

 

2. Basic Auth 확인해보기

spring security 가 적용된 상태에서 아무런 인증값 없이 호출을 하면 401 에러가 발생한다.

이때, 콘솔에 찍힌 security password 를 가지고 Basic Auth 설정 후 통신을 하면 정상적으로 호출이 된다.

 

위 과정을 통해 spring security 에서는 기본적으로 Basic Auth를 제공하는것을 알 수 있다.

하지만, 프로젝트에서는 Bearer token을 사용할것이기 때문에 커스텀이 필요하다.

 

다음 장에서 Filter 를 이용해 Custom 인증 필터를 만드는 것을 진행해 보도록 하겠다.

반응형
반응형

배경

application.yml 에 작성한 설정값을 `@Value` 어노테이션으로 클래스의 필드에 주입시켜 값을 설정하도록 하였다.

그리고 application 을 실행시켜서 ApplicationRunner의 run 메소드에 따라 값이 출력되는것을 확인하였다.

package io.project

import org.springframework.stereotype.Component
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner


@Component
class BValidator : ApplicationRunner {

    @Value("\${spring.aaa}")
    private lateinit var someValue: String

    override fun run(args: ApplicationArguments) {
        println("check some value: $someValue")
    }


    fun doValidate(accessToken: String) {
        println(someValue)
    }
}

 

> application 실행 시

[2025-04-25 14:28:53.341] [] 5372 [main] [INFO ] ProjectApplicationKt [logStarted:59] Started GbikeAuthServerApplicationKt in 5.616 seconds (process running for 5.88)   
check some value: hello@123

 

하지만, 그 값을 사용하려는 함수 `doValidate` 를 호출하니 값이 설정되지 않는 오류가 발생하였다.

 

someValue = null

 

원인

원인은, BValidator 클래스를 AService에서 새로운 인스턴스로 생성하였기 때문에 있었기 때문에 Spring 컨테이너 밖의 객체가 되어서 였다.

Spring 컨테이너는 빈을 다음과 같은 순서로 처리한다.

  • 빈 인스턴스 생성
  • 의존성 주입 (@Value 포함)
  • 초기화 메서드 실행

하지만 AService에서 `BValidator()` 를 호출함에 따라 직접 인스턴스를 생성하여 Spring 컨테이너의 관리 밖에 놓이게 되었고, Spring은 이런 인스턴스에 대해 `@Value` 어노테이션을 처리하지 않아 값이 지정되지 않은 것이다.

 

해결

BValidator 인스턴스를 Spring 컨테이너 관리 빈으로써 처리하도록 한다. 그러기 위해서 AService의 생성자에 BValidator를 주입받아 Spring Container에 등록시키고 등록된 인스턴스로 함수를 호출하게 수정하였다.

 

반응형
반응형

지난 1탄에서는 content-type이 `application/json` 일때 문서 작성하는 법에 대해 다뤄보았다. 이번에는 `x-www-form-urlencoded` 형식의 api 일때 작성하는 법에 대해 다뤄보자.

기본적인 환경설정은 동일하기 때문에 패스하고

 

- 테스트 코드 작성시 사용하는 메소드

- index.adoc 설정법

 

만 다르기 때문에 이것만 다루도록 하겠다.

[1] 테스트코드 작성하기

1. 테스트 대상 코드

x-www-form-urlencoded 방식으로 전달 시 메소드는 POST 로 전달된다. 아래와 같이 테스트할 api를 작성해 준다.

@PostMapping("/tmp2")
fun tmp2(requestDto: TmpRequestDto): TmpResponseDto {
    val recommendEmail = requestDto.name + "@gmail.com"
    val responseDto =
        TmpResponseDto(
            code = 200,
            email = recommendEmail,
            message = "hello, ${requestDto.nickname ?: "guest"}",
        )
    return responseDto
}

 

 

2. 테스트코드 : params

application/json 타입일때에는 요청 파라미터를 정의할때 requestFields 메소드를 사용하였다. 하지만 x-www-form-urlencoded일때에는 params를 사용해야 한다.

// given
val requestDto =
    HealthcheckController.TmpRequestDto(
        userId = 1,
        name = "tester2",
        nickname = "banana",
        age = 25,
        productName = "잘팔리는노트",
        hobby = mutableListOf("클라이밍", "뜨개질"),
        address =
            HealthcheckController.TmpAddress(
                city = "세종특별시 아름동",
                street = "654-321",
                zipcode = 897,
            ),
    )

val xxxRequest =
    LinkedMultiValueMap(
        mapOf(
            "userId" to listOf(requestDto.userId.toString()),
            "name" to listOf(requestDto.name),
            "nickname" to listOf(requestDto.nickname),
            "age" to listOf(requestDto.age.toString()),
            "productName" to listOf(requestDto.productName),
            "hobby" to listOf(requestDto.hobby.joinToString(",")),
            "address.city" to listOf(requestDto.address.city),
            "address.street" to listOf(requestDto.address.street),
            "address.zipcode" to listOf(requestDto.address.zipcode.toString()),
        ),
    )

+) Map<String!, String!>

params의 value 타입에 맞춰 작성해 주어야 한다. 이때 문제가 될만한 것으로는 list 타입의 hobby이다.

콤마 (,) 로 구분하여 string으로 변환하는 작업을 해준 뒤 params의 값으로 작성해야 한다.

 

// when
val result =
    this.mockMvc.perform(
        RestDocumentationRequestBuilders
            .post("$domain/tmp2")
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .params(xxxRequest)
            .accept(MediaType.APPLICATION_JSON),
    )

이제 요청 준비는 다 되었고 결과값을 받아 문서로 만들 코드를 작성한다.

 

// then
result
    .andExpect(MockMvcResultMatchers.status().isOk)
    .andDo(
        document(
            "docs_xxxform",
            documentRequest,
            documentResponse,
            formParameters(
                parameterWithName("userId").description("유저Id").attributes(Attributes.key("type").value("integer")),
                parameterWithName("name").description("이름").attributes(Attributes.key("type").value("string")),
                parameterWithName("nickname").description("닉네임").attributes(Attributes.key("type").value("string")),
                parameterWithName("age").description("나이").attributes(Attributes.key("type").value("integer")),
                parameterWithName("productName").description("상품명").attributes(Attributes.key("type").value("string")),
                parameterWithName("hobby").description("취미").attributes(Attributes.key("type").value("array")),
                parameterWithName("address.city").description("도시").attributes(Attributes.key("type").value("string")),
                parameterWithName("address.street").description("도로명").attributes(Attributes.key("type").value("string")),
                parameterWithName("address.zipcode").description("우편번호").attributes(Attributes.key("type").value("integer")),
            ),
            responseFields(
                fieldWithPath("code").description("code").optional(),
                fieldWithPath("email").description("이메일").optional(),
                fieldWithPath("message").description("메시지").optional(),
            ),
        ),
    )

+) formParameters

요청파라미터를 정의하는 메소드로 `formParameters`를 사용한다. 객체안의 키는 application/json 형식때와 같이 마침표로 구분하여 작성한다.

 

[2] 문서 생성하기

1. 테스트 실행 : adoc 파일 생성

테스트를 실행하여 `docs_xxxform` 에 대한 adoc 파일들이 생성되는걸 확인해보자

이전에 생성된 `docs_json` 과 함께 테스트 성공으로 `docs-xxxform` 폴더가 생성되었다.

2. rest docs 포맷 설정 : index.adoc

`x-www-form-urlencoded` 형식에 맞춰 Index.adoc 폼도 수정해 주어야 한다.

 

application/json과 x-www-form-urlencdoed의 차이점!

- `request-fields.adoc` 은 application/json 에만 있다

- urlencoded에는 `form-parameters.adoc` 이 있다.

 

위 두가지가 요청 필드에 대한 명세서가 되므로 이에 유의하여 index.adoc파일을 작성해 준다.

 

 

 

 

 

 

# REST API

## [1] REST DOCS EXAMPLE : application/json
### [http : 요청]
include::{snippets}/docs_json/http-request.adoc[]
#### [요청 필드]
include::{snippets}/docs_json/request-fields.adoc[]
### [http : 응답]
include::{snippets}/docs_json/http-response.adoc[]
#### [응답 필드]
include::{snippets}/docs_json/response-fields.adoc[]

## [2] REST DOCS EXAMPLE : x-www-form-urlendcoded
### [http : 요청]
include::{snippets}/docs_xxxform/http-request.adoc[]
#### [요청 필드]
include::{snippets}/docs_xxxform/form-parameters.adoc[]
### [http : 응답]
include::{snippets}/docs_xxxform/http-response.adoc[]
#### [응답 필드]
include::{snippets}/docs_xxxform/response-fields.adoc[]

이제 build를 하고 `docs/index.html` 경로로 만들어진 문서를 확인해보자

3. /docs/index.html 로 발행된 문서 확인

참고로! 테스트 실행을 하고나서 build 하지 않고, build 시 테스트코드가 자동으로 실행되는 설정이라면 바로 build만 해도된다.

또 로컬환경에서 실행중일때 어라?! 빌드 됐는데 왜 문서 안바뀌지?! 하지 말고 서버도 재실행 해주는걸 잊지말자 🙂

한 페이지에 작성되다 보니 너무 길어서 창 두개에 띄움!

 

이렇게 content-type이 `x-www-form-urlencoded` 일때에 spring rest docs 작성도 완성했다! 다음은 이 문서를 보기좋게 커스텀 하는 방법에 대해 다뤄보고자 한다.

반응형
반응형

[1] 공식문서

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#introduction

 

Spring REST Docs

Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.

docs.spring.io

[2] 세팅하기

[build.gradle.kts]

plugins {
	id("org.asciidoctor.jvm.convert") version "3.3.2" // 1번
}

dependencies {
    asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.5.RELEASE") // 2번

    testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") // 3번
    testImplementation("org.springframework.restdocs:spring-restdocs-asciidoctor") // 4번
}

tasks {
    val snippetsDir by extra { file("build/generated-snippets") } // 5번

    test { // 6번 : test Task 의 output 디렉토리 설정
        outputs.dir(snippetsDir)
        useJUnitPlatform()
    }

    asciidoctor { // asciidoctor Task 생성
        configurations(asciidoctorExt.name)
        inputs.dir(snippetsDir) // 7번 : asciidoctor Task 의 input 디렉토리 설정

        dependsOn(test)
        doLast { // 8번
            copy {
                from("build/docs/asciidoc")
                into("src/main/resources/static/docs")
            }
        }
    }


- 1번 : 플러그인 추가

- 2번 : asciidoctor 의존성 추가

- 3번 : spring rest docs 의존성 추가 (mockMvc)

- 4번 : spring rest docs 의존성 추가 (asciidoctor)

- 5번 : snippets 경로 설정

- 6번 : test Task의 output 디렉토리 설정

- 7번 : asciidoctor Task의 input 디렉토리 설정

- 8번 : 테스트 코드 실행 종료 후 파일 복붙하는 설정 (from 하위 파일을 into 폴더 하위로 복제한다)

 

[3] 테스트코드 작성하기

1. abstract class 작성

우아한 형제들 블로그를 참고하여 작성하였다.

package io.my.project

import com.fasterxml.jackson.databind.ObjectMapper
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.boot.test.context.SpringBootTest
import org.springframework.context.annotation.ComponentScan
import org.springframework.test.web.servlet.MockMvc

@SpringBootTest
@AutoConfigureRestDocs // REST Docs 설정
@AutoConfigureMockMvc
@ComponentScan(basePackages = ["io.gbike.hwikgo"]) // 컴포넌트 스캔 범위 지정
abstract class ApiDocumentationTest : BaseMockData() {
    @Autowired
    protected lateinit var mockMvc: MockMvc

    @Autowired
    protected lateinit var objectMapper: ObjectMapper
}

 

? SpringBootTest

여기서 `SpringBootTest` 대신 `WebMvcTest` 를 사용하여 빌드속도를 좀더 빠르게 할 수 있지만 테스트하려는 Controller 에 연결되어있는 많은 Service들과 Service에서 또 참고하는 유틸성 컴포넌트 등 일일이 주입해주어야 할 의존성이 너무 많은듯 하여 일단 SpringBootTest 어노테이션을 사용하였다.

### : MockMvc

spring REST Docs 문서화를 위해선 MockMvc 의 perform을 수행해야 한다. 동일하게 사용되는 객체이므로 abstract에서 생성했다.

 

2. 테스트 코드 작성

먼저 테스트 코드 작성을 위해 controller에 테스트 대상 코드를 작성해 주었다.

@RestController
class HealthcheckController() {
    @GetMapping("/tmp")
    fun tmp(requestDto: TmpRequestDto): TmpResponseDto {
        val recommendEmail = requestDto.name + "@gmail.com"
        val responseDto =
            TmpResponseDto(
                code = 200,
                email = recommendEmail,
                message = "hello, ${requestDto.nickname ?: "guest"}",
            )
        return responseDto
    }

    data class TmpRequestDto(
        val userId: Int = 1,
        val name: String = "",
        val nickname: String = "",
        val age: Int = 1,
        val productName: String = "",
        val hobby: MutableList<String> = mutableListOf(),
        val address: TmpAddress = TmpAddress(city = "", street = "", zipcode = 0),
    )

    data class TmpAddress(
        val city: String,
        val street: String,
        val zipcode: Int,
    )

    data class TmpResponseDto(
        val code: Int,
        val email: String,
        val message: String = "",
    )
}

그리고 나서 해당 controller 를 호출할 테스트 코드를 작성해 주었다. 이때 호출당시의 content-type 에 따라 사용하는 메소드가 달라진다.

- content-type : x-www-form-urlencoded

요청을 보내는 쪽에서 body가 `x-www-form-urlencoded` 타입일 경우 formParameters 를 사용한다.

해당 방식은 다음 글에서 다루도록 하겠다.

- content-type : application/json

요청을 보내는 쪽에서 body가 `application/json` 타입일 경우 requestFields 를 사용한다.

package io.myproject.controller

import io.myproject.ApiDocumentUtils.Companion.documentRequest
import io.myproject.ApiDocumentUtils.Companion.documentResponse
import io.myproject.ApiDocumentationTest
import org.junit.jupiter.api.Test
import org.springframework.http.MediaType
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders
import org.springframework.restdocs.payload.PayloadDocumentation.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

class HealthcheckControllerTest : ApiDocumentationTest() {
    @Test
    fun tmpTest() {
        // given
        val re =
            HealthcheckController.TmpRequestDto(
                userId = 1,
                name = "tester1",
                nickname = "apple",
                age = 30,
                productName = "잘팔리는필통",
                hobby = mutableListOf("수영", "독서"),
                address =
                    HealthcheckController.TmpAddress(
                        city = "서울시 강남구 역삼동",
                        street = "123-456",
                        zipcode = 123,
                    ),
            )

        // when
        val res =
            this.mockMvc.perform(
                RestDocumentationRequestBuilders
                    .get("$domain/tmp")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(re))
                    .accept(MediaType.APPLICATION_JSON),
            )

        res
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andDo(
                document(
                    "docs_json",
                    documentRequest,
                    documentResponse,
                    requestFields(
                        fieldWithPath("userId").description("유저Id").optional(),
                        fieldWithPath("name").description("이름").optional(),
                        fieldWithPath("nickname").description("닉네임").optional(),
                        fieldWithPath("age").description("나이").optional(),
                        fieldWithPath("productName").description("상품명").optional(),
                        fieldWithPath("hobby").description("취미").optional(),
                        fieldWithPath("address").description("주소").optional(),
                        fieldWithPath("address.city").description("도시").optional(),
                        fieldWithPath("address.street").description("도로명").optional(),
                        fieldWithPath("address.zipcode").description("우편번호").optional(),
                    ),
                    responseFields(
                        fieldWithPath("code").description("code").optional(),
                        fieldWithPath("email").description("이메일").optional(),
                        fieldWithPath("message").description("메시지").optional(),
                    ),
                ),
            )
    }
}

 

[4] 문서 생성하기

1. 테스트 실행 : adoc 파일 생성

먼저 작성한 테스트코드가 올바르게 실행되는지 확인한다. 테스트가 성공했다면 `build/generated-snippets` 에 테스트코드에서 지정한 documentdml identifier 이름의 폴더가 있고 그 아래에 사진과 같은 adoc 파일이 생성되어 있을 것이다.

 

- curl-request.adoc : curl 요청 형식으로 작성된 파일

- form-parameters : 본인의 테스트 코드 request body가 form-data 형식이어서 `formParameters`를 사용했기에 생성된 파일

- http-request : http 요청 파일

- http-response : http 응답 파일

- httpie-request : http client 요청 파일

- response-fields : 본인 테스트 코드 `responseFields`에 의해 생성된 파일

 

 

 

 

2. rest docs 포맷 설정 : index.adoc

테스트코드 실행으로 생성된 여러가지 adoc 파일을 활용해 rest docs를 구성해야 한다.

`src/docs/asciidoc/index.adoc`을 다음과 같이 작성하였다.

# REST API

## REST DOCS EXAMPLE

### [http : 요청]
include::{snippets}/docs_json/http-request.adoc[]

#### [요청 필드]
include::{snippets}/docs_json/request-fields.adoc[]

### [http : 응답]
include::{snippets}/docs_json/http-response.adoc[]

#### [응답 필드]
include::{snippets}/docs_json/response-fields.adoc[]

플러그인을 설치하면 다음와 같이 작성했을때 어떻게 보여지는지 바로 확인할 수 있다.

3. html 문서 확인

이제 프로젝트를 빌드하면 위의 adoc 파일의 내용index.adoc의 형식으로 index.html 파일로 변환된다. 또한  build.gradle.kts 에 작성했던 아래 설정으로 인해 `build/docs/asciidoc` 에 있는 파일이 `src/main/resources/static/docs` 로 복제 된다.

tasks {
    ... (생략) ...
    asciidoctor {
    	... (생략) ...
        dependsOn(test)
        doLast {
            copy {
                from("build/docs/asciidoc")
                into("src/main/resources/static/docs")
            }
        }
    }

 

해당 resources 하위에 존재해야지만 url path로 접근 할 수 있는데 이때 application 파일에 아래의 항목이 true 로 되어있어야 스프링 어플리케이션의 기본 정적 리소스 매핑을 사용할 수 있다.

spring:
    web:
        resources:
            add-mappings: true # 정적 리소스 매핑 활성화

 

 

4. /docs/index.html 로 발행된 문서 확인

정적 리소스 매핑으로 도메인 뒤에 `/docs/index.html` 를 입력하면 `main/resources/static/docs/index.html` 에 접근 할 수 있다.

참고로 build 로 html문서 생선 전에 `localhost:8080/docs/index.html` 로 접속하면 그러한 리소스는 없다는 오류가 뜬다.

테스트코드가 성공했다고 하여도 전체적인 build가 성공해야지만 docs가 생성된다는것!!

BUILD SUCCESSFUL in 24s

 

문서 생성 확인 완료!!

다음 2탄은 content-type 이 x-www-form-urlencoded 일때 문서 생성하는 방법에 대해 다뤄보도록 하겠다.

반응형

+ Recent posts