[내일배움캠프 Spring_3기] CH2 커머스 과제 - 도전기능

jiiim_ni·2026년 1월 23일
post-thumbnail

필수 기능 구현 이후 Enum, 람다, 스트림을 실제로 적용해보았다.

  • 장바구니와 주문 흐름을 실제 서비스처럼 설계하기
  • 관리자 모드를 통한 상품 관리 기능 추가
  • Enum을 활용한 고객 등급 관리
  • 람다와 스트림을 활용한 데이터 조회 및 관리

생각해보기

CommerceSystem에 모든 로직이 몰리지 않도록 하는 게 목표였다.

장바구니 관련 로직은 Cart가 책임지도록 분리하고 상품 관리 로직은 Category와 Product가 담당하도록 했다.


Product 클래스

상품의 기본 정보를 관리하는 객체이다.
상품명, 가격, 설명, 재고 수량을 필드로 가지고 재고 차감은 decreaseStock() 메서드를 통해서만 가능하도록 제한했다.

public void decreaseStock(int quantity) {
    if (stock < quantity) {
        throw new IllegalArgumentException("재고가 부족합니다.");
    }
    stock -= quantity;
}

재고 변경 로직을 한 곳에 모아 잘못된 상태 변경을 방지하려고 했다.

Product.java 전체 코드

package kr.spartaclub.com.example.commerce;

public class Product {
    private final String name;
    private int price;
    private String description;
    private int stock;

    public Product(String name, int price, String description, int stock) {
        this.name = name;
        this.price = price;
        this.description = description;
        this.stock = stock;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }

    public String getDescription() {
        return description;
    }

    public int getStock() {
        return stock;
    }

    // 주문 확정 시 재고 감소를 위한 메서드
    public void decreaseStock(int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("차감 수량은 0보다 커야 합니다.");
        }
        if (stock < quantity) {
            throw new IllegalArgumentException("재고가 부족합니다.");
        }
        stock -= quantity;
    }

    public void setPrice(int price) {
        if (price <= 0) throw new IllegalArgumentException("가격은 0원보다 커야 합니다.");
        this.price = price;
    }

    public void setDescription(String description) {
        if (description == null || description.isBlank())
            throw new IllegalArgumentException("설명은 비어 있을 수 없습니다.");
        this.description = description;
    }

    public void setStock(int stock) {
        if (stock < 0) throw new IllegalArgumentException("재고는 0개 이상이어야 합니다.");
        this.stock = stock;
    }

}

Category 클래스

카테고리별로 상품을 관리하는 역할을 담당한다.
카테고리 이름과 상품 리스트를 보유하고 카테고리 단위로 상품 조회 및 관리 기능이 있다.
카테고리를 도입하면서 상품 조회 로직을 더 명확하게 분리할 수 있었다.

Category.java 전체 코드

package kr.spartaclub.com.example.commerce.challenge;

import kr.spartaclub.com.example.commerce.Product;

import java.util.List;


 // 하나의 카테고리(전자제품, 의류, 식품)를 표현하는 객체 - 카테고리 이름과 해당 카테고리에 속한 상품 목록을 함께 관리한다.
public class Category {

    // 카테고리 이름 - 외부에서 직접 변경하지 못하도록 private + final
    private final String name;

    // 해당 카테고리에 속한 상품 리스트 - 외부에서 직접 접근하지 못하도록 private으로 선언
    private final List<Product> products;

    /**
     * Category 생성자
     * @param name 카테고리 이름
     * @param products 해당 카테고리에 포함될 상품 목록
     */
    public Category(String name, List<Product> products) {
        this.name = name;
        this.products = products;
    }

    //카테고리 이름 조회
    public String getName() {
        return name;
    }

    // 카테고리에 속한 상품 목록 조회
    public List<Product> getProducts() {
        return products;
    }
}

Cart/CartItem 클래스

장바구니 기능을 담당하는 핵심 객체이다

  • Cart는 장바구니 전체를 관리하고
  • CartItem은 상품 + 수량 한 줄의 의미를 가진다
public void add(Product product, int quantity) {
    for (CartItem item : items) {
        if (item.getProduct() == product) {
            item.addQuantity(quantity);
            return;
        }
    }
    items.add(new CartItem(product, quantity));
}

이미 담긴 상품일 경우 수량만 증가시키도록 구현해 장바구니 구조를 단순하게 유지했다.

Cart.java 전체 코드

package kr.spartaclub.com.example.commerce.challenge;

import kr.spartaclub.com.example.commerce.Product;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// 장바구니 전체를 관리하는 객체 - 항목 추가/조회/초기화/총금액 계산
public class Cart {

    private List<CartItem> items = new ArrayList<>();

    public boolean isEmpty() {
        return items.isEmpty();
    }

