트러블슈팅과 고민

롤링페이퍼 유저 상호작용 부하 테스트 및 성능 개선

정재익 2025. 8. 7. 17:28

롤링페이퍼 유저 상호작용 부하 테스트 및 성능 개선 종합 리포트

 

목차

1. 서론

    1.1 테스트 목적

    1.2 핵심 성과 요약 및 개선 범위

 

2. 부하 테스트 수행 및 문제점 분석

    2.1 테스트 환경 및 시나리오

    2.2 테스트 결과 요약

    2.3 문제점 분석 (우선 순위 별)

        2.3.1 우선순위 상: Connection Refused (병목 지점)

        2.3.2 우선순위 중: CPU 사용량 급상승의 원인

        2.3.3 우선순위 하: GC 스파이크, N+1 문제 및 불 필요 컬럼 조회, 동기화 이슈

 

3. 성능 개선 구현 및 검증

    3.1 우선순위 상 개선: Connection Refused 해결

    3.2 우선순위 중 개선: DB 커넥션 최적화

    3.2 우선순위 하 개선: GC, 쿼리, 비동기 처리 최적화

 

4. 성능 개선 효과 검증

    4.1 개선 전후 성능 비교

    4.2 목표 달성도 평가

 

5. 결론 및 향후 과제

    5.1 개선 성과 종합

    5.2 롤링페이퍼 시나리오 향후 개선 방향

    5.3 1000명 이상의 동시 사용자 대비책

 

1. 서론

1.1 테스트 목적

서비스의 핵심 기능인 익명 메시지를 남기고 메시지를 확인하는 시나리오를 가정하여 운영 환경의 성능 한계점을 식별하고 인프라 및 애플리케이션 전반의 문제점을 찾아 성능 개선 방안을 도출합니다.

- 동시 접속자 500명 환경에서의 시스템 안정성을 확인하고 그 이상의 인원이 활동할 시에 일어나는 상황을 대비

- 각 API별 성능 임계점 및 병목지점 식별

- 운영 환경 배포 전 성능 최적화 방향 수립

1.2 핵심 성과 요약 및 개선 범위

지표 개선 전 개선 후 배율
최대 동시 사용자 416명 1000명 2.4배 증가
최대 TPS 193 306 1.6배 증가
평균 TPS 89 205 2.3배 증가
성공한 HTTP 요청 65,000건 122,000건 1.9배 증가
평균 응답시간 1.2초 0.35초 3.4배 감소
오류율 50% 0.1% 오류 제거

개선 범위 : 우분투 설정, DB 설정, DBCP 설정, Tomcat 설정, 애플리케이션 로직 최적화, 쿼리 최적화

 

2. 부하 테스트 수행 및 문제점 분석

2.1 테스트 환경 및 시나리오

테스트 환경 

  - 테스트 대상 서버 : 실제 운영 예정인 백엔드 서버 AWS EC2 t2.micro (ubuntu)

    - 파일 디스크립터 : 1024개

    - 메모리 : 950MB

    - 스왑 메모리 2GB

- WAS  : Tomcat

    - 스레드 풀 최소 스레드 수: 10개

    - 스레드 풀 최대 스레드 수 : 1000개

- 데이터베이스 : AWS RDS db.t4g.micro (Mysql)

    - max_connection : 13개

- Load Generator : AWS RDS db.t4g.micro (Mysql)

    - Monitoring : Scouter APM

테스트 시나리오 

  - 테스트 방식 : 시나리오 기반 점진적 부하 테스트

  - 부하 증가 패턴 : 1명에서 1000명까지 동시 접속자 증가

  - 사용자 행동 패턴 : 2초마다 메시지 작성 및 롤링페이퍼 조회 반복

2.2 테스트 결과 요약

- 최대 TPS : 193

- HTTP 요청 합계 : 130,000

- 오류 발생 시점 : 동시 사용자 416명

- 오류율 : 50%

- 테스트 종료 시점 : 8분 30초 후 오류율 50% 도달로 자동 종료

- 안정적인 메모리 사용량 : 테스트 중 메모리 사용량 10% 증가

- 안정적인 CPU 사용량: 테스트 중 CPU 사용량 약 50% 유지

2.3 문제점 분석 (우선 순위 별)

- 병목 또는 문제가 있는 부분이 다수 있었고 테스트의 목표인 성능 개선 가능성을 지표로 삼아 우선순위 상, 중, 하로 구분하였습니다.

