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

feat: 추억 목록 정렬, 필터링 API 구현 #583

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from

Conversation

linirini
Copy link
Contributor

@linirini linirini commented Jan 8, 2025

⭐️ Issue Number

🚩 Summary

  • /memories?term=true&sort=NEWEST와 같은 형식으로 필터링과 정렬 조건을 query string으로 전달하여 적용합니다.
  • query string의 value로 유효하지 않은 값이 전달될 경우 default 정렬 조건을 사용하고, 필터링은 무시 되도록 구현하였습니다.
    • 잘못된 query string의 value에 대해서 사용자가 예외를 반환받을 필요는 없다고 생각하였습니다.
    • 다만, 클라이언트 측에서는 명세서를 잘 확인하여 잘못된 값을 전달하지 않도록 유의해야 합니다.

🛠️ Technical Concerns

  • Controller테스트에서 Application Context를 재사용하도록 리팩터링 했습니다. 최적화를 의도했지만, 확연한 성능 최적화는 경험하지 못했습니다..
  • 필터링과 정렬은 Enum으로 관심사를 분리하였습니다.
  • 내부적으로 함수형으로 관련 정렬 / 필터링 로직을 적용하도록 구현했습니다.
  • 최근 수정 순을 구현하기 위해 EntityListener의 CallbackMethod를 활용하여 MomentEntityListener를 만들고 Moment 엔티티에 등록하였습니다. 해당 리스너에서는 Moment에 대한 Persist, Remove, Update 작업이 발생하였을 때 Memory의 UpdatedAt을 갱신합니다. 이때 트랜잭션 내부에서 작업하도록 PreXXX 콜백을 사용하였습니다.

🙂 To Reviewer

  • 고민이 되었던 부분에 대해서는 코멘트를 작성해놓았습니다. 확인 부탁드립니다.
  • 추가로, 갑자기 momentRepositoryTest.findAllByMemoryIdOrderByCreatedAt()가 터지는데, 왜인지 모르겠습니다. 그 외에도 어떤 생성 날짜/수정 날짜 갱신에 대한 테스트가 랜덤하게 터지고 있습니다.....

📋 To Do

@linirini linirini added backend We are backend>< feat 기능 (새로운 기능) labels Jan 8, 2025
@linirini linirini self-assigned this Jan 8, 2025
@linirini linirini linked an issue Jan 8, 2025 that may be closed by this pull request
10 tasks
Copy link

github-actions bot commented Jan 8, 2025

Test Results

 29 files   29 suites   5s ⏱️
180 tests 180 ✅ 0 💤 0 ❌
188 runs  188 ✅ 0 💤 0 ❌

Results for commit 0b3710c.

♻️ This comment has been updated with latest results.

Copy link

github-actions bot commented Jan 8, 2025

🌻Test Coverage Report

Overall Project 78.81% -0.41% 🍏
Files changed 94.55% 🍏

File Coverage
MemoryController.java 100% 🍏
Term.java 100% 🍏
MemoryMembers.java 100% 🍏
MemorySort.java 100% 🍏
MemoryFilter.java 100% 🍏
MemoryService.java 97.1% 🍏
Memory.java 88.69% 🍏
MomentEntityListener.java 80% -20% 🍏
MemoryReadRequest.java 77.14% -22.86% 🍏
BaseEntity.java 76.47% -23.53%
Moment.java 64.94% 🍏

