ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 최종 프로젝트 회고 (ECommerce 프로젝트)
    개발 일지 2024. 5. 14. 00:34

     

    최종 프로젝트에 들어가기에 앞서, 캠프측에서 너무 과하게 잘하는 사람이 쏠리지 않도록 밸런스를 맞출 것을 요구했다.

    나는 랜덤으로 꾸려지는 팀을 경계했기에 평소에 잘 알고지내던 캠프원들을 모아 직접 팀을 신청했고,

    내가 팀의 리더를 맡게 되었다.

     


    주제

    우리가 정한 최종 프로젝트의 주제는 ECommerce이다.

    이유는 개개인이 원하는 기술을 접목함에 있어서 유연한 기능 확장이 가능하고, 대용량 트래픽 대응 경험을 쌓기에도 알맞은 주제라고 생각했기 때문이다.

     

    기술 : Java, SpringDataJpa, Mysql, Redis, Docker, Prometheus, Grafana, K6

            AWS EC2, RDS, ElasticCache, S3, CloudWatch

     

    인원 : BE 4인

     

    Github : https://github.com/weare4potato/eCommerce

     

    아키텍처


     

    설계

    이번 프로젝트에 있어서 가장 후회되는 부분이 이 설계 파트였는데, 어떤 문제가 있었는지 되짚어 보자.

     

    우리가 진행한 설계 단계는 이와 같았다.

    API 설계 → ERD 설계 → 와이어 프레임 → 상태 다이어그램(시간 문제로 생략)

     

    이전의 프로젝트들과는 다르게 요구사항이 정해져있지 않았기에, API 설계와 요구사항을 거의 동일하게 여기며 진행했다.

    API 설계는 기능 외에도 http method, 상태 코드, request, response 등 신경써야 할 것이 많이 있기 때문에, '기능' 자체에는 집중을 하기 힘든 환경이었고, 이는 이후의 설계에 악영향을 끼쳤다.

    그렇게 진행된 API 설계 토대로 엔티티와 속성을 도출하기에는 어려움이 있었고, 결과적으로 두 설계가 유기적으로 연계되지 못했다.

     

    또한 도메인 분석에 공을 들이지 못한 것 또한 ERD 설계에 악영향을 끼쳤다.

    상품 → 장바구니 → 주문 → 결제로 이어지는 엔티티의 연관관계는 정답이 존재하지 않으며, 때문에 튜터님들의 조언또한 우리의 의도에 따라 달라진다는 대답의외엔 받을 수 없었다. 

    결과적으로 어떤 엔티티를 만들 것인가, 어떤 연관관계를 통해 기능을 구현할 것인가에 대해 논의하는 시간이 너무 길어졌다.

    이는 개발시간 감소, 잦은 회의로 인한 팀원들의 사기 저하로 이어졌다.

     

    해결책으로 ERD 설계를 일시 중단하고, 와이어 프레임 설계를 통해 부족한 부분을 보완하고자 했다.

    와이어 프레임은 어느정도 정형화 되어있는 형식이 존재하며, 그를 통해 놓치고 있던 엔티티, 연관관계, 필드를 도출할 수 있기 때문이다.

    이는 어느정도 성과를 이뤘으나, 깔끔하게 ERD 설계에 마침표를 찍을 수는 없었다.

     

    고뇌의 시간과 주변의 도움을 통해 어떻게든 설계를 마쳤으나, 이미 1주일이라는 시간을 소모하고 말았다.

     

    프로젝트 종료 후 위의 실수를 반복하지 않기 위해 DB 설계에 대한 공부를 지속하고 있다.

    그에 대한 내용 정리는 이 글의 마지막에 정리하도록 하겠다.


     

    Scope 축소

    최종적으로 완성된 ERD 설계에 대한 튜터님들의 답변은 공통적으로 '너희 그거 할 수 있겠어?' 였다.

    우리가 제시한 ERD는 아래와 같다.

     

    이커머스에 필수적으로 존재해야 할 도메인 + 팀원들이 개인적으로 하고 싶었던 도메인을 합친 결과이다.

    최종 프로젝트는 총 5주 동안 진행되지만 1주일은 설계로 소모했으며, 2주일 뒤에는 MVP 완성 및 유저 테스트를 진행해야 했다.

    때문에 위의 ERD 설계를 보고 튜터님들은 부정적인 의견을 보였으며, 우리는 의견을 수용하여 아래와 같이 스코프를

    수정, 축소하였다.

     

    제외된 기능들은 최종 프로젝트 종료 후에 다시 논의하는 것으로 결정됐다.


     

    1차 기능 개발 종료 이후 성능 테스트

    1차적으로 계획한 모든 API를 개발하는 것은 총 1주일이 걸렸고,

    모니터링 툴인 Prometheus + Grafana와 부하 테스트 툴인 k6를 통해 성능 테스트를 진행했다.

     

    로그인 ~ 결제까지의 유저 워크 플로우 또한 테스트 하고 싶었으나, AWS RDS t3.micro의 용량 한계로 인해 사용자가 가장 많이 이용하고, 또 가장 느릴것으로 예상되는 API를 위주로 하나씩 테스트 했다.


    성능 테스트에 대한 자세한 내용은 추후에 따로 정리를 하도록 하겠다.

     

    테스트 조건은 아래와 같다.

    - 데이터 10만 건 기준

    - 가상 사용자 0명 부터 500명까지 50초 동안 증가, 10초 유지

     

    측정된 결과는 이렇다

    상품 목록 조회

    - 1분 동안 총 1000건 요청
    - 터널 현상(지표 끊김 현상) 발생
    - RPS : 5 → 응답속도 개선 필요
    - duration : 14.9

     

    주문 목록 조회

    - 1분 동안 총 500건 요청

    - 터널 현상 발생 이후 시스템 다운 → 45초 이후 복구
    - HikariCP Connection pool 에러 발생
    - RPS : 3.8 → 응답속도 개선 필요
    - duration : 22.2

     

    주문 생성

    - 1분 동안 총 6600건 요청

    - 성공률 8%

    - 상품 재고 DeadLock 발생

     

    추가로 처음 진행한 테스트 이후 JVM Heap Memory가 항상 80%를 넘어 서버의 성능 저하를 불러왔다.

    JVM Heap Memory는 구동시에 결정되며, default는 컴퓨터 메모리의 1/4로 설정된다.

    우리 프로젝트가 사용하는 EC2 t2.micro의 메모리는 1GB로,  JVM Heap Memory가 할당받은 크기는 최대 250MB가 된다.

     

    너무 작다면 GC가 빈번히 일어나 부하가 발생하거나, 처리하지 못하여 아예 다운되어버리는 상황이 벌어진다.

    반대로 너무 크다면 GC가 한번에 처리해야 하는 객체가 늘어나 부하가 발생한다.

    때문에 JVM Heap Memory의 적정 크기는 2GB~4GB라고 한다.

     

    우리는 코드의 성능에 집중하고 싶었고, 그 외의 요소가 서버의 성능 저하를 일으켜 테스트의 영향을 끼치는 것을 피하고 싶었기에 다소의 비용을 각오하고 t3.medium(Memory 4GB)로 Scale up을 선택했다.

     

    최종적으로 1차 테스트의 내용을 정리하면 아래와 같다.

     

    결과
    - 전체적으로 응답속도의 개선이 필요하다.
    - 메모리 scale up 1GB → 4GB
    - JVM 힙 메모리 scale up 250MB → 2GB
    - 주문 생성시에 동시성 제어 필요
    - 응답속도 개선 이후 order 목록 조회 시에 Connection pool 에러 문제가 다시 발생하는지 확인

     

     


    성능 개선

    상품 목록 조회

    - 쿼리 최적화

    - CDN 적용 (S3 이미지 캐싱)

     

    주문 목록 조회

    - 쿼리 최적화

    - fetch join으로 N+1 해결

    - 캐싱

     

    주문 생성

    - 쿼리 최적화

    - Redis 분산락으로 동시성 제어

     

    JVM Heap Memory 250MB → 2GB Scale up

     


     

    2차 테스트

    터널 현상을 없애고 정확한 CPU, Memory의 임계점을 찾기 위해  테스트 조건 변경 

    - 데이터 10만 건 기준

    - 가상 사용자 0명 부터 200명까지 20초 동안 증가, 40초 유지

    - 500명까지 증가했던 이전 테스트와 비교하여 터널 현상은 사라지고, 요청 수에는 차이가 없음을 확인

     

    상품 목록 조회

    - 1분 동안 총 2500건 요청
    - RPS : 5 → 40 (약 8배 증가)
    - duration : 14.9 -> 4.8

     

    주문 목록 조회

    - 1분 동안 총 18300건 요청
    - RPS : 6.8 → 296 (약 75배 증가)
    - duration : 22.2 → 0.564

    - Connection pool 에러 해결

     

    주문 생성

    - 1분 동안 총 2100건 요청

    - 99% 성공
    - RPS : 33.4
    - duration : 6.1
    - 동시성 제어 목표 달성

     


     

    트러블 슈팅

    검색 기능 성능 이슈

    프로젝트 중후반쯤 검색 기능 구현에 대해 논의가 있었다.

    하지만 남은 기간안에 학습곡선이 높은 ElasticSearch를 구현하기에는 시간이 부족했고, 우선적으로 SQL을 통해 구현하였다.

    다만 검색 조건에 인덱스를 걸어도 항상 Full Table Scan이 발생하는 문제가 발생했다.

    조사 결과, 와일드카드(%)가 좌측에 붙은 경우에는 Index Range Scan이 불가능 하다는 것을 알게 되었다.

    그렇다면 인덱스를 활용하기 위해서는 'word%' 와 같은 식으로만 사용해야 한다는 뜻인데, 이렇게 제한적인 사용 방법은 원하는 방향이 아니었다.

     

    때문에 MySQL에서 지원하는 Full Text Index를 사용했다.

    Full Text Index는 인덱싱이 된 문자열을 정해진 방법으로 분리하여 인덱스를 생성하고, 이를 통해 검색 성능을 향상시킨다.

     

    위와 같은 방법을 통해 3.9s가 걸리던 검색 기능을 139ms로 단축할 수 있었다.

     

    DeadLock

    위의 성능 테스트에서 정리한대로 주문 생성 API의 부하 발생 시 상품 재고로 인해 DeadLock이 발생했다.

    우리는 최대한 실무에 가까운 서버 환경을 구성하기 위해 LoadBalancer, AutoScaling를 적용했다.

    때문에 멀티 서버에서 상대적으로 다른 락 보다 성능이 뛰어난 분산 락을 선택했고, Lettuce보다 DB의 부하가 덜 한 Redisson을 사용해 분산 락을 구현했다.

    Redisson은 재시도 횟수와 타임아웃을 지정할 수도 있어 동시성 제어 성공률을 높일 수도 있었다.

     

    Cors 에러

    유저 테스트를 위해 프론트를 만들고 백엔드의 API를 호출하는 작업 중, Cors 에러가 발생했다.

    Cors는 출처가 다른 클라이언트가 서버에 요청을 보내는 것을 허용하는 정책이다.

     

    Cors 에러가 뜨는 직접적인 원인은 Preflight 요청이 적절한 Access-Control-Header를 찾지 못했을 때 이다.

    해결하는 방법은 총 세가지가 있는데,

    우리는 그 중에서 WebMvcConfigurer를 사용하여 서버가 원하는 적절한 Access-Control-Header를 설정해 주어 해결하였다.


     

    기술적 의사결정

    OAuth Kakao Login API

    • 사용자가 번거로운 회원가입 과정을 거치지 않고도 간편하게 서비스를 시용할 수 있는 방법이 필요했다.
    • 사용자에게 친숙한 Kakao Login을 통해 신뢰성 있고 간편한 로그인 선택지를 제시했다.

    Toss Payment API

    • 사용자에게 안전한 결제 서비스를 제공할 방법이 필요했다.
    • Toss Payment는 간단한 조작을 통해 위젯을 커스텀 할 수 있어, 우리의 정책에 맞지 않는 결제 서비스들을 위젯에서 간단히 제거할 수 있었다.
    • 가이드 또한 매우 자세하여 간편하게 결제 시스템을 구현할 수 있었다.
    • 추후 KakaoPay, NaverPay 등의 간편한 결제 선택지 또한 구현할 예정으로, 두 Open API와의 연동도 가능했다.

    Redis

     

    캐시는 두가지 고민을 거쳤다.

     

    1. JPA 2차 캐시 혹은 LocalCache

    • 해당 두가지 선택지는 단일 서버에서 Redis를 대체할 선택지가 될 수 있다.
    • 하지만 우리는 AutoScaling을 적용한 멀티 서버 환경이기에, 선택지에서 제외하였다.

    2. Redis vs MemCache

    • 단순히 캐시 적용을 위해 사용할 것이라면 MemCache가 더 명료하고 단순하여 좋은 선택지가 될 수 있다.
    • 다만 동시성 제어를 위한 작업에 결국 Redis를 사용하게 될 확률이 높았다. (데이터 스토어 단일화 이점)
    • 이후 개발에서 채팅, 위치 서비스 등의 구현에 Redis의 기능을 사용하게 될 확률이 높았다. 
    • 클러스터링 기능 지원과 데이터 복원 가능에 대한 가능성을 고려했다.

    위와 같은 이유로 우리는 Redis를 채택했다.

     

    Github Actions

    • 개발을 진행하며 배포된 서버를 새로운 버전으로 교체해야 하는 일이 빈번히 일어날 것이라 예상했다.
    • 자동화된 TEST와 배포를 통해 코드의 안정성과 생산성을 증가시키는 효과를 기대 했으며,협업 툴로 github를 사용하고 있어 github actions를 사용하여 CI/CD를 설정하면 하나의 플랫폼에서 모든 작업을 처리 할 수 있다는 장점이 있다.

    위 방식은 Github 서버에 의존적이라는 점과, 다양한 확장성을 지닌 Jenkins가 여전히 현업에서 많이 사용되는 것을 익히 들었기에 Jenkins를 통한 CI/CD도 학습할 예정이다.

     

    Docker

    • 개발, 테스트 및 프로덕션 환경에서의 일관된 환경을 제공한다.
    • 이미지를 다운 받고, 컨테이너를 실행시키는 것으로 간단히 배포를 할 수 있다.
    • Docker 컨테이너는 가볍고 빠르게 시작할 수 있으므로, AutoScaling 환경에서의 트래픽 대응이 신속해진다.

    Monitoring

    • 서버의 성능 지표 확인과 에러 감지를 위해 Monitoring을 구성했다.
    • 메트릭 정보의 상호 보완으로 신뢰성을 높이고, 모니터링 서버의 장애 상황에 지표 손실을 고려하여 Prometheus + Grafana / CloudWatch로 이중 모니터링을 구축했다.
    • 두 모니터링 툴 모두 CPU, Memory가 임계점을 넘어서면 Slack을 통해 개발팀에 메시지를 발송하도록 Alert 설정을 했다.

     

    남은 과제

    • 테스트 코드 커버리지 상승
    • 로드 밸런서, 오토 스케일링 동작 검증과 테스트
    • DB 이중화 (성능 향상)
    • ELK 적용 (검색, 로그 추적)
    • Spring Batch (주문 처리, 재고 관리)
    • ECS + Fagate로 변경 (비용 절약, 관리성 증가)
    • 서버 리다이렉션
    • index host 비용 계산
    • 실시간 채팅 서버 구현 (Redis Pub/Sub)
    • 백오피스(Admin) 기능 구현
    • 쿠폰 기능 구현
    • 기간 별 회원가입 수, 주문 수 등 운영 관련 커스텀 메트릭 정보 시각화
    • 소나 큐브, 체크 스타일 적용

     


    개선할 점

     

    팀원 개개인이 이루고 싶었던 목표를 빠듯한 일정으로 인해 많이 포기해야만 했던 것 같아 팀장으로써 많이 아쉬움을 느낀다.

    결정적인 원인은 위에서도 언급 했듯이 ERD 설계에 너무 오랜 시간을 써 버린 것으로 생각한다.

    지금까지 주입식으로 배운 API 설계 → ERD 설계 → 와이어 프레임으로 이어지는 단계에 아무런 의문을 느끼지 못했으며,

    그저 '이런 도메인에는 이런 기능과 테이블들이 존재하겠지' 라는 생각으로 안일하게 설계해 왔다는 것을 이번 기회에 깨달았다.

     

    프로젝트 종료 이후 DB 설계에 관한 집중 학습을 시작했으며, 다음은 아래와 같은 단계를 걸쳐 설계할 것이다.

    1. 도메인 분석 : 레퍼런스 참고 + 신기술 조사

    2. 요구사항 정리

    3. 일정 및 개발 범위 예측

    4. 기능 목록 설계

    5. ERD 설계

    6. 와이어프레임 설계

    7. 상태 다이어그램 작성

     

    4~7 까지의 설계는 유기적으로 연결하여 언제든 상호 보완적인 움직임을 가져야 한다는 것을 주의하고 임하려고 한다.

     


     

    마무리

    5주간 잠자는 시간 외의 모든 시간을 프로젝트에 할애했다고 해도 과언이 아니다.

    그만큼 탈력감도 있고, 뒤늦게나마 건강도 챙기려다 보니 프로젝트 종료 이후 이 글을 작성하기까지 시간이 걸렸다.

    위에 적었던 것도 있고, 적지 못한 아쉬운 점도 있지만, 한달간 정말 많은 성장을 이룰 수 있었다.

    결과적으로 나쁘지 않은 결과물을 만들었다고 생각한다.

    함께 고생해준 팀원들과 많은 조언을 준 튜터님들에게 감사한 마음을 전한다.

     

Designed by Tistory.