- 우선순위 상은 병목 지점입니다.

- 우선순위 중은 나머지 문제점 중에서 가장 급하게 해결할 과제이고 성능에 치명적인 요소입니다.

- 우선순위 하는 성능에 치명적이지 않지만 개선하면 성능 향상을 기대할 수 있는 요소입니다. 

- 로직이 비효율적 이지만 성능에 크게 영향을 미치지 않는 요소는 개선을 해도 성능 향상을 기대할 수 없다고 판단하여 제외하였습니다.

- 또한 디스크(EC2 볼륨), RDS, 네트워크 등 문제가 없는 지표는 다루지 않았습니다. 

 

문제점 비즈니스 임팩트 우선순위 예상 해결 비용
Connection Refused (병목 지점) 서비스 중단 위험 높음 낮음
DB 커넥션 부족 현상 데이터 처리 지연 중간
부하 초반 GC 800ms 스파이크 사용자 경험 저하 중간
N+1 쿼리 문제 응답 성능 저하 낮음
메시지 남기기와 알림 처리 로직의 동기화 응답 성능 저하 낮음



2.3.1 우선순위 상 : Connection Refused (병목 지점)

1. 문제 현상

- Connection Refused는 Tomcat 연결 실패와 OS레벨 연결 실패의 2가지의 이유로 나타날 수 있습니다. 때문에 어떤 연결이 실패한 것인지 식별하는 것이 중요합니다.

- OS레벨의 원인인 경우는 TCP소켓이 부족할 때, 대기 큐에서 제가 설정한 시간(5초)가 만료됐을 때 연결 거부 처리가 될 수 있습니다.

- Tomcat이 원인인 경우는 서버가 실행되고 있지 않거나 해당 포트에서 리스닝 하지 않을 때 또는 많은 요청에 스레드가 고갈되어 OOM등 예외가 발생했을 때도 TCP레벨에서 거부될 수 있습니다.

- 제 서비스는 Tomcat은 실행되고 있었고 헬스체크도 정상이었습니다. 그럼 메모리를 확인하여 원인이 Tomcat에 있는지 알아보아야 합니다.

 

2. 원인 분석

메모리

항목 변화량 해석
Memory 사용률 72% → 82% → 72% 근처로 하락 일시적으로 메모리 압박 발생 후 회복
Available Memory 240MB → 170MB → 200MB RAM 가용량 감소 → 천천히 회복
ActualUsed 700MB → 790MB → 760MB 실제 사용 메모리 증가 후 점진적 하락

메모리는 테스트 동안 약 10%의 변동이 있었습니다. 스왑 메모리도 테스트동안 당겨쓰지 않았으며 메모리에 부하는 없다고 볼 수 있습니다.

 

힙 메모리 지표

GC가 일어날 때마다 힙 메모리가 줄고 있는 것을 보아 안정된 GC/Heap 패턴이라고 판단됩니다. 즉 GC자체에 시스템 부하나 메모리 회수에 문제가 없다는 것을 알 수 있습니다.

우분투 서버 설정과 첫 오류 발생 지점 분석
현재 파일 디스크립터 1024, somaxconn 4096, backlog 128로 설정되어 있습니다.
첫 오류 발생 지점은 사용자 416명 지점 입니다. 해당 지점의 테스트 수는 527개, HTTP요청은 1054개 입니다. 이 때 480개의 요청은 연결 거부 되었습니다.
이전 지점 HTTP요청의 최대 응답 수치가 2초가 넘는다는 것에 1024개의 소켓이 전부 유휴상태가 아니었을 것이고 파일 디스크립터를 초과하는 요청이 들어온 시점에서 OS레벨의 연결 실패라고 판단할 수 있습니다.

3. 원인 확정

syn 수신 시점의 연결 요청을 대기 시키는 backlog의 수치가 낮아 대부분의 요청이 대기 큐에 들어가지 못하고 거부되었을 것이고 그 뒤편에 있는 somaxconn은 여유로운 상태였을 것으로 예상합니다. 소켓 수 뿐만 아니라 대기 큐에도 병목이 있는 겁니다.

 

4. 비즈니스 임팩트 : 서비스 중단 위험 높음

2.3.2 우선순위 중: CPU 사용량 급상승의 원인

1. 문제 현상

