[REFACTOR] 소설 필터 조회 최적화 - 소설 통계 데이터 추가#543
Conversation
GiJungPark
left a comment
There was a problem hiding this comment.
확인했습니다!
그런데, SELECT FOR UPDATE에서 JOIN으로 연관된 테이블까지 LOCK이 걸려서 Novel의 정합성을 보장한 것 같은데 맞는지 확인 부탁드려요!
| @Column(nullable = false, columnDefinition = "decimal(4, 3) default 0.000") | ||
| @Comment("작품 평균 평점") | ||
| private BigDecimal averageRating = BigDecimal.ZERO; | ||
|
|
||
| @Column(nullable = false, columnDefinition = "decimal(19, 1) default 0.0") | ||
| @Comment("작품 평점 합계") | ||
| private BigDecimal ratingSum = BigDecimal.ZERO; | ||
|
|
||
| @Column(nullable = false, columnDefinition = "bigint default 0") | ||
| @Comment("작품 평점 등록 수") | ||
| private Long ratingCount = 0L; | ||
|
|
||
| @Column(nullable = false, columnDefinition = "bigint default 0") | ||
| @Comment("관심 등록 또는 하차 외 독서 상태인 사용자 수") | ||
| private Long popularity = 0L; |
There was a problem hiding this comment.
p3;
Novel이라는 도메인은 현재 서비스에서 거의 공통으로 사용되는 감이 크다고 생각해요! (피드, 서재, 곧 추가되는 컬렉션 등..)
그래서 Novel과 동일한 PK를 가지도록 하고 엔티티를 구분해서 JOIN을 통해서 소설 상세 정보가 필요할 때 가져올 수 있도록 하는건 어떨까 하는 생각이 드는데 어떻게 생각하시나요?
There was a problem hiding this comment.
좋습니다! novel_statistics 테이블을 별도로 만든후에 로직 이전 후 머지하겠습니다
넵 맞습니다! 사용자의 요청이 중복으로 들어왔을때 정확한 집계를 위해 락을 걸었습니다. |
변경 사항
|
Related Issue
Key Changes
소설 통계 데이터 역정규화
소설 필터 조회에서 매 작품마다 수행되던 평균 평점 및 인기도 집계 서브쿼리를 제거했습니다.
Novel에 평균 평점, 평점 합계, 평가 수, 인기도 컬럼을 추가했습니다.평균 평점과 평점 합계는 부동소수점 오차를 방지하기 위해
DECIMAL을 사용하도록 했습니다.UserNovel 변경 시 통계 차이 반영
UserNovel생성, 수정, 삭제 및 관심 등록 상태 변경 시 작품 통계를 갱신하도록 추가했습니다.전체 데이터를 다시 집계하지 않고 변경 전후의 평점 합계, 평가 수, 인기도 차이만 작품 테이블에 원자적으로 반영합니다.
통계에 영향을 주지 않는 변경은 작품 통계 UPDATE 쿼리를 실행하지 않도록 했습니다.
동시성 처리
동일한 사용자의 동일 작품 평가가 동시에 변경될 때 통계 차이가 중복 반영되지 않도록
SELECT FOR UPDATE조회를 추가했습니다.비관적 잠금 조회는
UserNovelCustomRepository에서 QueryDSL로 구현했습니다.관심 등록은
INSERT IGNORE후 잠금 조회를 수행해 신규 생성과 기존 데이터 변경을 구분하도록 했습니다.소설 필터 조회 쿼리 개선
평균 평점 조건과 인기도 정렬이 작품 테이블의 역정규화 컬럼을 사용하도록 변경했습니다.
기본 평점 범위인 0.0~5.0 조회에서는 불필요한 평균 평점 조건을 추가하지 않도록 했습니다.
장르와 플랫폼 조인은 해당 필터가 요청된 경우에만 수행하도록 변경했습니다.
소설 목록 응답 N+1 완화
소설 목록 응답의 평균 평점과 평가 수는 작품 테이블의 역정규화 컬럼을 사용하도록 변경했습니다.
관심 수는 조회된 페이지의 작품 ID만 대상으로 한 번의 GROUP BY 쿼리로 조회하도록 변경했습니다.
작품별 관심 수 조회 결과는
NovelInterestCountprojection으로 분리했습니다.Library 패키지 구조 정리
기존 공용 controller 패키지의
LibraryController를library.controller패키지로 이동했습니다.작품 통계 기여값을 나타내는
UserNovelStatistics를library.domain의 별도 record로 분리했습니다.테스트 추가
평점 및 인기도 차이 계산, 통계 UPDATE 생략, 삭제 시 통계 차감, 관심 수 집계에 대한 단위 테스트를 추가했습니다.
To Reviewers
애플리케이션 실행 전 DB에
average_rating,rating_sum,rating_count,popularity컬럼이 반영되어 있어야 합니다.소설 필터 조회의 평균 평점 및 인기도 집계 부하는 제거했지만, 장르, 플랫폼, 키워드 필터의 추가 인덱스 및 쿼리 튜닝은 실행 계획 측정 후 별도 작업으로 진행할 예정입니다.
개발 db에 아래 쿼리를 적용 하였습니다.
CREATE TABLE novel_statistics (
novel_id BIGINT NOT NULL,
average_rating DECIMAL(4, 3) NOT NULL DEFAULT 0.000 COMMENT '작품 평균 평점',
rating_sum DECIMAL(19, 1) NOT NULL DEFAULT 0.0 COMMENT '작품 평점 합계',
rating_count BIGINT NOT NULL DEFAULT 0 COMMENT '작품 평점 등록 수',
popularity BIGINT NOT NULL DEFAULT 0 COMMENT '관심 등록 또는 하차 외 독서 상태인 사용자 수',
PRIMARY KEY (novel_id),
INDEX idx_novel_statistics_average_rating (average_rating),
INDEX idx_novel_statistics_popularity (popularity),
CONSTRAINT fk_novel_statistics_novel
FOREIGN KEY (novel_id) REFERENCES novel (novel_id)
ON DELETE CASCADE
);
INSERT INTO novel_statistics (
novel_id,
average_rating,
rating_sum,
rating_count,
popularity
)
SELECT
n.novel_id,
CAST(
ROUND(COALESCE(AVG(NULLIF(un.user_novel_rating, 0)), 0), 3)
AS DECIMAL(4, 3)
),
CAST(
ROUND(COALESCE(SUM(NULLIF(un.user_novel_rating, 0)), 0), 1)
AS DECIMAL(19, 1)
),
COUNT(NULLIF(un.user_novel_rating, 0)),
COUNT(CASE
WHEN un.is_interest = TRUE OR un.status <> 'QUIT' THEN 1
END)
FROM novel n
LEFT JOIN user_novel un ON un.novel_id = n.novel_id
GROUP BY n.novel_id
ON DUPLICATE KEY UPDATE
average_rating = VALUES(average_rating),
rating_sum = VALUES(rating_sum),
rating_count = VALUES(rating_count),
popularity = VALUES(popularity);
References