Skip to content
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

[Feature] - 여행기 제목 기준 검색 기능 구현 #302

Merged
merged 19 commits into from
Aug 14, 2024

Conversation

hangillee
Copy link

✅ 작업 내용

  • 여행기의 제목을 기준으로 여행기를 조회하는 기능 구현
  • QueryDSL을 도입하여 사용자가 입력한 검색 키워드 공백을 무시하는 검색 쿼리 작성

🙈 참고 사항

  • QueryDSL 의존성이 추가되었기 때문에, gradle - clean 작업 한번 수행 후, gradle - build 해주셔야 정상 작동할 수도 있습니다.

@hangillee hangillee added the BE label Aug 13, 2024
@hangillee hangillee added this to the sprint 4 milestone Aug 13, 2024
@hangillee hangillee self-assigned this Aug 13, 2024
Copy link

github-actions bot commented Aug 13, 2024

Test Results

 27 files   27 suites   7s ⏱️
200 tests 200 ✅ 0 💤 0 ❌
214 runs  214 ✅ 0 💤 0 ❌

Results for commit dab43fb.

♻️ This comment has been updated with latest results.

Copy link

@Libienz Libienz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리건 코드 작업 고생하셨습니다 👍

궁금한 부분들 질문 남겨놓았는데 구두로든 코멘트로든 한번 공유해주시면 너무 좋을 것 같아요!
QueryDsl을 도입해야 했던 이유도 궁금하군요.

리건과의 이야기 타임 기대하고 있겠습니다!

우선은 Comment로 리뷰 남겨놓아요!

Comment on lines +55 to 60
// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인 👍