구분 사용률 특징
CPU 10% → 100% → 40% 이하로 하락 user와 sys를 합친 전체 CPU 사용률입니다.
CPU user 7% → 90% 급등 → 점차 하락 (최저 10%대) JVM 내부에서 객체를 만들고, 스레드를 생성 하거나, DB 커넥션 생성시에 상승합니다.
CPU sys 약 3% → 평균 10~15% 유지 Linux 시스템의 CPU 사용률 입니다. 전반적으로 낮고 안정적입니다.

테스트 초반 CPU 사용률이 100% 까지 급상승했는데 대부분의 영향이 CPU user의 영향이었습니다.

2. 원인 분석

DB 커넥션 생성 지표



총 78회의 생성이 있었으며 대부분 테스트 초반에 발생했습니다.
테스트의 부하는 선형적인데 해당 요청은 역순인것에 근거하여 DB 커넥션이 연결되지 못했다고 판단하여 RDS의 지표를 확인하였고. RDS의 커넥션은 테스트동안 13개로 유지되었으며 max_connection 파라미터가 낮아 애플리케이션의 커넥션이 연결되지 못한 것을 확인하였습니다.

3. 원인 확정
초반에 급격한 커넥션 생성이 발생하게 되어 3.2.2의 초반 CPU 사용량 증가에 영향을 미쳤을 것이고 커넥션은 DB에 연결되지 못하여 GC의 빈도도 상승하였을 것이라 판단할 수 있습니다.

4. 비즈니스 임팩트 : 데이터 처리 지연

2.3.3 우선순위 하: GC 스파이크, N+1 문제 및 불필요 컬럼 조회, 동기화 이슈

2.3.3.1 GC 스파이크

1. 문제 현상

GC Time 지표, GC Count 지표

테스트 약 20초 후 800ms의 스파이크가 눈에 띕니다.

점진적 부하 테스트라 요청이 심하지 않은 초반에 800ms의 스파이크라 더욱 특이합니다.

2. 원인 분석
GC 타임을 보아 Mixed나 Full GC일 가능성이 높고 Old Gen이 20초 만에 어느정도 공간을 차지 했다는 뜻 입니다. GC 카운트 지표를 보니 대형 GC 이전 약 20초동안 약 30번의 minorGC가 일어난 것을 확인할 수 있고 부하에 비하여 상당히 빠른 움직임으로 짧은 시간 동안 Eden이 급격히 찼다는건데 이 정도 카운트면 짧은 시간이지만 Old Gen으로 승격된 객체가 있다고 생각할 수 있습니다. 초반 20초 동안 작동하는 비즈니스 로직으로 이러한 움직임을 보이긴 힘들고 다른 요소에서 생각하지면 스레드 풀을 생각할 수 있습니다.

3.원인 확정
스레드 풀의 최소 스레드 수는 10입니다. 부하 초기에 스레드의 처리량을 초과하는 요청이 들어오고 반환되는 스레드보다 필요로 하는 스레드가 더 많으니 급격하게 스레드를 생성하며 JVM 객체 생성량이 급증한 것으로 예상됩니다. 이로인해 GC가 유발되었고 GC는 2.3.2의 초반 CPU 사용량 상승에 영향을 주었다고 생각합니다.
사용자 수가 30명일 때 GC가 크게 일어나고 그 뒤 500명이 될때까지 큰 움직임이 없어 비즈니스 영향력은 크지 않다고 생각하여 우선순위 하로 지정하였습니다.

4.비즈니스 임팩트 : 사용자 경험 저하

 

2.3.3.2 N+1 문제 및 불 필요 컬럼 조회

1. 문제 현상 및 원인 분석

메시지 조회 API의 프로파일

-- [T+0ms] 사용자 조회
SELECT u.id, u.created_at, u.name, u.external_id, u.nickname, u.updated_at, u.role, u.setting_ref, u.image_url, u.token_ref
FROM users u
WHERE u.name = ?  -- 'sample_farm'

-- [T+3ms] 사용자 알림 설정 조회
SELECT s.id, s.is_comment_featured_enabled, s.is_comment_enabled, s.is_farm_enabled, s.is_post_featured_enabled
FROM settings s
WHERE s.id = ?

-- [T+4ms] 알림 설정 FETCH 완료

-- [T+4ms] 사용자 토큰 정보 조회
SELECT t.id, t.created_at, t.jwt_refresh, t.external_access_token, t.external_refresh_token, t.updated_at
FROM tokens t
WHERE t.id = ?

