목차
1. 기존 방식 및 문제점
1.1 기존 친구 추천 API 방식
1.2 기존 방식의 문제점
1.3 실시간 조회를 구축하기 위해 반드시 해결해야 할 것
2. 결과 요약
2.1 원인
2.2 성능 지표
2.3 성과 요약
3. 설계 목표와 고민
3.1 설계 목표
3.2 고민과 대안
4. 실시간 친구 추천 API 아키텍처
5. 테스트
5.1 테스트 결과
5.2 친구가 많고 많은 추천친구를 보여줘야하는 상황의 테스트 결과
5.3 레디스 장애시 DB조회 테스트 결과
6. 정합성
1. 기존 방식 및 문제점
1.1 기존 친구 추천 API 방식
아래는 기존 친구 추천 API의 아키텍처입니다.

1시간마다 전체 유저를 대상으로 기준에 따라 친구 점수를 계산한 뒤, 상위 10명을 추천 친구 테이블에 저장하는 배치 방식이었습니다.
아래는 점수 기준 입니다.
| 2촌 | 3촌 | 글 추천 | 댓글 추천 | 댓글 작성 | 공통 2촌 추가 점수 | 공통 3촌 추가 점수 | |
| 점수 | 50점 | 20점 | 0.5점 | 0.5점 | 0.5점 | 2점 | 0.5점 |
| 점수 제한 | 최대 50점 | 최대 20점 | 최대 10점 | 최대 20점 |
최대 5점 | ||
1.2 기존 방식의 문제점
실시간성 불가
가장 심각한건 실시간의 부재 입니다. 제 서비스는 SNS이며 추천친구기능은 사람들의 상호작용을 이끌어내는 중요한 로직입니다.
예를들어 나 - 친구A - 친구B - 친구C 에서 나와 친구C는 3촌관계 입니다. 그런데 저와 친구B가 친구가 되면 친구C는 3촌에서 2촌이 됩니다. 하지만 여전히 추천친구 테이블에 친구C는 3촌으로 저장되어 있습니다. SNS의 특성상 이런 지연은 서비스에 치명적입니다.
친구관계 변동 시 전체 재계산 부담
아래 표의 점수 기준 중 촌 수와 공통 친구는 친구 관계 변동 시 다른 사용자의 점수에도 영향을 줍니다. 실시간 전환을 위해서는 변동이 발생할 때마다 테이블을 최신화해야 하지만, 영향 범위를 특정할 수 없어 전체 유저를 대상으로 재계산해야 했습니다. 유저 1,000명 기준으로 40초가 소요되어 매 변동마다 최신화하는 것은 현실적으로 불가능했습니다.
회원 증가 시 배치 로직 소요시간 증가
유저 1,000명 기준으로 배치 로직에는 약 40초가 소요됩니다. 만약 미래에 회원이 1만명으로 증가하면 더 많은 시간이 소모될 것 입니다.
1.3 실시간으로 전환하기 위해 반드시 해결해야 할 것
실시간으로 전환하기 위한 가장 큰 문제는 친구 관계 변동 시 다른 유저의 친구 점수도 달라진다는 것 입니다.
예를들어 나 - A - B - C 로 이뤄지는 관계에서 내가 B와 친구가 되면 B는 2촌에서 1촌(친구)로 C는 3촌에서 2촌이 됩니다. 많은 친구들이 있고 친구들끼리 연결되어있다면 어디까지 영향을 입었는 지 파장을 가늠할 수 없습니다.
한마디로 관계가 바뀌었을 때 친구 네트워크가 동적으로 바뀌기 때문에 친구 관계가 바뀔 때 마다 모든 회원을 상대로 추천 친구 테이블을 업데이트 해야합니다.
2. 결과 요약
2.1 원인
친구 관계 변동 시 다른 유저의 친구 점수도 달라진다는 것에 타격을 입은 이유는 다른 사용자의 점수를 달라지게 할 수 있는 점수까지 저장했기 때문입니다.
친구 관계 변동시 다른 유저의 점수에 영향을 끼치는가에 따라 데이터를 분리했습니다.
그리고 다른 유저의 점수에 영향을 미치지 않는 데이터는 정적, 그렇지 않은 데이터는 동적으로 분류했습니다.
정적인 데이터는 저장하고 동적인 데이터는 조회 시 애플리케이션에서 계산하는 방법으로 이 문제를 극복했습니다
2.2 성능 지표
| 테스트 (회원 1000명, 회원당 친구 15명) 10회 연속 실행 | 평균 응답 시간 | 배치 저장 시간 |
| 기존 추천 친구 조회 | 49.6ms | 37.85초 |
| 실시간 추천 친구 조회 | 40.5ms | 0초 |
| 테스트 (회원 1000명, 회원당 친구 300명) 100회 연속 실행 | 평균 응답 시간 | 레디스 CPU 사용률 |
| 실시간 추천 친구 조회 상위 10명 조회 | 43.3ms | 0.03% |
| 실시간 추천 친구 조회 상위 50명 조회 | 44.7ms | 0.03% |
| DB 폴백 테스트 (회원 1000명, 회원당 친구 300명) 100회 연속 실행 | 평균 응답 시간 |
| 실시간 추천 친구 조회 상위 10명 조회 | 26.5ms |
| 실시간 추천 친구 조회 상위 50명 조회 | 28.8ms |
| 구분 | 데이터 |
| 동적이자 연쇄 작용에 영향받는 데이터 | 촌 수, 공통 친구 |
| 비교적 정적이자 연쇄 작용의 근원인 데이터 | 친구관계 |
| 정적인 데이터이자 미리 산정할 수 있는 점수 | 상호관계 점수 |
2.3 성과 요약
1. 실시간 : 데이터의 분리로 어떠한 변화가 다른 유저에게 영향을 주는 것을 제어했습니다.
2. 개발 생산성 저하 방지 : 리소스를 추가하지 않아 개발 생산성 하락을 방지했습니다.
3. 미래 지향적 시스템 : 어떤 회원의 친구가 수만 명이거나 추천 친구를 10명이 아닌 50명을 반환해야하는 상황에서도 응답시간에 차이가없습니다.
4. 서버 리소스 절약 : 배치 로직의 삭제로 배치 로직의 오버헤드로 인한 자원 소모 가능성이 제거되었습니다.
3. 설계 목표와 고민
3.1 설계 목표
회원이 35명인 제 서비스는 배치로직을 돌리지않고 그냥 DB에서 즉석으로 조회해도 아무 상관없는 트래픽입니다.
그러나 미래를 대비하여 최소 1000명의 회원에서 원할하게 작동하는 시스템을 구축하고 싶습니다.
| 분야 | 목표 |
| 실시간 | 1시간 단위 최신화에서 실시간으로 변경되어야합니다. |
| 응답속도 | 인간이 느림을 인지할 수 없는 100ms 이하의 속도여야 합니다. |
| 안정성 | 데이터를 복구할 기반 데이터가 있어야하고 정확한 응답을 해야합니다. |
| 개발 생산성 | 지나치게 복잡한 로직, 관리해야할 요소 증가로 개발 생산성이 저하되면 안됩니다. |
| 비용 | 스케일 아웃이나 스케일 업, DB의 추가 증설등 서버비가 증가하면 안됩니다. . |
| 서버 리소스 | 메모리와 CPU등의 서버 리소스 소모가 적어야합니다. |
3.2 고민과 대안
처음에는 배치로직을 좀 더 빠르게 하는 방법을 생각했습니다. 그러다가 중간에 데이터의 분리라는 깨달음을 얻었습니다
아래는 고민한 아키텍처들을 실시간으로 전환했을 때 위에 제시한 목표를 달성할 수 있는지 요약한 표 입니다.
| 방법 | 응답 속도 | 안정성 | 개발 생산성 | 비용 | 서버 리소스 |
| 기존 방식 | 빠름 | 높음 | 높음 | 없음 | 매우 높음 |
| 친구 점수 재계산 배치 쿼리 최적화 | 빠름 | 높음 | 높음 | 없음 | 높음 |
| MongoDB | 빠름 | 높음 | 낮음 | 증가 | 높음 |
| Redis에 친구 데이터 저장 | 빠름 | 낮음 | 보통 | 없음 | 높음 |
| Redis에 미리 모든 점수를 삽입 | 매우 빠름 | 높음 | 보통 | 없음 | 보통 |
| 파티셔닝 + 분할업데이트 | 빠름 | 낮음 | 낮음 | 없음 | 보통 |
| 데이터 분리 | 매우 빠름 | 높음 | 높음 | 없음 | 매우 낮음 |
친구 점수 재계산 배치 쿼리 최적화
친구 점수를 재 계산하는 쿼리가 오래 걸리기 때문에 친구 관계에 변동이 발생할 때마다 테이블을 수정하기 어렵습니다. 따라서 쿼리를 최적화하는 방법을 고려했습니다.
쿼리를 최적화하더라도, 유저 수가 증가하면 글, 글·추천, 댓글, 댓글 추천, 친구, 유저 테이블의 친구점수의 재료가 되는 레코드가 함께 늘어나고, 친구관계 변동도 잦아집니다. 변동이 발생할 때마다 추천친구 테이블을 갱신하는 방식은 쿼리 성능의 문제가 아니라, 데이터 증가에 구조적으로 취약한 설계의 문제라고 판단했습니다.
MongoDB 도입
가장 먼저 생각난 것은 Nosql이었습니다. 동적인 친구 관계를 저장하기엔 Rdb보다 적합하다고 생각했습니다.
그러나 제 서비스의 수많은 로직 중 배치로직 하나를 위해 DB를 하나 더 도입한다는 건 Trade-off가 크다고 생각합니다. DB를 더 도입하는 것은 이후의 개발 생산성을 저하시킨다고 생각합니다. 그리고 친구관계 변화시 변화의 파장을 가늠하지 못하여 전체 유저에 대한 배치 로직도 여전히 존재합니다. 조금 빨라질 수는 있겠지만 근본 원인을 해결하지 못합니다.
Redis에 친구 데이터 저장
그렇게 고민중인 찰나, 이미 Redis를 쓰고 있다는 것을 떠올렸습니다. Redis도 Nosql기반이고 굳이 MongoDB를 도입할 필요없이 기존의 Redis를 쓰자고 생각했습니다.
하지만 Redis의 치명적인 단점은 메모리기반이라 영속성이 없다는 것입니다. 물론 AOF와 RDB를 통해 저장과 복구가 가능하지만 레디스에만 정보를 저장해서는 안됩니다. 또한 MongDB와 동일하게 근본 원인을 해결하지 못합니다.
모든 친구 점수 사전계산
만약 Redis ZSET에 모든 멤버에 대한 각각의 관계에 대한 점수를 미리넣어둔다면? 그렇게 모든 연결 점에서 이벤트 발행시 점수가 증가하고 추천 멤버를 조회했을 때 상위권만 반환한다면?
즉 관계의 연쇄 작용을 방지하기 위해 미리 모든 관계를 펼쳐놓는 방법입니다. 이 방법은 실시간 조회가 가능하고 조회도 빠릅니다. 하지만 멤버가 1000명이면 그 모든관계는 제곱인 100만개입니다. 제곱이 아니더라도 회원이 10만명이라면 각 회원당 친구가 천명이라면 1억개 입니다. 그리고 멤버가 늘어날수록 기하급수적으로 레코드와 메모리 사용량이 증가합니다.
회원 1만 명 기준으로 전체 데이터는 1억 개에 달하며, skiplist 기준 약 10GB, ziplist 기준 약 3GB가 필요합니다. ziplist는 대부분의 명령이 O(N)이므로 사실상 사용할 수 없어 skiplist를 써야 하는데, 회원이 10배 늘어날 때마다 메모리는 100배씩 증가합니다.
또한 조회는 빠르지만 수정 삭제 삽입시 친구 그래프를 탐색하며 모든 점수를 변환해야해서 서버 리소스의 부담이 증가할 것입니다.
Mysql 파티셔닝 + 분할 업데이트
학교의 한 반에서도 친구 무리가 있는 것에 착안하여. 친구 네트워크를 몇 개의 거대 네트워크로 분할하여 각각의 파티션에 두고 파티션 별로 조회용 테이블에 분할 업데이트하는걸 생각했습니다.
이렇게 하면 각 파티션 안에 있는 회원들로 친구 관계의 파장을 계산할 수 있어 배치 쿼리가 빨라질 것입니다.
하지만 거대 네트워크들의 특정 노드들의 일부는 다른 네트워크와 이어지고 그렇게 되면 하나의 파티션을 뛰어넘게 됩니다. 따라서 모든 회원을 상대로 정확한 계산이 되지 않기 때문에 예전 결과를 반환할 수 있습니다.
그리고 네트워크에서 거대 네트워크들은 어떻게 식별할건지? 식별한다고해도 분할의 범위를 어떻게 잡을 것인지? 신경 쓸 것이 많고 실시간 추천친구라는 로직에 비해 매우 복잡한 설계입니다.
깨달음과 데이터 분리
그때 중요한 것을 깨달았습니다. Rdb든 Nosql이든 각자의 자료구조로 디스크든, 메모리든 어딘가에 저장하는 데이터베이스입니다. 결국엔 정적인 데이터처리에 가장 유리합니다. Nosql이 유연하긴해도 동적으로 변화하는 친구 네트워크를 즉각적으로 표현하기 어렵습니다.
친구 점수의 촌 수, 함께 아는 친구는 친구관계의 변경에 따라 다른 사용자의 점수도 달라지게 할 수 있습니다.
친구 점수를 전체를 저장하면, 친구관계 변동시 다른 유저에게 영향을 주는 데이터까지 함께 저장하는 셈입니다. 친구관계 변동 시 다른 유저에게 영향을 주는 데이터와 주지 않는 데이터로 분리하여 저장해야 합니다.
동적과 정적을 구분하면 아래와 같은 표가 나옵니다.
| 구분 | 데이터 |
| 친구 관계 변동시 다른 유저에게 영향이 가는 데이터 (동적) | 촌 수, 함께 아는 친구 수 |
| 친구 관계 변동시 다른 유저에게 영향이 가지 않는 데이터 (정적) | 상호작용 점수 |
그렇다면 정적인 것은 DB에 저장하고 동적인 것은 애플리케이션 로직에서 즉각처리하면 안될까요?
동적인 것을 애플리케이션에서 처리하기 때문에 실시간성이 확보되고 DB에 동적 데이터를 저장하지 않기 때문에 친구 관계의 변화 시에 전체 유저를 재계산하는 일이 필요 없습니다.
중요한 것은 미리 계산 가능한 부분은 미리 계산해서 정적으로 가지고 있고 미리 계산이 불가능한 부분인 동적인 부분만 애플리케이션에서 계산하여 실시간성을 갖추고 조회 시의 부하도 줄이는 것 입니다.
결국 Rdb든 Nosql이든 친구 관계 네트워크 전체를 저장할 필요가 없었습니다. 누군가가 하늘에서 친구관계 연결그래프를 전체적으로 보고 있는 것이 아니기 때문입니다.
4. 실시간 친구 추천 API 아키텍처
아래의 그림은 실시간 아키텍처를 간략화한 그림입니다.

