[회고] 계산기 과제 회고

wannabeing·2025년 3월 6일
1

SPARTA-회고

목록 보기
1/5

✅ 개요

  • 개발기간: 2025.02.26(목) ~ 2025.03.05(목)
  • 자바버전: OpenJDK 17
  • Java를 사용하여 구현된 콘솔 기반 계산기 프로그램
  • 기본적인 사칙연산을 수행하고, 연산 결과를 저장/출력 및 삭제할 수 있는 기능을 제공
  • 프로젝트 링크: GitHub Repository

✅ 요구사항 정의

Level1

  • 클래스 없이 기본적인 연산을 수행할 수 있는 계산기 만들기
  • 사칙연산 기호(➕,➖,✖️,➗)를 입력받기
  • 위에서 입력받은 양의 정수 2개와 사칙연산 기호를 사용하여 연산을 진행한 후 결과값을 출력하기
  • 반복문을 사용하되, 반복의 종료를 알려주는 “exit” 문자열을 입력하기 전까지 무한으로 계산을 진행할 수 있도록 소스 코드를 수정하기

Level2

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

Level3

  • Enum 타입을 활용하여 연산자 타입(➕,➖,✖️,➗)에 대한 정보를 관리
  • 제네릭으로 실수, 즉 double 타입의 값을 전달 받아도 연산이 수행하도록 만들기 (Level2까지 정수만 받음)
  • 제네릭, 람다&스트림을 이해한 계산기 만들기 (적용해보기)

✅ Keep

현재 프로젝트에서 만족했고, 앞으로의 훈련기간에서 지속하고 싶은 부분을 작성했다.

  • 새로운 배운 개념 (Enum, 제네릭, 람다, 스트림) 들을 적용해보았다.
  • 단순한 과제라도 요구사항을 깊이 고민하고 구현하는 과정이 중요하다는 점을 배웠다.
  • 피드백을 통해 내가 미처 생각하지 못한 부분을 개선할 수 있었고, 이를 통해 더 나은 코드를 작성했다고 생각한다.
  • 과제에 몰입하다 보니 문제를 더 깊이 분석하게 되었고, 다양한 접근 방식을 시도할 수 있었다.
  • 앞으로 남은 훈련 과정에서도 주어진 과제에 적극적으로 임하며, 더 완성도 높은 결과물을 만들고 싶다.


✅ Problem

현재 프로젝트에서 어려웠던 점과 아쉬웠던 점을 작성했다.

🚀 Level1

- 프로그램 실행 예시

### [LEVEL1] 프로그램 실행예시
---------------------------------------

첫번째 숫자를 입력하세요. (exit 입력 시 종료): 1
두번째 숫자를 입력하세요. (exit 입력 시 종료): 2
사칙연산 기호를 입력하세요! (+, -, *, /): *
결과 (1 * 2): 2

첫번째 숫자를 입력하세요. (exit 입력 시 종료): 

- 문제 상황

두번째 숫자를 입력 받고나서 사칙연산을 받을 때, 사칙연산에 자동으로 공백이 들어가는 오류가 있었다.

- 피드백 받았던 부분

    // 사용자의 숫자 입력 값을 검증하는 메서드
    private static int readInt(Scanner scanner) {
        // 입력 값이 "exit"일 경우 프로그램을 종료합니다.
        if (scanner.hasNext("exit")) {
            System.out.println("프로그램을 종료합니다.");
            System.exit(0);
        }

        // 입력 값이 숫자가 아닐 경우 다시 입력 받도록 합니다.
        while (!scanner.hasNextInt()) {
            System.out.print("올바른 숫자를 입력하세요!: ");
            scanner.next(); // 잘못된 입력 제거
        }
        // 입력 값이 올바를 경우, 반환합니다.
        return scanner.nextInt();
    }

숫자를 입력받으면서 exit를 입력했을 때 프로그램 종료 기능을 수행하고 싶어서 흐름대로 해당 메서드에 넣었는데,

해당 메서드에서 프로그램 종료 기능이 들어가는 것 자체가 메서드의 책임을 벗어난다는 피드백을 받았다. 사용자의 숫자 값을 검증하는 메서드에서 종료기능이 같이 들어가는 것이 알맞지 않다는 뜻이였다.

또한 메서드 네이밍도 조금 더 생각해보면 좋을 거 같다고 하셨다.
어떻게 보면 네이밍이 제일 어려운 듯.. 😂

- 해결방안

문제해결

  • 사칙연산 입력을 받을 때, 입력 버퍼에 남은 Enter 버퍼가 남아서 그렇다고 한다.
    nextInt()는 입력값으로 들어온 값 중에 Enter공백을 기준, 그 앞의 Int형 값을 가져온다. 버퍼에 남아있는 값은 Enter(공백)인데, 그 값이 그대로 사칙연산 값에 들어가는 것이였다. 해당 문제는 스캐너.nextLine(); 한 줄을 추가해서 해결할 수 있었다. 버퍼에 남아 있는 값을 지워주는 역할을 한다!

