Skip to content

1000만 건 상품 데이터가 있는 의류 쇼핑몰

Notifications You must be signed in to change notification settings

SpringMarket/SpringMarket

Repository files navigation

🌿Spring Market

image

🌏 프로젝트 소개

  • 1000만건의 상품 데이터가 있는 상황에서 사용자 편의성을 극대화 해보고자 했습니다.

Spring Market 이용해보기 Click!
Spring Market 팀 노션 Click!

⚙ InfraStructure

InfraStructure2

⛓ ERD

OPEN

image


🧬API

Spring Market API Click!


🕹 Tech Stack

✔ Back-end


✔ DevOps


✔ Front-end


📣버전

Application

  • JAVA 11
  • Spring Boot _2.7.0
  • Spring Security _0.11.2
  • JPA
  • Query DSL _5.0.0
  • Full Text Search

Data

  • AWS RDS - MySQL _8.028
  • AWS ElastiCache for Redis _7.0.4
  • AWS S3
  • Faker (faker_15.2.0)

CI/CD

  • Github Action
  • AWS EC2
  • AWS Elastic Beanstalk

Monitoring

  • Cloud Watch
  • AWS OpenSearch
  • Logback

TestCode

  • Junit 5
  • Mock
  • TestContainer

Front

  • React - yarn
📣기술 & 라이브러리 사용 이유

1️⃣ Query DSL
  1. 동적인 쿼리 작성이 필요했습니다.
  2. 자동 완성 등 IDE의 도움을 받을 수 있어 편리합니다.
  3. 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있어 더욱 안전합니다.
  4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있었고 가독성이 좋아졌습니다.
2️⃣ Full Text Search
  • 대용량 데이터에서 빠른 키워드 조회를 위해 사용했습니다.

3️⃣ RDS- MySQL

  • 대용량 데이터를 다루기 때문에 PostgreSQL과 MySQL 사이에서 고민을 했는데 MySQL을 선택한 이유에 대해서 작성해 보았습니다.
  1. 프로젝트 전 MySQL을 미리 학습한 경험이 있어 빠르게 마무리해야 하는 프로젝트 특성 상 다른 DB보다 빠르게 프로젝트에 적용이 가능하기 때문에 선택했습니다.

  2. 단순 CRUD 시 MySQL의 성능이 조금 더 우수했습니다.

    PostgreSQL은 Update시 MySQL과는 다르게 변경 전 값을 삭제 마크처리 후 변경 후 값을 새 행으로 추가하는 방식으로 작업이 진행되기 때문에 Update의 성능이 떨어졌고, Update 기능이 많이 일어나는 프로젝트 특성 상 MySQL을 사용하는 것이 적합하다고 생각했습니다. (그래서 PostgreSQL은 보통 Insert, Select 위주의 서비스에 사용된다고 합니다.)

  3. 현업에서 MySQL의 점유율 높기 때문에 레퍼런스를 찾기 쉬웠습니다.

    PostgreSQL도 4위의 점유율과 급격하게 성장하고 있지만 MySQL은 전 세계 2위의 점유율을 가지고 있고 오랫동안 높은 점유율을 가지고 있기 때문에 개발을 공부하는 입장으로서 많은 레퍼런스들을 참고 할 수 있다는 점에서 선택하였습니다.

4️⃣ AWS ElastiCache for Redis
1. AWS 아키텍처로 구성된 프로젝트에서 최적화된 서비스와 팀 프로젝트 환경에서 효과적인 모니터링을 위해서 AWS 클라우드 제품을 사용했습니다. 2. 다양한 데이터 타입과 영속화가 필요했습니다.
5️⃣ Faker & SQLAlchemy
  • 1000만건 상품 데이터를 목표로 하였기 때문에 실제 의류 쇼핑몰 크롤링을 하기에는 무리가 있다고 판단하여 더미데이터 생성을 하기로 결정하였습니다.

  • 카테고리별로 다른 상품 이름, 사진을 생성해야 하고, 상품과 주문 생성일자가 Primary key인 id에 따라 증가하도록 데이터를 구성할 수 있어야 하고 생성한 대량의 데이터를 손쉽게 RDS MySQL DB로 보낼 수 있어야 했습니다.

  • 파이썬 Faker 라이브러리로 위 조건을 만족하는 더미데이터를 생성했고 Python과 MySQL을 연결시켜주는 라이브러리 mysqlclient를 설치하고 파이썬의 ORM인 SQLAlchemy를 이용하여 생성한 더미데이터 1000만 건을 데이터베이스에 입력했습니다.