-- [T+6ms] 토큰 정보 FETCH 완료

-- [T+6ms] 사용자 작물 정보 조회
SELECT c.id, c.created_at, c.type, c.height, c.message, c.updated_at, c.nickname, c.user_id, c.width
FROM crops c
LEFT JOIN users u ON u.id = c.user_id
WHERE u.id = ?

 

현재 유저를 조회하면 연관된 설정과 토큰이 조회가 되는 N+1 문제가 발생하고 있습니다 그래서 하나의 로직에 4번의 쿼리가 발생하고 있고 중간에 결과를 메모리에 FETCH하느라 지연이 발생하고 있습니다.

 

현재 로직상 메시지 조회에서는 유저를 한번 조회하기 때문에 N+1의 부정적 영향이 심각하지 않아 우선 순 하로 배치하였습니다. 그러나 로직의 성격에 따라서 우선 순위 상이 될수도 있는 요소입니다.

 

그리고 select * 의 불 필요한 컬럼 조회가 있습니다.컬럼을 가져오는것 보다는 디스크에서 row를 찾는것이 가장 비싼 비용이 들지만 네트워크와 전송 비용 측면에서 많은 요청 쌓이면 데이터 양이 쌓여 차이가 커질 것이라 생각합니다. 또한 메모리에도 불 필요한 용량이 쌓입니다. 또한 설계 안정성을 위해서도 필요한 컬럼만 조회하는 것이 타당합니다.

2. 비즈니스 임팩트 : 응답 성능 저하

2.3.3.3 메시지 남기기와 알림 처리 로직의 동기화

1. 문제 현상 및 원인 분석 (메시지 남기기 API의 프로파일)

 
-- [T+0ms] 알림 이벤트 객체 생성
NotificationUtil.createEventDTO(type, title, content)  [0ms]

-- [T+0ms] 알림 전송 호출 (프록시 레이어 포함)
NotificationService.send(userId, eventDTO)  [31ms]
-- [T+0ms] 알림 이벤트 객체 생성
NotificationUtil.createEventDTO(type, title, content)  [0ms]

-- [T+0ms] 알림 전송 호출 (프록시 레이어 포함)
NotificationService.send(userId, eventDTO)  [31ms]​

순번 타임스탬프 커밋 위치 소요 시간
첫 번째 커밋 19:27:00.659 [000038] COMMIT 10 ms
두 번째 커밋 19:27:00.676 [000044] COMMIT 0 ms
세 번째 커밋 19:27:00.693 [000056] COMMIT 8 ms

메시지를 남기면 롤링페이퍼 주인에게 알림이 발송됩니다. 하지만 알림 처리가 메시지 저장과 동기적으로 실행되고 있습니다. 그리고 알림 처리는 총 요청시간의 30%라는 높은 점유율을 가지고 있습니다.
또한 API 하나에 두 번의 트랜잭션이 발생하였고 커밋이 3회 일어나고 있으며 총 커밋 소요시간은 18ms로 커밋에 총 요청 시간의 17%이 소요되었습니다. 메시지 남기기와 알림이라는 서로 다른 책임의 작업을 분리된 트랜잭션으로 처리한 것이 문제입니다.

2. 비즈니스 임팩트 : 응답 성능 저하

 

3. 성능 개선 구현 및 검증

3.1 우선순위 상 개선: Connection Refused 해결

1. 조치

항목 변경 전 변경 후 변화 내용
파일 디스크립터 1024 2000 더 많은 소켓을 활성화 시켰습니다.
somaxconn 4096 1024 과도하여 4배 하락 시켰습니다.
backlog 128 2048 syn 수신 시점의 요청이 대기하는 backlog의 수치가 작아 많은 요청이 거부 당하여 상승시켰습니다

2. 개선 효과

- 이전 테스트에서 EC2 서버의 소켓, 대기 큐 병목으로 인해 많은 HTTP 요청이 거부 당했습니다.

- 우분투 서버의 소켓, 대기 큐 설정을 서비스에 맞는 형태로 변경하여 모든 요청이 Tomcat에 접근하는 것을 확인하였습니다.

3. 트레이드 오프

- backlog증가와 파일 디스크립터의 증가로 서버의 메모리 사용량이 증가하여 메모리 압박이 커지는 문제가 있지만 somaxconn를 하락시켜 부담을 감소시켰고 결과적으로 메모리에 큰 부담이 가지 않았습니다.

