-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 사용자 검색 api 구현 #113
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
[feat] 사용자 검색 api 구현 #113
Changes from all commits
800d49c
5bc42c4
0cf7ddb
ad22612
5462afd
48f4960
38d25e2
0d31ac3
321eadd
b42806f
8d02525
1d7df08
0b9f378
46adbec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import static konkuk.thip.common.exception.code.ErrorCode.RECENT_SEARCH_NOT_FOUND; | ||
| import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; | ||
|
|
||
| @Repository | ||
|
|
@@ -23,14 +24,31 @@ public class RecentSearchCommandPersistenceAdapter implements RecentSearchComman | |
| private final RecentSearchMapper recentSearchMapper; | ||
|
|
||
| @Override | ||
| public void save(Long userId,RecentSearch recentSearch) { | ||
| public void save(RecentSearch recentSearch) { | ||
|
|
||
| UserJpaEntity userJpaEntity = userJpaRepository.findById(userId) | ||
| UserJpaEntity userJpaEntity = userJpaRepository.findById(recentSearch.getUserId()) | ||
| .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
|
|
||
| RecentSearchJpaEntity recentSearchJpaEntity = | ||
| recentSearchMapper.toJpaEntity(recentSearch, userJpaEntity); | ||
|
|
||
| recentSearchJpaRepository.save(recentSearchJpaEntity); | ||
| } | ||
|
|
||
| @Override | ||
| public void delete(Long id) { | ||
| recentSearchJpaRepository.delete( | ||
| recentSearchJpaRepository.findById(id) | ||
| .orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND)) | ||
| ); | ||
| } | ||
|
Comment on lines
+38
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion delete 메서드 구현을 개선해주세요 현재 구현에서 다음과 같이 개선해주세요: @Override
public void delete(Long id) {
- recentSearchJpaRepository.delete(
- recentSearchJpaRepository.findById(id)
- .orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
- );
+ if (!recentSearchJpaRepository.existsById(id)) {
+ throw new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND);
+ }
+ recentSearchJpaRepository.deleteById(id);
}🤖 Prompt for AI Agents
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호 이런 구현 방식도 괜찮은거 같네요
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai 너가 제안한 delete 방식이 현재 구현된 방식보다 어떤 점에서 더 효율적인지 설명해줘 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seongjunnoh 좋은 질문이네요! 제가 제안한 방식이 더 효율적인 이유를 설명드리겠습니다. 현재 구현 방식 vs 제안된 방식현재 구현 (비효율적):// 1단계: SELECT 쿼리로 전체 엔티티 조회
RecentSearchJpaEntity entity = recentSearchJpaRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND));
// 2단계: DELETE 쿼리 실행
recentSearchJpaRepository.delete(entity);제안된 구현 (효율적):// 1단계: SELECT COUNT 쿼리로 존재 여부만 확인
if (!recentSearchJpaRepository.existsById(id)) {
throw new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND);
}
// 2단계: DELETE 쿼리 실행
recentSearchJpaRepository.deleteById(id);효율성 차이점
특히 대용량 데이터나 빈번한 삭제 작업이 있는 환경에서 이런 최적화가 누적되면 상당한 성능 개선 효과를 가져올 수 있습니다! ✏️ Learnings added
🧠 Learnings usedThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @seongjunnoh 제가 제안한 delete 방식이 더 효율적인 이유를 데이터베이스 쿼리와 메모리 관점에서 설명드리겠습니다. 현재 구현 방식의 문제점// 현재 방식
recentSearchJpaRepository.delete(
recentSearchJpaRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND))
);실행되는 쿼리:
제안한 방식의 장점// 제안한 방식
if (!recentSearchJpaRepository.existsById(id)) {
throw new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND);
}
recentSearchJpaRepository.deleteById(id);실행되는 쿼리:
구체적인 효율성 차이1. 네트워크 트래픽 감소
2. 메모리 사용량 감소
3. DB 성능 최적화
실제 운영 환경에서는 이런 작은 최적화들이 누적되어 상당한 성능 차이를 만들어냅니다! 🧠 Learnings used |
||
|
|
||
| @Override | ||
| public void update(RecentSearch recentSearch) { | ||
| RecentSearchJpaEntity recentSearchJpaEntity = recentSearchJpaRepository.findById(recentSearch.getId()) | ||
| .orElseThrow(() -> new EntityNotFoundException(RECENT_SEARCH_NOT_FOUND)); | ||
|
|
||
| recentSearchJpaEntity.updateFrom(recentSearch); | ||
| recentSearchJpaRepository.save(recentSearchJpaEntity); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package konkuk.thip.recentSearch.adapter.out.persistence.repository; | ||
|
|
||
| import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; | ||
| import konkuk.thip.recentSearch.adapter.out.jpa.SearchType; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface RecentSearchQueryRepository { | ||
| Optional<RecentSearchJpaEntity> findBySearchTermAndTypeAndUserId(String searchTerm, SearchType type, Long userId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package konkuk.thip.recentSearch.adapter.out.persistence.repository; | ||
|
|
||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; | ||
| import konkuk.thip.recentSearch.adapter.out.jpa.SearchType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import static konkuk.thip.recentSearch.adapter.out.jpa.QRecentSearchJpaEntity.recentSearchJpaEntity; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class RecentSearchQueryRepositoryImpl implements RecentSearchQueryRepository { | ||
|
|
||
| private final JPAQueryFactory queryFactory; | ||
|
|
||
| @Override | ||
| public Optional<RecentSearchJpaEntity> findBySearchTermAndTypeAndUserId(String searchTerm, SearchType type, Long userId) { | ||
| RecentSearchJpaEntity result = queryFactory | ||
| .selectFrom(recentSearchJpaEntity) | ||
| .where( | ||
| recentSearchJpaEntity.searchTerm.eq(searchTerm), | ||
| recentSearchJpaEntity.type.eq(type), | ||
| recentSearchJpaEntity.userJpaEntity.userId.eq(userId) | ||
| ) | ||
| .fetchOne(); | ||
|
|
||
| return Optional.ofNullable(result); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
| package konkuk.thip.recentSearch.application.port.out; | ||
|
|
||
| import konkuk.thip.recentSearch.domain.RecentSearch; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface RecentSearchQueryPort { | ||
|
|
||
| Optional<RecentSearch> findRecentSearchByKeywordAndUserId(String keyword, Long userId); | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package konkuk.thip.recentSearch.application.service.manager; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import konkuk.thip.recentSearch.application.port.out.RecentSearchQueryPort; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import konkuk.thip.recentSearch.domain.RecentSearch; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.LocalDateTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class RecentSearchCreateManager { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String USER_SEARCH_TERM = "사용자 검색"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 하드코딩된 상수를 enum으로 개선 권장 "사용자 검색"이 하드코딩되어 있습니다. - private static final String USER_SEARCH_TERM = "사용자 검색";
+ private static final SearchType USER_SEARCH_TYPE = SearchType.USER_SEARCH;그리고 line 32에서도 수정: - RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId);
+ RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TYPE.getDescription(), userId);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RecentSearchCommandPort recentSearchCommandPort; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final RecentSearchQueryPort recentSearchQueryPort; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void saveRecentSearchByUser(Long userId, String keyword) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ifPresentOrElse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+24
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3 : 왜 메서드의 인자값으로 type은 넘기지 않고, 메서드 내부에서 jpa repository 를 호출할때 USER_SEARCH 를 주입해주는 건가요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 제가 SearchType을 서비스 로직에서 import하여 쓰지 않은 이유와 일맥상통합니다! 다만, 뒷 pr에서 정의한 RecentSearchType을 여기서 활용할 수 있겠네요! 한번 수정해보겠습니다. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existingRecentSearch -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 이미 존재하면 createdAt만 갱신 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existingRecentSearch.updateCreatedAt(LocalDateTime.now()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchCommandPort.update(existingRecentSearch); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+29
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3 : 엇 createdAt이 udpate 안되지 않나요??
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네넵 그걸 뒷 pr에서 발견해서 뒤쪽 pr에서 수정하였습니다! 혼란을 드렸네요,, 지송 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 없으면 새로 저장 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RecentSearch userRecentSearch = RecentSearch.withoutId(keyword, USER_SEARCH_TERM, userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3 : 지금처럼 RecentSearchCreateManager에서 상수로 선언한 String 값을 주입해주는 구조보다는 유지보수를 위해 RecentSearch를 생성할 때 SearchType.USER_SEARCH.getSearchType() 을 주입 하는 방식으로 수정하는건 어떤가요? 현재 User 등 여러 도메인 엔티티에서 위와 같은 방식으로 코드가 작성되어 있긴 합니다! public class User extends BaseDomainEntity {
private Long id;
private String nickname;
private String userRole;
private String oauth2Id;
private Integer followerCount; // 팔로워 수
private Alias alias;
public static User withoutId(String nickname, String userRole, String oauth2Id, Alias alias) {
return User.builder()
.id(null)
.nickname(nickname)
.userRole(userRole)
.oauth2Id(oauth2Id)
.followerCount(0)
.alias(alias)
.build();
}
}
public Long signup(UserSignupCommand command) {
Alias alias = Alias.from(command.aliasName());
User user = User.withoutId(
command.nickname(), USER.getType(), command.oauth2Id(), alias
);
return userCommandPort.save(user);
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 처음에 이렇게 고려하려고 했습니다! 다만, SearchType은 저희가 adapter.out 계층에서 사용중인 Enum 클래스이기 때문에 해당 클래스를 서비스가 import하고 있는 것은 적절하지 않다고 판단했습니다! 뒷 pr 리뷰하시면 보실 수 있겠지만, Domain 차원에서 param과 SearchType.searchType들을 매핑지어서 활용하도록 했습니다!!
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뒷 pr에 관련해서 코멘트 달았습니다! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recentSearchCommandPort.save(userRecentSearch); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 트랜잭션 관리 추가 고려 메서드에서 조회 후 저장/업데이트 작업을 수행하고 있습니다. 데이터 일관성을 위해 + @Transactional
public void saveRecentSearchByUser(Long userId, String keyword) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
| import lombok.Getter; | ||
| import lombok.experimental.SuperBuilder; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @SuperBuilder | ||
| public class RecentSearch extends BaseDomainEntity { | ||
|
|
@@ -15,4 +17,16 @@ public class RecentSearch extends BaseDomainEntity { | |
| private String type; | ||
|
|
||
| private Long userId; | ||
|
|
||
| public static RecentSearch withoutId(String searchTerm, String type, Long userId) { | ||
| return RecentSearch.builder() | ||
| .searchTerm(searchTerm) | ||
| .type(type) | ||
| .userId(userId) | ||
| .build(); | ||
| } | ||
|
|
||
| public void updateCreatedAt(LocalDateTime localDateTime) { | ||
| this.setCreatedAt(localDateTime); | ||
| } | ||
|
Comment on lines
+29
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain가변성 설계에 대한 검토가 필요합니다.
다음 스크립트로 BaseDomainEntity의 변경사항을 확인해 주세요: 🏁 Script executed: #!/bin/bash
# Description: BaseDomainEntity의 setCreatedAt 메서드 존재 여부와 가변성 설계 확인
# BaseDomainEntity 클래스에서 setCreatedAt 메서드 찾기
ast-grep --pattern $'class BaseDomainEntity {
$$$
setCreatedAt($_) {
$$$
}
$$$
}'
# 또는 createdAt 필드와 관련된 setter 메서드들 확인
rg -A 5 -B 5 "setCreatedAt|createdAt.*=" src/main/java/konkuk/thip/common/entity/Length of output: 950 도메인 객체의 가변성 (createdAt) 재검토 필요
도메인 레벨에서
🤖 Prompt for AI Agents |
||
| } | ||
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.
🛠️ Refactor suggestion
createdAt 필드 수정 가능성에 대한 신중한 검토가 필요합니다.
Base 엔티티의
createdAt필드에@Setter를 추가한 것은 일반적인 도메인 규칙에 어긋날 수 있습니다. 생성 시간은 보통 불변이어야 하는데, 최근 검색 기능에서 "최근성" 업데이트를 위해 수정하는 것이 적절한지 검토가 필요합니다.대안으로 다음을 고려해보세요:
lastSearchedAt필드 추가🤖 Prompt for AI Agents