비즈니스 요구사항 1. 숫자를 입력 받고, 2를 곱한 수를 출력한다. 2. 숫자가 아닌 문자를 입력 받으면 [ERROR]로 시작하는 에러 메시지를 출력하고, 다시 입력 받는다.
// main 메서드를 호출하는 Practice 클래스
public class Practice {
public static void main(String[] args) {
Study study = new Study();
study.multiple();
}
}
// 비즈니스 로직을 구현한 Study 클래스
public class Study {
public void multiple() {
int number = readNumber();
System.out.printf("%d에 2를 곱하면 %d 입니다.", number, number*2);
}
private int readNumber() {
System.out.print("숫자를 입력해주세요 : ");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
return number;
}
}
위의 코드는, 숫자를 입력 받아 2를 곱한 출력을 보여주는 로직을 구현한 것입니다.
출력은 아래 사진과 같습니다.
그런데, 숫자가 아닌 문자를 입력한다면 어떻게 될까요?
InputMismatchException이 발생하고 프로그램이 종료됩니다.
비즈니스 요구사항을 만족하려면
에러 메시지를 출력하고, 입력을 다시 받아야 합니다.
아래와 같이 코드를 수정하겠습니다.
// 재귀적 호출
public class Study {
public void multiple() {
try {
int number = readNumber(); // 1번
System.out.printf("%d에 2를 곱하면 %d 입니다.", number, number*2); // 2번
} catch (InputMismatchException e) { // 3번
System.out.println("[ERROR] 숫자를 입력해주세요."); // 4번
multiple(); // 5번
}
}
private int readNumber() {
System.out.print("숫자를 입력해주세요 : ");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
return number;
}
}
예외는 readNumber()에서 발생합니다.
그것을 호출하는 multiple()에 try-catch를 적용하고,
재귀적으로 multiple()을 다시 호출했습니다.
즉, 1번에서 예외가 발생하면
2번을 건너뛰고
3번에서 예외를 catch합니다.
그리고 4번, 5번 문장이 실행됩니다.
하지만 재귀적 호출 방식에는 치명적인 단점이 있습니다.
메모리에 스택 방식으로 메서드 호출 명령이 저장되지만,
호출된 메서드가 종료되지 않으면 메모리에 명령이 계속 쌓이게 되어
스택오버플로우(SOF) 현상이 발생할 수 있습니다.
사실 위의 예시 코드는 재귀적 호출 시 별도의 연산을 하지 않고,
함수를 호출하기만 하는 방식입니다.
그래서 SOF의 부담이 적기는 하지만, 호출된 깊이만큼 메모리에 스택이 쌓이는 것은 마찬가지입니다.
호출되는 로직이 변경되는 경우도 있을 수 있습니다.
따라서 재귀적 호출의 위험을 없애기 위해,
반복문을 통해서도 문제를 해결할 수 있습니다.
// 반복문 사용
public class Study {
public void multiple() {
while (true) {
try {
int number = readNumber();
System.out.printf("%d에 2를 곱하면 %d 입니다.", number, number * 2);
break;
} catch (InputMismatchException e) {
System.out.println("[ERROR] 숫자를 입력해주세요.");
}
}
}
private int readNumber() {
System.out.print("숫자를 입력해주세요 : ");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
return number;
}
}
catch문에서 multiple()을 다시 호출하지 않고,
try문이 온전히 실행되어 break 될 때까지
try-catch문을 반복합니다.
multiple()이 아닌 readNumber()에서 try-catch를 할 수도 있습니다.
다만, readNumber()의 반환 타입은 int이기 때문에
catch문에서는 두 가지 중 하나를 실행해야 합니다.
1. int를 반환한다.
2. Throwble 객체를 throw 한다.
입력을 다시 받지 않고, catch 문에서 int를 반환하는 것은 어색합니다.
Throwble 객체를 throw한다면, 결국 상위 메서드에서 try-catch를 통해 예외를 처리해야 합니다.
따라서 readNumber()가 아닌 상위 메서드 multiple()에서 예외 처리를 진행했습니다.
문제 상황 2번 - 특정 예외를 발생시킨 후 재실행
비즈니스 요구사항 1. 숫자를 입력 받고, 2를 곱한 수를 출력한다. 2. 숫자가 아닌 문자를 입력 받으면 IllegalArgumentException을 발생시키고, [ERROR]로 시작하는 에러 메시지를 출력한 후, 다시 입력 받는다.
이번에는 이를 IllegalArgumentException으로 바꿔서 예외를 발생시켜보겠습니다.
// 재귀적 호출
public class Study {
public void multiple() {
try {
int number = readNumber();
System.out.printf("%d에 2를 곱하면 %d 입니다.", number, number * 2);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
multiple();
}
}
private int readNumber() {
try {
System.out.print("숫자를 입력해주세요 : ");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
return number;
} catch (InputMismatchException e) {
throw new IllegalArgumentException("[ERROR] 숫자를 입력해주세요.");
}
}
}
이번에는 readNumber()내부에서 발생하는 InputMismatchException을 catch한 후 IllegalArgumentException을 발생시켰습니다.
그리고 상위 메서드인 multiple에서 그것을 catch한 후, 다시 multiple을 재귀적으로 호출합니다.
이 또한, 재귀적 호출로 인한 SOF의 위험에서 벗어나기 위해
반복문을 사용할 수 있습니다!
위의 문제 상황 1번을 참고하여, 직접 반복문으로 수정해보신다면
예외 처리에 대한 이해도가 높아지시리라 생각됩니다..만
아래에 반복문으로 수정한 코드를 공유드립니다.
// 반복문 사용
public class Study {
public void multiple() {
while (true) {
try {
int number = readNumber();
System.out.printf("%d에 2를 곱하면 %d 입니다.", number, number * 2);
break;
} catch (IllegalArgumentException e) {
System.out.println("[ERROR] 숫자를 입력해주세요.");
}
}
}
private int readNumber() {
try {
System.out.print("숫자를 입력해주세요 : ");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
return number;
} catch (InputMismatchException e) {
throw new IllegalArgumentException("[ERROR] 숫자를 입력해주세요.");
}
}
}
이렇게 예외가 발생했을 때, 예외가 발생한 메서드를 반복하는 로직을 구현해보았습니다.
1. 재귀적 호출
2. 반복문 사용
위의 두가지 방법으로 로직을 구현할 수 있었는데,
각각의 장단점이 있습니다.
재귀적인 호출은 반복문을 사용하지 않아 들여쓰기가 깊어지지 않는다는 장점이 있습니다.
또한 메모리에 명령 호출 스택이 쌓여, SOF(Stack OverFlow)가 발생할 수 있습니다.
반복문은 들여쓰기가 깊어지지만, 메서드 내부에서 반복을 원하는 부분만 반복할 수 있습니다.
import java.util.ArrayList;
import java.util.List;
public class Chef {
private static final String COMPLETE_COOK_ANNOUNCEMENT = " 준비가 완료되었습니다.";
private static final String COMPLETE_MENUS_ANNOUNCEMENT = "완성된 요리 : ";
private static List<String> dishes = new ArrayList<>();
private int countOfMe = 0;
public void cook(String menu) {
System.out.println(menu + COMPLETE_COOK_ANNOUNCEMENT);
dishes.add(menu);
this.countOfMe += 1;
}
public void informCountMadeMyMe() {
System.out.println(this.countOfMe);
}
public void informWholeCompleteMenus() {
System.out.println(COMPLETE_MENUS_ANNOUNCEMENT + dishes);
}
}
위 Chef 클래스는 음식을 요리하고,
해당 요리사가 만든 음식 개수와,
총 준비된 메뉴 리스트 알려줍니다.
여기서 변수의 종류를 정리해보면,
멤버 변수 : COMPLETE_COOK_ANNOUNCEMENT, COMPLETE_MENUS_ANNOUNCEMENT, dishes, countOfMe
클래스 변수 : COMPLETE_COOK_ANNOUNCEMENT,COMPLETE_MENUS_ANNOUNCEMENT, dishes
인스턴스 변수 : countOfMe
와 같습니다.
아래와 같이 main함수를 실행해보겠습니다.
public class Practice {
public static void main(String[] args) {
Chef woojin = new Chef();
Chef paul = new Chef();
woojin.cook("밥");
paul.cook("국");
woojin.informCountMadeMyMe();
paul.informCountMadeMyMe();
woojin.informWholeCompleteMenus();
paul.informWholeCompleteMenus();
}
}
woojin와 paul이 각각 밥과 국을 요리했습니다.
그리고 각자가 만든 요리 개수 묻습니다.
마지막으로 woojin. paul은 전체 준비된 메뉴 리스트를 묻습니다.
위와 같이 출력됨을 확인할 수 있습니다.
Chef 클래스의 countOfMe 변수(인스턴스 변수)는 각 인스턴스에만 영향을 받았습니다.
하지만 woojin이 준비한 밥, paul이 준비한 국 모두 dishes라는 클래스 변수에 추가되었습니다.
while(true) {
System.out.println("hi");
}
// hi 무한 출력
int count = 0;
while(count < 3) {
System.out.println(count);
count += 1;
}
// 0 1 2 출력
do-while문의 사용
do {
System.out.println("hi")
} while(true)
// hi 무한 출력
int count = 0;
do {
System.out.println(count);
count += 1;
} while(count < 3)
// 0 1 2 3 출력
위의 while문 코드를 그대로 do while문으로 변경한 코드입니다.
0 1 2 가 출력된 while문이,
do while문에서는 0 1 2 3이 출력됨을 알 수 있습니다.
그렇다면 do while문을 왜 사용하는 것일까요..
그 이유에 대해 정리해보겠습니다.
do-while문을 사용하는 이유
1, 4가 이미 들어있는 numbers라는 리스트가 있습니다.
1과 9사이의 랜덤 숫자를 뽑아 numbers에 넣고 싶은데,
이미 들어 있는 숫자는 안 된다고 하겠습니다.
예제 코드 - while문 사용)
class practice {
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 4));
int randomNumber = 0;
boolean isIn = true;
while (isIn) {
randomNumber = Randoms.pickNumberInRange(1, 9); // 1과 9 사이 숫자를 랜덤 반환
isIn = number.contain(randomNumber); // randomNumber가 numbers에 이미 들어 있는지 여부
}
numbers.add(randomNumber);
}
while문으로 number에 들어있지 않은 숫자를 뽑을 때까지
randomNumber에 새로운 수를 가져오는 식으로 구현했습니다.
그런데, while의 반복이 지속되는 조건(isIn)은 number에 이미 randomNumber가 들어있다는 것입니다.
그런데 randomNumber를 뽑기도 전에 isIn이 true라는 것은 코드의 가독성 측면에서 좋지 않습니다.
예제 코드의 수정 - do while 적용)
class practice {
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 4));
int randomNumber = 0;
boolean isIn;
do {
randomNumber = Randoms.pickNumberInRange(1, 9); // 1과 9 사이 숫자를 랜덤 반환
isIn = number.contain(randomNumber); // randomNumber가 numbers에 이미 들어 있는지 여부
} while(isIn)
numbers.add(randomNumber);
}