3.2 우선순위 중 개선: DB 커넥션 최적화

1. 조치

항목 개선 전 개선 후 비고
RDS max_connections 13 45 40개 커넥션 + 5개 여유(모니터링, SSH 등)
DBCP 최대 커넥션 수 미설정 40 커넥션 풀 고정 설정
DBCP 최소 커넥션 수 미설정 40 커넥션 풀 고정 설정
RDS wait_timeout 8시간 32분 DBCP 생명주기 보다 2분 여유 설정
커넥션 생명주기 (DBCP) 미설정 30분 혹시 모를 병목에 빠진 커넥션을 낭비하지 않게 하였습니다. 서비스에 맞는 생명주기를 설정했습니다.
커넥션 누수 감지 주기 미설정 60초 누수 탐지 및 개선

2. 개선 효과
개선 전 커넥션 생성 빈도


개선 후 커넥션 생성 빈도



- 이전 테스트에서 애플리케이션에서 다량의 커넥션을 생성했지만 RDS의 max_connection 파라미터가 낮아 커넥션을 연결하지 못하였습니다. 테스트 초반부터 많은 커넥션이 생성되었지만 활용을 하지 못해 의미없이 CPU, 메모리를 낭비하고 GC카운트를 증가시켰습니다.

- 커넥션 풀을 고정시킨 이유는 테스트 중의 커넥션 생성 리소스를 절약하기 위해서입니다. 부하의 정도를 예상할 수 있기 때문에 동적 커넥션풀을 사용했다면 테스트 중 커넥션을 생성하면서 메모리, CPU의 자원을 사용하게 되고 GC부담도 커져 응답시간과 리소스 전체에 악영향을 끼쳤을 것 입니다.

- 결과적으로 지표를 보면 개선 전에는 초반 5분에 다량의 커넥션 생성이 있었지만 개선 후에는 생성이 없는 것을 확인할 수 있습니다.

3. 트레이드 오프

- 커넥션 풀 고정 설정으로 인해 오버헤드 제거라는 장점이있지만 저부하시에도 커넥션을 유지하며 메모리를 낭비하며 유연성이 부족하다는 문제점이 있지만 여기서 커넥션풀을 더 늘려도 서버가 감당을 하지못해 현재 서버 환경과 예상 트래픽 범위 내에서 최적의 균형점을 찾았다고 생각합니다.

- 부하가 예상되지 않는 평상시에는 커넥션풀을 고정이 아닌 동적으로 설정하여 최소10 최대30 사이를 유지하여 저부하시에 메모리 낭비를 막을것이고 유연성을 가질 것 입니다.

- 생명주기와 wait_timeout의 조정으로 좀비 커넥션을 방지하여 DB리소스 소모를 방지할 수 있지만 장시간 트랜잭션을 처리하지 못한다는 문제점이 있습니다. 하지만 현재 제 서비스에 장시간 트랜잭션을 요구하는 로직은 없기 때문에 30분으로 설정하였으며 만약 장시간의 트랜잭션을 요구하는 로직을 추가하면 다시 설정을 조정하는 방식을 취할 것입니다. 종합적으로 트레이드 오프 상황에서 우위를 가져왔다고 생각합니다.

3.3 우선순위 하 개선: GC, 쿼리, 비동기 처리 최적화

3.3.1 부하 초반 GC 800ms 스파이크 개선

1. 조치

항목 변경 전 변경 후 비고
최소 스레드 수 10 40 테스트 중 리소스 절약, GC 압박 감소
최대 스레드 수 1000 400 서버가 감당할 수 있는 요청까지 조절 앞으로도 지속적 모니터링으로 점진적 하락시켜 서버에 감당할 수 있는 스레드의 수를 파악해야합니다.

2.개선 효과

항목 개선 전 개선 후
그래프

설명 테스트 시작 20초 뒤 800ms GC 테스트 시작 1분 뒤 200ms GC


- 이전 테스트에서 테스트 20초 후 0.8초 동안의 GC 스파이크로 인해 초반 응답이 지연되었습니다. 짧은 순간에 많은 Minor GC가 일어나고 20초 만에 대형 GC가 발생했습니다.

- 개선 후 0.8초에서 0.2초로 GC의 부하를 낮추었습니다.

3. 트레이드 오프