6️⃣ Github Action
  • 비용에 문제가 발생하지 않습니다.
  • 클라우드에서 동작하므로 서버 설치가 필요하지 않습니다.
  • Github Repository로 관리하는 프로젝트이기에 호환이 좋고 Github 이벤트(ex. PR) 처리가 가능합니다.
  • GitHub의 완전 관리 서비스이기에 사용이 편리하기에 인프라를 관리하는데 드는 코스트를 낮출 수 있습니다.
7️⃣ AWS Elastic Beanstalk
  • 다양한 인프라 서비스를 간편하게 사용할 수 있습니다. < 용량 프로비저닝, 로드 밸런싱, 모니터링, 협업 도구 >

  • Github에서 통합이 가능

  • 완전 관리형 서비스 사용으로 프로젝트에서 인프라에 사용하는 코스트를 낮출 수 있음

8️⃣ Logback
  • 개발 과정에서 문제 원인 파악 및 개발의 안정성 확보를 위해 콘솔 로그 외의 로그 관리의 필요성을 느꼈습니다.
  • Spring Boot에서 로깅은 대표적으로 Log4j, Logback, Log4j2으로 로그 구현체를 사용합니다.
  • Log4j는 가장 오래된 프레임워크이며 2015년에 단종되었기 때문에 선택지에서 제외하였습니다.
  • Logback은 Log4j를 개발한 개발자가 개발한 Log4j의 후속 버전으로 지속적으로 업데이트되고 있습니다.
  • Log4j2는 가장 최근에 나온 로깅 프레임워크로 logback과 마찬가지로 필터링 기능과 자동 리로드 기능을 가지고 있습니다. logback과의 가장 큰 차이점은 Multi Thread 환경에서 비동기 로거(Async Logger)의 경우 log4j, logback 보다 처리량이 더 높고, 대기 시간이 훨씬 짧습니다. 또한 람다 표현식과 사용자 정의 로그 레벨도 지원합니다.
  • 로깅으로 DB관련 로그, 에러 로그, api 통신 로그를 분리해 파일로 관리하는 것을 목표로 했기 때문에 스프링 기본 설정인 logback 사용을 결정하였습니다.
  • ConsoleAppender로 Info레벨 이상 로그를 콘솔에 출력,RollingFileAppender로 Debug레벨 이상 DB관련 로그와 Warn레벨 이상 에러로그를 각각 다른 파일로 저장하고 logback-access 모듈을 이용해 api 통신 관련 통신 로그 또한 파일로 저장하여 모니터링하였습니다.
9️⃣ JUnit5 & Jacoco
  • Spring Boot 2.2.0 버전부터 JUnit5가 기본으로 채택되었으며 JUnit4보다 다양한 기능이 제공되어 JUnit5로 테스트 코드를 작성하였습니다.
  • 컨트롤러 테스트는 @WebMvcTest 어노테이션으로 Web Layer에만 집중하여 테스트하였으며, 서비스 테스트는 모듈 간의 상호작용이 정상적으로 수행되는지 확인하기 위해 통합 테스트를 진행했습니다.
  • JUnit 테스트 결과를 바탕으로 커버리지를 결과를 리포트해주는 Jacoco 라이브러리를 도입하여 구문 커버리지를 측정하며 안 쓰이는 코드와 어노테이션을 확인하며 리팩토링을 진행하였습니다.
🔟 TestContainer

  • DB를 사용한 서비스 코드를 어느 환경에서든 바로 테스트가 가능하게 하기 위해서 사용했습니다.

