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: 인기 게시글 기능 구현 완료 #610

Merged
merged 25 commits into from
Dec 15, 2024
Merged

feat: 인기 게시글 기능 구현 완료 #610

merged 25 commits into from
Dec 15, 2024

Conversation

SongJaeHoonn
Copy link
Contributor

@SongJaeHoonn SongJaeHoonn commented Nov 8, 2024

Summary

#607

게시판 상단에 핫 게시글을 노출하는 기능을 위해, 지난주의 인기 게시글을 조회하는 api를 구현했습니다.

Tasks

  • hot 게시글 조회 로직 구현

ETC

  • 추후 핫 게시글 개수의 유연한 변경을 위해 파라미터에 size를 추가해 개수 조절이 가능하도록 구현했습니다. (기본값은 5)
  • 만약 인기 게시글의 반응 수가 같다면 최신순으로 정렬합니다.
  • 지난 주의 핫 게시글이 size개보다 적다면, 이번 주를 기준으로 두 주 전, 세 주 전... 을 계속 조회하여 핫 게시글이 size 개수가 될 때까지 반복합니다.
  • 만약 전체 게시글의 개수가 size개보다 적다면 전체 게시글을 반응 순으로 정렬 후 반환합니다.

API Response

  • 2024-11-08일에 api 호출했을 때 (size = 3)

1번 게시글 댓글 1, 이모지 2
2번 게시글 댓글 1, 이모지 5
3번 게시글 댓글 1, 이모지 1

{
  "success": true,
  "data": [
    {
      "id": 2,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "free",
      "title": "Free Board Title 1",
      "content": "This is a free board content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-10-30T17:30:00"
    },
    {
      "id": 1,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 1",
      "content": "This is a notice content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-10-29T21:00:00"
    },
    {
      "id": 3,
      "writerId": null,
      "writerName": "User2",
      "category": "qna",
      "title": "QnA Title 1",
      "content": "This is a QnA content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-11-02T23:45:00"
    }
  ]
}

1번 게시글 댓글 1, 이모지 1
2번 게시글 댓글 1, 이모지 1
3번 게시글 댓글 1, 이모지 1
-> 최신 순 정렬

{
  "success": true,
  "data": [
    {
      "id": 3,
      "writerId": null,
      "writerName": "User2",
      "category": "qna",
      "title": "QnA Title 1",
      "content": "This is a QnA content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-11-02T23:45:00"
    },
    {
      "id": 2,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "free",
      "title": "Free Board Title 1",
      "content": "This is a free board content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-10-30T17:30:00"
    },
    {
      "id": 1,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 1",
      "content": "This is a notice content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-10-29T21:00:00"
    }
  ]
}
  • 만약 지난 주의 게시글이 size - 1개 이하일 때

11월 8일 기준 지난주는 10월 28일 ~ 11월 3일 (1, 2번)
1번 → 댓글 1개, 이모지 0개
2번 → 댓글 0개, 이모지 0개
5번, 6번, 7번 → 지지난주 핫게시글

{
  "success": true,
  "data": [
    {
      "id": 1,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 1",
      "content": "This is a notice content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-10-29T21:00:00"
    },
    {
      "id": 2,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "free",
      "title": "Free Board Title 1",
      "content": "This is a free board content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-10-30T17:30:00"
    },
    {
      "id": 7,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 7",
      "content": "This is a notice content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-10-24T21:00:00"
    }
  ]
}

@SongJaeHoonn SongJaeHoonn added the ✨ Feature 새로운 기능 명세 및 개발 label Nov 8, 2024
@SongJaeHoonn SongJaeHoonn self-assigned this Nov 8, 2024
Copy link
Collaborator

@mingmingmon mingmingmon left a comment

Choose a reason for hiding this comment

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

메소드추출을 잘 해주셔서 로직을 이해하기 편했습니다.

다만, 저와 @SongJaeHoonn님, @Jeong-Ag님이 핫 게시글 정책에 대해 회의를 했을 때,
일주일마다 핫 게시글을 새로 등재하기로 이야기 나눴던 것 같아요.

지금까지의 작업으로는 매번 API를 호출할 때마다 핫 게시글을 판단하는 쿼리문이 실행되는 것 같습니다. 즉, 일주일에 한 번 핫 게시글이 갱신 되는 것이 아니라 페이지 방문할 때마다 핫 게시글이 계속 갱신 되는 것 같아요. (물론, 게시글이 많이 올라오고, 반응도 그만큼 많이 달려야겠지만요)

이러한 동작은 저희가 처음 설계한 정책과는 조금 다른 모습이기도 하고, api 호출 시 발생하는 쿼리가 많아서 부담이 되어보입니다.

