flyway로 초기 스키마 생성
설정값 세팅
Flyway 의존성 추가: backend/build.gradle
org.flywaydb:flyway-core
org.flywaydb:flyway-database-postgresql
테스트 안정화: backend/src/test/resources/application.properties
spring.flyway.enabled=false 추가(H2 테스트와 PostgreSQL 전용 SQL 충돌 방지)
CREATE TABLE members (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255),
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price_amount NUMERIC(19, 4) NOT NULL CHECK (price_amount >= 0),
stock_quantity INT NOT NULL CHECK (stock_quantity >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
member_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(19, 4) NOT NULL CHECK (unit_price >= 0),
status VARCHAR(32) NOT NULL,
total_amount NUMERIC(19, 4) NOT NULL CHECK (total_amount >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_orders_member
FOREIGN KEY (member_id)
REFERENCES members (id),
CONSTRAINT fk_orders_product
FOREIGN KEY (product_id)
REFERENCES products (id)
);
테이블 구성
이번 스키마는 “먼저 동작하는 최소 주문 시스템”을 목표로 단순하게 잡았습니다.
핵심은 회원 → 상품 → 주문 흐름을 빠르게 구현할 수 있게 하는 것입니다.
- members
- 회원 기본 정보 (email, password_hash, name)
- email은 유니크로 중복 가입 방지
- products
- 상품명, 가격, 재고 수량(stock_quantity)
- orders
- 주문자(member_id), 주문 상품(product_id)
- 수량(quantity), 주문 단가(unit_price), 주문 총액(total_amount)
- 현재는 단일 상품 주문 1건 기준 모델
JPA 매핑 요약 (기본형)
JPA도 스키마와 동일하게 최소로 매핑했습니다.
엔티티
- Member ↔ members
- Product ↔ products
- Order ↔ orders
관계
- Order → Member (ManyToOne)
- Order → Product (ManyToOne)
주문 엔티티가 회원/상품을 직접 참조해 구조를 직관적으로 유지했습니다.
시간 컬럼 처리
- created_at은 DB의 DEFAULT CURRENT_TIMESTAMP를 사용
- 엔티티에서는 insertable=false, updatable=false로 읽기 전용 매핑
애플리케이션 코드에서 시간을 직접 채우지 않고 DB 기본값에 맡기는 가장 단순한 방식입니다.
CRUD API
1) Repository
- member/domain/MemberRepository
- product/domain/ProductRepository
- order/domain/OrderRepository
2) Service
- member/application/MemberService
- product/application/ProductService
- order/application/OrderService
3) API DTO
- member/api/MemberDtos
- product/api/ProductDtos
- order/api/OrderDtos
4) Controller
- member/api/MemberController (/api/members)
- product/api/ProductController (/api/products)
- order/api/OrderController (/api/orders)
제공되는 CRUD 엔드포인트
각 리소스 공통:
- POST /api/{resource}
- GET /api/{resource}/{id}
- GET /api/{resource}
- PUT /api/{resource}/{id}
- DELETE /api/{resource}/{id}
리소스:
- members
- products
- orders
모든 응답은 기존 공통 포맷 ApiResponse로 반환됩니다.
주문 API 현재 동작
- 주문 생성 시 memberId, productId, quantity를 받음
- 상품 현재 가격을 unit_price로 저장
- total_amount = unit_price * quantity 계산
- 재고 차감/락/멱등성은 아직 미구현 (다음 단계에서 개선 예정)
CRUD 단위 테스트 (Mockito)
추가된 테스트 파일
- backend/src/test/java/com/marketengine/backend/member/application/MemberServiceTest.java
- backend/src/test/java/com/marketengine/backend/product/application/ProductServiceTest.java
- backend/src/test/java/com/marketengine/backend/order/application/OrderServiceTest.java
테스트 범위
MemberServiceTest
- 회원 생성 성공
- 이메일 중복 시 CONFLICT 예외
- 없는 회원 수정 시 RESOURCE_NOT_FOUND 예외
ProductServiceTest
- 상품 생성 성공
- 없는 상품 조회 시 RESOURCE_NOT_FOUND 예외
- 상품 수정 시 필드 변경 반영 확인
OrderServiceTest
- 주문 생성 시 상품 가격으로 unitPrice/totalAmount 계산 확인
- 회원 없을 때 주문 생성 실패(RESOURCE_NOT_FOUND)
- 주문 수정 시 상태/수량/총액 갱신 확인
Swagger/OpenAPI 자동화 적용
- backend/build.gradle
- springdoc-openapi-starter-webmvc-ui 의존성 추가
- backend/src/main/resources/application.yml
- OpenAPI 문서 경로: /api-docs
- Swagger UI 경로: /swagger-ui.html
- UI 정렬 옵션 추가
- backend/src/main/java/com/marketengine/backend/common/config/OpenApiConfig.java
- 문서 메타 정보(title/version/description) 설정
앱 실행 후 아래 주소로 접속
- Swagger UI: http://localhost:8080/swagger-ui.html
- OpenAPI JSON: http://localhost:8080/api-docs

