좋아요 기능을 개발하면서 고민한 내용 v2 (Test와 대규모 트래픽 환경에서의 고민...)

2025. 10. 3. 05:43·Project
728x90

이전 게시물에서 소셜미디어 플랫폼 내 피드 좋아요 기능을 개발했다.

지금까지 개발한 부분은 Redis 캐시를 활용해서 빠른 응답을 받도록 하고 있고, Scheduler를 통해서 DB 동기화를 배치 작업을 통해 진행하고 있다.

아래 링크에 지금까지 개발한 좋아요 기능을 대해서 정리해 두었다.

현재 개발 로직

 

문제 해결 및 성능 개선

소셜미디어플랫폼 프로젝트. Contribute to Hm-source/gridgestagram development by creating an account on GitHub.

github.com

현재 좋아요 기능을 정리한 시퀀스 다이어그램입니다. 

 

테스트

해당 기능을 로컬 컴퓨터에서 테스트하고자 한다.

테스트 환경

하드웨어

* CPU: Apple M4
  * 코어 : 10
  * 논리 프로세서: 10
* 메모리: 32GB (RAM)


소프트웨어

* OS: macOS Sequoia 15.6
* JVM
  * JDK: OpenJDK 17
  * GC: G1 GC
  * 힙 크기: 최소 8MB, 초기 512MB, 최대 8,192MB
* IDE: IntelliJ IDEA 2024.2.0.1

애플리케이션 환경

* Framework: Spring Boot 3.5.5 (내장 Tomcat)
* Tomcat thread
   * min-spare: 10
   * max: 200
* HikariCP maximum-pool-size: 10

데이터베이스 환경

* DB: MySQL 8.0
* Cache: Redis (in-memory)
* max-connections: 151

 

부하테스트는 Locust를 통해서 진행하였다.

테스트의 목적으로는 현재 로직이 제대로 돌아가는지와, 현 개발 상황에서 얼마큼의 부하를 견디는지를 확인하고자 하였다.

일단 jwt 기반 로그인이기 때문에 로그인 후 발급받은 토큰을 기반으로 feed에 대해서 동시 다발적으로 요청이 들어가는 것을 메인 로직으로 정했다.

이를 위해서 먼저 회원 1000명을 미리 회원가입을 진행하였다.

import requests

HOST = "http://localhost:8080"
PASSWORD = "password123!"

for i in range(1, 1001):
    username = f"user{i}"
    payload = {
        "username": username,
        "name": f"테스트유저{i}",
        "password": PASSWORD,
        "phone": f"010{i:08d}",  # 01000000001 ~ 01000010000
        "birthdate": "1999-01-01",
        "termsAgreement": {
            "agreements": [
                {"termsId": 1, "agreed": True},
                {"termsId": 2, "agreed": True},
                {"termsId": 3, "agreed": True}
            ]
        }
    }

    res = requests.post(f"{HOST}/api/auth/signup", json=payload)
    if res.status_code in (200, 201):
        print(f"[회원가입 성공] {username}")
    elif res.status_code == 400:
        print(f"[이미 존재] {username}")
    else:
        print(f"[실패] {username}, code={res.status_code}")

이후에 로그인, 좋아요 요청을 하는 테스트 파일을 만들어서 테스트를 진행하였다.

from locust import HttpUser, task, between
import random

class FeedLikeUser(HttpUser):
    wait_time = between(1, 3)

    def on_start(self):
        self.username = f"user{random.randint(1, 1000)}"
        self.password = "password123!"

        # 로그인
        login_resp = self.client.post("/api/auth/login", json={
            "username": self.username,
            "password": self.password
        })
        if login_resp.status_code == 200:
            self.token = login_resp.json().get("accessToken")
        else:
            self.token = None
            print(f"[로그인 실패] {self.username}, code={login_resp.status_code}")

    @task
    def toggle_like(self):
        if not self.token:
            return
        # feed_id = random.randint(1, 1000)  # 1~1000 피드 중 랜덤
        feed_id = 3
        headers = {"Authorization": f"Bearer {self.token}"}
        resp = self.client.post(f"/api/feeds/{feed_id}/likes", headers=headers)
        if resp.status_code != 200:
            print(f"[실패] {self.username}, code={resp.status_code}, body={resp.text}")
        else:
            print(f"[성공] {self.username} -> feed {feed_id}, resp={resp.json()}")
        

# 1. 100명 동시 유저
# locust -f locustfile.py --users 100 --spawn-rate 20 --host http://localhost:8080 --headless --run-time 1m --csv=like_test_100

