JHParrrk/book_market 프로젝트 기술 보고서
1. 보고서 개요
본 문서는 JHParrrk/book_market 프로젝트에 적용된 개인적으로 학습하거나 발전시킨 핵심 기술과 설계 패턴을 분석합니다. 분석은 크게 데이터베이스 설계 및 활용, 견고한 아키텍처, 보안 및 안정성, 그리고 유연한 API 설계의 네 가지 관점에서 진행됩니다.
2. 데이터베이스 중심의 문제 해결 및 최적화
복잡한 비즈니스 로직을 어플리케이션 코드가 아닌, 데이터베이스의 고급 기능과 최적화된 설계를 통해 해결하여 성능과 효율을 모두 확보하려고 노력하였습니다.
2.1. 재귀 CTE를 활용한 계층 구조 데이터 탐색
도입 이유
categories 테이블은 parent_id를 통해 스스로를 참조하는 자기 참조(Self-Referencing) 관계로 설계되었습니다. 이와 같은 설계는 하위 카테고리를 표현하기 위해 별도의 테이블을 다수 생성하는 복잡함을 방지하고, 단일 테이블로 계층 구조를 표현할 수 있도록 합니다.
그러나 이러한 구조에서는 특정 상위 카테고리(예: '국내도서')를 선택했을 때, 그에 속하는 모든 하위 카테고리의 데이터를 탐색해야 하는 상황이 자주 발생합니다. 만약 이러한 탐색을 어플리케이션 레벨에서 반복적으로 쿼리를 실행하여 처리한다면, 데이터베이스와의 다중 통신으로 인해 성능 저하가 발생할 수 있습니다.
이 문제를 해결하기 위해, 데이터베이스의 재귀적인 계층 탐색을 지원하는 CTE(Common Table Expression) 기능을 학습하고 도입하였습니다. CTE를 활용하면 단일 쿼리로 계층 구조를 효율적으로 탐색할 수 있어, 성능을 크게 향상시킬 수 있습니다.
실제 사용된 코드 (modules/books/book.repository.js)
const applyCategoryFilter = (category_id, params) => {
if (!category_id) return "";
params.push(category_id);
return ` AND b.category_id IN (
WITH RECURSIVE CategoryTree AS (
-- 1. 시작점: 사용자가 선택한 최상위 카테고리
SELECT id FROM categories WHERE id = ?
UNION ALL
-- 2. 재귀: 직전 단계에서 찾은 카테고리를 부모로 하는 자식들을 반복 탐색
SELECT c.id FROM categories c
JOIN CategoryTree ct ON c.parent_id = ct.id
)
SELECT id FROM CategoryTree
)`;
};
2.2. 단계별 실행 흐름 (Execution Flow)
1단계 카테고리가 "국내도서"이고 이는 category_id = 1을 가졌을 때, 이를 기준으로 탐색하는 경우를 예로 들어 설명합니다.
- 1단계 카테고리: "국내도서"는 최상위 카테고리로,
id = 1이고parent_id = NULL입니다.
그 밑으로 2단계 카테고리는 parent_id = 1인 하위 카테고리들이 있으며, "소설"과 "경제/경영"이 속해 있습니다.
- 2단계 카테고리:
- "소설"은
id = 2이고,parent_id = 1입니다. - "경제/경영"은
id = 3이고,parent_id = 1입니다.
- "소설"은
그 밑으로 3단계 카테고리는 각각 parent_id = 2와 parent_id = 3인 하위 카테고리들이 있으며, "현대소설"과 "마케팅"이 속해 있습니다.
- 3단계 카테고리:
- "현대소설"은
id = 10이고,parent_id = 2입니다. - "마케팅"은
id = 15이고,parent_id = 3입니다.
- "현대소설"은
앵커 Member 실행
SELECT id FROM categories WHERE id = 1이 실행됩니다.
CategoryTree의 임시 결과 집합은{1}이 됩니다.
재귀 Member 첫 번째 실행
JOIN CategoryTree ct ON c.parent_id = ct.id쿼리가 실행됩니다. 현재ct.id는1입니다.... WHERE c.parent_id = 1과 동일하게 동작하여parent_id가 1인 자식 카테고리들(예: '소설'(ID: 2), '경제/경영'(ID: 3))을 찾습니다.- 이번 반복의 결과로
{2, 3}이 생성됩니다. UNION ALL에 의해CategoryTree의 임시 결과 집합은{1, 2, 3}으로 확장됩니다.
재귀 Member 두 번째 실행
- 다시
JOIN CategoryTree ct ON c.parent_id = ct.id쿼리가 실행됩니다. 이번에는 이전 반복의 결과인{2, 3}을ct.id로 사용합니다. ... WHERE c.parent_id IN (2, 3)과 동일하게 동작합니다.parent_id가 2인 자식 카테고리(예: '현대소설'(ID: 10))와parent_id가 3인 자식 카테고리(예: '마케팅'(ID: 15))를 찾습니다.- 이번 반복의 결과로
{10, 15}가 생성됩니다. UNION ALL에 의해CategoryTree의 임시 결과 집합은{1, 2, 3, 10, 15}로 확장됩니다.
종료 조건
- 이 과정은 재귀 Member가 더 이상 어떠한 행도 반환하지 않을 때(즉, 더 이상 하위 카테고리가 없을 때)까지 반복됩니다.
기대 효과
- 성능: 여러 번의 DB 통신 없이 단 한 번의 쿼리로 복잡한 계층 구조를 탐색하므로 속도 향상
- 효율성: 데이터베이스가 가장 잘하는 일(데이터 집계 및 관계 탐색)을 DB에 위임
- 유지보수성: 카테고리 깊이가 아무리 깊어져도 코드 수정 없이 동적으로 처리
2.3. 테이블 분리를 통한 조회 성능 최적화
설계 의도
메인 페이지의 도서 목록처럼 간단한 정보를 표시할 때, book_details의 상세 설명(description)과 같은 무거운 데이터를 함께 조회하는 것은 불필요한 부하를 유발합니다. 이를 해결하기 위해 테이블을 books와 book_details로 분리했습니다.
books테이블: 목록 표시에 필요한 핵심 정보(제목, 저자, 가격, 이미지 URL 등)를 저장book_details테이블: 상세 페이지에서만 필요한 무거운 정보(상세 설명, 목차, ISBN 등)를 저장
이러한 정규화(Normalization)를 통해, 일반적인 목록 조회 시에는 가벼운 books 테이블만 사용하므로 응답 속도를 크게 향상시킬 수 있었습니다.
3. 견고한 아키텍처: 3-Tier 아키텍처 기반 설계
Controller - Service - Repository로 역할을 명확히 분리하여 코드의 유지보수성과 확장성을 높였습니다. 이는 향후 Nest.js와 같은 프레임워크로의 전환을 염두에 둔 전략적인 설계입니다.
3.1. 계층별 역할 분리 (주문 생성 기능 예시)
order.controller.js(표현 계층)- 역할: HTTP 요청/응답 처리 및 데이터 유효성 검증.
order.service.js(비즈니스 계층)- 역할: 핵심 비즈니스 로직 처리.
order.repository.js(데이터 접근 계층)- 역할: 데이터베이스 통신(CRUD), 트랜잭션 처리.
3.2. Nest.js 도입을 위한 초석
이번 프로젝트에 3-Tier 아키텍처를 적용한 이유는, 궁극적으로 Nest.js 프레임워크 도입을 목표로 하고 있기 때문입니다. 현재의 구조는 Nest.js의 핵심 아키텍처 패턴과 매우 유사합니다.
- Controller ↔ Nest.js의
Controller - Service ↔ Nest.js의
Provider(Service) - Repository ↔ Nest.js에서
TypeORM같은 ORM의Repository패턴
이처럼 명확한 역할 분리를 미리 경험하고 구조를 정립함으로써, 향후 Nest.js로 마이그레이션할 때 발생할 수 있는 러닝 커브를 최소화하고, 보다 빠르고 안정적으로 프레임워크를 도입할 수 있는 기반을 마련했습니다.
4. 유연하고 효율적인 API 설계
상태 변화를 나타내는 '플래그 변수'를 적극적으로 활용하여, 단일 엔드포인트에서 다양한 시나리오에 대응하고 코드의 분기 처리를 명확하게 구현하였습니다.
4.1. 요청(Request) 단계의 플래그: use_default_address
클라이언트 요청 단계에서부터 use_default_address라는 명시적인 플래그를 사용하여 배송지 설정 로직을 분기했습니다. 이를 통해 프론트엔드는 UI 상태에 따라 전송 데이터를 조절하고, 백엔드는 플래그 값에 따라 유연하게 비즈니스 로직을 처리하는 명확한 약속을 수립할 수 있었습니다.
4.2. 데이터베이스 응답을 활용한 플래그: EXISTS vs SELECT 1
데이터의 존재 유무를 확인하는 '플래그'를 얻기 위해 두 가지 SQL 패턴을 목적에 맞게 사용했습니다.
EXISTS사용 예:EXISTS는 데이터베이스가 직접TRUE/FALSE라는 논리 플래그를 반환합니다. 사용자의 '좋아요' 여부처럼, SQL 쿼리 내에서 다른 데이터와 함께 조합될 때 유용합니다.EXISTS(SELECT 1 FROM book_likes bl WHERE bl.book_id = b.id AND bl.user_id = ?) AS isLikedSELECT 1사용 예:SELECT 1 ... LIMIT 1은 '행의 존재'라는 재료를 반환하며, 애플리케이션이result.length > 0과 같은 코드로 직접 플래그를 생성합니다.이 프로젝트에서는 이 패턴을 통해 "도서를 구매한 사용자만 리뷰를 작성"할 수 있도록 접근 권한을 분기 처리했습니다.SELECT 1 FROM orders o JOIN order_details od ON o.id = od.order_id WHERE o.user_id = ? AND od.book_id = ? AND o.status = ? LIMIT 1
4.3. 인증 상태를 활용한 플래그: authenticateIfPresent 미들웨어
authenticateIfPresent 미들웨어는 토큰의 존재 여부 및 유효성을 하나의 '플래그'로 활용합니다. 이를 통해 단일 API 엔드포인트(GET /books/:bookId)가 로그인 사용자와 비로그인 사용자 모두에게 각기 다른 경험을 제공하도록 설계했습니다.
- 로그인 사용자:
req.user객체가 설정되어, '좋아요' 여부(isLiked)와 같은 개인화된 정보를 포함한 전체 데이터를 받습니다. - 비로그인 사용자:
req.user가 설정되지 않아, 개인화 정보가 제외된 기본 데이터만 받습니다.
이 설계는 API 엔드포인트의 수를 줄여 관리를 용이하게 하고, 비로그인 사용자의 서비스 접근성을 보장합니다.
4.4. URL 컨텍스트를 활용한 플래그: 자원 소유권 검증
PUT /books/23/reviews/8과 같은 요청에서, URL에 포함된 bookId와 reviewId를 단순 식별자를 넘어 '자원 관계 검증을 위한 플래그'로 활용했습니다. 이를 통해 서비스 계층에서는 "ID가 8인 리뷰가 정말 23번 책에 속하는가?"를 검증하여, 다른 책의 리뷰 ID를 악의적으로 사용하는 등의 비정상적인 접근을 원천 차단할 수 있었습니다. 이는 RESTful 원칙에 대한 깊은 이해를 바탕으로 API의 논리적 무결성과 안정성을 크게 향상시킵니다.
5. 보안 및 안정성 원칙의 철저한 준수
인증, 권한, 데이터 무결성 등 안정적인 서비스의 기본 원칙을 충실히 지키고 있습니다.
5.1. Access/Refresh 토큰을 활용한 상태 비저장(Stateless) 인증
서버가 사용자의 로그인 상태를 세션에 저장하지 않는 Stateless 인증 방식을 구현하기 위해 JWT(JSON Web Token) 기반의 토큰 인증 시스템을 도입했습니다.
- Access Token (단기 인증 토큰, 1시간): API 요청 시 신원 증명. 탈취 시 피해 최소화.
- Refresh Token (장기 인증 토큰, 7일): Access Token 재발급 전용.
5.2. 안전한 비밀번호 저장을 위한 해시(Hashing)
사용자의 비밀번호를 원본 그대로 저장하지 않고, 암호화된 문자열로 변환하여 저장하기 위해 해시 함수를 사용했다. 특히, crypto 모듈 대신 bcrypt를 선택하여 보안 수준을 한층 강화했습니다.
crypto vs bcrypt
crypto(Node.js 내장 모듈):pbkdf2와 같은 표준 해시 함수를 제공하며 빠르다. 하지만bcrypt에 비해 GPU를 사용한 무차별 대입 공격(Brute-force attack)에 상대적으로 취약할 수 있다.bcrypt: 패스워드 저장을 위해 특별히 설계된 해시 함수이다. 내장된 Salting 과정과 Key Stretching 횟수(cost factor)를 조절하여 의도적으로 해시 생성 속도를 늦출 수 있다. 이 '느린 속도'가 오히려 장점이 되어, 공격자가 초당 시도할 수 있는 비밀번호 추측 횟수를 급격히 감소시켜 무차별 대입 공격을 매우 어렵게 만든다.
단순 기능 구현을 넘어, 현대적인 웹 보안 표준을 깊이 이해하기 위해 bcrypt를 채택하여 적용하였습니다.
5.3. SQL Injection 방어
모든 외부 입력값을 신뢰하지 않고, 준비된 구문(Prepared Statement)을 사용하여 SQL Injection 공격을 원천적으로 방어하였습니다. dbPool.query(sql, params) 패턴을 프로젝트 전반에 일관되게 적용하여, 입력값이 악의적인 SQL 코드가 아닌 순수한 데이터로만 처리되도록 보장하였습니다.
5.4. 데이터 정합성을 위한 트랜잭션
주문 생성과 같이 여러 테이블에 걸쳐 데이터 변경이 일어나는 중요한 작업에 트랜잭션(Transaction)을 적용하였습니다. beginTransaction, commit, rollback을 사용하여 'All-or-Nothing' 원칙을 구현함으로써, 작업 중간에 오류가 발생하더라도 데이터가 불일치 상태에 빠지는 것을 방지하고 데이터의 정합성을 보장하였습니다.
5.5. 역할 기반 접근 제어 (RBAC)
사용자 테이블에 role 컬럼을 두고 JWT 페이로드에 역할을 포함시켜, 특정 API에 접근하기 전 사용자의 역할(admin 또는 member)을 검증하는 미들웨어를 구현하였습니다. 이를 통해 일반 사용자가 관리자 전용 기능에 접근하는 것을 막아 서비스의 안정성과 데이터 보호 수준을 높였습니다.
'Programmers' 카테고리의 다른 글
| [34일차]SQL 데이터 관리와 Node.js에서의 효율적인 사용법 (0) | 2025.10.28 |
|---|---|
| [33일차]Node.js의 논블로킹 I/O와 비동기 처리 방식 (1) | 2025.10.27 |
| [31일차]데이터베이스 무결성 관리, 스키마 개선 작업 결과 (0) | 2025.10.23 |
| [30일차]COUNT, AS, 서브쿼리, EXISTS (0) | 2025.10.22 |
| [29일차]SQL 시간 범위 검색, parent_id, 페이지네이션 (0) | 2025.10.21 |