Coder Social home page Coder Social logo

market-gola's Introduction

Market-Gola

로고

프로젝트 소개

신선식품을 판매하는 이커머스 서비스의 Rest API 서버를 만드는 프로젝트입니다.

사용 기술

  • Java 11
  • Spring 5.3.20
  • Spring Boot 2.7.0
  • Mysql 8.0.29
  • JPA 2.2.3
  • Redis 4
  • Github Actions
  • AWS S3, Lambda

프로젝트 중점 사항

  • 상황에 맞는 적절한 기술 선택
  • 서버 확장을 고려한 설계
  • 객체지향적이고 깔끔한 코드
  • 꼼꼼하고 가독성 높은 테스트 코드
  • Restful한 API

프로젝트 구조

프로젝트 구조도 1

주요 기능

  1. 유저 - 로그인, 회원 가입
  2. 상품 - 단 건 조회, 여러 건 조회, 수정, 삭제
  3. 주문

유저

UseCase

  • 사용자는 '회원 가입', '로그인', '로그아웃'을 할 수 있다.

고민

어떤 방식으로 로그인을 구현할까? JWT vs Session

보안

JWT는 탈취될 경우 Access Token이 만료되기 전까지 해커가 마음대로 접근 가능하다.
Session의 경우 Session을 만료시킴으로써 바로 접근 차단이 가능하다.
따라서 Session이 보안상 더 뛰어나다.

서버에서 상태 저장

JWT의 경우 사용성 문제로 Refresh Token을 같이 사용해야 한다.
따라서 서버에서 상태를 저장하는 단점은 Session과 같다.

성능

Scale-out 상황에서 상태 관리를 위해 글로벌 스토리지를 쓸 경우 Refresh Token은 가끔 접근해도 되는 반면, Session은 매 번 접근해야 한다.
따라서 네트워크 비용이 더 적은 JWT를 이용한 방식의 성능이 더 뛰어나다.

선택

JWT가 성능상 이점이 있다고 판단되지만 보안상의 문제를 커버할만큼 뛰어나게 성능이 좋은지는 확실하지 않다.
추후 성능 목표를 잡고 Session 방식으로 인해 목표 달성이 어렵다 느껴질 경우에 한해 JWT 방식으로의 전환을 고려해보면 좋을 것 같다.

로그인 체크 Filter vs Interceptor vs AOP

필터

  • 제외 경로를 지정하기 위해서는 추가적인 구현 코드가 필요하다.
  • 예외가 발생했을 때 ControllerAdvice에서 처리해줄 수 없어 일관된 예외 처리 하기가 어렵다.

AOP

  • URL Path 단위로 로그인을 체크하고 싶을 때 구현이 번거롭다. 직접 Request 객체로부터 URL를 꺼내고 작업해야 한다.

선택

위와 같은 문제가 없는 인터셉터를 사용한다.

Scale-out 상황에서 세션 관리를 어떻게 할까?

Sticky Session vs Session Clustering vs Global Session Storage

Sticky Session

서버 하나로 부하가 몰릴 수 있는 문제가 있다.

Session Clustering

서버가 늘어날수록 각 서버 간에 세션 동기화를 위한 네트워크 비용 또한 증가한다.
유저가 늘어날수록 유지하는 세션으로 인해 서버의 메모리가 부족해진다.
따라서 확장성이 떨어지는 문제가 있다.

선택

위와 같은 문제가 없는 Global Session Storage를 사용한다.
Global Session Storage의 경우 추가적인 컴포넌트로 인해 시스템이 복잡해진다는 문제가 있다.
어쩔 수 없는 부분이라 생각되고 '확장성', '추후 캐시 저장소로도 사용할 수 있다는 점'과 트레이드오프하자.

세션 저장소 Redis vs Memcached

후보 선정

