이번 포스팅에선, 한편의 수학 질문 게시판 성능 최적화 과정을 어떻게 이뤄냈는지 설명한다.
1. 문제점 파악
질문 게시판 기능뿐만 아니라 어플리케이션의 모든 기능들은 모놀리식 아키텍쳐로 구성됐다.
200명 정도의 사용자임에 따라 문제가 없을거라 예상했지만, 게시판 동시 조회 시 응답속도가 현저히 낮아지는 문제가 발생했다.
파악된 문제점들은 다음과 같았다.
1. 조회수 락으로 인한 응답속도 저하
조회수는 실시간성이 중요한 데이터 도메인이다.
사용자는 자신의 조회수가 실시간으로 반영되어 있지 않다면, 사용감에 부정적인 영향을 줄 수 있다.
또한 동시접근이 빈번함에 따라, race Condition이 빈번히 발생할 가능성이 존재한다.
기존 모놀리식 어플리케이션에선 Row Level Lock ( X-LOCK )을 통해 race condition을 방지할 수 있었다.
하지만, 매 트랜잭션에 락이 걸리며 해당 row에 접근하는 모든 트랜잭션이 블록되는 상황이 발생됐으며,
이로 인해 TPS가 현저히 낮아지는 이슈가 발생했다.
2. 조회 마다 조인 연산으로 인한 성능저하
질문 조회시 해당 질문에 등록된 댓글, 작성자, 질문자에 대한 정보를 제공하기 위헤 조인연산이 필수적이였다.@Cacheable
이 제공하는 cache aside 정책으로 해당 결과를 캐싱해둘수 있지만, 앞서 말한 댓글과 같이 실시간성이 강한 데이터를 반영하기 위해
Cache Invalidation이 필수불가결했다.
3. 오프셋 페이징의 처리시간 문제
스프링 JPA을 사용함에 따라, Pageable
클래스를 활용하여 페이징을 처리하고 있었다. 오프셋 방식 페이징은 모든 레코드를 카운트함에 따라 offset 값이 커질 수록 처리시간이 길어지는 문제가 있다. MySQL에서 인덱스를 통해 응답시간을 단축할 수 있을거라 생각했지만, 이는 예상대로 동작하지 않았다.
각각의 문제점을 어떻게 해결해나갔는지 이번 포스팅에서 작성해보겠다.
2. 문제 해결
1. 조회수 락으로 인한 응답속도 저하 - redis
MySQL에서 실시간 데이터를 갱신함에 따라 Race Condition을 방지하기 위해선, Lock이 필수적이다.
동시성이 존재하는 한, Lock을 어떻게든 구현해내야한다.
동시성 걱정없이 빠른 속도의 인메모리 DB인 redis를 활용하여 해당 문제를 해결했다.
원자적 연산을 제공하는 레디스 API를 통해 조회수 조회 및 증가를 구현할 수 있었으며, Cache Write-Back 패턴을 통해 디스크 I/O를 모두 제거했다.
이를 통해 클라이언트에 빠른 응답속도를 제공할 수 있었다.
(또한, Cache Write-Back을 활용하여 조회수를 MySQL에 백업하도록 하여 혹시모를 레디스클러스터의 장애를 방지했다. )
2. 조회 마다 조인 연산으로 인한 성능저하 - Query Model ( Redis )
조회에 필요한 데이터를 미리 만들고, DB조회 대신 해당 모델을 read하도록 하여 빠른 응답속도를 보장할 수 있었다.
또한, 이전의 실시간성이 강한 데이터 ( 조회 수 )는 Query Model에 포함하지 않도록 하여 Cache Invalidation을 최소화 했으며, 응답시간을 단축할 수 있었다.
3. 오프셋 페이징의 처리시간 문제 - 커버링 인덱스, 서브 쿼리
오프셋 기반 페이징은 모든 레코드를 카운트하게 된다. 이를 효율적으로 읽기 위해 인덱스를 적용했으나, MySQL 옵티마이저는 사용하지 않는다.
mysql> explain select * from question order by registered_date_time limit 30 offset 0;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | question | NULL | ALL | NULL | NULL | NULL | NULL | 11 | 100.00 | Using filesort |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)
이를 위해 다음과 같이 서브쿼리에서 커버링인덱스를 통해 PK를 추출하고, 조인하는 것으로 성능향상을 이뤄낸다.
mysql> explain select * from (select question.id from question order by registered_date_time limit 30 offset 0) ques JOIN question ON question.id = ques.id;
+----+-------------+------------+------------+--------+---------------+--------------+---------+---------+------+----------+----------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+--------+---------------+--------------+---------+---------+------+----------+----------------------------------+
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 11 | 100.00 | NULL |
| 1 | PRIMARY | question | NULL | eq_ref | PRIMARY | PRIMARY | 8 | ques.id | 1 | 100.00 | NULL |
| 2 | DERIVED | question | NULL | index | NULL | idx_dateTime | 9 | NULL | 11 | 100.00 | Backward index scan; Using index |
+----+-------------+------------+------------+--------+---------------+--------------+---------+---------+------+----------+----------------------------------+
3 rows in set, 1 warning (0.00 sec)
3. 느낀 점
어플리케이션의 성능은 개발자의 도메인 이해도에 달렸다는점을 알 수 있었다.
'프로젝트 일기 > 한편의 수학 학원' 카테고리의 다른 글
API 테스트 하는데... 인증까지 매번..? (0) | 2025.04.22 |
---|---|
데코레이터 패턴을 통한 비동기 처리의 안정적 도입 (0) | 2025.04.14 |
이미지 업로드 API에서의 트랜잭션 병목과 비동기 처리 전략 (0) | 2025.04.13 |
[Refactor] Bean Validation Duplicated (0) | 2025.04.09 |
벌크연산을 통한 쿼리 최적화 (0) | 2025.04.04 |