Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- linux
- annotation
- 메모리의 불연속적 할당
- 프로세스 동기화
- 웹개발
- 스프링
- Shared Page
- 운영체제
- spring
- 알고리즘
- 자바 문제풀이
- Allocation of Physical Memory
- 문제풀이
- 프로세스 불연속 할당
- 메모리 관리
- jpa
- 코드스테이츠 백엔드 과정 39기
- Effective Access Time
- 자바 알고리즘
- Inverted Page Table
- 다단계 페이지 테이블
- CS
- 웹 프로그래밍
- Segmentation with Paging
- Page Table의 구현
- 리눅스
- springboot
- 프로세스 할당
- 2단계 Page Table
- 스프링부트
Archives
- Today
- Total
GrowMe
[API 문서화] Spring Rest Docs를 통한 API 문서화 본문
Spring Rest Docs를 통한 API 문서화
# API 문서화
# Spring Rest Docs
# 스니핏
# Spring Rest Docs를 통한 API 문서화 흐름
# Spring Rest Docs를 통해 API 문서화 해보기
*API 문서화
- 개발 진행 시, 백엔드 쪽에서 서버 애플리케이션을 구현하고나면, 협업을 위해서는 이를 사용할 수 있도록 프론트엔드 쪽에 안내를 해주어야 한다.
- API 문서화란 이처럼 만들어낸 API의 사용법을 안내할 수 있도록 보기 편하게 문서로 만드는 것을 말한다.
- API 사용을 위해 어떤 정보들이 담겨 있는 문서를 API 문서 또는 API 스펙(사양 : Specification)이라고 한다.
*API 문서화 자동화 Tool
- Swagger
- 장점 : Swagger는 Postman을 통해 API를 테스트 하듯이, API 문서 안에서 Execute 버튼을 눌러 실제로 요청을 전송해볼 수 있습니다.
@ApiOperation(value = "회원 정보 API", tags = {"Member Controller"}) // (1)
@RestController
@RequestMapping("/v11/swagger/members")
@Validated
@Slf4j
public class MemberControllerSwaggerExample {
private final MemberService memberService;
private final MemberMapper mapper;
public MemberControllerSwaggerExample(MemberService memberService, MemberMapper mapper) {
this.memberService = memberService;
this.mapper = mapper;
}
// (2)
@ApiOperation(value = "회원 정보 등록", notes = "회원 정보를 등록합니다.")
// (3)
@ApiResponses(value = {
@ApiResponse(code = 201, message = "회원 등록 완료"),
@ApiResponse(code = 404, message = "Member not found")
})
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post memberDto) {
Member member = mapper.memberPostToMember(memberDto);
member.setStamp(new Stamp()); // homework solution 추가
Member createdMember = memberService.createMember(member);
return new ResponseEntity<>(
new SingleResponseDto<>(mapper.memberToMemberResponse(createdMember)),
HttpStatus.CREATED);
}
...
...
// (4)
@ApiOperation(value = "회원 정보 조회", notes = "회원 식별자(memberId)에 해당하는 회원을 조회합니다.")
@GetMapping("/{member-id}")
public ResponseEntity getMember(
@ApiParam(name = "member-id", value = "회원 식별자", example = "1") // (5)
@PathVariable("member-id") @Positive long memberId) {
Member member = memberService.findMember(memberId);
return new ResponseEntity<>(
new SingleResponseDto<>(mapper.memberToMemberResponse(member))
, HttpStatus.OK);
}
...
...
}
@ApiModel("Member Post") // (1)
@Getter
public class MemberPostDto {
// (2)
@ApiModelProperty(notes = "회원 이메일", example = "hgd@gmail.com", required = true)
@NotBlank
@Email
private String email;
// (3)
@ApiModelProperty(notes = "회원 이름", example = "홍길동", required = true)
@NotBlank(message = "이름은 공백이 아니어야 합니다.")
private String name;
// (4)
@ApiModelProperty(notes = "회원 휴대폰 번호", example = "010-1111-1111", required = true)
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phone;
}
- 단점 : Swagger를 사용하면 API 문서 자동화를 위해서는 위처럼 실제 코드 작성 시 코드마다 애너테이션과 문서 작성 내용을 추가해주어야 합니다. 이 때문에 본래 코드가 길어질수록 가독성이 떨어질 수 있으며 리펙토링하거나 기능 추가 시, 헷갈릴 수 있습니다.
- Spring Rest Docs
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@MockBean
private MemberMapper mapper;
@Autowired
private Gson gson;
@Test
public void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
"홍길동",
"010-1234-5678");
String content = gson.toJson(post);
MemberDto.response responseDto =
new MemberDto.response(1L,
"hgd@gmail.com",
"홍길동",
"010-1234-5678",
Member.MemberStatus.MEMBER_ACTIVE,
new Stamp());
// willReturn()이 최소 null은 아니어야 한다.
given(mapper.memberPostToMember(Mockito.any(MemberDto.Post.class)))
.willReturn(new Member());
given(memberService.createMember(Mockito.any(Member.class)))
.willReturn(new Member());
given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);
// when
ResultActions actions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
// then
actions
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.email").value(post.getEmail()))
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andExpect(jsonPath("$.data.phone").value(post.getPhone()))
.andDo(document("post-member", // =========== (1) API 문서화 관련 코드 시작 ========
getRequestPreProcessor(),
getResponsePreProcessor(),
requestFields(
List.of(
fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호")
)
),
responseFields(
List.of(
fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
fieldWithPath("data.email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("data.phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
fieldWithPath("data.memberStatus").type(JsonFieldType.STRING).description("회원 상태"),
fieldWithPath("data.stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수")
)
)
)); // =========== (2) API 문서화 관련 코드 끝========
}
}
- Spring Rest Docs를 사용하여 API 문서화를 하면, 위처럼 별도의 TestCase를 통해 문서화의 자동화가 진행됩니다. 이로써 본래 코드는 전혀 영향이 가지 않는다는 장점이 있습니다.
- 하지만, 문서화를 하려면 TestCase를 필히 작성해주어야 하며, 테스트가 fail이되면 문서화가 되지 않습니다.
- 그런데, Test가 통과될 때만 문서화가 된다는 의미는, 실제로 만들어지는 문서 내의 API 문서 정보와 구현해낸 애플리케이션의 API 스펙 정보가 항상 일치한다는 의미입니다. 이는 사실 장점이 될 수도 있는 부분인 것입니다. (Swagger는 API 스펙 정보를 문자열로 타이핑하는 경우가 많아 문서와 애플리케이션 사이의 API 스펙정보가 불일치할 가능성이 있습니다.)
- 또한 Swagger처럼 API 문서 내에서 직접 requst 요청을 보낼 수는 없다는 단점도 있습니다.
*Spring Rest Docs의 API 문서 생성 흐름
- 슬라이스 테스트 코드 작성
- API 스펙 정보(Request Body, Response Body, Query Parameter 등)를 코드로 작성
- test 실행(Gradle의 빌드 태스크 test task 실행) -> 테스트 통과 시, API 문서 스니핏 생성(.adoc 확장자)
-> 테스트 실패 시, 테스트 케이스 수정 Or 오류 해결 - 생성된 API 문서 스니핏(문서의 일부 조각)을 모아 API 문서 생성
- API 문서를 HTML 파일로 변환
*Spring Rest Docs로 API 문서화해보기
- build.gradle 설정
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "org.asciidoctor.jvm.convert" version "3.3.2" // (1)
id 'java'
}
group = 'com.codestates'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
// (2)
ext {
set('snippetsDir', file("build/generated-snippets"))
}
// (3)
configurations {
asciidoctorExtensions
}
dependencies {
// (4)
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// (5)
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.mapstruct:mapstruct:1.5.1.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'com.google.code.gson:gson'
}
// (6)
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
// (7)
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
// (8)
task copyDocument(type: Copy) {
dependsOn asciidoctor // (8-1)
from file("${asciidoctor.outputDir}") // (8-2)
into file("src/main/resources/static/docs") // (8-3)
}
build {
dependsOn copyDocument // (9)
}
// (10)
bootJar {
dependsOn copyDocument // (10-1)
from ("${asciidoctor.outputDir}") { // (10-2)
into 'static/docs' // (10-3)
}
}
- (1) .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해주는 Asciidoctor를 사용하기 위한 플러그인을 추가
- (2) API 문서 스니핏이 생성될 경로를 지정
- (3) AsciiDoctor에서 사용되는 의존 그룹을 지정하고 있습니다. :asciidoctor task가 실행되면 내부적으로 (3)에서 지정한 ‘asciidoctorExtensions’라는 그룹을 지정합니다.
- (4) 'org.springframework.restdocs:spring-restdocs-mockmvc'를 추가함으로써 spring-restdocs-core와 spring-restdocs-mockmvc 의존 라이브러리가 추가됩니다.
- (5) spring-restdocs-asciidoctor 의존 라이브러리를 추가합니다. (3)에서 지정한 asciidoctorExtensions 그룹에 의존 라이브러리가 포함이 됩니다.
- (6) test task 실행 시, API 문서 생성 스니핏 디렉토리 경로를 설정합니다.
- (7) asciidoctor task 실행 시, Asciidoctor 기능을 사용하기 위해 :asciidoctor task에 asciidoctorExtensions 을 설정합니다.
- (8) build task 실행 전에 실행되는 task입니다. copyDocument task가 수행되면 index.html 파일이 src/main/resources/static/docs 에 copy 되며, copy된 index.html 파일은 API 문서를 파일 형태로 외부에 제공하기 위한 용도로 사용할 수 있습니다.
- (8-1)에서는 :asciidoctor task가 실행된 후에 task가 실행 되도록 의존성을 설정합니다.
- (8-2)에서는 "build/docs/asciidoc/" 경로에 생성되는 index.html을 copy한 후,
- (8-3)의 "src/main/resources/static/docs" 경로로 index.html을 추가해 줍니다.
- (9) build task가 실행되기 전에 :copyDocument task가 먼저 수행 되도록 합니다.
- (10) 애플리케이션 실행 파일이 생성하는 :bootJar task 설정입니다.
- (10-1)에서는 :bootJar task 실행 전에 :copyDocument task가 실행 되도록 의존성을 설정합니다.
- (10-2)에서는 Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가해 줍니다. jar 파일에 index.html을 추가해 줌으로써 웹 브라우저에서 접속(http://localhost:8080/docs/index.html) 후, API 문서를 확인할 수 있습니다.
- Gradle 기반 프로젝트에서는 src/docs/asciidoc/ 디렉토리를 생성해주어야 합니다.
- 또한 해당 경로에 비어있는 index.adoc 템플릿 문서를 생성해주어야 API 문서화가 정상적으로 작동됩니다.
- Bz_Controller
@RestController
@RequiredArgsConstructor
public class Bz_Controller {
private final Bz_Service bz_service;
// 전체 조회
@GetMapping("/search")
public ResponseEntity All_Search() {
List<Member_Dto> list = bz_service.find_All();
return new ResponseEntity<>(new Multi_ResponseDto<>(list),HttpStatus.OK);
}
}
- 테스트 케이스 작성
import api.v1.Bz_Controller;
import api.v1.Bz_Service;
import api.v1.Member_Dto;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static api.v1.util.ApiDocumentUtils.*;
@WebMvcTest(Bz_Controller.class)
@AutoConfigureRestDocs
public class Bz_Controller_Test {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@MockBean
private Bz_Service bz_service;
@Test
public void All_Search_Test() throws Exception {
// request - 필요X
// response
Member_Dto response1 = Member_Dto.builder()
.name("홍길동1")
.sex("m")
.company_name("나라사랑")
.company_type("005")
.company_location("001")
.build();
Member_Dto response2 = Member_Dto.builder()
.name("김민영2")
.sex("w")
.company_name("우리나라")
.company_type("003")
.company_location("002")
.build();
List<Member_Dto> list = List.of(response2, response1);
// Stubbing
given(bz_service.find_All()).willReturn(list);
// when
ResultActions actions = mockMvc.perform(
get("/search")
.accept(MediaType.APPLICATION_JSON));
// then
actions.andExpect(status().isOk())
// ---------- API 문서화 작성 시작 --------------
.andDo(document( //
"get-All-Members",
getRequestPreProcessor(),
getResponsePreProcessor(),
responseFields(
List.of(
fieldWithPath("list").type(JsonFieldType.ARRAY).description("회원 전체 리스트"),
fieldWithPath("list[].name").type(JsonFieldType.STRING).description("회원 이름"),
fieldWithPath("list[].sex").type(JsonFieldType.STRING).description("회원 성별"),
fieldWithPath("list[].company_name").type(JsonFieldType.STRING).description("회사 이름"),
fieldWithPath("list[].company_type").type(JsonFieldType.STRING).description("회사 업종코드"),
fieldWithPath("list[].company_location").type(JsonFieldType.STRING).description("회사 위치코드")
)
)
));
// ---------- API 문서화 작성 끝 --------------
}
}
- ApiDocumentUtils
public interface ApiDocumentUtils {
static OperationRequestPreprocessor getRequestPreProcessor() {
return preprocessRequest(prettyPrint());
}
static OperationResponsePreprocessor getResponsePreProcessor() {
return preprocessResponse(prettyPrint());
}
}
- @webMvcTest : 인자로 주어진 Controller만 지정하여 스캔하여 빈을 Load한다. @SpringBootTest 처럼 모든 빈을 로드하지 않고, 지정한 컴포넌트의 빈만 스캔하여 로드하기 때문에, Controller 레이어만 슬라이스 테스트 진행 시, 빠르고 효율적으로 진행할 수 있다. Service, Repository 객체의 사용이 필요할 경우 @MockBean으로 주입받아 테스트 진행하면된다.
- @AutoConfigureRestDocs : Spring Rest docs의 자동 구성을 도와 준다. 이 어노테이션이 테스트 클래스에 선언되어야 API 문서화가 자동으로 진행된다.
- 주석으로 구분한 API 문서화 코드 시작 전에는 Mocking을 통해 Controller만 잘라내어 테스트를 진행하고, API 문서화에 대한 설명이 목적이므로 검증코드는 최대한 간단하게 작성하였다.
- andDo() : 테스트의 실행결과를 가지고 후처리할 수 있는 ResultHandler를 지정하며, log를 찍거나 실행결과를 print 시키는 등을 할 수 있다. 여기서 document() 메서드는 RestDocumentationResultHandler 내부에 존재하면서 이 클래스를 그대로 반환한며 이는 ResultHandler를 상속 받고 있다.
- document() : 1번째 파라미터는 API 문서 스니핏의 식별자 역할을 한다. 즉 여기서는 "get-All-Members"로 정했으므로 문서 스니핏이 get-All-Members 디렉토리 하위에 생성된다. 2번째, 3번째 파라미터는 request, response 문서 영역 전처리로 각각 request body와 responsebody를 예쁘게 디자인해준다.
- responseFields() : 문서로 표현될 response body를 의미. 파라미터로 List가 전달되는데 List의 원소로는 fieldWithPath()의 반환 값인 FieldDescriptor로 채워진다. FieldDescriptor 객체는 response body에 포함된 데이터를 표현한다. 체인형식으로 연쇄되어 메서드를 사용할 수 있다. 이는 requestFields() 메서드에도 request body에 대한 내용으로 똑같이 적용된다.
- fieldWithPath()는 어떤 키 값의 Json 데이터를 가져와 표현할 것인지 지정한다.
- type()은 Json 데이터의 property 값이 어떤 타입인지를 지정하여 문서에 표현한다.
- descrption()은 해당 데이터가 어떤데이터인지에 대한 설명을 문서에 표현한다.
- test task 실행 -> 문서 스니핏 생성
- 위 처럼 우리가 default로 설정해놓은 스니펫 생성 디렉토리 하위에서, 다시 document()에서 지정한 디렉토리 하위에 스니펫(adoc 확장자 파일)들이 생성되었다.
- 스니펫들 중 http-response.adoc 파일의 모습과 문서로 렌더링 된 모습
- 스니펫들을 모아 API 문서화하기
- Gradle 프로젝트에서는 html로 렌더링하기위한 adoc 파일의 default 경로가 위 경로이다. 위 경로에 index.adoc 파일을 생성한다.
- index.adoc에 스니펫을 이용해 문서 작성하기
= 전국 사업자 연합 커뮤니티 애플리케이션 // (1)
:sectnums: // (2)
:toc: left // (3)
:toclevels: 4 // (4)
:toc-title: Table of Contents // (5)
:source-highlighter: prettify // (6)
Park Seong Jae <mirea720@gmail.com>
v1.0.0, 2022.08.17
*** // (7)
API 문서 개요
// (8)
이 문서는 전국 사업자 연합 커뮤니티 애플리케이션의 설명을 위한 API 문서입니다.
필요한 Request 데이터와 Response 응답 데이터 등, API의 기본적인 내용을 담았습니다.
추가적으로 문서 상 정보가 필요할 경우 위 메일로 전달 부탁드리겠습니다.
***
== Bz_Controller
=== 전체 회원 조회
.curl-request // (9)
include::{snippets}/get-All-Members/curl-request.adoc[] // (10)
.http-request
include::{snippets}/get-All-Members/http-request.adoc[]
.http-response
include::{snippets}/get-All-Members/http-response.adoc[]
.response-fields
include::{snippets}/get-All-Members/response-fields.adoc[]
***
- (1) 문서의 제목을 작성하기 위해서는 =를 추가하면 됩니다. ====와 같이 =의 개수가 늘어날 수록 글자는 작아집니다.
- (2) 목차에서 각 섹션에 넘버링을 해주기 위해서는 :sectnums: 를 추가하면 됩니다.
- (3) :toc: 는 목차를 문서의 어느 위치에 구성할 것인지를 설정합니다. 여기서는 문서의 왼쪽에 목차가 표시되도록 left를 지정했습니다.
- (4) :toclevels: 은 목차에 표시할 제목의 level을 지정합니다. 여기서는 4로 지정했기 때문에 ==== 까지의 제목만 목차에 표시됩니다.
- (5) :toc-title: 은 목차의 제목을 지정할 수 있습니다.
- (6) :source-highlighter: 소스 코드의 강조표시의 방법을 지정합니다. 여기서는 prettify를 지정했습니다.
- (7) : *** 는 단락을 구분 짓는 수평선을 추가합니다.
- (8) : 한 문단의 제목 다음 한 라인을 띄운 후, 다음 줄에서 한 칸 들여쓰기 문단을 작성 시, 박스 문단이 사용 가능합니다.
- CAUTION: 을 사용해 경고문구를 추가할 수 있습니다. 이 외에도 NOTE: , TIP: 등이 있습니다.
- http , https, fps, irc, mailto, hgd@gmail.com 등의 URL Scheme는 Asciidoc에서 자동으로 인식해 링크로 설정됩니다.
- 이미지 추가 : 이미지를 추가하고 싶다면 image:: 를 사용합니다.
ex) image::https://spring.io/images/spring-logo-9146a4d3298760c2e7e49595184e1975.svg[spring] - (9) : . 은 하나의 스니핏 섹션 제목을 표현합니다. curl-request는 섹션의 제목이며 원하는대로 수정 가능합니다.
- (10) : include는 Asciidoctor에서 사용하는 매크로 중 하나이며, 스니핏을 템플릿 문서에 포함할 때 사용합니다.
{snippets}는 해당 스니핏이 생성되는 디폴트 경로이며, 이것은 build.gradle에서 설정한 snippetsDir 변수를 참조합니다.
-> 여기서는 build/generated-snippets 입니다.
- 템플릿 문서 -> HTML 파일로 변환하기
- Gradle 탭에서 bootJar 또는 build task를 더블클릭하여 명령을 실행합니다.
- 위처럼 html파일이 잘 생성되었습니다.
- 애플리케이션 서버 실행 후, url로도 잘 확인됩니다.
'About Spring' 카테고리의 다른 글
[API 문서화] Swagger의 기본 사용법 (0) | 2022.08.30 |
---|---|
[Spring] 이벤트를 처리하는 방법 (0) | 2022.07.12 |
[JPA] 엔티티 간 연관관계 매핑 방법 (2) | 2022.07.12 |
SpringBoot에서 JPA 사용하기 (0) | 2022.06.30 |
[Spring] Mapper와 MapStruct에 대해 알아보자 (0) | 2022.06.28 |
Comments