[TIL] Calculator 과제 - 피드백 반영

김홍기·2026년 3월 27일

1. 작성 목적

  • 자바 문법 학습을 본격적으로 시작하며 계산기 과제를 제출하였다.
  • 과제에 대한 피드백을 바탕으로 부족한 부분을 정리하고 개선하기 위해 기록한다.

2. 피드백 요약 (핵심 피드백은 🔥표시)

1) 기능적 요소

  • 변수 초기화 방식 개선 필요 (double num1 = 0.1 ---> double num1 = 0.0;)
  • 🔥Enum 클래스 내부로 계산 로직 이동 및 잘못된 구조 사용
  • 🔥ArrayList가 아닌 List 타입으로 선언
  • 🔥제네릭 설계 부족 (🔥요구사항 미충족)
  • 문자열 비교 시 "yes".equals(입력받은 문자열) 방식 권장
    (주어진 문자열이 존재하고 입력받은 문자열이 비어 있을 수 있는 경우)
  • 잘못된 return 0 처리 로직
  • 예외처리 try-catch문 활용 (현재, 모든 경우의 수 검증하여 선처리함)

2) 가독성 및 유지보수성 요소

  • 코드 스타일 및 정렬 필요 (정렬 단축키 : CTRL + ALT + L)
  • 변수명이나 메서드명 가독성 부족 (의미있는 이름으로 작성)

3) 기타

  • 통일성 있는 주석 처리 필요 (Java에서는 Javadoc 지원)

3. 핵심 피드백 분석

3-1. 🔥Enum 클래스 내부로 계산 로직 이동 및 잘못된 구조 사용

📌 피드백 원본

6. 입력받은 문자열에 따라서 switch case로 어떤 OperatorType인지 찾아서 type 변수에 담는 과정을 작성하신 부분도 좋습니다. 이부분은 Enum 클래스에서 메서드로 구현이 가능한 부분이기 때문에 재사용성과 책임을 줄이기 위해 Enum 안쪽에 구현하시는 편이 더 좋은 코드가 됩니다! 13. Enum 클래스의 선언 방식이 올바른 형태가 아닙니다. Enum 또한 클래스이기 때문에 지금은 클래스안에 클래스를 만든 형태입니다. 또한 Enum을 사용하는것에 있어서 Enum도 속성을 가질 수 있고 메서드를 가질 수 있다는점을 꼭 기억해주세요!

🔍 문제상황

0) Enum에 대한 잘못된 인식

  • C#에서 enum을 사용할 때 주로 정수값으로 처리되는 경험으로 상수 집합으로 인식
  • java에서 enum에 대한 학습이 부족하여 아래와 같은 문제 상황 발생

1) Enum 선언 방식이 올바르지 못함 (클래스 안에 클래스를 만든 형태)

  • 별도의 타입으로 분리하지 않고 EnumClass 내부에 선언하여 불필요한 중첩 구조 생성
  • 이로인해 EnumClass.OperatorType 형태로 접근해야하는 불편함
  • 독립성, 가독성, 재사용성 저하
            public class EnumClass {

                public enum OperatorType{
                    ADD,
                    SUBTRACT,
                    MULTIPLY,
                    DIVIDE
                }

            }

2) 연산 로직이 Enum이 아닌 외부 클래스에 존재

  • 연산 타입을 정의했음에도 불구하고 실제 연산 로직은 ArithmeticCalculator에서 switch-case로 처리

  • 연산 타입과 실제 동작이 분리됨

  • 새로운 연산 추가 시 Switch 수정 필요

     public double calculate(T a, T b, EnumClass.OperatorType type) {
    
            //제네릭 매개변수는 직접적으로 연산을 할 수가 없어서 해당 메서드 사용.
            double num1 = a.doubleValue();
            double num2 = b.doubleValue();
            double result;
    
            switch (type) {
                case ADD:
                    result = num1 + num2;
                    resultList.add(result);
                    return result;
    
                case SUBTRACT:
                    result = num1 - num2;
                    resultList.add(result);
                    return result;
    
                case MULTIPLY:
                    result = num1 * num2;
                    resultList.add(result);
                    return result;
    
                case DIVIDE:
                    if (num2 == 0) {
                        System.out.println("나눗셈 연산에서 분모(두번째 정수)에 0이 입력될 수 없습니다.");
                        return 0;
                    }
                    result = num1 / num2;
                    resultList.add(result);
                    return result;
    
                default:
                    return 0;
            }
        }
  • 연산 실행도 main 클래스에서 처리

    while (true) {
                  System.out.print("사칙연산 기호를 입력하세요(+, -, *, /) : ");
    
                  String lengthCheck = sc.next();
    
                  if (lengthCheck.length() != 1) {
                      System.out.println("연산자는 한 글자만 입력하세요.");
                      continue;
                  }
    
                  char str = lengthCheck.charAt(0);
    
                  OperatorType.OperatorType type;
    
                  switch (str) {
                      case '+':
                          type = OperatorType.OperatorType.ADD;
                          break;
                      case '-':
                          type = OperatorType.OperatorType.SUBTRACT;
                          break;
                      case '*':
                          type = OperatorType.OperatorType.MULTIPLY;
                          break;
                      case '/':
                          type = OperatorType.OperatorType.DIVIDE;
                          break;
                      default:
                          System.out.println("잘못된 연산자입니다.");
                          continue;
                  }
                  result = arithmeticCal.calculate(num1, num2, type);
                  break;
              }

