-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CHORE] Swagger + Rest Docs 로 API 문서 생성 #44
Conversation
# Conflicts: # src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java
Test Results68 tests 68 ✅ 7s ⏱️ Results for commit 98ca7eb. ♻️ This comment has been updated with latest results. |
📝 Test Coverage Report
|
/noti |
좋아요! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/noti
커찬! 정말 정말 고생많았습니다. 사실 사용법이나 설정 관련해서는 너무 이해가 잘갔어요. 다만 사소한 부분들에서 제 의견 남겼으니 천천히 의견 남겨주면 좋을 것 같아요!
논의했던 대로 화요일까지는 approve&merge하겠습니다 😄
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' | ||
testImplementation 'io.rest-assured:rest-assured:5.5.0' | ||
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' | ||
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[질문 ✋ ]
mockMvc가 webMvc를 사용하지 않음에도 불구하고 필요한 설정인것이죠?
(제 기억에는 RestAssured 사용하더라도 추가해야 했던 기억이 있긴 한데...)
asciidoc generate 태스크는 그날 회의 이후로 커찬이 제거한 것인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 에러 핸들링한 자료 공유해드려요~ 링크
아마도 해당 부분은 라이브러리 쪽 버그인 것 같아요. 어쩔 수 없이 의존성 추가했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다~
|
||
public RestDocumentationRequest description(String description) { | ||
resourceBuilder.description(description) | ||
.summary(description); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[사소한 제안]
summary는 간단한 요약 이라는 설정이고 desription은 파라미터에 대한 상세한 설명을 기술하는 것이라 생각되는데요. 메서드명은 description인데 하나의 파라미터로 둘다 초기화해주는 이유가 있나요?
아니라면 분리하거나 혹은 description만 설정해주는 건 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swagger UI 부분에서도 중복되는 부분이라 제거하는 게 좋을 것 같네요
반영하도록 하겠습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 부분을 요약해서 설명한다는 느낌에서 summary로 변경하고 summary만 설정해주도록 했습니다~
return this; | ||
} | ||
|
||
public RestDocumentationRequest pathParameter(ParameterDescriptor... descriptors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[사소한 제안]
가변 인자를 좋아하진 않지만 빌더같은 유틸성 클래스를 만들때는 좋다고 생각해요. 다만 가변인자의 경우 배열이기에 null 값도 허용하여 인수가 0인 값이 런타임에 잡히게 되는 것으로 알고 있어요.
하나 이상의 null이 아닌 파라미터를 무조건 받는다를 시그니처에 녹여도 좋을 것 같아요!
public RestDocumentationRequest pathParameter(ParameterDescriptor... descriptors) { | |
public RestDocumentationRequest pathParameter(ParameterDescriptor descriptor, ParameterDescriptor... descriptors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매우 좋은 의견이라고 생각해요! 하지만 아래 부분은 더 생각해봐야 할 것 같아요.
- 그러면 매번 descriptor, descriptors를 추가한 새로운 List or Array를 만들어야 하는데, 해당 부분은 정말 많은 부분에서 사용하고 있어요. 그렇다면 정말로 새로운 객체를 만들어야 할 정도로 중요하게 컴파일 타임에 신경써야 할 부분일까요?
- 해당 부분을 사용하는 테스트 코드는 특정 부분이 누락되면 실패하게 되어있어요. 내가 특정 파라미터를 추가하려고 해당 메서드를 사용했지만, 누락했을 경우에는 쉽게 알 수 있게 되요. 이러한 환경에서도
하나 이상의 null이 아닌 파라미터를 무조건 받는다
는 점을 강조해야 할까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인정합니다. 현재 구조로도 괜찮다고 생각해요!
} | ||
|
||
public List<Snippet> getSnippets() { | ||
return List.copyOf(snippets); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
방어적 복사 좋네요 ㄷㄷ 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개인적으로는 어짜피 RestDocumentFilterBuilder에만 사용할 껀데 방어적 복사를 해야되나 고민 했음...
그런데 애초에 내부적으로 가변 객체를 사용하니, 이건 방어적 복사를 추가해야 된다고 생각했음
} | ||
|
||
protected RestDocumentationFilterBuilder document(String identifierPrefix, int status) { | ||
return new RestDocumentationFilterBuilder(identifierPrefix, Integer.toString(status)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RestDocumentationFilterBuilder가 있는 라이브러린줄 알았는데 만든거였군요? 😮
대단한 사람...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rest Docs의 static import가 너무 귀찮아서 참을 수 없었다...
Builder 만들어서 실제 DocumentTest에서는 어떤 static import가 필요한지 모르도록 최선을 다했다...
.contentType(ContentType.JSON) | ||
.body(request) | ||
.when().post("/api/member") | ||
.then().log().all() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[질문 ✋ ]
일전에 설명해주엇던 것 같긴한데 로깅을 뺀 이유는 필요하지 않아서
가 맞을 까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이번에 BaseControllerTest에, RequestLoggingFilter, ResponseLoggingFilter를 추가해 주었는데요.
따로 log().all()을 명시해주지 않더라고 볼 수 있게끔 해주었습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이해했습니다 👍
} | ||
|
||
@EnumSource( | ||
value = ClientErrorCode.class, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[제안]
이거 문자열로 관리하기가 힘들 것 같은데 MethodSource 사용해보는 건 어떨까요?
관리 용이성과 타입안전성에 좋을 것 같아요.
value = ClientErrorCode.class, | |
@MethodSource("memberCreateError") | |
@ParameterizedTest | |
void 회원_생성_실패(ClientErrorCode errorCode) { | |
MemberCreateRequest request = new MemberCreateRequest("커찬"); | |
when(memberService.createMember(request)).thenThrow(new DTClientErrorException(errorCode)); | |
var document = document("member/create", errorCode) | |
.request(requestDocument) | |
.response(ERROR_RESPONSE) | |
.build(); | |
given(document) | |
.contentType(ContentType.JSON) | |
.body(request) | |
.when().post("/api/member") | |
.then().statusCode(errorCode.getStatus().value()); | |
} | |
private static Stream<ClientErrorCode> memberCreateError() { | |
return Stream.of( | |
ClientErrorCode.INVALID_MEMBER_NICKNAME_FORM, | |
ClientErrorCode.INVALID_MEMBER_NICKNAME_LENGTH | |
); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개인적으로 비선호하긴 합니다. 저는 어떤 ERROR CODE를 사용하는지 최대한 테스트와 붙여 놓은 것을 선호하긴 합니다.
물론 컴파일 환경에서 관리할 수 있다는 것이 큰 장점이긴 하지만, 저 에러 코드가 바뀌는 케이스를 거이 못봐서 @EnumSource
를 이용했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인정합니다. 개인적인 취향으로 제가 MethodSource를 선호하는 이유는 문자열 관리를 싫어하고 + 오타가 많아서 그런가봐요 😓 지금으로도 괜찮다 생각해요!
protected static long EXIST_MEMBER_ID = 123L; | ||
protected static Member EXIST_MEMBER = new Member(EXIST_MEMBER_ID, "존재하는 멤버"); | ||
|
||
protected static RestDocumentationResponse ERROR_RESPONSE = new RestDocumentationResponse() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[제안]
이거 description이 "에러 메시지" 로 통일되기 보다 실제 반환되는 에러 메시지를 넘겨주도록 protected method로 선언하는 건 어떨까요?
ex)
protected static RestDocumentationResponse ERROR_RESPONSE = new RestDocumentationResponse() | |
protected RestDocumentationResponse errorResponse(ErrorCode errorCode){ | |
return new RestDocumentationResponse() | |
.responseBodyField( | |
fieldWithPath("message").type(STRING).description(errorCode.getMessage()) | |
); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- PR에서 표기하실 때 '변경하고 싶은 부분'을 '앞 뒤 맥락 포함해서' 표시해주세요. 내가 코드를 어떻게 썼는지 한 눈에 안들어오고, 콜리의 의견과 비교하기가 어려워요.
- 저 부분의 description이 Swagger UI에 안나타나는 부분이네요. 해당 description을 지우는 건 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
넵. 문제 개선안을 혼자 고민하다보니 당연히 커찬도 해당 문제를 인식하고 있을 줄 알았는데 넘겨짚은 감이 있는 것 같습니다. 유의할게요 😅
이 코멘트에서 변경하고 싶었던 부분은 api docs에서 ErrorResponse를 "에러 메시지" 통일하는 것보다 각 에러 상황에 맞도록 에러 객체를 지정하기 위해 method를 만들어주는 건 어떨까? 하는 부분이었어요. -
description이 Swagger UI 에 나타나지 않음은 RestAssured의 실행 결과를 기반으로 adoc 파일이 생성되기 때문이군요!
그럼 지워도 괜찮을 것 같아요.
|
||
@Nested | ||
class Save { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
공통 필드는 변수로 빼주는 건 어떨까요? 다만 request, response builder 시그니처 수정이 조금 필요할 것 같아요.
private final FieldDescriptor[] tableFields = {
fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"),
fieldWithPath("info.name").type(STRING).description("테이블 이름"),
fieldWithPath("info.agenda").type(STRING).description("토론 주제"),
fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"),
fieldWithPath("table[].stance").type(STRING).description("입장"),
fieldWithPath("table[].type").type(STRING).description("발언 유형"),
fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"),
fieldWithPath("table[].speakerNumber").type(NUMBER).description("발언자 번호").optional()
};
private final RestDocumentationRequest requestDocument = request()
.tag(Tag.PARLIAMENTARY_API)
.description("새로운 의회식 토론 시간표 생성")
.queryParameter(
parameterWithName("memberId").description("멤버 ID")
)
.requestBodyField(tableFields);
private final RestDocumentationResponse responseDocument = response()
.responseBodyField(
fieldWithPath("id").type(NUMBER).description("테이블 ID"),
tableFields
);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
공통 필드는 변수로 빼주는 건 어떨까요?
싫어요.
생각을 안해본 건 아닙니다. 하지만, Request Response가 어떻게 생겼는지 한 눈에 잘 안들어 올 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이해했습니다. 커찬이 한번 생각하고 내린 결정이라면 그 방식이 더 합리적인 것 같아요. DocumentTest가 요청-응답을 기록하기 위한 테스트이니 목적에 더 알맞는 명세라는 생각이 들기도 하네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/noti
이미 콜리가 상세히 리뷰해줬고 직접 써봐야 될 것 같아 approve합니다
/noti @coli-geonwoo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/noti
커찬! 답변 잘 확인하여 approve 합니다. 대부분은 설득되었으나 한두가지에서 의견 더 남겼으니 확인해주세요. 모든 의견은 반영보다는 커찬이 한번쯤 생각해주었으만 하는 의견들일 뿐입니다 ㅎㅎ
의견에 대한 반영은 커찬의 자유에 맡기고자 merge는 커찬의 몫으로 남겨두겠습니다.
고생많으셨어요! (<- 코드에서 시간 쏟은게 느껴짐! )
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' | ||
testImplementation 'io.rest-assured:rest-assured:5.5.0' | ||
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' | ||
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다~
this.id = id; | ||
this.nickname = nickname; | ||
} | ||
|
||
public Member(String nickname) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아... 그러네요 이해했습니다! 별도의 변경 없이 지금도 괜찮은 것 같아요. 1/2번이 합리적으로 생성자 체이닝을 못하는 이유라 생각합니다.
protected static long EXIST_MEMBER_ID = 123L; | ||
protected static Member EXIST_MEMBER = new Member(EXIST_MEMBER_ID, "존재하는 멤버"); | ||
|
||
protected static RestDocumentationResponse ERROR_RESPONSE = new RestDocumentationResponse() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
넵. 문제 개선안을 혼자 고민하다보니 당연히 커찬도 해당 문제를 인식하고 있을 줄 알았는데 넘겨짚은 감이 있는 것 같습니다. 유의할게요 😅
이 코멘트에서 변경하고 싶었던 부분은 api docs에서 ErrorResponse를 "에러 메시지" 통일하는 것보다 각 에러 상황에 맞도록 에러 객체를 지정하기 위해 method를 만들어주는 건 어떨까? 하는 부분이었어요. -
description이 Swagger UI 에 나타나지 않음은 RestAssured의 실행 결과를 기반으로 adoc 파일이 생성되기 때문이군요!
그럼 지워도 괜찮을 것 같아요.
private MemberRepository memberRepository; | ||
|
||
@MockitoBean | ||
protected MemberService memberService; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[의견]
커찬의 입장을 이해했습니다. RestAssured에서 뜨는 전반적인 빈들을 다른 documentTest에서도Mocking하는 작업이 필요하다는 거군요!
반영을 바라는 건 아니지만 의견을 말하자면 해당 memberService와 소통하는 다른 controller 계층이 있을지에 대해선 조금 의문이 남는 것 같아요. 서비스가 다계층으로 복잡해진다면 전반적으로 모든 모킹을 BaseXXXtest에 넣는 것이 맞겠지만 그럼 DocumentTest가 추가될때마다 BaseTest가 두꺼워질 것 같아서요. 개인적으로 인증 작업은 MemberRepository 모킹으로 충분하다 생각하고 각각의 DocumentTest에서 필요한 모킹 설정을 해주는 것이 응집성에 좋다 생각합니다. 그런데 현재 서비스는 크게 복잡하지 않으니 지금 구조로도 괜찮은 것 같아요.
return this; | ||
} | ||
|
||
public RestDocumentationRequest pathParameter(ParameterDescriptor... descriptors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인정합니다. 현재 구조로도 괜찮다고 생각해요!
.contentType(ContentType.JSON) | ||
.body(request) | ||
.when().post("/api/member") | ||
.then().log().all() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이해했습니다 👍
} | ||
|
||
@EnumSource( | ||
value = ClientErrorCode.class, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인정합니다. 개인적인 취향으로 제가 MethodSource를 선호하는 이유는 문자열 관리를 싫어하고 + 오타가 많아서 그런가봐요 😓 지금으로도 괜찮다 생각해요!
|
||
@Nested | ||
class Save { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이해했습니다. 커찬이 한번 생각하고 내린 결정이라면 그 방식이 더 합리적인 것 같아요. DocumentTest가 요청-응답을 기록하기 위한 테스트이니 목적에 더 알맞는 명세라는 생각이 들기도 하네요!
/noti |
🚩 연관 이슈
closed #41
🗣️ 리뷰 요구사항 (선택)
기타