Spring Boot - 장바구니 기능 구현

송진우·2026년 1월 2일
post-thumbnail

장바구니란?

사용자가 구매하고 싶은 상품을 임시로 담아두는 기능

주요 기능

  • 상품 담기
  • 장바구니 목록 조회
  • 수량 변경
  • 개별 삭제 / 전체 비우기

장바구니 전체 흐름


📁 프로젝트 구조

src/main/java/com/onandhome/
├── cart/
│   ├── entity/
│   │   └── CartItem.java           # 장바구니 아이템 엔티티
│   ├── CartItemRepository.java     # JPA Repository
│   ├── CartService.java            # 비즈니스 로직
│   └── CartRestController.java     # REST API
├── admin/adminProduct/
│   └── entity/Product.java         # 상품 엔티티
└── user/
    └── entity/User.java            # 사용자 엔티티

1. 엔티티 설계

CartItem.java

@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class CartItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 사용자 (N:1)
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    // 상품 (N:1)
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    // 수량
    @Column(nullable = false)
    private int quantity;
}

핵심 포인트

  • FetchType.EAGER: 장바구니 조회 시 상품 정보 즉시 로딩
  • User, Product와 N:1 관계
  • quantity: 장바구니에 담은 수량

ERD 구조


2. Repository

CartItemRepository.java

public interface CartItemRepository extends JpaRepository<CartItem, Long> {
    
    // 사용자의 장바구니 조회
    List<CartItem> findByUser(User user);
    
    // 중복 체크 (사용자 + 상품)
    Optional<CartItem> findByUserAndProduct(User user, Product product);
    
    // 사용자의 장바구니 전체 삭제
    void deleteByUser(User user);
}

3. Service

CartService.java (핵심 메서드만)

@Service
@RequiredArgsConstructor
@Transactional
public class CartService {

    private final CartItemRepository cartRepo;
    private final ProductRepository productRepo;
    private final UserRepository userRepo;

    /**
     * 장바구니 목록 조회
     */
    public List<CartItem> getCartItems(Long userId) {
        User user = userRepo.findById(userId).orElse(null);
        if (user == null) return List.of();
        
        return cartRepo.findByUser(user);
    }

    /**
     * 장바구니 담기
     * - 중복 상품: 수량 증가
     * - 새 상품: CartItem 생성
     */
    @Transactional
    public CartItem addToCart(Long userId, Long productId, int qty) {
        // User, Product 조회
        User user = userRepo.findById(userId)
            .orElseThrow(() -> new IllegalArgumentException(
                "사용자를 찾을 수 없습니다."));
        
        Product product = productRepo.findById(productId)
            .orElseThrow(() -> new IllegalArgumentException(
                "상품을 찾을 수 없습니다."));
        
        // 중복 체크
        Optional<CartItem> existing = 
            cartRepo.findByUserAndProduct(user, product);
        
        if (existing.isPresent()) {
            // 기존 아이템: 수량 증가
            CartItem item = existing.get();
            item.setQuantity(item.getQuantity() + Math.max(qty, 1));
            return cartRepo.save(item);
        }
        
        // 새 아이템: 생성
        CartItem item = new CartItem();
        item.setUser(user);
        item.setProduct(product);
        item.setQuantity(Math.max(qty, 1));
        return cartRepo.save(item);
    }

    /**
     * 수량 변경
     */
    @Transactional
    public CartItem updateQuantity(Long cartItemId, int quantity) {
        CartItem item = cartRepo.findById(cartItemId).orElseThrow();
        item.setQuantity(Math.max(quantity, 1));
        return cartRepo.save(item);
    }

    /**
     * 아이템 삭제
     */
    @Transactional
    public void removeItem(Long cartItemId) {
        cartRepo.deleteById(cartItemId);
    }

    /**
     * 장바구니 비우기
     */
    @Transactional
    public void clearCart(Long userId) {
        User user = userRepo.findById(userId).orElseThrow();
        cartRepo.deleteByUser(user);
    }
}

4. REST API

CartRestController.java

@RestController
@RequestMapping("/api/cart")
@RequiredArgsConstructor
public class CartRestController {

    private final CartService cartService;
    private final JWTUtil jwtUtil;
    private final UserRepository userRepository;

    /**
     * 장바구니 담기
     * POST /api/cart/add
     */
    @PostMapping("/add")
    public ResponseEntity<Map<String, Object>> addToCart(
            @RequestBody AddToCartRequest request,
            @RequestHeader("Authorization") String authHeader) {
        
        Map<String, Object> response = new HashMap<>();
        
        try {
            // JWT 토큰 검증
            String token = authHeader.substring(7);
            Map<String, Object> claims = jwtUtil.validateToken(token);
            String userId = (String) claims.get("userId");
            
            User user = userRepository.findByUserId(userId)
                .orElseThrow(() -> new IllegalArgumentException(
                    "사용자를 찾을 수 없습니다."));

            // 장바구니 담기
            CartItem cartItem = cartService.addToCart(
                user.getId(), 
                request.getProductId(),
                request.getQuantity()
            );

            response.put("success", true);
            response.put("message", "장바구니에 상품이 추가되었습니다.");
            response.put("data", cartItem);
            
            return ResponseEntity.ok(response);

        } catch (Exception e) {
            response.put("success", false);
            response.put("message", e.getMessage());
            return ResponseEntity.status(500).body(response);
        }
    }