세션은 영구적인 데이터 저장이 필요하지 않다. 따라서 In-Memoery DB를 활용하여 가능한 빠르게 데이터에 접근하는 것이 효율적이다.
또 세션은 key-value 형식으로 저장하기에 알맞다. 따라서 In-Memory DB 중 key-value 형태로 저장하는 Redis와 Memcached를 후보로 둘 수 있다.

세션 저장소로서 비교해 볼 포인트는 '성능'과 '장애' 관련 부분이다.

성능

둘 다 1ms 이하의 응답하며, 2016년 자료에 따르면 초당 10만개의 작업을 처리할 수 있다. 한 편 한번에 트래픽이 몰리게 될 경우 Redis는 응답속도가 불안정 해진다는 문제가 있다.

장애 복구

Redis는 장애가 일어나도 데이터를 보존할 수 있다. 예를 들어 RDB 방식으로 Redis 메모리 내에 있는 데이터들의 스냅샷을 찍어 디스크에 저장하거나, AOF 방식으로 현재까지 수행된 모든 Write 연산을 디스크에 저장하면 된다. 반면 Memcached는 데이터를 백업하는 기능이 없어 장애가 일어날 경우 모든 데이터가 사라지게 된다.

선택

세션 저장소로써의 각 장단점을 사용자 관점에서 생각해보자.
Memcached는 중간에 사용자가 사이트를 사용 중 로그인이 풀릴 수 있다.
만약 결제하는 과정이었다면 결제하는 사람의 신원을 알 수 없게 되어 결제에 실패할 수 있다.

반면 Redis는 대규모 트래픽 발생 시 여러 요청들이 간헐적으로 느리게 처리될 수 있다.

로그인이 풀리는 현상이 더 사용성이 좋지 못하다고 판단되므로 Redis를 세션 저장소로 사용한다.

세션 저장소에 저장할 LoginUser 객체를 어떤 방식으로 직렬화 해야할까?

자바 직렬화는 용량이 커진다는 문제가 있다.
Json 데이터에 비해 최소 2배가 커지고 Redis는 In-Memory DB로써 용량에 민감하다.
따라서 상대적으로 보편적이면서 용량 문제도 없는 Json 방식을 이용해 직렬화한다.

상품

UseCase

  • 관리자는 상품을 '등록', '수정', '삭제' 할 수 있다.
  • 사용자는 '하나의 상품을 조회' 할 수 있다.
  • 사용자는 카테고리 별로 '여러 상품을 조회' 할 수 있다.
  • 사용자는 가격, 신상품 순으로 '여러 상품을 정렬해 조회'할 수 있다.

고민

~외 4종과 같은 상품들은 어떤 구조를 만들어서 처리하지?

‘전시용 상품’(DisplayProduct)이라는 테이블을 새로 만든다.
그 후 '전시용 상품'과 '상품'(Product) 테이블을 일대다 관계로 맺어준다.
유저에게 보이는 상품은 '전시용 상품' 테이블에 들어 있는 상품들이며 실제로 구매하게 되는 상품은 '상품' 테이블에 있는 상품이 된다.

관련 글

만약 카테고리가 3,4,5뎁스로 많아진다면 어떡하지?

'전시용 상품' 테이블에서 '카테고리' 테이블을 따로 뺀 후 하나의 카테고리가 부모 카테고리와 연관되도록 자가참조 관계를 맺어주자.
새로운 카테고리가 추가된다면 '카테고리' 테이블에 새로운 카테고리를 추가하기만 하면 된다.
‘전시용 상품’ 테이블에 있는 각각의 상품은 뎁스 최하위의 카테고리만 갖고 있으면 '부모 카테고리 id' 필드를 통하여 연관된 모든 카테고리를 찾아낼 수 있다.

관련 글