지금까지의 결론은
1. 정적인 요소만 DB에 저장할 것, 사전에 정적으로 만들 수 있는 데이터는 가능한 미리 정적으로 만들 것
2. 애플리케이션에서 동적 요소의 계산을 빠르게 하기 위해 계산에 필요한 데이터를 빠르게 가져올 것
3. 레디스에서 데이터가 사라져도 복구할 수 있는 데이터가 존재할 것
1번에 따르면 글, 댓글, 추천등 각 테이블을 조회하는 상호작용 점수는 미리 점수로 산정이 되어있어야합니다.
2번에 따르면 조회시 속도를 빠르게 하기 위해 DB보다 Redis를 통해 정적 요소를 가져오는 것이 가장 좋은 방법입니다.
3번에 따르면 RDB에 친구관계 데이터를 두고 Redis에도 친구관계 데이터를 두어야 합니다. 중복처럼 보이지만 정합성을 지킬 수 있는 좋은 방법입니다. SNS 서비스에서 친구관계 데이터가 사라지면 그것은 곧 서비스의 종료를 뜻하기 때문입니다.
응답속도를 빠르게 하려면 추천 친구를 계산하는 로직을 최소화해야합니다. 이는 1번으로 미리 계산할 수 있는 것은 미리 만들어 놓는 것과 같은 뜻 입니다. 만약 Redis에서 데이터를 가져와서 추천 친구를 계산할 때 일정 부분 이미 완료가 되어있다면? 즉, 사전계산 입니다.
친구 네트워크의 변동성은 친구의 연결, 해제에서 나옵니다. 즉, 데이터 변경 시 다른 유저에 영향을 주지 않는 데이터들은 충분히 미리 저장할 수 있습니다. 제 친구 점수 기준의 상호작용 점수(글, 댓글 추천, 댓글 작성 등)은 다른 유저에 영향을 주지 않습니다. 한마디로 이 상호작용 점수에 필요한 요소들은 충분히 정적인 성격의 자료들이고 미리 계산할 수 있습니다
그렇다면 상호작용 점수는 상호작용이 일어날 때 마다 이벤트를 발행하여 Redis에 미리 계산해두면 됩니다. 혹여나 데이터가 사라져도 RDBMS조회를 통해 복구가 가능합니다.
상호작용 점수 (zset)
상호작용 점수는 상호간에 댓글 작성, 글 추천, 댓글 추천에서 이벤트로 실시간으로 레디스에 점수를 올린다.
이때 회원1의 회원2의 값과 회원2의 회원1의 값은 같습니다.
```
회원:1
회원2 = 10
회원3 = 5
회원4 = 8
회원:2
회원1 = 10
회원3 = 7
회원4 = 3
```
이렇게하면 상호작용 점수가 미리 완성이 되어 조회 때마다 쿼리를 조회하는 것이 아닌 단 하나의 레디스 요청으로 완성된 상호작용 점수를 가져올 수 있습니다. 친구 데이터를 가져와 촌 수와 공통친구 점수를 계산하고 상호작용점수에 더하기만 하면 끝입니다. 점수를 계산하기 위한 DB 조회 필요가 사라진 것입니다.
그렇다면 이제 촌수를 계산하고 공통친구를 계산해야합니다.
친구관계 그래프 (set)
```
friend:1 → [2,3,4]
friend:2 → [1,5,7]
friend:3 → [1,9,10]
```
Redis의 친구관계 그래프는 Set을 활용하여 중복 친구가 등록되지 않게 하였습니다. 또한 친구 추가 삭제 요청에서 비동기 이벤트 리스너로 레디스에 반영됩니다. 이 때 촌 수는 친구의 친구를 조회하면서 자연스레 계산됩니다. 공통친구는 미리 산출된 적이 있는 추천친구 후보자가 또다시 등록되는 횟수로 공통친구 횟수를 산출 할 수 있습니다. 친구관계 그래프도 유실된 경우에 얼마든지 Rdb에서 복구가 가능합니다.
5. 테스트
5.1 테스트 결과
| 테스트 (회원 1000명, 회원당 친구 15명) 10회 연속 실행 | 평균 응답 시간 | 배치 저장 시간 |
| 기존 추천 친구 조회 | 49.6ms | 37.85초 |
| 실시간 추천 친구 조회 | 40.5ms | 0초 |
기존 방식의 배치 저장이 약 37.85초가 걸렸는데 1시간 기준으로 약 1.05%의 시간이 배치로 돌아갑니다. 배치를 조금 더 빠르게 하는 것은 불가능할 점유율입니다.
평균 응답 시간도 감소했고 37.85초의 배치 시간이 완전히 사라졌습니다.
5.2 친구가 많고 많은 추천친구를 보여줘야하는 상황의 테스트 결과
| 테스트 (회원 1000명, 회원당 친구 300명) 100회 연속 실행 | 평균 응답 시간 | 레디스 CPU 사용률 |
| 실시간 추천 친구 조회 상위 10명 조회 | 43.3ms | 0.03% |
| 실시간 추천 친구 조회 상위 50명 조회 | 44.7ms | 0.03% |