🔥 주요 기능

애플리케이션 미리보기

회원가입/로그인 메인페이지 랭킹보드
필터링 검색 키워드 검색
장바구니 주문

✔ 검색

  • 📢 Latency 목표값 설정 기준 Click!

    KISSmetrics는 고객의 47%가 2초 이내의 시간에 로딩이 되는 웹 페이지를 원하고 있으며, 40%는 로딩에 3초 이상 걸리는 페이지를 바로 떠난다고 설명했습니다.

  • 메인 페이지의 로딩 속도는 플랫폼 첫 인상에 큰 영향을 주기에 100ms 이내를 목표했습니다.
  • 필터링/키워드 조회 속도 2초, 상세 조회는 400ms 이내를 목표했습니다.

1⃣ Redis Sorted Set - 랭킹보드 구현을 통한 메인 페이지 로딩 최적화

  • Redis Sorted Set을 통해 평균 100ms의 속도로 랭킹보드를 제공하고 있습니다.
  • 메인 페이지에 접근할 때마다 Order By가 동작하는 기존의 코드보다 27900% 조회 성능이 향상되었습니다. ( 28s -> 100ms )
  • 파이프라인 + 스케줄러를 통해 주기적으로 랭킹보드를 세팅하고 있습니다.

2⃣ Redis Cache Aside - 상품 상세페이지 캐싱

  • 카테고리별 상위 5,000개의 상품 페이지를 캐싱하여 사용하고 있습니다.
  • Redis 캐시 데이터를 통해 DB의 부하를 최소화했습니다.
  • 파이프라인을 통해 Cache Warmup을 동작시키고 있습니다.

3⃣ 결합 인덱스 추가, 커버링 인덱스 적용

  • 조회수+pk로 결합 인덱스를 추가하여 조회순, 날짜순 정렬 시 성능 저하의 가장 큰 원인이었던 sort 부하를 해결했습니다.
  • QueryDSL은 서브쿼리를 지원하지 않기 때문에 커버링 인덱스를 활용해 페이징 조회 성능을 1900% 개선했습니다.

4⃣ Full Text Search로 키워드 검색

  • 키워드 조회 시 Full-Text-Search 방식을 사용하여 like문을 사용한 쿼리보다 약 634% 조회 성능을 개선했습니다.

5⃣ 데이터 반정규화

  • 쿼리문에서 join문을 제거를 위해 데이터 반정규화를 하여 조회수, 재고수 테이블을 상품 테이블과 병합했고
    조회 성능을 66.6% 개선했습니다.

✔ 주문

  • 📢 동시성 제어 목표값 설정 기준 Click!

    온라인 패션 스토어 무신사가 선보인 패션 특화 라이브 방송 ‘무신사 라이브’ 메종 키츠네 편이 방송 시작 5분 만에 매출 1억 원을 돌파했습니다.
    동시 상품의 주문으로 가정했을 때 300초 동안 2000건의 주문이 발생한 상황입니다.
    (상품 가격 50,000원 기준)
  • 대규모 트래픽 상황에서 주문과 재고 데이터의 정합성은 서비스의 신뢰도에 큰 영향을 주고 있습니다.
  • 안정적인 동시성 제어를 위해 목표치를 '무신사 라이브 메종 키츠네 편'의 15배로 설정했습니다.
  • ( 30초 동안 동시 주문 3000건에 대한 정합성 유지 )
1⃣ Pessimistic Lock으로 동시성 제어

  • 트랜잭션이 시작될 때 MySQL DB에 Exclusive Lock을 걸어 Race Condition을 해결했습니다.
  • 서비스 코드에서 재고 변경 로직의 트랜잭션을 분리해 효율적으로 처리했습니다.
  • 부하테스트 결과 30초 동안 이루어지는 3000건의 동일 상품 주문 데이터 정합성을 유지 성공했습니다.

