Java/계산기 구현

3단계 계산기 - enum 및 Generic을 활용한 데이터 분류

JuNo_12 2025. 4. 21. 20:22

완성된 계산기 코드를 보면서 얘기를 해보자. 아래는 전체 코드이다.

package calculator;

import java.util.Objects;

public class OperationExecutor<T extends Number> {
    private final ResultHistory resultHistory; // ResultHistory 객체를 참조하는 resultHistory 필드

    public OperationExecutor(ResultHistory resultHistory) { // ResultHistory 객체를 인자로 받아 resultHistory 필드에 저장
        this.resultHistory = resultHistory;
    }

    public enum Operator {
        ADD('+') {
            public double apply(Number n1, Number n2) {
                return n1.doubleValue() + n2.doubleValue();
            }
        },
        SUBTRACT('-') {
            public double apply(Number n1, Number n2) {
                return n1.doubleValue() - n2.doubleValue();
            }
        },
        MULTIPLY('*') {
            public double apply(Number n1, Number n2) {
                return n1.doubleValue() * n2.doubleValue();
            }
        },
        DIVIDE('/') {
            public double apply(Number n1, Number n2) {
                if (n2.doubleValue() == 0.0) {
                    throw new ArithmeticException("0으로 나눌 수 없습니다.");
                }
                return n1.doubleValue() / n2.doubleValue();
            }
        };

        private final char symbol; // 각 연산자에 대응되는 문자 기호를 저장

        Operator(char symbol) { // 각 enum 값 생성 시 기호를 설정하는 생성자
            this.symbol = symbol;
        }

        public abstract double apply(Number n1, Number n2); // 각 연산자마다 구현해야 하는 추상 메서드. 두 정수를 받아 계산한 결과를 반환

        public static Operator fromSymbol(char symbol) { // 연산자 기호를 받아서 해당하는 Operator enum 값을 반환. 일치하는 것이 없으면 예외 처리
            for (Operator op : Operator.values()) {
                if (op.symbol == symbol) {
                    return op;
                }
            }
            return null;
        }
    }
    
    public double execute(T num1, T num2, char operator) { // 두 실수와 연산자 기호를 입력받아 계산을 수행하고 결과를 반환하는 메서드
        double result = Objects.requireNonNull(Operator.fromSymbol(operator)).apply(num1, num2); // fromSymbol을 통해 operator(ADD,DIVIDE...)를 구하고, apply()를 호출해 Operator를 실행한 계산 결과를 얻는다.
        resultHistory.getResults().add(result); // 계산 결과를 ResultHistory에 저장
        return result;
    }
}


우선 enum은 열거형 타입으로 아래 코드에서는 사칙연산 기호들을 '고정된 상수 집합'으로 정의해주었다.

ADD, SUBTRACT 등은 각각 하나의 연산자 '유형'이다.

 

여기서 enum을 활용한 부분만 떼어서 보자.

public class OperationExecutor<T extends Number> {
public enum Operator {
//1.  
	ADD('+') {
            public double apply(Number n1, Number n2) {
                return n1.doubleValue() + n2.doubleValue();
            }
        },
    SUBTRACT('-') { ... },
    MULTIPLY('*') { ... },
    DIVIDE('/') { ... };

//2.
	private final char symbol;

//3.
	Operator(char symbol) { ... }

//4.
	public abstract double apply(Number n1, Number n2);

//5.
	public static Operator fromSymbol(char symbol) { ... }
}
}

 

 

1.

	ADD('+') {
            public double apply(Number n1, Number n2) {
                return n1.doubleValue() + n2.doubleValue();
            }
        },

 

여기서 Number라는 클래스를 상속받아주었다. Number 타입의 변수 n1, n2는 기본 연산자를 '직접' 사용할 수 없다.

추상 메서드는 "무엇을 해야 하는지"만 정의하고, 실제 "어떻게 해야 하는지"는 서브 클래스나 enum 상수 등에서 구현하게 되기 때문이다. Number는 추상 클래스이기때문에 double 기본형으로 변환한 뒤 연산을 해주게끔 변환시켜준 것이다.

 

ADD 부분을 순서대로 정리를 해보면,

ADD라는 이름의 연산자를 만들고 '+' 라는 기호를 연결해주었다.

그리고 연산을 수행하는 apply() 메서드를 생성해준 것이다.

enum에 추상메서드를 정의하고 연산자 기호마다 각각 오버라이딩해준 방식이다.

 

 

2.

private final char symbol;

 

각 연산자(enum 타입 상수)와 연산기호를 연결하는 필드이다.

