팬 아웃 타임라인의
조회를 더 빠르게한 경험을 담은 글 입니다.
FanOut이란
소셜 미디어나 뉴스 피드와 같은 시스템에서 사용되는 데이터 전송 패턴으로, 사용자의 활동이나 게시물이 다른 사용자들의 타임라인에 어떻게 전달되는지를 설명하는 방식입니다.
FanOut Read란
사용자가 자신의 타임라인을 조회할 때마다, 그때 실시간으로 팔로우하는 사용자의 최신 게시물을 수집하여 타임라인을 구성하는 방식입니다.
FanOutRead 흐름잡기
public PostCursorDto<PostDto> execute(Long userId, PostsCursorRequest request) {
userReadService.getUser(userId);
List<Long> followings = followReadService.getFollowings(userId).stream().map(follow -> follow.getId()).collect(
Collectors.toList());
return postReadService.getPosts(followings, request);
}
(1) User의 존재 여부를 파악한다.
(2) User가 팔로우하고 있는 사용자의 id를 가져온다.
(3) 팔로우하는 사용자 id의 게시물들을 가져온다.
FanOut Write란
사용자가 게시물을 작성할 때, 그 게시물이 해당 사용자의 모든 팔로워의 타임라인에 즉시 추가되는 방식
FanOutRead 흐름잡기
public Long execute(PostCreateRequest request) {
userReadService.getUser(request.getUserId());
Long postId = postWriteService.create(request.getUserId(), request.getContents(), request.getCreatedDate(), request.getCreatedAt());
// 내가 글 작성자면, 나를 팔로워한 사람들의 항목을 추가해야한다.
List<Follow> followers = followReadService.getFollowers(request.getUserId());
List<TimeLine> timeLines = new ArrayList<>();
for(Follow follow : followers) {
timeLines.add(TimeLine.builder().userId(follow.getFromUserId()).postId(postId).createdAt(LocalDate.now()).build());
}
timeLineWriteService.bulkCreate(timeLines);
return postId;
}
게시물을 작성하는 경우, 게시물을 작성하는 userId를 팔로우하고 있는 time_line 테이블에 모든 사람들과 게시물을 하나의 컬럼으로 묶어준다.
TimeLine Entity
package com.example.fake_sns.timeline.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.LocalDate;
import java.util.Objects;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@NoArgsConstructor
public class TimeLine {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId; // 게시물의 주인을 팔로우한 유저 아이디
private Long postId;
private LocalDate createdAt;
@Builder
public TimeLine(Long id, Long userId, Long postId, LocalDate createdAt) {
this.id = id;
this.userId = Objects.requireNonNull(userId);
this.postId = Objects.requireNonNull(postId);
this.createdAt = createdAt == null ? LocalDate.now() : createdAt;
}
}
타임라인 테이블은 위 엔티티로 구성했다.
public List<Long> getPostIds(Long userId) {
// 타임라인에 존재하는 포스트 정보 가져와야한다.
return timeLineRepository.findAllByUserId(userId).stream().mapToLong(TimeLine::getPostId).boxed().collect(
Collectors.toList());
}
타임라인 테이블에서 userId 값을 기반으로 팔로잉한 게시물의 id를 모두 가져온다.
public List<PostDto> findAllLessThanByUserIdsAndOrderByIdDesc(List<Long> postIds, Long key, Long size) {
if(postIds.isEmpty()) {
return List.of();
}
String sql = String.format(
"""
select *
from %s
where id in (:postIds) and id < :id
order by id desc
limit :size
""", TABLE_NAME);
SqlParameterSource params = new MapSqlParameterSource()
.addValue("postIds", postIds)
.addValue("id", key)
.addValue("size", size);
return namedParameterJdbcTemplate.query(sql, params, postsRowMapper);
}
가져온 게시물 id를 in 조회로 한번에 가져온다.
두 방식 테스트하기
테스트 환경
- 유저수 : 2명
- 팔로우 : 1번 유저(id : 1)가 2번 유저(id : 2)를 팔로우 합니다.
- 게시물 : 2번 유저로 작성한 게시글이 200개 등록
포스트맨을 사용해서 응답 시간을 비교해보도록 하겠습니다.
FanOut Read방식의 응답 시간
응답 속도는 109ms가 걸렸습니다.
FanOut Write방식의 응답시간
응답속도는 43ms가 걸렸습니다.
Fan Out Time Write 방식이 데이터를 조회하는데 약 2배정도가 더 빠릅니다.
생각해보기
그렇다면, 무조건 Fan Out Time Write 방식이 정답인걸까?
그건 아니다. 예로 들어, 만약 A라는 사람이 팔로워가 1000만명이라고 가정할때, A라는 사람이 게시물을 작성하게 되면,
1000만명 모두가 A라는 사람의 게시글 작성시 타임라인에 즉시 추가되 DB input에 과부하가 옵니다.
Fan Out Time Read 방식은
A라는 사람이 1000만명을 팔로우하고 있을때, A라는 사람의 피드를 조회하면, 1000만명이 작성한 모든 게시글을 실시간으로 데이터를 조회하고 하게되어 조회 성능이 떨어질 수 있습니다.
결론
두 방식은 Trade-off관계이므로, 상황에 맞게 적절하게 사용을 하자!
'성능 개선' 카테고리의 다른 글
Cursor기반 페이지네이션으로 응답 속도 개선하기 (5) | 2024.10.10 |
---|---|
인덱스로 빠른 조회 만들기 (1) | 2024.10.08 |
[테스트 환경] 벌크 쿼리(Bulk Query) 사용하기 (0) | 2024.10.07 |