- 주제: 고객이 책을 검색하고 주문할 수 있는 인터넷 서점 개발
- 역할: 기본적인 CI/CD를 담당했고, 장바구니 기능과 주문 기능을 담당하여 개발했습니다.

- 모든 클라이언트의 요청을 앞단에 Nginx(80)를 두고, Nginx의 기본 로드 밸런싱인 라운드 로빈 방식으로 2개의 프론트 서버(8080 8081),에 순서대로 요청을 보내도록 설계했습니다. 각각의 프론트 서버는 백엔드 서버로 요청을 보내지만, 게이트웨이를 두어 모든 프론트 서버의 요청이 게이트웨이를 거치도록 설계했습니다. 이전까지는 아키텍처를 고려하지 않고, 기본적인 crud 기능을 구현하고 배포하는 방식에 머물렀지만, 이번 프로젝트를 통해 아키텍처를 설계해보고 깃허브액션을 사용하여 CI/CD를 구축하고 백엔드 서버는 블루그린 방식으로 무중단 배포도 적용해보는 경험을 했습니다.
- 실제 무중단 배포가 잘 작동하는지 테스트를 해보았지만, 문제가 발생했습니다. 분명 서버에서 배포 스크립트 실행까지 정상적으로 완료가 되었는데 게이트웨이에서 종료된 백엔드 서버에 요청을 보냅니다. 게이트웨이는 유레카 서버로부터 기본적으로 30초마다 서비스 목록을 내부적으로 캐싱하기 때문에 캐싱된 정보를 사용해서 최신화된 서비스 목록에 없는 죽은 서버로 요청을 보내는 문제가 발생합니다. 따라서 게이트웨이가 유레카로부터 최신화된 서비스 목록을 받고 라우팅 하기 위해서는 최대 30초의 시간이 필요합니다.
- 이를 반영하여 배포 스크립트에서 그린 서버의 상태가 UP 된 후 30초 대기하고, 블루 서버의 상태를 DOWN 시킨 후 30초 대기 후에 종료하도록 수정하여 게이트웨이가 유레카 서버로부터 최신화된 서비스 목록을 받아 라우팅하여 완전한 무중단 배포를 구현했습니다.

- 메인페이지 왼쪽에는 도서에 모든 최상위카테고리부터 최하위카테고리까지 선택할 수 있도록 하여 해당하는 카테고리의 책 목록을 보여주도록 개발했습니다.
- 하지만 실제 메인페이지를 요청할 때 수많은 쿼리가 발생했습니다.(패치 조인을 사용했지만 예상과 다르게)
최상위카테고리1 (rootCategory1)
├── 상위카테고리1 (category1)
├── 하위카테고리1 (subCategory1)
├── 하위카테고리2 (subCategory2)
최상위카테고리2 (rootCategory2)
├── 상위카테고리2 (category2)
- 위의 구조를 가지고, 테스트 코드를 작성하여 쿼리를 확인해본 결과 총 5개의 쿼리가 발생했습니다. 저는 패치 조인을 사용하여 최상위카테고리 목록과 모든 하위카테고리도 가져와서 당연히 단 1개의 쿼리가 발생할 것이라고 착각했습니다. 실제 패치 조인으로 한 단계까지만 가져오고, 더 깊은 하위카테고리는 가져오지 못하여 상위카테고리1에서 subCategories를 조회할 때 1개, 하위카테고리1에서 subCategories를 조회할 때 1개, 하위카테고리2에서 subCategories를 조회할 때 1개, 상위카테고리2에서 subCategories를 조회할 때 1개가 추가로 발생하여 총 5개(1+4)가 발생했습니다. 따라서 실제 데이터베이스에 존재하는 카테고리 구조에서 많은 쿼리가 발생한 것입니다.
- 이를 더 개선하기 위해, 배치 사이즈를 사용했을 때 총 4개의 쿼리가 발생했습니다. 배치 사이즈를 사용하면, 1 + 카테고리의 깊이만큼 쿼리가 발생합니다. 하지만, 카테고리의 구조에 따라 패치 조인 혹은 배치 사이즈를 사용하는 것이 더 좋다고 할 수 있습니다. 이 둘을 같이 사용하면, 패치 조인으로 최상위카테고리 → 1단계 하위카테고리를 가져오고, 배치 사이즈가 적용되어 1단계 하위카테고리 → 더 깊은 하위카테고리를 IN 쿼리로 최적화해서 가져오기 때문에 총 3개의 쿼리가 발생합니다. 따라서 1번째 쿼리로 최상위카테고리와 1단계 하위카테고리를 가져오고, 2번째 쿼리로 2단계 하위카테고리를 가져오고, 3번째 쿼리로 3단계 하위카테고리를 가져옵니다. 여기서 더 개선해보자고 생각했습니다..!
- 해결책은 바로 재귀 쿼리입니다. 단 한 번의 쿼리로 카테고리의 모든 계층 구조를 가져올 수 있습니다. 따라서 패치조인과 배치사이즈를 함께 사용하는 방식보다 훨씬 더 성능이 좋습니다.
- 결론적으로, 재귀 쿼리는 한 번의 쿼리로 전체 카테고리를 가져오기 때문에 성능이 더 좋고, 패치 조인과 배치사이즈는 추가적인 SELECT 쿼리가 실행되므로 프로젝트에서 사용하는 MySQL은 재귀 쿼리가 지원되는 데이터베이스이기 때문에 재귀 쿼리를 사용했습니다.
장바구니.mov
- 장바구니의 요구사항인 빈번하게 일어나는 장바구니 조회, 즉 성능을 고려해야 하기 때문에 클라이언트가 장바구니를 조회할 때 매번 MySQL 데이터베이스에서 장바구니 목록을 가져오지 않고 redis를 사용했습니다. redis에 session Id를 키로 사용해 도서 번호와 수량을 저장했습니다. 코드에서 보다시피 도서를 장바구니에 추가하면, 도서에 모든 정보를 저장하는 것이 아니라 도서의 번호와 수량 만을 저장하고 있습니다. 도서의 제목과 가격 등 추가적인 정보는 도서 번호를 key로 가지는 redis에서 조회하고, 없는 경우 db에서 정보를 가져옵니다. 이렇게 설계한 이유는 만약 session Id를 키로 사용해 책에 대한 모든 정보를 저장한다면, 한 책을 여러 클라이언트가 장바구니에 담을 수 있기 때문에 그렇게 되면 처음 장바구니를 조회할 때 같은 책에 대해 db에 여러번 접근해야 됩니다. 저는 이게 비효율적이라고 생각하여 장바구니에 담긴 도서 자체를 캐싱하여 여러 클라이언트가 같은 책을 장바구니에 담을 경우 db에 한 번만 접근하도록 구현했습니다.
- 만약 클라이언트가 장바구니에 상품이 담긴 채로 로그인을 성공하면, redis에 저장된 장바구니 데이터를 db에 동기화하고, 다시 db에 장바구니에 담긴 도서의 번호와 수량을 redis에 동기화하도록 구현했습니다.