- 최소 스레드 수 증가로 메모리 사용량 상시 증가, 부하가 없을 때의 유휴 리소스 낭비라는 문제점이 있지만 메모리 사용량은 안정적인 것을 확인했습니다. 또한 부하가 예상될 때에는 최소 스레드 수를 증가 시킬 것이지만 평상시 운영할때에는 유휴 리소스를 아끼기 위해 최소 스레드 수를 감소 시킬 것 입니다.

- 최대 스레드 수 감소로 인한 처리량 제한, 응답 지연 증가로 일정 수준 이상의 부하에는 응답하지 못하는 문제점이 있지만 동시 사용자 1000명의 부하에는 견딜 수 있다는 것을 알게되었고 400에서 더 이상 증가 시켜도 서버가 버티지 못하기 때문에 현재 서버 환경과 예상 트래픽 범위 내에서 최적의 균형점을 찾았다고 생각합니다.

 

3.3.2 쿼리 개선

1. 조치

개선 전 롤링페이퍼 조회 API N+1 문제 및 모든 컬럼 조회 (4개의 쿼리)

-- [T+0ms] 사용자 정보 조회 (name = 'sample_farm')
SELECT u.id, u.created_at, u.name, u.external_id, u.nickname, u.updated_at, u.role, u.setting_ref, u.image_url, u.token_ref
FROM users u
WHERE u.name = ?

-- [T+3ms] 사용자 설정 정보 조회 (id = ?)
SELECT s.id, s.is_comment_featured_enabled, s.is_comment_enabled, s.is_farm_enabled, s.is_post_featured_enabled
FROM settings s
WHERE s.id = ?

-- [T+4ms] 설정 정보 FETCH 완료

-- [T+4ms] 사용자 토큰 정보 조회 (id = ?)
SELECT t.id, t.created_at, t.jwt_refresh, t.external_access_token, t.external_refresh_token, t.updated_at
FROM tokens t
WHERE t.id = ?

-- [T+6ms] 작물 정보 조회 (user_id = ?)
SELECT c.id, c.created_at, c.type, c.height, c.message, c.updated_at, c.nickname, c.user_id, c.width
FROM crops c
LEFT JOIN users u ON u.id = c.user_id
WHERE u.id = ?

 

 

개선 후 롤링페이퍼 조회 API N+1 해결 및 필요한 컬럼만 조회 (1개의 쿼리)

-- [T+0ms] 사용자 메시지 꾸미기 정보 조회 (user_name = 'test_user')
SELECT u.id, m.deco_type, m.width, m.height
FROM messages m
JOIN users u ON u.id = m.user_id
WHERE u.name = ?

 

 

2. 개선 효과

구분 개선 전 개선 후 비고
N+1 문제 연관된 엔티티 즉시 로딩 FetchType.LAZY 적용 필요 시점에 엔티티 로딩하여 N+1 해결
조회 컬럼 범위 모든 컬럼 조회 QueryDSL로 필요한 컬럼만 조회 메모리 용량, 설계 안정성, 보안 개선
쿼리 실행 여러 쿼리 발생 및 중간 결과 FETCH로 지연 발생 단일 쿼리로 로직 처리, 불필요 오버헤드 제거 전체적인 응답시간 개선과 그로인한 커넥션, 스레드 절약에 긍정적 영향을 주었습니다.
응답 시간 개선 약 12ms 약 1ms 약 12배 빠른 처리 시간 달성

3. 트레이드 오프
- FetchType.LAZY는 영속성 컨텍스트 밖에서 접근 시 LazyInitializationException 예외가 발생하며,이를 잘못 처리하려다 수동으로 세션/커넥션을 관리할 경우 커넥션 누수로 이어질 수 있어 개발자가 더욱 신경 써야 한다는 단점이 있습니다. 또한 성능은 최적화 되었지만 재사용성이 떨어져 요구사항 변경시 매번 컬럼을 분석하고 최적화된 쿼리 작성 및 인덱스를 고려해야한다는 단점이 있습니다.

- JPA의 더 깊은 공부로 영속성 컨텍스트를 이해하고 적절한 트랜잭션 범위로 LazyInitializationException를 최대한 방지할 것이고 만약 팀 프로젝트라고 가정하면 목적별 Repo 메서드 규칙을 철저하게 지키고 DTO 공통 인터페이스를 통한 타입 안정성 확보를 통해 개발 가이드라인을 작성하여 단점들을 극복할 생각입니다.

 

3.3.3 메시지와 알림 관심사 분리

1. 조치