    // 외부에서 리스트를 직접 수정 못하게 방어적 반환
    public List<CartItem> getItems() {
        return Collections.unmodifiableList(items);
    }

    // 상품을 장바구니에 담기 - 이미 담긴 상품이면 수량만 증가
    public void add(Product product, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("수량은 1개 이상이어야 합니다.");

        for (CartItem item : items) {
            if (item.getProduct() == product) { // 같은 객체면 같은 상품으로 판단
                item.addQuantity(quantity);
                return;
            }
        }
        items.add(new CartItem(product, quantity));
    }

    public int getTotalAmount() {
        int sum = 0;
        for (CartItem item : items) {
            sum += item.getTotalPrice();
        }
        return sum;
    }

    public void clear() {
        items.clear();
    }

    public void removeProduct(Product product) {
        items.removeIf(item -> item.getProduct() == product);
    }


    public boolean removeByProductName(String name) {
        int before = items.size();
        items = items.stream()
                .filter(item -> !item.getProduct().getName().equalsIgnoreCase(name.trim()))
                .toList();
        items = new ArrayList<>(items);
        return items.size() != before;
    }


}

CartItem.java 전체코드

package kr.spartaclub.com.example.commerce.challenge;

import kr.spartaclub.com.example.commerce.Product;

//장바구니에 담긴 한 줄 항목 - 어떤 상품을 몇 개 담았는지를 보관
public class CartItem {
    private final Product product;
    private int quantity;

    public CartItem(Product product, int quantity) {
        if (product == null) throw new IllegalArgumentException("상품이 null일 수 없습니다.");
        if (quantity <= 0) throw new IllegalArgumentException("수량은 1개 이상이어야 합니다.");

        this.product = product;
        this.quantity = quantity;
    }

    public Product getProduct() {
        return product;
    }

    public int getQuantity() {
        return quantity;
    }

    // 같은 상품을 추가로 담을 때 수량만 늘릴 수 있게
    public void addQuantity(int add) {
        if (add <= 0) throw new IllegalArgumentException("추가 수량은 1개 이상이어야 합니다.");
        this.quantity += add;
    }

    public int getTotalPrice() {
        return product.getPrice() * quantity;
    }
}

CommerceSystem 클래스

프로그램 전체 흐름을 제어하는 클래스이다
메인 메뉴 출력, 사용자 입력 처리, 카테고리 이동, 주문 흐름 제어, 관리자 모드 진입 등
실제 로직은 각 객체에 위임하고, CommerceSystem은 흐름 제어자 역할에 집중할 수 있도록 설계했다.

전체코드는 너무 길어서 github 통해서 확인

@jiiim_ni CommerceProject - github링크

기능이 실제로 잘 돌아가는지 확인하기 위해서


        // 테스트용 고객 데이터
        customers.put("bronze@test.com", new Customer("브론즈", "bronze@test.com", CustomerGrade.BRONZE, 0));
        customers.put("silver@test.com", new Customer("실버", "silver@test.com", CustomerGrade.SILVER, 600_000));
        customers.put("gold@test.com", new Customer("골드", "gold@test.com", CustomerGrade.GOLD, 1_200_000));
        customers.put("platinum@test.com", new Customer("플래티넘", "platinum@test.com", CustomerGrade.PLATINUM, 2_500_000));
    }

테스트용 고객 데이터를 만들어서 확인할 수 있었다.


핵심 로직

Lv1. 장바구니 + 주문 흐름

  • 상품 선택 -> 수량 입력 -> 장바구니 담기
  • 장바구니 조회 시 총 금액 계산
  • 주문 확정 시 재고 차감
  • 주문 취소 시 장바구니 초기화

가장 중요하게 고려한 점은 재고 차감 시점이었다.

장바구니에 담는 순간이 아니라 주문 확정 시점에만 재고를 차감하도록 구현해 실제 커머스 흐름과 최대한 유사하게 만들었다.

Lv2. 관리자 모드

관리자 모드는 별도의 메뉴로 분리해서 구현했다.

  • 비밀번호 인증(3회 실패 시 메인 메뉴 복귀)
  • 상품 추가/수정/삭제
  • 카테고리 선택 후 상품 관리
  • 삭제된 상품이 장바구니에 있을 경우 함께 제거

상품 수정 시에는 가격, 설명, 재고를 각각 선택적으로 수정할 수 있게끔 했다.

Product 객체의 setter 사용 이유

Lv3. Enum, 람다 & 스트림 활용

고객 등급 할인(Enum)을 정의해 등급별 할인율을 관리했다.

public enum CustomerGrade {
    BRONZE(0),
    SILVER(5),
    GOLD(10),
    PLATINUM(15);
}

주문 시 고객 이메일을 입력받아 해당 고객의 등급에 맞는 할인율을 적용했다.

CustomerGrade.java(Enum) 전체코드

