이번 포스팅은 GPT와 함께 자바 구현 문제를 구성해서 문제를 내가 직접 풀어보는 식으로 공부를 진행해보았다. 이런식으로 여러번 했었는데 이렇게 공부하는게 정말 많이 도움되는 것 같다. 아무튼 이것도 포스팅으로 한번 남겨보려고 한다.
이번 문제는 주문 관리 시스템이였다. 많이 단순화된 주문 관리 시스템이지만, 우테크에서 적용하는 컨벤션을 따르려고 노력하면서 문제를 풀어보았다.
최근에 풀어보았던 문제처럼 Model, View, Controller로 역할을 나눈 후 기능명세서를 구현하였다. 주요 기능을 설계하고 설계된 기능에서 세부 로직은 그때 그때 단위로 분리해주는 방식으로 코드를 구현하였다.
패키지는 일반적인 MVC 패턴으로 구현하였다.
public class Order {
private Long productId;
private String productName;
private Integer quantity;
private Integer price;
public Order(Long productId, String productName, Integer quantity, Integer price) {
this.productId = productId;
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
public void updateOrder(Integer quantity, Integer price) {
this.quantity = quantity;
this.price = price;
}
public Long getProductId() {
return productId;
}
public String getProductName() {
return productName;
}
public Integer getQuantity() {
return quantity;
}
public Integer getPrice() {
return price;
}
}
Order 클래스는 새로운 주문을 생성자를 통해 생성하고 updateOrder를 통해 기존의 Order를 수정하는 기능을 구현하였다. 해당 문제가 구현의 난이도가 그렇게 높지 않은 편이다보니 Order에서 구현할 기능이 많지 않았다.
import java.util.ArrayList;
import java.util.List;
public class OrderManager {
private List<Order> orders = new ArrayList<>();
public void addOrder(Order order) {
this.orders.add(order);
}
public void deleteOrder(Long productId) {
Order findOrder = findOrderByProductId(productId);
orders.remove(findOrder);
}
public Integer readAllOrderPrice() {
return orders.stream()
.mapToInt(Order::getPrice)
.sum();
}
private Order findOrderByProductId(Long productId) {
return orders.stream()
.filter(order -> order.getProductId().equals(productId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("해당 Order는 존재하지 않습니다."));
}
public List<Order> getOrders() {
return orders;
}
}
주문목록을 관리하는 OrderManager 클래스이다. 요구사항이 많지않아 주문목록만 관리하는 클래스가 되었는데 해당 클래스에서 수행하는 주요 기능은 다음과 같다.
주요 기능은 3가지로 구성되어있고 기능단위로 분리된 메서드가 하나 있는데, 이는 JPA에서 findBy와 같이 pid를 통해 List에서 해당 Order를 가져오는 함수이다.
이런식으로 자바 구현 문제를 풀다보면 객체지향사고 뿐만 아니라 JPA와 같은 관리 툴의 기본적인 핵심들을 이해할 수 있는 것 같아 좋다.
View는 우테크의 4주차 문제를 참고하여 InputView, OutputView로 나누었다.
package practice1.view;
import practice1.model.Order;
import java.util.Map;
import java.util.Scanner;
public class InputView {
public static final String PRODUCT_ID = "pid";
public static final String QUANTITY = "quantity";
public static final String PRICE = "price";
private final Scanner sc;
public InputView() {
this.sc = new Scanner(System.in);
}
public Order inputOrder() {
System.out.println("상품의 이름, 수량, 가격, PID를 공백으로 구분하여 차례대로 입력해주세요");
String[] orderArr = sc.nextLine().split(" ");
return new Order(
Long.parseLong(orderArr[3]),
orderArr[0],
Integer.parseInt(orderArr[1]),
Integer.parseInt(orderArr[2]));
}
public Long inputDeleteOrder() {
System.out.println("주문목록에 삭제할 상품의 PID를 입력해주세요.");
return Long.parseLong(sc.nextLine());
}
public Map<String, Object> inputUpdateOrder() {
System.out.println("주문목록의 수정할 상품 PID와 수량, 가격을 공백으로 구분하여 차례대로 입력해주세요");
String[] updateOrderArr = sc.nextLine().split(" ");
return Map.of(
PRODUCT_ID, Long.parseLong(updateOrderArr[0]),
QUANTITY, Integer.parseInt(updateOrderArr[1]),
PRICE, Integer.parseInt(updateOrderArr[2]));
}
public Integer selectProcess() {
return Integer.parseInt(sc.nextLine());
}
public void closeScanner() {
sc.close();
}
}
InputView는 주문목록에 주문을 생성,삭제,수정 과 같은 프로세스를 수행시키기위한 파라미터를 입력받는 형태로 만들었다. 이렇게 구현을 하다보니 의문점이 생긴건 콘솔에 출력하는 sout 를 OutputView로 따로 구성해야하나? 였다. 이 부분은 나중에 코드리뷰를 하거나 스터디를 진행하면 질문을 던져보려 한다.
update메서드의 경우 객체를 하나 더 생성할까 했지만, 소규모 애플리케이션에서 클래스를 굳이 하나 더 만드는 느낌이여서 Map으로 처리하였다.
import practice1.model.Order;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class OutputView {
public void printOrderProcess() {
System.out.println("==============================");
System.out.println("1.주문 생성 2.주문 수정 3.주문 삭제 4.주문목록 조회 5.주문 총 금액 조회 / 0. 종료");
System.out.println("==============================");
}
public void printOrderList(List<Order> orders) {
AtomicInteger i = new AtomicInteger(1);
orders.forEach((order) -> {
System.out.printf("===== 상품 %s =====\n", i.getAndIncrement());
System.out.printf("PID : %s\n", order.getProductId());
System.out.printf("주문 상품 이름 : %s\n", order.getProductName());
System.out.printf("주문 상품 수량 : %s\n", order.getQuantity());
System.out.printf("주문 상품 가격 : %s\n", order.getPrice());
});
}
public void printAllOrderPrice(Integer sumPrice) {
System.out.printf("주문 총 금액 : %s\n", sumPrice);
}
public void printOrderStart() {
System.out.println("===== 안녕하세요! 주문을 시작합니다 =====");
}
}
OutputView는 콘솔에 출력되는 기본 메세지, 메서드 결과를 print 하는 기능을 수행하게끔 구현하였다.
package practice1.controller;
import practice1.model.Order;
import practice1.model.OrderManager;
import practice1.view.InputView;
import practice1.view.OutputView;
import java.util.List;
import java.util.Map;
public class OrderController {
public static final String PRODUCT = "pid";
public static final String QUANTITY = "quantity";
public static final String PRICE = "price";
private final InputView inputView;
private final OutputView outputView;
private final OrderManager orderManager;
public OrderController(InputView inputView, OutputView outputView, OrderManager orderManager) {
this.inputView = inputView;
this.outputView = outputView;
this.orderManager = orderManager;
}
public void run() {
outputView.printOrderStart();
while (true) {
outputView.printOrderProcess();
Integer select = inputView.selectProcess();
if (select.equals(1)) {
addOrder();
} else if (select.equals(2)) {
updateOrder();
} else if (select.equals(3)) {
deleteOrder();
} else if (select.equals(4)) {
readOrder();
} else if (select.equals(5)) {
readSumPrice();
} else if (select.equals(0)) {
break;
} else {
throw new IllegalArgumentException("프로세스에 존재하지 않는 번호입니다.");
}
}
}
// 주문 총 금액 조회
private void readSumPrice() {
Integer sumPrice = orderManager.readAllOrderPrice();
outputView.printAllOrderPrice(sumPrice);
}
// 주문 목록 조회
private void readOrder() {
outputView.printOrderList(orderManager.getOrders());
}
// 주문 삭제
private void deleteOrder() {
Long productId = inputView.inputDeleteOrder();
orderManager.deleteOrder(productId);
}
// 주문 추가
private void addOrder() {
Order order = inputView.inputOrder();
orderManager.addOrder(order);
}
// 주문 수정
private void updateOrder() {
Map<String, Object> orderMap = inputView.inputUpdateOrder();
Long pid = (Long) orderMap.get(PRODUCT);
Integer quantity = (Integer) orderMap.get(QUANTITY);
Integer price = (Integer) orderMap.get(PRICE);
List<Order> orders = orderManager.getOrders();
Order findOrder = findOrderByProductId(pid, orders);
updateProcess(quantity, price, orders, findOrder);
}
private static void updateProcess(Integer quantity, Integer price, List<Order> orders, Order findOrder) {
orders.remove(findOrder);
findOrder.updateOrder(quantity, price);
orders.add(findOrder);
}
private static Order findOrderByProductId(Long pid, List<Order> orders) {
return orders.stream()
.filter(order -> order.getProductId().equals(pid))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("해당 Order는 존재하지 않습니다."));
}
}
controller의 경우 처음에는 run() 메서드 하나로 관리하려했지만, 단일책임에서 많이 벗어나는 것 같아 기능 단위로 controller도 분리하게 되었다.
어쨌든 controller의 역할은 Model과 View의 통로라고 생각하면서 코드를 구현하였다. 우테크 프리코스에서는 switch-case문 사용을 하지 않는 것을 원칙으로 했기에 이번 구현도 사용하지 않고 if-else 문을 이용해 각각의 기능 단위의 코드들을 수행시켰다.
import practice1.controller.OrderController;
import practice1.model.OrderManager;
import practice1.view.InputView;
import practice1.view.OutputView;
public class Application {
static InputView inputView = new InputView();
static OutputView outputView = new OutputView();
static OrderManager orderManager = new OrderManager();
public static void main(String[] args) {
try {
OrderController orderController = new OrderController(inputView, outputView, orderManager);
orderController.run();
} finally {
inputView.closeScanner();
}
}
}
Controller에 의존성을 주입하여 run()메서드를 실행하였다. 추가적으로 메모리 누수 방지를 위한 closeScanner() 메서드를 구현하여 사용하였다.
일단 우테크 프리코스 문제에 비하면 난이도는 훨씬 쉬웠지만, 역시 설계에 대한 고민을 안할 수 없었다. 하지만 구현 문제를 계속 풀어보면서 설계를 어떻게 해야 할지에 대한 감을 많이 잡은 것 같아 다행이다.
GPT를 이용해 공부하는 것은 상당히 효율적이라고 생각한다. 인공지능을 제대로 이용할 줄 아는 것이 현재 시대에서 살아남을 수 있는 법이라고 생각하기에 더욱 다양한 방법을 통해 새롭게 공부해봐야겠다. 다음에는 난이도를 조금 더 올려서 문제를 구성하고 구현해보아야겠다.