-
Notifications
You must be signed in to change notification settings - Fork 5
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
Conversation
Test Results 27 files 27 suites 7s ⏱️ Results for commit dab43fb. ♻️ This comment has been updated with latest results. |
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.
리건 코드 작업 고생하셨습니다 👍
궁금한 부분들 질문 남겨놓았는데 구두로든 코멘트로든 한번 공유해주시면 너무 좋을 것 같아요!
QueryDsl을 도입해야 했던 이유도 궁금하군요.
리건과의 이야기 타임 기대하고 있겠습니다!
우선은 Comment로 리뷰 남겨놓아요!
// 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" | ||
} |
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.
확인 👍
@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); | ||
} |
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.
jakarta.validation
에서 던지는 예외인 듯 하네요? 저희 서비스의 예외로 한번 싸서 던질 수 있으면 좋을 텐데 메서드 핸들러 argument에러 처럼 그럴 수 없는 상황인가보네요.
어떤 예외 잡고있는 것인지 관련해서 어떻게 처리되고 있는지 한 번 설명 듣고 싶은데 구두로든 아니면 코멘트로든 한 번 부탁드려도 될까욥?
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.
TravelogueController
에서 클래스 단위 @Validated
가 없다면 Bean validation 시 HandlerMethodValidationException
이 발생합니다. 이 예외의 문제점이, bean validation에서 따로 지정해둔 메시지가 전부 Validation failure
로 오버라이드 됩니다.
심지어, 400 Bad Request
를 500 Internal Server Error
로 치환까지 해버리기 때문에 다른 방법이 필요했고, 그러던 중 Baeldung에서 이런 글을 발견하여 @Validated
를 클래스 단위에 붙여 메소드 파라미터의 bean validation이 ConstraintViolationException
이 발생하도록 했습니다.
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.
밸덩 확인했습니다. 깔끔한 설명 감사합니다!
근데 밸덩 글이긴 해도 깔끔하지 못한 처리인 것 같긴 하네요 🤔
조금 더 좋은 생각있으면 아래 코멘트에 붙여보겠습니다.
@RequiredArgsConstructor | ||
@Validated | ||
@RestController |
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.
위의 코멘트에 답변드렸습니당~
@NotBlank(message = "검색어는 2글자 이상이어야 합니다.") | ||
@Size(min = 2, message = "검색어는 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.
Bean Validation 정말 헷갈리네요 (@Valid
필요한지 아닌지) 저는 아무리 찾아봐도 동작과 적절한 사용 예시를 못찾겠음요..
어디까지 활용되고 어떻게 사용되는지 코치님들한테 한번 같이 물어보러 가시쥬..
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.
SearchKeyword
에 validation이 많이 붙기도하고 이후에 searchKeyword 여러 개 될 수도 있으니 DTO로 빼고 @Valid
붙여보는건 어떨까용? 될지는 잘 모르겠지만 츄라이츄라이
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.
설명을 듣고나니 지금의 구현도 나름 합리적이라고 생각해서요 아래에서 제안드리는 내용은 리건이 반영여부를 판단해주시면 감사하겠습니다.
검색어는 2글자 이상이어야 한다
를 비즈니스 로직으로 정의SearchCondition
DTO 운용SearchCondition
이라는 도메인 클래스를 만든다.- 비즈니스 로직을 도메인과 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.
Dto에서 검증하는 것도 괜찮아 보이네요.
한 번 고민해보시죵 😄
private final TravelogueRepository travelogueRepository; | ||
private final AwsS3Provider s3Provider; | ||
private final TravelogueQueryRepositoryImpl travelogueQueryRepositoryImpl; | ||
|
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.
동감합니다. 지적 감사해유!
@Bean | ||
public TravelogueQueryRepositoryImpl travelogueQueryRepositoryImpl() { | ||
return new TravelogueQueryRepositoryImpl(jpaQueryFactory()); |
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.
ditto
@TestConfiguration | ||
public class TestQueryDslConfig { | ||
|
||
@PersistenceContext | ||
private EntityManager entityManager; | ||
|
||
@Bean | ||
public JPAQueryFactory jpaQueryFactory() { | ||
return new JPAQueryFactory(entityManager); | ||
} | ||
|
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.
레포지토린데 테스트에서 다른 config 써야하는 이유가 있나유?
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.
QueryDSL
을 활용하는 TravelogueQueryRepositoryImpl
은 JpaRepository
의 구현체가 아니기 때문에 @DataJpaTest
에서 빈으로 감지하지도 못하고 심지어 해당 리포지토리를 활용하는 서비스에서 자동 의존성 주입(Autowire) 후보(candidate)로 판단하지도 못합니다. 해결책을 찾던 도중 테스트 config를 작성해서 직접 빈으로 생성하면 자동 주입을 활용할 수 있다고 해서 활용했습니다.
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.
몇 가지 코멘트 달았지만 어프로브 드립니다~
수고하셨슴다 리건
@NotBlank(message = "검색어는 2글자 이상이어야 합니다.") | ||
@Size(min = 2, message = "검색어는 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.
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() |
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.
PageImpl
이랑 Page<>.map
이랑 별 차이 없다면 후자가 좀 더 코드가 깔끔해질 것 같슴다.
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.
수고하셨습니다 리건!
궁금한 내용 정도만 커멘트로 남기겠습니다람쥐렁이빨대나무다리어카센타이어화둥둥가~
@NotBlank(message = "검색어는 2글자 이상이어야 합니다.") | ||
@Size(min = 2, message = "검색어는 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.
Dto에서 검증하는 것도 괜찮아 보이네요.
한 번 고민해보시죵 😄
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()); | ||
} | ||
} |
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.
저희가 동적 쿼리는 QueryDsl 사용을 컨벤션으로 정했던 것 같은데 추상화를 한 번 하신 이유는 그럼에도 JPQL이나 Native Query가 필요할 것 같아서일까요?
추상화를 한 근거가 궁금하네욤 🤔
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 계층은 다른 계층으로부터 많이 의존됩니다. 이렇게 다른 계층을 의존하지 않고 의존만 당하는 객체를 구체화에 의존하게 된다면 변경에 매우 취약하다고 생각합니다. QueryDSL 외에 다른 기술이 사용될 여지가 많이 적지만, 그래도 미래를 위해서 대책을 세워두는 편이 낫다고 생각했습니다.
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.
수고하셨습니다리건!
@@ -59,8 +59,8 @@ void setUp() { | |||
@Test | |||
void readTravelogues() { | |||
// given | |||
travelogueTestHelper.initTravelogueTestDate(member); | |||
travelogueTestHelper.initTravelogueTestDate(member); |
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.
👍🏻
* 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: 조회 쿼리에 정렬 및 페이지네이션 정보 추가
✅ 작업 내용
🙈 참고 사항
gradle - clean
작업 한번 수행 후,gradle - build
해주셔야 정상 작동할 수도 있습니다.