💡 개선 방향

1) Enum을 독립된 타입으로 분리

  • Enum 또한 하나의 클래스이므로 불필요한 중첩 구조 제거하고 독립된 enum 파일로 분리

2) 연산자 해석 및 로직을 Enum 내부로 이동

  • 기존 ArithmeticCalculator 에서 연산하던 것을 개선하여 Enum 내부 메서드에 정의

    public enum OperatorType {
       ADD('+') {
           @Override
           public double calculate(double num1, double num2) {
               return num1 + num2;
           }
       },
       SUBTRACT('-'){
           @Override
           public double calculate(double num1, double num2){
               return num1 - num2;
           }
       },
       MULTIPLY('*'){
           @Override
           public double calculate(double num1, double num2){
               return num1 * num2;
           }
       },
       DIVIDE('/'){
           @Override
           public double calculate(double num1, double num2){
               if (num2 == 0)
               {
                   throw new ArithmeticException("나눗셈 연산에서 분모(두번째 정수)에 0이 입력될 수 없습니다.");
               }
               return  num1 / num2;
           }
       };
    
       private final char type;
    
       OperatorType(char type){
           this.type = type;
       }
    
       public abstract double calculate(double num1, double num2);
    
       public static OperatorType searchtype(char type){
           switch (type){
               case '+':
                   return OperatorType.ADD;
               case '-':
                   return OperatorType.SUBTRACT;
               case '*':
                   return OperatorType.MULTIPLY;
               case '/':
                   return OperatorType.DIVIDE;
               default:
                   throw new IllegalArgumentException("잘못된 연산자입니다.");
    
           }
       }
     }

3) 메인 메서드 case문 삭제 및 Enum 메서드 사용

while (true) {
                System.out.print("사칙연산 기호를 입력하세요(+, -, *, /) : ");

                String lengthCheck = sc.next();

                if (lengthCheck.length() != 1) {
                    System.out.println("연산자는 한 글자만 입력하세요.");
                    continue;
                }

                char str = lengthCheck.charAt(0);

                //enum에서 throw로 "에러 메세지"를 던지고 catch로 받아서 출력
                try {
                    OperatorType type = OperatorType.searchtype(str);
                    result = type.calculate(num1, num2);
                    break;
                } catch (IllegalArgumentException e) {
                    System.out.println(e.getMessage());
                }
            }

📖 개념 학습 (아래 링크 이동)

https://velog.io/@gpekd5/Java-%EB%AC%B8%EB%B2%95-enum-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

1) Enum 이란?

  • 서로 관련된 상수들의 집합을 정의하는 특별한 클래스
  • 단순한 값의 나열이 아니라, 필드와 메서드를 가질 수 있는 하나의 객체 타입

2) Enum의 특징

2-1) 객체로 동작

  • Enum의 각 값은 모두 객체이며, 서로 다른 인스턴스로 존재

2-2) 클래스처럼 필드, 생성자, 메서드를 가질 수 있다.

  • 필드
    private final char symbol;
  • 생성자
    OperatorType(char symbol) {
        this.symbol = symbol;
    }
  • 메서드
    public char getSymbol() {
        return symbol;
    }

2-3) 생성자는 외부에서 호출 할 수 없다.

  • Enum의 객체는 직접 생성할 수 없고, 정의된 값만 사용할 수 있다.

    OperatorType op = OperatorType.ADD; // 가능
    // new OperatorType() → 불가능

3) Enum의 핵심 활용

  • Enum은 단순한 값이 아니라 행동(로직)을 포함할 수 있다.

    public enum OperatorType {
       ADD {
           public double apply(double a, double b) {
               return a + b;
           }
       },
       SUBTRACT {
           public double apply(double a, double b) {
               return a - b;
           }
       };
    
       public abstract double apply(double a, double b);
    }

3-2. 🔥ArrayList가 아닌 List 타입으로 선언

📌 피드백 원본