개선 전 메시지 남기기 API (알림 처리 로직과 동기화 되어있습니다)

-- [T+0ms] 작물 데이터 삽입
INSERT INTO crops (created_at, crop_type, height, message, modified_at, nickname, user_id, width)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);

-- [T+0ms] INSERT 결과 페치 완료
-- RESULT-SET-FETCH #1 [0ms]

-- [T+0ms] 트랜잭션 커밋
-- COMMIT [6ms]

-- [T+6ms] AutoCommit 활성화
-- setAutoCommit(true) [0ms]

-- [T+6ms] 알림용 DTO 생성
-- NotificationUtil#createEventDTO()

 

 

개선 후 메시지 남기기 API (알림 처리를 분리하였습니다.)

// [T+0ms] SSE 알림 처리
call:thread:NotificationEventListener#handlePaperPlantEventForSse() [0ms]

// [T+0ms] FCM 알림 처리
call:thread:NotificationEventListener#handlePaperPlantEventForFcm() [0ms]

-- [T+0ms] 작물 데이터 삽입
INSERT INTO crops (created_at, crop_type, height, message, modified_at, nickname, user_id, width)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);

-- [T+0ms] INSERT 결과 페치 완료
-- RESULT-SET-FETCH #1 [0ms]

-- [T+0ms] 트랜잭션 커밋
-- COMMIT [6ms]

-- [T+6ms] AutoCommit 활성화
-- setAutoCommit(true) [0ms]

-- [T+6ms] 알림용 DTO 생성
-- NotificationUtil#createEventDTO()

 

2. 개선 효과

구분 개선 전 개선 후 비고
메시지 저장 및 알림 처리 방식 다른 성격의 로직을 트랜잭션으로 분리하여 알림 로직이 응답시간의 30% 점유 비동기 처리 (이벤트리스너, @Async 활용) 트랜잭션 전파 회피로 사용자에게 빠른 응답반환
트랜잭션 커밋 횟수 3회 (응답시간의 17% 점유) 1회 로직의 분리로 불필요한 커밋 시간 절약 및 사용자에게 빠른 응답반환
커넥션 및 스레드 점유 시간 스레드 및 커넥션 장시간 점유로 비효율적 리소스 사용 스레드, 커넥션, HTTP응답 빠르게 반환 전체적인 리소스 회전율 향, 사용자의 체감 응답시간 개선

3. 트레이드 오프
- 비동기 방식으로 변경하여 응답시간은 개선되었지만, 전체 시스템 리소스 사용량 관점에서는 여전히 개선의 여지가 있습니다. 이를 위해 fixedDelay를 활용한 배치 처리 방식으로 문제점을 극복할 것을 고려하고 있습니다.

- 이벤트리스너를 활용할시 예외가 발생되면 이벤트 유실 가능성이 있고 커넥션 누수가 발생할 수 있다는 단점이 있습니다. @TransactionalEventListener를 사용하여 비동기 상황에서의 세션과 커넥션 관리를 명확히 하고 해당 예외를 로그로 저장하여 이벤트 유실을 보완할 것 입니다.

- 또한 이벤트 객체 생성으로 인한 GC부하와 큐잉된 이벤트의 메모리 점유가 일어날 수 있습니다. 때문에 알림 스레드풀의 최적화가 필요합니다.

- 현재 SSE의 스레드풀은 13개 대기열은 50개 FCM의 스레드풀은 10개 대기열은 100개로 설정하였으며 1000명의 동시 사용자 상황에서 정상적으로 작동했고 메모리와 GC에 문제가 없는 것을 확인했습니다. 앞으로 천천히 스레드를 감소시키며 서버와 서비스에 맞는 스레드의 수를 파악해야합니다.

 

4. 성능 개선 효과 검증

4.1 개선 전후 성능 비교

지표 개선 전 개선 후 배율
최대 동시 사용자 416명 1000명 2.4배 증가
최대 TPS 193 306 1.6배 증가
평균 TPS 89 205 2.3배 증가
성공한 HTTP 요청 65,000건 122,000건 1.9배 증가
평균 응답시간 1.2초 0.35초 3.4배 감소
오류율 50% 0.1% 오류 제거

4.2 목표 달성도 평가

