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/#221] 아티클 조회수 저장 방식 및 조회 방식 개선 #223

Merged
merged 13 commits into from
Jul 21, 2024

Conversation

hun-ca
Copy link
Member

@hun-ca hun-ca commented Jul 20, 2024

🎫 연관 이슈

resolved #221

💁‍♂️ PR 내용

  • flyway 'V1.00.0.14' 배포: ARTICLE_VIEW_COUNT 테이블 추가
  • 조회수를 가져오는 방식
    • as-is: 조회 요청이 왔을때 즉시 count 쿼리
    • to-be: ARTICLE_VIEW_COUNT에서 article id로 조회수 자체를 select
  • ARTICLE_VIEW_COUNT 테이블은 추후 메인 페이지 내 아티클을 조회수 기반으로 정렬할 때에도 사용될 중요한 테이블로서 역할을 할 예정입니다

🙏 작업

🙈 PR 참고 사항

해당 PR 배포 후 아래 SQL롤 테스트 확인 필요

SELECT * FROM ARTICLE_VIEW_COUNT ORDER BY view_count DESC LIMIT 10;

📸 스크린샷

🤖 테스트 체크리스트

  • 체크 미완료
  • 체크 완료

@hun-ca hun-ca added refactor 기존 기능에 대해 개선할 때 사용됩니다. DB DB 관련 수정이 이러날 때 사용됩니다. labels Jul 20, 2024
@hun-ca hun-ca self-assigned this Jul 20, 2024
@hun-ca hun-ca requested a review from belljun3395 as a code owner July 20, 2024 06:44
@github-actions github-actions bot added the feature 새로운 기능을 만들 때 사용됩니다 label Jul 20, 2024
Copy link
Member Author

@hun-ca hun-ca left a comment

Choose a reason for hiding this comment

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

리뷰 부탁드립니다

Comment on lines 2 to 8
CREATE TABLE ARTICLE_VIEW_COUNT
(
article_id BIGINT NOT NULL,
view_count BIGINT NOT NULL,
deleted_at TIMESTAMP NULL DEFAULT NULL,
CONSTRAINT article_view_count_pk PRIMARY KEY (article_id)
);
Copy link
Member Author

Choose a reason for hiding this comment

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

ARTICLE_VIEW_COUNT 테이블 정의

Copy link
Member Author

Choose a reason for hiding this comment

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

(참고로 PK 이름 지정함: article_view_count_pk)

Comment on lines 14 to 20
fun upsertArticleViewCount(query: ArticleViewCountQuery) {
dslContext.insertInto(ARTICLE_VIEW_COUNT)
.set(ARTICLE_VIEW_COUNT.ARTICLE_ID, query.articleId)
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, 1)
.onDuplicateKeyUpdate()
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, ARTICLE_VIEW_COUNT.VIEW_COUNT.plus(1))
}
Copy link
Member Author

Choose a reason for hiding this comment

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

ARTICLE_VIEW_COUNT 테이블에 조회수 컬럼을 upsert함.
기존 로우가 존재하면 count 값만 늘리고 로우 자체가 없으면 Insert진행 (최초 아티클 조회시에 해당)

Comment on lines 26 to 28

articleViewCountDao.upsertArticleViewCount(ArticleViewCountQuery(articleId))
log.debug { "Successfully upserted article view count for articleId: $articleId and memberId: $memberId" }
Copy link
Member Author

Choose a reason for hiding this comment

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

기존에 아티클 view his 테이블에 저장하는걸 async로 구성했는데, 마찬가지로 해당 과정에서 Insert가 끝나면 ARTICLE_VIEW_COUNT 테이블에 Upsert를 진행함. (어제 DB 트리거로 만들어뒀던 부분을 애플리케이션 단으로 옮긴 것)