피드백

  • Level2에서 프로그램 종료 기능은 Calculator 클래스에 넣었다.

      // ✅ 계산기 종료 메서드
       public void exitCalculate(){
           outputPrinter.printExitPrompt();
    
           // 프로그램 종료
           System.exit(0);
       }
  • 기능별로 분리하기 위해 io.input 패키지를 생성하였고, InputReader 클래스에 메서드를 분리하였다.

  • getIntInput() 로 네이밍을 하였다.

    package level2.io.input;
    
    public class InputReader {
       // Scanner 객체 생성
       private static final Scanner scanner = new Scanner(System.in);   
       
       // ✅ 사용자의 숫자 입력 값을 검증하는 메서드
       public int getIntInput() {
           // 입력 값이 숫자가 아닐 경우 다시 입력 받음
           while (!scanner.hasNextInt()) {
               System.out.print("올바른 숫자를 입력하세요!: ");
               scanner.next(); // 잘못된 입력 제거
           }
    
           // 입력 값이 숫자일 경우, 변수에 숫자 저장
           int number = scanner.nextInt();
    
           // 개행 문자 제거
           scanner.nextLine();
    
           // 입력 값이 올바를 경우, 반환
           return number;
       }
       
    	. . .
    }

🚀🚀 Level2

- 프로그램 실행 예시

### [LEVEL2] 프로그램 실행예시
---------------------------------------

[ 계산기 프로그램 ]
1. 계산하기
2. 출력하기(All)
3. 삭제하기
4. 종료하기
선택: 

- 피드백 받았던 부분

package level2.calculator;

public class Calculator {
    // 사칙연산 기호와 Operation 객체를 매핑하는 Map 컬렉션
    private final Map<String, Operation> operations;

    // ✅ 생성자
    public Calculator() {
        // FIXME: 추후 레벨 3에서 수정이 필요한 부분
        // 사칙연산 기호와 Operation 객체를 생성 및 연결
        operations = Map.of(
                "+", new Addition(),
                "-", new Subtraction(),
                "*", new Multiplication(),
                "/", new Division()
        );
    }
package level2.operations;

public interface Operation {
    int calculate(int firstNumber, int secondNumber);
}
package level2.operations;

public class Addition implements Operation {
    @Override
    public int calculate(int firstNumber, int secondNumber) {
        return firstNumber + secondNumber;
    }
}

코드설명

  • Map 컬렉션을 사용하여 사용자의 연산기호와 연산하는 객체를 매핑
  • Calculate 클래스 생성할 때, 사용자의 연산기호와 알맞는 연산 객체를 매핑
  • 연산기능을 계산기에서 분리하여 Operation 함수형 인터페이스로 구현
  • Calculate 클래스에서는 operations.calculate() 로 계산

피드백

  • Map 컬렉션을 사용하는 부분을 Enum을 활용하면 좋겠다는 피드백
  • operations 패키지 안에 사칙연산 별 클래스파일이 있어서 유지보수 및 파일이 불필요하게 많다는 피드백

- 해결방안

  • Level3에서 Map 컬렉션Enum 클래스 사용

  • 연산기능(ADD, SUBTRACT, MULTIPLY, DIVIDE)을 Enum에서 정의

  • 사용자의 입력값으로 알맞는 연산기능을 찾는 메서드 생성 (findByOperator)

  • Enum 클래스의 calculate() 메서드를 통해 Calculator 클래스에서는 호출만 하여 연산 가능하도록 함

    // Calculator 클래스에서 사용하는 방법
     Operator operator = Operator.findByOperator(userOperator);
    
     return operator.calculate(firstNumber, secondNumber);
    package level3.operations;
    
    public enum Operator {
      // ✅ADD, SUBTRACT, MULTIPLY, DIVIDE Enum 객체 생성
      ADD("+", ((firstNumber, secondNumber) -> firstNumber + secondNumber)),
      SUBTRACT("-", ((firstNumber, secondNumber) -> firstNumber - secondNumber)),
      MULTIPLY("*", (firstNumber, secondNumber) -> firstNumber * secondNumber),
      DIVIDE("/", (firstNumber, secondNumber) -> {
          if (secondNumber == 0) {
              throw new ArithmeticException("0으로 나눌 수 없습니다.");
          }
          return firstNumber / secondNumber;
      });
    
      private final String symbol; // ✅연산자 기호 (+, -, *, /)
      private final Operation operation; // ✅연산 기능 (함수형 인터페이스)
    
      // ✅생성자
      Operator(String userOperator, Operation operation) {
          this.symbol = userOperator;
          this.operation = operation;
      }
    
      // ✅Getter || 연산자 기호(Operator) 반환 메서드
      public String getSymbol() {
          return symbol;
      }
    
      // ✅연산 메서드
      public int calculate(int firstNumber, int secondNumber) {
          return operation.calculate(firstNumber, secondNumber);
      }
    