public class MemoryMembers {
private final List<MemoryMember> memoryMembers;

public List<Memory> operate(List<String> filters, String sort) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

필터링과 정렬 연산을 수행하는 역할을 합니다. 도메인일까요, 서비스일까요?

Copy link
Contributor

Choose a reason for hiding this comment

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

음 명확하게 구분하는 것이 애매할 수 있지만 지금 구성에서는 MemoryFilterMemroySort를 통해 결과를 반환하는 역할이기에 stream과 굉장히 유사한 역할을 하고있다고 생각합니다 따라서 도메인보다는 서비스의 성격에 가깝지 않나라는 생각을 했습니다

sortByCreatedAtDescending(memoryMembers);
public MemoryNameResponses readAllMemoriesByDate(Member member, LocalDate currentDate) {
MemoryMembers memoryMembers = new MemoryMembers(memoryMemberRepository.findAllByMemberIdAndDate(member.getId(), currentDate));
List<Memory> memories = memoryMembers.operate();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

default용 operate() 메서드를 오버로딩했습니다. 내부적으로 operate(Filter, Sort)를 호출합니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

위의 코멘트와 유사하게 만약 현재의 MemoryMembersMemories로 변화하면서 필드로 List<Memory>를 가지게 된다면 해당 메서드는 없어질 수 있을 것 같아요!

@Schema(hidden = true)
public List<String> getFilters() {
List<String> filters = new ArrayList<>();
if (isActive(term)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

아주........마음에 안듭니다...... 이상태로는 필터링 조건이 늘어날 때마다 분기가 늘어날텐데 분기 처리를 줄일 수 있는 방법이 떠오르지 않아요..

Copy link
Contributor

Choose a reason for hiding this comment

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

위의 코멘트에 달아놓은 것 처럼 List<Enum>으로 받는다면 해결 가능할 것 같네요

Copy link
Contributor

@Ho-Tea Ho-Tea left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 리니!!!
개인적으로 많은 고민과 시간이 쏟아부어진 느낌이 드네요🥲
얘기나눠볼 코멘트 달아드렸으니 확인 부탁드려요!

@@ -42,18 +39,18 @@ private boolean isOnlyOneDatePresent(LocalDate startAt, LocalDate endAt) {
}

private boolean isInvalidTerm(LocalDate startAt, LocalDate endAt) {
return isExist(startAt, endAt) && endAt.isBefore(startAt);
return Objects.nonNull(startAt) && Objects.nonNull(endAt) && endAt.isBefore(startAt);
Copy link
Contributor

Choose a reason for hiding this comment

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

밑에서 사용하는 isExist()를 사용하지 않으신 이유가 있을까요?

private List<Memory> getMemories() {
return memoryMembers.stream()
.map(MemoryMember::getMemory)
.collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

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

불변한다면 toList()를 사용하는게 더 나을 것 같아요!

@Schema(description = "추억 목록 조회시 정렬과 필터링 조건을 위한 요청 형식입니다.")
public record MemoryReadRequest(
@Schema(description = "기간 필터 사용 여부 (대소문자 구분 X)", example = "true")
String term,
Copy link
Contributor

Choose a reason for hiding this comment

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

기간 필터의 사용 유무라면 boolean으로 표현하는 방법도 있을 것 같은데 String으로 표현하신 이유가 있을가용?

Copy link
Contributor

Choose a reason for hiding this comment

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

우선은 추후 필터링 조건이 늘어날 수 있기에 String으로 표현하신걸로 알고 넘어갈게요!

Copy link
Contributor

Choose a reason for hiding this comment

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

필터링 조건이 중복되어 있을 수 있기에 처음 받을때 List<Enum>으로 받는다면 더 괜찮을 것 같아요!

public static class MemoryReadRequest {
    private List<Status> statuses;
// Status 는 Enum

GET /example?statuses=ACTIVE&statuses=INACTIVE&statuses=PENDING

List<MemoryMember> memoryMembers = memoryMemberRepository.findAllByMemberId(member.getId());
sortByCreatedAtDescending(memoryMembers);
public MemoryResponses readAllMemories(Member member, MemoryReadRequest memoryReadRequest) {
MemoryMembers memoryMembers = new MemoryMembers(memoryMemberRepository.findAllByMemberId(member.getId()));
Copy link
Contributor

Choose a reason for hiding this comment

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

현재 인자로 MemoryMemberList 형을 받지만 내부적으로는 MemoryList만을 사용하는 것으로 보입니다.
클래스 명을 Memories로 변경하고 필드로 List<Memory>만 받는 것으로 바꾸는 건 어떨까요?
(memoryMembers.operate 가 조금은 어색하게 다가왔습니다)

Comment on lines +13 to +15
memoryList.stream()
.filter(Memory::hasTerm)
.collect(Collectors.toList())
Copy link
Contributor

Choose a reason for hiding this comment

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

enum 내부에 함수형 인터페이스를 구현하는 것과 외부의 메서드로 메서드를 구성해놓은 것 중 함수형 인터페이스를 내부에 구현하는 방법을 선택하신 이유가 궁금합니다!

sortByCreatedAtDescending(memoryMembers);
public MemoryNameResponses readAllMemoriesByDate(Member member, LocalDate currentDate) {
MemoryMembers memoryMembers = new MemoryMembers(memoryMemberRepository.findAllByMemberIdAndDate(member.getId(), currentDate));
List<Memory> memories = memoryMembers.operate();
Copy link
Contributor

Choose a reason for hiding this comment

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

위의 코멘트와 유사하게 만약 현재의 MemoryMembersMemories로 변화하면서 필드로 List<Memory>를 가지게 된다면 해당 메서드는 없어질 수 있을 것 같아요!

private final Function<List<Memory>, List<Memory>> operation;

public static List<Memory> apply(String sortValue, List<Memory> memories) {
return Stream.of(MemorySort.values())
Copy link
Contributor

Choose a reason for hiding this comment

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

values() 만 사용해도 될 것 같아요!

@Schema(hidden = true)
public List<String> getFilters() {
List<String> filters = new ArrayList<>();
if (isActive(term)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

위의 코멘트에 달아놓은 것 처럼 List<Enum>으로 받는다면 해결 가능할 것 같네요

Comment on lines +15 to 18
@Setter
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
Copy link
Contributor

Choose a reason for hiding this comment

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

createdAt까지 setter의 영향을 받을 수 있다 생각되어 새로운 updatedAt 수정 메서드만 구성하는게 좋을 것 같아요!

@@ -25,6 +27,7 @@

@Entity
@Getter
@EntityListeners({MomentEntityListener.class})
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 Moment 내부에서 메서드를 구현한 후 EntityListener를 구성하는 방법도 있었는데 새롭게 클래스를 만드신 이유가 궁금해요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend We are backend>< feat 기능 (새로운 기능)
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

feat: 추억 목록 정렬, 필터링 API 구현
2 participants