-
Notifications
You must be signed in to change notification settings - Fork 2
feat: 모든 스타카토 조회 시 위경도 범위/카테고리를 기준으로 조회할 수 있도록 구현 #759 #763 #779
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
Conversation
Test Results 41 files 41 suites 6s ⏱️ Results for commit 9b23842. ♻️ This comment has been updated with latest results. |
🌻Test Coverage Report
|
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.
까다로운 부분인데 구현 잘 해주셨네요, 리니! 수고하셨습니다.
남긴 코멘트 확인 부탁드려요~
@@ -73,4 +86,44 @@ void readAllStaccato() throws Exception { | |||
.andExpect(status().isOk()) | |||
.andExpect(content().json(expectedResponse)); | |||
} | |||
|
|||
@DisplayName("유효하지 않은 위도 또는 경도 쿼리로 스타카토 목록 조회 시 예외가 발생한다.") |
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.
StaccatoLocationRangeRequestFixtures
를 만들어서 사용하는 아래와 같은 방식은 어떻게 생각하시나요?
재사용성이 높지 않아 과하다는 생각도 들지만, 가독성 측면에서는 나쁘지 않은 것 같습니다.
static Stream<Arguments> invalidLatLngProvider() {
return Stream.of(
Arguments.of(StaccatoLocationRangeRequestFixtures.defaultStaccatoLocationRangeRequest()
.withNeLat(BigDecimal.valueOf(-90.0001)).build(), "위도는 -90.0 이상이어야 합니다."),
Arguments.of(StaccatoLocationRangeRequestFixtures.defaultStaccatoLocationRangeRequest()
.withNeLat(BigDecimal.valueOf(90.0001)).build(), "위도는 90.0 이하여야 합니다."),
Arguments.of(StaccatoLocationRangeRequestFixtures.defaultStaccatoLocationRangeRequest()
.withNeLng(BigDecimal.valueOf(-180.0001)).build(), "경도는 -180.0 이상이어야 합니다."),
Arguments.of(StaccatoLocationRangeRequestFixtures.defaultStaccatoLocationRangeRequest()
.withNeLng(BigDecimal.valueOf(180.0001)).build(), "경도는 180.0 이하여야 합니다.")
);
}
@DisplayName("유효하지 않은 위도 또는 경도 쿼리로 스타카토 목록 조회 시 예외가 발생한다.")
@ParameterizedTest
@MethodSource("invalidLatLngProvider")
void failReadAllStaccatoWithInvalidSingleLatLng(StaccatoLocationRangeRequest invalidRequest, String expectedMessage) throws Exception {
// given
ExceptionResponse exceptionResponse = new ExceptionResponse(
HttpStatus.BAD_REQUEST.toString(),
expectedMessage);
// when & then
mockMvc.perform(get("/v2/staccatos")
.param("neLat", String.valueOf(invalidRequest.neLat()))
.param("neLng", String.valueOf(invalidRequest.neLng()))
.param("swLat", String.valueOf(invalidRequest.swLat()))
.param("swLng", String.valueOf(invalidRequest.swLng()))
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isBadRequest())
.andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse)));
}
public class StaccatoLocationRangeRequestFixtures {
public static StaccatoLocationRangeRequestBuilder defaultStaccatoLocationRangeRequest() {
return new StaccatoLocationRangeRequestBuilder()
.withCategoryId(null)
.withNeLat(BigDecimal.valueOf(0))
.withNeLng(BigDecimal.valueOf(0))
.withSwLat(BigDecimal.valueOf(0))
.withSwLng(BigDecimal.valueOf(0));
}
public static class StaccatoLocationRangeRequestBuilder {
Long categoryId;
BigDecimal neLat;
BigDecimal neLng;
BigDecimal swLat;
BigDecimal swLng;
public StaccatoLocationRangeRequestBuilder withCategoryId(Long categoryId) {
this.categoryId = categoryId;
return this;
}
public StaccatoLocationRangeRequestBuilder withNeLat(BigDecimal neLat) {
this.neLat = neLat;
return this;
}
public StaccatoLocationRangeRequestBuilder withNeLng(BigDecimal neLng) {
this.neLng = neLng;
return this;
}
public StaccatoLocationRangeRequestBuilder withSwLat(BigDecimal swLat) {
this.swLat = swLat;
return this;
}
public StaccatoLocationRangeRequestBuilder withSwLng(BigDecimal swLng) {
this.swLng = swLng;
return this;
}
public StaccatoLocationRangeRequest build() {
return new StaccatoLocationRangeRequest(categoryId, neLat, neLng, swLat, swLng);
}
}
}
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.
RequestFixture를 분리하려다가 하지 않았던 이유를 공유드려요!
StaccatoLocationRangeRequest는 내부의 모든 값들이 기준이 되어 핵심 로직 테스트가 이루어지게 됩니다.
그렇다보니, 매 테스트마다 모든 값들에 대해 개발자가 설정을 해주어야 해요.
그렇다보니 픽스처를 분리하더라도 그 이점을 누리기 어렵다고 판단하여 따로 만들지 않았습니다.
폭포는 어떻게 생각하시나요?
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.
매 테스트마다 모든 값들에 대해 개발자가 설정을 해주어야 해요.
그렇다보니 픽스처를 분리하더라도 그 이점을 누리기 어렵다고 판단하여 따로 만들지 않았습니다.
충분히 납득가는 이유네요! 😄 이러면 저도 굳이 만들 필요 없다고 생각합니다.
.map(CategoryMember::getCategory) | ||
.map(Category::getId) | ||
.toList(); | ||
return new StaccatoLocationResponsesV2(responses); |
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.
이번 변경사항은 아니지만, 전체적인 코드를 보다가 발견했습니다.
이 코드 상단에 있는 Entity -> Dto 변환 로직을 Dto 내부로 이동시키는 것에 대해서 어떻게 생각하시나요?
public record StaccatoLocationResponsesV2(List<StaccatoLocationResponseV2> staccatoLocationResponses) {
public static StaccatoLocationResponsesV2 from(List<Staccato> staccatos) {
return new StaccatoLocationResponsesV2(
staccatos.stream()
.map(staccato -> new StaccatoLocationResponseV2(staccato, staccato.getCategory().getColor()))
.toList()
);
}
// ...
}
public StaccatoLocationResponsesV2 readAllStaccato(Member member, StaccatoLocationRangeRequest staccatoLocationRangeRequest) {
List<Staccato> staccatos = staccatoRepository.findByMemberAndLocationRangeAndCategory(
member,
staccatoLocationRangeRequest.swLat(),
staccatoLocationRangeRequest.neLat(),
staccatoLocationRangeRequest.swLng(),
staccatoLocationRangeRequest.neLng(),
staccatoLocationRangeRequest.categoryId()
);
return StaccatoLocationResponsesV2.from(staccatos);
}
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.
마찬가지로 이번 변경사항은 아니지만, StaccatoLocationResponseV2
를 보다가 발견한건데,
여기서 왜 color를 staccato를 통해서 얻지 않고 직접 전달하는 방식으로 구현되어있는지 궁금하네요.
// 현재 코드
public StaccatoLocationResponseV2(Staccato staccato, Color color) {
this(staccato.getId(), color.getName(), staccato.getSpot().getLatitude(), staccato.getSpot().getLongitude());
}
// 수정 버전
public StaccatoLocationResponseV2(Staccato staccato) {
this(staccato.getId(), staccato.getColor().getName(), staccato.getSpot().getLatitude(), staccato.getSpot().getLongitude());
}
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.
이 코드 상단에 있는 Entity -> Dto 변환 로직을 Dto 내부로 이동시키는 것에 대해서 어떻게 생각하시나요?
staccato.getCategory().getColor()
를 지연로딩으로 처리하지 않고 fetch Join
으로 바로 가져와서 처리하게 된다면 Controller
에서는 트랜잭션이 유지되지 않는 현재 전략을 그대로 가져가면서 변환로직을 DTO
내부로 이동시킬 수 있겠네용
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.
여기서 왜 color를 staccato를 통해서 얻지 않고 직접 전달하는 방식으로 구현되어있는지 궁금하네요.
스타카토는 카테고리의 색상을 따른다
는 것은 비즈니스 정책 중 하나라고 생각했어요. 따라서, 서비스 계층에서 알고 처리해야하는 부분이라고 생각했어요.
폭포는 어떻게 생각하시나요?
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.
스타카토는 카테고리의 색상을 따른다는 것은 비즈니스 정책 중 하나라고 생각했어요. 따라서, 서비스 계층에서 알고 처리해야하는 부분이라고 생각했어요.
"스타카토는 카테고리의 색상을 따른다는 것은 비즈니스 정책 중 하나이며, 이는 서비스 계층에서 알고 처리해야 하는 부분이다"라는 부분은 매우 동의합니다. 다만, 저는 코드 그 자체의 가독성과 사용성 관점을 좀더 많이 바라봤던 것 같습니다.
staccatoId를 갖고 있는 staccato는 반드시 정해진 color를 갖고 있습니다. 그런데 public StaccatoLocationResponseV2(Staccato staccato, Color color)
이런 생성자가 있으면, 마치 스타카토와 색깔을 주면 해당 색깔을 갖는 스타카토를 만들어버리는 느낌을 받았습니다.
사용부에서도 Staccato class 내부의 getColor()
메서드를 사용하게 되면 new StaccatoLocationResponseV2(staccato, staccato.getColor())
이렇게 될텐데, staccato를 줬는데 왜 color를 나눠주는거지 라는 생각이 들게 합니다.
양쪽을 다 챙길 수 있는 방안이 있는지는 좀더 고민해봐야 할것 같네요. 🤔
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.
음... 어떻게 보면 staccato.getColor()
내부에 staccato의 색깔을 결정하는 책임은 staccato에게 있고, 그 색깔이 카테고리의 색깔을 따른다는 것도 그 일부라고 볼 수도 있을 것 같네요!
반영했습니다👍👍
staccato2), | ||
() -> assertThat(anotherMemberResult.size()).isEqualTo(1), | ||
() -> assertThat(anotherMemberResult).containsExactlyInAnyOrder(anotherStaccato) | ||
assertThat(result).hasSize(2).containsExactlyInAnyOrder(staccato1, staccato2); |
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.
의미적으로 봤을 때, 생성된 anotherStaccato
를 활용해서 아래 코드처럼 만들어도 좋을 것 같습니다.
assertThat(result)
.hasSize(2)
.containsExactlyInAnyOrder(staccato1, staccato2)
.doesNotContain(anotherStaccato);
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.
궁금한 점이 있습니다!
staccato1과 staccato2만 포함이 되어야 한다 == 나머지(anotherStaccato)는 포함되어야하지 않는다. 이기 때문에 의미가 중첩된다고 생각했는데요.
조금 더 구체적인 폭포 의견을 들어보고 싶어요!
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.
anotherStaccato
를 만들지 않아도 테스트는 통과합니다. 하지만 그렇게 하면, 단순히 만든 모든 객체를 조회하는 의미밖에 가지지 않습니다. 조건을 만족하지 않는 객체를 만든 의미를 살려서, 해당 객체들은 걸러져서 포함되지 않는다는걸 명시해주고 싶었습니다.
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.
검증 측면에서는 containsExcatlyInAnyOrder()
만으로 이미 충분하다고 생각해요. 사실 anotherStaccato
를 만들지 않는다면, 모든 테스트 데이터가 조건에 만족하는 객체이기 때문에 모든 객체가 조회되는 것이 당연하지 않을까 생각이 들었어요. (어떻게 보면, doesNotContain()
에 들어갈 데이터를 만들지 않는다는 말이기도 하니까요)
테스트의 가독성이나 의도를 더 드러낸다는 측면에서는 폭포가 말한 방식이 매력적으로 다가오네요! 반영하겠습니다. 좋은 의견 감사해요😆
); | ||
|
||
// then | ||
assertThat(result).hasSize(3).containsExactlyInAnyOrder(inside1, inside2, inside3); |
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.
위 코멘트와 동일합니다. doesNotContainAnyElementOf
라는 메서드도 있긴 하지만, 굳이라는 생각이 들어서 사용하지 않았습니다. 기능은 똑같고 가독성 측면에서 약간 차이가 있습니다.
assertThat(result)
.hasSize(3)
.containsExactlyInAnyOrder(inside1, inside2, inside3)
.doesNotContain(outsideLat, outsideLng);
StaccatoLocationRangeRequest request = new StaccatoLocationRangeRequest( | ||
null, | ||
new BigDecimal("37.5"), | ||
new BigDecimal("127.5"), | ||
new BigDecimal("36.5"), | ||
new BigDecimal("126.5") | ||
); |
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.
StaccatoLocationRangeRequestFixtures
를 만든다면, 이 코드에서 해당 Fixture를 활용할 수 있을 것 같습니다.
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.
Fixtures를 만들만한 충분한 이유가 사라져서, 이 부분도 그대로 유지가 좋을 것 같아요!
@@ -160,6 +160,61 @@ void readAllStaccato() { | |||
); | |||
} | |||
|
|||
@DisplayName("특정 사용자의 위경도 범위 내에 있는 모든 스타카토 목록 조회에 성공한다.") |
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.
위경도/카테고리 정보 등등 조건에 따라 스타카토 목록이 조회되는 것은 repository의 책임이라고 생각했어요. 따라서, 해당 테스트에서는 조회가 제대로 되고, 색상 매핑이 잘 되는지를 판단하는 정도로 구성하고, repositoryTest를 조금 더 보충했습니다.
); | ||
|
||
// then | ||
assertThat(result).hasSize(2).containsExactlyInAnyOrder(category1Inside1, category1Inside2); |
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.
이 부분도 동일합니다!
assertThat(result)
.hasSize(2)
.containsExactlyInAnyOrder(category1Inside1, category1Inside2)
.doesNotContain(category1Outside, category2Inside);
) | ||
AND (:categoryId IS NULL OR c.id = :categoryId) | ||
""") | ||
List<Staccato> findByMemberAndLocationRangeAndCategory( |
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.
제 생각이 맞을지 모르겠는데, 색깔을 가져올때 staccato.getCategory()
를 사용하기 때문에, 이 쿼리에서 fetch join을 사용하지 않으면 N+1 문제가 발생할 것 같습니다. 어떻게 생각하시나요?
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.
N+1이 충분히 발생할 것 같네용
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.
고생하셨습니다 리니!!!
간단한 코멘트 몇개 남겨놓았으니, 확인 부탁드려요
) | ||
AND (:categoryId IS NULL OR c.id = :categoryId) | ||
""") | ||
List<Staccato> findByMemberAndLocationRangeAndCategory( |
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.
N+1이 충분히 발생할 것 같네용
public record StaccatoLocationRangeRequest( | ||
@Schema(example = SwaggerExamples.CATEGORY_ID) | ||
@Min(value = 1L, message = "카테고리 식별자는 양수로 이루어져야 합니다.") | ||
Long categoryId, |
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
를 통해 프로덕션 코드가 더럽혀진다는 것을 확실히 느낄 수 있었습니닷... 문서화에 대해서도 조만간 한번 얘기해 보시죠!!
.map(CategoryMember::getCategory) | ||
.map(Category::getId) | ||
.toList(); | ||
return new StaccatoLocationResponsesV2(responses); |
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.
이 코드 상단에 있는 Entity -> Dto 변환 로직을 Dto 내부로 이동시키는 것에 대해서 어떻게 생각하시나요?
staccato.getCategory().getColor()
를 지연로딩으로 처리하지 않고 fetch Join
으로 바로 가져와서 처리하게 된다면 Controller
에서는 트랜잭션이 유지되지 않는 현재 전략을 그대로 가져가면서 변환로직을 DTO
내부로 이동시킬 수 있겠네용
.param("neLat", invalidParamKey.equals("neLat") ? invalidParamValue : "37.5") | ||
.param("neLng", invalidParamKey.equals("neLng") ? invalidParamValue : "127.0") |
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.
경계값 테스트를 위해 ParameterizedTest를 수행하는데, 어떤 query String key를 대상으로 하는지에 따라 유효값을 사용할지 말지를 결정해야하다보니 조건문 분기가 필요해졌어요.
이 테스트에서 같은 경우에는 조건문으로 분기하는 것이 오히려 가독성을 헤칠 수 있겠다는 생각이 들어 삼항 연산자를 활용했는데,, 테스트가 깔끔하지 않아 마음에 안들긴 하네요! 조금 더 리팩터링 해보겠습니다~
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를 분리하겠다고 말씀드렸었는데, 최종적으로는 분리하지 않기로 결정했습니다. 그 이유는 고민하는 과정에서 제가 API의 책임을 화면 단위로만 바라보고 있었다는 점을 깨달았기 때문입니다. 그런데 REST 관점에서 다시 고민해보니, staccato라는 동일한 자원을 다양한 조건으로 필터링해 조회하는 것이라면 API가 하나로 묶여 있는 것이 어색하지 않다는 결론에 도달했습니다. 물론 이 결정은 제 개인적인 판단이고, 혹시 다른 분들은 어떻게 생각하시는지도 궁금합니다! |
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를 분리하겠다고 말씀드렸었는데, 최종적으로는 분리하지 않기로 결정했습니다.
리니가 담당하신 태스크이고 논리도 납득이 가기에 그대로 진행하셔도 좋을 것 같아요. 그렇지만 의견이 궁금하다고 하셨으니 제 의견을 간다안 하게 남겨보겠습니다.
- 단순하게 생각해보았을 때.. Staccato는 Category의 하위 리소스로 표현되어 있고, 카테고리 내부 자원에 대한 조회가 이루어지기에 category를 통한 uri 접근이 이루어져야 된다고 생각했습니다.
다른 예로 폴더
라는 상위 리소스 안에 파일
이라는 하위 리소스가 있었다고 가정하겠습니다. 여기서 화면에 보여지는 모든 파일을 조회하는 것과 특정 a 폴더 내부에 위치하는 파일들을 조회하는 경우 서로 같은 uri
를 통해 접근되는 것이 어색하게 느껴집니다.
물론 제시한 예가 완벽히 저희와 동일한 상황이 아니기에 어느정도 이런 느낌이다를 전달하고 싶었습니다 🙃
의견에 대한 토의가 이루어질 것 같으니 RC
우선 드리겠습니다.
@@ -122,41 +119,38 @@ void failCreateStaccato() { | |||
.hasMessageContaining("요청하신 카테고리를 찾을 수 없어요."); | |||
} | |||
|
|||
@DisplayName("스타카토 목록 조회에 성공한다.") | |||
@DisplayName("주어진 조건에 만족하는 스타카토 목록을 조회한다.") |
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.
사소할 수 있지만... 테스트 메서드명과 @DisplayName
이 조금은 삐그덕거리는 느낌이 있습니다.
@DisplayName
에 적혀있는 주어진 조건
을 조금더 구체적으로 작성하는건 어떤가요!
윽,,설득 되어 버렸습니다............. 팔랑귀는 API 분리하러 갑니다...ㅋㅋㅋㅋ |
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.
정말 수고많으셨습니다, 리니!!!
제가 남긴 코멘트는 답변 + 이후에 고민해보면 좋을 점들 이라서, 당장 변경사항이 필요한 부분은 없기 때문에 approve로 했습니다.
심해 탐험 ㄱㅂㅈㄱ~
.map(CategoryMember::getCategory) | ||
.map(Category::getId) | ||
.toList(); | ||
return StaccatoLocationResponsesV2.of(staccatos); |
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.
역시 도메인->Dto 변환 로직은 Dto가 갖고 있어야 서비스 코드가 간결해지네요! 👍 👍 👍
public StaccatoLocationResponseV2(Staccato staccato, Color color) { | ||
this(staccato.getId(), color.getName(), staccato.getSpot().getLatitude(), staccato.getSpot().getLongitude()); | ||
public StaccatoLocationResponseV2(Staccato staccato) { | ||
this(staccato.getId(), staccato.getColor().getName(), staccato.getSpot().getLatitude(), staccato.getSpot() |
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.
음... 어떻게 보면 staccato.getColor() 내부에 staccato의 색깔을 결정하는 책임은 staccato에게 있고, 그 색깔이 카테고리의 색깔을 따른다는 것도 그 일부라고 볼 수도 있을 것 같네요!
오히려 저보다 깔끔하게 정리해주셨네요 ㅋㅋㅋ 👍 👍 👍
|
||
// then | ||
assertThat(result).hasSize(2).containsExactlyInAnyOrder(staccatoInCategory1, staccatoInCategory2) | ||
.doesNotContain(anotherStaccato); |
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.
검증 측면에서는 containsExcatlyInAnyOrder() 만으로 이미 충분하다고 생각해요.
동의합니다! 검증 측면에서는 저도 이것만으로 충분하다고 생각해요.
테스트의 가독성이나 의도를 더 드러낸다는 측면에서는 폭포가 말한 방식이 매력적으로 다가오네요! 반영하겠습니다. 좋은 의견 감사해요😆
다만, 의도를 더 드러내기 위해서였습니다. annotherStaccato
만들고 안쓰면 섭섭해할 것 같아서요 😄
JOIN FETCH s.category c | ||
JOIN FETCH c.categoryMembers cm |
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:N 관계에서 JOIN FETCH
를 사용하는 경우, 중복 row로 인해 불필요한 네트워크 트래픽과 메모리 소비를 하게 됩니다. (DISTINCT
의 중복 제거는 SQL 레벨이 아닌 JPA에서 동작)
이를 해결할 수 있는 방법을 검색해보니까, 정말 많은 방법이 나오네요. (여기에 다 정리 못할 정도로)
@EntityGraph
사용- DISTINCT + DTO로 매핑
- Hibernate
@BatchSize
- 쿼리 분리
저는 각각의 조회 쿼리를 최적화할 수 있고 그만큼 유지보수가 쉬워진다는 장점에서 쿼리 분리가 좋을 것 같습니다. (Category
먼저 조회하는 쿼리 + CategoryMember
별도로 조회하는 쿼리)
일단 JOIN FETCH
쓰도록 만들고, 여러 방법 시도해보면서 시간 개선하며 시나리오 만들어봐요~ ㅎㅎ 😄
@@ -231,4 +232,22 @@ void updateCategoryUpdatedDateWhenStaccatoDeleted() { | |||
assertThat(afterDelete).isAfter(beforeDelete); | |||
} | |||
} | |||
|
|||
@DisplayName("스타카토의 색상은 카테고리의 색상을 따른다.") |
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.perform(get("/v2/staccatos") | ||
.param("neLat", neLat) | ||
.param("neLng", neLng) | ||
.param("swLat", swLat) | ||
.param("swLng", swLng) |
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.
Fixture 사용보다는 이 방식이 훨씬 낫네요! 👍
@@ -12,11 +13,47 @@ public interface StaccatoRepository extends JpaRepository<Staccato, Long> { | |||
@Query("SELECT s FROM Staccato s WHERE s.category.id = :categoryId order by s.visitedAt desc, s.createdAt desc") | |||
List<Staccato> findAllByCategoryIdOrdered(long categoryId); | |||
|
|||
List<Staccato> findAllByCategory_CategoryMembers_Member(Member member); | |||
@Query(""" | |||
SELECT DISTINCT s |
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.
생각해보니 저희 항상 SELECT ALL
을 사용하고 있었네요. 스타카토는 워낙 속성값들이 많다보니, 가져오는 속성 값을 명확하게 지정하면 유의미한 차이가 있을지도 모르겠다는 생각이 들었습니다.
ResponseDto
에서 필요한 값이 아닌 다른 값을 비즈니스 로직에서 사용하지 않는다.- 엔티티를 수정하여 변경 감지가 일어나야 하는 로직이 없다.
- 엔티티 연관관계를 사용하지 않는다.
와 같은 이유로, 엔티티 전체 조회말고 DTO projection을 시도해봐도 좋을 것 같습니다! 이것도 일단 지금 방법으로 머지한 후에, 심해탐험에서 해보시죠!
@@ -76,6 +78,17 @@ public ResponseEntity<CategoryDetailResponse> readCategory( | |||
return ResponseEntity.ok(categoryDetailResponse.toCategoryDetailResponse()); | |||
} | |||
|
|||
@GetMapping("/{categoryId}/staccatos") |
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 따로, 하나의 카테고리에 담긴 스타카토 전체를 불러오는 API 따로로 만들었다면,
후자의 API를 /categories/{categoryId}/staccatos
로 만들었을테고, 그러면 이번엔 이 API랑 겹쳐버리게 되겠네요.
당장 저 API endpoint만 봤을 때는 하나의 카테고리에 속한 스타카토들의 모든 정보를 불러온다는 느낌이 강해서 생각해봤습니다.
그런 의미에서 이 API는 staccatos?categoryId={categoryId}
가 되어야 할 것 같은데, 그러면 다시 쿼리스트링의 고민으로 돌아오네요. RESTful한 API 만드는 게 이렇게 어려울 줄이야...
이 부분은 좀더 의논이 필요할 것 같습니다. 일단 이대로 가시죠!
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.
음 저는 호티 의견에 동의했는데요.
category에 속한 staccato들을 조회하는 것이기 때문에 위의 표기가 적합하게 다가왓어요!
그리고
당장 저 API endpoint만 봤을 때는 하나의 카테고리에 속한 스타카토들의 모든 정보를 불러온다는 느낌이 강해서 생각해봤습니다.
이 부분에 대해서는 뒤에 Query String이 따라오지 않는다면 모든 스타카토 목록을 조회하는 것이 맞고, Query String이 따라온다면, 그 범위에 해당하는 스타카토 목록을 조회하는 것이다보니 REST하다고 생각했습니다!
.withCategory(anotherMemberCategory).buildAndSave(categoryMemberRepository); | ||
@Nested | ||
@DisplayName("사용자(member)의 스타카토 목록 조회") | ||
class FindAllStaccatosBy { |
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.
테스트 엄청 치밀하고 꼼꼼하게 만드셨네요! 수고하셨습니다 👍
가독성도 좋아서 한눈에 이해하기 쉬웠어요 😄
@Test | ||
void findAllStaccatoByMemberWithLocationRange() { | ||
// given | ||
BigDecimal threshold = new BigDecimal("0.01"); |
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/+1로는 부족하지 않을까 threshold 사용하면 더 좋지 않을까라는 생각했었는데, 리니가 구현해주셨네요 ㅎㅎ
이 방식 좋은 것 같습니다!
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.
컨트롤러 분리하는 작업도 꽤 번거로운 작업이셨을텐데, 정말 고생많으셨습니다 리니!!!
오랜만에 Restful한 api가 무엇인가에 대해 깊게 고민해볼 수 있었던것같아요 👍👍👍
CategoryMemberFixtures.defaultCategoryMember() | ||
.withMember(anotherMember) | ||
.withCategory(anotherMemberCategory).buildAndSave(categoryMemberRepository); | ||
@Nested |
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.
해도해도 끝이 없는 테스트 세계.....
⭐️ Issue Number
🚩 Summary
조회할 수 있도록, jpql로 구현했습니다.
🛠️ Technical Concerns
🙂 To Reviewer
📋 To Do