Java 계산기 과제 트러블슈팅 + 피드백

ssongyi·2025년 2월 28일
0

개발환경 - IndelliJ IDEA / JDK 17

필수기능

Lv 1. 클래스 없이 기본적인 연산을 수행할 수 있는 계산기 만들기

  • 양의 정수(0 포함) 입력받기
  • 사칙연산 기호(+, -, *, /) 입력받기
  • 위에서 입력받은 양의 정수 2개와 사칙연산 기호를 사용하여 연산을 진행한 후 결과값을 출력
  • 반복문을 사용하되, 반복의 종료를 알려주는 “exit” 문자열을 입력하기 전까지 무한으로 계산을 진행할 수 있도록 소스 코드를 수정하기

Lv 2. 클래스를 적용해 기본적인 연산을 수행할 수 있는 계산기 만들기

  • 사칙연산을 수행 후, 결과값 반환 메서드 구현 & 연산 결과를 저장하는 컬렉션 타입 필드를 가진 Calculator 클래스를 생성
  • Lv 1에서 구현한 App 클래스의 main 메서드에 Calculator 클래스가 활용될 수 있도록 수정
  • App 클래스의 main 메서드에서 Calculator 클래스의 연산 결과를 저장하고 있는 컬렉션 필드에 직접 접근하지 못하도록 수정 (캡슐화)
  • Calculator 클래스에 저장된 연산 결과들 중 가장 먼저 저장된 데이터를 삭제하는 기능을 가진 메서드를 구현한 후 App 클래스의 main 메서드에 삭제 메서드가 활용될 수 있도록 수정

도전기능

Lv 3. Enum, 제네릭, 람다 & 스트림을 이해한 계산기 만들기

  • Enum 타입을 활용하여 연산자 타입에 대한 정보를 관리하고 이를 사칙연산 계산기 ArithmeticCalculator 클래스에 활용
  • 실수, 즉 double 타입의 값을 전달 받아도 연산이 수행하도록 만들기
  • 저장된 연산 결과들 중 Scanner로 입력받은 값보다 큰 결과값 들을 출력

charAt 메서드 사용 불가 에러

Scanner input = new Scanner(System.in);

// 사칙연산 기호 입력받기
System.out.print("사칙연산 기호를 입력하세요: ");
char operator = input.charAt(0)
System.out.println("operator = " + operator);

구현 조건에 charAt 메서드를 사용하라는 조건이 있어서 코드에 구현했는데, charAt 메서드를 사용할 수 없다는 에러가 떴다.

그 이유는 Scanner 클래스가 해당 메서드를 제공하지 않기 때문이었다.

해결 방법으로는 아래와 같다.

  1. input.next() 로 문자열로 입력받는다.
  2. 입력받은 문자열의 첫 번째 문자를 charAt(0) 으로 추출한다.
char operator = input.next().charAt(0);

그런데 왜 String 이 아니라 char 로 처리해야할까?

그 이유는 아래와 같다고 한다.

  1. 메모리 효율성: char는 2바이트를 사용하지만, String은 더 많은 메모리를 사용
  2. 연산 효율성: char는 기본 데이터 타입으로, 비교 연산 시 '==' 연산자를 사용하여 직접 값을 비교할 수 있습니다. 반면 String은 객체이므로 equals() 메소드를 사용
  3. 목적 부합: 사칙연산 기호는 단일 문자이므로, 문자를 표현하는 데 특화된 char 타입이 더 적합
  4. 타입 안정성: char를 사용하면 입력이 단일 문자로 제한되어, 여러 문자가 입력되는 실수 방지 가능

입력 불일치(InputMismatchException) 에러

System.out.print("첫 번째 숫자를 입력하세요: ");
int num1 = input.nextInt();
System.out.println("num1 = " + num1);
계산을 시작합니다. 종료하려면 'exit' 을 입력하세요.
계속하려면 아무 키나 누르세요(종료: exit)첫 번째 숫자를 입력하세요: e
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at calculator.Calculator.main(Calculator.java:22)

Process finished with exit code 1

내가 정수 대신 문자 'e' 를 입력했기 때문에 입력 불일치 에러가 났다.

나는 숫자 입력시 예외 처리를 추가하는 조치 + 유효한 숫자가 입력될 때까지 반복하여 요청하는 조치를 취했다.

nt num1 = 0, num2 = 0;
boolean validInput = false;

// 첫 번째 숫자 입력
            while (!validInput) {
                System.out.print("첫 번째 숫자를 입력하세요: ");
                try {
                    num1 = Integer.parseInt(input.nextLine());
                    validInput = true;
                } catch (NumberFormatException e) {
                    System.out.println("유효한 숫자를 입력해주세요.");
                }
            }
            System.out.println("num1 = " + num1);

// 두 번째 숫자 입력
            validInput = false;
            while (!validInput) {
                System.out.print("두 번째 숫자를 입력하세요: ");
                try {
                    num2 = Integer.parseInt(input.nextLine());
                    validInput = true;
                } catch (NumberFormatException e) {
                    System.out.println("유효한 숫자를 입력해주세요.");
                }
            }
            System.out.println("num2 = " + num2);
            

