본문 바로가기
Java/공부 & 트러블 슈팅

커머스 프로그램 만들면서 적용해본 객체 활용법

by 정구정구 2026. 6. 20.

 간단한 커머스 프로그램을 만들면서 최근 공부한 다양한 객체 활용법들을 적용해보았다. 

 

https://github.com/wjdals0508/commerceProject

 

GitHub - wjdals0508/commerceProject: 내일배움캠프 커머스 과제

내일배움캠프 커머스 과제 . Contribute to wjdals0508/commerceProject development by creating an account on GitHub.

github.com

 

 

정적 팩토리 메서드 (싱글톤 패턴)

public class CustomerManager {

    //// 싱글톤 ////
    private static CustomerManager instance;

    private CustomerManager() {}

    public static CustomerManager getInstance() {
        if (instance == null) {
            instance = new CustomerManager(); // 싱글톤
        }
        return instance;
    }


    //// 비즈니스 로직 ////
    private final Map<String, Customer> customers = new HashMap<>();

    public void addCustomer(Customer customer) {

        Customer previous = customers.putIfAbsent(customer.getId(), customer);
        if (previous != null) {
            throw new IllegalArgumentException("이미 존재하는 유저입니다.");
        }
    }

    public Map<String, Customer> getCustomers() {
        return Map.copyOf(customers);
    }
}

 

 메모리 관리에도 좋고, 객체가 하나만 존재하길 원하기 때문에,  Model 객체들을 관리하는 Manager 객체들은 싱글톤으로 생성해 관리했다.

 

 

합성 (Composition)

public class Customer {

    ... 

    private final String id;
    private String name;
    private String eMail;
    private Grade grade;

    private final ShoppingCart shoppingCart = new ShoppingCart(); // 합성

    ...
    
    public ShoppingCart getShoppingCart() { return shoppingCart; }
}

 

 모든 유저는 자신의 장바구니를 가진다.(has-a) 그렇기 때문에 둘을 합성으로 설계를 했다.

 

 

스트림 활용

if (products.containsKey(category)) {
    products.get(category).add(product);
} else {
    products.put(category, new ArrayList<>());
    products.get(category).add(product);
}

 

->

products.computeIfAbsent(category, k -> new ArrayList<>())
        .add(product);

 

 새로 익힌 Stream을 활용해 리팩토링 또한 진행해봤다. 코드가 줄어서 가독성이 향상되었다. 

 

 

값 객체 활용 (Value Object)

public class Money {
    private final long amount;

    DecimalFormat df = new DecimalFormat("###,###");

    public Money(long amount) {
        if (amount < 0) throw new IllegalArgumentException("가격은 0보다 낮을 수 없습니다.");
        this.amount = amount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money other)) return false;
        return amount == other.amount;
    }

    @Override
    public int hashCode() {
        return Long.hashCode(amount);
    }

    public long getAmount() {
        return amount;
    }

    public String getAmountString() {
        return df.format(amount);
    }

    public Money add(Money other) {
        return new Money(this.amount + other.amount);
    }

    public Money sub(Money other) {
        return new Money(this.amount - other.amount);
    }

    public Money multi(Money other) {
        return new Money(this.amount * other.amount);
    }

    public Money multi(int other) {
        return new Money(this.amount * other);
    }

    public Money multi(double rate) {
        if (rate < 0) throw new IllegalArgumentException("배율은 0보다 낮을 수 없습니다.");

        return new Money((long)(this.amount * rate));
    }

    public boolean isOver(Money other) {
        return this.amount - other.amount > 0;
    }

    public boolean islessOrSame(Money other) {
        return this.amount - other.amount <= 0;
    }
}

 

사실 간단한 프로그램이라서 값 객체까지 구현할 필요는 없었지만, 배운 것을 활용해보자는 의미로 Price 변수를 값 객체로 구현했다. 

 

 