Filter HTTP 요청 응답 로깅
- backend/src/main/java/com/marketengine/backend/common/logging/RequestResponseLoggingFilter.java
package com.marketengine.backend.common.logging;
import java.io.IOException;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class RequestResponseLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestResponseLoggingFilter.class);
private static final String REQUEST_ID_KEY = "requestId";
private static final String REQUEST_ID_HEADER = "X-Request-Id";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
long start = System.currentTimeMillis();
String requestId = resolveRequestId(request);
MDC.put(REQUEST_ID_KEY, requestId);
response.setHeader(REQUEST_ID_HEADER, requestId);
try {
filterChain.doFilter(request, response);
} catch (Exception ex) {
log.error("Request failed method={} uri={} message={}",
request.getMethod(), request.getRequestURI(), ex.getMessage(), ex);
throw ex;
} finally {
long durationMs = System.currentTimeMillis() - start;
int status = response.getStatus();
log.info("Request completed method={} uri={} status={} durationMs={}",
request.getMethod(), request.getRequestURI(), status, durationMs);
MDC.remove(REQUEST_ID_KEY);
}
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI();
return uri.startsWith("/swagger-ui")
|| uri.equals("/swagger-ui.html")
|| uri.startsWith("/api-docs");
}
private String resolveRequestId(HttpServletRequest request) {
String requestId = request.getHeader(REQUEST_ID_HEADER);
if (requestId == null || requestId.isBlank()) {
return UUID.randomUUID().toString();
}
return requestId;
}
}
- OncePerRequestFilter 기반으로 전역 요청/응답 로깅
로그에 남는 항목:
- method
- uri
- status
- durationMs
- requestId (MDC + 응답 헤더 X-Request-Id)
예외 발생 시:
- Request failed ... 에러 로그 출력 후 예외 재던짐
- 최종적으로 완료 로그도 남김 (finally)

'백엔드 엔지니어링 일지' 카테고리의 다른 글
| 마켓 백엔드 엔진 5 : product DB 구체화, 대용량 더미 데이터 생성, 검색 기능 개발 (0) | 2026.04.24 |
|---|---|
| 마켓 백엔드 엔진 4 : Next.js 프론트 생성, Docker Compose로 통합 배포, 더미 데이터 생성 (0) | 2026.04.23 |
| 마켓 백엔드 엔진 2 : Github Actions 테스트 자동화 및 Docker Compose로 local PostgreSQL DB 설정 (0) | 2026.04.21 |
| 마켓 백엔드 엔진 1 : 스프링부트 프로젝트 생성 및 전역 예외 설정, 공통 응답 형식 지정 (0) | 2026.04.21 |
| 마켓 백엔드 엔진 0 : 대용량 트래픽에서 성능 최적화 및 데이터 무결성 보장 (0) | 2026.04.17 |