ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,60 @@
+package store.util;
+
+import store.model.domain.customer.Customer;
+import store.model.domain.product.Product;
+import store.model.domain.product.Products;
+
+public class InventoryManager {
+
+	public static void decreaseProductQuantities(Customer customer, Products products) {
+		customer.getBuyingProduct().forEach((productName, quantity) -> {
+			decreaseBuyingProductQuantities(products, productName, quantity);
+		});
+
+		customer.getGiftProduct().forEach((productName, quantity) -> {
+			decreaseGiftProductQuantities(products, productName, quantity);
+		});
+	}
+
+	private static void decreaseBuyingProductQuantities(Products products, String productName, int quantity) {
+		Product promotionProduct = products.findPromotionProductByName(productName);
+		Product regularProduct = products.findRegularProductByName(productName);
+
+		int promotionQuantity = calculatePromotionQuantity(promotionProduct, quantity);
+		int remainingQuantity = quantity - promotionQuantity;
+
+		decreaseProductQuantity(promotionProduct, promotionQuantity);
+		decreaseProductQuantity(regularProduct, remainingQuantity);
+	}
+
+	private static void decreaseGiftProductQuantities(Products products, String productName, int quantity) {
+		Product promotionProduct = products.findPromotionProductByName(productName);
+		Product regularProduct = products.findRegularProductByName(productName);
+
+		int promotionQuantity = calculateGiftPromotionQuantity(promotionProduct, quantity);
+		int remainingQuantity = quantity - promotionQuantity;
+
+		decreaseProductQuantity(promotionProduct, promotionQuantity);
+		decreaseProductQuantity(regularProduct, remainingQuantity);
+	}
+
+	private static int calculatePromotionQuantity(Product promotionProduct, int quantity) {
+		int promotionQuantity = 0;
+		if (promotionProduct != null) {
+			promotionQuantity = Math.min(quantity, promotionProduct.getQuantity());
+		}
+		return promotionQuantity;
+	}
+
+	private static int calculateGiftPromotionQuantity(Product promotionProduct, int quantity) {
+		if (promotionProduct == null)
+			return 0;
+		return Math.min(quantity, promotionProduct.getQuantity());
+	}
+
+	private static void decreaseProductQuantity(Product product, int quantity) {
+		if (product != null && quantity > 0) {
+			product.decreaseQuantity(quantity);
+		}
+	}
+}

리뷰: 이 부분이랑 밑의 getGiftProducts() 부분을 두 개의 void 메서드 분리하셨어도 좋을 것 같아요
@@ -0,0 +1,32 @@
+package store.util;
+
+import static store.constant.AnswerConstant.*;
+
+import store.model.domain.customer.Customer;
+import store.model.domain.product.Products;
+import store.model.domain.product.Promotions;
+import store.service.CalculateService;
+import store.view.InputView;
+import store.view.OutputView;
+import store.model.domain.discount.MembershipDiscount;
+
+public class ReceiptGenerator {
+	public static void generateReceipt(Customer customer, Products products, Promotions promotions, CalculateService calculateService) {
+		int totalPrice = calculateService.calculateBuyProductTotalPrice(customer, products);
+		int promotionDiscount = calculateService.calculatePromotionDiscount(customer, products, promotions);
+		int membershipDiscount = applyMembershipDiscount(totalPrice - promotionDiscount);
+
+		int giftProductTotalPrice = calculateService.calculateGiftProductTotalPrice(customer, products);
+		int finalPrice = totalPrice + giftProductTotalPrice - promotionDiscount - membershipDiscount;
+
+		OutputView.printReceipt(customer, promotionDiscount, membershipDiscount, finalPrice, products);
+	}
+
+	private static int applyMembershipDiscount(int discountedTotal) {
+		OutputView.checkMembershipDiscount();
+		if (InputView.checkDiscount().equals(ANSWER_YES)) {
+			return new MembershipDiscount().discount(discountedTotal);
+		}
+		return 0;
+	}
+}