7. private ArrayList results = new ArrayList<>(); 로 선언된 자료구조의 선언 형태는 주로 다형성을 활용하여 선언합니다. e.g) private List results = new ArrayList<>(); 형태로 List 타입으로 선언 후 뒤쪽에는 어떤 구현체를 선언할 것인지 작성해주시면 됩니다.

🔍 문제상황

ArrayList<Integer> results = new ArrayList<>();
  • 처음부터 ArrayList라는 클래스를 바로 사용함

💡 개선 방향

List<Integer> results = new ArrayList<>();
  • 처음부터 ArrayList라는 클래스를 바로 사용함
  • 다형성을 위해 인터페이스로 선언하고 구현체로 생성한다.

📖 개념 학습 (아래 링크 이동)

List와 ArrayList의 차이

=> 왜 List numbers = new ArrayList<>(); 이런 방식을 사용하는가?
=> ArrayList numbers = new ArrayList<>(); 이런 방식은 왜 안쓰는가??
=> 자바에서는 보통 인터페이스로 선언하고, 구현체로 생성하는 방식을 사용한다.

1.List 란?

=> List는 인터페이스이다. "이러한 기능을 제공해야한다"는 규칙을 가짐

2. ArrayList 란?

=> List 인터페이스를 구현한 클래스
=> List가 규칙이라면 ArrayList는 규칙을 실제로 만든 구체적인 코드이다.

3. 비유를 통한 이해

  • List = 자동차
  • ArrayList = 소나타
  • LinkedList = 아반떼
    => List list = new ArrayList<>(); 는 "자동차를 사용할 건데, 지금은 소나타 사용" 의미
    => ArrayList list = new ArrayList<>(); 는 "나는 무조건 소나타만 사용"의 의미

4. 코드 차이

4.1. ArrayList 직접 선언

ArrayList<Integer> numbers = new ArrayList<>();
	numbers.add(10);
	numbers.add(20);

=> numbers 변수가 ArrayList 타입으로 고정
numbers = new LinkedList<>(); // 오류

=> 이런식으로 바꾸려고하면 오류 발생 (다른 클래스이기 때문)

4.2 List 선언

List<Integer> numbers = new ArrayList<>();
	numbers.add(10);
	numbers.add(20);

=> numbers 변수가 List<>타입이므로 다른 클래스도 담을 수 있다.
numbers = new LinkedList<>();

=> 이런식으로 구현 가능

=> 변수 이름은 그대로 두고 구현체만 바꿀 수 있다.

5. 다형성

=> 같은 부모 타입으로 여러 자식 객체를 다룰 수 있는 것

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new LinkedList<>();

=> 둘 다 타입은 List<Integer> 이나 실제 객체는 다름

6. 인터페이스 선언의 장점 (List로 선언하는 것)

6-1. 구현체를 바꾸기 쉽다. (4.2 내용)

6-2. 코드가 구현체에 덜 묶인다. (반대일 경우 4.1 내용)

6-3. 메서드 작성에 유리하다.

public void printNumbers(ArrayList<Integer> list) {
  System.out.println(list);
}

=> 해당 메서드는 ArrayList만 받을 수 있다.

public void printNumbers(List<Integer> list) {
  System.out.println(list);
}

=> 해당 메서드는 ArrayList도 받을 수 있고, LinkedList도 받을 수 있다.
=> 재사용성이 더 좋다.

3-3. 🔥🔥제네릭 설계 부족

📌 피드백 원본

10. ArithmeticCalculator의 calculate의 매개변수 a, b는 모두 T로 선언되어 딱 하나의 타입만 계산할 수 있습니다. 이는 어떤 타입의 조합으로 계산하여도 가능하도록 만들라는 요구사항에 충족되지 않습니다. 여러개의 제네릭 타입 변수를 선언할 수 있다는 점을 기억해주세요! e.g) 정수 + 정수 조합, 실수 + 실수 조합

🔍 문제상황

  • 단순히 int가 아닌 double로 바꾼 상황이라 정수+정수 형태, 실수 + 실수 형태 둘 다 받는 계산이 가능한 기능이 아님
  • 초기 구현에서 입력값을 모두 double로 처리하였다.