이렇게 했더니 첫 번째 숫자나 두번째 입력칸에 숫자가 아니라 문자를 입력해도 알고리즘이 진행해버리는 에러가 발생했다.
정수를 입력하면 다시 입력하라고 돌아갔다...

이것은 두 번째 숫자 입력 부분에서 validInput 변수를 재설정하지 않은 오류였다.

int num1 = 0, num2 = 0;
boolean validInput; // <-- 변경: 여기서 초기화 되지 않음

 // 첫 번째 숫자 입력
            validInput = false; // <-- 변경: 여기서 false 로 설정
            while (!validInput) {
                System.out.print("첫 번째 숫자를 입력하세요: ");
                try {
                    num1 = Integer.parseInt(input.nextLine());
                    validInput = true;
                } catch (NumberFormatException e) {
                    System.out.println("유효한 숫자를 입력해주세요.");
                }
            }
            System.out.println("num1 = " + num1);

 // 두 번째 숫자 입력
            validInput = false; // <-- 변경: 두 번째 숫자 입력 전 다시 false로 설정
            while (!validInput) {
                System.out.print("두 번째 숫자를 입력하세요: ");
                try {
                    num2 = Integer.parseInt(input.nextLine());
                    validInput = true;
                } catch (NumberFormatException e) {
                    System.out.println("유효한 숫자를 입력해주세요.");
                }
            }
            System.out.println("num2 = " + num2);

두 번째 숫자 입력 전에 validInput = false;로 재설정하여 문제를 해결했다.


대소문자 구분 없이 문자열 비교

exit 을 'EXIT' 으로 대문자로 입력했을 경우 계산기가 종료되지 않는 버그가 있었다.

if(continueInput.equalsIgnoreCase("exit")) {
  break;
}

해당 버그는 equalsIgnoreCase()를 사용하여 대소문자 구분 없이 "exit"를 인식하도록 했다.


System.out.println() / print() 계산기 종료되지 않는 버그

대소문자 구분 없이 문자열을 비교하여 계산기를 종료하려했으나...
exit 이나 EXIT 을 입력해도 계산기가 종료되지 않는 버그가 발생했다.

 System.out.print("계속하려면 아무 키나 누르세요(종료: exit): ");
 String continueInput = input.nextLine();

println()은 줄 바꿈을 포함하기 때문에, 사용자 입력이 새 줄에서 시작되어 빈 문자열로 인식되어 나는 에러라고 한다.

System.out.print("계속하려면 아무 키나 누르세요(종료: exit): "); // println()을 print()로 변경

print() 를 사용했더니, 사용자 입력이 같은 줄에서 이어지므로, 입력한 'exit' 이 정확히 인식되어 프로그램이 정상적으로 종료되었다.


개행 문자(\n) 버그

System.out.println("계산을 시작합니다. 종료하려면 'exit'을 입력하세요.\n");

System.out.print("계속하려면 Enter 키를 누르세요(종료: exit): ");
String continueInput = input.nextLine().trim();

가독성을 위해 문자 제일 끝에 \n 개행문자를 삽입했더니,
아무 키도 누르지 않았음에도 첫번째 숫자를 입력하라는 시스템 메시지가 출력됐다.

이 문제는 입력 버퍼에 남아있는 개행 문자 \n 때문에 발생한다고 한다.
이전 계산 후 사용자가 Enter 키를 눌러 결과를 확인하면, 그 Enter 키 입력이 다음 루프의 첫 번째 입력으로 처리되어 버린다고 한다 ....

System.out.println("\n계산을 시작합니다. 종료하려면 'exit'을 입력하세요.");

System.out.print("계속하려면 Enter 키를 누르세요(종료: exit): ");
String continueInput = input.nextLine().trim(); // trim()을 추가하여 앞뒤 공백 제거

문제 해결 방법은 다음과 같다.

  1. 개행문자를 문자열 앞에 삽입
  2. trim()을 사용하여 입력의 앞뒤 공백을 제거

방어적 복사(Defensive copy) : Java의 다형성

연산 결과를 저장하는 컬렉션 타입 필드를 만들었다.

<Integer> 타입을 사용하여 정수형 결과를 저장하고, 생성자에서 초기화하여 항상 빈 리스트로 시작한다.

getResults

그런데 왜 List<> 가 아닌 ArrayList<> 로 반환해야 할까?

이유는 다음과 같았다.

  1. 방어적 복사(Defensive copy)
    • 이 방식은 원본 'results' 리스트의 내용을 변경할 수 없게 한다.
    • 메서드 호출자가 반환한 리스트를 수정하더라도 원본 'results' 에는 영향 X
  2. 캡슐화 유지
    • 내부 구현 세부사항을 숨기고 인터페이스만 노출함으로써 캡슐화를 강화한다.

return List<> 는 원본에도 영향을 준다

List<> 로 리턴하는 것은 방어적 복사가 이루어지지 않는다고 한다.
원본 리스트의 참조를 그대로 전달하게 되어, 반환된 리스트를 수정하면 원본에도 영향을 준다.