Comment on lines +10 to +18
@Component
class ArticleViewCountHandler(
private val articleViewCountDao: ArticleViewCountDao,
) {
@Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRES_NEW)
fun browseArticleViewCount(articleId: Long): Long {
return (articleViewCountDao.selectArticleViewCount(ArticleViewCountCommand(articleId)) ?: 0L) + 1L
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

아티클 조회수를 조회하는 서비스

  • Isolation.READ_UNCOMMITTED: 조회수 데이터 정합성 필요없이 최대한 빠르게 조회하기 위해 격리 레벨을 최하위로 낮춤. 조회수 자체가 크게 중요한 데이터도 아니고 해서 커밋 안되었더라도 읽게 함
  • Propagation.REQUIRES_NEW: 기존 트랜잭션이 있을 경우 새로운 트랜잭션을 만들어서 진행. (기존 트랜잭션에 격리레벨을 분리하기 위함)

Comment on lines 10 to 12
-- 조회수 순으로 아티클 조회시 사용하기 위한 인덱스
-- ex. SELECT * FROM ARTICLE_VIEW_COUNT ORDER BY view_count;
CREATE INDEX article_view_count_desc_idx1 ON ARTICLE_VIEW_COUNT(view_count DESC);
Copy link
Member Author

Choose a reason for hiding this comment

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

메인 페이지에서 아티클 전체 리스트업 정렬 기준이 조회수로 내림차순 되기 때문에 필요한 인덱스 사전작업

ex SQL

SELECT * FROM ARTICLE_VIEW_COUNT ORDER BY view_count DESC limit 50;

Copy link
Member Author

@hun-ca hun-ca left a comment

Choose a reason for hiding this comment

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

변경 사항 재확인 부탁드려요...

Comment on lines +2 to +9
CREATE TABLE ARTICLE_VIEW_COUNT
(
article_id BIGINT NOT NULL,
view_count BIGINT NOT NULL,
category_cd TINYINT NOT NULL,
deleted_at TIMESTAMP NULL DEFAULT NULL,
CONSTRAINT article_view_count_pk PRIMARY KEY (article_id)
);
Copy link
Member Author

Choose a reason for hiding this comment

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

ARTICLE_VIEW_COUNT 테이블 CATEGORY_CD 컬럼 추가 반영. 해당 값은 아티클 마스터에 있는 컬럼이지만 정규화하면 오히려 손해임

Comment on lines 14 to 21
fun upsertArticleViewCount(query: ArticleViewCountQuery) {
dslContext.insertInto(ARTICLE_VIEW_COUNT)
.set(ARTICLE_VIEW_COUNT.ARTICLE_ID, query.articleId)
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, 1)
.set(ARTICLE_VIEW_COUNT.CATEGORY_CD, query.categoryType?.code)
.onDuplicateKeyUpdate()
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, ARTICLE_VIEW_COUNT.VIEW_COUNT.plus(1))
}
Copy link
Member Author

Choose a reason for hiding this comment

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

ARTICLE_VIEW_COUNT 테이블 upsert 쿼리 수정: CATEGORY_CD 추가 반영. 메인화면 아티클 조회수 별 정렬에서 사용될 테이블인데, 정렬기준에 카테고리 까지는 포함이 되어야 할 거 같음...

Copy link
Collaborator

Choose a reason for hiding this comment

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

upsert 쿼리가 무엇인지 알 수 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

테이블에 업데이트할 로우가 없으면 인서트하고, 있으면 업데이트

CREATE INDEX article_view_count_idx1 ON ARTICLE_VIEW_COUNT (view_count DESC);

-- 카테고리 별 필터링을 위한 인덱스
CREATE INDEX article_view_count_idx2 ON ARTICLE_VIEW_COUNT (category_cd);
Copy link
Member Author

Choose a reason for hiding this comment

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

카테고리 코드에 인덱싱 추가

Copy link
Member Author

Choose a reason for hiding this comment

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

특별히 카테고리를 넣은 이유는 메인 페이지에서 카테고리별 정렬될때 사용됨... article_view_count 테이블 자체가 거기서 메인페이지에서 아티클 ordering할 기준 테이블이 되기 때문에 여기에 카테고리가 있어야지 없으면 ordering할 떄 다시 아티클 테이블 가서 조회해 와야 함

.set(ARTICLE_VIEW_COUNT.ARTICLE_ID, query.articleId)
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, 1)
.set(ARTICLE_VIEW_COUNT.CATEGORY_CD, query.categoryType?.code)
.onDuplicateKeyUpdate()
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거는 뭔가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

