public static void main(String[] args)
new MenuItem("ShackBurger", 6.9, "토마토, 양상추, 쉑소스가 토핑된 치즈버거")
new MenuItem("ShackBurger", 6.9, "토마토, 양상추, 쉑소스가 토핑된 치즈버거")
토MenuItem
을 포함합니다.Scanner
활용법, 조건문, 반복문을 재확인하며 입력 데이터를 처리하는 방법 강화Scanner
를 사용하여 여러 햄버거 메뉴를 출력합니다.원래 여기까지가 기능 구현이었으나, 나는 이것보다 더 나아가서 잘 입력했는지에 대한 validation을 하고 싶었으며, 구매수량까지 물어보고, 총 금액까지 구하고 싶었다.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because "this.items" is null
at com.example.kiosk.Menu.outMenu(Menu.java:22)
at com.example.kiosk.Main.main(Main.java:13)
해당 에러가 발생햇다.
이 오류는 Menu 클래스의 items 리스트가 초기화되지 않았거나 null 상태에서 접근하려고 할 때 발생합니다.
Menu 클래스의 생성자에서 items 리스트가 올바르게 초기화되었는지 확인해야 합니다
메뉴가 존재하지 않습니다.
메뉴를 선택해주세요: 1
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because "this.items" is null
at com.example.kiosk.Menu.getMenuSize(Menu.java:39)
at com.example.kiosk.Main.main(Main.java:30)
수정해줬음에도 에러가 발생한다.
Menu클래스에서 기본 생성자 활용을 안해서 그런 것 같다.
public Menu() {
this.items = new ArrayList<>();
Menu();
}
이제 정상 작동하는 것을 확인할 수 있다.
그런데 너무 무겁지 않은가 ?
Main 클래스에 너무 많은 것을 담고 있다.
이것을 Kiosk 클래스에 분할하려고한다.
Main
package com.example.kiosk;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Menu menu = new Menu();
boolean running = true;
while (running) {
menu.outMenu();
System.out.print("메뉴를 선택해주세요: ");
int choice;
try {
choice = Integer.parseInt(br.readLine());
} catch (NumberFormatException e) {
System.out.println("잘못된 입력입니다. 숫자만 입력해주세요");
continue;
}
if (choice == 0) {
System.out.println("프로그램 종료합니다.");
running = false;
break;
}
if (choice < 1 || choice > menu.getMenuSize()) {
System.out.println("잘못된 입력입니다. 다시 시도하세요. \n");
continue;
}
MenuItem selectItem = menu.getMenuItem(choice - 1);
System.out.println("주문하신 메뉴: " + selectItem);
System.out.print("주문하신 메뉴가 맞을까요? (Y/N) 입력: ");
String filter = br.readLine().trim().toLowerCase();
if (filter.equals("n")) {
System.out.println("다시 주문해주세요.");
continue;
} else if (filter.equals("y")) {
System.out.print("몇 개를 주문하시겠습니까? :");
int quantity;
try {
quantity = Integer.parseInt(br.readLine());
} catch (NumberFormatException e) {
System.out.println("올바른 수량을 입력해주세요");
continue;
}
if (quantity <= 0) {
System.out.println("올바른 수량을 입력하세요");
continue;
}
double totalPrice = selectItem.getPrice() * quantity;
System.out.println("총 주문 금액은: W "+totalPrice+" 입니다." );
}else System.out.println("잘못된 입력입니다. 다시 시도하세요.");
}
}
}
Menu
package com.example.kiosk;
import java.util.ArrayList;
import java.util.List;
/**
* MenuItem 클래스를 관리하는 클래스
*/
public class Menu {
private List<MenuItem> items;
public Menu() {
this.items = new ArrayList<>();
Menu();
}
public void Menu() {
items = new ArrayList<>();
items.add(new MenuItem("ShackBurger", 6.9, "토마토, 양상추, 쉑소스가 토핑된 치즈버거"));
items.add(new MenuItem("SmokeShack", 8.9, "베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"));
items.add(new MenuItem("CheeseBurger", 6.9, "포테이토 번과 비프패티, 치즈가 토핑된 치즈버거"));
items.add(new MenuItem("HamBurger", 5.4, "비프 패티를 기반으로 야채가 들어간 기본 버거"));
}
public void outMenu() {
if (items == null || items.isEmpty()) {
System.out.println("메뉴가 존재하지 않습니다.");
return;
}
System.out.println("[ SHAKESHACK MENU ]\n");
for (int i = 0; i < items.size(); i++) {
System.out.println(i + 1 + ". " + items.get(i));
}
System.out.println("0. 종료 | 종료");
}
public MenuItem getMenuItem(int index) {
return items.get(index);
}
public int getMenuSize() {
return items.size();
}
}
MenuItem
package com.example.kiosk;
/**
* 세부 메뉴 속성
*/
public class MenuItem {
private String burger;
private double price;
private String intro;
public MenuItem(String shackBurger, double price, String intro) {
this.burger = shackBurger;
this.price = price;
this.intro = intro;
}
public String getBurger() {
return burger;
}
public double getPrice() {
return price;
}
@Override
public String toString() {
return burger + "\t| W " + price + " |" + intro;
}
}
이제 무거웠던 클래스를 내 나름대로 좀 분할해봤다.
Kiosk()클래스에 구현하면서, 나는 메뉴를 보여주는 것을 해야해서, Menu 클래스에 있는 outMenu메서드를 불러와야하는데 initializeMenu()를 불러와서 메뉴판 없이 바로 메뉴를 선택하는 정말 이상한 프로그램이 개발되었던 것이다.
이제 수정된 코드를 공유하겠습니다.
package com.example.kiosk;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
Kiosk kiosk = new Kiosk();
kiosk.run();
}
}
package com.example.kiosk;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 프로그램 순서 및 흐름 제어
*/
public class Kiosk {
private Menu menu;
private BufferedReader br;
public Kiosk() {
this.menu = new Menu();
this.br = new BufferedReader(new InputStreamReader(System.in));
}
public void run() throws IOException {
boolean running = true;
while (running) {
// menu.initializeMenu();
menu.outMenu();
System.out.print("메뉴를 선택하세요: ");
int choice;
try {
choice = Integer.parseInt(br.readLine());
} catch (NumberFormatException e) {
System.out.println("잘못된 입력입니다. 숫자를 입력해주세요.\n");
continue;
}
if (choice == 0) {
System.out.println("프로그램을 종료합니다.");
running = false;
break;
}
if (choice < 1 || choice > menu.getMenuSize()) {
System.out.println("잘못된 입력입니다. 다시 시도하세요.\n");
continue;
}
handleOrder(choice);
}
}
// private void initializeMenu() {
// menu.outMenu();
// }
private void handleOrder(int choice) throws IOException {
MenuItem selectedItem = menu.getMenuItem(choice - 1);
System.out.println("주문하신 메뉴: " + selectedItem);
System.out.print("주문하신게 맞을까요? (Y/N): ");
String confirm = br.readLine().trim().toUpperCase();
if (confirm.equals("N")) {
System.out.println("다시 주문해주세요.\n");
return;
} else if (confirm.equals("Y")) {
System.out.print("몇 개를 주문하시겠습니까? : ");
int quantity;
try {
quantity = Integer.parseInt(br.readLine());
} catch (NumberFormatException e) {
System.out.println("올바른 수량을 입력해주세요.\n");
return;
}
if (quantity <= 0) {
System.out.println("올바른 수량을 입력해주세요.\n");
return;
}
double totalPrice = selectedItem.getPrice() * quantity;
System.out.println("총 주문 금액은: W " + totalPrice + "입니다.\n");
} else {
System.out.println("잘못된 입력입니다. 다시 시도하세요.\n");
}
}
}
package com.example.kiosk;
import java.util.ArrayList;
import java.util.List;
/**
* MenuItem 클래스를 관리하는 클래스
*/
public class Menu {
private List<MenuItem> items;
public Menu() {
this.items = new ArrayList<>();
this.initializeMenu();
}
public void initializeMenu() {
items = new ArrayList<>();
items.add(new MenuItem("ShackBurger", 6.9, "토마토, 양상추, 쉑소스가 토핑된 치즈버거"));
items.add(new MenuItem("SmokeShack", 8.9, "베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거"));
items.add(new MenuItem("CheeseBurger", 6.9, "포테이토 번과 비프패티, 치즈가 토핑된 치즈버거"));
items.add(new MenuItem("HamBurger", 5.4, "비프 패티를 기반으로 야채가 들어간 기본 버거"));
}
public void outMenu() {
if (items == null || items.isEmpty()) {
System.out.println("메뉴가 존재하지 않습니다.");
return;
}
System.out.println("[ SHAKESHACK MENU ]");
for (int i = 0; i < items.size(); i++) {
System.out.println(i + 1 + ". " + items.get(i));
}
System.out.println("0. 종료 | 종료");
}
public MenuItem getMenuItem(int index) {
return items.get(index);
}
public int getMenuSize() {
return items.size();
}
}
가독성을 위해 Menu -> initializeMenu로 수정했습니다.
MenuItem
클래스와 List
를 통해 관리합니다.MenuItem
클래스 생성하기이름
, 가격
, 설명
필드를 갖습니다.main
함수에서 MenuItem
클래스를 활용하여 햄버거 메뉴를 출력합니다.MenuItem
객체 생성을 통해 이름
, 가격
, 설명
을 세팅합니다.new
List
를 선언하여 여러 MenuItem
을 추가합니다.List<MenuItem> menuItems = new ArrayList<>();
menuItems
를 탐색하면서 하나씩 접근합니다.public static void main(String[] args) {
// List 선언 및 초기화
// add 함수를 통해 new MenuItem(이름, 가격, 설명) List에 삽입
// (add 보다 더 좋은 방법이 있다면 그렇게 해도 됩니다!)
// Scanner 선언
// 반복문을 활용해 List 안에 있는 MenuItem을 하나씩 출력
// 숫자를 입력 받기
// 입력된 숫자에 따른 처리
// 프로그램을 종료
// 선택한 메뉴 : 이름, 가격, 설명
}
이제 level 2를 구현하면 된다.
멘토님이 레벨 1부터 차근히 순서대로 구현하시면 된다는 말씀에 level2는 읽어보지도 않고 1부터 진행했다. 그런데 왠걸?!
이미 레벨 1에구현이 끝났다.ㅎㅎㅎㅎ
main
함수에서 관리하던 전체 순서 제어를 Kiosk
클래스를 통해 관리합니다.Kiosk
클래스 생성하기MenuItem
을 관리하는 리스트가 필드로 존재합니다.main
함수에서 관리하던 입력과 반복문 로직은 이제 start
함수를 만들어 관리합니다.List<MenuItem> menuItems
는 Kiosk
클래스 생성자를 통해 값을 할당합니다.Kiosk
객체를 생성하고 사용하는 main
함수에서 객체를 생성할 때 값을 넘겨줍니다.0
을 입력하면 프로그램이 ‘뒤로가기’되거나 ‘종료’됩니다.Menu
클래스 생성하기MenuItem
을 포함합니다.List<MenuItem>
은 Kiosk
클래스가 관리하기에 적절하지 않으므로 Menu 클래스가 관리하도록 변경합니다.카테고리 이름
필드를 갖습니다.public static void main(String[] args) {
// Menu 객체 생성하면서 카테고리 이름 설정
// Menu 클래스 내 있는 List<MenuItem> 에 MenuItem 객체 생성하면서 삽입
// Kiosk 객체 생성
// Kiosk 내 시작하는 함수 호출
}
public class Kiosk {
start() {
// 스캐너 선언
// 반복문 시작
// List와 Menu 클래스 활용하여 상위 카테고리 메뉴 출력
// 숫자 입력 받기
// 입력 받은 숫자가 올바르다면 인덱스로 활용하여 List에 접근하기
// List<Menu>에 인덱스로 접근하면 Menu만 추출할 수 있겠죠?
// Menu가 가진 List<MenuItem>을 반복문을 활용하여 햄버거 메뉴 출력
// 숫자 입력 받기
// 입력 받은 숫자가 올바르다면 인덱스로 활용해서 Menu가 가지고 있는 List<MenuItem>에 접근하기
// menu.getMenuItems().get(i); 같은 형식으로 하나씩 들어가서 얻어와야 합니다.
}
}
public class Menu {
// MenuItem 클래스를 List로 관리
// List에 들어있는 MenuItem을 순차적으로 보여주는 함수
// List를 리턴하는 함수
// 구조에 맞게 함수를 선언해놓고 가져다 사용하세요.
}
public class MenuItem {
// 이름, 가격, 설명 필드 선언하여 관리
// 구조에 맞게 함수를 선언해놓고 가져다 사용하세요.
}
[ MAIN MENU ]
1. Burgers
2. Drinks
3. Desserts
0. 종료 | 종료
1 <- // 1을 입력
[ BURGERS MENU ]
1. ShackBurger | W 6.9 | 토마토, 양상추, 쉑소스가 토핑된 치즈버거
2. SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
3. Cheeseburger | W 6.9 | 포테이토 번과 비프패티, 치즈가 토핑된 치즈버거
4. Hamburger | W 5.4 | 비프패티를 기반으로 야채가 들어간 기본버거
0. 뒤로가기
2 <- // 2를 입력
선택한 메뉴: SmokeShack | W 8.9 | 베이컨, 체리 페퍼에 쉑소스가 토핑된 치즈버거
[ MAIN MENU ]
1. Burgers
2. Drinks
3. Desserts
0. 종료 | 종료
0 <- // 0을 입력
프로그램을 종료합니다.
어찌하다보니 레벨4까지 구현을 하게 된 것 같다.
level 5까지 구현하고,도전 기능 가이드를 하려고 한다.
인텔리제이에서 깃헙을 연동을 해서 추가와 커밋, 브랜치 생성까지 해서 작업했다.
원래 나는 터미널에서 작업을 했었는데, 그냥 갑자기 인텔리제이랑 깃헙 연동해서 있는 VCS를 쓰고 싶었다.
그래서 연결하고 깃 올릴 주소랑 연결해서 feature 브랜치를 파고 개발하고 푸쉬했는데, main에 merge가 안되는 상황이 발생한 것이다.
분명 pr은 뜨고, pr누르면 창으로 이동해야하는데, 코드 변경 내역은 확인되는데, pr 및 머지 하는 창이 안뜨고 그냥 비교할 내용이 없다고만 뜨는 상황이 발생한 것이었다. 이런 상황은 나도 처음봤다. 결국 난 다시 터미널에서 해결해보고자 했다.
rebase도 해보고 이것 저것 많이 해봤는데 추가하고 커밋까지 했는데, history는 없고 이게 뭔가..?
리모트 브랜치(origin/main)와 로컬 브랜치(main) 간에 충돌이 발생한 상황으로 파악하고,
리모트 브랜치와 로컬 브랜치를 병합으로 해결하려 했는데,리모트 브랜치에 로컬 브랜치에 없는 변경 사항이 있기 때문인 줄 알고 git pull origin main --allow-unrelated-histories
로 리모트 브랜치에서 변경 사항을 가져오고 병합하려고 했는데,
➜ KioskProject git:(main) git pull origin main --allow-unrelated-histories
https://github.com/sjMun09/Kiosk URL에서
* branch main -> FETCH_HEAD
힌트: You have divergent branches and nee]\d to specify how to reconcile them.
힌트: You can do so by running one of the following commands sometime before
힌트: your next pull:
힌트:
힌트: git config pull.rebase false # merge
힌트: git config pull.rebase true # rebase
힌트: git config pull.ff only # fast-forward only
힌트:
힌트: You can replace "git config" with "git config --global" to set a default
힌트: preference for all repositories. You can also pass --rebase, --no-rebase,
힌트: or --ff-only on the command line to override the configured default per
힌트: invocation.
fatal: Need to specify how to reconcile divergent branches.
깃 헙, 너란녀석 간만에 말썽이구나,,
현재 로컬 브랜치와 리모트 브랜치 간에 히스토리가 달라 병합 방법을 명확히 지정하지 못해서 발생한 문제였다.
이를 해결 하기 위해 git pull 명령어에서 병합 방식 옵션을 명시적으로 지정하거나 기본 설정을 설정을 진행해줬다.
해결 방법으로는 알아보니 총 3가지 방법이 있었다.
git pull origin main --allow-unrelated-histories --no-rebase
git pull origin main --allow-unrelated-histories --rebase
git pull origin main --ff-only
병합 방식을 항상 고정하고 싶다면 git config를 사용해 글로벌 설정을 변경할 수 있습니다.
git config --global pull.rebase false
git config --global pull.rebase true
git config --global pull.ff only
위 명령어를 실행하면 병합이나 리베이스 중 충돌이 발생할 수 있습니다. 충돌이 생기면 Git이 충돌 난 파일을 알려줍니다. 해당 파일을 열어 충돌을 해결하고 저장한 뒤 add, commit, push까지 순차적으로 해주면 된다.
MenuItem
, Menu
그리고 Kiosk
클래스의 필드에 직접 접근하지 못하도록 설정합니다.lv5를 구현하기에 앞서, 레벨 5를 적용하는 이유를 먼저 알고 저의 코드를 확인해야된다는 생각하여 저의 코드를 분석해봤습니다. 충분히 캡슐화했다고 생각하고, 유지보수 및 확장성도 어느정도 구현되었다고 생각했었습니다.
1 - 데이터 보호
클래스의 필드를 private로 설정함으로써 외부에서 직접 접근을 차단하고, 데이터 무결성을 보장합니다. 외부 클래스가 필드에 잘못된 값을 설정하거나 무분별하게 수정하는 것을 방지할 수 있습니다.
2 - 변경에 유연
필드를 Getter와 Setter로 관리하면 내부 구현이 변경되더라도 외부 코드에 영향을 주지 않고 수정할 수 있습니다. 예를 들어, 가격을 계산하는 로직을 getPrice() 메서드에 추가해도 호출하는 코드에는 영향을 주지 않습니다.
3 - 유지보수성 증가
캡슐화를 통해 코드의 변경 범위를 줄이고, 특정 클래스의 내부 구현을 외부로부터 숨김으로써 코드의 안정성을 높입니다.
4 - 확장성 증가
Getter와 Setter를 통해 필드에 접근하면, 향후에 데이터 검증 로직, 로깅, 혹은 값 변환 등의 추가 작업을 쉽게 구현할 수 있습니다.
라는 이유 때문에, MenuItem에 게더세터를 생성했었는데, 이것 만으로는 부족한가 ? 라는 생각이 문득 들었다. 오히려 사용하지 않는 게더세터를 생성해주면서 코드가 길어지고 불편해진 느낌이 있었기 때문이다.
그렇다면 이전에 내가 구현한 코드는 캡슐화가 적용 안된건가?
-> 캡슐화가 부분적으로 적용된 코드입니다. 다음과 같은 이유에서 "캡슐화가 완벽히 적용되지 않았다"고 말 할 수 있습니다.
모든 클래스(MenuItem, Menu, Kiosk)의 필드가 private로 설정되어 있습니다. 외부에서 필드에 직접 접근하지 못하도록 제한한 것은 캡슐화의 기본 원칙 중 하나입니다.
데이터를 읽거나 수정할 수 있도록 Getter와 Setter를 통해 데이터를 관리하고 있습니다.
정리
이전 코드도 부분적인 캡슐화는 적용되어 있었지만, 내부 데이터 구조를 외부에 노출하는 방식은 캡슐화의 원칙에 위배됩니다.
단순히 돌아가는 코드가 아닌, 확장성과 유지보수성이 높은 코드를 작성하기 위해 캡슐화를 강화하는 것이 필요합니다.