Public 으로 선언했는데 캡슐화가 된다

이 메서드는 Public 이지만, 여전히 캡슐화의 원칙을 유지하고 있다.
이유는 다음과 같다.

  1. 방어적 복사
  2. 정보 은닉
    • 메서드가 public 이더라도, 내부 데이터 구조(results) 의 실제 구현을 숨기고 있다.
    • 외부에서는 리스트의 내용만 볼 수 있을 뿐, 어떻게 저장되고 관리되는지 알 수 X
  3. 인터페이스 노출
    • 'List' 인터페이스를 반환함으로써 구체적인 세부사항을 숨기고 필요한 기능만 노출한다.

ArrayList = List 인터페이스

Java 의 다형성 개념이 적용되는 것 같다.
: 코드의 유연성과 재사용성을 높이는 객체지향 프로그래밍의 중요한 원칙

  1. ArrayList = List 인터페이스를 구현한 구체 클래스
  2. Java 에서는 상위 타입(List 인터페이스) 으로 하위 타입(ArrayList) 객체를 참조할 수 있음
    --> 업캐스팅
  3. new ArrayList<>(results) 는 ArrayList 객체를 생성하지만, 이 객체는 List 인터페이스 타입으로 반환됨
  4. 메서드 시그니처에서 반환 타입이 List<Integer> 으로 선언되어 있으므로, 실제로는 ArrayList 객체를 생성하지만 이를 List 타입으로 반환하는 것임

Java 에서 제공하는 예외(Exception) 클래스

ArithmeticException

  1. java.lang 패키지에 속함
  2. 산술 연산 중 발생하는 예외를 나타냄
  3. 주로 정수를 0으로 나누려고 할 때 발생
  4. RuntimeException 의 하위 클래스로, 체크되지 않는 예외(unchecked exception) 이다.

IllegalArgumentException

  1. java.lang 패키지에 속함
  2. 메서드에 부적절한 인자가 전달되었을 때 발생하는 예외
  3. 메서드의 매개변수가 허용되지 않는 값을 가질 때 사용됨
  4. RuntimeException 의 하위 클래스로, 체크되지 않는 예외(unchecked exception) 이다.

NumberFormatException

  1. 숫자가 아닌 문자가 포함된 문자열을 변환하려 할 때
    • ex. "123a" 를 정수로 변환 시도
  2. 변환하려는 숫자 타입의 범위를 초과하는 값을 변환하려 할 때
    • ex. Integer.MAX_VALUE 보다 큰 값을 int 로 변환 시도
  3. 빈 문자열이나 공백만 있는 문자열을 변환하려 할 때
  4. null 을 변환하려 할 때

과제 평가

질문

답변

Enum

  • 변하지 않는 수 (상수)
  • 사칙연산과 같이 변하지 않는 개념들을 정의할 때 사용
  • Enum 을 사용하게 되면 개발자가 변경해주지 않는 이상 상수가 추가될 일 X
    - 타입에 대한 안정성이 생김

Enum vs Class ?

  • 확장이 얼만큼 진행되는가? 기준으로 선택
  • Enum 을 확장시키려면 개발자가 빌드를 다시 해야함 vs Class 는 사용자의 input 으로 객체를 새로 생성할 수 있음

Interface vs Class ?

  • 공통 로직이 있는가? 기준으로 선택
  • 동작 방식만 맞추려면 Interface vs 구현체가 있어야 한다면 Class
  • 다중상속 가능한 Interface vs 단일상속만 가능한 Class

과제 피드백 (완성도)

과제 피드백 (이해도)

과제 피드백 (우수성)

피드백 정리

  1. "!"는 인지가 잘 되지 않기때문에 ignore 함수(equalsIgnoreCase)를 사용하는 것이 훨씬 좋음

  2. continueInput.equalsIgnoreCase("exit") !== "exit".equalsIgnoreCase(continueInput)

  • NullPointException 발생 유무가 다름
  • "exit".equalsIgnoreCase(continueInput) 함수 권장
    - 상수와 비교할 때 에러가 발생할 상황을 없애버리기 때문
  1. setter 는 캡슐화가 쉽게 깨지니 지양할 것
  • 객체끼리 정보를 교환할 때는 DTO 를 이용해 값을 넣어주면 양질의 코드 작성 가능
  1. 주석을 작성할 때, method 뿐 아니라 다른 코드에도 달기
  • javaDoc 을 검색해보기! 주석 형태 있음!
  1. README 는 프로젝트를 설명해주는 가장 첫 번째 문서이기 때문에, JDK 버전, 목적, 주요 기능, 개선 사항 등을 적기

JavaDoc ?

JavaDoc 을 활용한 문서 처리

  • Java 언어 소스 코드에서 "프로그램을 설명하는 문서"를 생성하는 메커니즘
  • JDK가 설치 되어있다면 Javadoc 사용 가능
  • MVC 패턴으로 개발하는 방식에서 다른 클래스에서 메소드를 호출하는 경우가 많아 사용하기 편리


0개의 댓글