개발하는 햄팡이

[JAVA][키오스크 만들기] 완성 후 리팩토링. 본문

Back-End/Java

[JAVA][키오스크 만들기] 완성 후 리팩토링.

hampangee 2025. 5. 1. 04:06

3번째 과제는 키오스크 만들기.

과제가 생각보다 까다로워서 블로그 포스팅을 아얘 못했다..

생각할 것도 너무 많았고 수정할것도 너무 많아서 이 내용을 전부 블로그 글에 쓰기가 힘들었다.

 

어쨌든 일주일동안 고민해서 완성은 했는데

완성하고 제출할려고 보니 SOLID 원칙의 S가 전혀 안지켜지고 있었던 것..

S는 Single Responsibility Principle의 줄임말로 단일 책임 원칙이라는 뜻인데, 각 클래스는 하나의 책임을 가져야 한다는 뜻이다.

 

그런데 내 코드를 보면

Kiosk 클래스에 무슨 고봉밥마냥 입출력, 계산, 메뉴 출력 등 각종 기능을 다 때려박은 것 같은 느낌이라서 이를 분리하기위해 추가 작업을 하려고 한다.

(+ 내가 잘못한건 알겠는데 정확히 뭘 잘못했는지 모를땐 chatGpt한테 내가 쓴 코드 SOLID원칙을 지킨 것 같니..? 라고 물어보면 신랄하게 비판해준다.)

 

https://github.com/GyeongSe99/java_kiosk

 

GitHub - GyeongSe99/java_kiosk: 내일배움캠프 Chapter3. 자바 프로젝트 - 키오스크 만들기

내일배움캠프 Chapter3. 자바 프로젝트 - 키오스크 만들기. Contribute to GyeongSe99/java_kiosk development by creating an account on GitHub.

github.com

 

전체 코드는 위의 링크에 있는데 kiosk_lv6까지가 딱 요구사항을 구현한 내용이고 Kiosk안에 고봉밥이 들어있는 문제의 구조이다.

그래서 새벽2시...

이런 거슬리는걸 못참는 나는 리팩토링을 하고 자려고 한다..!!

 


일단 내가 보기에 가장 큰 문제는

뭐 확장성을 위해 인터페이스 쓰는건 일단 넘기고

class만 사용해서 구현한다고 하더라도 Kiosk의 책임이 너무 많다..!!

일단 하나의 메소드만 분리하면 되는 입력메소드인

private int readIntInRange(int min, int max) {
    int input;
    while (true) {
        try {
            input = sc.nextInt();
            if (input < min || input > max) {
                System.out.println("범위를 벗어났습니다. 다시 시도해주세요.");
                continue;
            }
            return input;
        } catch (InputMismatchException e) {
            System.out.println("정수만 입력해주세요.");
            sc.nextLine();
        } catch (Exception e) {
            System.out.println("시스템 오류가 발생했습니다.");
            sc.nextLine();
        }
    }
}

 

이 부분을 다른 클래스로 분리하기로 했다. 

이전 계산기 클래스에서는 input을 받는 메소드의 매개변수에 prompt도 넣어 적재적소에 사용할 수 있게 했는데,

지금은 prompt다음에 메뉴를 출력한다음 입력을 받아야하는 부분도 있어서 prompt는 안받는 걸로 했다.

대신 메뉴의 수에 따라서 선택할 수 있는 범위가 달라지기때문에 min과 max를 설정하여 해당 범위를 넘어가면 exception을 발생하도록 했다.

그리고 각종 입출력 Exception도 해당 부분에서 처리.

 

InputReader라는 클래스를 만들어 Kiosk에서 불러와서 사용할 수 있도록 했다.

그렇게 했더니 exit메소드에서 원래 scanner를 닫아줬었는데 어떻게 해야될지 몰라서 그냥 종료하도록 함...

package kiosk;

import java.util.InputMismatchException;
import java.util.Scanner;

public class InputReader {
    private Scanner sc;

    public InputReader() {
        this.sc = new Scanner(System.in);
    }

    public int readIntInRange(int min, int max) {
        int input;
        while (true) {
            try {
                input = sc.nextInt();
                if (input < min || input > max) {
                    System.out.println("범위를 벗어났습니다. 다시 시도해주세요.");
                    continue;
                }
                return input;
            } catch (InputMismatchException e) {
                System.out.println("정수만 입력해주세요.");
                sc.nextLine();
            } catch (Exception e) {
                System.out.println("시스템 오류가 발생했습니다.");
                sc.nextLine();
            }
        }
    }
}

 

 

일단 책임 하나 덜었다....

 


 

 

그 다음으로 따로 뺄 수 있는 기능이 뭐가 있을까 고민을 해봤는데

분기 처리하는 부분이랑 키오스크에 선택지 출력하는 것 정도는 Kiosk가 할일이니깐 클래스에 있어도 될 것 같은데

주문 기능이 같이 있는게 좀 맘에 들지 않았다..

 