방어적 복사

    public Order(Map<Product, Integer> products, String customerId, Money totalPrice, Money originPrice, List<Discount> discountList) {

        if (products.isEmpty() || customerId.isEmpty() || totalPrice.getAmount() < 0) {
            throw new IllegalArgumentException("옳바르지 않은 요청 입니다.");
        }
        this.id = idCounter;
        this.products = Map.copyOf(products);
        this.customerId = customerId;
        this.totalPrice = new Money(totalPrice.getAmount());
        this.originPrice = new Money(originPrice.getAmount());
        this.state = State.REQUEST;
        this.discountList = List.copyOf(discountList);
        idCounter++;
    }

 

 콜렉션 객체를 객체 내로 가져오거나 객체 밖으로 내보낼 때, copyOf로 감싸줘서 외부에서 값이 바뀌는 것을 막을 수 있다.

 

새로 알게된 내용

    public List<Product> getProductsByCategory(Categories category) {

        if (!categoryProducts.containsKey(category))
            return new ArrayList<>();

        return  List.copyOf(categoryProducts.get(category)); // 방어적 복사
    }
List<Product> productLists = getProductsByCategory(category);

productLists.add(otherProduct); // 불가능

productLists.get(index).setName("새우깡"); // 적용됨 ("감자깡" -> "새우깡")


List<Product> productLists2 = getProductsByCategory(category);

productLists2.get(index).getName(); // "새우깡"

 

 사실 copyOf로 콜렉션을 내보내면 콜렉션의 구조는 변경 불가능하지만, 내부 객체까지 새로 복사하지 않는다. 그렇기 때문에 위처럼 복사된 콜렉션의 내부 객체에 접근해서 값을 바꾸면 그 객체의 값이 바뀐다. 이런 복사를 얕은 복사 라고 한다.

 

 

전략 패턴

@FunctionalInterface
public interface DiscountPolicy{
    Money apply(Money price);
}
public class BronzeDiscount implements DiscountPolicy{
    @Override
    public Money apply(Money price) {
        return price.multi(Customer.Grade.BRONZE.getDiscountRate());
    }
}
public class SilverDiscount implements DiscountPolicy{
    @Override
    public Money apply(Money price) {
        return price.multi(Customer.Grade.SILVER.getDiscountRate());
    }
}
    private Money gradeDiscount(Money totalPrice, Customer customer, List<Discount> discountList) {

        Money discountMoney = new Money(0);

        switch (customer.getGrade()) {
            case BRONZE -> { discountMoney = PriceCalculator.calculate(totalPrice, new BronzeDiscount()); }
            case SILVER -> { discountMoney = PriceCalculator.calculate(totalPrice, new SilverDiscount()); }
            case GOLD -> { discountMoney = PriceCalculator.calculate(totalPrice, new GoldDiscount()); }
            case PLATINUM -> { discountMoney = PriceCalculator.calculate(totalPrice, new PlatinumDiscount()); }
        }

        String discountName = customer.getGrade() + " 등급 할인";
        discountList.add(new Discount(discountName, customer.getGrade().getDiscount(), discountMoney));

        return totalPrice.sub(discountMoney);
    }

 

 유저의 등급에 따라 등급 별 할인정책을 적용하기 위해서 전략 패턴을 적용했다. 이를 이용해서 if-else 코드를 많이 줄일 수 있었다.

 

 

record 사용

public record Discount(String name, int rate, Money discountMoney) {

    public Discount(String name, int rate, Money discountMoney) {
        this.name = name;
        this.rate = rate;
        this.discountMoney = new Money(discountMoney.getAmount());
    }
}

 

 Discount 객체는 상품을 주문했을때, 그 주문에 저장된 할인들을 저장하기 위한 객체이다. 데이터를 들고 있는 역할만 하기 때문에 record 객체로 선언했다.

 

 

동작 파라미터화

    public static <T> List<T> filter(List<T> items, Predicate<T> condition) {
        List<T> result = new ArrayList<>();
        for (T item : items) {
            if (condition.test(item)) {
                result.add(item);
            }
        }
        return result;
    }
        List<Product> products = category.getProductsByCategory(categoryEnum);
        if (ui.getScanInt() == 2 ) {
            if (!products.isEmpty()) {
                Predicate<Product> condition =
                        ui.getFilterOver()
                                ? x -> x.getPrice().isOver(ui.getFilterPrice())
                                : x -> x.getPrice().isLessOrSame(ui.getFilterPrice());

                products = Util.filter(products ,condition);
        }

 

동작을 파라미터로 바꿔서 위와 같이 코드해서, Category 내에 여러 filter 메서드를 만들지 않고 위와 같이 구현했다.