도커로 실제 운영환경과 비슷하게 조정하여 레디스의 CPU를 1로 메모리를 100MB로 설정 후 각 회원당 친구가 300명인 상황을 100회 연속 테스트 하였고. 상위 50명으로 늘려서 조회도 해보았습니다. 평균 응답시간은 얼마나 조회하는지에 전혀 영향을 받지 않고 레디스 CPU 사용률도 크지 않습니다. 미래에 요구사항이 어떻게 변동이되어도 설계를 변경할 필요가 없게 되었습니다.
5.3 레디스 장애시 DB조회 테스트 결과
| DB 폴백 테스트 (회원 1000명, 회원당 친구 300명) 100회 연속 실행 | 평균 응답 시간 |
| 실시간 추천 친구 조회 상위 10명 조회 | 26.5ms |
| 실시간 추천 친구 조회 상위 50명 조회 | 28.8ms |


DB조회가 더 빠른건 DB조회시에는 상호작용 점수를 계산하지 않기 때문입니다. 추천친구에서 상호작용 점수의 비중은 10점으로 낮습니다. 촌 수로 어느정도 순위를 정해놓고 상세한 부분을 상호작용 점수로 가르는 느낌입니다. DB에서 상호작용 점수는 긴 시간이 듭니다. 개선 전 배치에서 가장 많은 시간을 담당한 것도 상호작용 계산이었습니다. 레디스 사용시에는 바로 가져올 수 있지만 DB를 이용하려면 글, 댓글, 글 추천의 테이블과 복잡한 쿼리를 맺어야합니다. 점수의 비중과 시간을 고려했을 때 정확도를 우선해도 점수의 비중이 낮아 큰 이점이 없기에 응답속도를 우선으로 생각했습니다.
6. 정합성
이벤트 기반으로 친구관계의 Redis와 RDBMS의 정합성을 맞추는데 네트워크 오류등으로 Redis가 반영하지 못하면 정합성이 뒤틀릴 수 있습니다. 따라서 DLQ를 구성하였습니다.
| 구분 | 데이터 양 | 스레드 수 | 소요시간 |
| DLQ 유실 데이터 복구 | 10만 레코드 | 1 | 10초 |
10만개의 이벤트를 처리하는데 10초가 걸렸습니다.
또한 Redis의 친구관계가 소실될 수 있습니다. 블로킹 큐를 사용하여 RDBMS로 부터 복구하는 로직을 만들었습니다.
| 구분 | 데이터 양 | 스레드 수 | 소요시간 |
| 친구 관계 복구 | 1500만 레코드 | 생산자 : 1, 소비자 : 1 | 39초 |
| 상호작용 점수 복구 | 3000만 레코드 | 생산자 : 5, 소비자 : 1 | 2분 33초 |
10만명의 회원이 각각 300명의 친구를 가지고 각각 300명과 상호작용을 했을 때 총 복구 시간은 3분 12초가 소요되었습니다.
'트러블슈팅과 고민 > 트러블슈팅' 카테고리의 다른 글
| 게시판 조회 쿼리 개선 (1) | 2025.12.31 |
|---|---|
| 작성자 검색 인덱스 사용 불가 문제 해결 (0) | 2025.11.25 |
| 분산락에서 비동기 스레드풀 실행 거부 예외 식별과 해결 (0) | 2025.11.19 |
| Redis TTL 만료 시점 병목 해소: Cache Stampede 방어 전략 측정 및 분석 (0) | 2025.11.03 |
| 하이버네이트 커넥션 해제 로직의 커넥션 누수 결함 개선 (0) | 2025.10.19 |