package kr.spartaclub.com.example.commerce.challenge;

public enum CustomerGrade {
    BRONZE(0),
    SILVER(5),
    GOLD(10),
    PLATINUM(15);

    private final int discountPercent;

    CustomerGrade(int discountPercent) {
        this.discountPercent = discountPercent;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }

    public double getDiscountRate() {
        return discountPercent / 100.0;
    }
}

가격대별 상품 필터링(Stream)

카테고리 상품 조회 시 스트림을 활용해 가격 조건에 따른 필터링을 구현했다.

products.stream()
        .filter(p -> p.getPrice() <= 1_000_000)
        .forEach(...);

이를 통해 전체 상품, 100만원 이하, 100만원 초과 조회 기능을 구현했다.

장바구니 상품 제거(Stream)

장바구니에서 특정 상품을 제거할 때 stream().filter()를 활용해 해당 상품을 제외한 리스트로 재구성했다.

items = items.stream()
        .filter(item -> !item.getProduct().getName().equalsIgnoreCase(name))
        .toList();

결과

재고 변경 + 주문 취소 기능 결과


관리자 모드 기능 결과


가격 필터링 기능 + 회원 등급 + 장바구니 제거 기능 전체 결과


트러블슈팅 및 회고

1) 도전 기능 구현 난이도가 올라갔음
필수 기능까지는 요구사항이 비교적 단순해서 기능 구현 -> 출력 확인 흐름으로 진행할 수 있었다.
하지만 도전 기능부터는 장바구니, 관리자모드 처럼 실제 서비스 흐름을 흉내 내야해서 단순히 코드를 추가하는 방식으로는 해결이 잘 안되었다.

특히 한 기능을 추가하면 연관된 기능도 같이 바뀌는 게 헷갈렸다.
장바구니에 담기 기능을 만들면 재고 차감 시점을 다시 생각해야 했고, 주문 기능을 만들면 주문 취소 시 상태 복구가 필요했다. 그리고 관리자 모드에서 상품 삭제가 되면 장바구니에 들어있는 같은 상품도 같이 제거 해야했다.

이 과정에서 요구사항 하나가 단독으로 존재하지 않는다는 걸 체감했고 구현 전에 흐름을 먼저 그려보는 것이 중요하다는 걸 느꼈다.

2) CommerceSystem 코드가 길어지면서 수정 포인트를 찾기 어려웠음
도전 기능을 추가하면서 CommerceSystem에 메뉴 처리, 입력 처리, 출력 처리, 주문 로직까지 계속 쌓였다. 결과적으로 코드가 길어지니 어떤 기능을 수정하려고 했을 때 어디를 고쳐야 하는지 바로 찾기 힘들었다.

예를 들어서 메뉴 번호 하나를 추가하거나 출력 형식을 바꾸려고 하면 printMainMenu(), start(), 실제 기능 메서드 이렇게 여러 곳을 동시에 봐야 했고, 중간에 놓치면 콘솔 흐름이 꼬이기도 했다.

3) 커밋을 그때그때 하지 않아서 나중에 정리할때 더 헷갈렸음
기능 구현에 집중하다 보니 완성된 다음에 한번에 커밋하자 라고 생각을 해버렸다.
나중에 기능을 구현한 뒤에, 커밋을 기능별로 나누려고 할 때 어떤 변경이 어느 기능에 해당하는지 정리하기가 쉽지 않았다.

파일 하나에 여러 기능 변경이 섞인 경우, 출력 형식 수정 + 로직 변경이 동시에 들어간 경우, 다른 구조까지 영향을 준 경우 등 여러 상황에서 헷갈렸다.

커밋은 완성 후가 아니라 기능 단위로 작게, 실행 가능한 상태에서, 변경 목적이 명확한 시점에 하는 것이 좋겠다는, 커밋의 중요성을 체감했다.

다음 과제부터는 커밋 관리 문제를 개선하여 더 잘 해야겠다.

4) AI 활용에 대한 회고
솔직히 말하면 이번 과제를 진행하면서 AI의 도움을 전혀 받지 않았다고 하면 거짓말이다. 하지만 AI를 정답 생성기처럼 사용하지는 않으려고 노력했다.

막히는 부분이 생겼을 때도 "이 코드를 작성해줘" 보다는 이 문제를 어떤 방향으로 풀어가면 좋을지, 구조를 어떻게 나누면 좋을지에 대한 힌트를 얻는 용도로 활용했다.

물론 정말로 여러 번 고민해도 해결되지 않는 부분에서는 코드 예시를 요청한 적도 있다. 하지만 그럴 때에도 그대로 복사해서 사용하기 보다는, 코드를 읽고 이해한 뒤 직접 작성하면서 적용하려고 했다.


프로젝트 파일 설정

0개의 댓글