      // ✅사용자로부터 Operator 객체를 찾아 반환하는 메서드
      public static Operator findByOperator(String userOperator) {
          return Arrays.stream(values()) // 1. enum의 모든 항목(ADD, SUBTRACT, MULTIPLY, DIVIDE)을 Stream 으로 변환
                  .filter(op -> op.symbol.equals(userOperator)) // 2. op.symbol == userOperator 인 경우를 찾음
                  .findFirst() // 3. 찾은 첫 번째 요소 반환
                  .orElseThrow(() -> new IllegalArgumentException("올바르지 않은 연산자입니다: " + userOperator)); // 4. 찾지 못한 경우 예외처리 (inputReader 에서 예외처리하므로 발생확률 ↓)
      }
    }

🚀🚀🚀 Level3

- 프로그램 실행 예시

### [LEVEL3] 프로그램 실행예시
---------------------------------------

[ 정수 계산기 프로그램 ]
1. 계산하기
2. 출력하기(All)
3. 삭제하기
4. 종료하기
선택: 

- 피드백 받았던 부분

과제제출 기한이 별로 남지 않아서 제대로 받지 못했다...

- 문제점

  • getNumberInput() 메서드는 제네릭을 사용하면서 프로그램 시작 할 때 1~4의 int형 정수를 받기도 하면서, 계산기능을 수행할 때는 int, double 타입 관계없이 숫자면 모두 받을 수 있게 하고 싶었다.
    하지만, 코드를 수정하다보니 모든 반환타입을 건드려야 됐고 이건 아닌데..라는 생각이 들었다.
    그래서, 받을 때는 숫자면 모두 받을 수 있게 하였고, 반환은 int형 정수로 하는 것으로 최종 제출하였다..!
  • 제네릭&람다식&스트림을 사용하다보니, 코드가 가독성이 더 떨어진 것 같았다. 간결하고 가독성이 높은 코드는 아닌 것 같다는 생각이 든다.
    내가 100% 이해하고 썼다는 생각도 들지 않았다.
    // ✅ 사용자의 숫자 입력 값을 검증하는 제네릭 메서드
    @SuppressWarnings("unchecked")
    public <T extends Number> T getNumberInput() {
        return getValidInput(() -> {
            // hasNextDouble()은 입력값이 double, int 일 경우 true 반환
            if (!scanner.hasNextDouble()) {
                scanner.next(); // 잘못된 입력 제거
                return null; // getValidInput() 메서드에서 error message 출력
            }

            // 입력값이 int, double 일 경우 변수에 저장
            double number = scanner.nextDouble();

            // 개행 문자 제거
            scanner.nextLine();

            // ✅ 해당 메서드의 반환 값은 제네릭 T 타입을 유지하면서 Integer 타입만 반환하고 싶음
            // 1️⃣ Integer.valueOf((int) number)는 Integer 타입으로 형변환
            // 2️⃣ 하지만 컴파일은 T 타입으로 반환하라고 요구함 (T extends Number)
            // 3️⃣ 따라서 (T)로 캐스팅하여 제네릭 타입을 유지해야 함
            // 4️⃣ 결국, 반환값은 Integer 이지만, 제네릭 T로 변환하여 반환
            // 💡 (T)는 컴파일러에게 "이 값은 T 타입이야!"라고 알려주는 역할을 함
            return (T) Integer.valueOf((int) number);
        }, "올바른 숫자를 입력하세요!: ");
    }

    /**
     * ✅입력값을 검증하고 유효한 값이 입력될 때까지 반복하는 제네릭 메서드
     * (Supplier<T>는 매개변수를 받지 않고 새로운 값을 반환하는 기능에 적합한 함수형 인터페이스)
     *
     * @param inputProvider 입력을 처리하는 함수 (유효한 값이면 반환, 무효한 값이면 null 반환)
     * @param errorMessage  잘못된 입력이 들어왔을 때 출력할 메시지
     * @return 검증된 유효한 입력값 (T)
     */
    private <T> T getValidInput(Supplier<T> inputProvider, String errorMessage) {
        while (true) {
            // 함수의 결과값을 변수에 저장
            T input = inputProvider.get();

            // 변수가 유효하면 return
            if (input != null) {
                return input;
            }

            // 변수가 null 이면 다시 입력 요청
            System.out.print(errorMessage);
        }
    }

- 해결방안

  • 사용자가 실수(double) 또는 정수(int) 연산을 선택할 수 있도록 구현하기

✅ Try

다음 프로젝트에서 시도해볼 점들을 작성했다.

  • Enum, 제네릭, 람다, 스트림을 제대로 이해하고, 사용하기
  • TIL 작성 및 트러블슈팅 작성을 미리미리 하기
  • 할 수 있는 것과 없는 것을 명확히 구분하는 연습하기
  • 내 코드의 구조와 동작원리를 명확히 이해하고, 설명하기

출처

인프런 회고문화

profile
wannabe---ing

0개의 댓글