주제 : 유저 간 물물교환을 쉽고 편리하게 할 수 있도록 도와주는 사이트입니다!
당장 구매 할 현금은 없지만 내가 있는 물건과 내가 원하는 물건을 교환하고 싶을 때!
상대방이 내 물건보다 더 가치있어서 추가금을 지불해서라도 바꾸고 싶을 때!
당신에겐 필요없던 물건, 스왑으로 빛내보세요!
- 대용량 선착순 쿠폰 발급 기능
- Web Socket / Stomp를 이용한 채팅 기능
- SSE를 이용한 알림 기능
- LogBack을 이용한 배포환경 로그 파일 저장
선착순 쿠폰 발급에 따른 동시성 제어
먼저 저희 프로젝트의 선착순 쿠폰 발급에 대한 요구사항은 아래와 같습니다.
먼저 들어온 요청이 먼저 처리되야 한다.
위 요구사항을 만족하기 위해서 동시성 문제를 해결해줄 3가지 문제점을 모두 적용해보았습니다.
낙관적 락은 충돌이 일어나지 않을 것이라 예상하고 어느 정도(변경 감지라면 트랜잭션 끝날 때까지) 코드를 진행 후, 업데이트가 실행될 때, 기존의 조회 버전과 다르다면 예외가 발생하기 떄문에 쓸데없는 코드 실행을 발생시킵니다. 또한, 쿠폰 발급이 성공할 때까지 무한 재시도 로직을 작성해 주어야 합니다. 심지어 쿠폰 발급에 성공할 수 있을지도 모릅니다.
비관적 락은 조회 시점부터 X락을 걸기 때문에 뒤에서 락을 얻기 위해서 락 경합을 하는 쓰레드들에 대해서 별도의 재시도 로직이 필요없습니다. 그래서 보기에는 낙관적 락보다 편해 보이지만 데이터베이스의 자체에 락을 사용하는 것이기 때문에 성능면에서 높은 이득을 보긴 힘들다고 생각하고 낙관적 락보다 속도면에서도 우수하다고 볼 수 없다고 생각했습니다.
그리고 결국 위 두가지 방법 모두, 처음에 얘기한 먼저 들어온 요청이 먼저 처리되어야 한다. 라는 요구사항을 만족할 수 없습니다. 예를 들어낙관적 락의 경우, 어떤 두 요청이 동시에 온다면 성공하지 않은 나머지 요청은 그 다음으로 성공하길 기대하지만, 언제 성공할 지 모르기 때문입니다. 또한, 비관적 락의 경우도 락 경합때문에 락을 얻기 위해 기다리는 쓰레드들 중 어느 쓰레드가 먼저 락을 얻을지 알 수 없기때문에 마찬가지입니다.
때문에 위 두가지 방법보다 성능면에서도 우수하고 완벽하진 않지만 요구사항을 만족해줄 수 있는 레디스를 최종적으로 선택하였습니다. 레디스의 Incr()를 이용해 조회와 쓰기를 원자 단위로 가져감으로써 동시성에서 발생할 수 있는 문제점을 배제하고 성능을 높일 수 있었습니다.
채팅 메시지를 영속화할 DB 선택
채팅방 목록을 보여줄 때 채팅방의 마지막 메시지, 마지막 메시지의 시간을 보여주어야 합니다. 이런 요구사항을 만족하기 위해서는 매번 메시지가 전송될 때마다 트랜잭션을 열고 채팅방을 업데이트 해주어야 하는데 이러한 작업은 성능 문제를 야기할 수 있습니다.
또한 채팅 메시지 데이터는 사용자가 많아지고 서비스가 활성화 될 수록 더 많은 데이터가 쌓이게 됩니다. 그래서 수평적 확장 측면을 중요하게 생각하게 되었고 RDBMS는 NoSQL에 비해 수평적 확장이 어렵기때문에 NoSQL을 사용하게 되는 요인 중 하나가 되었습니다.
RDBMS에 비해 수평적 확장이 용이하도록 설계되어 있습니다. 데이터 양이 증가하면 서버를 추가함으로써 시스템의 확장이 가능하며, 이는 높은 트래픽 및 대용량 데이터 처리에 유리합니다.
NOSQL 데이터 베이스는 특히 쓰기 작업에 있어서 빠른 성능을 제공하는데 이는 메시지를 보낼 때마다 데이터 베이스에 저장을 해야하는 저희 서비스의 요구사항에 맞는 데이터 베이스 입니다.
{
"roomId":"65b8d7ed9b50660d9cf98b26",
"senderId":1,
"type":"CHAT",
"text":"채팅입니다."
}위처럼 JSON 형태로 넘어온 데이터를 쉽게 처리할 수 있는 MongoDB를 선택했습니다.
채팅 내역이 쌓임에 따라 collection의 크기가 커지면 읽고 쓰기에 시간이 소요될 수 있으나 MongoDB가 제공하는 Sharding과 기능을 사용하여 사용하여 대용량 데이터를 분산하여 저장할 수 있습니다.
외부 서비스 API 비동기 처리
- PostService 에서 DeletePost 를 진행 하던 중 문제가 생겨 롤백
- Post는 트랜잭션에 걸려 롤백
- S3 객체는 외부 리소스여서 트랜잭션을 타지 않기에 삭제가 되어 롤백이 안된다.
- 회원 입장에서 롤백된 게시글에서 이미지를 찾을 수 없어진다.
- 이미지를 중요시하는 물물교환 서비스에서 오류가 난 이미지를 사용자에게 보여주면 서비스에 대한 이미지 훼손
- Post는 트랜잭션에 걸려 롤백
- 만약 S3가 아닌 다른 외부 리소스를 사용하는데 이미지 삭제하는데에 오래 걸린다면?
- 회원 입장에서 Post가 삭제되는 것이 중요한 것이지, S3 객체가 사라지는 것이 중요한 것이 아니다.
- 만약 이미지 삭제 로직이 오래 걸린다면 사용자가 이걸 기다려야 할까?
ApplicationEventPublisher를 사용하고 deletePost 메서드의 트랜잭션 환경이 문제없이 끝나면 S3 이미지 삭제 메서드를 비동기 형식으로 처리 한 다음 사용자에게 먼저 게시글 삭제 완료 됐다고 반환
커서 기반 페이지 네이션
데이터가 100만건이 증가할때마다 응답시간이 늘어나는 것을 볼 수 있습니다.
실제로 테스트를 해보기 위해 더미데이터를 생성하고 PostMan으로 테스트를 진행 해봤습니다.
오프셋 기반 페이지 네이션의 두번째 문제는 데이터 중복 조회 문제입니다.
게시글 목록을 보고 있다가 다음 페이지로 넘기기 전에 누군가가 새로운 게시글을 쓰면 중복된 게시글을 조회해본 경험 다들 있으실텐데요, 저희 서비스는 전국적으로 글을 올릴 수 있는 서비스이기에 이런 문제가 빈번히 일어날 수 있습니다.
일반적으로 어떤 유저가 도대체 100만 페이지가 넘게 보나? 싶지만 예외가 있습니다. 쿼리 파라미터로 유저가 직접 offset을 넣어서 검색할 수도 있고, 추후 엘라스틱 서치 도입 시 게시글을 서치해야하는데 이때 100만 건이 넘는 곳을 서치할 수도 있습니다. 그리고 악의적인 사용자가 디도스로 100만 페이지 넘는 곳으로 계속 공격을 한다면? 서버가 금방 망가질 수도 있습니다.
그래서 커서 기반 페이지 네이션을 적용했습니다.
사용자 알림 기능 기술 선택
고민을 하다가 SSE로 선택하였는데 가장 큰 이유는 저희 웹 애플리케이션에서의 상황을 생각하였을 때 이벤트(거래요청, 채팅)가 발생하면 사용자는 별다른 HTTP 요청을 할 필요없이 일방적으로 서버로부터 알림만 받으면 되었기에 양방향으로 통신을 할 필요가 없었습니다.
SSE는 HTTP프로토콜만 사용하여 비교적 가볍고 애플리케이션 특성상 빈번하게 알림이 발생할 수 있는 상황임에도 처음 한번만 연결하면 재연결 과정이 필요가 없어 서버의 부하를 줄일 수 있습니다. 그리고 이벤트들을 비동기적으로 전송하기 때문에 실시간성이 좋아 사용자 경험을 향상시킬 수 있습니다.
| 손창현 (지갑, 쿠폰) | 조원호 (게시글, 로그) | 차우빈 (거래) | 우성현 (유저, 알림) | 문기현 (채팅) |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |













