
핫 게시판을 만들고자 할 때 내가 생각했던 방법은 좋아요 수와 댓글 수를 합쳐서 내림차순으로 정렬시키는 것이었다
아는 형이 인기 게시물 알고리즘을 설명해주시는 것을 듣고 눈앞이 캄캄했다
꽤나 복잡한 편이라고 생각했기 때문에 굉장히 어려울것 같았다
그래서 이해를 하기 위해 서칭을 했다
하지만 한글로 구글링을 하니 알고리즘을 찾을 수 없었다
그래서 내가 유일하게 들어본 외국 커뮤니티인 Reddit의 인기 게시물 알고리즘을 서칭해보니 아주 자세하게 설명되어있었다
그래서 아래 게시물에서 설명하는 reddit hot algorithm을 기반으로 인기 게시물을 개발하였다
https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9
나는 myBatis를 사용하여 개발중이기 때문에 데이터베이스에 hot() 함수를 만드는 것이 나을것이라고 판단했다
그래서 우선 아래와 같이 함수를 생성하였다
(꼭 루트권한을 가진 사용자로 로그인 한 후 함수를 생성하도록 하자)
CREATE DEFINER=`root`@`%` FUNCTION `hot`(ups INT, date_created DATETIME) RETURNS float
BEGIN
DECLARE s INT;
DECLARE order_val FLOAT;
DECLARE sign_val INT;
DECLARE seconds FLOAT;
DECLARE random_val FLOAT;
IF ups > 1 THEN
SET order_val = LOG(ups, 10);
ELSE
SET order_val = 0;
END IF;
SET sign_val = CASE WHEN ups > 0 THEN 1 ELSE 0 END;
SET seconds = UNIX_TIMESTAMP(date_created) - 1134028003;
RETURN ROUND(sign_val * order_val + seconds / 45000, 7);
END
로그를 사용함으로써 처음 10개의 찬성표는 다음 100개의 찬성표와 같은 가중치를 갖게 된다
즉 초반에한 투표가 게시물의 인기도에 더 많은 영향을 준다는 것이다
레딧의 알고리즘을 참고하였지만 필요하지 않은 몇개의 요소들은 생략하여 함수를 작성하였다
<select id="findHotBoards" resultType="HotBoardDto">
SELECT
b.id AS boardId,
title,
SUBSTRING(b.body, 1, 30) as body,
b.created_at AS createdAt,
b.modified_at AS modifiedAt,
IF(is_anonymous, '익명',u.nickname) AS userNickname,
comment_cnt AS commentCnt,
like_cnt AS likeCnt,
is_anonymous AS isAnonymous,
is_hide AS isHide,
IF(blike.user_id IS NOT NULL, 1, 0) as isLike,
b.images_url as imagesUrlString,
GROUP_CONCAT(t.name ORDER BY t.name ASC SEPARATOR ',') as tagStrings,
type_id AS typeId,
bt.name AS type
FROM board b
INNER JOIN user u ON b.user_id = u.id
INNER JOIN board_type bt ON b.type_id = bt.id
LEFT OUTER JOIN board_like blike ON b.id = blike.board_id AND blike.user_id = #{userId}
LEFT OUTER JOIN board_tag ON b.id = board_tag.board_id
LEFT OUTER JOIN tag t ON board_tag.tag_id = t.id
WHERE
is_hide = 0 AND
b.state = 0 AND
b.category_id = #{categoryId} AND
like_cnt >= 10
GROUP BY b.id
ORDER BY hot(like_cnt, b.created_at) + RAND(#{seed}) DESC
LIMIT #{pagingDto.recordSize} OFFSET #{pagingDto.offset}
</select>
유의하여 볼 곳은 ORDER BY 절이다
이 문장에서 hot()의 결과값에 랜덤값을 기준으로 게시물을 정렬한다
Rand(seed)를 사용하여 인기도에 무작위성을 추가하여 페이지를 로드할때 마다 게시물 목록의 순서를 변경한다
이를 통해 사용자에게 다양한 인기게시물을 보여줄 수 있다
fun findHotBoardList(
userId: Int,
category: Int,
boardTypeId: Int?,
pagingDto: PagingDto
): PagingResponse<HotBoardDto> {
//페이지가 1일떈 새로운 seed값 생성 10분 후 삭제
if (pagingDto.page == 1) {
redisService.set("seed|$userId", Random().nextLong().toString())
redisService.expire("seed|$userId", 60 * 10)
}
var seed = redisService.get("seed|$userId")?.toLong();
//사용자가 page가 1이아닌 2부터 요청할 경우 && redis에 seed값이 없을 경우 null일 수 있음
if (seed == null) {
seed = Random().nextLong()
redisService.set("seed|$userId", seed.toString())
redisService.expire("seed|$userId", 60 * 10)
}
val pagination: Pagination
val hotBoards: List<HotBoardDto>
if (boardTypeId != null) {
val count = boardMapper.getHotBoardCountWithBoardType(category, boardTypeId);
pagination = Pagination(count, pagingDto)
hotBoards = boardMapper.findHotBoardsWithBoardType(userId, category, boardTypeId, seed, pagingDto)
} else {
val count = boardMapper.getHotBoardCount(category);
pagination = Pagination(count, pagingDto)
hotBoards = boardMapper.findHotBoards(userId, category, seed, pagingDto)
}
return PagingResponse(hotBoards, pagination)
}
서비스 함수의 초반 부분을 보면 redis를 이용하여 각 user마다 seed값을 랜덤으로 설정해주는것을 볼 수 있다
페이지가 1일때는 seed가 새로 생성되고 10분 후에 삭제된다
그리고 사용자가 페이지2부터 요청할 경우 seed값이 만료되었다면 다시 seed값을 생성해 주도록 한다
이렇게 만들어진 seed 변수를 매퍼 함수의 인자로 사용하여 인기 게시물을 로드할 때마다 이전과는 다른 게시물 목록을 보여줄 수 있다
🫢