t2.micro를 사용하는 제 서버와 SNS인 제 서비스의 성격을 고려했을때 1차 성능 개선은 동시 사용자 500명을 목표로 했습니다. 메인 비즈니스인 메시지를 남기고 확인하는 로직에서 동시 사용자 1000명이 활발히 활동해도 모든 응답을 2초이내에 반환할 수 있다는 것을 확인하였습니다. 따라서 목표를 2배 뛰어넘은 성과를 얻었습니다.

5. 결론 및 향후 과제

5.1 개선 성과 종합

우선순위 - 상 (병목 지점)

- Connection refused 우분투 서버의 TCP 커넥션 거부 해결

우선순위 - 중

- 테스트 초반 CPU 사용량 급상승 현상 해결

- DB 커넥션 부족 현상 해결

우선순위 - 하

- 테스트 초반 GC 800ms 시간 스파이크 개선 (0.8초 → 0.2초)

- N+1 문제와 불필요한 컬럼 호출 해결 (12ms → 1ms)

- 메시지 남기기와 알림 처리 로직의 동기화 해결

 

5.2 롤링페이퍼 시나리오 향후 개선 방향

- 최대 스레드 개수 점진적 감소 : 서버가 감당할 수 있는 최대 스레드 수를 파악해야합니다.

- 이벤트리스너의 스레드풀과 대기 큐 조정 : 이벤트리스너의 스레드풀과 대기 큐의 길이도 서버와 서비스에 맞는 수치를 파악해야합니다.

- 쿼리의 재사용성 향상 : DTO 공통 인터페이스를 통해 타입안정성을 확보해야합니다.

- 비동기 상황에서의 세션과 커넥션 관리 : @TransactionalEventListener를 사용하여 커넥션 누수를 방지해야합니다.

5.3 1000명 이상의 동시 사용자 대비책

이미 최적화가 상당히 이루어진 상태이므로 가장 간단한 방법은 스케일 아웃과 스케일 업이 있습니다.

 

구분 비용 비고
스케일 아웃 월 약 15,000원, 예약 인스턴스 활용 시 약 5,000원 비용 효율적이며 간단한 확장 방법
스케일 업 스케일 아웃과 동일한 효과를 얻으려면 월 50,000원의 비용이 소모 비용이 높아 스케일 아웃에 비해 효율이 낮음

그러나 서버의 리소스를 늘리는 것은 비용이 듭니다. 때문에 다른 방법으로 HTTP Keep-Alive 최적화알림 로직의 완전한 비동기 처리가 있습니다.

- DB 최적화의 경우에는 향후 DB에 데이터가 얼마나 쌓이는 것에 따라 다르지만 현재 시점에서 RDS의 리소스는 1000명의 부하에도 여유로운 모습을 보이고 있어 백엔드 서버와 애플리케이션의 로직 관련 개선을 최우선으로 고려중입니다.

- 캐싱 도입은 SNS인 서비스 특성상 큰 성능향상이 되지 않을 것으로 예상합니다. 메시지의 조회는 대부분 분산되어 일어나고 있어 캐싱의 효율이 떨어집니다 이는 서비스의 다른 로직들도 비슷합니다.

- 알림 처리를 RabbitMQ, AWS SQS와 같은 메시지 큐로 완전 분리하고 배치로 처리하면 실시간성은 조금 하락하지만 처리량을 극대화 시킬 수 있습니다.

- HTTP Keep-Alive방식을 사용하면 통신마다 TCP연결을 종료 시키지 않고 커넥션을 재사용하여 오버헤드를 감소시킬 수 있습니다. 또한 서버의 파일디스크립터에 여유가 생겨 수치를 감소시킬 수 있어 서버의 리소스를 절약할 수 있습니다.

- 다만 Keep-Alive 방식은 타임아웃까지 메모리를 점유하기 때문에 서버의 메모리에 영향을 미칩니다 . 따라서 GC 압박이 증가 할 수 있습니다. 또한 Dos 공격에 취약하다는 단점이 있습니다. 부분적으로 Keep-Alive 설정을 하면 이 문제점을 극복할 수 있습니다. 연결 풀의 분할 또는 동적 할당을 통해 유연하게 Keep-Alive의 비율을 조정할 수 있어 Dos공격에 대항할 시간을 벌고 과도한 요청을 하는 클라이언트는 제한하는등의 방어를 할 수 있습니다. 그리고 서버가 Keep-Alive를 얼마나 유지할 수 있는지 모니터링하여 설정의 최적화를 통해 메모리 방어와 처리량 향상을 동시에 기대할 수 있습니다.