이지은님의 블로그
250114 - Java 키오스크 구현과 트러블슈팅: 카테고리화, 문자열 출력, 제네릭 메서드, indexOf()의 한계 본문
250114 - Java 키오스크 구현과 트러블슈팅: 카테고리화, 문자열 출력, 제네릭 메서드, indexOf()의 한계
queenriwon3 2025. 1. 14. 21:05▷ 코드 문제풀이
[JAVA] 코드카타 - (36)~(40)
문제 (36) : 문자열 다루기 기본문자열 s의 길이가 4 혹은 6이고, 숫자로만 구성돼있는지 확인해주는 함수, solution을 완성하세요. 예를 들어 s가 "a234"이면 False를 리턴하고 "1234"라면 True를 리턴하면
queenriwon3.tistory.com
▷ 오늘 배운 것
키오스크 과제를 level5까지 작성하면서 생긴 문제나 해결 과정에 대해 작성해보도록 하겠다.
<< 목차 >>
1. 초기 키오스크(ver.1)
2. 메인 창에서 예외처리와 메서드화
3. 상위 카테고리메뉴 구현하기
4. toString()으로 구매한 버거 출력하기
5. 제네릭을 이용하여 중복된 메서드 하나로 합치기 (트러블 슈팅)
6. 효율적인 코드 작성하기 (트러블 슈팅)
1. 초기 키오스크(ver.1)
우선 메뉴 이름과 가격, 설명을 저장할 수 있는 MenuItem 클래스를 작성하고, 생성자, getter, setter를 생성했다.
package com.example.kiosk;
public class MenuItem {
private String menuName;
private double menuPrice;
private String menuDescription;
public MenuItem(String menuName, double menuPrice, String menuDescription) {
this.menuName = menuName;
this.menuPrice = menuPrice;
this.menuDescription = menuDescription;
}
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public double getMenuPrice() {
return menuPrice;
}
public void setMenuPrice(double menuPrice) {
this.menuPrice = menuPrice;
}
public String getMenuDescription() {
return menuDescription;
}
public void setMenuDescription(String menuDescription) {
this.menuDescription = menuDescription;
}
}
이 kiost.java는 start()메서드를 통해 kiosk가 실행되도록 동작한다. 이때 생성자를 이용해서 ArrayList에 MenuItem을 담는다.
package com.example.kiosk;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Kiosk {
private List<MenuItem> menuItems = new ArrayList<>();
public Kiosk() {
menuItems.add(new MenuItem("ShackBurger", 6.9, "토마토, 양상추, 쉑소스가 토핑된 치즈버거"));
menuItems.add(new MenuItem("SmokeShack", 8.9, "베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"));
menuItems.add(new MenuItem("Cheeseburger", 6.9, "포테이토 번과 비프패티, 치즈가 토핑된 치즈버거"));
menuItems.add(new MenuItem("Hamburger ", 5.4, "비프패티를 기반으로 야채가 들어간 기본버거"));
}
public void start() {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("[ SHAKESHACK MENU ]");
for (MenuItem item : menuItems) {
System.out.println(menuItems.indexOf(item) + ". " + item.getMenuName() + " | W " + item.getMenuPrice() + " | " + item.getMenuDescription());
}
System.out.println("0. 뒤로가기");
String ans = scanner.nextLine();
if ("0".equals(ans)) break;
}
System.out.println("프로그램을 종료합니다.");
}
}
레벨 1을 시도하려다가 이후 코딩 방향성을 생각하면 바로 메뉴 클래스를 만들고 그 클래스를 ArrayList에 담아야 겠다고 생각했다.(lv.2)
그래서 메서드에 .add를 하는 것 보다는 생성자에 리스트를 추가하여 앞으로 코드를 수정할때 빠르게 수정될 수 있도록 구현해보았고, 더욱이 꺼내쓸때마다 인덱스도 함께 출력이 되도록 .indexOf()도 사용해보았다.
수정할 점은 출력 형식이 깔끔하지 않다는 점과 인덱스가 0까지 출력되므로 이점 주의해서 출력해야 겠다고 생각하여 바로 출력하는 코드를 수정해보았다.
System.out.println((menuItems.indexOf(item)+1) + ". " + item.getMenuName() + "\t | W " + item.getMenuPrice() + " | " + item.getMenuDescription());
문제를 인지하고 수정한 부분이다. (Lv.1 + Lv.2)
2. 메인 창에서 예외처리와 메서드화(lv.3)
예외처리를 어떻게 할 것인가…
특히 과제에서 주어진 과제가 숫자를 입력하면서 진행하는 콘솔이기 때문에 int값으로 받아들이는게 좋을 것 같다고 생각했다. 이때 생각할 수 밖에 없는 사항이 있다. int값 외의 입력을 받아 들이면 어떻게 해야하나? 라는 물음으로 이어진다.
이전 계산기 문제를 풀때는 예외처리에 관해 응용이 다소 부족했다는 판단과 함께 다음 과제는 예외처리에 집중을 해보겠다고 생각했다. 그래서 내가 떠올릴 수 있는 예외에 관해 잘 각각 잘 처리할 수 있도록 예외를 분리해보고자 했다.
처음은 이 프로그램에서 예상 외의 답을 했을때의 예외에 대해 생각해보자.
Int형을 받아들이지 못했을 때에 대한 예외는 InputMismatchException로 처리할 수 있다.
그래서 try-catch문을 사용하여 입력값 미스가 난 것을 한번에 처리해줄 것이다. 이부분을 공부하면서 여러 예외상황에 대해 어떤 catch상황을 제공할지 공부해야겠다는 생각을 했다.
3. 상위 카테고리메뉴 구현하기
과제에서 원하는 것은
Menu(상위카테고리이름, MenuItem(상품이름, 가격, 설명)) 라는 형태로 ArrayList를 다루라는 뜻 같다.
이렇게 하려면 Kiost클래스에서 직접 MenuItem을 다루지 않는다는 뜻이다.
그럼 상위카테고리와 MenuItem을 다루는 클래스를 작성해야한다.
package com.example.kiosk;
import java.util.ArrayList;
import java.util.List;
public class Menu {
private String menuCategory;
private List<MenuItem> menuItems = new ArrayList<>();
public Menu(String menuCategory) {
this.menuCategory = menuCategory;
}
public String getMenuCategory() {
return menuCategory;
}
public void setMenuCategory(String menuCategory) {
this.menuCategory = menuCategory;
}
public List<MenuItem> getMenuItems() {
return menuItems;
}
public void setMenuItems(List<MenuItem> menuItems) {
this.menuItems = menuItems;
}
}
일단 과제 예시 상황을 따라 Main클래스에 메뉴들을 넣어보도록하겠다.
이렇게 만들어진 Menu ArrayList는 Kiosk 안 start의 매개변수로 입력이된다.
// Main.java
Kiosk kiosk = new Kiosk(menuList);
kiosk.start();
// Kiosk.java
public void start() {
while (true) {
try {
int selectCategoryAns = selectCategory();
if (selectCategoryAns == 0) break;
Menu menu = menuList.get(selectCategoryAns - 1);
int selectMenuItemAns = selectMenuItem(menu);
if (selectMenuItemAns == 0) continue;
System.out.println("구매창");
} catch (InputMismatchException e){
System.out.println("[오류] 숫자값을 입력해주세요.");
scanner.nextLine();
} catch (Exception e){
System.out.println(e.getMessage());
}
}
System.out.println("프로그램을 종료합니다.");
}
public int selectCategory() {
System.out.println("[ MAIN MENU ]");
for (Menu menu : menuList) {
System.out.println((menuList.indexOf(menu)+1) + ". " + menu.getMenuCategory());
}
System.out.println("0. 종료");
int selectCategoryAns = scanner.nextInt();
if (selectCategoryAns > menuList.size())
throw new IndexOutOfBoundsException("[오류] 해당되는 번호를 입력해주세요.");
return selectCategoryAns;
}
public int selectMenuItem(Menu menu) {
System.out.println("[ "+ menu.getMenuCategory().toUpperCase() +" MENU ]");
List<MenuItem> menuItems = menu.getMenuItems();
for (MenuItem item : menuItems) {
System.out.println((menuItems.indexOf(item)+1) + ". " + item.getMenuName() + "\t | W " + item.getMenuPrice() + " | " + item.getMenuDescription());
}
System.out.println("0. 뒤로가기");
int selectMenuItemAns = scanner.nextInt();
if (selectMenuItemAns > menuItems.size())
throw new IndexOutOfBoundsException("[오류] 해당되는 번호를 입력해주세요.");
return selectMenuItemAns;
}
이때 메뉴 카테고리를 출력 및 입력하는 기능과 메뉴 아이템을 출력 및 입력하는 기능을 각 메서드로 나누어 구현을 해보았는데, 형태가 다소 비슷하다는 것을 알 수 있다. 일단 구현은 이렇게 해보았지만 추후 하나의 메서드로 사용할 수 있는 방법을 알아보는 것도 좋을 것 같다.
아래는 burgers 카테고리 안의 burgers menu가 실행되는 내용, 예외처리가 구현된 결과이다.
4. toString()으로 구매한 버거 출력하기
선택한 메뉴를 출력할 때, 다음과 같이 출력하게 되는데, 둘다 길이가 너무 길어서 각 클래스의 toString메서드로 따로 만들어 주는 것이 재사용성, 코드 중복을 막는데 도움이 될 것이라고 판단했다. 그래서 Menu와 MenuItem에 toString메서드를 오버라이딩 해서 출력할 수 있도록 수정해보았다. 또한 toString()으로 구매한 메뉴까지 출력 할 수 있다.
System.out.println((menuList.indexOf(menu)+1) + ". " + menu.getMenuCategory());
System.out.println((menuItems.indexOf(item)+1) + ". " + item.getMenuName() + "\t | W " + item.getMenuPrice() + " | " + item.getMenuDescription());
@Override
public String toString() {
return menuName + "\t | W " + menuPrice + " | " + menuDescription;
}
5. 제네릭을 이용하여 중복된 메서드 하나로 합치기 (트러블 슈팅)
코드 수정 전
하나는 상위메뉴 출력, 하는 하위메뉴 출력에 사용하는 메서드인데 출력하고 입력하는 과정이 매우 유사하다.
코드 중복을 줄이고 사용성을 늘리기 위해 하나의 메서드로 만들어보고자 한다.
Object 클래스를 이용해 코드 수정
그래서 매개변수를 메뉴 제목이 될 문자열(String title)과, 가져올 ArrayList(List<Object> list)로 정하고 코드를 수정했다. 이때 List 형식은 제일 최상위 클래스인 Object를 선택하는게 좋을 것 같다고 마냥 생각했다.
이떄 문제는 메서드를 호출하면서 발생한다.
Object를 입력해야한다고 매개변수를 유연하게 입력받을 수 없다고 한다.
이때 생각할 수 있는 방법은 바로 제네릭이다.
제네릭의 특징은
1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
인데, 특히 3번을 주목해보자.
지금은 코드의 재사용성을 늘리기 위해 코드의 중복을 줄이고 있는 과정이다. 그러므로 매개변수에 제네릭을 사용함으로서 타입을 메서드 외부에서 지정해주는 방법이 좋을 것 같다고 판단했다.
그러므로 Object로 지정했던 컬렉션 타입을 전부 제네릭 T로 변경해주기로 했다. 이 <T>는 <Menu>도 될 수 있고 <MenuItem>도 될 수 있도록 코드 확장성을 늘려줄 수 있다.
그러나 Object를 단순히 T를 바꾸는 것만으로 바로 문제를 해결할 수는 없었다.
결국 오류 코드를 복사하여 검색하는 방법으로 아래 문서를 발견했다. 잘은 모르겠지만 제네릭 메소드를 이용하면 해결될거라고 하는데, 제네릭 메서드에 대해서 알아보는게 좋을 것 같다고 생각했다.
https://stackoverflow.com/questions/35622101/t-cannot-be-resolved-to-a-type
<T> cannot be resolved to a type?
I want to convert a json string to a List<Someclass> using jackson json library. public static List<T> toList(String json, Class<T> type, ObjectMapperProperties objectMapperPrope...
stackoverflow.com
제네릭 메소드는 메소드의 선언 부에 적은 제네릭으로 리턴 타입, 파라미터의 타입이 정해지는 메소드이다.
보통은 제네릭과 static을 함께 사용할 수 없는데(인스턴스가 되기 전에 static은 메모리에 올라가는데 이 때 타입 T가 결정되지 않기 때문에), 제네릭 메소드는 static을 할 수 있는 것도 큰 특징이다. 제너릭 메소드는 호출 시에 매게 타입을 지정하기 때문에 static이 가능하다.
주의해야 할 점은 클래스의 제너릭 타입 <T>와 제너릭 메소드에 붙은 <T>는 같은 T를 사용하더라도 전혀 별개(지역변수같은 느낌)
그리고 제네릭 메서드를 사용하려면 메서드 선언부에도 <T>를 붙여서 이 메서드가 제네릭 메서드 임을 알려야한다고 한다.
이를 참고하여 다음과 같이 수정해보자.
제네릭 메서드를 사용하여 코드 재활용성을 높인 코드
public <T> int selectMenu(String title, List<T> list) {
System.out.println("[ "+ title.toUpperCase() +" MENU ]");
for (T item : list) {
System.out.println((list.indexOf(item)+1) + ". " + item.toString());
}
if ("Main".equals(title)) System.out.println("0. 종료\t\t\t | 종료");
else System.out.println("0. 뒤로가기\t\t\t | 뒤로가기");
int selectMenuAns = scanner.nextInt();
if (selectMenuAns > list.size() || selectMenuAns < 0)
throw new IndexOutOfBoundsException("[오류] 원하는 메뉴를 선택해주세요");
return selectMenuAns;
}
6. 효율적인 코드 작성하기 (트러블 슈팅)
팀원들과 스크럼을 하는 도중 ArrayList.indexOf()가 선형 탐색을 반복 수행한다는 문제가 있다는 것을 알게되었다. 시간 복잡도도 신경써서 효율적인 코드를 작성해야 된다는 것을 느꼈으며 수정하여 for-each반복문 보다는 for반복문을 사용하여 인덱스를 출력하는게 좋을 것 같다고 생각했다.
다음과 같이 코드를 수정했으며 이렇게 level4(level5) 작성은 마칠 수 있었다.
public <T> int selectMenu(String title, List<T> list) {
System.out.println("[ "+ title.toUpperCase() +" MENU ]");
for (int i = 0; i < list.size(); i++){
System.out.println((i+1) + ". " + list.get(i).toString());
}
if ("Main".equals(title)) System.out.println("0. 종료\t\t\t | 종료");
else System.out.println("0. 뒤로가기\t\t | 뒤로가기");
int selectMenuAns = scanner.nextInt();
if (selectMenuAns > list.size() || selectMenuAns < 0)
throw new IndexOutOfBoundsException("[오류] 원하는 메뉴를 선택해주세요");
return selectMenuAns;
}
▷ 참고 블로그
https://devlog-wjdrbs96.tistory.com/201
[Java] 제네릭 메소드(Generic Method)란?
제너릭 메소드 제네릭 메소드는 메소드의 선언 부에 적은 제네릭으로 리턴 타입, 파라미터의 타입이 정해지는 메소드이다. 제너릭에 대한 예시를 보면서 이해해보자. public class Student { static T nam
devlog-wjdrbs96.tistory.com