따라서 핫 게시글 선정 로직을 매주 스케쥴링을 걸어 DB에 저장해두는 방식을 제안드립니다.

@SongJaeHoonn
Copy link
Contributor Author

지금까지의 작업으로는 매번 API를 호출할 때마다 핫 게시글을 판단하는 쿼리문이 실행되는 것 같습니다. 즉, 일주일에 한 번 핫 게시글이 갱신 되는 것이 아니라 페이지 방문할 때마다 핫 게시글이 계속 갱신 되는 것 같아요. (물론, 게시글이 많이 올라오고, 반응도 그만큼 많이 달려야겠지만요)

@mingmingmon
현재 로직에서도 일주일에 한 번 핫 게시글이 갱신되고 있어요.
API를 호출할 때마다, 현재 시각을 기준으로 지난 주에 가장 반응이 많았던 핫 게시글을 조회해 가져오는 방식입니다..!

그러나 현재 방식은 API를 호출할 때마다 댓글 수와 이모지 수를 계산하는 복잡한 로직들과 복잡한 조회 방식이 요구되므로 원래의 DB에 isHotBoard 같은 새로운 칼럼을 넣어 일주일마다 스케줄링 되는 방식이 현재 구조에서는 더욱 효율적일 것 같네요!
소중한 의견 감사합니다 🙇‍♂️

@limehee limehee changed the title feat: 핫 게시글 조회 api 구현 완료 feat: 인기 게시글 기능 구현 완료 Nov 12, 2024
@limehee limehee linked an issue Nov 12, 2024 that may be closed by this pull request
1 task
@limehee
Copy link
Collaborator

limehee commented Nov 12, 2024

그러나 현재 방식은 API를 호출할 때마다 댓글 수와 이모지 수를 계산하는 복잡한 로직들과 복잡한 조회 방식이 요구되므로 원래의 DB에 isHotBoard 같은 새로운 칼럼을 넣어 일주일마다 스케줄링 되는 방식이 현재 구조에서는 더욱 효율적일 것 같네요! 소중한 의견 감사합니다 🙇‍♂️

@SongJaeHoonn 님의 제안도 효율적인 접근 방식이지만, 추가적으로 캐싱 전략을 활용하는 방안도 고려해보시면 좋을 것 같아요. 새로운 컬럼을 추가할 경우 테이블 관리와 데이터 정합성 유지가 더 복잡해질 수 있고, 매번 연산이 발생할 때마다 기존 데이터를 동기화해야 한다는 부담이 있어요.

이번 인기 게시글 기능처럼 정합성이 절대적으로 요구되지 않고 주기적으로 갱신되는 데이터라면, TTL(Time to Live) 설정이 가능한 Redis와 같은 캐시 시스템을 통해 성능을 최적화할 수 있어요. 예를 들어, 인기 게시글 정보를 Redis에 저장하고 일정 시간마다 갱신하도록 설정하면 데이터베이스 부하를 줄이면서도 빠른 응답성을 유지할 수 있어요.

또한, 현재는 카테고리와 무관하게 인기 게시글이 관리되는 것으로 보이는데, 카테고리별 인기 게시글을 도입하면 사용자에게 더욱 맞춤화된 정보를 제공할 수 있을 것으로 보여요.

@limehee
Copy link
Collaborator

limehee commented Nov 12, 2024

프론트에서 인기 게시글을 어떻게 표시할지는 모르지만, 일반 게시글과 함께 하나의 목록으로 표시된다면, 게시글 조회 시 인기 게시글 정보를 함께 반환하여 API 호출 횟수를 줄이는 방법도 고려해보시면 좋을 것 같네요.

@SongJaeHoonn
Copy link
Contributor Author

이번 인기 게시글 기능처럼 정합성이 절대적으로 요구되지 않고 주기적으로 갱신되는 데이터라면, TTL(Time to Live) 설정이 가능한 Redis와 같은 캐시 시스템을 통해 성능을 최적화할 수 있어요. 예를 들어, 인기 게시글 정보를 Redis에 저장하고 일정 시간마다 갱신하도록 설정하면 데이터베이스 부하를 줄이면서도 빠른 응답성을 유지할 수 있어요.

또한, 현재는 카테고리와 무관하게 인기 게시글이 관리되는 것으로 보이는데, 카테고리별 인기 게시글을 도입하면 사용자에게 더욱 맞춤화된 정보를 제공할 수 있을 것으로 보여요.

Redis를 사용하는 것이 익숙하지 않아 이런 방법은 생각해보지 않았는데, 인메모리로 동작하는 Redis 특성상 조회가 빠르고 DB에 부담을 주지 않아 좋을 것 같아요..! 고려해보도록 하겠습니다.

