깃허브 주소
https://github.com/Rayakeem/study-ddia
GitHub - Rayakeem/study-ddia
Contribute to Rayakeem/study-ddia development by creating an account on GitHub.
github.com
데이터 중심 애플리케이션 설계 (DDIA) 스터디를 진행하며, 1장의 핵심 주제인 "부하(Load)에 따른 아키텍처 패턴"을 실제 코드로 구현해보았다. 단순한 DB 조회(Pull)에서 시작해, 성능 최적화를 위한 Fan-out(Push), 그리고 유명인 문제를 해결하기 위한 Hybrid 전략까지의 진화 과정을 기록한다.
Tech Stack
- Language: Java 17, Spring Boot 3.x
- Database: H2 (JPA/Hibernate)
- Cache: Redis (Docker)
- Test: JUnit5
1. 문제 정의: 왜 DB만으로는 안 되는가? (Pull Model)
초기 트위터처럼 유저가 피드를 조회할 때마다 SELECT * FROM tweets WHERE ... 쿼리를 날리는 방식은 Pull 모델이다.
- 장점: 구현이 매우 쉽다.
- 단점: 팔로우하는 사람이 많아질수록 쿼리가 무거워지고, 동시 접속자가 늘어나면 DB가 병목이 된다. (Read 부하)
이를 해결하기 위해 Redis를 활용한 Push 모델(Fan-out)을 도입했다.
2. 해결책 1: Push 모델 (Fan-out on Write)
글을 쓰는 시점에 팔로워들의 "우편함(Redis List)"에 미리 글 ID를 넣어주는 방식이다. 이렇게 하면 읽을 때는 Redis에서 꺼내기만 하면 되므로 속도가 획기적으로 빨라진다.
구현 코드 (FeedService.java)
// "내 글을 팔로워들의 우편함에 꽂아준다"
public void postTweet(Long userId, String content) {
// 1. DB에 원본 저장 (Single Source of Truth)
Tweet tweet = new Tweet(userId, content);
tweetRepository.save(tweet);
// 2. 나를 팔로우하는 사람 목록 조회
List<Follow> followers = followRepository.findByFolloweeId(userId);
// 3. Fan-out: 팔로워들의 타임라인(Redis)에 트윗 ID 배달 (Push)
for (Follow follow : followers) {
String key = "timeline:" + follow.getFollowerId(); // Key 생성 (예: timeline:200)
redisTemplate.opsForList().leftPush(key, tweet.getId()); // Value는 ID만 저장 (메모리 절약)
}
}
3. 한계점: "팔로워가 많은 유명 셀럽" 문제
하지만 Push 모델에도 치명적인 단점이 있다. 팔로워가 수천만 명인 초대형 인플루언서(VIP)가 글을 쓸 때다. 글 하나 썼는데 1,000만 번의 루프를 돌며 Redis에 데이터를 넣어야 한다. 시스템이 순간적으로 멈출 수 있다.
이를 해결하기 위해 Hybrid(혼합) 아키텍처를 적용했다.
4. 해결책 2: Hybrid 모델 (Push + Pull)
전략은 다음과 같다.
- 일반 유저: 기존처럼 Push(Fan-out) 방식을 사용한다. (빠른 읽기 속도 유지)
- VIP (연예인): Push를 하지 않고 DB에만 저장한다. (쓰기 부하 방지)
- 읽을 때: 일반인 친구 글(Redis)과 VIP 글(DB)을 합쳐서(Merge) 보여준다.
구현 코드 1: 쓰기 로직
private static final int CELEBRITY_THRESHOLD = 5; // (실습용) 팔로워 5명 이상이면 연예인 취급
public void postTweet(Long userId, String content) {
// ... DB 저장 로직 동일 ...
List<Follow> followers = followRepository.findByFolloweeId(userId);
// [Hybrid 핵심] 팔로워가 너무 많으면 Redis 배달을 생략한다! (Pull 대상이 됨)
if (followers.size() >= CELEBRITY_THRESHOLD) {
log.info(" 연예인(ID:{})입니다. Fan-out을 생략합니다.", userId);
return;
}
// 일반인인 경우에만 Fan-out 실행
for (Follow follow : followers) {
redisTemplate.opsForList().leftPush("timeline:" + follow.getFollowerId(), tweet.getId());
}
}
구현 코드 2: 읽기 로직 (Merge)
피드를 조회할 때 두 가지 소스(Redis + DB)를 가져와 메모리에서 합친다.
public List<Tweet> getFeed(Long fanId) {
// 1. [Redis] 일반인 친구들의 글 ID 가져오기 (Push 모델)
List<Object> redisResult = redisTemplate.opsForList().range("timeline:" + fanId, 0, -1);
// Redis의 Object 데이터를 Long으로 안전하게 변환
List<Long> redisTweetIds = new ArrayList<>();
if (redisResult != null) {
redisTweetIds = redisResult.stream()
.map(obj -> Long.valueOf(obj.toString()))
.toList();
}
// ID 목록으로 실제 내용 조회 (WHERE id IN (...))
List<Tweet> normalTweets = tweetRepository.findAllById(redisTweetIds);
// 2. [DB] 내가 팔로우한 연예인들의 글 가져오기 (Pull 모델)
List<Follow> followings = followRepository.findByFollowerId(fanId);
List<Long> vipIds = new ArrayList<>();
// 내가 팔로우한 사람 중 연예인만 골라냄
for (Follow follow : followings) {
if (followRepository.countByFolloweeId(follow.getFolloweeId()) >= CELEBRITY_THRESHOLD) {
vipIds.add(follow.getFolloweeId());
}
}
// 연예인 글 조회 (WHERE user_id IN (...))
List<Tweet> vipTweets = new ArrayList<>();
if (!vipIds.isEmpty()) {
vipTweets = tweetRepository.findAllByUserIdIn(vipIds);
}
// 3. [Merge] 합치기 및 최신순 정렬
List<Tweet> allTweets = new ArrayList<>();
allTweets.addAll(normalTweets);
allTweets.addAll(vipTweets);
// ID 역순(최신순) 정렬
allTweets.sort((t1, t2) -> t2.getId().compareTo(t1.getId()));
return allTweets;
}
💡 Retrospective (회고)
- Redis 데이터 타입: RedisTemplate<String, Object>를 사용할 때, 꺼내온 데이터가 Integer인지 Long인지 불확실하므로 toString() 후 파싱하는 과정이 필요했다.
- JPA의 In 쿼리: findAllById나 findAllByUserIdIn을 통해 N+1 문제를 피하고 한 번의 쿼리로 데이터를 가져오는 것이 성능의 핵심이었다.
- 트레이드오프: Hybrid 방식은 읽기 로직(getFeed)이 복잡해지는 단점이 있지만, 시스템 전체의 안정성을 위해 꼭 필요한 선택이었다.
DDIA 책의 이론을 실제로 코드로 짜보니, 왜 대규모 시스템들이 이런 복잡한 아키텍처를 선택하는지 피부로 느낄 수 있었다.
요약
- Push: 미리 배달해둔다. 읽기가 빠르다. (일반 유저용)
- Pull: 그때그때 찾는다. 쓰기가 빠르다. (VIP용)
- Hybrid: 둘을 섞어서 최적의 효율을 찾는다.
'Dev Book Review > 데이터 중심 애플리케이션 설계' 카테고리의 다른 글
| [DDIA Deep Dive] 트위터(X)는 어떻게 1억 명의 타임라인을 만들까? - 이론편 (0) | 2025.12.17 |
|---|