    /**
     * 장바구니 조회
     * GET /api/cart
     */
    @GetMapping
    public ResponseEntity<Map<String, Object>> getCart(
            @RequestHeader("Authorization") String authHeader) {
        
        Map<String, Object> response = new HashMap<>();
        
        try {
            String token = authHeader.substring(7);
            Map<String, Object> claims = jwtUtil.validateToken(token);
            String userId = (String) claims.get("userId");
            
            User user = userRepository.findByUserId(userId)
                .orElseThrow(() -> new IllegalArgumentException(
                    "사용자를 찾을 수 없습니다."));

            List<CartItem> cartItems = 
                cartService.getCartItems(user.getId());

            response.put("success", true);
            response.put("data", cartItems);
            response.put("count", cartItems.size());
            
            return ResponseEntity.ok(response);

        } catch (Exception e) {
            response.put("success", false);
            response.put("message", e.getMessage());
            return ResponseEntity.status(500).body(response);
        }
    }

    /**
     * 수량 변경
     * PUT /api/cart/{cartItemId}
     */
    @PutMapping("/{cartItemId}")
    public ResponseEntity<Map<String, Object>> updateQuantity(
            @PathVariable Long cartItemId,
            @RequestBody UpdateQuantityRequest request,
            @RequestHeader("Authorization") String authHeader) {
        
        Map<String, Object> response = new HashMap<>();
        
        try {
            CartItem updatedItem = cartService.updateQuantity(
                cartItemId, request.getQuantity());

            response.put("success", true);
            response.put("data", updatedItem);
            return ResponseEntity.ok(response);

        } catch (Exception e) {
            response.put("success", false);
            response.put("message", e.getMessage());
            return ResponseEntity.status(500).body(response);
        }
    }

    /**
     * 아이템 삭제
     * DELETE /api/cart/{cartItemId}
     */
    @DeleteMapping("/{cartItemId}")
    public ResponseEntity<Map<String, Object>> removeItem(
            @PathVariable Long cartItemId,
            @RequestHeader("Authorization") String authHeader) {
        
        Map<String, Object> response = new HashMap<>();
        
        try {
            cartService.removeItem(cartItemId);

            response.put("success", true);
            response.put("message", "장바구니에서 상품이 제거되었습니다.");
            return ResponseEntity.ok(response);

        } catch (Exception e) {
            response.put("success", false);
            response.put("message", e.getMessage());
            return ResponseEntity.status(500).body(response);
        }
    }

    // DTO 클래스
    public static class AddToCartRequest {
        private Long productId;
        private int quantity;
        // getter, setter
    }

    public static class UpdateQuantityRequest {
        private int quantity;
        // getter, setter
    }
}

테스트 방법

1. Postman으로 API 테스트

1-1. 로그인하여 JWT 토큰 얻기

POST http://localhost:8080/api/user/login
Content-Type: application/json

{
  "userId": "user123",
  "password": "Password1!"
}

응답에서 accessToken 복사

{
  "success": true,
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "user": { ... }
}

1-2. 장바구니 담기 테스트

POST http://localhost:8080/api/cart/add
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json

{
  "productId": 1,
  "quantity": 2
}

1-3. 장바구니 조회 테스트

GET http://localhost:8080/api/cart
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

1-4. 수량 변경 테스트

PUT http://localhost:8080/api/cart/1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json

{
  "quantity": 10
}

1-5. 아이템 삭제 테스트

DELETE http://localhost:8080/api/cart/1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

실제 구현 화면

1. 상품 상세 페이지 - 장바구니 담기

  • 상품 정보 확인 및 하단에 "장바구니 담기" 버튼 제공

2. 장바구니 사아드 바

  • 우측에서 슬라이드로 열리는 간편 장바구니
  • 수량 조절, 개별 삭제가 가능하며 실시간 합계 금액 표시
  • 하단 "장바구니 가기" 버튼으로 전체 페이지 이동 가능

3. 장바구니 페이지

표시 정보

  • 상품 이미지
  • 상품명
  • 가격 (할인가)
  • 수량 조절 (+/- 버튼)
  • 개별 삭제 버튼 (X)
  • 선택한 상품 총 금액

4. 빈 장바구니 페이지

  • "장바구니가 비어있습니다" 메시지 표시
  • "쇼핑 계속하기" 버튼으로 쇼핑 재개 유도

0개의 댓글