리뷰: ReceiptGenerator 파일에서 Receipt 도메인이나 DTO 생성이 아닌 OuputView에 접근하는 이유가 어떻게 될까요?? 계산과 출력 로직이 섞여서 비즈니스 로직과 뷰가 분리되지 않고 있는 것 같아요. 단일 책임 원칙이 지켜지지 않는 것 같고 뷰와의 상호작용은 컨트롤러에서 일어나야 된다고 생각합니다!
그리고 개인적으로 caculateService가 있으면 거기서 finalPrice를 받아오도록 하는 것도 좋았을 것 같습니다.
@@ -4,7 +4,9 @@ name,price,quantity,promotion
 사이다,1000,8,탄산2+1
 사이다,1000,7,null
 오렌지주스,1800,9,MD추천상품
+오렌지주스,1800,0,null
 탄산수,1200,5,탄산2+1
+탄산수,1200,0,null
 물,500,10,null
 비타민워터,1500,6,null
 감자칩,1500,5,반짝할인

리뷰: 엇 저는 프로모션이 있는 상품에 밑에 재고가 없는 같은 상품이 없을 경우를 로직에서 처리해주는 게 이번 요구사항이라고 생각했는데 이렇게 하셨군요..!
@@ -0,0 +1,55 @@
+package store.facade;
+
+import java.io.IOException;
+import store.config.StoreInitializer;
+import store.discount.promotion.domain.Promotions;
+import store.discount.promotion.domain.PromotionsImpl;
+import store.item.inventory.Inventory;
+import store.order.domain.Order;
+import store.order.exception.OrderCanceledException;
+import store.presentation.view.input.BuyMoreInputView;
+import store.presentation.view.output.OrderOutputView;
+import store.presentation.view.output.ProductsOutputView;
+import store.presentation.view.output.WelcomeOutputView;
+
+public class ConvenienceStore {
+
+    private final OrderFacade orderFacade;
+    private final Inventory inventory;
+
+    private final WelcomeOutputView welcomeOutputView = new WelcomeOutputView();
+    private final ProductsOutputView productsOutputView = new ProductsOutputView();
+    private final OrderOutputView orderOutputView = new OrderOutputView();
+    private final BuyMoreInputView buyMoreInputView = new BuyMoreInputView();
+
+    public ConvenienceStore(OrderFacade orderFacade, Inventory inventory) {
+        this.orderFacade = orderFacade;
+        this.inventory = inventory;
+    }
+
+    public void setUp() {
+        Promotions PROMOTIONS = new PromotionsImpl();
+        try {
+            StoreInitializer.initialize(PROMOTIONS, inventory);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void enter() {
+        do {
+            welcomeOutputView.print();
+            productsOutputView.print(inventory);
+            orderHandlingCancelException();
+        }
+        while (ExceptionFacade.process(buyMoreInputView::read));
+    }
+
+    private void orderHandlingCancelException() {
+        try {
+            Order order = orderFacade.process();
+            orderOutputView.print(order);
+        } catch (OrderCanceledException exception) {
+        }
+    }
+}

리뷰: 한 클래스에 필드가 너무 많은 것 같다고 느껴집니다
@@ -0,0 +1,66 @@
+package store.order.domain;
+
+public class OrderItem {
+
+    private final String name;
+    private final int price;
+    private int quantity = 0;
+    private int promotionApplied = 0;
+    private int free = 0;
+
+    public OrderItem(String name, int price, int quantity, int free) {
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.free = free;
+    }
+
+    public void setPromotionApplied(int promotionApplied) {
+        this.promotionApplied = promotionApplied;
+    }
+
+    public void setFree(int free) {
+        this.free = free;
+    }
+
+    public void addQuantity(int quantity) {
+        this.quantity += quantity;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getRawPayAmount() {
+        int promotionNotApplied = quantity - promotionApplied;
+        return promotionNotApplied * price;
+    }
+
+    public int getFree() {
+        return free;
+    }
+
+    public int getQuantity() {
+        return quantity;
+    }
+
+    public boolean hasFree() {
+        return free > 0;
+    }
+
+    public int getTotalPrice() {
+        return price * quantity;
+    }
+
+    public int getPromotionDiscountPrice() {
+        return price * free;
+    }
+
+    public static OrderItem ready(String name, int price) {
+        return new OrderItem(name, price, 0, 0);
+    }
+}

리뷰: 필드의 개수를 줄여보면 좋을 것 같습니다
@@ -0,0 +1,62 @@
+package store.item.inventory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+import store.item.domain.Item;
+import store.item.domain.NormalItem;
+import store.item.domain.PromotionItem;
+
+public class InventoryImpl implements Inventory {
+
+    private final List<Item> inventory = new ArrayList<>();
+
+    @Override
+    public void addItem(Item item) {
+        inventory.add(item);
+    }
+
+    @Override
+    public boolean hasItemByName(String itemName) {
+        return inventory.stream()
+            .map(Item::getName)
+            .anyMatch(itemName::equals);
+    }
+
+    @Override
+    public Optional<PromotionItem> findPromotionItemByName(String itemName) {
+        return getByName(itemName)
+            .filter(item -> item instanceof PromotionItem)
+            .map(item -> (PromotionItem) item)
+            .findAny();
+    }
+
+    @Override
+    public Optional<NormalItem> findNormalItemByName(String itemName) {
+        return getByName(itemName)
+            .filter(item -> item instanceof NormalItem)
+            .map(item -> (NormalItem) item)
+            .findAny();
+    }
+
+    private Stream<Item> getByName(String itemName) {
+        return inventory.stream()
+            .filter(item -> item.getName().equals(itemName));
+    }
+
+    @Override
+    public int getTotalStockByName(String itemName) {
+        int totalStock = 0;
+        totalStock += findNormalItemByName(itemName).map(Item::getStock).orElse(0);
+        totalStock += findPromotionItemByName(itemName).map(Item::getStock).orElse(0);
+
+        return totalStock;
+    }
+
+    @Override
+    public List<Item> getAllItems() {
+        return Collections.unmodifiableList(inventory);
+    }
+}

리뷰: ItemRepository를 구현하신 것 같은데 다른 프로그래머가 해당 패키지와 클래스를 더 쉽게 이해하게 하기 위해 관례적으로 많이 사용되는 단어를 사용하는게 어떨까요?
@@ -0,0 +1,14 @@
+package store.item.exception;
+
+import static store.constant.Errors.NOT_FOUND_ITEM;
+
+public class ItemNotFoundException extends IllegalArgumentException {
+
+    public ItemNotFoundException() {
+        super(NOT_FOUND_ITEM.message());
+    }
+
+    public ItemNotFoundException(Throwable cause) {
+        super(NOT_FOUND_ITEM.message(), cause);
+    }
+}

리뷰: 예외 종류를 따로 선언하신 부분 좋네요
이런 방식은 시도해 본적이 없는데 하나 배워갑니다.
@@ -0,0 +1,68 @@
+package store.item.domain;
+
+import static store.presentation.view.Message.PRODUCT;
+import static store.presentation.view.Message.PRODUCT_NO_STOCK;
+
+import java.util.Objects;
+import store.item.exception.NotEnoughStockException;
+
+public abstract class Item implements Comparable<Item> {
+
+    protected final String name;
+    protected final int price;
+    protected int stock;
+
+    public Item(String name, int price, int stock) {
+        this.name = name;
+        this.price = price;
+        this.stock = stock;
+    }
+
+    public void removeStock(int amount) {
+        if (amount > stock) {
+            throw new NotEnoughStockException();
+        }
+        this.stock -= amount;
+    }
+
+    @Override
+    public String toString() {
+        if (stock > 0) {
+            return PRODUCT.get(name, price, stock);
+        }
+        return PRODUCT_NO_STOCK.get(name, price);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getPrice() {
+        return price;
+    }
+
+    public int getStock() {
+        return stock;
+    }
+
+    @Override
+    public int compareTo(Item other) {
+        if (comparingNormalPromotion(other)) {
+            return 1;
+        }
+        if (comparingPromotionNormal(other)) {
+            return -1;
+        }
+        return 0;
+    }
+
+    private boolean comparingPromotionNormal(Item other) {
+        return Objects.equals(name, other.name)
+            && this instanceof PromotionItem && other instanceof NormalItem;
+    }
+
+    private boolean comparingNormalPromotion(Item other) {
+        return Objects.equals(name, other.name)
+            && this instanceof NormalItem && other instanceof PromotionItem;
+    }
+}

리뷰: 이 부분에 대해서 이야기를 나누고 싶습니다. 3주 차 공통 피드백에 일반적인 get, set 사용을 줄이라는 내용이 있었습니다. 이 부분이 정말 어려웠는데, 어떻게 생각하시나요!?
@@ -0,0 +1,39 @@
+package store.constant;
+
+import static store.constant.ConstantNumbers.EXACT_GET_PROMOTION;
+import static store.constant.ConstantNumbers.MAX_BUY_PROMOTION;
+import static store.constant.ConstantNumbers.MAX_LENGTH_ITEM_NAME;
+import static store.constant.ConstantNumbers.MAX_LENGTH_PROMOTION_NAME;
+import static store.constant.ConstantNumbers.MAX_STOCK_PRICE;
+import static store.constant.ConstantNumbers.MIN_BUY_PROMOTION;
+import static store.constant.ConstantNumbers.MIN_LENGTH_ITEM_NAME;
+import static store.constant.ConstantNumbers.MIN_LENGTH_PROMOTION_NAME;
+import static store.constant.ConstantNumbers.MIN_STOCK_PRICE;
+
+public enum Errors {
+
+    NOT_NEGATIVE_QUANTITY("수량은 음수일 수 없습니다."),
+    LENGTH_STOCK_NAME("상품 이름은 %d자에서 %d자 사이여야 합니다.",
+        MIN_LENGTH_ITEM_NAME.get(), MAX_LENGTH_ITEM_NAME.get()),
+    LENGTH_STOCK_PRICE("상품 가격은 최소 %d원에서 %d원 사이여야 합니다.",
+        MIN_STOCK_PRICE.get(), MAX_STOCK_PRICE.get()),
+    LENGTH_PROMOTION_NAME("프로모션 이름은 %d자에서 %d자 사이여야 합니다.",
+        MIN_LENGTH_PROMOTION_NAME.get(), MAX_LENGTH_PROMOTION_NAME.get()),
+    LENGTH_PROMOTION_BUY("프로모션 혜택은 최소 %d개에서 최대 %d까지 구매해야 합니다.",
+        MIN_BUY_PROMOTION.get(), MAX_BUY_PROMOTION.get()),
+    EXACT_PROMOTION_GET("프로모션 혜택 당 증정상품은 반드시 %d개입니다.", EXACT_GET_PROMOTION.get()),
+    NOT_ENOUGH_STOCK("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."),
+    NOT_FOUND_PROMOTION("존재하지 않는 프로모션입니다."),
+    NOT_FOUND_ITEM("존재하지 않는 상품입니다. 다시 입력해 주세요.");
+
+    private static final String PREFIX = "[ERROR] ";
+    private final String message;
+
+    Errors(String messageFormat, Object... args) {
+        this.message = PREFIX + String.format(messageFormat, args);
+    }
+
+    public String message() {
+        return message;
+    }
+}

리뷰: 이렇게 정의하신 부분 엄청 꼼꼼하시다는 느낌이 듭니다 👍👍
@@ -0,0 +1,25 @@
+package store.constant;
+
+public enum ConstantNumbers {
+    MIN_STOCK_PRICE(100),
+    MAX_STOCK_PRICE(100_000),
+    MIN_LENGTH_ITEM_NAME(2),
+    MAX_LENGTH_ITEM_NAME(10),
+    MIN_LENGTH_PROMOTION_NAME(4),
+    MAX_LENGTH_PROMOTION_NAME(12),
+    MIN_BUY_PROMOTION(1),
+    MAX_BUY_PROMOTION(4),
+    EXACT_GET_PROMOTION(1),
+    PERCENT_MEMBERSHIP(30),
+    MAX_MEMBERSHIP(8000);
+
+    private final int value;
+
+    ConstantNumbers(int value) {
+        this.value = value;
+    }
+
+    public int get() {
+        return value;
+    }
+}

리뷰: 항상 Enum을 쓰면서 느끼지만, 매번 getValue() 이렇게 정의했습니다.
하지만 현 코드에서는 단지 get() 이라는 메서드로 값을 추출하는데 협력하는 과정의 의사 소통에 문제가 없울까요?!?! 이 부분이.. 저도 큰 고민이라 ㅠ
@@ -1 +1,64 @@
-# java-convenience-store-precourse
+# 편의점
+
+구매자의 할인 혜택과 재고 상황을 고려하여 최종 결제 금액을 계산하고 안내하는 결제 시스템
+
+## 기능 목록
+
+- [x] 입력
+    - [x] `products.md` 에서 재고를 읽는다.
+    - [x] `promotions.md` 에서 프로모션 할인 정보를 읽는다.
+    - [x] 구매할 상품과 수량을 입력받는다. ex) `[콜라-10],[사이다-3]`
+    - [x] 프로모션 적용 가능 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 그 수량만큼 추가 여부를 입력받는다.
+    - [x] 프로모션 재고가 부족하면 정가로 결제할 지 여부를 입력받는다.
+    - [x] 멤버십 할인 적용 여부를 입력받는다.
+    - [x] 추가 구매 여부를 입력받는다.
+
+- [x] 재고 관리
+    - [x] 고객이 상품을 구매하면 재고를 차감한다.
+    - [x] 일반 재고와 프로모션 재고가 있다.
+
+- [x] 할인
+    - [x] 프로모션 할인
+        - [x] N개 구매시 1개를 무료 증정한다.
+        - [x] 오늘 날짜가 프로모션 기간 내 포함된 경우에만 적용 가능하다.
+        - [x] 하나의 상품에 하나의 프로모션만 적용 가능하다.
+        - [x] 프로모션 재고를 우선 차감하며 프로모션 재고 내에서만 적용 가능하다.
+        - [x] 프로모션 재고가 부족하면 일반 재고에서 정가로 결제해야 한다.
+    - [x] 멤버십 할인
+        - [x] 프로모션 미적용 금액의 30%를 할인받는다.
+        - [x] 프로모션 적용 후 남은 금액에 대해 멤버십 할인을 적용한다.
+        - [x] 멤버십 할인의 최대 한도는 8,000원이다.
+
+- [x] 구매
+    - [x] 재고가 부족하면 구매할 수 없다.
+    - [x] 프로모션 및 멤버십 할인 정책에 따라 최종 결제 금액을 계산한다.
+    - [x] 프로모션 적용 가능 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 필요한 수량을 추가로 가져오면 혜택을 받을 수 있음을 안내한다.
+    - [x] 프로모션 재고가 부족하면 정가로 결제할 지 여부에 대해 안내한다.
+
+  - [x] 출력
+      - [x] 환영 인사와 함께 상품명, 가격, 프로모션 이름, 재고를 안내한다.
+      - [x] 재고가 없으면 `재고 없음`을 출력한다.
+      - [x] 프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제할지 여부에 대한 안내 메시지를 출력한다.
+      - [x] 멤버십 할인 적용 여부 안내 문구를 출력한다.
+      - [x] 영수증 (구매 상품 내역, 증정 상품 내역, 금액 정보)을 출력한다.
+      - [x] 추가 구매 여부 안내 문구를 출력한다.
+      - [x] 사용자가 잘못된 값을 입력했을 때, "[ERROR]"로 시작하는 오류 메시지와 함께 상황에 맞는 안내를 출력한다.
+
+- [x] 검증 목록
+  - [x] 입력
+    - [x] 구매할 상품과 수량 형식을 검증한다.
+    - [x] Y/N 입력에는 Y, N 외에는 올 수 없다.
+    
+  - [x] 재고
+    - [x] 상품 수량은 음수일 수 없다.
+    - [x] 상품 이름은 2자 이상 10자 이하이다.
+    - [x] 상품 가격은 100원 이상 10만원 이하이다.
+
+  - [x] 구매
+    - [x] 구매 수량은 0이하일 수 없다. 
+    - [x] 하나의 상품은 한 번에걸쳐 구매 가능하다.
+    
+  - [x] 프로모션
+    - [x] 프로모션 이름은 4자 이상 12자 이하이다.
+    - [x] 프로모션의 Buy는 1부터 최대 4까지 가능하다.
+    - [x] 프로모션의 Get은 반드시 1이다.
\ No newline at end of file

리뷰: README.md 기능 목록 명세가 상당히 자세하네요 ! 칭찬합니다 :)
@@ -0,0 +1,32 @@
+package store.config;
+
+import store.discount.membership.Membership;
+import store.discount.membership.MembershipImpl;
+import store.facade.ConvenienceStore;
+import store.facade.OrderFacade;
+import store.item.inventory.Inventory;
+import store.item.inventory.InventoryImpl;
+import store.order.service.ConfirmListener;
+import store.order.service.OrderService;
+import store.order.service.OrderServiceImpl;
+import store.presentation.view.input.PromotionAppendInputView;
+import store.presentation.view.input.PromotionFailedInputView;
+
+public class AppConfig {
+
+    public static final Inventory INVENTORY = new InventoryImpl();
+
+    private static final ConfirmListener<String, Integer> promotionAppendListener = new PromotionAppendInputView();
+    private static final ConfirmListener<String, Integer> promotionFailedListener = new PromotionFailedInputView();
+    public static final OrderService ORDER_SERVICE = new OrderServiceImpl(INVENTORY,
+        promotionAppendListener, promotionFailedListener);
+
+    public static final OrderFacade ORDER_FACADE = new OrderFacade(ORDER_SERVICE);
+    public static final ConvenienceStore CONVENIENCE_STORE = new ConvenienceStore(ORDER_FACADE,
+        INVENTORY);
+
+    public static final Membership MEMBERSHIP = new MembershipImpl();
+
+    private AppConfig() {
+    }
+}

리뷰: 이렇게 객체를 상수로 선언하면, 객체가 사용되지 않아도 항상 메모리를 잡아먹고 있을 것이라고 유추가 되는데 혹시 이런 단점에 대해선 어떻게 생각하시고, 이런 단점을 감수하고도 사용하는 이유가 있으실까요?
@@ -0,0 +1,41 @@
+package store.common.collector;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import store.constant.FilePath;
+
+public abstract class FileContentCollector<T> {
+
+    private final FilePath filePath;
+    private long sequence = 0L;
+
+    public FileContentCollector(FilePath filePath) {
+        this.filePath = filePath;
+    }
+
+    public final List<T> collect() throws IOException {
+        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath.get()));
+        bufferedReader.readLine();
+        List<T> result = executeCollect(bufferedReader);
+
+        bufferedReader.close();
+        return result;
+    }
+
+    private List<T> executeCollect(BufferedReader bufferedReader) throws IOException {
+        List<T> instances = new ArrayList<>();
+        while (true) {
+            String line = bufferedReader.readLine();
+            if (line == null) {
+                break;
+            }
+            instances.add(toInstance(line, sequence++));
+        }
+        return instances;
+    }
+
+    protected abstract T toInstance(String line, long sequence);
+}

