Spring Boot와 React를 이용한 풀스택 쇼핑몰 구축 및 실무 환경 설정 튜토리얼입니다.
Spring Boot 3.4와 React 19를 기반으로 구축한 풀스택 쇼핑몰 시스템입니다. 토스페이먼츠 실결제 연동, Redis 캐싱, 비관적 락 기반 재고 동시성 제어, SSE 실시간 알림, OAuth2 소셜 로그인, 관리자 통계 대시보드 등 실무 수준의 이커머스 핵심 비즈니스 로직을 단계별로 구현하며 아키텍처를 발전시켰습니다.
핵심 도메인: 회원(인증/인가) · 상품(검색/이미지) · 장바구니 · 주문/결제 · 리뷰 · 쿠폰 · 찜하기 · 관리자 통계
| 구분 | 기술 |
|---|---|
| 프론트엔드 | React 19, Vite, Recharts, Vanilla CSS, 커스텀 훅 패턴 |
| 백엔드 | Java 17, Spring Boot 3.4, Spring Security, Spring Data JPA |
| 인증 | JWT (Stateless), OAuth2 소셜 로그인, BCrypt 패스워드 인코딩 |
| 데이터베이스 | MySQL 8.0 (영속성), Redis 7 (캐싱) |
| 결제 | 토스페이먼츠 샌드박스 (서버 간 금액 검증) |
| 실시간 통신 | SSE (Server-Sent Events), Slack/Discord 웹훅 |
| DevOps | Docker, Docker Compose, GitHub Actions CI/CD, SSH 원격 배포 |
┌─────────────────────────────────────────────────────────────────────┐
│ 사용자 브라우저 │
│ React 19 + Vite (포트 5173 / Nginx :80) │
│ [App.jsx] [AuthManager] [CartList] [OrderList] [CheckoutTest] ... │
└───────────────────────────┬─────────────────────────────────────────┘
│ HTTP /api/* (Vite 프록시 또는 Nginx 역방향)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Spring Boot 백엔드 (포트 8080) │
│ │
│ 요청 진입 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ DebugFilter → JwtAuthenticationFilter → SecurityFilterChain │ │
│ │ - JWT 검증 (Authorization: Bearer <token>) │ │
│ │ - 공개 경로 우회: /api/auth/**, /api/products (GET), OAuth2 │ │
│ └────────────────────────┬─────────────────────────────────────┘ │
│ ▼ │
│ 컨트롤러 계층 │
│ ┌────────────┐ ┌───────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │Auth/User │ │Product │ │Order/Pay │ │Admin(통계/주문) │ │
│ │Controller │ │Controller │ │Controller │ │Controller │ │
│ └─────┬──────┘ └─────┬─────┘ └──────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴─────────────────┘ │
│ ▼ │
│ 서비스 계층 (비즈니스 로직) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ - 상품 조회: Redis 캐시 우선 → 미스 시 MySQL 조회 후 캐시 저장 │ │
│ │ - 주문 생성: 비관적 락(SELECT FOR UPDATE)으로 재고 차감 │ │
│ │ - 결제 확인: 프론트 금액 vs DB 주문 금액 교차 검증 후 토스 API │ │
│ │ - 주문 완료 이벤트 → @Async 비동기 리스너 호출 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ 데이터 계층 │
│ ┌────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ MySQL 8.0 │ │ Redis 7 │ │
│ │ User / Product / │ │ 상품 목록 캐시 / 장바구니 캐시 │ │
│ │ Order / OrderItem / │ │ (TTL 기반 자동 만료) │ │
│ │ CartItem / Review / │ └──────────────────────────────────┘ │
│ │ Coupon / Wishlist / │ │
│ │ Notification 등 │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
주문 완료 이벤트 (Spring ApplicationEvent)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 비동기 알림 파이프라인 │
│ │
│ OrderEventListener (@Async) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 1. Notification 엔티티 DB 저장 │ │
│ │ 2. SseEmitterRegistry → 해당 유저 브라우저로 실시간 푸시 전송 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ GlobalExceptionHandler │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 예외 발생 시 → Slack / Discord 웹훅으로 에러 알림 전송 │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
| 기능 | 구현 방식 |
|---|---|
| 재고 동시성 제어 | ProductRepository에 @Lock(PESSIMISTIC_WRITE) → DB 수준 SELECT FOR UPDATE |
| 결제 금액 위변조 방어 | 결제 승인 전 프론트 전달 금액과 DB 주문 합계 교차 검증 |
| 상품 소프트 삭제 | isDeleted 플래그로 주문 내역 보호, 이미지 파일 물리 삭제 병행 |
| Redis 캐싱 전략 | 상품 조회·장바구니를 캐시 우선 처리, 변경 시 캐시 무효화 |
| 실시간 알림 | SSE 연결 유지 → 주문 이벤트 발생 시 SseEmitterRegistry로 즉시 푸시 |
| 에러 모니터링 | GlobalExceptionHandler에서 Slack/Discord 웹훅 비동기 전송 |
개발자 로컬
│
│ git push origin main
▼
┌───────────────────────────────────────────────────────────┐
│ GitHub Actions CI │
│ │
│ 백엔드 잡 프론트엔드 잡 │
│ ├─ Java 17 세팅 ├─ Node.js 20 세팅 │
│ ├─ Gradle 캐시 복원 ├─ npm install │
│ ├─ ./gradlew test └─ npm run build │
│ └─ ./gradlew build -x test │
└───────────────────────┬───────────────────────────────────┘
│ CI 성공 시 CD 워크플로우 트리거
▼
┌───────────────────────────────────────────────────────────┐
│ GitHub Actions CD │
│ │
│ 1. Docker Hub 로그인 │
│ 2. 백엔드 이미지 빌드 → Docker Hub 푸시 │
│ (welchs1423/spring-react-tutorial-backend:latest) │
│ 3. 프론트엔드 이미지 빌드 → Docker Hub 푸시 │
│ (welchs1423/spring-react-tutorial-frontend:latest) │
│ 4. SSH로 원격 클라우드 서버 접속 │
│ └─ docker-compose pull │
│ └─ docker-compose up -d --remove-orphans │
│ └─ docker image prune -f │
└───────────────────────┬───────────────────────────────────┘
▼
┌───────────────────────────────────────────────────────────┐
│ 클라우드 서버 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────┐ │
│ │ frontend │ │ backend │ │ MySQL 8.0 │ │
│ │ Nginx :80 │ │ Spring :8080│ │ shop_db │ │
│ └──────┬──────┘ └──────┬──────┘ └────────────────┘ │
│ │ │ ┌────────────────┐ │
│ └────────────────┘ │ Redis 7 │ │
│ app-network │ (캐시) │ │
│ └────────────────┘ │
└───────────────────────────────────────────────────────────┘
컨테이너 구성: docker-compose.yml이 MySQL → Redis → Backend → Frontend 순으로 헬스체크 기반 의존성을 관리하며, 이미지·업로드 데이터는 Named Volume으로 영속 보존합니다.
| 구분 | 기술 |
|---|---|
| Backend | Java 17, Spring Boot 3.4.x, Spring Data JPA, MySQL |
| Frontend | React 19, Vite, CSS3 |
| 결제 | 토스페이먼츠(Toss Payments) 샌드박스 |
- 프로젝트 클론 후 루트 디렉토리에
uploads폴더를 직접 생성해야 이미지 업로드가 정상 작동합니다. - Docker MySQL 접속:
docker exec -it shop-mysql mysql --default-character-set=utf8mb4 -u root -p1234 shop_db
- [완료] 프로젝트 최종 아카이빙 및 시스템 아키텍처 문서화 적용
- [Completed] GitHub Actions CD 연동 및 클라우드 서버 자동 배포 파이프라인 구축
- [Completed] 테스트 코드 정적 분석 경고 해소 —
OrderEventTest,StatisticsServiceTest에@SuppressWarnings("null")적용 - [Completed] 글로벌 예외 처리 및 Slack/Discord 실시간 에러 모니터링(Webhook) 구축 완료
- [Completed]
GlobalExceptionHandlerTest미사용verifyNoInteractionsimport 제거
- [Completed] Docker 및 Docker Compose를 활용한 전체 서비스 컨테이너화 구축 완료
- [Completed] 장바구니 DB 동기화 및 마이페이지 찜하기(Wishlist) 기능 연동
- [Completed] 대규모 트래픽 대비 Redis 기반 상품 조회 캐싱 및 장바구니 메모리 최적화
- [Completed] Spring Event 기반 비동기 주문 알림 시스템 및 TDD 검증 완료
- [Completed] Server-Sent Events(SSE) 기반 실시간 웹 푸시 알림 구현 및 단위 테스트 완료
- [Completed] 관리자 통계 대시보드 API 및 차트(Chart) UI 연동 완료 (TDD 검증)
- [Completed] GitHub Actions 기반 CI(자동 테스트/빌드) 파이프라인 구축
- 상품 썸네일 및 포토 리뷰를 위한 이미지 파일 업로드 연동
- [Completed] 상품 동적 검색(키워드/가격/카테고리) 및 다중 필터링 연동
- 관리자(Admin) 상품 관리 API 및 UI 뼈대 구축
- 사용자 리뷰 시스템 구현 및 주문 상태 관리 고도화(
DELIVERED) 완료 - 쿠폰/포인트 시스템 및 결제 할인 연동
OrderServiceNull 타입 안전성 경고 해소- OAuth2 소셜 로그인 및 JWT 보안 아키텍처 구축
userRepository.findById(user.getId())등 5개 호출부에서Long→@NonNull Longunchecked conversion 경고를 발생시키던 문제 수정.Objects.requireNonNull()래핑으로 비-null 보장을 명시적으로 전달,@SuppressWarnings("null")억제 대신 실질적 픽스 방식으로 통일.
- 여러 사용자가 동시에 동일 상품을 결제할 때 재고가 음수가 되거나 정합성이 깨지는 Race Condition을 차단.
ProductRepository에@Lock(LockModeType.PESSIMISTIC_WRITE)JPQL 메서드(findByIdWithLock)를 추가하여 DB 수준의SELECT ... FOR UPDATE보장.OrderService.createOrder: 재고 차감 직전 락이 걸린 메서드로 상품을 재조회하여 동시 요청 간 재고 수량 충돌을 원천 차단.OrderService.cancelOrder: 환불·취소 시 재고 복구 구간에도 동일한 비관적 락을 적용하여 취소와 신규 주문이 동시에 들어오는 시나리오를 안전하게 처리.
JJWT Deprecated API 현대화 (JwtTokenProvider)
Jwts.parser()→Jwts.parserBuilder()체인으로 교체 (JJWT 0.11.5 권장 방식).signWith(SignatureAlgorithm.HS512, key)→signWith(key)로 변경 (SecretKey에서 알고리즘 자동 추론).
NullSecurityContextRepository Spring Security 6 대응
- 폐기(Deprecated)된
loadContext(HttpRequestResponseHolder)유지 + 신규loadDeferredContext(HttpServletRequest)오버라이드 추가. @SuppressWarnings("deprecation")로 불가피한 인터페이스 구현 경고 억제.
Product 엔티티 @Builder.Default 적용
reviews,cartItems,isDeleted필드에@Builder.Default를 추가해 빌더 사용 시 기본값이 무시되던 문제 해소.
UploadController 잠재적 NPE 수정
getOriginalFilename()결과가 null이거나 확장자가 없는 경우를 사전 검증, 400 Bad Request로 명시적 응답.
미사용 코드 제거
CustomUserDetailsService.passwordEncoder필드 및 생성자 파라미터 제거.OrderService.productRepository필드 및 임포트 제거.SecurityConfig의 미사용CorsConfigurer임포트 제거.AuthService의 미사용AuthenticationManagerBuilder,Collectors임포트 제거.JwtAuthenticationFilter의 미사용CustomUserDetailsService,UserDetails임포트 제거.
@NonNull 계약 명시 (JwtAuthenticationFilter)
doFilterInternal,shouldNotFilter파라미터에@NonNull어노테이션 추가 — 상위OncePerRequestFilter계약 충족.
정적 분석 @NonNull 오탐 억제
- Lombok 빌더 반환값 및 컨트롤러 경유
Long파라미터를findById()에 전달할 때 JDT 가 잘못 경고하는 케이스에@SuppressWarnings("null")적용 (CartService, ProductService, ReviewService, OrderService, AuthService, DatabaseInitializer).
GlobalExceptionHandler 도입 (예외 처리 일원화)
exception/GlobalExceptionHandler.java신규 생성으로 모든 컨트롤러의 try-catch 중복 코드 제거.exception/BusinessException.java를 도입해 비즈니스 규칙 위반(재고 부족, 권한 없음 등)을 HTTP 상태 코드와 함께 명시적으로 표현.IllegalArgumentException→ 400,IllegalStateException→ 409,AccessDeniedException→ 403으로 표준화.
N+1 쿼리 문제 해결
OrderRepository에@QueryFETCH JOIN 메서드(findByUserWithItems,findByIdWithItems)를 추가해 주문 목록/상세 조회 시 OrderItem + Product를 단일 쿼리로 조회.
재고 이중 차감 버그 수정
OrderService.completePayment에서 재고를 재차감하던 버그 수정.- 재고는
createOrder에서 1회만 차감하며,completePayment는 주문 상태를PAID로 변경하고paymentKey를 저장하는 역할에만 집중.
PaymentController/PaymentService 책임 분리
PaymentController의 결제 API 통신 로직을PaymentService로 분리.@Autowiredfield injection →@RequiredArgsConstructorconstructor injection으로 전환.RestTemplate을WebMvcConfig에@Bean으로 등록해 의존성 주입 방식 준수.
Controller 정리
OrderController에서UserRepository직접 의존 제거 →CustomUserDetailsService로 위임.ProductController의 디버그용System.out.println제거.Order엔티티에서 Lombok으로 커버되는paymentKeygetter/setter 중복 선언 제거.AuthController생성자 주입 방식@RequiredArgsConstructor로 통일.AuthService의 중복 username 예외를BusinessException(409 CONFLICT)으로 교체.
공통 API 클라이언트 (src/api/apiClient.js)
- 모든 컴포넌트에 흩어져 있던
API_BASE_URL상수 및fetch호출을 단일 모듈로 통합. sessionStorage에서 JWT 토큰을 자동 주입하고, 에러 응답을 일관된Error로 throw.get / post / put / patch / delete / upload메서드 제공.
커스텀 훅으로 비즈니스 로직 분리 (src/hooks/)
useAuth.js: 로그인·회원가입·로그아웃 및 세션 상태 관리.localStorage/sessionStorage혼용 불일치를sessionStorage로 통일.useProducts.js: 상품 목록 조회, 등록, 삭제.useCart.js: 장바구니 조회, 수량 변경, 삭제, 주문.useOrders.js: 주문 목록/상세 조회, 취소, 리뷰 CRUD.
컴포넌트 분리 및 정리
App.jsx에 인라인으로 존재하던ProductForm을src/components/ProductForm.jsx로 분리.App.jsx:useAuth/useProducts훅 적용,useMemo로 상품 검색 필터링 최적화,useCallback으로 핸들러 메모이제이션.AuthManager.jsx:useAuth훅 위임, 상태 변수 명 충돌(username필드명) 해소.CartList.jsx/OrderList.jsx/ProductManager.jsx: 각각 전용 훅(useCart,useOrders,useProducts)으로 API 로직 분리.
결제 취소 UI 연동
- 주문 목록 카드에
ORDERED(파랑),PAID(초록),CANCELED(빨강) 상태를 시각적으로 구분하는 컬러 배지 표시. - 상태가
PAID인 주문에만 '결제 취소' 버튼을 노출, 클릭 시 취소 사유 입력 모달이 열리며/api/payment/cancel/{orderId}로 POST 요청. - 취소 API 성공 시 추가 네트워크 요청 없이
orders목록 상태와 상세 모달의selectedOrder를 즉시CANCELED로 갱신.
- 프론트엔드에서 전달된 결제 금액(
amount)과 DB의 실제 주문 금액(totalAmount)을 결제 승인 직전에 교차 검증하여 악의적인 데이터 조작 원천 차단. Order엔티티에paymentKey필드를 추가하여 추후 환불 및 결제 추적성 확보./api/payment/cancel/{orderId}엔드포인트 구축 및 사유(Reason)를 포함한 취소 통신 완료.- 환불 성공 시
OrderService.cancelOrder를 재활용하여 상품 재고(Stock) 복구 및 주문 상태(CANCELED) 롤백 보장.
- Toss Payments와의 서버 간 통신을 통해 결제 무결성 검증 완료.
OrderStatusEnum에PAID상태 추가 및 결제 완료 시 상태 변경 로직 구현.- 결제 성공 시 상품 테이블(
Product)의 물리적 재고(stockQuantity) 자동 차감 로직 연동. - React StrictMode로 인한 API 중복 호출 이슈를
main.jsx수정으로 해결. - JPA 순환 참조 에러를
@JsonIgnore로 해결하여 데이터 전송 최적화.
CheckoutTest.jsx컴포넌트를 통한 토스페이먼츠 일반 결제창 구현 및 독립적 테스트 완료.URLSearchParams를 활용한 결제 성공 데이터(paymentKey) 추출 로직 구현.window.history.replaceState를 사용하여 주소창 URL 정리 기능 추가.
- 모든 엔티티의 생성 및 수정 시간을 자동으로 관리하는 JPA Auditing 기반 마련.
- 이모지 및 다국어 지원을 위해
utf8mb4캐릭터셋으로 Docker MySQL 서버 환경 재구축. - 프로젝트 기본 브랜치를
main으로 전환하고 원격 저장소와 동기화 완료.
- 데이터 무결성을 위해
isDeleted플래그를 사용하는 Soft Delete 도입 — 주문 내역이 있는 상품도 안전하게 관리. - 상품 삭제 또는 이미지 변경 시 서버 내 물리적 파일(
uploads/)을 자동으로 삭제. - 통합
.gitignore로 설정 파일 및 바이너리 파일 유출 방지. application.properties를 추적 제외하여 민감한 DB 및 JWT 정보 보호.