๐REST Docs๋?
- Test ์ฝ๋ ๊ธฐ๋ฐ์ผ๋ก Restful API ์๋น์ค๋ฅผ ๋ฌธ์ํ๋ฅผ ๋์
- Asciidoctor๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ด๋ฅผ ์ด์ฉํด html์ ์์ฑ
- ํฐ ์ฅ์ ์ ํ ์คํธ ์ฝ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฌธ์๋ฅผ ์์ฑํ๋ค๋ ์
- ํ ์คํธ ํต๊ณผ์ Snippet์ด ์์ฑ๋จ
- ๋ง์ฝ ํ ์คํธ ์คํจ ์, Snippet์ด ์์ฑ๋์ง ์์
๐กREST Docs VS Swagger
REST Docs | Swagger | |
์ฅ์ | ํ ์คํธ์ ์ฑ๊ณตํด์ผ ๋ฌธ์ ์์ฑ ๊ฐ๋ฅ | API ๋ฅผ ํ ์คํธ ํด ๋ณผ์ ์๋ ํ๋ฉด์ ์ ๊ณต |
์ ํ ์ฝ๋์ ์ํฅ์ด ์์ | ํ ์คํธ ์ฝ๋๊ฐ ํ์ ์์ผ๋ฏ๋ก ์ ์ฉํ๊ธฐ ์ฌ์ | |
๋จ์ | ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ๋ฏ๋ก ์ ์ฉ์ด ๋ถํธ | ์ ํ ์ฝ๋์ ์ด๋
ธํ
์ด์
์ถ๊ฐํด์ผ ํจ |
ํ ์คํธ ๊ธฐ๋ฐ์ด ์๋๊ธฐ ๋๋ฌธ์ 100% ์ ํํ์ง ์์ ์ ์์ |
- Swagger ์ฌ์ฉ ์, ์ ํ ์ฝ๋์ ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํด์ผ ํจ
- ๋๋ฌธ์ ์ง์์ ์ผ๋ก ์ฌ์ฉ ์, ์ด๋ ธํ ์ด์ ์ด ๋ง์ด ๋ถ๊ฒ ๋๊ณ ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง ์ ์์
๐ ๋ค์ด๊ฐ๊ธฐ ์ ์ : build.gradle ์ถ๊ฐ
: REST Docs๋ฅผ ๋ง๋ค๊ธฐ ์ํด, ํ์ํ ๋ถ๋ถ๋ค์ ์ถ๊ฐํด ์ฃผ์๋ค.
plugins {
...
id "org.asciidoctor.jvm.convert" version "3.3.2" /* ์ถ๊ฐ */
}
group = 'org.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
...
asciidoctorExtensions /* ์ถ๊ฐ */
}
repositories {
mavenCentral()
}
ext {
set('snippetsDir', file("build/generated-snippets")) /* ์ถ๊ฐ */
}
dependencies {
...
/* Rest Docs ์ถ๊ฐ */
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
/* Gson */
implementation 'com.google.code.gson:gson'
}
/* ์๋์ ๋ด์ฉ๋ค ์ถ๊ฐ */
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("${asciidoctor.outputDir}")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
bootJar {
dependsOn copyDocument
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
๐ REST Docs ์์ฑ 1 - Test Code ์์ฑ
GitHub - Bhinney/Rec_Study: โจ ๊ณต๋ถํ๋ฉด์ ๊ธฐ๋กํ๋ ๊ณต๊ฐ โจ
โจ ๊ณต๋ถํ๋ฉด์ ๊ธฐ๋กํ๋ ๊ณต๊ฐ โจ. Contribute to Bhinney/Rec_Study development by creating an account on GitHub.
github.com
1๏ธโฃ ์์ฒญ๊ณผ ์๋ต ์์๊ฒ ํ๋ฆฐํธ ํ๊ธฐ ์ํ Util ์ถ๊ฐ
public interface ApiDocumentUtils {
static OperationRequestPreprocessor getRequestPreProcessor() {
return preprocessRequest(prettyPrint());
}
static OperationResponsePreprocessor getResponsePreProcessor() {
return preprocessResponse(prettyPrint());
}
}
2๏ธโฃ BoardControllerTest ํด๋์ค ์์ฑ
: ๊ฒ์๋ฌผ ๋ฑ๋ก๊ณผ ์์ ์ StubData๊ฐ ์๋ ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฅํด์ฃผ์๋ค.
@WebMvcTest(BoardController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class BoardControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BoardService boardService;
@MockBean
private BoardMapper mapper;
@Autowired
private Gson gson;
@Test
@DisplayName("๊ฒ์๊ธ ๋ฑ๋ก ํ
์คํธ")
public void postBoardTest() throws Exception {
/* given */
BoardDto.Post post = new BoardDto.Post("๊น์ด๋ฆ", "์ ๋ชฉ์
๋๋ค.", "๋ด์ฉ์
๋๋ค.");
String content = gson.toJson(post);
BoardDto.Response response =
new BoardDto.Response(1L, "๊น์ด๋ฆ", "์ ๋ชฉ์
๋๋ค.", "๋ด์ฉ์
๋๋ค.", LocalDate.now(), LocalDate.now());
given(mapper.boardPostDtoToBoard(Mockito.any(BoardDto.Post.class))).willReturn(new Board());
given(boardService.create(Mockito.any(Board.class))).willReturn(new Board());
given(mapper.boardToBoardResponseDto(Mockito.any(Board.class))).willReturn(response);
/* when */
ResultActions actions
= mockMvc.perform(
post("/api/boards")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
/* then */
actions
.andExpect(status().isCreated())
.andExpect(jsonPath("$.nickName").value(post.getNickName()))
.andExpect(jsonPath("$.title").value(post.getTitle()))
.andExpect(jsonPath("$.content").value(post.getContent()))
.andDo(document("post-board",
getRequestPreProcessor(),
getResponsePreProcessor(),
requestFields(
List.of(
fieldWithPath("nickName").type(JsonFieldType.STRING).description("์์ฑ์"),
fieldWithPath("title").type(JsonFieldType.STRING).description("์ ๋ชฉ"),
fieldWithPath("content").type(JsonFieldType.STRING).description("๋ด์ฉ")
)
),
responseFields(
List.of(
fieldWithPath("boardId").type(JsonFieldType.NUMBER).description("๊ฒ์ํ ์๋ณ์"),
fieldWithPath("nickName").type(JsonFieldType.STRING).description("์์ฑ์"),
fieldWithPath("title").type(JsonFieldType.STRING).description("์ ๋ชฉ"),
fieldWithPath("content").type(JsonFieldType.STRING).description("๋ด์ฉ"),
fieldWithPath("createdAt").type(JsonFieldType.STRING).description("์ต์ด ์์ฑ ์ผ์"),
fieldWithPath("modifiedAt").type(JsonFieldType.STRING).description("์์ ์ผ์")
)
)
));
}
@Test
@DisplayName("๊ฒ์๊ธ ์์ ํ
์คํธ")
public void patchBoardTest() throws Exception {
/* given */
long boardId = 1L;
BoardDto.Patch patch = new BoardDto.Patch("์์ ๋ ์ด๋ฆ", "์์ ๋ ์ ๋ชฉ์
๋๋ค.", "์์ ๋ ๋ด์ฉ์
๋๋ค.");
String content = gson.toJson(patch);
BoardDto.Response response =
new BoardDto.Response(1L,"์์ ๋ ์ด๋ฆ", "์์ ๋ ์ ๋ชฉ์
๋๋ค.", "์์ ๋ ๋ด์ฉ์
๋๋ค.", LocalDate.now(), LocalDate.now());
given(mapper.boardPatchDtoToBoard(Mockito.any(BoardDto.Patch.class))).willReturn(new Board());
given(boardService.update(Mockito.any(Board.class), Mockito.anyLong())).willReturn(new Board());
given(mapper.boardToBoardResponseDto(Mockito.any(Board.class))).willReturn(response);
/* when */
ResultActions actions
= mockMvc.perform(
patch("/api/boards/{boardId}", boardId)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
/* then */
actions
.andExpect(status().isOk())
.andExpect(jsonPath("$.nickName").value(patch.getNickName()))
.andExpect(jsonPath("$.title").value(patch.getTitle()))
.andExpect(jsonPath("$.content").value(patch.getContent()))
.andDo(document("patch-board",
getRequestPreProcessor(),
getResponsePreProcessor(),
requestFields(
List.of(
fieldWithPath("nickName").type(JsonFieldType.STRING).description("์์ฑ์"),
fieldWithPath("title").type(JsonFieldType.STRING).description("์ ๋ชฉ"),
fieldWithPath("content").type(JsonFieldType.STRING).description("๋ด์ฉ")
)
),
responseFields(
List.of(
fieldWithPath("boardId").type(JsonFieldType.NUMBER).description("๊ฒ์ํ ์๋ณ์"),
fieldWithPath("nickName").type(JsonFieldType.STRING).description("์์ฑ์"),
fieldWithPath("title").type(JsonFieldType.STRING).description("์ ๋ชฉ"),
fieldWithPath("content").type(JsonFieldType.STRING).description("๋ด์ฉ"),
fieldWithPath("createdAt").type(JsonFieldType.STRING).description("์ต์ด ์์ฑ ์ผ์"),
fieldWithPath("modifiedAt").type(JsonFieldType.STRING).description("์์ ์ผ์")
)
)
));
}
}
3๏ธโฃ ํ ์คํธ ์ฝ๋๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ํ๊ธฐ ์ํด StubData ์์ฑ
: ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฅํ๋ ๊ฒ์ด ์๋ StubData๋ฅผ ์ด์ฉํด์ ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
๋๋ฌธ์ ์ดํ์ ํ ์คํธ ์ฝ๋ ์์ ์ ํด๋ณผ ์์ ์ด๋ค.
public class StubData {
private static Map<HttpMethod, Object> stubRequestBody;
static {
stubRequestBody = new HashMap<>();
stubRequestBody.put(HttpMethod.POST, new BoardDto.Post("๊น์ด๋ฆ", "์ ๋ชฉ์
๋๋ค.", "๋ด์ฉ์
๋๋ค."));
stubRequestBody.put(HttpMethod.PATCH, new BoardDto.Patch("์์ ๋ ์ด๋ฆ", "์์ ๋ ์ ๋ชฉ์
๋๋ค.", "์์ ๋ ๋ด์ฉ์
๋๋ค."));
}
public static class MockBoard {
public static Object getRequestBody(HttpMethod method) {
return stubRequestBody.get(method);
}
public static BoardDto.Response getBoardResponse() {
BoardDto.Response board = BoardDto.Response.builder()
.boardId(1L)
.nickName("๊น์ด๋ฆ")
.title("์ ๋ชฉ์
๋๋ค.")
.content("๋ด์ฉ์
๋๋ค.")
.createdAt(LocalDate.now())
.modifiedAt(LocalDate.now())
.build();
return board;
}
public static Page<Board> getBoardListStubData() {
Board board1 = Board.builder()
.nickName("๊น์ด๋ฆ1")
.title("์ฒซ ๋ฒ์งธ ์ ๋ชฉ์
๋๋ค.")
.content("์ฒซ ๋ฒ์งธ ๋ด์ฉ์
๋๋ค.")
.build();
Board board2 = Board.builder()
.nickName("๊น์ด๋ฆ2")
.title("๋ ๋ฒ์งธ ์ ๋ชฉ์
๋๋ค.")
.content("๋ ๋ฒ์งธ ๋ด์ฉ์
๋๋ค.")
.build();
Board board3 = Board.builder()
.nickName("๊น์ด๋ฆ3")
.title("์ธ ๋ฒ์งธ ์ ๋ชฉ์
๋๋ค.")
.content("์ธ ๋ฒ์งธ ๋ด์ฉ์
๋๋ค.")
.build();
return new PageImpl<>(List.of(board3, board2, board1),
PageRequest.of(0, 10, Sort.by("memberId").descending()),3);
}
public static List<BoardDto.Response> getBoardListResponse() {
BoardDto.Response board1 = BoardDto.Response.builder()
.boardId(1L)
.nickName("๊น์ด๋ฆ1")
.title("์ฒซ ๋ฒ์งธ ์ ๋ชฉ์
๋๋ค.")
.content("์ฒซ ๋ฒ์งธ ๋ด์ฉ์
๋๋ค.")
.createdAt(LocalDate.now())
.modifiedAt(LocalDate.now())
.build();
BoardDto.Response board2 = BoardDto.Response.builder()
.boardId(2L)
.nickName("๊น์ด๋ฆ2")
.title("๋ ๋ฒ์งธ ์ ๋ชฉ์
๋๋ค.")
.content("๋ ๋ฒ์งธ ๋ด์ฉ์
๋๋ค.")
.createdAt(LocalDate.now())
.modifiedAt(LocalDate.now())
.build();
BoardDto.Response board3 = BoardDto.Response.builder()
.boardId(3L)
.nickName("๊น์ด๋ฆ3")
.title("์ธ ๋ฒ์งธ ์ ๋ชฉ์
๋๋ค.")
.content("์ธ ๋ฒ์งธ ๋ด์ฉ์
๋๋ค.")
.createdAt(LocalDate.now())
.modifiedAt(LocalDate.now())
.build();
return List.of(
board3, board2, board1
);
}
public static Board getBoardStubData(long boardId) {
Board board = Board.builder()
.nickName("๊น์ด๋ฆ")
.title("์ ๋ชฉ์
๋๋ค.")
.content("๋ด์ฉ์
๋๋ค.")
.build();
return board;
}
}
}
4๏ธโฃ ํ ์คํธ ์ฝ๋๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ํ๊ธฐ ์ํด Helper ์์ฑ
: ํ ์คํธ ์ฝ๋์์ ResultActions์ ๊ฐ๊ฒฐํ๊ฒ ํ๊ธฐ ์ํ Helper ์์ฑ
public interface ControllerTestHelper<T> {
default RequestBuilder getRequestBuilder(String url, long sourceId) {
return get(url, sourceId)
.accept(MediaType.APPLICATION_JSON);
}
default RequestBuilder getRequestBuilder(String url, MultiValueMap<String, String> queryParams){
return get(url)
.params(queryParams)
.accept(MediaType.APPLICATION_JSON);
}
default RequestBuilder deleteRequestBuilder(String uri, long sourceId) {
return delete(uri, sourceId);
}
default URI createURI(String url) {
return UriComponentsBuilder.newInstance().path(url).build().toUri();
}
default URI createURI(String url, long resourceId) {
return UriComponentsBuilder.newInstance().path(url).buildAndExpand(resourceId).toUri();
}
default String toJsonContent(T t) {
Gson gson = new Gson();
String content = gson.toJson(t);
return content;
}
}
public interface BoardControllerHelper extends ControllerTestHelper {
String url = "/api/boards";
String resource = "/{boardId}";
default String getBoardURI() {
return url;
}
default String getBoardResourceURI() {
return url + resource;
}
}
4๏ธโฃ BoardControllerTest ํด๋์ค ์ถ๊ฐ ๊ตฌํ
: ํน์ ๊ฒ์๋ฌผ ์กฐํ, ๊ฒ์๋ฌผ ๋ฆฌ์คํธ ์กฐํ, ๊ฒ์ํ ์ญ์ ์ ๋ํ ํ ์คํฐ๋ StubData์ Helper๋ฅผ ์ด์ฉํด ๊ตฌํํ์๋ค.
Helpers๋ implements ํ์ฌ ์ฌ์ฉํ์๋ค.
@WebMvcTest(BoardController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class BoardControllerTest implements BoardControllerHelper {
@Autowired
private MockMvc mockMvc;
@MockBean
private BoardService boardService;
@MockBean
private BoardMapper mapper;
@Autowired
private Gson gson;
...
@Test
@DisplayName("ํน์ ๊ฒ์๋ฌผ ์กฐํ ํ
์คํธ")
public void getBoardTest() throws Exception {
/* given */
long boardId = 1L;
BoardDto.Response response = StubData.MockBoard.getBoardResponse();
String url = getBoardResourceURI();
given(boardService.findBoard(Mockito.anyLong())).willReturn(new Board());
given(mapper.boardToBoardResponseDto(Mockito.any(Board.class))).willReturn(response);
/* when */
ResultActions actions = mockMvc.perform(getRequestBuilder(url, boardId));
/* then */
actions.andExpect(status().isOk())
.andDo(document("get-board",
getResponsePreProcessor(),
pathParameters(parameterWithName("boardId").description("๊ฒ์ํ ์๋ณ์")),
responseFields(
List.of(
fieldWithPath("boardId").type(JsonFieldType.NUMBER).description("๊ฒ์ํ ์๋ณ์"),
fieldWithPath("nickName").type(JsonFieldType.STRING).description("์์ฑ์"),
fieldWithPath("title").type(JsonFieldType.STRING).description("์ ๋ชฉ"),
fieldWithPath("content").type(JsonFieldType.STRING).description("๋ด์ฉ"),
fieldWithPath("createdAt").type(JsonFieldType.STRING).description("์ต์ด ์์ฑ ์ผ์"),
fieldWithPath("modifiedAt").type(JsonFieldType.STRING).description("์์ ์ผ์")
)
)
));
}
@Test
@DisplayName("๊ฒ์๋ฌผ ๋ฆฌ์คํธ ์กฐํ")
public void getBoardListTest() throws Exception {
Page<Board> boardList = StubData.MockBoard.getBoardListStubData();
List<BoardDto.Response> responseList = StubData.MockBoard.getBoardListResponse();
String url = getBoardURI();
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("page", "1");
queryParams.add("size", "3");
given(boardService.findBoardList(Mockito.anyInt(), Mockito.anyInt())).willReturn(boardList);
given(mapper.boardToBoardResponseDto(Mockito.anyList())).willReturn(responseList);
/* when */
ResultActions actions = mockMvc.perform(getRequestBuilder(url,queryParams));
/* then */
actions.andExpect(status().isOk())
.andDo(document("get-boardList",
getRequestPreProcessor(),
getResponsePreProcessor(),
requestParameters(
List.of(
parameterWithName("page").description("ํ์ด์ง ๋ฒํธ"),
parameterWithName("size").description("ํ์ด์ง ์ฌ์ด์ฆ")
)
),
responseFields(
List.of(
fieldWithPath("[].boardId").type(JsonFieldType.NUMBER).description("๊ฒ์ํ ์๋ณ์"),
fieldWithPath("[].nickName").type(JsonFieldType.STRING).description("์์ฑ์"),
fieldWithPath("[].title").type(JsonFieldType.STRING).description("์ ๋ชฉ"),
fieldWithPath("[].content").type(JsonFieldType.STRING).description("๋ด์ฉ"),
fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("์ต์ด ์์ฑ ์ผ์"),
fieldWithPath("[].modifiedAt").type(JsonFieldType.STRING).description("์์ ์ผ์")
)
)
));
}
@Test
@DisplayName("๊ฒ์๋ฌผ ์ญ์ ํ
์คํธ")
public void deleteBoardTest() throws Exception {
long boardId = 1L;
String message = "๊ฒ์ํ์ด ์ฑ๊ณต์ ์ผ๋ก ์ญ์ ๋์์ต๋๋ค.";
given(boardService.delete(Mockito.anyLong())).willReturn(message);
String url = getBoardResourceURI();
/* when */
ResultActions actions = mockMvc.perform(deleteRequestBuilder(url, boardId));
actions.andExpect(status().isOk())
.andDo(document("delete-board",
pathParameters(parameterWithName("boardId").description("๊ฒ์ํ ์๋ณ์"))
)
);
}
}
๐ REST Docs ์์ฑ 2 - adoc ๋ฌธ์ ์์ฑ & Html ์์ฑ
1๏ธโฃ Test๋ฅผ ํ์ฌ ํต๊ณผ๊ฐ ๋๋ฉด build ํด๋ ์์ Snippet์ด ์์ฑ ๋จ
2๏ธโฃ ํด๋น ์ค๋ํ์ ๋ฐํ์ผ๋ก adoc ๋ฌธ์ ์์ฑ
GitHub - Bhinney/Rec_Study: โจ ๊ณต๋ถํ๋ฉด์ ๊ธฐ๋กํ๋ ๊ณต๊ฐ โจ
โจ ๊ณต๋ถํ๋ฉด์ ๊ธฐ๋กํ๋ ๊ณต๊ฐ โจ. Contribute to Bhinney/Rec_Study development by creating an account on GitHub.
github.com
= ๊ฒ์ํ ์ดํ๋ฆฌ์ผ์ด์
:sectnums:
:toc: left
:toclevels: 4
:toc-title: Table of Contents
:source-highlighter: prettify
v1.0.0, 2023.01.04
***
์ด ๋ฌธ์๋ ์คํ๋ง ๊ณต๋ถ๋ฅผ ์ํด ์์ฑ๋ ๊ฒ์ํ CRUD์ ๋ฌธ์์
๋๋ค.
***
== BoardController
==== ๊ฒ์๊ธ ๋ฑ๋ก
.curl-request
include::{snippets}/post-board/curl-request.adoc[]
.http-request
include::{snippets}/post-board/http-request.adoc[]
.request-fields
include::{snippets}/post-board/request-fields.adoc[]
.http-response
include::{snippets}/post-board/http-response.adoc[]
.response-fields
include::{snippets}/post-board/response-fields.adoc[]
=== ๊ฒ์๊ธ ์์
.curl-request
include::{snippets}/patch-board/curl-request.adoc[]
.http-request
include::{snippets}/patch-board/http-request.adoc[]
.request-fields
include::{snippets}/patch-board/request-fields.adoc[]
.http-response
include::{snippets}/patch-board/http-response.adoc[]
.response-fields
include::{snippets}/patch-board/response-fields.adoc[]
=== ํน์ ๊ฒ์๊ธ ์กฐํ
.curl-request
include::{snippets}/get-board/curl-request.adoc[]
.http-request
include::{snippets}/get-board/http-request.adoc[]
.http-response
include::{snippets}/get-board/http-response.adoc[]
.response-fields
include::{snippets}/get-board/response-fields.adoc[]
=== ๊ฒ์๊ธ ๋ฆฌ์คํธ ์กฐํ
.curl-request
include::{snippets}/get-boardList/curl-request.adoc[]
.http-request
include::{snippets}/get-boardList/http-request.adoc[]
.request-parameters
include::{snippets}/get-boardList/request-parameters.adoc[]
.http-response
include::{snippets}/get-boardList/http-response.adoc[]
.response-fields
include::{snippets}/get-boardList/response-fields.adoc[]
=== ๊ฒ์๊ธ ์ญ์
.curl-request
include::{snippets}/delete-board/curl-request.adoc[]
.http-request
include::{snippets}/delete-board/http-request.adoc[]
.http-response
include::{snippets}/delete-board/http-response.adoc[]
3๏ธโฃ Gradle ์์ bootJar ์คํ
4๏ธโฃ ์ ๋๋ก ์คํ์ด ๋์๋ค๋ฉด resources/static/docs์์ html์ด ์์ฑ
GitHub - Bhinney/Rec_Study: โจ ๊ณต๋ถํ๋ฉด์ ๊ธฐ๋กํ๋ ๊ณต๊ฐ โจ
โจ ๊ณต๋ถํ๋ฉด์ ๊ธฐ๋กํ๋ ๊ณต๊ฐ โจ. Contribute to Bhinney/Rec_Study development by creating an account on GitHub.
github.com
5๏ธโฃ ํด๋น html ํ์ผ์ ํฌ๋กฌ์ผ๋ก ์ด์ด ๋ณด๊ธฐ
: ์๋์ ์ผ๋ถ๋ฅผ ์บก์ณํด ์๋ค. ์๋์ ์ฌ์ง์ฒ๋ผ ํ์ธ์ด ๊ฐ๋ฅํ๋ค.
'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Spring] ๋ก๊ทธ์ธ ๊ตฌํ ๋ฒ์ธ - ์์ ๋ก๊ทธ์ธ ์ ๊ถํ ๋ถ์ฌ (0) | 2022.12.20 |
---|---|
[Spring] ๋ก๊ทธ์ธ ๊ตฌํ 3 - ์นด์นด์ค ๋ก๊ทธ์ธ ๊ตฌํ (1) | 2022.12.20 |
[Spring] ๋ก๊ทธ์ธ ๊ตฌํ 2 - ํ์ ๊ฐ์ & ์์ฒด ๋ก๊ทธ์ธ ๊ตฌํ (1) | 2022.12.20 |
[Spring] ๋ก๊ทธ์ธ ๊ตฌํ 1 - ํ์ ์ํฐํฐ, Mapper, Repository ๊ตฌํ (0) | 2022.12.20 |
[Spring] Transaction (0) | 2022.12.19 |
๋๊ธ