리뷰: sequence라는 변수를 long 타입으로 선언하신 이유가 있으실까요? 21억 줄 이상의 파일은 입력될 일이 없을텐데 방어적인 코딩이라고 이해하면 될까요?
@@ -0,0 +1,41 @@
+package store.common.collector;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import store.constant.FilePath;
+
+public abstract class FileContentCollector<T> {
+
+    private final FilePath filePath;
+    private long sequence = 0L;
+
+    public FileContentCollector(FilePath filePath) {
+        this.filePath = filePath;
+    }
+
+    public final List<T> collect() throws IOException {
+        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath.get()));
+        bufferedReader.readLine();
+        List<T> result = executeCollect(bufferedReader);
+
+        bufferedReader.close();
+        return result;
+    }
+
+    private List<T> executeCollect(BufferedReader bufferedReader) throws IOException {
+        List<T> instances = new ArrayList<>();
+        while (true) {
+            String line = bufferedReader.readLine();
+            if (line == null) {
+                break;
+            }
+            instances.add(toInstance(line, sequence++));
+        }
+        return instances;
+    }
+
+    protected abstract T toInstance(String line, long sequence);
+}

리뷰: 제네릭 이용하신 점 인상깊습니다 :)
@@ -0,0 +1,41 @@
+package store.common.collector;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import store.constant.FilePath;
+
+public abstract class FileContentCollector<T> {
+
+    private final FilePath filePath;
+    private long sequence = 0L;
+
+    public FileContentCollector(FilePath filePath) {
+        this.filePath = filePath;
+    }
+
+    public final List<T> collect() throws IOException {
+        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath.get()));
+        bufferedReader.readLine();
+        List<T> result = executeCollect(bufferedReader);
+
+        bufferedReader.close();
+        return result;
+    }
+
+    private List<T> executeCollect(BufferedReader bufferedReader) throws IOException {
+        List<T> instances = new ArrayList<>();
+        while (true) {
+            String line = bufferedReader.readLine();
+            if (line == null) {
+                break;
+            }
+            instances.add(toInstance(line, sequence++));
+        }
+        return instances;
+    }
+
+    protected abstract T toInstance(String line, long sequence);
+}

