
내일배움캠프 9일차 TIL : Java - 제네릭 실습
사실 9일차 TIL은 쓰레드가 될 줄 알았는데 제네릭이 생각보다 많이 어렵다.
그래서 개인 프로젝트를 진행하면서 사용했던 제네릭 기술들을 정리하는 과정으로 9일차 TIL을 작성하려고 한다. (일종의 코드리뷰가 되는건가...)
https://github.com/WonGi-Kim/sparta_calculator_level3
요구사항은 기존 정수로 받던 입력을 실수도 가능하게 하도록 변경하는것.
그 과정에서 제네릭을 사용하여 구현하는 것이었다.
main을 가지고 있는 클래스다.
여기서는 계산기 타입과 입력을 받는 기능이 구현되어 있다.
main내부에서 ArithmeticCalculator 객체를 생성해서 입력받는 연산자와 숫자를 넘겨준다.
ArithmeticCalculator<Number> arithmetic = new ArithmeticCalculator();
try {
arithmetic.calculateOperation(operand,firstNumber,secondNumber);
} catch (DivisionByZeroException e) {
throw new RuntimeException(e);
} catch (InvalidOperatorException e) {
throw new RuntimeException(e);
}
여기서 보면
ArithmeticCalculator<Number> arithmetic = new ArithmeticCalculator();
기존의 ArithmeticCalculator은 제네릭이 아닌 일반 객체였지만 수정된 객체는 제네릭으로 되어있다.
이번 요구사항에서도 ArithmeticCalculator 을 수정하는 부분이니 해당 클래스를 봐야한다.
그 전에 ArithmeticCalculator은 Calculator의 자식 클래스이기 때문에 Calculator를 먼저 보도록 하겠다.
기본적으로 Calculator는 추상클래스로 선언되어있으며 제네릭 타입 역시 선언하였다.
public abstract class Calculator<T extends Number>
또한 추상 메서드 역시 정의된 T를 통해 생성된다.
public abstract void calculateOperation(String operator, T firstNumber, T secondNumber) throws
DivisionByZeroException, InvalidOperatorException;
Calculator가 추상 클래스와 메서드이므로 실질적인 구현은 ArithmeticCalculator 에서 진행된다.
먼저 클래스의 선언부분을 보면
public class ArithmeticCalculator<T extends Number> extends Calculator<T>
이미 부모 클래스인 Calculator에서 제네릭을 명시해줬는데 ArithmeticCalculator 클래스의 선언부분에 다시 선언을 해줘야 한다.
처음에는 상속을 받았으니 자식 클래스인 ArithmeticCalculator 은 명시할 필요가 없다고 생각했는데
제네릭 타입은 클래스와 메서드마다 독립적으로 정의된다고 한다.
따라서 상속받은 자식클래스 ArithmeticCalculator 클래스 선언부분에 새로 명시하도록 하자.
다음으로는 ArithmeticCalculator클래스의 필드를 봐야한다.
필드 부분을 보면
private final AddOperator<T> addOperator;
private final SubstractOperator<T> substractOperator;
private final MultiplyOperator<T> multiplyOperator;
private final DivideOperator<T> divideOperator;
private final ModOperator modOperator;
// 연산자 타입 enum 추가
private OperatorType operatorType;
// 생성자
public ArithmeticCalculator() {
this.addOperator = new AddOperator<>();
this.substractOperator = new SubstractOperator<>();
this.multiplyOperator = new MultiplyOperator<>();
this.divideOperator = new DivideOperator<>();
this.modOperator = new ModOperator();
}
일단 각 Operator를 보면 ArithmeticCalculator에 정의된 타입 T를 가진다.
각 Operator를 생성할 때 타입 T를 지정했기 때문에 생성자 내부에서 초기화 할 때에는 타입 선언을 필요로 하지 않는다.
사실 위의 개념도 어려웠는데 구현할 때는 여기가 좀 더 어려웠달까?
일단 코드를 보자면
public interface OperatorInterface<T extends Number> {
Double operate(T firstNumber, T secondNumber);
}
public class AddOperator<T extends Number> implements OperatorInterface<T>{
@Override
public Double operate(T firstNumber, T secondNumber) {
return firstNumber.doubleValue() + secondNumber.doubleValue();
}
}
아까 위에서 봤듯이 매 클래스, 메서드 마다 타입 변수를 지정해줘야 한다.
다만 어려웠던 부분은 이 부분이었다.
Double operate(T firstNumber, T secondNumber);
평소 메서드를 작성할 때 기본적인 구조는
리턴타입 메서드 이름 (매개변수 1, 매개변수 2) {
return ~~
}
형식이기 때문에 당연히 타입은 T니까 리턴 타입도 T겠지 하면서 처음 인터페이스를 구현할 때는
public interface OperatorInterface<T extends Number> {
T operate(T firstNumber, T secondNumber);
}
public class AddOperator<T extends Number> implements OperatorInterface<T>{
@Override
public T operate(T firstNumber, T secondNumber) {
return firstNumber + secondNumber;
}
}
같은 형식으로 구현했는데 Operator 구현 부분에서 오류가 발생했다.
왜 오류가 날까 하면서 구글링을 해보니까 Number클래스를 상속받았기 때문이다.
(일단 여기서 T는 그냥 T로 쓰이는게 아니라 Number의 Integer, Double 같은 값이 들어간다는거...)
제네릭을 사용하면서 타입 변수로 Wrapper클래스를 지정해줘야 하는데 여기서는 Number클래스를 정의했다.
Number클래스는 모든 숫자 타입의 슈퍼 클래스로 Integer, Double, Float, Long 등의 클래스를 포함한다.
Number클래스는 구체적인 숫자 타입을 알지 못하고, 숫자를 다른 타입으로 변환하는 메서드를 가진 추상 클래스이므로 직접적인 연산을 할 수 있게 정의하지 못한다.
따라서 단순히 T로 사용한다면 firstNumber나 secondNumber가 어떤 타입인지 정확히 정의할 수 없기 때문에 연산을 하지 못한다.
이를 해결하기 위해 리턴값을 명시적인 Wrapper클래스로 정의 해준다면 Number클래스의 형변환 메서드를 이용하여 연산을 할 수 있게된다.
public interface OperatorInterface<T extends Number> {
Double operate(T firstNumber, T secondNumber);
}
public class AddOperator<T extends Number> implements OperatorInterface<T>{
@Override
public Double operate(T firstNumber, T secondNumber) {
return firstNumber.doubleValue() + secondNumber.doubleValue();
}
}
위의 코드처럼 Double로 리턴 타입을 지정하고 Number클래스의 .doubleValue() 메서드를 통해 값을 실수로 변환하면 어떤 숫자는 실수형이지만 연산이 가능하게 된다.
사실 TIL이라 하기도 그렇지만 어려운 개념이라 내 코드에 어떻게 쓰였는지 흐름을 정리하기 위한 글이다...