Comment on lines 42 to 56
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ExceptionResponse> handleConstraintViolationException(
ConstraintViolationException exception
) {
log.warn("CONSTRAINT_VIOLATION_EXCEPTION :: message = {}", exception.getMessage());

String message = exception.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.findFirst()
.orElseThrow();
ExceptionResponse data = new ExceptionResponse(message);
return ResponseEntity.badRequest()
.body(data);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jakarta.validation에서 던지는 예외인 듯 하네요? 저희 서비스의 예외로 한번 싸서 던질 수 있으면 좋을 텐데 메서드 핸들러 argument에러 처럼 그럴 수 없는 상황인가보네요.

어떤 예외 잡고있는 것인지 관련해서 어떻게 처리되고 있는지 한 번 설명 듣고 싶은데 구두로든 아니면 코멘트로든 한 번 부탁드려도 될까욥?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TravelogueController에서 클래스 단위 @Validated가 없다면 Bean validation 시 HandlerMethodValidationException이 발생합니다. 이 예외의 문제점이, bean validation에서 따로 지정해둔 메시지가 전부 Validation failure로 오버라이드 됩니다.
스크린샷 2024-08-14 오전 10 07 35
심지어, 400 Bad Request500 Internal Server Error로 치환까지 해버리기 때문에 다른 방법이 필요했고, 그러던 중 Baeldung에서 이런 글을 발견하여 @Validated를 클래스 단위에 붙여 메소드 파라미터의 bean validation이 ConstraintViolationException이 발생하도록 했습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

밸덩 확인했습니다. 깔끔한 설명 감사합니다!

근데 밸덩 글이긴 해도 깔끔하지 못한 처리인 것 같긴 하네요 🤔
조금 더 좋은 생각있으면 아래 코멘트에 붙여보겠습니다.

Comment on lines 38 to 40
@RequiredArgsConstructor
@Validated
@RestController
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

얘는 컨트롤러 레벨에서 왜 필요한가요!?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위의 코멘트에 답변드렸습니당~

Comment on lines 127 to 128
@NotBlank(message = "검색어는 2글자 이상이어야 합니다.")
@Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean Validation 정말 헷갈리네요 (@Valid필요한지 아닌지) 저는 아무리 찾아봐도 동작과 적절한 사용 예시를 못찾겠음요..

어디까지 활용되고 어떻게 사용되는지 코치님들한테 한번 같이 물어보러 가시쥬..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 한 번 찾아봐야겠네용

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchKeyword에 validation이 많이 붙기도하고 이후에 searchKeyword 여러 개 될 수도 있으니 DTO로 빼고 @Valid 붙여보는건 어떨까용? 될지는 잘 모르겠지만 츄라이츄라이

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설명을 듣고나니 지금의 구현도 나름 합리적이라고 생각해서요 아래에서 제안드리는 내용은 리건이 반영여부를 판단해주시면 감사하겠습니다.

  • 검색어는 2글자 이상이어야 한다를 비즈니스 로직으로 정의
  • SearchCondition DTO 운용
  • SearchCondition이라는 도메인 클래스를 만든다.
  • 비즈니스 로직을 도메인과 DTO에 적절히 응집

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dto에서 검증하는 것도 괜찮아 보이네요.
한 번 고민해보시죵 😄

Comment on lines 20 to 23
private final TravelogueRepository travelogueRepository;
private final AwsS3Provider s3Provider;
private final TravelogueQueryRepositoryImpl travelogueQueryRepositoryImpl;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현체보단 인터페이스를 알고있어야 할 듯 합니다 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동감합니다. 지적 감사해유!

Comment on lines 21 to 23
@Bean
public TravelogueQueryRepositoryImpl travelogueQueryRepositoryImpl() {
return new TravelogueQueryRepositoryImpl(jpaQueryFactory());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Comment on lines 10 to 20
@TestConfiguration
public class TestQueryDslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레포지토린데 테스트에서 다른 config 써야하는 이유가 있나유?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryDSL을 활용하는 TravelogueQueryRepositoryImplJpaRepository의 구현체가 아니기 때문에 @DataJpaTest에서 빈으로 감지하지도 못하고 심지어 해당 리포지토리를 활용하는 서비스에서 자동 의존성 주입(Autowire) 후보(candidate)로 판단하지도 못합니다. 해결책을 찾던 도중 테스트 config를 작성해서 직접 빈으로 생성하면 자동 주입을 활용할 수 있다고 해서 활용했습니다.

Copy link

@Libienz Libienz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변 꼼꼼히 달아주셔서 감사합니다~

덕분에 이해 충분히 할 수 있었어요 👍

Copy link

@eunjungL eunjungL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

몇 가지 코멘트 달았지만 어프로브 드립니다~
수고하셨슴다 리건

Comment on lines 127 to 128
@NotBlank(message = "검색어는 2글자 이상이어야 합니다.")
@Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchKeyword에 validation이 많이 붙기도하고 이후에 searchKeyword 여러 개 될 수도 있으니 DTO로 빼고 @Valid 붙여보는건 어떨까용? 될지는 잘 모르겠지만 츄라이츄라이

public Page<TravelogueSimpleResponse> findSimpleTraveloguesByKeyword(Pageable pageable, String keyword) {
Page<Travelogue> travelogues = travelogueService.findByKeyword(keyword, pageable);

return new PageImpl<>(travelogues.stream()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageImpl이랑 Page<>.map이랑 별 차이 없다면 후자가 좀 더 코드가 깔끔해질 것 같슴다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다! 고쳐볼게용

Copy link

@slimsha2dy slimsha2dy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다 리건!
궁금한 내용 정도만 커멘트로 남기겠습니다람쥐렁이빨대나무다리어카센타이어화둥둥가~

Comment on lines 127 to 128
@NotBlank(message = "검색어는 2글자 이상이어야 합니다.")
@Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dto에서 검증하는 것도 괜찮아 보이네요.
한 번 고민해보시죵 😄

Comment on lines 16 to 27
public class TravelogueQueryRepositoryImpl implements TravelogueQueryRepository {

private final JPAQueryFactory jpaQueryFactory;

@Override
public Page<Travelogue> findByTitleContaining(String keyword, Pageable pageable) {
return new PageImpl<>(jpaQueryFactory.selectFrom(travelogue)
.where(Expressions.stringTemplate("replace({0}, ' ', '')", travelogue.title)
.containsIgnoreCase(keyword.replace(" ", "")))
.fetch());
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희가 동적 쿼리는 QueryDsl 사용을 컨벤션으로 정했던 것 같은데 추상화를 한 번 하신 이유는 그럼에도 JPQL이나 Native Query가 필요할 것 같아서일까요?
추상화를 한 근거가 궁금하네욤 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repository 계층은 다른 계층으로부터 많이 의존됩니다. 이렇게 다른 계층을 의존하지 않고 의존만 당하는 객체를 구체화에 의존하게 된다면 변경에 매우 취약하다고 생각합니다. QueryDSL 외에 다른 기술이 사용될 여지가 많이 적지만, 그래도 미래를 위해서 대책을 세워두는 편이 낫다고 생각했습니다.

Copy link

@eunjungL eunjungL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다리건!

@@ -59,8 +59,8 @@ void setUp() {
@Test
void readTravelogues() {
// given
travelogueTestHelper.initTravelogueTestDate(member);
travelogueTestHelper.initTravelogueTestDate(member);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😓 오타의 주범 정말 죄송합니다

Copy link

@Libienz Libienz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

@hangillee hangillee merged commit 116d079 into develop/be Aug 14, 2024
3 checks passed
@hangillee hangillee deleted the feature/be/#254 branch August 20, 2024 01:06
hangillee added a commit to hangillee/2024-touroot that referenced this pull request Aug 20, 2024
* feat: 여행기 제목 키워드 기준 검색 기능 구현

* feat: 여행기 검색 기능을 위한 키워드 검증 예외 처리

* feat: QueryDSL 의존성 추가

* refactor: 예외 메시지 추출 로직 변경

* refactor: API 문서 설명 수정

* test: 여행기 제목 키워드 검색 기능 테스트 작성

* feat: 여행기 제목 키워드 기준 검색 기능 구현

* refactor: 추상화에 의존하도록 변경

* refactor: 필드 final 추가

* refactor: DTO 변환 과정 개선

* refactor: 필요 없어진 예외 처리 로직 제거

* refactor: 검색 키워드 request parameter DTO로 분리

* refactor: 검색 메소드 시그니처 리팩토링

* refactor: 여행기 조회 테스트 검증 대상 수정

* chore: 오타 수정

* refactor: pagination 관련 테스트 fixture 수정

* fix: conflict 해결

* refactor: 조회 쿼리에 정렬 및 페이지네이션 정보 추가
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants