ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,73 @@
+package store.inventory.domain;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import store.exception.AdditionalBenefitException;
+import store.exception.PromotionNotAvailableException;
+
+public class PromotionHandler {
+
+    private static final int ZERO = 0;
+
+    private final Stock stock;
+    private Optional<Promotion> promotion;
+
+    public PromotionHandler(final Promotion promotion, final Stock stock) {
+        this.promotion = Optional.ofNullable(promotion);
+        this.stock = stock;
+    }
+
+    public boolean isApplicable(final int quantity, final LocalDate today) {
+        return promotion.isPresent() && promotion.get().isApplicable(quantity, today);
+    }
+
+    public int calculateFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateFreeQuantity(quantity, stock.getPromotionStock())).orElse(0);
+    }
+
+    public void applyDiscount(final int quantity, String productName) {
+        final int promotionQuantity = calculatePromotionStock(quantity);
+        final int nonPromotionQuantity = quantity - promotionQuantity;
+
+        stock.drainDuringPromotionPeriod(promotionQuantity);
+        if (nonPromotionQuantity > ZERO) {
+            throw new PromotionNotAvailableException(productName, nonPromotionQuantity);
+        }
+    }
+
+    public void checkAdditionalBenefitEligibility(final int quantity, final String productName) {
+        final int additionalEligibleQuantity = calculateRemainingFreeQuantity(quantity);
+        if (isEligibleForAdditionalBenefit(additionalEligibleQuantity, quantity)) {
+            throw new AdditionalBenefitException(productName, additionalEligibleQuantity);
+        }
+    }
+
+    public void changePromotion(final Promotion promotion) {
+        this.promotion = Optional.ofNullable(promotion);
+    }
+
+    public String getPromotionName() {
+        return promotion.map(Promotion::getName).orElse("");
+    }
+
+    private int calculateRemainingFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateRemainingFreeQuantity(quantity)).orElse(ZERO);
+    }
+
+    private boolean isEligibleForAdditionalBenefit(final int additionalEligibleQuantity, final int quantity) {
+        final int availablePromotionStock = calculatePromotionStock(quantity);
+        final int totalRequiredStock = calculateTotalRequiredStock(additionalEligibleQuantity, availablePromotionStock);
+
+        return additionalEligibleQuantity > ZERO && stock.getPromotionStock() >= totalRequiredStock;
+    }
+
+    private int calculatePromotionStock(final int quantity) {
+        return promotion.map(promo -> promo.calculateAvailablePromotionStock(quantity, stock.getPromotionStock()))
+                .orElse(ZERO);
+    }
+
+    private int calculateTotalRequiredStock(final int additionalEligibleQuantity, final int availablePromotionStock) {
+        final int promotionSetSize = promotion.get().getPromotionSetSize();
+        return availablePromotionStock + additionalEligibleQuantity * promotionSetSize;
+    }
+}

리뷰: 헉 기본적인 개념을 놓치고 있었네요. 조언 감사합니다!
@@ -0,0 +1,56 @@
+package store.inventory.domain;
+
+import static store.exception.messages.ErrorMessage.INSUFFICIENT_STOCK;
+
+public class Stock {
+
+    private int generalStock;
+    private int promotionStock;
+
+    public Stock(final int generalStock, final int promotionStock) {
+        this.generalStock = generalStock;
+        this.promotionStock = promotionStock;
+    }
+
+    public void useGeneralStock(final int quantity) {
+        validateGeneralStock(quantity);
+        generalStock -= quantity;
+    }
+
+    public void drainDuringPromotionPeriod(final int totalQuantity) {
+        final int remainingPromo = usePromotionStock(totalQuantity);
+        useGeneralStock(remainingPromo);
+    }
+
+    public void addGeneralStock(final int quantity) {
+        this.generalStock += quantity;
+    }
+
+    public void addPromotionStock(final int quantity) {
+        this.promotionStock += quantity;
+    }
+
+    public int getGeneralStock() {
+        return generalStock;
+    }
+
+    public int getPromotionStock() {
+        return promotionStock;
+    }
+
+    public int getTotalStock() {
+        return generalStock + promotionStock;
+    }
+
+    private int usePromotionStock(final int quantity) {
+        final int usedStock = Math.min(quantity, promotionStock);
+        promotionStock -= usedStock;
+        return quantity - usedStock;
+    }
+
+    private void validateGeneralStock(final int quantity) {
+        if (quantity > generalStock) {
+            throw new IllegalArgumentException(INSUFFICIENT_STOCK.getMessage());
+        }
+    }
+}

리뷰: 프로모션 재고를 사용하는 것이 중점이라 생각하여 해당 네이밍으로 메서드명을 명명했었는데, 부족하면 일반 재고를 사용한다라는 의미를 포함하여 메서드명을 정했어도 됐을 것 같네요! 한번 더 생각할 기회를 주셔서 감사합니다
@@ -0,0 +1,52 @@
+package store.io;
+
+import store.object.Receipt;
+import store.product.Product;
+
+import java.util.List;
+
+public class OutputView {
+    public void greeting() {
+        System.out.println("안녕하세요. W편의점입니다.");
+        System.out.println("현재 보유하고 있는 상품입니다.\n");
+    }
+
+    public void printProducts(List<Product> products) {
+        products.forEach(System.out::println);
+    }
+
+    public void purchaseGuide() {
+        System.out.println("\n구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])");
+    }
+
+    public void promotionAdditionalGuide(String productName) {
+        System.out.printf("\n현재 %s은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)\n", productName);
+    }
+
+    public void promotionImpossibleGuide(String productName, int amount) {
+        System.out.printf("\n현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)\n", productName, amount);
+    }
+
+    public void membershipGuide() {
+        System.out.println("\n멤버십 할인을 받으시겠습니다? (Y/N)");
+    }
+
+    public void printReceipt(Receipt receipt) {
+        System.out.println("\n==============W 편의점================");
+        String productReportFormat = "%-19s%-10s%-6s";
+        System.out.printf((productReportFormat) + "%n", "상품명", "수량", "금액");
+        System.out.printf(receipt.productReport());
+        System.out.println("==============증    정================");
+        System.out.printf(receipt.promotionReport());
+        System.out.println("======================================");
+        System.out.printf(receipt.paymentReport());
+    }
+
+    public void closingGuide() {
+        System.out.println("\n감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)");
+    }
+
+    public void printError(String errorMessage) {
+        System.out.println("\n" + errorMessage);
+    }
+}

리뷰: `\n`은 OS에 종속적인 개행 문자인 거 같아요!
`System.lineSeparator()`를 활용해보시면 더 좋을 거 같아요!
@@ -0,0 +1,61 @@
+package store.product;
+
+import store.file.FileReader;
+import store.io.OutputView;
+import store.object.Amount;
+import store.type.ErrorMessage;
+
+import java.util.List;
+import java.util.Map;
+
+public class ProductManager {
+    private final List<Product> products;
+
+    public ProductManager() {
+        FileReader fileReader = new FileReader();
+        products = fileReader.createProduct();
+    }
+
+    public Amount purchase(String name, int amount) {
+        List<Product> purchaseProducts = products.stream()
+                .filter(product -> product.isCorrect(name))
+                .toList();
+
+        isExceed(purchaseProducts, amount);
+        return applyPromotion(purchaseProducts, amount);
+    }
+
+    public Amount applyPromotion(List<Product> purchaseProducts, int amount) {
+        Amount purchaseAmount = purchaseProducts.getFirst().buy(amount);
+        if (purchaseAmount.isAdditional()) {
+            Amount additionalAmount = purchaseProducts.getLast().buy(purchaseAmount.getAdditional());
+            purchaseAmount.addBuyAmount(additionalAmount.getBuy());
+        }
+        return purchaseAmount;
+    }
+
+    public void print() {
+        OutputView outputView = new OutputView();
+        outputView.printProducts(products);
+    }
+
+    public void isExceed(List<Product> purchaseProducts, int amount) {
+        int totalQuantity = 0;
+        for (Product product : purchaseProducts) {
+            totalQuantity += product.getQuantity();
+        }
+        if (totalQuantity < amount) {
+            throw new IllegalArgumentException(ErrorMessage.EXCEED_QUANTITY.getMessage());
+        }
+    }
+
+    public void validate(Map<String, Integer> purchaseData) {
+        for (String productName : purchaseData.keySet()) {
+            boolean exists = products.stream()
+                    .anyMatch(product -> product.isExist(productName));
+            if (!exists) {
+                throw new IllegalArgumentException(ErrorMessage.NON_EXISTING_PRODUCT.getMessage());
+            }
+        }
+    }
+}

리뷰: `ConvenienceStore`에서 `outputview`를 가지고 있는데, 따로 또 생성해서 출력하신 이유가 궁금합니다!
@@ -0,0 +1,18 @@
+package store.type;
+
+public enum ProductType {
+    NAME(0),
+    PRICE(1),
+    QUANTITY(2),
+    PROMOTION(3);
+
+    private final int value;
+
+    ProductType(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+}

리뷰: split한 배열을 접근할 인덱스를 enum으로 만드신 점이 인상적입니다!

`value`는 의미가 명확하지 않은 거 같아요!
`index`는 어떨까요?
@@ -0,0 +1,24 @@
+package store.file;
+
+import store.product.Product;
+import store.promotion.Promotion;
+import store.promotion.PromotionManager;
+
+import java.io.InputStream;
+import java.util.List;
+
+public class FileReader {
+    public List<Product> createProduct() {
+        ProductParser productParser = new ProductParser(new PromotionManager(createPromotion()));
+        return productParser.read(road("products"));
+    }
+
+    public List<Promotion> createPromotion() {
+        PromotionParser promotionParser = new PromotionParser();
+        return promotionParser.read(road("promotions"));
+    }
+
+    public InputStream road(String filename) {
+        return getClass().getClassLoader().getResourceAsStream(filename + ".md");
+    }
+}

리뷰: 혹시 `load`나 `read`를 잘못 작성하신 부분일까요?
@@ -0,0 +1,45 @@
+package store.io;
+
+import camp.nextstep.edu.missionutils.Console;
+import store.type.ErrorMessage;
+
+public class InputView {
+    private final OutputView outputView = new OutputView();
+
+    public String readAnswer() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                answerValidate(input);
+                return input;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public String readPurchaseProduct() {
+        while (true) {
+            try {
+                String input = Console.readLine();
+                purchaseProductValidate(input);
+                return input;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void answerValidate(String input) {
+        if (!"Y".equals(input) && !"N".equals(input)) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_INPUT.getMessage());
+        }
+    }
+
+    public void purchaseProductValidate(String input) {
+        String regex = "\\[([a-zA-Z가-힣]+-[1-9][0-9]*)\\](,\\[([a-zA-Z가-힣]+-[1-9][0-9]*)\\])*";
+        if (!input.matches(regex)) {
+            throw new IllegalArgumentException(ErrorMessage.INVALID_FORMAT.getMessage());
+        }
+    }
+}

리뷰: while-true 부분이 반복되어서 함수형 인터페이스도 활용해보시면 유지보수에 더 좋을 거 같아요!
@@ -0,0 +1,77 @@
+package store.object;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Receipt {
+    private final List<Amount> amounts = new ArrayList<>();
+    private boolean membershipDiscount;
+    private int totalPrice = 0;
+    private int totalBuyAmount = 0;
+    private int promotionPrice = 0;
+    private int membershipAvailablePrice = 0;
+    private int membershipPrice = 0;
+    private int payment = 0;
+
+    public void addAmount(Amount amount) {
+        if (amount.isPurchase()) {
+            amounts.add(amount);
+        }
+    }
+
+    public void membershipApply(String input) {
+        if (input.equals("Y")) {
+            membershipDiscount = true;
+        }
+    }
+
+    public String productReport() {
+        StringBuilder productReport = new StringBuilder();
+        amounts.forEach(amount -> productReport.append(amount.parseProductDetails()));
+        return productReport.toString();
+    }
+
+    public String promotionReport() {
+        StringBuilder promotionReport = new StringBuilder();
+        amounts.forEach(amount -> promotionReport.append(amount.parsePromotionDetails()));
+        return promotionReport.toString();
+    }
+
+    public String paymentReport() {
+        calcPayment();
+        return parsePaymentDetails();
+    }
+
+    public void calcPayment() {
+        amounts.forEach(amount -> totalPrice += amount.totalPrice());
+        amounts.forEach(amount -> totalBuyAmount += amount.getBuy());
+        amounts.forEach(amount -> promotionPrice += amount.promotionPrice());
+        calcMembershipPrice();
+
+        payment = totalPrice - promotionPrice - membershipPrice;
+    }
+
+    public void calcMembershipPrice() {
+        amounts.forEach(amount -> membershipAvailablePrice += amount.membershipAvailablePrice());
+        if (membershipDiscount) {
+            membershipPrice = (int) Math.floor(membershipAvailablePrice * 0.3);
+        }
+        if (membershipPrice > 8000) {
+            membershipPrice = 8000;
+        }
+    }
+
+    public String parsePaymentDetails() {
+        StringBuilder paymentReport = new StringBuilder();
+        String totalPriceFormat = "%-19s%-10d%,-6d";
+        String otherPriceFormat = "%-29s%s%,-6d";
+        return (paymentReport.append(String.format(totalPriceFormat, "총구매액", totalBuyAmount, totalPrice)).append("\n")
+                .append(String.format(otherPriceFormat, "행사할인", "-", promotionPrice)).append("\n")
+                .append(String.format(otherPriceFormat, "멤버십할인", "-", membershipPrice)).append("\n")
+                .append(String.format(otherPriceFormat, "내실돈", "", payment)).append("\n")).toString();
+    }
+
+    public boolean isExistence() {
+        return amounts.size() > 0;
+    }
+}

리뷰: `0.3`과 `8000`는 상수화하시면 의미가 더 명확해질 거 같아요!
@@ -0,0 +1,80 @@
+package store;
+
+import camp.nextstep.edu.missionutils.Console;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import store.object.ConvenienceStore;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ReceiptTest {
+    private static ByteArrayOutputStream outputStream;
+
+    @BeforeEach
+    void setUpStream() {
+        outputStream = new ByteArrayOutputStream();
+        System.setOut(new PrintStream(outputStream));
+    }
+
+    @AfterEach
+    void restoreStream() {
+        Console.close();
+        System.setOut(System.out);
+    }
+
+    @ParameterizedTest
+    @CsvSource(value = {"3,1", "4,1", "10,3", "12,3"})
+    void 영수증으로_고객의_구매와_증정_내역_보기(int amount, int promotionAmount) {
+        String yes = "Y\n";
+        String input = yes + yes;
+        System.setIn(new ByteArrayInputStream(input.getBytes()));
+
+        ConvenienceStore convenienceStore = new ConvenienceStore();
+        convenienceStore.init();
+        Map<String, Integer> purchaseData = Map.of(
+                "콜라", amount
+        );
+        convenienceStore.createReceipt(purchaseData);
+        convenienceStore.showReceipt();
+
+        String expectedProduct = String.format("콜라%d%,d", amount, amount * 1000);
+        String expectedPromotion = String.format("콜라%d", promotionAmount);
+
+        assertTrue(outputStream.toString().replaceAll("\\s", "").contains(expectedProduct));
+        assertTrue(outputStream.toString().replaceAll("\\s", "").contains(expectedPromotion));
+    }
+
+    @Test
+    void 영수증으로_금액_정보_보기() {
+        String yes = "Y\n";
+        String input = yes + yes;
+        System.setIn(new ByteArrayInputStream(input.getBytes()));
+
+        ConvenienceStore convenienceStore = new ConvenienceStore();
+        convenienceStore.init();
+        Map<String, Integer> purchaseData = Map.of(
+                "콜라", 3,
+                "에너지바", 5
+        );
+        convenienceStore.createReceipt(purchaseData);
+        convenienceStore.showReceipt();
+
+        String expectedReceipt = "총구매액813,000";
+        String expectedPromotion = "행사할인-1,000";
+        String expectedMembership = "멤버십할인-3,000";
+        String expectedPayment = "내실돈9,000";
+
+        assertTrue(outputStream.toString().replaceAll("\\s", "").contains(expectedReceipt));
+        assertTrue(outputStream.toString().replaceAll("\\s", "").contains(expectedPromotion));
+        assertTrue(outputStream.toString().replaceAll("\\s", "").contains(expectedMembership));
+        assertTrue(outputStream.toString().replaceAll("\\s", "").contains(expectedPayment));
+    }
+}

리뷰: 꼼꼼한 테스트가 인상적입니다!
사소한 부분이지만, 위에 있는 assert가 실패하면 밑에 있는 assert는 실행되지 않을 거 같아요!
assertAll도 활용해보시면 더 좋을 거 같아요!
@@ -0,0 +1,82 @@
+package store.object;
+
+import store.io.InputParser;
+import store.io.InputView;
+import store.io.OutputView;
+import store.product.ProductManager;
+
+import java.util.Map;
+
+public class ConvenienceStore {
+    private final InputView inputView = new InputView();
+    private final OutputView outputView = new OutputView();
+    private final InputParser inputParser = new InputParser();
+    private final ProductManager productManager = new ProductManager();
+    private Receipt receipt;
+    private boolean isOpen = true;
+
+    public void open() {
+        while (isOpen) {
+            init();
+            announcement();
+            purchase();
+            close();
+            System.out.println();
+        }
+    }
+
+    public void init() {
+        receipt = new Receipt();
+    }
+
+    public void announcement() {
+        outputView.greeting();
+        productManager.print();
+    }
+
+    public void purchase() {
+        outputView.purchaseGuide();
+        while (true) {
+            try {
+                inputHandle();
+                return;
+            } catch (IllegalArgumentException e) {
+                outputView.printError(e.getMessage());
+            }
+        }
+    }
+
+    public void inputHandle() {
+        String purchaseProducts = inputView.readPurchaseProduct();
+        Map<String, Integer> purchaseData = inputParser.mapping(purchaseProducts);
+        productManager.validate(purchaseData);
+        createReceipt(purchaseData);
+    }
+
+    public void createReceipt(Map<String, Integer> purchaseData) {
+        purchaseData.forEach((name, amount) -> {
+            Amount purchaseAmount = productManager.purchase(name, amount);
+            receipt.addAmount(purchaseAmount);
+        });
+        if (receipt.isExistence()) {
+            membership();
+            showReceipt();
+        }
+    }
+
+    public void membership() {
+        outputView.membershipGuide();
+        receipt.membershipApply(inputView.readAnswer().trim());
+    }
+
+    public void showReceipt() {
+        outputView.printReceipt(receipt);
+    }
+
+    public void close() {
+        outputView.closingGuide();
+        if (inputView.readAnswer().equals("N")) {
+            isOpen = false;
+        }
+    }
+}

리뷰: 추출하기보다 그대로 쓰는 게 가독성 측면으로 더 괜찮아보이는데, 어떻게 생각하시나요?
@@ -0,0 +1,61 @@
+package store.product;
+
+import store.file.FileReader;
+import store.io.OutputView;
+import store.object.Amount;
+import store.type.ErrorMessage;
+
+import java.util.List;
+import java.util.Map;
+
+public class ProductManager {
+    private final List<Product> products;
+
+    public ProductManager() {
+        FileReader fileReader = new FileReader();
+        products = fileReader.createProduct();
+    }
+
+    public Amount purchase(String name, int amount) {
+        List<Product> purchaseProducts = products.stream()
+                .filter(product -> product.isCorrect(name))
+                .toList();
+
+        isExceed(purchaseProducts, amount);
+        return applyPromotion(purchaseProducts, amount);
+    }
+
+    public Amount applyPromotion(List<Product> purchaseProducts, int amount) {
+        Amount purchaseAmount = purchaseProducts.getFirst().buy(amount);
+        if (purchaseAmount.isAdditional()) {
+            Amount additionalAmount = purchaseProducts.getLast().buy(purchaseAmount.getAdditional());
+            purchaseAmount.addBuyAmount(additionalAmount.getBuy());
+        }
+        return purchaseAmount;
+    }
+
+    public void print() {
+        OutputView outputView = new OutputView();
+        outputView.printProducts(products);
+    }
+
+    public void isExceed(List<Product> purchaseProducts, int amount) {
+        int totalQuantity = 0;
+        for (Product product : purchaseProducts) {
+            totalQuantity += product.getQuantity();
+        }
+        if (totalQuantity < amount) {
+            throw new IllegalArgumentException(ErrorMessage.EXCEED_QUANTITY.getMessage());
+        }
+    }
+
+    public void validate(Map<String, Integer> purchaseData) {
+        for (String productName : purchaseData.keySet()) {
+            boolean exists = products.stream()
+                    .anyMatch(product -> product.isExist(productName));
+            if (!exists) {
+                throw new IllegalArgumentException(ErrorMessage.NON_EXISTING_PRODUCT.getMessage());
+            }
+        }
+    }
+}

리뷰: ```suggestion
            products.stream()
                    .findAny(product -> product.isExist(productName))
                    .orElseThrow(() -> new IllegalArgumentException());
```

이 형태로 더 간단하게 만드는 방식을 사용해도 괜찮을 것 같습니다.
@@ -0,0 +1,18 @@
+package store.promotion;
+
+import java.util.List;
+
+public class PromotionManager {
+    private final List<Promotion> promotions;
+
+    public PromotionManager(List<Promotion> promotions) {
+        this.promotions = promotions;
+    }
+
+    public Promotion match(String promotion) {
+        return promotions.stream()
+                .filter(p -> p.isMatch(promotion))
+                .findFirst()
+                .orElse(null);
+    }
+}

리뷰: 굳이 null로 리턴하시는 이유가 있을까요? 클라이언트가 null에 대한 대비가 안되어있다면 NPE가 발생할텐데, 여기서 바로 예외를 던지는 것은 어떠신가요?
@@ -0,0 +1,19 @@
+package store.type;
+
+public enum PromotionType {
+    NAME(0),
+    GET(1),
+    BUY(2),
+    START_DATE(3),
+    END_DATE(4);
+
+    private final int value;
+
+    PromotionType(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+}

리뷰: 이걸 굳이 enum으로 만드신 의도가 있나요? 딱히 enumeric해보이지 않습니다..
@@ -1,7 +1,11 @@
 package store;
 
+import store.object.ConvenienceStore;
+
 public class Application {
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        ConvenienceStore convenienceStore = new ConvenienceStore();
+        convenienceStore.open();
+        System.out.println();
     }
-}
+}
\ No newline at end of file

리뷰: 이 부분은 왜 있는건지 궁금합니다!
@@ -0,0 +1,24 @@
+package store.file;
+
+import store.product.Product;
+import store.promotion.Promotion;
+import store.promotion.PromotionManager;
+
+import java.io.InputStream;
+import java.util.List;
+
+public class FileReader {
+    public List<Product> createProduct() {
+        ProductParser productParser = new ProductParser(new PromotionManager(createPromotion()));
+        return productParser.read(road("products"));
+    }
+
+    public List<Promotion> createPromotion() {
+        PromotionParser promotionParser = new PromotionParser();
+        return promotionParser.read(road("promotions"));
+    }
+
+    public InputStream road(String filename) {
+        return getClass().getClassLoader().getResourceAsStream(filename + ".md");
+    }
+}

리뷰: 파일명을 상수로 따로 저장해두면 더 좋을 것 같습니다.