Post

⚓ DATT 13 - Place 리뷰, 평점 시스템과 사용자 반응 기반 추천 구조

개요

현재 DATT는 다음 기능들을 제공한다.

1
2
3
4
장소 검색
Nearby Search
Bookmark
Anchor 자동 큐레이션

하지만 이것만으로는:

1
"사용자 반응"

을 알 수 없다.

예를 들어:

1
2
3
이 장소가 실제로 좋은가?
사람들이 만족했는가?
어떤 Anchor가 반응이 좋은가?

등을 판단할 수 없다.

따라서 이번 단계에서는:

1
2
Place 리뷰/평점 시스템
Anchor 좋아요 시스템

을 구축하였다.

핵심 목표는:

1
사용자 반응 데이터 축적

이다.


왜 Place 품질 데이터가 필요한가

처음에는 단순:

1
공공데이터 기반 장소 제공

만으로도 충분할 수 있다고 생각했다.

하지만 실제 서비스에서는 큰 문제가 생긴다.

예를 들어:

1
2
3
폐업 직전 가게
평점이 매우 낮은 장소
실제 만족도가 낮은 장소

등도 그대로 노출될 수 있다.

즉:

1
2
3
공공데이터
≠
좋은 장소

이다.


결국 중요한 것은 사용자 반응이다

실제 사용자 서비스에서는:

1
2
3
4
5
조회수
좋아요
리뷰
평점
저장 수

등이 훨씬 중요하다.

왜냐하면:

1
사용자 행동 데이터

는:

1
"실제 사람들이 좋아했는가"

를 보여주기 때문이다.

즉:

1
2
데이터 자체보다
사용자 반응이 더 중요하다.

그래서 리뷰/평점 시스템을 도입했다

이번 단계에서는:

1
PlaceReview

구조를 도입하였다.

핵심은:

1
사용자 경험 기록

이다.


PlaceReview Entity 설계

최종 구조는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(
    name = "place_review",
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uk_place_review_member_place",
            columnNames = {"member_id", "place_id"}
        )
    }
)
public class PlaceReview extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private PlaceMaster placeMaster;

    private int rating;

    private String content;
}

왜 리뷰와 평점을 분리하지 않았는가

처음에는 다음 구조도 고민했다.

1
2
PlaceRating
PlaceReview

즉:

1
평점만 남기는 구조

이다.

하지만 MVP 단계에서는 복잡도가 너무 커진다.

예:

1
2
3
4
평점만 남긴 사용자
리뷰도 남긴 사용자
평점 수정 정책
리뷰 생성 시 기존 평점 처리

등.


그래서 리뷰 안에 평점을 포함시켰다

최종적으로는:

1
리뷰 작성 = 평점 작성

구조로 설계했다.

즉:

1
2
3
4
{
  "rating": 5,
  "content": "데이트하기 분위기가 좋았습니다."
}

처럼 동작한다.

이 방식의 장점은:

1
2
3
구조 단순화
중복 정책 단순화
사용자 흐름 단순화

이다.


왜 사용자당 리뷰 1개만 허용했는가

중요한 정책이다.

이번 구조에서는:

1
2
3
한 사용자
=
한 장소당 리뷰 1개

만 허용한다.

즉:

1
member_id + place_id

에 Unique Constraint를 걸었다.


이유는 무엇인가

만약 중복 리뷰를 허용하면:

1
2
3
리뷰 스팸
평점 조작
의미 없는 반복 리뷰

문제가 발생한다.

또한 MVP 단계에서는:

1
리뷰 수정

만으로 충분하다.

즉:

1
후기 갱신

개념으로 보는 것이 자연스럽다.


평점 정책

현재 평점 범위는:

1
1 ~ 5

이다.

도메인 내부에서 직접 검증한다.

1
2
3
4
5
private void validateRating(int rating) {
    if (rating < 1 || rating > 5) {
        throw new BusinessException(ErrorCode.INVALID_REVIEW_RATING);
    }
}

즉:

1
잘못된 상태 자체를 생성하지 못하게

막았다.


Place 평점 집계 구조

이번 단계에서는:

1
2
averageRating
reviewCount

를 제공한다.

예:

1
2
3
4
{
  "averageRating": 4.5,
  "reviewCount": 12
}

왜 실시간 집계 방식으로 구현했는가

처음에는:

1
PlaceMaster에 컬럼 저장

도 고려했다.

예:

1
2
average_rating
review_count

하지만 현재 MVP에서는:

1
2
트래픽 규모
리뷰 규모

가 크지 않다.

따라서 우선은:

1
2
AVG(rating)
COUNT(*)

기반 실시간 집계로 구현했다.


나중에 어떻게 확장 가능한가

사용자 수가 증가하면:

1
캐시 컬럼 반정규화

방식으로 확장 가능하다.

예:

1
2
3
리뷰 생성
→ 평균 평점 갱신
→ PlaceMaster 저장