2⃣ Connection Pool Size 설정으로 데드락 문제 해결

  • Pessimistic Lock은 데드락 발생 가능성이 있었습니다.
  • 데드락을 피하는 Connection Pool Size 공식과 JMeter 부하테스트를 통해 데드락을 회피할 수 있으며
    에러율이 가장 낮은 지점(20)을 발견하고 적용하였습니다.
  • Default Connection Pool Size인 10개 기준보다 에러율이 10% 하락했습니다.

💉 프로젝트 관리

✔ 애플리케이션 배포

  • Github Actions + Elastic Beanstalk 사용으로 CI/CD 환경을 구축했습니다. ( 추가 배치를 통한 롤링 )
  • ALB를 활용한 Trigger 기반 오토 스케일링으로 유동적인 트래픽에 효율적으로 대응하고 있습니다.
  • Github Actions CI 동작 시 Gradle 세팅 캐싱을 통해 더욱 생산성을 높였습니다. ( 2m -> 1m 9s )
  • 인터넷 보안 환경을 위해 HTTPS를 적용했습니다.

✔ 모니터링

  • Cloud Watch를 사용하여 로그 + 성능 지표를 모니터링 하고 있습니다.
  • CPU가 70%를 초과하면 알림이 울리는 경보 프로세스를 구축했습니다. ( 경보 시 오토 스케일링 동작 )

✔ 테스트 커버리지 91%

  • 발생할 수 있는 주요 시나리오에 대해 대처하고자 했습니다.
  • 쓰지 않는 Lombok 코드와 같이 사용하지 않거나 중복된 프로덕션 코드를 제거하고 코드에 대한 이해도를 올리고자 했습니다.
  • 📢 클린코드 中

    얼마만큼의 코드를 자동화한 단위 테스트로 계산해야 할까? 대답할 필요조차 없다.
    모조리 다 해야 한다. 모.조.리! 100% 테스트 커버리지를 권장하냐고? 권장이 아니라 강력히 요구한다.
    작성한 코드는 한 줄도 빠짐없이 전부 테스트해야 한다. 군말은 필요 없다. ― 클린코드 (로버트 마틴 저)

✔ React를 통한 클라이언트 코드 작성

  • Single Page Application 구현으로 UX를 최대화 시키고자 했습니다.
  • 백엔드 프로젝트지만 React 코드를 직접 작성하여 프론트엔드와 함께 진행하는 실제 협업 프로세스처럼 진행하고자 했습니다.

🎯 트러블 슈팅

📌 5,000 건의 상품 데이터 Cache Warmup 동작 시 Redis Latency의 지연이 발생했습니다.

❗ 문제상황

  • 상품 데이터의 빠른 조회와 DB 부하 분산을 위해 캐싱은 필수였습니다.
  • 하지만 TCP 기반으로 동작하는 Redis에 5,000 건의 데이터를 개별로 Input 하면서 Latency의 지연이 발생했습니다.
  • ( 카테고리별 상위 5,000건의 상품 데이터를 캐싱하여 사용하고 있습니다. )
  • Request +5000 ( Redis 요청 5000건 발생)
  • 1313
  • nonepipe 5000-

💡 Solution : Redis Pipeline 구축

  • 작업의 단위를 직접 구축해서 요청이 가능해졌습니다. ( 다중 Insert 가능 )

✔ 결과

  • 5,000건의 TCP 통신이 1건(+5000)으로 축소되었습니다.
  • Request +1 ( Redis 요청 1건 발생)
  • 131313
  • pipe 5000_
📌 높은 트래픽 상황에도 상품이 클릭될 때마다 조회수 Update 쿼리가 동작했습니다.

❗ 문제상황

  • 높은 트래픽이 발생할 때 조회와 함께 발생하는 Update 쿼리는 서버에 큰 무리가 있었습니다.
  • 10초간 상품 상세 조회가 1만회 동작할 때 에러율이 62.31% 발생했습니다.
  • 10,000 view update1
  • 10,000 view update graph

💡 Solution : Cache Write Back

  • 조회수를 캐시에 모아 일정 주기 DB에 배치하는 프로세스를 구현했습니다.
  • 싱글쓰레드인 Redis의 특성상 Atomic하게 Increment를 처리할 수 있었습니다..
  • 조회 기능의 많은 I/O와 함께 발생하는 Update 쿼리를 컨트롤할 수 있었습니다..