리뷰: 클래스 제네릭 선언은 처음 알게 되었네요! 배워갑니다 !!
@@ -0,0 +1,39 @@
+package store.common.collector;
+
+import store.constant.FilePath;
+import store.discount.promotion.domain.Promotion;
+import store.discount.promotion.domain.Promotions;
+import store.item.domain.Item;
+import store.item.domain.NormalItem;
+import store.item.domain.PromotionItem;
+
+public class ItemCollector extends FileContentCollector<Item> {
+
+    private final Promotions promotions;
+    public static final String PROMOTION_NOTHING = "null";
+
+    public ItemCollector(FilePath filePath, Promotions promotions) {
+        super(filePath);
+        this.promotions = promotions;
+    }
+
+    @Override
+    protected Item toInstance(String line, long sequence) {
+        String[] split = line.trim().split(",");
+        String name = split[0];
+        int price = Integer.parseInt(split[1]);
+        int quantity = Integer.parseInt(split[2]);
+        String promotionName = split[3].trim();
+
+        return generateItem(name, price, quantity, promotionName);
+    }
+
+    private Item generateItem(String name, int price, int quantity, String promotionName) {
+        if (promotionName.equals(PROMOTION_NOTHING)) {
+            return new NormalItem(name, price, quantity);
+        }
+
+        Promotion promotion = promotions.getByName(promotionName);
+        return new PromotionItem(name, price, quantity, promotion);
+    }
+}

리뷰: 제 입장에선 굉장히 생소한 방법이에요..! 혹시 이런 방법으로 구현하신 것에 장점이 무엇이라고 생각하시나요?