상품 저장 도중 이미지 저장에 실패하면 저장된 이미지는 어떻게 롤백 처리를 하지?
(관련 코드 : https://github.com/sgo8308/Market-Gola-Batch)

주문

UseCase

  • 사용자 원하는 상품을 '주문'할 수 있다.

고민

여러 사람이 동시에 주문할 경우 동시성 문제를 어떻게 해결할까?

문제 상황

여러 명이 동시에 상품 주문을 진행하게 될 경우, 실제 주문량보다 적은 수가 재고에서 차감되는 문제가 발생할 수 있다. 이렇게 될 경우 주문에 성공한 사용자는 시간이 지나고 나서야 재고가 없다는 사실을 알게 되고, 이는 사용자 경험을 크게 떨어뜨린다.

원인

주문 로직을 수행하는 두 개 이상의 트랜잭션이 재고에 대한 데이터를 동시에 조회하는 것이 원인이다.

비관적 락

장점

  • 구현이 간단하다.

단점

  • 데드락에 걸릴 위험이 있고, 락을 걸고 해제하는 과정의 오버헤드로 성능 저하가 있을 수 있다.
  • DB Replication이 진행된 경우에는 사용할 수 없다. 서로 다른 DB에서 읽기를 진행할 수 있으므로.

낙관적 락

장점

  • 동시에 주문하는 경우가 적다면 락으로 인한 오버헤드가 없으므로 성능이 좋다.

단점

  • 동시에 주문하는 경우가 많을 때는 업데이트 실패 후 다시 시도하는 과정이 오래 걸리므로 성능이 떨어진다.
  • DB Replication이 진행된 경우 쓰기용 Master DB가 여러개라면 낙관적 락으로 문제를 해결하기가 어렵다.

분산 락

공통 장점

  • 락을 걸 대상이 존재하지 않을 때도 사용 가능하다.
  • 분산 DB 환경에서도 사용 가능하다.

MySQL의 네임드 락을 이용한 분산 락

장점

  • 추가적인 컴포넌트가 필요 없으므로 비용이 적고 시스템 복잡도가 낮다.

단점

  • 락 획득용 커넥션 풀을 따로 만들어야 하고 이 때문에 구현이 꽤 복잡하다.

Redis와 Redisson Client를 이용한 분산 락

장점

  • MySQL을 이용한 네임드 락보다 성능상 뛰어나다고 하지만 이 부분은 실제와 가까운 환경에서 성능 테스트를 통해 비교해봐야 할 듯하다.

단점

  • 추가적으로 Redis를 구축하고 운영하는 비용이 든다.
  • 락을 잡은 채로 영원히 놓지 않을 수 있기 때문에 타임아웃 시간을 잡아야 한다. 이로 인해 잠깐의 지연 문제로 락이 풀린 상태에서 그대로 로직을 진행하게 되면 동시성 문제가 여전히 발생할 수 있다. 이를 해결하기 위해 낙관적 락을 같이 써야할 수도 있다.

언제 어떤 것을 쓸까?

동시성 문제가 자주 발생하지 않는 상황
-> 성능상 가장 좋은 낙관적 락을 사용

동시성 문제가 자주 발생하는 상황
-> 간단한 프로그램이라면 비관적 락 사용
-> DB Replication할 정도로 큰 프로그램이라면 분산락 사용
-> Redis를 구축할 비용이 없고 성능적으로 요구사항이 크지 않다면 MySQL로 분산락 구현
-> 성능이 중요하다면 Redis로 분산락 구현

결론

서비스가 이제 시작하는 상황이라고 가정할 때 하나의 상품을 동시에 주문하는 경우는 매우 드물게 일어난다고 판단된다. 또 어느 정도 큰 서비스인 마켓 컬리에서도 초당 10개의 상품을 판매하고 있고, 이것을 전체 상품 갯수 1만개로 나누면 각 상품은 초당 0.001개가 판매되므로 동시성 문제가 자주 발생하지 않는 것으로 보인다.

따라서 동시성 문제가 자주 발생하지 않는 상황에서 성능이 좋은 낙관적 락을 사용하여 구현하자.

왜 운영 DB인 MySQL을 사용할 때는 낙관적 락 테스트 도중 무한 루프가 발생하지?

화면 설계

kakao oven

화면 설계

API 문서

swagger

api doc

DB ERD

설계 과정

ERD 최종

market-gola's People

Contributors

sgo8308 avatar f-lab-bot avatar jieun-dev1 avatar

Stargazers

 avatar GeonHee Park avatar 박진영 avatar Gyeongho Park avatar KAGSA avatar kim yoonho avatar . avatar YHT avatar  avatar

Watchers

James Cloos avatar  avatar

market-gola's Issues

단위 테스트 환경 구축하기

기능 구현 내용

테스트 시에는 DB에 값이 저장되지 않고 각각의 테스트가 독립적으로 반복해서 수행될 수 있도록 환경 구축하기.

Exception Handler 구현

기능 구현 내용

  • 요청 시 로직에서 예외가 발생하면 일관된 response가 내려가도록 하기
  • 서버에서는 예외가 발생할 때 로깅하기

회원 - 회원 CRUD

Use Case

  • 사용자가 서비스를 이용하기 위해서 '회원가입' 기능이 필요하다.
  • 사용자가 서비스 이용을 그만두기 위해 '회원탈퇴' 기능이 필요하다.
  • 사용자가 개인정보를 확인하거나 수정하기 위해 '개인정보 확인/수정' 기능이 필요하다.

상품 조회 - 베스트 탭

Use Case

  • 사용자는 많이 팔리고 후기가 많은 제품을 베스트 탭에서 조회할 수 있다.
  • 사용자는 판매량, 낮은 가격, 높은 가격, 신상품에 대해 정렬하여 조회할 수 있다.

상품 조회 - 카테고리

Use Case

  • 사용자가 전체 카테고리를 조회하고, 특정 카테고리 내의 (ex. 과일, 견과, 쌀) 세부 카테고리(제철과일) 별로 상품을 조회할 수 있다.
  • 사용자는 판매량, 낮은 가격, 높은 가격, 신상품에 대해 정렬하여 조회할 수 있다.

상품 조회 - 알뜰 상품 탭

Use Case

  • 사용자는 현재 할인 중인 상품을 알뜰 상품 탭에서 조회할 수 있다.
  • 사용자는 판매량, 낮은 가격, 높은 가격, 신상품에 대해 정렬하여 조회할 수 있다.

주문 기능 구현

Use Case

  • 사용자가 원하는 상품을 주문하기 위해 '주문하기' 기능이 필요하다.

회원 - 로그인, 로그아웃

Use Case

  • 사용자가 서비스를 이용하기 위해서 '로그인' 기능이 필요하다.
  • 사용자가 서비스 이용을 중지하기 위해서 '로그아웃' 기능이 필요하다.

상품 조회 - 검색

Use Case

  • 사용자가 검색 창에서 키워드로 제품을 검색하여 상품을 조회할 수 있다.
  • 사용자는 판매량, 낮은 가격, 높은 가격, 신상품에 대해 정렬하여 조회할 수 있다.

DB 설계 질문 관련

ERD 링크 : https://www.erdcloud.com/d/2QyezNQZi4XGgtqCS

질문

  1. 화면이 없더라도 조회, 정렬 등을 시험해보려며 더미 데이터를 넣어야 할텐데요. 대용량 트래픽에 최적화 된 설계라고 한다면, 데이터를 정말 몇천만개씩(?) 저장해야 할까요? 더미 데이터를 넣는 방법이 따로 있나요?

  2. 상세페이지에서 설명 + 사진이 있을 때 데이터 구조를 어떻게 설계하는게 좋을까요?
    우선 비슷한 사례를 활용해서 BLOB 데이터 타입을 넣었습니다. https://user-images.githubusercontent.com/46917538/68458221-9429fc80-0245-11ea-9cc3-92f7a35fd534.png

  3. 마켓컬리는 상품마다 두 가지 카테고리를 갖습니다. (ex. 과일, 견과, 쌀 → 수입 과일) Main 과 Sub Category 를 Product 에 칼럼으로 넣고, 추후 카테고리 별 상품 조회를 할 때, SubCategory나 Main 카테고리를 인자로 넣어서 검색/정렬하고자 합니다. 더 효율적인 방법이 있을까요?

  4. Discount rate은 변경점이 많을 수 있는 데이터입니다 (할인 정책은 자주 바뀌니까요). 현재 비율로 할인하는 방식이라서 Discount Rate 칼럼을 Product 테이블에 넣었는데요. 할인 방식이 바뀔 수도 있는데 이런 식으로 넣는게 유연한 설계일까요?

  5. 추후 고객이 자신이 결제한 내역의 상세 청구서(?) 를 보려면 아래 상세 데이터를 저장해두어야 할 것 같습니다. 설계할 때 조금 헷갈렸던 부분이어서 별도로 생각해볼 부분이 있다면 짚어주시면 감사하겠습니다.

  • 참고: 컬리는 45,000(?)원 이상 주문하면 배송비가 무료입니다. 제품 별 배송비는 다르지 않습니다.

전체 금액 (ex.10,000)
배송 금액(3000)
적립금 사용(1,000)
할인 금액 (2,000)
최종 결제금액 (10000)

  1. 컬리는 주문번호를 123456789 이런 식으로 고유값 9자리를 부여했는데요. 아마 랜덤 번호를 매긴 것 같습니다 1부터 시작해도 될 것 같은데 왜 굳이 이렇게 하는 지 잘 이해가 되지 않는데, 이렇게 저장해야 하는 이유/이점이 있을까요?

  2. User 테이블의 Password 는 암호화 된다고 할 때, Varchar 256으로 했는데 현업에서는 어떻게 하나요? 이전 프로젝트에서는 이보다 작은 사이즈였어서 아마 알고리즘 마다 다를 것 같아서요.

  3. user 테이블에서 유저의 id 혹은 이메일 같은 고유값이 있을 때는, id 생성전략을 안써도 될까요? 예전 프로젝트에서 배울 때, 이전 테이블에서는 모든 테이블을 기본키를 auto incremenet 로 생성하라고 배웠었습니다 (중복 확인 거친 id 같은 고유값이 있더라도)

주문하기 기능 무한 루프 문제

상세 내용

"동시에 주문을 하더라도 주문 수량과 차감된 재고가 정확히 일치한다."에 관한 테스트 진행시 테스트용 Embedded DB에서는 문제가 없으나, 실제 MySQL을 연결하여 테스트할 경우 무한 루프가 생기는 문제가 있음.

버그 재현 방법

  1. application-unit.yml의 설정을 test용 DB에서 MySQL로 변경
  2. OrderServiceTest에 createOrder_concurrent_order_ok() 메소드 실행
  3. 무한 루프 발생으로 인한 테스트 실패

상품 조회 - 신상품 탭

Use Case

  • 사용자는 신상품 탭에서 신상품을 조회할 수 있다.
  • 사용자는 판매량, 낮은 가격, 높은 가격에 대해 정렬하여 조회할 수 있다.

Redis를 세션 스토리지로 활용하여 로그인 구현하기

개요

서버에 세션 데이터를 저장하는 방식은 스케일 아웃 시 제대로 동작하지 않는다.
이를테면 로그인한 유저가 로그인 처리를 담당했던 서버가 아닌 다른 서버로 요청을 하게되면 로그인이 풀리게 된다.
외부 스토리지에 세션 데이터를 저장하여 스케일 아웃 시에도 로그인 기능이 제대로 동작할 수 있도록 한다.

상품 관리

Use Case

  • 운영자는 상품을 ‘등록’, ‘수정’, ‘삭제’, ‘조회’할 수 있다.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.