💡 개선 방향

  • 입력 단계부터 double로 고정하는 것이 아니라 Number 을 통해 타입을 유지하도록 변경

  • 입력받아 타입을 구분하는 방식으로 수정

      public static Number parseNumber(String input) {
        if (input.contains(".")) {
            return Double.parseDouble(input);
        }
        return Integer.parseInt(input);
       }
  • ArithmeticCalculator.java에서는 여러 제네릭 타입을 사용하여 다른 타입을 가지도록 구현하였다.

  • 또한, 타입에 따라 연산을 분기하여 입력값이 모두 정수인 경우에는 정수 연산을 수행하고 이외에는 실수 연산을 하였다.

  • 결과는 Number 타입으로 선언하여 정수와 실수 결과를 모두 담을 수 있도록 하였다.

        public <U extends Number, V extends  Number> Number calculator(U num1, V num2, OperatorType type ){
    
          Number result;
    
          if (num1 instanceof Integer && num2 instanceof  Integer){
              result = type.calculate(num1.intValue(), num2.intValue());
          }else {
              result = type.calculate(num1.doubleValue(), num2.doubleValue());
          }
    
          resultList.add(result);
          return result;
      }
  • OperatorType.java 파일에서는 메서드 오버로딩하여 같은 calculate 메서드를 int용 / double용으로 나눠서 처리하였다.

    public enum OperatorType {
        ADD('+') {
            @Override
            public int calculate(int num1, int num2) {
                return num1 + num2;
            }
            @Override
            public double calculate(double num1, double num2) {
                return num1 + num2;
            }
        },
        SUBTRACT('-'){
            @Override
            public int calculate(int num1, int num2){
                return num1 - num2;
            }
            @Override
            public double calculate(double num1, double num2){
                return num1 - num2;
            }
        },
        MULTIPLY('*'){
            @Override
            public int calculate(int num1, int num2){
                return num1 * num2;
            }
            @Override
            public double calculate(double num1, double num2){
                return num1 * num2;
            }
        },
        DIVIDE('/'){
            @Override
            public int calculate(int num1, int num2){
                if (num2 == 0)
                {
                    throw new ArithmeticException("나눗셈 연산에서 분모(두번째 정수)에 0이 입력될 수 없습니다.");
                }
                return  num1 / num2;
            }
            @Override
            public double calculate(double num1, double num2){
                if (num2 == 0)
                {
                    throw new ArithmeticException("나눗셈 연산에서 분모(두번째 정수)에 0이 입력될 수 없습니다.");
                }
                return  num1 / num2;
            }
        };
    
        private final char type;
    
        OperatorType(char type){
            this.type = type;
        }
    
        public abstract int calculate(int num1, int num2);
    
        public abstract double calculate(double num1, double num2);
    
           public static OperatorType searchType(char type){
            return switch(type){
                case '+' -> ADD;
                case '-' -> SUBTRACT;
                case '*' -> MULTIPLY;
                case '/' -> DIVIDE;
                default -> throw new IllegalArgumentException("잘못된 연산자입니다.");
            };
        }
    }

📖 개념 학습

1. 제네릭 (Generics)

=> 제네릭은 클래스나 메서드에서 사용할 `타입을 미리 정하지 않고, 사용 시점에 타입을 결정할 수 있도록 하는 기능
📌 이번 과제에서는 <U, V> 처럼 여러 제네릭 타입을 사용하여 서로 다른 타입의 조합 처리

  • 예 :
    • Integer + Integer
    • Double + Double
    • Integet + Double

2. Number 타입

=> Number는 Integer, Double, Float 등의 부모 클래스이며, 다양한 숫자 타입을 하나로 묶어 처리할 수 있다.
📌 Number 타입은 직접 연산 불가능 !! => intValue(), doubleValue() 등의 메서드를 통해 변환 후 연산

3. 메서드 오버로딩 Overloading

=> 같은 이름의 메서드를 매개변수 타입이나 개수를 다르게 하여 여러 개 정의하는 것
📌 적용 예시

	calculate(int, int)
	calculate(double, double)

4. instanceof

=> 객체가 특정 타입인지 확인하는 연산자
📌 적용 예시

	 if (num1 instanceof Integer && num2 instanceof  Integer)

4. 회고 및 느낀점

🌭 처음 도전 과제를 구현할 때 enum과 제네릭에 대한 이해가 충분하지 않은 상태에서 개발을 진행하였고,
그로 인해 요구사항과 맞지 않는 아주 아쉬운 코드가 작성되었다.

이후, 튜터님의 피드백을 바탕으로 개념을 다시 학습하고 코드를 전면적으로 수정하는 과정을 거쳤다.

이번 과제를 통해 단순히 기능을 구현하는 것이 아니라, 다형성과 재사용성을 고려한 설계가 중요하다고 느껴졌다.
앞으로도 코드를 작성할 때 유지보수성과 확장성을 함께 고민하는 습관을 가져야겠다고 생각했다.

이전 장비 개발 업무에서는 이미 만들어진 구조 위에 기능을 추가하는 경우가 많았지만,
앞으로는 직접 구조를 설계하는 경험을 통해 좋은 설계가 무엇인지에 대해서도 계속 고민해봐야겠다.

profile
개발자 기록용

0개의 댓글