그리고, 카테고리별 인기 게시글을 생각해보긴 했지만 회의 결과 활발하지 않은 카테고리는 인기 게시글이 계속 고정되거나 아예 올라오지 않을 가능성도 고려하여 모든 카테고리에서 전체 게시글 중의 인기 게시글을 표시하기로 결정했습니다.

@mingmingmon
Copy link
Collaborator

mingmingmon commented Nov 13, 2024

프론트에서 인기 게시글을 어떻게 표시할지는 모르지만, 일반 게시글과 함께 하나의 목록으로 표시된다면, 게시글 조회 시 인기 게시글 정보를 함께 반환하여 API 호출 횟수를 줄이는 방법도 고려해보시면 좋을 것 같네요.

저는 인기 게시글 조회 API를 따로 추출하는 것이 좋을 것 같아요.

@limehee 님이 제안 주신 방식으로 구현하게 되면 페이지네이션된 결과(10개의 일반 게시글)와 다른 응답 객체(5개의 인기 게시글)을 다시 묶어서 responseDto로 반환하는 방식일 것 같은데, 제가 이해한 바가 맞을까요?

현재 일반 게시글 조회 시에 페이지네이션이 적용되어있어요. 만약 일반 게시글 조회에서 인기 게시글을 함께 반환하도록 한다면, 10개의 일반게시글 + 5개의 인기 게시글이 함께 반환되는 형태가 될 것 같습니다. 그렇게 되면 responseDto가 더욱 복잡해지고, 사용자가 페이지를 넘길 때 마다 동일한 정보인 5개의 인기 게시글이 중복해서 전달될 것 같아요.

@limehee
Copy link
Collaborator

limehee commented Nov 13, 2024

저는 인기 게시글 조회 API를 따로 추출하는 것이 좋을 것 같아요.

@limehee 님이 제안 주신 방식으로 구현하게 되면 페이지네이션된 결과(10개의 일반 게시글)와 다른 응답 객체(5개의 인기 게시글)을 다시 묶어서 responseDto로 반환하는 방식일 것 같은데, 제가 이해한 바가 맞을까요?

현재 일반 게시글 조회 시에 페이지네이션이 적용되어있어요. 만약 일반 게시글 조회에서 인기 게시글을 함께 반환하도록 한다면, 10개의 일반게시글 + 5개의 인기 게시글이 함께 반환되는 형태가 될 것 같습니다. 그렇게 되면 responseDto가 더욱 복잡해지고, 사용자가 페이지를 넘길 때 마다 동일한 정보인 5개의 인기 게시글이 중복해서 전달될 것 같아요.

제가 코드를 꼼꼼히 살펴보지 못하고 이전 코멘트에서 "일반 게시글"이라는 모호한 표현을 사용하면서 혼동을 드린 것 같아요(코멘트를 잘못 남긴 것 같습니다). 이에 따라 남겨주신 의견과 다소 어긋난 부분이 있을 수 있다는 점 양해 부탁드립니다.

retrieveHotBoards()의 반환형을 고려할 때, 커뮤니티 탭의 모아보기와 유사한 형태로 인기 게시글을 보여주고자 하는 것으로 예상돼요. 인기 게시글을 모아보기처럼 사용자에게 제공할 계획이라면, 둘의 성격이 다르므로 현재 구조대로 별도의 API로 제공하는 것이 더 적절해 보이네요. 혼동을 드린 점 다시 한번 사과드립니다.

@SongJaeHoonn
Copy link
Contributor Author

@limehee @mingmingmon
우선 질 높은 코드리뷰에 감사드리며, 제안주신 사항들 수정 및 리팩토링 완료했습니다.

변경 사항

  1. 인기 게시글 저장에 Redis 사용
  • redis에서 저장 및 조회시 RedisTemplate을 사용했습니다. (정렬 순서 문제로 인함)
  1. 일주일에 한 번만 저장되도록 스케줄링
  2. size 고정 (redis 사용으로 인함)
  3. Strategy Pattern 적용

Strategy 패턴 적용시 전략 클래스들은 일단 service 패키지에 넣어뒀는데 더 좋은 방안이 있다면 리뷰 부탁드립니다!

테스트

id 2, 3 ⇒ 지지지난주 2번 댓글 한개
id 4, 5 → 지지난주 4번 댓글 한개
id 6, 7 → 지난주
id 8 → 이번주 8번 댓글 한개
결과 : 7, 6, 5, 4, 2

