간단한 커머스 프로그램을 만들면서 최근 공부한 다양한 객체 활용법들을 적용해보았다.
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 메서드를 만들지 않고 위와 같이 구현했다.
'Java > 공부 & 트러블 슈팅' 카테고리의 다른 글
| 제네릭을 통해 계산기 프로그램의 기능 확장 (0) | 2026.06.08 |
|---|