그래서  handleOrder()메소드를 분리하려고 봤더니

굉장히 많은 출력과 로직이 뒤섞여있었다...

얘를 어떻게 빼내야할까...? 생각을 해봤는데 일단 얘를 OrderService라는 클래스로 따로 빼낸다고 해도

또 Kiosk에서도 MenuList의 메소드를 봐야할때도 있고... 그래서 하나만 생성해서 각 클래스가 다같이 공유하는 형태여야한다. (이게 싱글톤 패턴이 나오게 된 계기인 것 같다...)

일단 main함수에서 객체를 만들때 넣어주는 형태로 수정했다.

 

MenuList, OrderService, InputReader는 하나 만들어놓고 돌려쓰면되고, Cart도 어차피 Kiosk만들때 한번 만들고 끝이었으니깐 main에서 한번 선언하고 다른 곳에서도 계속 쓸 수 있도록 했다.

 

원래 main함수는 

package kiosk;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Menu burgers = new Menu("Burgers");
        Menu drinks = new Menu("drinks");
        Menu desserts = new Menu("desserts");

        burgers.addMenuItem("ShackBurger", 6900, "토마토, 양상추, 쉑소스가 토핑된 치즈버거");
        burgers.addMenuItem("SmokeShack", 8900, "베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거");
        burgers.addMenuItem("Cheeseburger", 6900, "포테이토 번과 비프패티, 치즈가 토핑된 치즈버거");
        burgers.addMenuItem("Hamburger", 5400, "비프패티를 기반으로 야채가 들어간 기본버거");

        drinks.addMenuItem("Orange juice", 2000, "미닛메이드 오렌지 주스");
        drinks.addMenuItem("Coke", 1500, "코카콜라");
        drinks.addMenuItem("Zero Coke", 1500, "제로 콜라");
        drinks.addMenuItem("Sprite", 1500, "스프라이트");

        desserts.addMenuItem("Soft Serve Cone", 1800, "바닐라 소프트 아이스크림이 담긴 콘");
        desserts.addMenuItem("Apple Pie", 2500, "달콤한 사과 필링이 가득한 따뜻한 파이");
        desserts.addMenuItem("Chocolate Sundae", 3200, "초콜릿 소스와 견과류 토핑이 어우러진 선데이");
        desserts.addMenuItem("Strawberry Shake", 4000, "신선한 딸기 과육이 들어간 밀크쉐이크");
        desserts.addMenuItem("Brownie", 2800, "촉촉한 초코 브라우니로 진한 초콜릿 맛");

        List<Menu> menuList = new ArrayList<>();
        menuList.add(burgers);
        menuList.add(drinks);
        menuList.add(desserts);

        Kiosk kiosk = new Kiosk(menuList);
        kiosk.start();
    }
}

이따구였는데...

아래처럼 수정했다.

 

package kiosk;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Menu> menuList = createMenus();
        Cart cart = new Cart();
        InputReader inputReader = new InputReader();
        OrderService orderService = new OrderService();
        Kiosk kiosk = new Kiosk(menuList, cart, inputReader, orderService);
        kiosk.start();
    }

    private static List<Menu> createMenus() {
        List<Menu> menuList = new ArrayList<>();

        Menu burgers = new Menu("Burgers");
        Menu drinks = new Menu("drinks");
        Menu desserts = new Menu("desserts");

        burgers.addMenuItem("ShackBurger", 6900, "토마토, 양상추, 쉑소스가 토핑된 치즈버거");
        burgers.addMenuItem("SmokeShack", 8900, "베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거");
        burgers.addMenuItem("Cheeseburger", 6900, "포테이토 번과 비프패티, 치즈가 토핑된 치즈버거");
        burgers.addMenuItem("Hamburger", 5400, "비프패티를 기반으로 야채가 들어간 기본버거");

        drinks.addMenuItem("Orange juice", 2000, "미닛메이드 오렌지 주스");
        drinks.addMenuItem("Coke", 1500, "코카콜라");
        drinks.addMenuItem("Zero Coke", 1500, "제로 콜라");
        drinks.addMenuItem("Sprite", 1500, "스프라이트");

        desserts.addMenuItem("Soft Serve Cone", 1800, "바닐라 소프트 아이스크림이 담긴 콘");
        desserts.addMenuItem("Apple Pie", 2500, "달콤한 사과 필링이 가득한 따뜻한 파이");
        desserts.addMenuItem("Chocolate Sundae", 3200, "초콜릿 소스와 견과류 토핑이 어우러진 선데이");
        desserts.addMenuItem("Strawberry Shake", 4000, "신선한 딸기 과육이 들어간 밀크쉐이크");
        desserts.addMenuItem("Brownie", 2800, "촉촉한 초코 브라우니로 진한 초콜릿 맛");

        menuList.add(burgers);
        menuList.add(drinks);
        menuList.add(desserts);

        return menuList;
    }
}

 

 


 

 

그리고 이제 진짜로 비지니스 로직을 따로 분리할 차례..!!

 