✔ 결과

  • 클릭 시마다 발생했던 Update 쿼리를 1시간 주기로 일어나는 배치 작업으로 최적화가 이루어졌습니다.
  • 동일 상황에 에러율 0%를 달성했습니다.
  • 10,000 view redis1
  • 10,000 view redis graph
📌 필터링 조회 동작 시 Index가 적용되지 않는 이슈가 발생했습니다.

❗ 문제상황

  • Full-Text-Search 키워드 필터가 포함된 필터링 조회 동작 시 타임아웃이 발생했습니다.
  • 필터링 조회에서 정렬(조회순, 날짜순)은 필수적으로 이루어져야 하는데 Full-Text-Search 키워드 필터가 동작하면서 Full-Text-Index가 쿼리에 적용되었고, 이로인해 정렬 컬럼의 인덱스가 누락되어 sort에 부하가 발생했습니다.

💡 Solution :

  • 필터링 조회 시 정렬 컬럼으로 인덱스를 사용하기 위해 키워드 검색은 contains문을 사용하였습니다.
  • 키워드만으로 검색이 이루어질 때는 Full-Text-Search가 동작 되도록 설정했습니다.

✔ 결과

  • 키워드에 따른 속도 편차는 발생하지만 평균 500ms로 성능의 안정화를 이루었습니다.
  • ( 약 11,900%의 성능향상 효과를 얻었습니다. )
📌 조회 쿼리 동작 시 cross join이 발생하여 성능 이슈가 발생했습니다.

❗ 문제상황

  • 조회 쿼리 동작 시 DB 로그에 cross join이 발생한 것을 확인했습니다.
    ( cross join 은 카다시안 곱을 수행하여 join하기 때문에 너무 많은 데이터를 가져와 성능이 저하됩니다. )

💡 Solution :

  • inner join 명시적으로 사용했습니다.
  • join을 명시적으로 사용하지 않은 쿼리문에서 자동으로 cross join이 발생되고 있었기 때문에 join이 필요한 테이블에 inner join을 작성하였습니다.

✔ 결과

  • cross join으로 나가던 쿼리문이 inner join 바뀌었습니다.
  • 200만건 기준 필터링 조회 시 평균 8초, 성능 200%까지 개선되었습니다.

🪨 기술적 챌린지

AWS 프리티어 인스턴스의 동작 최적화

✔ 아키텍처 및 애플리케이션 튜닝을 통한 여러 전략

📣 Step1. DB 읽기 전용 복제본을 생성해 Read 요청을 분산

❔원인

  • 하나의 DB에 많은 조회와 주문이 몰리면서 CPU에 병목이 발생

✔ 결과

  • Main DB에서는 Write 요청만 동작
  • 동일 상황에서 CPU의 안정화

image

📣 Step2. Hikari Connection Pool 최적의 개수 적용

❔원인

  • Cache Write Back 전략으로 조회수를 관리하고 있었기에 다중 DB Update를 위한 Hikari CP 확장이 필요했으나
    RDS micro.t3 인스턴스의 성능을 고려한 확장 필요

✔ 결과

  • Jmeter 부하테스트를 통해 에러율이 가장 낮아지는 Connection Pool Size가 20임을 발견

image

📣 Step3. Time_Wait 소켓의 최적화

❔원인

  • 낮은 성능의 DB로 대규모 상품 데이터를 핸들링하는 상황에서, 남아있는 모든 소켓에 요청마다
    TCP handshake가 발생하면서 불필요한 성능 낭비 발생

✔ 결과

  • KeepAlive 적용을 통해 매 요청마다 새로운 세션을 만들지 않고,
    1024개의 세션을 연결한 뒤 그 연결을 통해 요청을 처리하도록 설정

    image

🧑‍💻팀원


송제윤


계현준


윤수영

About

1000만 건 상품 데이터가 있는 의류 쇼핑몰

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published