final로 설정해주어서 참조 변수가 생성자에서 단 한 번만 초기화되고 그 이후에는 다른 객체를 참조할 수 없도록 해주었다.

이는 실수로 값을 덮어쓰는 걸 방지해주고 참조가 바뀌지 않는다는 것을 명확하게 들어내주는 용도이다.

물론 .add()와 같은 메서드를 호출해서 값은 바꿀 수 있지만 위의 이유로 사용해주었다.

 

3.

        Operator(char symbol) { // 각 enum 값 생성 시 기호를 설정하는 생성자
            this.symbol = symbol;
        }

 

상수 선언할 때 전달된 '+', '-' 등을 받아서 필드에 저장해주었다.

 

4.

public abstract double apply(Number n1, Number n2);

 

모든 연산자가 구현해야하는 추상 메서드이다.

각각의 연산자가 이 메서드를 오버라이딩해서 동작을 정의한다.

아까 1번에서 구현 apply 메서드이다.

자, 근데 여기서 궁금한 점이 있을 수도 있다. 

"아까 1번에서 구현해줬는데, 왜 굳이 추상 메서드를 선언해줘야하지?"

이에 대한 답은 이 추상 메서드 선언은 각 enum 상수에 대해 '반드시' apply() 메서드를 오버라이딩하게 '강제'한다.

이 추상 메서드 선언이 없다면, 자바는 각 enum 상수에서 apply() 메서드를 '꼭' 안써도 되는거로 판단한다.

그래서 실수로 apply()를 안적은 상수가 있어도 에러가 없이 넘어가버려서 나중에 오류가 발생할 수 있다.

사실 코드 안정성과 유지보수의 장점이 크기때문에 선언해주었다고 생각하면된다.

 

5.

        public static Operator fromSymbol(char symbol) { // 연산자 기호를 받아서 해당하는 Operator enum 값을 반환. 일치하는 것이 없으면 예외 처리
            for (Operator op : Operator.values()) {
                if (op.symbol == symbol) {
                    return op;
                }
            }
            return null;
        }

 

사용자가 입력한 기호를 받아서 해당 기호와 매칭되는 연산자를 찾아서 반환해주는 메서드이다.

못 찾으면  null을 반환한다. 이는 메인 클래스에서 활용하기 위해 null을 반환해주도록 설계했다.

 

 

이번에는 지금까지 하나하나 본 부분을 다시 순서대로 머릿속으로 구현해보자.

1. 먼저 Operator 라는 enum을 정의해준다.

2. 각 연산 기호를 상수로 선언하면서, 그 안에서 연산 기능도 함께 정의해준다.

3. 각 상수에 해당하는 문자 기호도 함께 저장해준다.

4. 문자 기호를 기반으로 어떤 연산자인지 찾아주는 메서드를 만들어주자.

5. 마지막으로, 연산을 강제 구현하게 만드는 추상 메서드를 선언해주자.

 

정리하자면, "이 enum은 각 연산자를 상수로 정의하면서, 그 안에 해당 연산을 수행하는 메서드를 직접 구현해서 계산기처럼 사용할 수 있도록 만든 구조다!"

 

자, 여기서 정리를 하다보니 한 가지 궁금한 점이 생길 수도 있다.

그렇다면 분명, 2번에서 각 연산 기호에 맞는 ADD 라던가 SUBTRACT 같은 것을 만들어주었는데,

굳이 3번에서 다시 선언해주어야하나?

 

해야한다. 왜냐하면, ADD('+')는 내부적으로 기호를 저장한 것 뿐이다.

우리가 enum을 사용할 때는 그냥 Operator.ADD 이런 식으로 '직접' 접근할 수는 있지만, 사용자가 콘솔에서 

'+'를 입력했을 때는 그걸 enum으로 바꿔주는 과정이 필요하다.

그 다리 역할을 해주는 것이 symbol 필드와 fromSymbol() 필드인 것이다.

 

이제 이를 메인 클래스에서 활용한 부분을 보면서 enum은 마무리해보겠다.

// enum 활용한 부분
OperationExecutor.Operator op = OperationExecutor.Operator.fromSymbol(operator);
if (op == null) {
    System.out.println("지원하지 않는 연산자입니다. 다시 입력해주세요.");
    continue;
}

// 계산 수행
double result = executor.execute(inputNumber1, inputNumber2, operator);

 

위에 적힌 코드가 너무 길어서 이해하기 힘드니 한 부분씩 떼어서 보자.