아래 부분이 좀 문제라고 생각이 드는데

나는 Kiosk에서는 콘솔 출력과 흐름 제어만 하고 싶은데 아래 부분은 할인율을 적용하는 로직도 있는 것 처럼 보인다..

뭔가 Kiosk가 또 다른 일을 하고 있는 느낌?

/**
 * 장바구니 내역 및 총 금액 확인 후 최종 주문
 */
private void handleOrder() {
    System.out.println("아래와 같이 주문 하시겠습니까?");
    System.out.println();
    cart.showCartItems();
    int totalPrice = cart.showTotalPrice();
    System.out.println("1. 주문        2. 메뉴판");

    int selectedNum = readIntInRange(1, 2);
    if (selectedNum == 1) {
        System.out.println("할인 정보를 입력해주세요.");
        Stream.of(UserType.values())
                .forEach(type -> System.out.printf("%d. %s : %d%%%n", type.ordinal()+1, type.getLabel(), type.getRate()));
        int selectedNumForUserType = readIntInRange(1, UserType.values().length);
        UserType userType = UserType.values()[selectedNumForUserType - 1];
        double discounted = totalPrice * (1 - userType.getRate() / 100.0);
        System.out.printf("주문이 완료되었습니다. 금액은 W %.1f 입니다.%n", (double) discounted / 1000);
        cart.resetCartItems();
    }
}

유저타입 보여주고 등등 주문이 Kiosk클래스에서 진행되는 것 처럼 보인다.

그래서 주문을 담당하는 클래스를 따로 분리!

콘솔 보여주는 부분도 일단 메소드 분리를 해주었다.

 

/**
 * 장바구니 내역 및 총 금액 확인 후 최종 주문
 */
private void handleOrder() {
    showCartInfo();

    int selectedNum = inputReader.readIntInRange(1, 2);
    if (selectedNum == 1) {
        showDiscountSelection();
        int selectedNumForUserType = inputReader.readIntInRange(1, UserType.values().length);
        orderService.order(selectedNumForUserType);
    }
}

private void showCartInfo() {
    System.out.println("아래와 같이 주문 하시겠습니까?");
    System.out.println();
    cart.showCartItems();
    System.out.println("1. 주문        2. 메뉴판");
}

private void showDiscountSelection() {
    System.out.println("할인 정보를 입력해주세요.");
    Stream.of(UserType.values())
            .forEach(type -> System.out.printf("%d. %s : %d%%%n", type.ordinal() + 1, type.getLabel(), type.getRate()));
}

 

 

해당 부분은 이렇게 바뀌고 order부분은 이런식으로 바꾸었다.

package kiosk;

public class OrderService {
    private final Cart cart;

    public OrderService(Cart cart) {
        this.cart = cart;
    }

    public double applyDiscount(int selectedNum, int totalPrice) {
        UserType userType = UserType.values()[selectedNum - 1];
        return totalPrice * (1 - userType.getRate() / 100.0);
    }


    public double order(int selectedNumForUserType) {
        double discounted = applyDiscount(selectedNumForUserType, cart.showTotalPrice());
        System.out.printf("주문이 완료되었습니다. 금액은 W %.1f 입니다.%n", discounted / 1000);
        cart.resetCartItems();
        return discounted;
    }
}

 

그 다음으로는 

private void showDiscountSelection() {
    System.out.println("할인 정보를 입력해주세요.");
    Stream.of(UserType.values())
            .forEach(type -> System.out.printf("%d. %s : %d%%%n", type.ordinal() + 1, type.getLabel(), type.getRate()));
}

 

이 할인율 선택할 수 있는 리스트를 보여주는 부분은 UserType을 순회하기 때문에 그 쪽에 있어야 할 것 같아서 그것도 수정했다.

    
// Kiosk class
private void showDiscountSelection() {
    System.out.println("할인 정보를 입력해주세요.");
    UserType.showUserTypeMenu();
}


// UserType enum
public static void showUserTypeMenu() {
    Stream.of(UserType.values())
            .forEach(type -> System.out.printf("%d. %s : %d%%%n", type.ordinal() + 1, type.getLabel(), type.getRate()));
}

 

 


 

일단 리팩토링은 여기까지 했는데

사실 콘솔 출력하는 부분이랑 흐름 제어를 하는 부분도 분리되어 있어야 하는 것도 알고,

확장성을 위해 인터페이스 어쩌구도 알고, 다른 할인 정책을 사용할 수도 있으니 할인 정책을 주입받고 어쩌구 등등 다 알고 있지만

 

개발 외에 다른 것들도 공부해야하는데 리팩토링이 막 뚝딱뚝딱 되는 것도 아니고...

사실 이 블로그 몇줄 쓰기 위해서 코드를 어떻게 수정해야될까 구조를 어떻게 잡아야할까 많은 고민을 한다.

 

따라서 과제는 이쯤에서 마무리하고
제출..!

피드백 받으면 그때 수정해 봅시다~