구현 과정에서 마주한 주요 트러블슈팅 포인트는 다음과 같습니다.
Java class
파일 생성버튼이 없어요!과제 중 인텔리제이에서 클래스 파일 생성 아이콘이 안 나오는 오류(?)가 발생했었습니다.
문제인 상황을 그대로 구글링해보니 제 상황과 비슷한 정리글이 있어 참고했습니다.
문제 원인으로는 src/
폴더가 아닌 out/
디렉토리에서 클래스 파일(.class)
를 생성하려고 해서 클래스 생성 아이콘이 보이지 않는 이슈가 있었습니다.
out/
폴더은 src/
폴더의 소스코드를 컴파일러를 통해 바이트코드(.class)로 변환된 파일을 저장합니다.
이 out/
폴더의 파일들은 보통 사람이 직접 수정하지 않고 IDE(통합 개발 환경)에서 자동으로 만들어서 처리됩니다.
그리고 이 바이트 코드들은 JVM이 실행할 수 있는 형태로 변환된 코드로 해당 코드를 JVM이 읽어 프로그램을 실행하게 됩니다.
즉, out/
폴더에서는 직접 Java 클래스를 만들 수 없습니다.
문제의 원인을 파악하고 src/
에서 New → Java Class를 통해 클래스 파일을 생성할 수 있었습니다.
다른 파일에서 공통적으로 사용할 ENUM 클래스 파일을 src/밑에 바로 밑에 만들었더니 접근이 안 돼서 당황했습니다.
생성한 ENUM파일을 가져와야 하는데 계속 에러가 나서 어려움이 있었습니다.
구글링을 통해 문제를 해결하는 과정에서 접근제어자
가 원인임을 알게됐습니다.
추가로 접근제어자를 수정하면서 공용파일을 생성해서 다른 파일들에서도
같은 import구문
으로 관리를 하면 좋겠다싶어서
공용으로 사용할 ENUM 클래스 파일을 src/common
같은 공통 패키지를 만들어서 사용할 파일에서 import를 통해 불러오는 거였습니다.
import common.KioskMenu;
import common.MenuItem;
공용 패키지와 접근제어자로 정리하니 깔끔하게 문제를 해결할 수 있었습니다.
lv3 과제 중 Kiosk클래스파일을 새로 생성하는 과정에서 입력을 처리하는 과정에서 문제가 생겼습니다. 입력을 처리하는 특정 메서드에서 입력 후 close()를 호출하여 문제가 발생했습니다.
BufferedReader를 사용하는 메서드에서 사용자 입력 후 스트림을 닫아 오류가 발생했습니다.
BufferedReader는 입력 스트림을 읽기 위해 사용되는데, 내부적으로 연결된 스트림에 의존합니다.
만약 BufferedReader나 그 밑의 스트림을 close()로 닫아 버린다면, 더이상 데이터를 읽을 수 없는 상태가 됩니다.
해결법으로는 프로그램 종료 시에만 close() 호출하는 것입니다.
BufferedReader가 더 이상 사용할 필요가 없을 때, 즉 프로그램이 종료될 때 닫는 게 안전합니다. 이렇게 하면 스트림이 불필요하게 열려 있는 시간을 줄이고, 자원 누수를 방지할 수 있습니다.
🚀 try (BufferedReader reader = ...) {}
구문을 사용하면 자동으로 닫힙니다.
예외 발생 시에도 안전하게 닫히게 됩니다.
try-with-resources
에서 자동으로 닫히는 건 try() 괄호 안에서 만든 객체뿐입니다. 예를 들어, BufferedReader를 괄호 안에서 선언하면 try 블록이 끝날 때 자동으로 close()가 호출돼서 편리합니다.
참고 자료:
👉 자바 bufferedreader & writer 사용법과 IOException
👉 왜 try-with-resources를 사용할까?
예외처리 중 비교문제
과제 중 너무 당연히 문자열 값의 비교를 ==
으로 처리해서 에러가 발생했습니다.
<public String isValidAnswer(String prompt) throws IOException{
while(true){
try {
String answer = inputString(prompt).toUpperCase();
if (answer.equals("Y") || answer.equals("N")) {
return answer;
}
System.out.println("입력이 올바르지 않습니다! 다시 입력해주세요!");
} catch (Exception e) {
System.out.println("입력이 올바르지 않습니다! 다시 입력해주세요!");
}
}
}
사용자의 입력의 유효한지 체크하는 메서드에서 한참동안 에러부분을 못 찾고 있다가 나중에서야 해당부분이 문제임을 인지했습니다.
==
사용 orequals()
사용String str1 = new String("Hello"); String str2 = new String("Hello"); // ❌ false - 서로 다른 객체, 서로 다른 참조값 System.out.println(str1 == str2); // ✅ true - 문자열 값이 동일 System.out.println(str1.equals(str2));
문자열 비교는 왜 equals()를 써야 하나?
문자열(String)을
==
로 비교하면 안 되는 이유는 문자열이 객체이기 때문입니다. Java에서 문자열은 String 클래스의 인스턴스이고,==
는 객체의 참조를 비교합니다. 반면 equals()는 문자열의 실제 내용(문자열 값)을 비교합니다.
위 코드의 isValidAnswer()
메서드를 보면
"Y"와 "N"은 문자열 리터럴입니다. Java에서는 메모리 효율을 위해
문자열 풀(String Pool)을 통해 리터럴을 저장하고 같은 값을 사용한 다면 같은 객체를 사용하는 식으로 관리합니다.
하지만 anwer는 사용자의입력을 통해 String객체로 변환되어 리터럴 객체와는 다른 주소값을 갖게 되어 ==
을 사용할 수 없습니다.
getResourceAsStream(resourcePath)
에서 리소스 파일( ex. 메뉴 파일들 햄버거, 드링크, 디저트 등)을 가져올 때 파일을 찾을 수 없는 문제가 발생했습니다.
InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("test.txt");
Main.class.getClassLoader()는 특정 클래스(Main)
의 클래스 로더를 통해 리소스 파일(test.txt)을 가져오는 방식입니다.
문제점은 inputStream이 null로 반환되면 해당 경로에서 파일을 찾을 수 없고, 추가로 사용하는 클래스가 Main
에서 다른 클래스로 바뀌면, 코드도 매번 수정해야 했습니다.
예를 들어, Kiosk.class.getClassLoader()
로 클래스가 바뀐다면 코드 또한 수정이 필요했습니다.
이 방식은 특정 클래스에 의존적이라 유연성이 떨어졌고, 유지보수가 불편했습니다.
해결 방법
특정 클래스의 클래스 로더를 고정적으로 사용하는 대신, 클래스 로더를 직접 활용하는 방식으로 수정했습니다.
아래는 개선된 코드입니다:
private InputStream getResourceAsStream(String resourcePath) {
return classLoader.getResourceAsStream(resourcePath);
}
특정 클래스의 클래스 로더를 사용하지 않고 클래스 로더를 직접 사용하여 코드 수정 없이 재사용 가능하고, 오류를 줄일 수 있었습니다.
import ...
public class Menu {
private MenuCategory category;
private List<MenuItem> menuItem;
public Menu(MenuCategory category, List<MenuItem> menuItem) {
this.category = category;
this.menuItem = menuItem;
}
public MenuCategory getCategory() {...}
public List<MenuItem> getMenuItem() {...}
}
Menu클래스를 통해 메뉴의 카테고리와 정보(이름, 가격,음식 정보)를 관리했습니다.
어떻게 하면 하나의 번호로 카테고리와 아이템을 동시에 찾을 수 있을까 고민을 했습니다.
장바구니 클래스에서
// idx, 장바구니 음식
private Map<Integer, MenuItem> basketMap;
// idx, 메뉴 카테고리
private Map<Integer, MenuCategory> categoryMap;
사용자로부터 받는 입력과 원하는 데이터를 하나씩 매핑해주는 Map을 사용하여 인덱스와 값들을 하나로 묶었습니다.
public Map<MenuCategory,MenuItem> findRemoveItem()throws IOException {
IOHandler ioHandler = new IOHandler();
Map<MenuCategory, MenuItem> removeItem = new HashMap<>();
try{
int removeIdx = ioHandler.inputInt("❌ 삭제할 메뉴 번호를 입력하세요 (0 입력 시 취소): ", basketMap.size())-1;
if (removeIdx == -1) {
System.out.println("🚫 메뉴 삭제를 취소하였습니다.");
return removeItem;
}
if (basketMap.containsKey(removeIdx)) {
MenuItem removeMenuItem = basketMap.get(removeIdx);
MenuCategory category = categoryMap.get(removeIdx);
// 삭제 메서드
removeItem.put(category, removeMenuItem);
return removeItem;
}
} catch (Exception e) {
System.out.println("오류가 발생했습니다!" + e.getMessage());
}
return removeItem;
}
// ✅ 장바구니에서 메뉴 제거
public void removeItem(Map<MenuCategory,MenuItem> removeItemMap) {
for (Map.Entry<MenuCategory, MenuItem> entry : removeItemMap.entrySet()) {
MenuCategory category = entry.getKey();
MenuItem item = entry.getValue();
if (menuBasket.get(category).remove(item)) {
System.out.println("✅ "+item.getName() + "를 장바구니에서 삭제합니다.");
totalSum -= item.getPrice();
} else {
System.out.println("⚠\uFE0F " + item.getName() + "는 장바구니에 없습니다");
}
}
}
반환된 Map을 순회하며 삭제하며 삭제할 값에 해당하는 정보를 삭제하는 방식으로 문제를 해결했습니다.
menuBasket.get(category)를 통해 MenuCategory의 List<>를 가져온 후 remove(item)을 통해 삭제를 시도합니다.
if-else문을 통해 삭제의 성공 여부를 확인할 수 있습니다.
아직 제네릭과 컬렉션에 익숙하지 않아 해결하는데 어려움이 많았습니다.
Map으로 데이터를 매핑하면 복잡한 정보를 단순하게 관리할 수 있었습니다.
Map.Entry 사용법 익히기: entrySet()으로 키-값 쌍 순회
List.remove()의 반환값(boolean) 활용
💭 참고자료
인터페이스 Map.Entry<K,V>: 오라클 공식 문서
Map.Entry 사용
[Java] ArrayList에서 특정 값 삭제하기