2025. 1. 17. 19:46ㆍ프로그래밍 언어/Java
개요
GitHub - mixedsider/KioskProject: [내배캠] 키오스크 과제
[내배캠] 키오스크 과제. Contribute to mixedsider/KioskProject development by creating an account on GitHub.
github.com
키오스크 과제를 하면서 여러 튜터님을 거치면서
점점 코드가 좋아지는 모습을 기록을 하려고 한다.
이번 키오스크 과제를 진행을 하면서 앞서 계산기 과제에서 받았던 지적 또한 신경을 많이 썼다.
- Depth 가 너무 깊다.
- 너무 많은 do - while 문, 조건문
그리고 변수, 클래스 명도 일관되게 작성을 하려고 노력을 하였다.
앞서 마지막 리펙토링을 진행을 할 때 생긴 문제점을 적어두었다.
[Java] 리펙토링 전 문제 점을 코드 리뷰 하면서 찾아보자.
개요코드를 나름 열심히 생각하고 만들면서 나중에 확정성하고, 코드를 보기 편하게 만들었다고 생각을 하였다하지만 코드를 들고 가서 많은 지적을 들었는데,오늘은 이 지적을 받아드리고 적
strnetwork.tistory.com
첫번째 리펙토링
1. Discount Enum
기존 코드
public enum Discount{
NATIONAL(0,"국가유공자", 10, (a) -> a * 0.9 ),
ARMY(1,"군인", 5, (a) -> a * 0.95 ),
STUDENT(2,"학생", 3, (a)-> a * 0.97),
DEFAULT(3, "일반", 0, a -> a);
private final int typeNum;
private final String typeStr;
private final int discountPercent;
private final Function<Double, Double> discount;
Discount(int typeNum, String typeStr, int discountPercent, Function<Double, Double> discount) {
this.typeNum = typeNum;
this.typeStr = typeStr;
this.discountPercent = discountPercent;
this.discount = discount;
}
/* 생략 */
public int getDiscountPercent() {
return this.discountPercent;
}
/* 생략 */
}
기본 코드는 Function 클래스를 사용하여 람다함수를 받아서 계산에 사용하도록 만들었다.
해당 코드는 유동적으로 변하는 계산식에 사용하면 좋다는 생각이 들었다.
값을 입력을 받으면 할인 값이 얼마 인지 계산만 해줄 수 있는 메소드만 있으면 된다는 생각이 들었다.
리펙토링 코드
public enum Discount {
NATIONAL(0, "국가유공자", 10),
ARMY(1, "군인", 5),
STUDENT(2, "학생", 3),
DEFAULT(3, "일반", 0);
private final int typeNum;
private final String typeStr;
private final int discountPercent;
Discount(int typeNum, String typeStr, int discountPercent) {
this.typeNum = typeNum;
this.typeStr = typeStr;
this.discountPercent = discountPercent;
}
/* 생략 */
public double calculate(double price) {
return price * (100 - this.discountPercent) / 100;
}
/* 생략 */
}
리펙토링을 하면서 Enum 값에는 람다 함수가 들어가지 않고 퍼센트만 넣어서
일관된 계산을 할 수 있는 메소드로 변경을 하였다.
2. InputUtil
기존 코드
public class inputUtil {
public static int inputNum(int first, int size) {
Scanner sc = new Scanner(System.in);
/* 로직 생략 */
}
}
기존 코드 같은 경우 클래스 이름이 inputUtil 로 클래스의 이름이 파스칼 케이스(Pascal Case)가 지켜지지 않았다.
그리고 inputNum 이 호출이 될 때 마다 신규 스캐너 객체가 생성이 되었다.
리펙토링 코드
public class InputUtil {
private final Scanner sc;
public InputUtil(Scanner sc) {
this.sc = sc;
}
public int inputNum(int first, int size) {
/* 로직 생략 */
}
}
리펙토링 이후 클래스에 생성자를 두고 의존성을 주입을 받을 수 있도록 리펙토링을 하였다.
튜터님께서 지적을 해주셔서 DI 에 대해서 찾아보고 공부 할 수 있었다.
3. MenuItem.class -> Menu.class
기존 코드
public class MenuItem {
/* 내부 생략 */
}
리펙토링 코드
public abstract class Menu {
/* 내부 생략 */
public abstract int getTypeNum();
public abstract String getTypeStr();
public abstract MenuType getType();
}
클래스 이름을 바꾸고 Menu 을 추상화를 진행을 하였다.
그리고 추상 클래스를 만드므로써 Menu에서도 자식클래스의 정보를 가져올 수 있게 되었다.
추상 클래스의 자세한 내용은 밑에 포스팅을 추가하였다.
[Java] 추상 클래스에 대해서.araboza
개요객체지향 프로그래밍에 중요한 개념 중 하나인 추상 클래스에 대해서 알아보고자 한다. 추상 클래스가 왜 사용이 되는지, 어떻게 사용해야 되는지에 대해서 알아보고, 예시까지 더해서 이
strnetwork.tistory.com
요구사항이 지켜지지 않았다는 것을 2번째 리펙토링 때 알게 되었다...
이후 내용은 밑에서 말하도록 하겠다.
문득 Menu 클래스을 MenuType Enum.class을 추가를 하고 MenuList을 같이 사용을 하면 되지 않나
생각을 하게 되었다.
그리하여 많은 전체적으로 바꾸게 되었다.
4. IMenu.class ( 삭제 )
public interface IMenu<T> {
/* 생략 */
}
5. Menu.class ( 삭제 )
public class Menu<T> implements IMenu<T>{
private List<String> category;
private List<T> list;
// 생성자
public Menu() {
this.category = new ArrayList<>();
this.list = new ArrayList<>();
}
/* 생략 */
}
리펙토링을 전부 끝낸 다음에 확인을 한 것이지만
가장 처음 Menu 클래스의 구조가 맞다는 생각이 들었다.
구조만 Map<String, List<T>> 형태로 만들어서 저장을 했다면 좋았을 것 같다는 생각이 든다.
5. MenuList
기존 코드
public class MenuList<T> implements IMenu<T> {
private List<T> menuItems;
public MenuList() {
this.menuItems = new ArrayList<>();
}
public void add(T item) {
this.menuItems.add(item);
}
/* 부분 생략 */
@Override
public void addItem(T item) {
int inputNum = END_NUMBER;
System.out.println("선택하신 메뉴 : " + item.toString());
System.out.println("위 메뉴를 장바구니에 추가하시겠습니까?");
System.out.println("1. 확인\t\t2. 취소");
inputNum = inputNum(1, 2);
if (inputNum == 0)
this.menuItems.add(item);
}
/* 부분 생략 */
@Override
public void clearItemList() {
this.menuItems.clear();
}
/* 부분 생략 */
}
기존 코드는 메뉴를 보여주는 부분과 장바구니 부분이 같이 공존을 하고 있는데
장바구니 클래스를 따로 추가를 하여 진행을 했었어야 했다.
리펙토링 코드
public class MenuList<T extends Menu> {
private final List<Menu> menuList;
public MenuList() {
this.menuList = new ArrayList<>();
}
/* 부분 생략 */
public List<T> getMenuList() {
return (List<T>) this.menuList;
}
public List<T> getMenuList(MenuType type) {
return (List<T>) menuList.stream()
.filter(item -> item.getType() == type)
.toList();
}
}
리펙토링 된 부분에서 List<Manu> 라고 한 부분에서 제네릭이 소용이 없어진 부분이 있다.
Menu 가 아니라 T를 붙이는게 맞다고 볼 수 있다.
이후 MenuType이 추가 되면서 메뉴 리스트를 가져올 때 타입으로 가져온다.
6. Burger, Drinks, Desserts 클래스
public class Burger extends Menu {
private final MenuType menuType;
public Burger(MenuType menuType, String itemName, double itemPrice, String[] itemDescribeList) {
super(itemName, itemPrice, itemDescribeList);
this.menuType = menuType;
}
@Override
public int getTypeNum() {
return menuType.getNumberType();
}
@Override
public String getTypeStr() {
return menuType.getStringType();
}
@Override
public MenuType getType() {
return menuType;
}
}
public class Burger extends Menu { /* 같은 내용 생략*/ }
public class Burger extends Menu { /* 같은 내용 생략*/ }
이제 상세 메뉴 클래스가 Menu 클래스를 상속받으므로 다양성이 생겼다.
7. MenuType
import java.util.Arrays;
import java.util.List;
public enum MenuType {
BURGER(0, "Burger"),
DRINKS(1, "Drinks"),
DESSERTS(2, "Desserts");
private final int numberType;
private final String stringType;
MenuType(int numberType, String stringType) {
this.numberType = numberType;
this.stringType = stringType;
}
public static MenuType getTypeNum(int num) {
return Arrays.stream(values())
.filter(item -> item.getNumberType() == num)
.findAny().orElse(null);
}
public static List<String> typeList() {
return Arrays.stream(values())
.map(MenuType::getStringType)
.toList();
}
public static int size() {
return MenuType.values().length;
}
public String getStringType() {
return stringType;
}
public int getNumberType() {
return numberType;
}
}
Enum 클래스를 사용하여 상위 메뉴( 가장 처음 목록 ) 을 한번에 추가를 하고 확인할 수 있도록 하였다.
그러고 MenuList 에 모든 Menu을 추가를 하여 확인을 할 수 있도록 하였다.
리펙토링 이후 튜터님께 피드백
[Java] 리펙토링 전 문제 점을 코드 리뷰 하면서 찾아보자.
개요코드를 나름 열심히 생각하고 만들면서 나중에 확정성하고, 코드를 보기 편하게 만들었다고 생각을 하였다하지만 코드를 들고 가서 많은 지적을 들었는데,오늘은 이 지적을 받아드리고 적
strnetwork.tistory.com
코드 리뷰를 전체적으로 진행을 하면서 고쳐야할 점과 구조상 이상한 점에 대해서 지적을 받았다.
그리고 팀원들과 코드 리뷰를 같이 진행을 해보면서 이후 2번째 리펙토링을 진행을 하였다.
두번째 리펙토링
1. Main.class
기존 코드
public class Main {
public static final int END_NUMBER = -1;
public static void main(String[] args) {
MenuList<Menu> menus = new MenuList<>();
menus.add(new Burger(MenuType.BURGER, "ShackBurger", 6.9, new String[]{"토마토", "양상추", "쉑소스가 토핑된 치즈버거"}));
/* 데이터 추가 생략 */
menus.add(new Desserts(MenuType.DESSERTS, "Coleslaw", 5.4, new String[]{"코울슬로"}));
Kiosk<Menu> kiosk = new Kiosk(menus);
kiosk.start();
}
}
원래 Main 에서 데이터를 추가를 하고 있었지만 팀원들과 같이 코드 리뷰를 하면서
다른 튜터님께서 데이터 입력하는 부분은 나누는 것이 좋다고 말씀하셨다고 나눈 것을 공유하게 되었다.
리펙토링 코드
public class Main {
public static void main(String[] args) {
// 데이터 가져오기
Menu<MenuItem> menus = MenuData.getMenus();
Kiosk<MenuItem> kiosk = new Kiosk(menus);
kiosk.start();
}
}
데이터를 입력하는 클래스를 따로 만들어서 추가를 하였다.
그리고 기존의 final int END_NUMBER을 연관이 있는 클래스 쪽으로 이동시켰다.
2. MenuData.class
public class MenuData {
public static Menu<MenuItem> getMenus() {
Menu<MenuItem> menus = new Menu<>();
menus.add(new Burger(1, "Burger", "ShackBurger", 6.9, new String[]{"토마토", "양상추", "쉑소스가 토핑된 치즈버거"}));
/* 데이터 입력 생략 */
menus.add(new Desserts(3, "Desserts", "Coleslaw", 5.4, new String[]{"코울슬로"}));
return menus;
}
}
3. Kiosk.class
기존 코드
public class Kiosk<T extends Menu> {
private final MenuList<T> menus;
private final MenuList<T> shop;
private final InputUtil iu;
private Discount discount;
// 기본 생성자
public Kiosk(MenuList<T> menus) {
this.menus = menus;
this.shop = new MenuList<>();
this.iu = new InputUtil(new Scanner(System.in));
}
/* 로직 생략 */
private void order(double payMoney) {
discount = inputPersonType();
String result = String.format("%.2f", discount.calculate(payMoney));
System.out.println("주문이 완료되었습니다. 금액은 W " + result + " 입니다.");
shop.clear();
}
private Discount inputPersonType() {
System.out.println("할인 정보를 입력해주세요.");
Discount.showType();
int inputNum = iu.inputNum(1, Discount.values().length);
return Discount.getTypeNum(inputNum);
}
}
문제가 있던 부분은
MenuList가 메뉴와 장바구니의 역활을 동시에 2개를 담당을 하고 있었다.
inputNum 메소드는 연관성이 높지만 InputUtil로 들어가있던 부분이 있었다.
계산을 진행할 때에 order 와 inputPersonType을 따로 나눠두어
얼마나 할인 받는지에 대한 내용을 따로 두었다.
리펙토링 코드
public class Kiosk<T extends MenuItem> {
private final int END = 0;
private final Scanner sc = new Scanner(System.in);
private final Menu<T> menus;
private final Cart<T> cart = new Cart<>();
private final int CATEGORY_SIZE;
// 기본 생성자
public Kiosk(Menu<T> menus) {
this.menus = menus;
this.CATEGORY_SIZE = menus.categorySize();
}
/* 로직 생략 */
private void order(double payMoney) {
// 할인 정보 입력 받기
System.out.println("할인 정보를 입력해주세요.");
Discount.showValues();
int inputNum = inputNum(1, Discount.values().length);
Discount discount = Discount.getNum(inputNum);
// 계산
String result = String.format("%.2f", discount.calculate(payMoney));
System.out.println("주문이 완료되었습니다. 금액은 W " + result + " 입니다.");
cart.clear();
}
private int inputNum(int size) {
return inputNum(0, size);
}
private int inputNum(int first, int last) {
int num = END;
while (true) {
try {
num = sc.nextInt();
if (first <= num && num <= last) break;
else throw new NumberFormatException("메뉴에 없습니다.");
} catch (NumberFormatException e) { // 넘치는 숫자 입력
System.out.println(e.getMessage());
} catch (InputMismatchException e) { // 문자 입력
System.out.println("잘못된 입력입니다.");
sc.next();
}
}
return num;
}
}
MenuList 을 원래대로인 Menu.Class로 바꾸고 원래 메뉴만 보여주는 기능만 수행해도록 바꾸었다.
Cart는 장바구니 역할을 할 수 있도록 메소드를 새롭게 만들어 주었다.
inputNum 같은 경우 2가지 경우의 수가 있었다.
- 0 ~ ( 입력 받은 수까지 )
- first 숫자 ~ last 숫자
각각의 경우의 수를 나누고 똑같이 돌아갈 수 있도록 만들었다.
그리고 계산과 얼마나 할인을 받는지 합쳐서 계산 로직을 하나로 만들었다.
입력과 출력만 Kiosk.class로 몰아두어 유저 UI 용으로만 사용할 수 있도록 진행을 하였다.
4. MenuList.class -> Menu.class
public class Menu<T extends MenuItem> {
private final List<T> menuList;
public Menu() {
this.menuList = new ArrayList<>();
}
/* 로직 생략 */
}
MenuList을 Menu로 이름을 바꾸면서 클래스가 좀더 명확한 역할을 가질 수 있도록 변경을 하였다.
5. Cart.class
public class Cart<T extends MenuItem> {
List<T> innerCart = new ArrayList<>();
public Cart() {
}
public void add(T item) {
this.innerCart.add(item);
}
public boolean isExist() {
return !this.innerCart.isEmpty();
}
public void clear() {
this.innerCart.clear();
}
public double getPayMoney() {
return innerCart.stream()
.mapToDouble(MenuItem::getItemPrice)
.sum();
}
public void showItems() {
this.innerCart.forEach(item -> {
System.out.println(item.toString());
});
}
}
앞선 Menu.class 와 같이 각 클래스의 이름을 어떤 역할을 하는지 명확하게 정하고
각 클래스 별로 명확한 기능을 추가를 하였다.
6. MenuItem.class 및 자식 클래스
public abstract class MenuItem {
/* 로직 생략 */
public abstract int getNum();
public abstract String getCategory();
/* 로직 생략 */
}
public class Burger extends MenuItem {
private final int num;
private final String category;
public Burger(int num, String category, String itemName, double itemPrice, String[] itemDescribeList) {
super(itemName, itemPrice, itemDescribeList);
this.num = num;
this.category = category;
}
@Override
public int getNum() {
return this.num;
}
@Override
public String getCategory() {
return this.category;
}
}
public class Drinks extends MenuItem {...}
public class Drinks extends MenuItem {...}
MenuItem 클래스에 추상 메소드를 만들어서 각각 자신의 카테고리와 카테고리넘버를 가져 올 수 있도록 변경을 하였다.
느낀점
과제를 진행을 하고 각 튜터님께 들고가면서 잘못된 것에 대해서 혼나고, 알려주시고, 배우고, 코드를 보면서
내가 발전을 하고 있다는 점이 보여서 감사하다.
아직 한지 얼마 안됬지만 벌써 몇 발자국 나아간 것이 이렇게 빨리 보이게 될지 몰랐다.
이번 프로젝트를 진행을 하면서
클래스, 변수, 객체, 메소드 등 이름에 대해서 많은 생각을 하게 되었다.
내가 생각보다 명확한 이름을 사용하지 않았구나.
만드는 방법만 알았을 뿐이라고 생각을 하게 되었다.
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java] 리펙토링 전 문제 점을 코드 리뷰 하면서 찾아보자. (1) | 2025.01.16 |
---|---|
[Java] 추상 클래스에 대해서.araboza (1) | 2025.01.15 |
[트러블 슈팅] 키오스크 과제 with Java (0) | 2025.01.13 |
[Java] 내일배움캠프 계산기 과제 분석을 해보자. (1) | 2025.01.10 |
[트러블 슈팅] CH 계산기 과제 with Java (0) | 2025.01.07 |