{
  "success": true,
  "data": [
    {
      "id": 7,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 7",
      "content": "This is a notice content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-11-19T21:00:00"
    },
    {
      "id": 6,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 7",
      "content": "This is a notice content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-11-18T21:00:00"
    },
    {
      "id": 5,
      "writerId": null,
      "writerName": "User3",
      "category": "organization",
      "title": "Club News Title 1",
      "content": "This is a club news content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-11-12T21:00:00"
    },
    {
      "id": 4,
      "writerId": "202500003",
      "writerName": "김민지",
      "category": "graduated",
      "title": "Graduated Board Title 1",
      "content": "This is a graduated board content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-11-11T21:00:00"
    },
    {
      "id": 2,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "free",
      "title": "Free Board Title 1",
      "content": "This is a free board content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-11-04T21:00:00"
    }
  ]
}

스케줄링 후 (8번을 지난주로 옮김)
결과 : 8, 7, 6, 5, 4

{
  "success": true,
  "data": [
    {
      "id": 8,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 7",
      "content": "This is a notice content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-11-20T22:00:00"
    },
    {
      "id": 7,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 7",
      "content": "This is a notice content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-11-19T21:00:00"
    },
    {
      "id": 6,
      "writerId": "202500001",
      "writerName": "설윤",
      "category": "notice",
      "title": "Notice Title 7",
      "content": "This is a notice content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-11-18T21:00:00"
    },
    {
      "id": 5,
      "writerId": null,
      "writerName": "User3",
      "category": "organization",
      "title": "Club News Title 1",
      "content": "This is a club news content.",
      "commentCount": 0,
      "imageUrl": null,
      "createdAt": "2024-11-12T21:00:00"
    },
    {
      "id": 4,
      "writerId": "202500003",
      "writerName": "김민지",
      "category": "graduated",
      "title": "Graduated Board Title 1",
      "content": "This is a graduated board content.",
      "commentCount": 1,
      "imageUrl": null,
      "createdAt": "2024-11-11T21:00:00"
    }
  ]
}

Copy link
Collaborator

@mingmingmon mingmingmon 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
Collaborator

@limehee limehee left a comment

Choose a reason for hiding this comment

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

인기 게시글 산정 방식에 대해 전략 패턴을 적용해달라고 요청드렸었는데, 현재 코드를 확인해보니 산정 방식뿐만 아니라 요청에 따라 DB에서 데이터를 조회하고 결과를 생성하는 전체 과정을 하나의 전략으로 묶은 것처럼 보여요.
그러나, 인기 게시글 산정 로직 외의 부분은 모든 전략에서 동일하게 동작할 가능성이 높기 때문에, 산정 로직만을 전략 패턴으로 분리하는 것이 더 적합하다고 판단돼요.
또한, 클래스명이 DefaultHotBoardService로 되어 있는데, 일반적으로 전략 패턴을 구현하는 클래스명은 ___Strategy의 형태를 따르는 경우가 많아요. 클래스명을 더 명확히 변경해주세요.


요약

  1. 전략 패턴은 특정 동작만을 분리하여 캡슐화하는 데 초점이 맞춰져 있어요. 현재 코드는 전략이 과도하게 많은 역할을 맡고 있어요.
  2. 게시글 조회나 매핑같은 로직은 산정 방식과 무관하게 모든 전략에서 동일하게 작동할 가능성이 높아요. 이러한 로직은 전략이 아닌 상위 서비스에서 관리되어야 해요.
  3. DefaultHotBoardService라는 이름은 전략 패턴의 일반적인 명명 규칙(___Strategy)에 부합하지 않아요. DefaultHotBoardStrategy와 같이 역할이 더 명확히 드러나는 이름으로 변경해주세요.

참고

💠 전략(Strategy) 패턴 - 완벽 마스터하기

@SongJaeHoonn
Copy link
Contributor Author

SongJaeHoonn commented Nov 28, 2024

@limehee

피드백주신 사항 잘 이해했습니다!
변수명 변경과 클래스명 변경, 전략 패턴 책임 분리 완료했습니다. 확인 부탁드려요!

그리고 전략 패턴 책임 분리에 관해서는,
저는 전략 패턴을 적용할 때, 다른 요구사항이나 정책이 주어졌을 때 쉽게 갈아 끼울 수 있다는 점에 초점을 맞췄습니다.
따라서 인기 게시글을 redis에 저장하도록 리팩토링하기 이전에, 객체지향 5원칙중 개방-폐쇄 원칙(OCP, Open-Closed Principle)을 생각했을 때 클라이언트가 직접 전략을 선택하도록 유도해서 코드의 변경은 확실히 없도록 하되, 확장에는 열려있게 구성했었습니다.
하지만 api 호출 효율에 redis를 사용하는 것의 이점과, 인기 게시글 선정시 한 전략이 과도하게 많은 책임을 가지고 있음을 인정하고, redis를 사용하고 저장과 조회 로직을 분리하도록 리팩토링 완료했습니다.