article_view_count 테이블 로우는 아티클 당 한개로 구성되는데, 첫번째로 조회될 땐 insert가 가고, 이미 row가 있는 경우(이미 1번 이상 조회됨) Update(view_count + 1) 쿼리가 날라가는 upsert 쿼리입니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하 신기하다..!!!

Copy link
Member Author

Choose a reason for hiding this comment

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

SQL 변환

INSERT INTO ARTICLE_VIEW_COUNT (ARTICLE_ID, VIEW_COUNT, CATEGORY_CD)
VALUES ($articleId, 1, $category)
ON DUPLICATE KEY UPDATE
VIEW_COUNT = VIEW_COUNT + 1;

class ArticleViewCountHandler(
private val articleViewCountDao: ArticleViewCountDao,
) {
@Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRES_NEW)
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거 @ViewTransactional 요런 어노테이션으로 선언 해버릴까요?


articleViewHisAsyncHandler.addArticleViewHis(useCaseIn.articleId, useCaseIn.memberId)
// ARTICLE VIEW HIS에 저장하기 전에 먼저 VIEW COUNT 조회하는 순서 변경 금지
val views = articleViewCountHandler.browseArticleViewCount(useCaseIn.articleId)
Copy link
Collaborator

Choose a reason for hiding this comment

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

요친구는 왜 네이밍이 handler이죠? 저는 반환을 하니 service가 어울릴꺼 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

service도 생각했었는데 같은 도메인이여서 네이밍 뭘로할지 고민되네요

Copy link
Collaborator

Choose a reason for hiding this comment

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

진짜 애매하다..

) {
private val log = KotlinLogging.logger {}

@Async(value = DATABASE_ACCESS_POOL)
@Transactional
fun addArticleViewHis(articleId: Long, memberId: Long) {
fun addArticleViewHis(articleId: Long, memberId: Long, categoryType: CategoryType?) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

요기 ? 를 사용하기 전에 처리하고 들어가는게 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

UC에서 카테고리 가져올때 CategoryType 자체적으로 코드 -> 객체 변환시 nullable 리턴이라 저길 어떻게 바꾸면 좋을지…

Copy link
Collaborator

Choose a reason for hiding this comment

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

요청 전에 ?: 을 사용해서 확인하고 널이면 예외를 발생시키면 될 것같아요

CategoryType.fromCode(articleRecord.category)
CategoryType.fromCode(articleRecord.category) ?: throw RuntimeException("invalid article")
Copy link
Member Author

Choose a reason for hiding this comment

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

모든 아티클은 카테고리가 필수인데, 카테고리에 정의되지 않은 경우가 발생하는거 자체가 말이 일단 안되는 상황이라.. 예외코드를 정의하고 던지는게 맞을까 싶어요

Copy link
Collaborator

Choose a reason for hiding this comment

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

저도 그렇긴 한데 말이 안되는 상황도 대비하는 것은 나쁘지 않은 것 같아요

@@ -1,2 +1,3 @@
article.notfound.id=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0069\u0064
article.notfound.articleidworkbookid=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u0069\u0064\u0077\u006f\u0072\u006b\u0062\u006f\u006f\u006b\u0069
article.invalid.category=\uc874\uc7ac\ud558\uc9c0\u0020\uc54a\ub294\u0020\uc544\ud2f0\ud074\u0020\uce74\ud14c\uace0\ub9ac\uc785\ub2c8\ub2e4\u000d
Copy link
Member Author

Choose a reason for hiding this comment

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

article.invalid.category 에러 코드 추가 (모든 아티클은 카테고리가 있기 때문에 이게 발생하는 상황이 오면 안됨)

Copy link
Member Author

Choose a reason for hiding this comment

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

@hun-ca hun-ca merged commit 28d00e8 into main Jul 21, 2024
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DB DB 관련 수정이 이러날 때 사용됩니다. feature 새로운 기능을 만들 때 사용됩니다 refactor 기존 기능에 대해 개선할 때 사용됩니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

아티클 조회수 관련 SQL 개선 사항
2 participants