# 2. 500명 동시 유저
# locust -f locustfile.py --users 500 --spawn-rate 50 --host http://localhost:8080 --headless --run-time 1m --csv=like_test_500

# 3. 1000명 동시 유저
# locust -f locustfile.py --users 1000 --spawn-rate 100 --host http://localhost:8080 --headless --run-time 1m --csv=like_test_1000

# 4. 2000명 동시 유저
# locust -f locustfile.py --users 2000 --spawn-rate 200 --host http://localhost:8080 --headless --run-time 1m --csv=like_test_2000

부하 테스트는 100명, 500명, 1000명... 유저를 늘리면서 진행하였다.

테스트 실행 명령어를 보면 다양한 옵션 정보들이 있다.

 

  • -f locustfile.py
    • 실행할 테스트 시나리오 파일 지정
  • --users 100
    • 가상 사용자(VU) 수: 100명
    • 동시 접속자 수
  • --spawn-rate 20
    • 초당 생성할 사용자 수: 20명/초
    • 100명 도달까지 5초 소요 (100 ÷ 20 = 5초)
  • --host http://localhost:8080
    • 테스트 대상 서버 주소
  • --headless
    • Web UI 없이 CLI 모드로 실행
    • 브라우저 없이 터미널에서만 결과 표시
  • --run-time 1m
    • 테스트 지속 시간: 1분
    • 100명 도달 후 1분간 테스트
  • --csv=like_test_100
    • 결과를 CSV 파일로 저장
    • 생성 파일:
      • like_test_100_stats.csv (통계)
      • like_test_100_stats_history.csv (시간별 기록)
      • like_test_100_failures.csv (실패 기록)

해당 명령어를 풀어서 설명하면 100명의 가상 유저가 20명/초 속도로 증가하여, 1분간 http://localhost:8080에 부하를 주는 테스트를 headless 모드로 실행하고, 결과를 CSV 파일로 저장한다는 뜻이다.

이때, 해당 컴퓨터의 CPU 상황(CPU 이용률, 메모리 사용)과 Redis 서버에 기록되는 queue 내 대기열 수, MySQL 서버에 기록된 좋아요 레코드도 같이 확인하고자 하였다.

아래 쉘을 통해서 3개의 로그 파일에 cpu, redis, mysql에 대한 로그를 기록하였다.

#!/bin/bash

# ========== 설정 ==========
PID=$(pgrep -f java)              # Spring Boot 서버 java PID
REDIS_KEY="like:sync:queue"       # Redis 큐 키
DB_USER=""                        # DB 사용자
DB_PASS=""                        # DB 비밀번호
DB_NAME="socialmedia"             # DB 이름
TABLE="feed_like"                 # 테이블명
INTERVAL=1                        # 몇 초 간격으로 수집할지

# 로그 파일
CPU_LOG="cpu_mem.log"
REDIS_LOG="redis_queue.log"
DB_LOG="db_likecount.log"
  
echo "Spring Boot PID: $PID"   
echo "로그 기록 시작... (CPU=$CPU_LOG, Redis=$REDIS_LOG, DB=$DB_LOG)"
echo "중지하려면 Ctrl+C"

# ========== 반복 수집 ==========

