티스토리 뷰
안녕하세요 VI Engineering 팀 김윤제입니다.
저는 현재 Gmarket Mobile Web Vip(View Item Page = 상품 상세)를 담당하고 있는 Backend Engineer 입니다.
올해 6월 Gmarket Mobile Web Vip Backend 쪽의 성능을 최대 9배, 평균 5배를 개선하였습니다.
개선 직후 BXE 실 & 팀장님께 발표를 하고 회사 블로그에 글을 작성해야지 하면서 미루다가
지금은 어느덧 12월이 되었네요.
작업을 시작했을 때만 생각해도 막막했었는데 성공적으로 끝내니 그 기분은 이루 말할 수 없습니다.
자세한 이야기는 아래에서 상세히 다루도록 하겠습니다.
View Item Page
성능개선 설명에 앞서 제가 맡고 있는 Vip (View Item Page) 파트에 대해 설명을 드려야 이해에 도움이 될 것 같아
간단하게 설명드리려고 합니다.
상품 상세 파트는 Home 다음으로 트래픽이 많은 곳이며 다른 여러 부서에서 api를 제공받아 보여주는 부서이기 때문에
여러 API들을 호출하여 조합하는 로직이 주를 이룹니다.
사용자 경험을 위해서는 항상 빠른 속도를 유지해야 하기 때문에 API들의 속도에 따라 탈락을 시켜야 할 수도 있으며,
여러 API들의 조합 시에는 0.01초라도 성능의 이점을 줄 수 있는 전략을 선택해야 합니다.
최대 9배? 평균 5배?
최대면 최대지 평균은 뭐고 수치 차이가 왜 이렇게 심해?라고 궁금해하실 수 있을텐데요.
그건 바로 Gmarket의 상품 특성에 관련이 있습니다.
Gmarket을 이용해 보신 분들이라면 아래와 같이 연관상품을 보신 적이 있으실 겁니다.
연관상품을 보여주는 경우에는 여러 API들 (ex 상품별 쿠폰적용가) 등을 추가로 호출하기 때문에
일반상품만 보여주는 경우와는 속도 차이가 있습니다.
아래는 API의 개선 전 후 성능을 표로 나타냈습니다.
API 성능 개선 표
개선 전 (평균) | 개선 후 (평균) | 결과 | |
연관상품 | 2초 ~ 4.8초 (3.4초) | 0.5초 ~ 0.8초 (0.6초) | 약 최대 9배, 평균 5배 |
일반상품 | 0.5초 ~ 1초 (0.75초) | 0.2초 ~ 0.4초 (0.3초) | 약 최대 5배, 평균 2.5배 |
원인 분석
현재 Gmarket Mobile Web Vip는 C#, .NetFramework으로 이루어진 상당한 난이도의 Legacy 프로젝트입니다.
원인은 크게 3가지가 있었습니다.
- 하나의 메소드안에서 대부분의 로직 수행 (2천 줄 이상)
- 메소드 분리가 되어있지 않아 장애 시 원인 파악이 쉽지 않음.
- 메소드 분리가 되어있지 않아 각 로직들이 무슨 기능을 수행하는지 모르겠음.
- 이름이 이쁘게 되어 있지도 않음.
- 히스토리 부재
- 메소드 분리가 되어있지 않아 장애 시 원인 파악이 쉽지 않음.
- 곳곳에 숨은 Dirty Code
- 성능 저하를 일으키는 Critical 한 코드 존재
- 동기식 절차적 수행
- 기능이 추가될 때마다 성능의 저하가 곧바로 나타나는 구조
(최근 들어 급격하게 Gmarket에 많은 기능이 추가되었음)
- 기능이 추가될 때마다 성능의 저하가 곧바로 나타나는 구조
우선 저는 어느 로직에서 성능에 저하가 나타나는지 확인하기 위해 아래와 같이 순서를 나누었습니다
- 하나의 메소드가 곧 전체인 메소드를 독립적으로 호출 가능한 아주 작은 단위로 쪼갠다.
- 각 메소드 별 수행 시간을 체크한다.
- 수행 시간이 오래 걸리는 메소드의 라인마다 수행시간을 체크한다.
해결
해결 방법은 아래와 같습니다.
메소드의 분리
대부분의 로직을 담고 있는 하나의 메소드를 독립적으로 호출 가능한 아주 작은 단위로 쪼개었습니다.
이 작업은 추후의 작업인 병렬호출에도 영향을 끼칩니다.
Clean Code
Gmarket Mobile Web Vip에서 성능 저하를 일으켰던 Critical Dirty Code에 대해 설명드리도록 하겠습니다.
가장 큰 Diry Code는 연관 상품 목록을 API로부터 받아 온 후 Converting 작업에서 아래와 같은 문제가 있었습니다.
● 반복문 내 다른 API 호출
아래 예시는 실제 API를 테스트해봤습니다.
(Event 팀 분들께 사죄의 말씀드립니다. 트래픽이 적은 시간에 했습니다.)
- 실제 API 1회 호출
- 실제 API 50회 호출
● 반복문 내 다중 형변환
아래와 같이 메소드를 호출하며 다중 형변환이 되는 작업이 있었습니다. 성능에 많은 영향을 끼칠 정도는 아니지만,
이런 코드를 생산해서는 안됩니다.
ex) 형변환 예시
● 반복문 내 Boxing & Unboxing
이펙티브 자바라는 책에도 나올 정도로 아주 유명한 내용입니다.
Boxing & Unboxing은 반복문의 횟수가 높아질수록 위험합니다.
아래의 예시는 약 11배 정도의 속도차이를 보여주고 있습니다.
ex) AutoBoxing 연산을 수행하는 로직
ex) 같은 형끼리의 연산을 수행하는 로직
위 예시를 보고선 너무 극단적으로 반복문을 돌린 거 아니냐?라는 생각을 하실 수도 있습니다.
하지만 개발을 할 때 보면 필드가 상당히 많을 수 있습니다.
반복 횟수가 많지 않더라도 Boxing & Unboxing을 수행하는 필드가 많다면 필드의 개수만큼 속도에 영향을 끼칩니다.
● 데이터가 정의된 자료구조를 반복문 내에서 다른 자료구조로 변경
C#에서는 Dictionary라는 자료구조가 있습니다. 이 자료구조로 정적 데이터를 만들어 놓았지만,
반복문 내에서 Dictionary구조를 List로 변환하는 작업이 있었습니다.
이 부분도 꽤나 많은 시간을 소요했습니다.
아래 예시는 HashMap에 20개의 데이터를 넣은 후 반복문 내에서 List로 변환하는 작업입니다.
ex) HashMap to List
● 타 API들끼리의 결과를 Merge 하는 방법
VIP에서는 아래와 같이 2개의 리스트를 병합을 해야 하는 일이 많습니다.
하지만 병합하는 방법에 따라 속도의 차이가 많이 나고 있는데요.
반복문이 50 x 50 임에도 불구하고, 아래의 두 결과는 약 9배가량의 속도차이가 나고 있습니다.
반복문의 수치가 높아지면 높아질수록 결과의 폭은 더 커질 것입니다.
ex) 이중 for문을 통한 병합 (기존)
ex) 한 개의 리스트에서 공통된 필드를 Key로 잡아 Map 변환 후 병합 (신규)
● 동일 API를 분리하여 병렬 호출 하는 방법
파라미터로 많은 수의 리스트를 보내는 API의 경우 이러한 전략도 있다는 것을 알아두시면 좋을 듯합니다.
아래의 AS-IS 와 TO-BE 각각 100번 호출 결과 평균 0.05초의 성능이 개선되었습니다.
(AS-IS) 상품 리스트 50개를 한 번에 API 호출에 담아 보낸 후 결과 Merge
(TO-BE) 상품 리스트 50개를 3분할 하여 API를 병렬 호출 후 결과 Merge
병렬 호출
코드적인 부분에서 상당한 시간을 단축시켰음에도 불구하고, 현재의 구조적인 이슈 때문에
더 이상 성능 개선이 어려웠습니다.
때문에 기존의 절차식&동기식 메소드 호출에서 병렬호출 구조로 변경을 해야만 했습니다.
병렬 호출 구조 변경 작업은 아래와 같이 분리하여 진행했습니다.
- 반드시 선행이 되어야 하는 작업 (DB 호출)
- 독립적으로 병렬 호출 되어도 무방한 작업 (타 API 호출)
- 반드시 후행이 필요한 비즈니스 로직 작업
검증
검증 방법은 아래와 같이 크게 2가지로 나누었습니다.
- 기존 API(V1)와 신규 API(V2)의 정합성 Test
- Gmarket에는 수십가지 ItemType이 존재합니다. ItemType별 상품번호를 기존 API(V1)과 신규 API(V2) 각각 호출 후 데이터 Diff를 통한 검증 (개발자 & QA 교차 검증)
- Rendering Test
- 위의 정합성 테스트가 끝난 후에 Front Server의 EndPoint를 하나 더 만들어서 신규 API를 바라보게 하였습니다.
- V1을 바라보는 프론트 화면과 V2를 바라보는 프론트 화면의 Rendering의 차이가 없는지 (동작 유무) 까지 검증하였습니다.
- 위의 정합성 테스트가 끝난 후에 Front Server의 EndPoint를 하나 더 만들어서 신규 API를 바라보게 하였습니다.
위의 모든 테스트가 끝난 후에 기존 API(V1)을 바라보고 있는 Front Server에서 신규 API(V2)를 바라보도록 수정 후에
점진적 배포를 통해 테스트 및 모니터링을 하였습니다.
끝으로
모든 내용을 담기에는 내용이 길어 중요한 부분들만 추려 각색하였습니다.
(실제로 리팩토링한 부분은 상당히 많습니다.)
또한 실제 데이터가 아닌 예시 케이스를 만들었기 때문에 실제의 수행시간을 나타내지는 못했습니다
예시보다 실제 상황에서 훨씬 더 많은 시간이 소요되었으며, 개선 효과 또한 부각되었습니다.
글로 쓰기에는 상당히 짧아졌지만 실제로 이 성능개선 작업은 업무 시간 외 한 달 반 정도의 시간을 주말까지 포함하여 걸린 듯합니다.
현재 Gmarket VIP는 Anna라는 이름으로 개편 작업 중에 있는데요.
비즈니스 로직 담당자로서 좋은 결과 보여드리도록 하겠습니다.
긴 글 읽어주셔서 감사합니다.
'Backend' 카테고리의 다른 글
개발자를 잠 못 들게 만드는 코드 (1) | 2024.02.25 |
---|---|
쿠버네티스 오퍼레이터를 Golang으로 개발해보기 (4) | 2024.02.15 |
Kotlin에서 리스트 추출하기 : subList, slice, take, drop (0) | 2023.11.22 |
오픈마켓에서 여행 플랫폼으로 살아남기 (feat. msa) (0) | 2023.10.11 |
G마켓 쿠폰적용가 도입기 - 캐시를 중심으로 (0) | 2023.08.02 |