Lv1. 클래스 없이 기본적인 연산을 수행할 수 있는 계산기 만들기
1-2. 사칙연산 기호 입력받기
가이드 예시로 주신 `charAt(0)` 이걸 굳이 왜 써야하는지 궁금했다.
>> 왜 charAt(0)을 사용하는가?
Scanner.next()가 문자열로 반환되기 때문이다. 따라서, 그 문자열에서 첫 번째 문자를 꺼내서 연산자로 사용한다. 여러 개의 문자를 입력할 경우, 첫 번째 문자만 추출한다.
1-3. 입력받은 값과 연산자를 사용하여 연산 진행
예시 코드로 result가 주어진 것을 보고, case : result, case : return 이 둘의 차이가 궁금했다.
>> case : result, case : return의 차이는?
case : result
result는 변수에 값을 할당하는 방식이다.
switch 문에서 어떤 조건에 해당하는 경우, result 변수에 값을 할당하고, 이후에 그 값을 사용하여 결과를 출력하거나 다른 연산을 진행할 수 있다.
여러 개의 조건을 처리하고 연산 결과를 저장한 다음, 한꺼번에 출력을 할 때 유용하다.
연산을 실행한 후 result에 값을 저장하고, 이후에 그 값을 출력.
case : return
return은 값을 반환하고 메서드를 즉시 종료하는 방식이다.
switch 문 안에서 return을 사용하면, 조건에 해당하는 경우 바로 값을 반환하고, 메서드의 실행을 종료한다.
단일 값을 즉시 반환하고, 이후에 다른 처리가 필요 없을 때 유용하다.
각 조건에 해당하면 바로 값을 반환(return)하고, 메서드 실행을 즉시 종료.
결론:
result는 결과를 변수에 저장하고, 이후에 값을 사용하고자 할 때 사용된다.
return은 값을 반환하고, 메서드를 종료할 때 사용된다.
>> 왜 case result를 사용하는가?
1. 후속 작업의 필요성
case result 방식은 각 연산을 처리한 후, 결과를 변수에 저장해서 공통된 후속 작업을 할 수 있다.
계산 결과를 저장한 후, 그 값을 출력하거나 다른 연산에 사용할 경우, 한 번에 모든 결과를 처리할 수 있어 코드를 간결하게 만들 수 있다.(반복된 코드 방지: 계산 결과를 한 곳에 모아놓고 나중에 한 번에 처리하면, 코드를 여러 번 작성하지 않아도 되고 더 깔끔해진다.)
2. 메서드가 종료되지 않음
case result 방식은 계산 후에도 메서드가 종료되지 않기 때문에, 이후에도 추가적인 코드나 연산을 이어서 작성할 수 있다. 즉, 메서드가 끝나지 않기 때문에 계산 후에도 다른 로직을 추가할 수 있습니다.
3. 반복문과의 결합
코드에서 반복문을 사용하고 있기에, result 변수를 사용하여 여러 번 계산을 반복할 수 있다. return을 사용하면 메서드가 종료(반복문이 중단)되므로, 반복적인 계산이 불가능하다.
>> 왜 case return을 사용하지 않았는가?
1. 메서드가 즉시 종료되기 때문
return을 사용하면 해당 메서드가 즉시 종료되기 때문에, 다른 추가 작업을 할 수 없다. 만약 반복적인 연산이나 다음 단계의 추가적인 로직을 실행해야 한다면, return을 사용할 수 없다.
2. 공통적인 후속 작업 처리 어려움
각 case에서 값을 바로 반환하면, 공통된 후속 작업을 처리하는 것이 어렵다. 연산 후에 모든 결과를 한 번에 출력하거나 다른 처리를 해야 할 때, return은 부적합하다.
1-4. 반복문을 사용하여 계산을 계속 진행할지 묻기
exit을 입력 받으면 반복 종료 구조를 어떻게 구현할 지 고민하다가 equals를 사용했다가 "EXIT" 대문자로 썼을 때는 종료가 안된다는 사실을 알게 돼서 대소문자를 구분하지 않고 문자열을 비교하는 equalsIgnoreCase를 사용하기로 함.
>> 왜 exitInput.equalsIgnoreCase("exit")를 사용하는가?
1. 대소문자 구분 없이 입력을 허용
2. 유연한 사용자 입력 처리
3. 사용자 편의성 향상
>> equals와 equalsIgnoreCase의 차이는?
equals: 대소문자를 구분하고 문자열을 비교 ("exit"과 "EXIT"는 다른 문자열로 인식)
equalsIgnoreCase: 대소문자를 구분하지 않고 문자열을 비교 ("exit"과 "EXIT"는 동일한 문자열로 인식)
Lv 2. 클래스를 적용해 기본적인 연산을 수행할 수 있는 계산기 만들기
2-1. Calculator 클래스 생성 및 메서드 구현
연산 결과를 저장하는 컬렉션 타입(List, Set, Queue, Map) 필드를 생성하라고 했는데, 4가지 중에서 어떤 컬렉션을 선택할 것인가?
>> 왜 List<Integer>를 사용하는가?
1. 순서가 있는 데이터 저장
List는 저장된 데이터의 순서를 보장한다. 연산이 이루어진 순서대로 결과를 저장하고, 나중에 그 순서대로 값을 조회할 수 있다.
2. 중복 허용
연산 결과에서 같은 값이 여러 번 나올 수 있다. List는 중복된 값을 허용하기 때문에, 중복된 연산 결과도 문제없이 저장할 수 있다.
3. 동적 크기
List는 크기가 동적이다. 연산이 몇 번 이루어질지 미리 알 수 없으므로, 연산이 진행될 때마다 결과를 계속 추가할 수 있도록 크기가 자동으로 조정된다.
4. 인덱스를 통한 접근
List는 인덱스를 통해 특정 위치에 저장된 데이터를 빠르게 접근할 수 있다. 특정 위치의 연산 결과를 조회하거나 수정하는 작업이 필요할 때, 인덱스를 통해 쉽게 처리할 수 있다.
>> 왜 다른 컬렉션(Set, Queue, Map)을 사용하지 않았는가?
Set:
중복을 허용하지 않는 자료구조이다. 연산 결과에서 같은 값이 여러 번 나올 가능성이 있기 때문에 Set은 적합하지 않다.
Set을 사용하면 중복된 값은 한 번만 저장된다. 모든 연산 결과를 저장해야 하므로, Set은 적절하지 않다.
Queue:
선입선출(FIFO) 방식으로 데이터를 처리하는 자료구조이다. 연산 결과를 순차적으로 저장하는 것은 가능하지만, Queue는 데이터를 첫 번째 요소부터 처리해야 한다는 제한이 있다.
즉, 나중에 저장된 연산 결과에 임의로 접근하기 어렵다. 모든 연산 결과를 저장한 후 임의의 순서로 조회하거나 처리할 수 있어야 하기 때문에, Queue는 적합하지 않다.
Map:
Map은 키-값 쌍으로 데이터를 저장하는 자료구조이다. 연산 결과는 단순한 값의 목록이기 때문에, 키-값 구조가 불필요하다.
또한, Map은 중복된 키를 허용하지 않으므로, 중복된 연산 결과를 저장하는 데 부적합하다. 단순히 결과값만 저장하는 상황에서는 Map이 적합하지 않다.
결론:
List는 순서가 있는 데이터 저장, 중복 허용, 동적 크기를 가진 자료구조로, 연산 결과를 저장하고 관리하기에 가장 적합한 선택이다. 다른 컬렉션인 Set(중복 허용 안 함), Queue(FIFO 방식), Map(키-값 구조)은 이러한 특성 때문에 연산 결과를 순차적으로 저장하고 관리하는 상황에는 적합하지 않다.
>> 왜 List를 사용하고 ArrayList로 초기화하는가?
List는 인터페이스이다. 인터페이스를 사용하면, 나중에 다른 리스트 구현체(예: LinkedList, Vector)로 쉽게 바꿀 수 있는 유연성이 생긴다.
ArrayList는 배열 기반 리스트로, 순차적으로 데이터를 저장하고 빠르게 접근할 수 있다. 계산기에서 연산 결과를 순서대로 저장하고 필요할 때 빠르게 조회하는 데 적합하다.
또, 초기화되지 않은 리스트에 값을 추가하려고 하면 NullPointerException이 발생할 수 있다.
결론:
List<Integer> results는 계산기의 연산 결과를 저장하는 리스트이며, 이를 ArrayList로 초기화하여 동적으로 크기를 조정하며 데이터를 저장할 수 있다.
>> 런타임 예외를 어떻게 처리할 것인가?
산술 오류가 발생했을 때(0으로 나눌 시) 직접 ArithmeticException 예외를 발생
사칙연산 기호가 +, -, *, / 중 하나가 아닌 경우, IllegalArgumentException 예외를 발생
런타임 예외의 종류:
1. NullPointerException: 객체가 null인데 해당 객체의 메서드나 필드에 접근하려고 할 때 발생.
2. ArrayIndexOutOfBoundsException: 배열의 잘못된 인덱스에 접근하려고 할 때 발생.
3. ClassCastException: 잘못된 타입으로 객체를 캐스팅하려고 할 때 발생.
4. IllegalArgumentException: 메서드에 잘못된 인수를 전달했을 때 발생.
5. ArithmeticException: 0으로 나누기와 같은 잘못된 산술 연산을 할 때 발생.
2-3. 캡슐화 (Getter, Setter)
Getter, Setter 메서드를 구현하고 나서 'Setter를 굳이 해야하는가?'하는 의문이 들었다. Setter를 왜 사용하는가? 바로 캡슐화를 위해서이다. Setter 메서드는 원래 캡슐화에 도움을 주지만, 내가 구현한 계산기에서는 외부에서 연산 결과를 수정할 필요가 없기 때문에 오히려 캡슐화에 방해가 된다.
>> 언제 Setter를 사용하는 것이 좋을까?
외부에서 리스트를 완전히 교체해야 할 필요가 있거나, 객체의 상태를 초기화하거나 재설정해야 할 경우에만 Setter를 사용하는 것이 적절하다. 그러나 Calculator 클래스의 경우, 리스트는 내부 연산에 의해 관리되어야 하므로 Setter는 필요하지 않다.
결론:
Getter만 사용하는 것이 더 적절하다. 외부에서는 연산 결과를 확인할 수만 있고, 리스트 자체는 내부에서만 관리되도록 하는 것이 Calculator 클래스의 안정성과 일관성을 유지하는 데 더 유리하다.
>> Setter가 필요하지 않은 이유
1. 일관성: 연산 결과와 같은 중요한 데이터는 외부에서 변경될 수 없도록 보호해야 한다.
2. 데이터 보호: 내부 데이터를 외부에서 마음대로 수정할 수 없도록 Setter를 제공하지 않는 것이 좋다.
3. 실수 방지: 실수로 잘못된 값이 설정되거나, 외부에서 리스트를 수정해 객체의 상태를 망가뜨릴 위험을 줄이기 위해 Setter를 없애는 것이 좋다.
4. 불필요한 외부 제어: 연산 결과는 클래스 내부에서 관리되는 것이지, 외부에서 임의로 설정할 필요가 없다.
따라서, 객체의 무결성을 지키고, 데이터 보호를 강화하기 위해 Setter를 제공하지 않는 것이 좋다. 객체는 내부적으로만 데이터를 관리하고, 외부에서는 데이터를 수정할 수 없도록 제한하는 것이 더 안전한 설계이다.
2-4. 가장 먼저 저장된 결과 삭제
어떻게 구현할 것인가? 삭제 기능 구현 (FIFO 방식)
Calculator 클래스에 저장된 연산 결과는 리스트 형태로 관리되고 있다. 리스트에서 가장 먼저 추가된 데이터를 삭제하려면 FIFO(First In, First Out) 방식으로 데이터를 제거해야 한다. 리스트에서는 인덱스 0이 가장 먼저 들어온 데이터에 해당하므로, 이 데이터를 삭제하면 된다.
Lv2 계산기 완성 후
실행해보니, 첫 번째 숫자, 두 번째 숫자에 정수가 아닌 값을 입력하면, 오류가 발생한다.
>> 어떻게 해결할 것인가?
try-catch 블록을 통해 InputMismatchException 예외를 처리한다.
try-catch 블록:
Scanner.nextInt()를 사용할 때, 사용자가 정수가 아닌 값을 입력하면 InputMismatchException이 발생한다.
이 예외를 catch하여 "잘못된 입력입니다. 숫자를 입력해 주세요."라는 메시지를 출력하고, 잘못된 입력을 건너뛴다.
반복 입력 처리:
잘못된 입력이 있을 경우, while 루프를 사용하여 계속해서 올바른 숫자가 입력될 때까지 반복적으로 입력을 받는다.
validInput 변수를 사용해 잘못된 입력이 있으면 다시 입력을 받도록 while 루프를 사용하고, 유효한 입력이 있을 때만 루프가 종료되도록 한다.
잘못된 입력 건너뛰기:
잘못된 입력을 sc.next()로 건너뛰어, 무한 반복에 빠지지 않고 사용자로부터 새로운 입력을 받을 수 있도록 처리한다.