부트캠프에서 내준 과제 수행 중 제네릭 관련해서 문제를 마주했었다. 계산기에서 피연산자 파라미터 타입을 제네릭으로 선언해서 어떤 자료형의 숫자 데이터든 받을 수 있도록 하는 것이었다.
public class Calculator<T extends Number> {
public T add(T a, T b) {
return a + b;
}
}
위 코드를 IDE에 작성하면 Operator '+' cannot be applied to 'T', 'T'라는 컴파일러 에러를 내뱉는다. 뭐가 문제인걸까? T라는 타입은 Number를 상속받는다고 명시했고, Integer, Double 같은 자료형은 + 연산이 될텐데?
결론부터 말하자면, Java 컴파일러가 코드를 바이트코드로 변환하는 과정에서 문제가 발생하기 때문에 위 코드는 실행되지 않는다.
오토박싱(boxing)과 오토언박싱(unboxing)을 제외하고 생각하더라도, 컴파일러가 + 연산자를 어떻게 처리해야 할지를 알 수 없다는 것이 문제다. 컴파일러는 + 연산자가 오버로딩된 여러 버전 중에서 어떤 것을 사용해야 할지 결정할 수 없다.
JVM은 서로 다른 기본형(primitive type)에 대해 각기 다른 산술 연산자(즉, 바이트코드 명령어)가 존재한다. 예를 들어, 정수형 덧셈 연산과 부동소수점 덧셈 연산은 완전히 다른 바이트코드 명령어다. 정수 연산과 부동소수점 연산은 근본적으로 다르고, 데이터 크기 또한 다르기 때문이다.
또한 BigInteger와 BigDecimal 같은 클래스도 Number를 확장하지만, 오토박싱을 지원하지 않으므로 이를 직접 처리할 수 있는 JVM 명령어가 없다. 이와 비슷한 Number 구현체들이 다른 라이브러리에도 많이 존재한다.
즉, 컴파일러가 T가 Number라는 점을 추론할 수 있다고 하더라도, 어떤 연산을 수행해야 할지(즉, 박싱, 언박싱, 산술 연산 등) 결정할 수 없기 때문에 문제가 발생하는 것이다.
그럼 이 코드는 어떨까?
class MathOperationV1<T extends Integer> {
public T add(T a, T b) {
return a + b;
}
}
이제 + 연산자는 정수 덧셈 연산자로 변환될 수 있지만, 덧셈 연산의 결과는 Integer 타입이 된다. 그러나 컴파일러 입장에서는 T가 Integer 이외의 다른 타입일 가능성도 있기 때문에 여전히 이 코드는 유효하지 않다.
만약 T a, T b를 메소드 파라미터로 받고, a에는 int, double 등 다양한 형태의 숫자 데이터를 받고 싶다면.
반환 타입을 Double, BigDecimal과 같이 다른 숫자 타입을 포함할 수 있는 데이터 타입을 사용하는 것이 좋다.
public <T extends Number> T add(T a, T b) {
double da = a.doubleValue();
double db = b.doubleValue();
double result = da + db;
T genericResult = (T) Double.valueOf(result);
return genericResult;
}
T는 Number를 extends한 타입이기 때문에 하위의 doubleValue() 메소드를 사용할 수 있다. (T) Double.valueOf(result)에서는 강제로 형변환을 해주고 있기 때문에 IDE에서 경고 메세지를 표시한다.
public <T> BigDecimal calculate(T a, T b, OperatorType operatorType) throws ArithmeticException {
// 어떤 데이터 타입이 들어오더라도 BigDecimal 타입으로 변환
BigDecimal bdA = new BigDecimal(String.valueOf(a));
BigDecimal bdB = new BigDecimal(String.valueOf(b));
// 연산 수행 후 결과 저장 및 반환
BigDecimal result = operatorType.calculate(bdA, bdB).stripTrailingZeros();
resultQueue.offer(result);
return result;
}
이건 과제로 제출한 코드인데, BigDecimal은 사실상 자바에서 가장 많은 숫자 데이터를 표현할 수 있다. 그리고 소수를 저장할 때 고정 소수점 방식을 사용하기 때문에 부동 소수점 방식을 사용하는 double, float 보다 소수를 더 정밀하게 표현할 수 있다.
위와 같은 이유로 BigDecimal을 반환 타입으로 사용 중이다. 만약 a, b에 String 타입이 들어오더라도 똑같이 연산을 수행할 수 있다.