이렇게 전략의 책임을 분리하고 나니, 컨트롤러에서는 인기게시글 선정과 저장에 관심사를 두는 것이 아니라 redis에서 조회하는 비즈니스 로직에만 연결되어 있기 때문에 인기게시글 선정에 관여할 수 없었습니다.
전략을 수정하려면 코드 내에 StrategyType만 수정하면 되긴 하지만, 수정할 부분이 한 줄밖에 안될지라도 결국 백엔드 코드를 고쳐야 하는 OCP 원칙을 위반하는 상황(확장하려면, 코드를 수정해야함)이 발생하게 됩니다.

이런 상황에서는 어떻게 코드를 작성해야 할까요?

@SongJaeHoonn SongJaeHoonn requested a review from limehee November 28, 2024 12:47
@limehee
Copy link
Collaborator

limehee commented Nov 30, 2024

@SongJaeHoonn

전략 패턴 적용 방식에 대한 개선 방향을 제안드려요.

  1. 전략의 빈 이름을 상수로 관리
    현재 Enum을 사용해 전략을 관리하고 있는데, Enum을 상수로 변경하면 상수를 이용해서 전략 식별자를 관리할 수 있어요. 이렇게 하면 전략 식별자를 중앙에서 일관되게 관리하고, 오탈자를 방지하면서, 변경 시 수정 범위를 최소화할 수 있어요.

새로운 전략을 추가할 때 StrategyConstants에 상수를 추가해야 하므로 엄밀히 말해 OCP를 완벽히 준수하지는 못해요.
하지만 이 방식은 값과 동작을 분리하고 의존성을 줄여 변경 용이성과 유지보수성을 개선할 수 있어요.

  1. 전략을 Map 형태로 관리
    전략 빈을 Map<String, HotBoardSelectionStrategy> 형태로 주입받아 관리하면, 새로운 전략이 추가되더라도 컨테이너가 자동으로 맵에 포함시켜 주기 때문에 별도의 추가 작업이 필요 없게 돼요.

  2. Redis와 전략 연계
    현재 캐싱 구조도 잘 설계되어 있지만, 전략별로 Redis 키를 분리해서 관리하면 여러 전략 간 데이터 충돌 없이 명확하게 관리할 수 있어요.
    Redis 키를 {prefix}:{strategyName} 형태로 구성하는 방식을 추천드려요.

  • 클라이언트 요청 시 전략을 파라미터로 함께 받아, 해당 전략을 활용해 Redis에서 데이터를 조회해주세요.
  • Redis에서 값을 조회했을 때 데이터가 없을 경우, 전략에 따라 데이터를 선정하고 Redis에 저장하는 로직으로 개선하면 성능과 확장성을 모두 확보할 수 있어요.
  1. OCP(Open-Closed Principle)에 대한 의견
    현재 방식에서 Enum 기반 StrategyType을 사용하는 것은 새로운 전략 추가 시 Enum 값을 변경해야 하므로 OCP를 완전히 만족하지 못할 수 있어요.
    하지만, 상수와 Map을 활용하면 기존 코드를 수정하지 않고도 새로운 전략을 추가할 수 있도록 설계되어 OCP 준수 측면에서 더 나은 선택이 될 수 있어요.

관련 구현 사례

제가 이전에 구현했던 전략 패턴 기반 코드가 현재 논의 중인 설계와 유사한 점이 많아, 참고하실 수 있도록 공유드려요.

https://github.com/limehee/ngram-similarity-search/blob/main/NGramSimilarityService.java
https://github.com/limehee/ngram-similarity-search/tree/main/similarity

