장바구니 발표
NHN Academy 인증과정 프로젝트 발표를 팀에서 대표로 ‘장바구니’ 라는 주제를 맡아 발표하였다.
나에게 되게 뜻깊고 좋은 경험이었기에 그에 대한 내용을 적어보려 한다.
해당 글은 노션을 통해 작성하였습니다.노션 링크
장바구니 구현 과정
1차 설계 && 구현
처음 장바구니에대해 구현 목표는 MySQL을 사용하여 구현 하는 것이었다.
그 이유로는 회원의 장바구니 데이터를 영구적으로 저장하고자 하였다.
팀원들과 ERD 설계와 구현에 대한 이야기를 나누었을 때 무신사, 쿠팡 등 실제 사용하고있는 쇼핑몰을 예시로 들어
회원의 장바구니가 영구적으로 저장되었으면 한다고 이야기를 나누었고, 나 또한 그에 맞는 구현을 하고 싶었다.
또한, 어느 한 인터넷 기사에서 사용자의 소비 패턴을 분석한 글을 보았는데 장바구니를 찜기능과 같이 사용하는 사용자들이 매우 많고,
당장은 구매하지 않지만, 장바구니에 담아두거나 하는 행위가 매출로 이어진다는 글이었다.
이는 즉, 실제 운영에 있어 큰 영향을 미칠 것이라 생각하였다.
그러나, 비회원의 경우 DB에 저장하기에는 무리가 있었다.
그렇기에, 비회원의 경우에는 Redis를 사용하고 회원의 경우 MySQL을 사용하는 방식으로 구현을 시작했다.
⚠️ 문제점
- 모든 코드에 회원과 비회원을 구분짓기 위한 분기문 코드가 작성되어야 했다.
- 비회원에게 비싼 자원인 Redis를 사용하고 회원에게는 사용하지 않는다는 점이 존재한다.
- 해당 구현을 통해 얻어지는 이점이 명확하지 않음.
- ‘왜?’, ‘굳이?’ 라는 의문점이 들게 하는 구현 방식이였음.
1차 구현에 대한 코드
@PostMapping("/product/{productNo}")
public boolean productAddToCart(@AuthenticationPrincipal Object principal,
@CookieValue(value = COOKIE_NAME)Cookie cookie,
@Pathvariable(value = "productNo")Integer productNo) {
if (principal instanceof UserDetailsDto) {
...
return cartService.addProductMemberToCart(cartMemberRequestDto);
}
...
return cartService.addProductAnonymousToCart(cookie.getValue(), productNo);
}
@DeleteMapping("/product/{produtNo}")
public void productDeleteToCart(@AuthenticationPrincipal Object principal,
@CookieValue(value = COOKIE_NAME) Cookie cookie,
@Pathvariable(value = "productNo") Integer productNo) {
if (principal instanceof UserDetailsDto) {
...
cartService.deleteProductMemberToCart(cartMemberRequestDto);
return;
}
...
cartService.deleteProductAnonymousToCart(cookie.getValue(), productNo);
}
2차 구현
그렇기에, 구현방식을 바꾸고자 하였다.
회원과 비회원 모두 장바구니 데이터를 Redis을 이용해 관리하고,
회원의 경우 로그아웃 할 경우 Redis에 존재하던 장바구니 데이터를 MySQL DB에 옮겨 저장하는 방식으로 구현을 하였다.
이러한 구현방식은 앞서 말한 문제점들을 해결 할 수 있었고,
쇼핑몰의 특성상 특정 시간대에 많은 이용자가 몰리게 되고,
이는 순간적으로 DB에 많은 상품 정보가 담기게 되고 DB에 부담을 주는 부분을
Redis에 장바구니 상품정보를 캐싱하여 사용할 수 있다는 이점도 존재하였다.
그리고, 회원이 로그인 하였을 경우에는
로그인 이전에 담아두었던 데이터와 DB에 존재하던 데이터를 병합하는 방식으로 관리하였다.
또한, 회원이 정상 로그아웃 하였을 경우에는
Redis에 존재하던 장바구니 데이터를 DB에 옮겨 저장하는 방식으로 구현을 하였다.
이를 통해 아래와 같은 플로우를 구현할 수 있었다.
그러나, 사실 정확하게 말을 하면 Write Through 패턴이 아닌 Write Back 패턴을 구현 하게 된 것이었다.
Write Through Patter
데이터베이스와 Cache에 동시에 데이터를 저장하는 전략
데이터를 저장할 때 먼저 캐시에 저장한 다음 바로 DB에 저장
캐시와 DB에 업데이틀 같이 하여 데이터 일관성을 유지할 수 있어서 안정적
단점으로는 매 요청마다 두번의 Write가 발생함으로 성능적인 이슈가 발생 할 수 있음*Write Back Patter *
데이터를 Cache에 모아서 관리 후 일정 주기 작업을 통해 DB에 저장
캐시와 DB 동기화를 비동기하기 때문에 동기화 과정이 생략
캐시에 모아놨다가 DB에 쓰기 때문에 쓰기 쿼리 회수 비용과 부하를 줄일 수 있음
단점으로는 캐시에서 오류가 날 경우 데이터가 유실 될 수 있고, 캐시와 DB의 값이 서로 다른 상황이 발생 할 수 있다.
이렇듯, Write Back 패턴을 이용하여 장바구니를 구현하였다.
그러나, 이 부분에 대해서도 문제가 존재하였다.
바로, 회원의 비정상 로그아웃의 경우에 Redis에 존재하던 장바구니 데이터를 어떤 방식으로 DB에 저장해야 되는지에 대한 고민이 생겼다.
처음 생각한 방법으로는 Scheduler를 이용하여 Redis에 저장해둔 Key의 prefix를 통해 찾아내어 Expiration Time을 체크 한 후 저장하는 방법이었다.
그렇게 된다면, 비정상 로그아웃을 하여 만료되기전에 Redis에서 관리되던 데이터를 성공적으로 찾아내어 DB에 옮겨 저장 할 수 있다고 생각하였다.
이러한 생각을 갖고 구현을 하였다.
@Scheduled(cron = "0 0 0 * * *")
public void saveAllCartByRedis() {
Set<String> keys = redisTemplate.keys("CID=*");
...
keys.forEach(
key -> redisHashMapList.add(redisTemplate.opsForHash().entries(key))
);
...
cartAdaptor.saveAllCart(list);
}
이를 통해, 비정상 로그아웃 한경우에 Redis에 존재하던 데이터를 성공적으로 DB에 옮겨 저장 할 수 있었다.
⚠️ 문제점
그러나, 위의 구현에는 큰 문제점이 존재했다.
바로, Redis에 존재하던 모든 Key를 스캔해야 한다는 점이었다.
Redis의 공식문서에 따르면 운영환경에서의 데이터 풀 스캔은 지양하고 있었다.
또한, Redis는 싱글 쓰레드로 동작하기 때문에 데이터 풀 스캔 도중
다른 요청에 대한 처리를 할 수 없게되고, 이는 동시에 Redis에 작업이 들어오게 될 경우 문제가 될 것이라고 판단 하였다.
2차구현에 대한 개선
그렇기에 다른 방식을 고려하게 되었다.
그렇게 고민을 하던 중, Redis의 데이터가 만료될 때 어떠한 메세지를 얻을 수 없을까라는 고민을 하게 되었다.
그러던 중, Redis에서 제공하는 기능인 Pub/Sub 기능에 대해 알 수 있었다.
Redis Pub/Sub 이란?
이벤트(메세지)를 발생하는 Publisher가 존재하며, Publisher는 특정 Channel(혹은 Topic)에 이벤트를 전송한다.
특정 Channel(혹은 Topic)을 구독하는 Subscriber가 존재하며, Publisher에 관계없이 **발행된 이벤트를 받을 수 있다.
**
Redis의 Channel은 말 그대로, TV의 Channel을 생각하면 된다. 하루 종일 TV에서는 수백 개의 채널에서 방송이 방영된다. 각 방송사(Publisher)에서 방영하는 라이브 방송은, 해당 채널을 시청 중일 때만 볼 수 있다. 또한 같은 시간대에 같은 채널의 시청자(Subscriber)들은 모두 같은 방송을 볼 수 있다.
그리고, Spring Data Redis 에서 해당 기능을 추상화 해두어 손쉽게 사용이 가능하다는 것을 알게 되었다.
해당 클래스로 인해 Redis의 데이터가 만료될 때 해당 데이터에 대한 메세지를 얻을 수 있었다.
그런데, 해당 데이터에 대한 Key값만 얻을 수 있을 뿐 안에 존재하는 데이터는 얻을 수 없다는 문제가 존재했다.
해당 기능은 데이터가 만료됨과 동시에 메세지를 보내줌으로, Redis는 데이터가 만료됨과 동시에 그 안에 존재하던 데이터도 함께 사라지기 때문이었다.
이러한 문제를 해결하기 위해 고민을 통해 본래 데이터 보다 먼저 만료되는 가데이터를 삽입하여,
해당 가데이터가 본래 데이터를 가르키게 만들어 본래 데이터를 얻어오는 방식을 생각해 내었다.
따라서, 본래 데이터의 Key에 Phantom
이라는 접미사가 붙은 가데이터를 회원이 로그인 하였을 경우 함께 삽입하였다.
해당 가데이터는 본래 데이터보다 5분 더 일찍 만료되어 이때 Pub/Sub 을 통해 얻어지는 메세지를 이용하여 본래 데이터를 찾아와 DB에 옮겨 저장하는 방식으로 구현을 마무리 하였다.
이를 통해 앞서 이야기한 데이터 풀 스캔 문제를 해결할 수 있었고, 처음 목표했던대로 구현을 끝 마칠 수 있었다.
여기까지는 구현에 대한 이야기 였고 이제는 발표에 대한 이야기를 풀어보려고 한다.
팀내 발표 주제선정
프로젝트를 마무리하고, 각 팀마다 발표 주제를 선정하요 판교 NHN 본사에 가서 발표를 진행하는 마지막 절차가 남아 있었다.
팀내에서 어떤 주제를 선정할지 고민을 하던 중,
책임님께서 해당 구현 내용이 나쁘지 않고 현업과 되게 유사하게 구현을 하였다고 말씀해주셨고, 해당 내용으로 발표를 진행해도 좋겠다고 말씀하셨다.
또한, 각 팀마다 발표 주제가 곂치지 않게 선정해야 했던 만큼 다른 팀들과의 차별점도 존재해야 했기에,
이러한 방식으로 구현한 팀은 우리팀밖에 존재하지 않았고, 팀원들과의 회의를 통해 발표를 담당하게 되었다.
발표는 대학교 때에도 자주 접해보지 못한 경험이었고, 해당 발표자리에는 NHN 현직자 분들 뿐만 아니라, 다른 협력사의 현직자, 채용 담당자 분들도 오시는 자리였기에 책임도 크게 느껴졌고, 긴장도 많이 되었다.
그렇기에 최대한 준비를 열심히 해보자 생각하여 아래와 같이 PPT도 끊이 없이 수정을 해 나아갔으며,
대본또한 준비하여 열심히 숙달하며, 내가 발표하는 내용에 대해 빠짐없이 숙지하려고 노력했다.
발표 당일
준비를 열심히 잘 한것인지, 혹은 순간의 각성효과인 것인지 모르겠지만
발표 당일 생각보다 긴장을 덜 하게되어 잘 마무리 할 수 있었다.
하지만 이번 발표에 대해서 아쉬운 점은 분명히 존재하였다.
내가 발표한 내용이 현직자 분들께는 너무 당연한 내용일 수 있어서 질문과 관심을 크게 받지 못한것 같다는 생각이 들었다.
그렇지만, 해당 발표의 준비부터 많은 사람들의 앞에 서서 내가 구현한 내용과 내 생각들을 전달할 수 있는 발표를 경험했다는것은 되게 뜻 깊었고,
바꿔 생각하면, 신입으로써 현직자 분들이 당연하다고 생각할 정도의 구현을 해내었다는것이 어떻게 보면 나에게는 인정 아닌 인정을 받았다고 생각 해 볼 수 있었다.
이로써 하나의 뜻 깊은 경험을 쌓을 수 있었고 이러한 기회를 제공해준 NHN Academy 그리고, 약 3달의 기간동안 함께했던 팀원들에게 모두 감사하다.
'1.프로그래밍 > 개발' 카테고리의 다른 글
헤이즐 캐스트 (Hazelcast) 란? (0) | 2024.10.24 |
---|---|
[NHN Academy] NHN Academy 인증 과정 (9) | 2023.03.24 |
[JWT] JWT 토큰이란? 토큰 기반 인증 방식 (0) | 2023.01.16 |
[Architecture] 모놀리식 아키텍처와 마이크로서비스 아키텍처란? (0) | 2023.01.07 |
[CI/CD] CI/CD란? (0) | 2023.01.07 |