Simple E-Commerce Web Application
2024.4. ~ 2024. 5.
URL : https://pjc1991.dev/
🔍 Summary
새로운 스택을 익히기 위해서 가장 단순한 형태의 웹 서비스를 다시 한번 처음부터 만드는 연습을 진행했습니다. NodeJs + ExpressJs를 이용한 웹 어플리케이션을 MongoDB와 통합해서 개발을 진행했습니다. 어플리케이션 구조는 심플하게 가져가면서도, 서비스 배포 방식 등을 다르게 가져가게 되었습니다.
이번 어플리케이션은 어디까지나 연습인만큼 실제 서비스를 목표로 하지 않고 개발하게 되었습니다.
💬 Main Works
- 배포 문제
- 익숙한 AWS 내의 가장 저렴한 서비스(EC2 혹은 Lightsail) 를 사용하여도 월 5달러 정도의 비용을 감안해야했습니다. 그러나, 어디까지나 연습 차원의 서비스를 구현하는데에 이런 비용을 지불할 필요는 없었습니다.
- Glitch 등의 유/무료 호스팅 서버를 사용하는 선택지도 있었다. 단 호스팅 서버 특유의 서버 자유도 문제가 있다는 점, 서버 엔지니어링에 대한 경험을 확보할 수 없다는 점에서 호스팅 서버는 사용하지 않기로 했습니다.
- 기존에 가지고 있던 저전력 SBC(Odroid N2+)를 이용해서 미니 서버를 구축하는 것으로 이 문제를 해결하기로 했습니다. 사용량을 생각하면 시간당 전력소모량은 3Wh 로 월 약 500원 정도의 비용이 예상되었습니다.
- 프라이빗 네트워크와 CI 문제
- Dedicated Server를 사용한 것으로 발생한 첫 번째 이슈로, 지속적 통합이 까다롭다는 문제가 있었습니다.
- Github Actions를 이용한 배포 자동화를 구현하려고 했으나, 네트워크상의 문제가 발생했습니다.
- Dedicated Server에 대해 Github Actions Runner 에서 접근할 방법이 없기 때문에 배포를 하는 것이 불가능했습니다.
- Runner 에서 SSH를 통해 Dedicated Server에 접속하여 배포를 진행하는 방법도 있었으나, Dedicated Server 자체의 보안 자체가 치밀하기 어려운 상황에서 외부 IP의 SSH 접근을 허용하는 것은 보안상 치명적으로 예상이 되었습니다.
- 고심 끝에 Self-hosted Runner를 Dedicated Server에 직접 추가해주는 방식으로 문제를 우회했습니다.
- 빌드/배포 자체를 서버에서 직접 처리하는 것으로 리소스는 추가로 소모하게 되었지만 큰 문제는 예상되지 않았고, 보안상 문제를 해결한 채로 쉽게 배포를 진행할 수 있었습니다.
- SSL 인증서와 관심사 분리
- 이 프로젝트는 결제 모듈을 포함하고 있었고, 보안상 인증서를 탑재하는 것이 바람직하다. 또한 연결하기 위해 준비한 도메인이 SSL을 필수 사양으로 요구하는 사양이었기 때문에 (.dev 도메인은 HTTP 접속이 불가능하고 HTTPS 접속만이 가능하다.) SSL 인증서를 탑재해야 했습니다.
- Dedicated Server 였기 때문에 Let’s Encrypt의 CertBot을 가동하는데에는 큰 문제가 없었습니다. 단, ExpressJs 서버에 어떤 식으로 SSL 연결을 할 지가 문제가 되었습니다.
- 일반적으로 NodeJs 서버에서 SSL 연결을 구현할 때에는 인증서 파일을 NodeJs FileServer로 읽는 방식을 많이 사용했습니다. 즉, 코드 내에 SSL 인증서에 관련된 내용을 추가하는 식으로 처리하는 것을 확인할 수 있었습니다.
- 겆얻뇌는 부분은 개발 환경에서는 위와 같은 코드가 작동하지 않는다는 것이었습니다. SSL 인증서가 실제로 존재하지 않기 때문에, 해당 코드에 대한 분기 처리 또는 예외 처리가 필요했습니다. 그리고 무엇보다 서버 환경에 해당되는 내용이 웹 어플리케이션의 코드에 작성된다는 부분이 관심사의 분리 면에서 부적절해보였습니다.
- 개발 환경에도 https 를 적용하는 방법도 있었습니다. 단, 이 경우에는 환경을 3개 이상 가지고 있었습니다. (Windows, Mac PC를 사용했고, Production 환경은 Ubuntu 였습니다.) 이 환경에 모두 https 를 적용하는 것은 배보다 배꼽이 커지는 일이기도 했고, 여전히 관심사의 분리 면에서 바람직해보이지 않았습니다. 또한 public repository 로 공개하고 있느니만큼 누구나 쉽게 로컬에서 구동할 수 있기를 바랐습니다.
- 따라서 나는 코드 내에 SSL 연동을 포함하지 않고, Nginx의 리버스 프록시를 통해서 SSL을 적용하는 방식을 택했습니다. 설정에 다소 시행착오가 있었지만, 문제 없이 localhost:3000 을 433 포트로 프록시하는데에 성공했고, 가지고 있는 도메인과의 연결도 문제없이 진행이 가능했습니다.
- 개발환경과 로컬 데이터베이스
- 이 프로젝트는 MongoDB의 사용을 필요로 했습니다.
- 개발 환경은 Kindergarten 이어야하기 때문에, local 구동에 MongoDB Cluster 를 필요로 하는 것은 바람직하지 않았습니다.
- MongoDB를 개인 PC에 설치하는 방법도 있었지만, 프로젝트 가동을 위해 DB 설치를 요구하는 것이 바람직하게 느껴지지 않았습니다. (여러 웹 어플리케이션을 구동하다보면 개인 PC가 각종 DB로 범람하게 될 것)
- Docker와 Docker-compose 를 이용해서 MongoDB Container 를 즉석으로 생성하는 것으로 문제를 해결했습니다.
- Document DB와 Category 문제
- 이번 프로젝트는 MongoDB를 이용해서 진행하게 되었는데, 기존의 관계형 데이터베이스와는 달리 조인이 불가능했습니다.
- MongoDB는 RDBMS에 비해 성능상 이점이 있고 scale up을 하기 쉽다는 장점이 있었지만 도큐멘트간의 관계성을 추론하기 어려운 문제가 있었습니다.
- 이번 프로젝트에서는 학습을 목적으로 MongoDB를 선택하여 사용했습니다.
- 대부분의 기능은 문제없이 진행할 수 있었는데, 카테고리 설계에 어려움이 존재했습니다.
- Product 엔티티에 Category를 내장시키는 방식을 택하면 다수의 상품을 한번에 가져올 때도 문제없이 처리할 수 있었습니다.
- 이 경우 Category의 수정을 모든 Product에 반영하는 것이 난감해지는 문제가 있었습니다.
- Category를 별개 테이블로 분리하고, Product의 해당 카테고리의 Id를 내장하는 방식을 선택할 수도 있었습니다.
- 이 경우에는 카테고리의 수정에는 문제가 없지만, 다수의 상품을 한번에 가져왔을 때 Category의 값을 Populate 하기 위해 많은 쿼리를 실행해야하는 문제가 있었습니다.
- 최종적으론 절충안으로 categoryId와 category의 노출되는 항목(주로 카테고리 명)을 같이 product에 내장시키는 방식을 택했습니다.
- categoryId가 내장되어있으므로, category 에 update가 일어날 때 모든 카테고리의 값을 수정할 수 있습니다. 내장된 category의 값들은 category에 update가 일어날 때마다 product에 다시 update가 필요했지만, category의 update는 product나 category의 read 보다 훨씬 적은 빈도로 발생했습니다.
- category 값들을 가지고 있으므로 따로 populate 할 필요없이 다수의 product를 가져와 바로 사용하는 것이 가능했습니다.
const productSchema = new Schema({
title: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
/*... ...*/
category : {
categoryId: {
type: Schema.Types.ObjectId,
ref: 'Category',
required: true
},
categoryName: {
type: String,
required: true
}
},
/*... ...*/
🤹Used Stack
Node.js, Express.js, EJS template, HTML, Javascript, MongoDB, Docker, Docker compose, Mongoose