해당 코드는 유사도 계산에 다양한 알고리즘을 적용하기 위해 전략 패턴을 활용한 구현 사례에요. 현재 설계에서 참고할만한 부분은 다음과 같아요.

  1. 전략 등록 및 선택 방식
    전략을 Map<String, SimilarityStrategy> 형태로 관리하여, 이름을 통해 동적으로 전략을 선택할 수 있도록 설계했어요. 새로운 전략을 추가해도 기존 코드를 수정할 필요 없이 확장이 가능해요. 현재 설계에서도 비슷한 방식으로 전략을 관리하면 확장성과 유지보수성을 모두 가져갈 수 있어요.

  2. 기본 전략 설정
    전략 이름이 없거나 잘못된 이름이 전달된 경우 기본 전략을 반환하도록 설정하여 안정성을 높였어요. 현재 설계에서도 동일하게 기본 전략을 지정하면 잘못된 요청에도 유연하게 대응할 수 있을 거예요.

  3. 캐싱 활용
    참고 코드에서는 Redis 대신 Caffeine 캐시를 사용했지만, 데이터를 조회하고 계산 결과를 캐싱하는 구조는 유사해요. 참고 코드에서 사용된 방식이 인기 게시글 선정 로직을 분리하면서 성능을 최적화하는 데 도움이 될 거예요.

  4. OCP 준수 설계
    새로운 전략 추가 시 기존 코드를 수정하지 않고 전략 구현체와 상수를 추가하는 방식으로 OCP를 준수했어요.
    현재 논의 중인 설계에서도 유연함을 더하는 데 도움이 될 거예요.

@SongJaeHoonn
Copy link
Contributor Author

SongJaeHoonn commented Dec 3, 2024

@limehee

상세히 써주신 리뷰와, 좋은 설계 패턴이 담겨있는 프로젝트를 참고용으로 추천해주셔서 정말 감사합니다!
해당 프로젝트를 둘러보기도 하고, 제안해주신 점들을 적용해보기도 하며 이틀간 많은 고민을 해봤습니다.

하지만,

  1. 전략의 빈 이름을 상수로 관리
    새로운 전략 생성시 상수로 정의된 클래스에 코드 한 줄을 추가하든, enum에 한 줄을 추가하든 비슷하다고 생각했습니다.
    상수 관리를 효율적으로 하기 위해 만들어둔 것이 열거형(enum)이기에 굳이 원시 상태(constants)로 돌아갈 필요가 있나 싶습니다.
    enum은 각 상수와 관련된 속성과 행동을 정의할 수 있어 가독성 및 유지보수성을 증가시킬 수 있다는 의견입니다. 특히, 필드의 discription에 해당 전략의 설명을 자세히 작성할 수 있다는 점이 좋은 것 같아요.

  2. 전략을 Map 형태로 관리
    저희는 인기게시글을 선정하고, 캐싱하는 로직을 매주 월요일 자정마다 자동으로 스케줄링되도록 계획했습니다.
    클라이언트가 월요일 자정마다 선정 api와 같은 것을 호출하지 않는다면 선정에 클라이언트는 관여할 수 없으며, 따라서 HotBoardRegisterService에서 Map으로 주입받아 코드 상에서 전략을 고를 수 있도록 구현했었습니다.
    이미 전략은 Map 형태로 관리되고 있습니다.

  3. Redis와 전략 연계
    이 부분을 가장 많이 고민했었는데, 2번의 이유와 같이 선정에 클라이언트가 관여할 수 없으므로 인기게시글 조회에서 클라이언트가 전략을 골라 조회하려면, 월요일 자정마다 미리 모든 전략들을 호출해 redis에 저장해 두어야 하기 때문에, 언제 다시 쓰일지도 모르는(이벤트성 조회 전략) 전략들을 매주 실행시키는 것이 더 효율적이지 않을 것 같다는 생각이 듭니다.

이와 같은 이유들로 코드 수정이 어려웠으며, 예시 들어주신 유사도 계산 알고리즘을 적용하는 프로젝트에서는, 우리의 인기 게시글 기능과는 달리 유사도 계산 및 저장에 클라이언트가 관여할 수 있는 것 같아 이 답글을 작성하는 바입니다..!

@limehee
Copy link
Collaborator

limehee commented Dec 5, 2024

@SongJaeHoonn
먼저, 저의 제안에 깊이 고민하고 구체적인 피드백을 남겨주셔서 감사하다는 말씀을 드립니다.

  1. 전략의 빈 이름을 상수로 관리
    새로운 전략 생성시 상수로 정의된 클래스에 코드 한 줄을 추가하든, enum에 한 줄을 추가하든 비슷하다고 생각했습니다.
    상수 관리를 효율적으로 하기 위해 만들어둔 것이 열거형(enum)이기에 굳이 원시 상태(constants)로 돌아갈 필요가 있나 싶습니다.
    enum은 각 상수와 관련된 속성과 행동을 정의할 수 있어 가독성 및 유지보수성을 증가시킬 수 있다는 의견입니다. 특히, 필드의 discription에 해당 전략의 설명을 자세히 작성할 수 있다는 점이 좋은 것 같아요.