while true; do
  ts=$(date +"%Y-%m-%d %H:%M:%S")
  # CPU/MEM 기록
  usage=$(ps -p 35576 -o %cpu= -o rss=)
  echo "$ts $usage" >> $CPU_LOG
  
  # Redis 큐 길이 기록
  llen=$(docker exec redis redis-cli llen $REDIS_KEY 2>/dev/null || echo "N/A") 
  echo "$ts $llen" >> $REDIS_LOG

  # MySQL row count
  count=$(docker exec mysql  mysql -u$DB_USER -p$DB_PASS -D $DB_NAME -se "SELEC$
  echo "$ts $count" >> $DB_LOG
  
  sleep $INTERVAL

done

 

 

시각화

이렇게 얻은 결과를 시각화하여 살펴보았다.

먼저 locust를 통해 얻은 csv 파일을 기반으로 100, 500, 1000 일 때에 평균 응답 시간, 초당 요청 수, 실패 개수를 시각화하였다.

 

  • Average Response Time (ms)
    • 100 → 500 → 1000 유저로 늘어날수록 응답시간이 선형적으로 가 아니라 급격히 증가
    • 1000명에서 1000ms 이상으로 치솟음 → 서버가 부하를 감당하지 못한다는 의미
  • Requests per Second (RPS)
    • 사용자 증가에 따라 RPS는 증가하지만 속도 증가가 점점 둔화
    • 1000 유저 시점에서 RPS 상승보다 응답 지연/실패가 더 크게 나타남
  • Failures
    • 100/500명에서는 실패 없음
    • 1000명에서 실패 발생 → 시스템 임계치 도달

 

현재 로컬 서버에서는 500명까지는 안정적이지만 1000명부터는 응답 속도가 지연되고 에러가 발생하는 것을 알 수 있었다.

500명 보다 더 많은 사용자 요청이 발생할 경우에는 서버, DB 스케일 아웃 과정이 필요하다고 생각이 들었다.

 

두 번째로, 부하테스트를 하며 cpu, redis, mysql 로그 파일에 기록한 것을 시각화하였다.

 

locust 테스트를 진행하면서 이때의 CPU와 Redis Queue 내 개수, MySQL feed_like 테이블 내 레코드 개수를 시간을 x축으로 두고 확인하였다.

이게 100명, 500명, 1000명 요청했을 때의 변화를 나타내는 그래프이다.

첫 번째 그래프의 Spring Boot CPU/MEM를 먼저 보면, 요청 구간에서 400~600% 폭증 (M4 칩 멀티코어 기준 코어 여러 개 사용)을 확인할 수 있었다.

 


위 그래프만 보면 레디스 큐가 다 비워지지 않아 더 오래 동안 측정한 다음 그래프를 살펴보겠다.

이 그래프를 보면,

  • Redis (like:sync:queue)는 초반에는 값이 적지만, 테스트 진행 중 25,000 이상까지 급격히 증가한다.
  • 이때, 서버가 Redis 큐에 작업만 쌓고 100개씩 배치 처리로 소모되기 때문에 소모가 늦게 이루어진다.
  • 점차 감소하기 시작하는데, 이때 scheduler(배치 프로세스)가 처리하기 시작했음을 의미한다.
  • MySQL (feed_like count)도 초기에는 변화가 크지 않다.
  • 하지만, Redis 큐가 줄어드는 시점부터 MySQL count가 빠르게 증가하는 것을 볼 수 있다.
  • 이는 동기화를 통해서 DB 쪽에 반영된 것을 의미한다.

결론

이번 부하 테스트를 통해 현재 좋아요 기능의 성능 특성을 파악할 수 있었다.

안정 구간 (100~500명)

  • 평균 응답 시간 100ms 이하 유지하고, 실패 없이 안정적인 처리가 이루어진다.

병목 지점 (1000명 이상)

  • 응답 시간 1000ms 이상으로 급증하고, 요청 실패가 발생한다.

비동기 처리의 효과

  • Redis 큐를 통한 쓰기 부하 분산가 이루어지고 있다.
  • Scheduler 배치 작업을 통해 큐에 쌓인 것들을 점진적으로 DB에 동기화하고 있는 것을 확인할 수 있었다.
  • Redis를 통한 읽기 작업과 DB 쓰기 작업 분리로 사용자에게는 즉각적인 피드백을 제공할 수 있었다.

 

단일 서버 환경

  • 500명 이상의 사용자가 요청했을 때, CPU 사용률 600% 도달 (멀티코어 활용)하는 상황이 발생했고, 단일 인스턴스의 처리량 한계를 확인했다.

Redis 큐 처리 속도

  • 큐 적재 속도 > 배치 처리 속도를 확인했고, Scheduler 주기 및 배치 크기 최적화 필요하다고 생각이 들었다.

회고

이번 테스트를 통해 현재 시스템의 성능 한계와 개선 방향을 명확히 파악할 수 있었다. 특히 단순히 "잘 동작한다"를 넘어서 "얼마나 많은 부하를 견딜 수 있는가"를 수치로 확인할 수 있었던 점이 의미 있었다.

잘 된 점

Redis 캐시와 비동기 처리의 효과

Redis를 활용하여 읽기 작업에 빠른 응답을 제공했고, DB 부하를 효과적으로 감소시킬 수 있었다. 또한 쓰기 작업을 큐에 적재하는 비동기 방식으로 사용자에게 즉각적인 피드백을 제공하여 피크 시간대의 부하를 분산시키고 사용자 경험을 개선할 수 있었다.

Locust 기반의 부하 테스트

100명, 500명, 1000명으로 단계적으로 부하를 증가시키며 시스템의 임계점을 파악했다. CPU, Redis, MySQL을 동시에 모니터링하여 병목 지점을 명확하게 식별할 수 있었고, 500명까지는 안정적이지만 1000명부터 응답 지연과 실패가 발생한다는 것을 확인했다.

개선이 필요한 점

배치 처리 성능 최적화

Redis 큐가 25,000개까지 쌓이는 것을 확인했는데, 이는 배치 처리 속도가 요청 적재 속도를 따라가지 못한다는 것을 의미한다. Scheduler의 실행 주기와 배치 크기를 더 세밀하게 조정할 필요가 있다.

모니터링 시스템 구축

쉘 스크립트로 로그를 수집하고 수동으로 시각화하는 과정이 번거로웠다. Prometheus, Grafana와 같은 실시간 모니터링 대시보드를 도입하여 시스템 상태를 즉각적으로 파악할 수 있어야 한다.

캐시 전략 보완

현재 구현에서는 Look-Aside 패턴이 제대로 적용되지 않았다. Redis에 캐시가 없을 때 DB에서 조회하는 로직이 누락되어 있어, 캐시 미스 상황에서 적절한 fallback 처리가 이루어지지 않는다.

확장 가능한 아키텍처 필요

테스트를 진행하면서 현재 아키텍처가 대규모 트래픽을 처리하기에는 한계가 있다는 것을 깨달았다. 실제 대규모 서비스에서는 좋아요 기록(feed_likes)과 집계(feed_likes_count)를 테이블로 분리하고, DB 샤딩과 CDC(Debezium + Kafka) 기반 이벤트 아키텍처를 통해 확장성을 확보한다는 것을 알게 되었다. 이에 대해서는 다음 게시물에서 더 자세히 다뤄보겠다.

 

더 알아볼 것

캐시 전략 개선

  • Look-Aside 패턴 적용
  • Write-Behind 패턴

대규모 트래픽 대응 아키텍처

  • 테이블 분리 전략
  • CDC 기반 이벤트 처리
  • DB 샤딩 전략
  • Debezium + Kafka 구성
  • Redis 샤딩
  • 데이터 정합성 보장
728x90
반응형
저작자표시 비영리 변경금지 (새창열림)

'Project' 카테고리의 다른 글

OAuth 2.0을 활용한 로그인 구현을 하면서 (OAuth 2.0 동작, 인증/인가, Redis blackList...)  (0) 2025.10.03
좋아요 기능을 개발하면서 고민한 내용 (Redis, eventual consistency ...)  (0) 2025.09.20
📌 ERD 설계 가이드 (+ 직접 겪은 ERD 설계 경험)  (0) 2025.01.29
[LMS] 10일 동안의 '학습 관리 시스템' 개발 그리고 다른 팀과의 협업 이야기(1)  (0) 2025.01.27
[inter-face]프로젝트 기획 마무리 회고록 (1)  (1) 2024.12.03
'Project' 카테고리의 다른 글
  • OAuth 2.0을 활용한 로그인 구현을 하면서 (OAuth 2.0 동작, 인증/인가, Redis blackList...)
  • 좋아요 기능을 개발하면서 고민한 내용 (Redis, eventual consistency ...)
  • 📌 ERD 설계 가이드 (+ 직접 겪은 ERD 설계 경험)
  • [LMS] 10일 동안의 '학습 관리 시스템' 개발 그리고 다른 팀과의 협업 이야기(1)
pink_salt
pink_salt
유익함을 주는 개발자가 되도록 keep going
  • pink_salt
    KeepGoingForever
    pink_salt
  • 전체
    오늘
    어제
    • 분류 전체보기 (117)
      • Project (7)
      • WEB study (3)
        • WEB(Springboot) (10)
        • Git, GitLab (13)
        • Clean code (1)
        • FrontEnd (3)
      • Study (21)
        • Algorithm (19)
        • 면접 준비 (2)
      • Cloud Computing (2)
        • AWS (2)
      • 프로그래밍 언어 (35)
        • Java (29)
        • Python (0)
        • javascript (6)
      • 운영체제 (0)
        • Linux (0)
      • Database (4)
        • MongoDB (8)
        • SQL (8)
      • 애플리케이션 개발 (1)
        • Android (1)
      • AI (1)
        • Deeplearning (1)
        • machinelearning (0)
      • Daily (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    spring boot
    Query
    무료코딩교육
    빅오표기법
    git branch
    SW
    자바
    무료IT교육
    mysql
    Git
    Java
    대외활동
    gitlab
    codepresso
    개념
    오블완
    코딩이러닝
    코딩강의
    백준
    SWEA
    dp
    IT교육
    BFS
    Database
    MongoDB
    코드프레소
    언어
    티스토리챌린지
    객체지향
    python
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
pink_salt
좋아요 기능을 개발하면서 고민한 내용 v2 (Test와 대규모 트래픽 환경에서의 고민...)
상단으로

티스토리툴바