즉 현재는:

1
단순성 우선

이다.


Place Detail API와의 연동

리뷰/평점은:

1
Place Detail API

에 연동하였다.

즉 장소 상세 조회 시:

1
2
3
4
{
  "averageRating": 4.5,
  "reviewCount": 12
}

정보를 함께 내려준다.


왜 Place 품질 데이터가 중요한가

이 데이터는 단순 UI 정보가 아니다.

향후:

1
2
3
4
추천 시스템
랭킹
인기 장소
Anchor 추천 품질

등의 핵심 데이터가 된다.

예:

1
2
평점 높은 장소 우선 추천
리뷰 수 많은 장소 가중치 증가

등.

즉:

1
2
3
Place 품질 데이터
=
추천 엔진 기반

이다.


Anchor 좋아요 시스템 설계

Place와 별개로:

1
AnchorLike

도 도입하였다.


왜 Anchor에도 좋아요가 필요한가

DATT의 핵심은:

1
경험 큐레이션

이다.

즉 중요한 것은:

1
"이 장소 하나가 좋은가?"

만이 아니다.

오히려:

1
"이 경험 흐름이 좋은가?"

가 더 중요하다.

예:

1
2
3
성수 데이트 코스
강릉 여행 코스
홍대 술집 탐방

등.


AnchorLike Entity 설계

최종 구조는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(
    name = "anchor_like",
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uk_anchor_like_member_anchor",
            columnNames = {"member_id", "anchor_id"}
        )
    }
)
public class AnchorLike extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    private Anchor anchor;
}

왜 좋아요를 별도 Entity로 분리했는가

처음에는:

1
Anchor.likeCount++

같은 단순 카운팅도 고려했다.

하지만 좋아요 여부는:

1
사용자별 상태

가 중요하다.

예:

1
2
좋아요 눌렀는가?
좋아요 취소했는가?

등.

따라서:

1
Member ↔ Anchor 관계

를 별도 Entity로 모델링했다.


좋아요 응답 구조

현재 Anchor 응답에는 다음 정보가 포함된다.

1
2
3
4
{
  "likeCount": 12,
  "isLiked": true
}

즉:

1
2
3
좋아요 수
+
현재 사용자 좋아요 여부

를 동시에 제공한다.


인기 Anchor 구조

이번 단계에서는:

1
인기 Anchor 조회

기반도 구축하였다.

정렬 기준은:

1
2
3
조회수
좋아요 수
최신성

이다.

즉:

1
실제 반응이 좋은 경험

을 우선 노출한다.


사용자 반응 기반 추천 구조

현재 DATT는 단순:

1
공공데이터 제공

수준이 아니다.

핵심은:

1
사용자 반응 데이터 축적

이다.

현재 축적되는 데이터는 다음과 같다.

1
2
3
4
5
Bookmark
리뷰
평점
AnchorLike
조회수

왜 이 데이터가 중요한가

이 데이터들은 이후:

1
2
3
4
추천 시스템
개인화 추천
랭킹
인기 지역 탐색

등의 기반이 된다.

예:

1
2
3
평점 높은 장소 추천
좋아요 많은 Anchor 추천
저장 수 기반 인기 지역 추천

등.

즉:

1
2
3
사용자 반응 데이터
=
서비스 핵심 자산

이다.


현재 단계에서 의도적으로 제외한 것들

이번 단계에서는 다음 기능은 제외했다.

1
2
3
4
5
리뷰 이미지
리뷰 신고
리뷰 좋아요
AI 추천
실시간 트렌드 분석

이유는 단순하다.

현재 중요한 것은:

1
MVP 수준 데이터 축적 구조

를 만드는 것이기 때문이다.


현재 구조의 장점

현재 구조는 다음 장점을 가진다.

1
2
3
4
단순하다
확장 가능하다
추천 시스템으로 발전 가능하다
사용자 행동 데이터를 축적할 수 있다

특히 중요한 점은:

1
"사용자 행동 자체를 데이터로 본다"

는 것이다.


앞으로의 확장 방향

향후에는 다음 방향으로 확장 가능하다.


1. 리뷰 기반 추천

예:

1
비슷한 취향 사용자 추천

2. Anchor 추천 점수 고도화

예:

1
2
3
4
좋아요
저장 수
조회수
평점

조합.


3. 개인화 추천

예:

1
사용자 리뷰 성향 기반 추천

4. AI 기반 요약

예:

1
2
리뷰 요약
분위기 분석

마무리

이번 단계에서는:

1
2
3
Place 리뷰/평점
AnchorLike
사용자 반응 데이터

구조를 구축하였다.

핵심은:

1
2
"공공데이터 자체보다
사용자 반응 데이터를 더 중요하게 본다"

는 것이다.

DATT는 앞으로:

1
2
3
4
5
탐색
경험
평가
추천
개인화

를 중심으로 발전할 예정이다.

This post is licensed under CC BY 4.0 by the author.