전략명을 상수로 관리하면, 굳이 열거형(enum)을 사용하여 설명을 추가할 필요가 없다고 생각했습니다. 또한, 전략에 따른 동작을 선언할 필요도 없을 것 같다는 판단 하에 이 방법을 제안드렸습니다. 열거형을 사용할 경우 {전략타입}.{전략}.get_() 형태로 전략을 명시할 수 있지만, 상수를 사용하면 {전략타입}.{전략} 형태로 더 간략하게 표현할 수 있습니다. 하지만, 재훈님의 의견에 따라 추후 전략이 다양해질 것을 고려하여 현재의 열거형을 유지하는 것도 좋은 선택이라고 생각됩니다.

현재 전략의 빈 이름이 상수로 관리되지 않고 하드코딩되어 있습니다. 추후 전략 이름이 변경될 경우를 대비하여 HotBoardSelectionStrategyType.DEFAULT.getKey()와 같은 형식으로 접근하는 것이 변경 용이성을 높일 수 있을 것으로 보입니다.

@Service("default") // 상수로 변경 필요
@RequiredArgsConstructor
public class DefaultHotBoardSelectionStrategy implements HotBoardSelectionStrategy {

  1. 전략을 Map 형태로 관리
    저희는 인기게시글을 선정하고, 캐싱하는 로직을 매주 월요일 자정마다 자동으로 스케줄링되도록 계획했습니다.
    클라이언트가 월요일 자정마다 선정 api와 같은 것을 호출하지 않는다면 선정에 클라이언트는 관여할 수 없으며, 따라서 HotBoardRegisterService에서 Map으로 주입받아 코드 상에서 전략을 고를 수 있도록 구현했었습니다.
    이미 전략은 Map 형태로 관리되고 있습니다.

현재 HotBoardRegisterService에서 모든 전략이 Map 형태로 등록되어 있지만, DEFAULT 타입으로 고정되어 있는 것을 확인했습니다. 전략 패턴은 특정 행위를 캡슐화하여 동적으로 설정하거나 변경할 수 있도록 하는 것이 핵심입니다. 현재의 코드는 전략 패턴의 본질을 충분히 살리지 못하고 있는 것으로 보입니다.

public void registerHotBoards() {
    HotBoardSelectionStrategy strategy = strategies.get(HotBoardSelectionStrategyType.DEFAULT.getKey()); // DEFAULT 전략으로 고정됨

따라서, 이전에 제안드린 대로 클라이언트로부터 인기 게시글 선정 방식을 받아 요청에 따라 다양한 전략을 유동적으로 사용할 수 있도록 설계를 개선하는 것이 더 나은 방향일 것 같습니다.


  1. Redis와 전략 연계
    이 부분을 가장 많이 고민했었는데, 2번의 이유와 같이 선정에 클라이언트가 관여할 수 없으므로 인기게시글 조회에서 클라이언트가 전략을 골라 조회하려면, 월요일 자정마다 미리 모든 전략들을 호출해 redis에 저장해 두어야 하기 때문에, 언제 다시 쓰일지도 모르는(이벤트성 조회 전략) 전략들을 매주 실행시키는 것이 더 효율적이지 않을 것 같다는 생각이 듭니다.

Redis 캐싱과 제가 이전에 제안드린 내용이 상충하는 부분이 있어 위와 같은 코멘트를 남기신 것으로 생각됩니다. 말씀하신 대로 모든 산정 방식을 미리 실행하여 결과를 저장해두는 것은 비효율적입니다.

번거롭겠지만 캐싱 방식을 다음과 같이 변경할 것을 제안드립니다.

  • 스케줄링을 통해 매주 월요일 자정에 Redis에 저장된 모든 캐시 데이터를 삭제합니다.
  • 기본(DEFAULT) 전략만을 실행하여 인기 게시글을 선정하고, 그 결과를 Redis에 저장합니다.
  • 기타 전략은 클라이언트로부터 요청이 들어올 때 실행하여 데이터를 Redis에 캐싱하도록 합니다.

이렇게 변경하면 필요한 시점에만 전략을 실행하고 캐싱함으로써 효율성을 높일 수 있을 것으로 생각됩니다.


요약

  1. 전략 이름 상수 관리: 열거형을 유지하면서 상수화하여 전략 이름 변경 시 용이하게 대응.
  2. 동적 전략 선택: 클라이언트 요청에 따라 다양한 전략을 유연하게 선택 및 적용.
  3. 캐싱 최적화: 기본 전략은 주기적으로 실행하여 캐싱하고, 기타 전략은 요청 시에만 실행 및 캐싱.

@SongJaeHoonn
Copy link
Contributor Author

@limehee
감사합니다! 제안해주신 내용이 작업 방향성에 많은 도움이 되었습니다.
따라서, 제안해주신 방향대로 코드를 리팩토링해보았습니다.

  1. 전략 이름 상수 관리
    원래 enum으로 상수를 관리하려고 했으나, enum의 이름이 HotBoardSelectionStrategies와 같이 길다는 점, @Service(상수)에 enum의 getKey()메서드를 사용할 수 없다는 등의 이유에서 enum이 아닌 단순 상수 관리 클래스로 작업하는 것이 좋아보여 열거형 대신 상수 관리 클래스를 사용했습니다.

  2. 동적 전략 선택, 캐싱 최적화
    클라이언트가 전략 이름과 함께 요청을 하면, 요청 시 redis key에 해당하는 value(인기 게시글)이 없다면 해당 전략을 이용해 저장 후 조회하고,
    이미 캐싱된 인기 게시글이 있다면 바로 조회하도록 변경했습니다.
    따라서 월요일 자정마다 default전략만을 저장하는 registerDefaultHotBoards , 전략 이름을 통해 동적으로 저장할 수 있는 메서드인 registerHotBoards로 메서드를 구분했습니다.
    그리고 전략 이름이 잘못되거나 없는 전략을 요청할 때에는 default전략을 자동 반환하도록 구현했습니다!

Copy link
Collaborator

@limehee limehee left a comment

Choose a reason for hiding this comment

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

“HotBoardSelectionStrategies”는 실질적인 도메인 로직보다는 전략 식별용 상수를 모아둔 유틸성 클래스에 가까워서 service 계층 쪽 패키지로 옮겨 관리하는 편이 더 자연스러울 것 같아요.

@SongJaeHoonn
Copy link
Contributor Author

“HotBoardSelectionStrategies”는 실질적인 도메인 로직보다는 전략 식별용 상수를 모아둔 유틸성 클래스에 가까워서 service 계층 쪽 패키지로 옮겨 관리하는 편이 더 자연스러울 것 같아요.

뭔가 서비스 계층에서는 비즈니스 로직들만 처리한다는 인식이 있어서 enum을 도메인 패키지에 넣듯이 이것도 도메인 패키지에 넣었었는데 꼭 그렇지만은 않는군요

해당 상수들을 service 패키지에서만 이용하다보니 import문이 하나씩 줄어들어서 이런 점에서도 괜찮네요!
수정했습니다.

@SongJaeHoonn SongJaeHoonn requested a review from limehee December 14, 2024 20:56
public class HotBoardSelectionStrategies {

public static final String DEFAULT = "default";

Copy link
Collaborator

Choose a reason for hiding this comment

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

전략패턴과 OCP 관련해서 많은 고민들을 진행해주신 작업인 것 같습니다! 저도 디자인 패턴 책 읽을 때 가볍게
들어봤던 전략패턴에 대해서 실제로 어떻게 사용할 수 있을 지 알아갈 수 있던 기회였습니다. 제가 @SongJaeHoonn님의 작업을 이해한 게 맞는지 한 번 확인해주시면 감사할 것 같습니다!

  1. 추후 새로운 인기 게시글 산정 기준이 생긴다면, 이 부분에 전략 이름을 명시하고, DefaultHotBoardSelectionStrategy처럼 새로운 전략을 구현하는 방식일까요?

  2. 다양한 전략들은 모두 HotBoardSelectionsStrategy를 implement하기 때문에 Map으로 관리 될 수 있는 것이고, 전략 이름으로 원하는 것을 사용할 수 있는 것인거죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

추후 새로운 인기 게시글 산정 기준이 생긴다면, 이 부분에 전략 이름을 명시하고, DefaultHotBoardSelectionStrategy처럼 새로운 전략을 구현하는 방식일까요?

정확합니다!

다양한 전략들은 모두 HotBoardSelectionsStrategy를 implement하기 때문에 Map으로 관리 될 수 있는 것이고, 전략 이름으로 원하는 것을 사용할 수 있는 것인거죠?

맞습니다. 클라이언트가 파라미터로 전략 이름과 함께 요청하면, HotBoardSelectionsStrategy가 전략 이름으로 매핑된 Map에서 전략을 찾아 인기 게시글을 저장(해당 전략으로 저장된 인기게시글이 없을 시)하거나 조회(해당 전략으로 저장된 인기게시글이 있을 시)하게 됩니다.

@mingmingmon mingmingmon merged commit 41f71c8 into develop Dec 15, 2024
3 checks passed
@limehee limehee deleted the feat/#607 branch December 23, 2024 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ Feature 새로운 기능 명세 및 개발
Projects
None yet
Development

Successfully merging this pull request may close these issues.

인기 게시글 기능 구현
3 participants