OperationExecutor.Operator op =  // op는 Operator enum 타입의 변수
OperationExecutor.Operator // Operator가 OperationExecutor 클래스 내의 정의된 enum 타입이라는 것
.fromSymbol(operator); // .fromSymbol은 Operator 내부에 정의된 정적 메서드를 호출
					   // operator에는 사용자가 입력한 연산자 기호가 담겨있다.
                       // 이 메서드는 입력 받은 operator 문자를 가지고 해당 연산자에 맞는 enum 값을
                       // enum 값을 찾아 반환하는 역할 (ex. '=' -> ADD 반환)

 

사실, 한 부분씩 떼어서 보면 별거 없죠? 

하지만, 다른 클래스에서 만든 메서드(기능)을 메인 클래스로 가져와 활용을 하기위해서는

메서드 호출 방법을 잘 알아놔야하기에 적어놓았다. enum은 끝!


이번에는 제네릭을 사용한 부분을 보자.

1. 

public class OperationExecutor<T extends Number>

 

T는 Number의 자식 클래스여야한다는 제한 조걸을 걸어두고 (Interger, Double 등 숫자 타입) 선언해주었다.

 

2.

public double execute(T num1, T num2, char operator)

 

메서드에서 T를 써서 실제로 계산에 사용하는 부분이다.

T 타입의 두 숫자를 받아서 enum에서 처리할 때, .doubleValue()로 바꿔서 연산한다.

이렇게하면 int, double 등 다 받아도 잘 동작한다.

 

3.

OperationExecutor<Double> executor = new OperationExecutor<>(calc);

 

여기서 <Double>로 타입을 명시하면, 그 뒤로 이 클래스는 Double 타입만 처리하게 된다. 

즉, execute(1.2, 3.4, '+') 이런식으로 사용 가능하다.


연산을 수행하고 그 결과를 반환하는 역할을 하는 메서드 구현

public double execute(T num1, T num2, char operator) { 
    // 두 실수와 연산자 기호를 입력받아 계산을 수행하고 결과를 반환하는 메서드
    double result = Objects.requireNonNull(Operator.fromSymbol(operator)).apply(num1, num2); 
    // fromSymbol을 통해 operator(ADD, DIVIDE...)를 구하고, apply()를 호출해 Operator를 실행한 계산 결과를 얻는다.
    resultHistory.getResults().add(result); 
    // 계산 결과를 ResultHistory에 저장
    return result; 
    // 계산 결과 반환
}

 

1. 연산자에 해당하는 Operator를 찾고 계산하는 부분

double result = Objects.requireNonNull(Operator.fromSymbol(operator)).apply(num1, num2);

 

a. Operator.fromSymbol(operator)

  • fromSymbol(operator)는 연산자 기호를 기반으로 해당하는 Operator enum 값을 찾아주는 메서드
  • 예를 들어, operator가 +라면 Operator.ADD를 반환
  • 이 메서드는 Operator enum 클래스 안에서 각 연산자 기호에 맞는 값을 찾고, 찾지 못하면 null을 반환하는데, 이 null을 처리해야한다. 

b. Objects.requireNonNull()

  • Objects.requireNonNull()은 null을 처리하는 방법
  • 만약 Operator.fromSymbol(operator)가 null을 반환하면 NullPointerException이 발생해. 즉, 잘못된 연산자 기호가 들어왔을 때 예외를 처리하기 위해 사용해. 예외 처리는 메인클래스에서!
  • null이 아닌 Operator가 반환되면, 그 값을 계속 사용할 수 있도록 보장

c. .apply(num1, num2)

  • Operator enum에 정의된 각 연산자(ADD, SUBTRACT 등)는 apply 메서드를 가지고 있다.
  • 이 메서드는 두 숫자(num1, num2)를 받아서 계산 결과를 반환. 예를 들어 ADD는 두 숫자를 더하고, DIVIDE는 나누는 연산을 수행
  • apply 메서드는 Operator enum 값에 맞는 계산을 실제로 실행

 

2. 결과를 기록하는 부분

resultHistory.getResults().add(result);

 

  • 계산한 결과를 resultHistory 객체의 결과 리스트에 추가
  • resultHistory.getResults()는 결과 리스트를 가져오고, add(result)는 계산된 값을 그 리스트에 추가하는 부분
  • 즉, 이 코드에서는 계산된 결과가 연산 결과 히스토리에 기록되어 추후 조회가 가능하게 된다.

3. 최종적으로 결과를 반환하는 부분

return result;

 

 

  • 계산된 결과를 메서드의 반환값으로 반환
  • double result로 계산된 값을 execute 메서드가 반환함으로써, 호출한 곳에서 계산 결과를 사용할 수 있게 된다.