저번에 만들었던 Level2
계산기 로직을 리팩토링해서 Level3
계산기를 구현했다.
요구사항에 맞게 추가한 기능은 이렇다
1️⃣ Enum으로 연산자 정보 관리
2️⃣ Generic을 적용해 피연산자를 여러 타입으로 받을 수 있도록 확장
3️⃣ Lambda & Stream을 적용해 입력한 값보다 큰 연산 결과만 출력
Enum
적용package level3;
public enum OperatorType {
SUM('+'), SUB('-'), MUL('*'), DIV('/');
private final char operator;
OperatorType(char operator) {
this.operator = operator;
}
public char getOperator() {
return operator;
}
/**
* 잘못된 연산자 예외 처리
*/
public static OperatorType verifyOperator(char operator) {
for (OperatorType op : values()) {
if (op.operator == operator) {
return op;
}
}
throw new IllegalArgumentException();
}
}
Enum
안에서 연산 로직을 구현하는 방법도 고려했으나, 그렇게 하면 OperatorType
의 역할이 단순한 연산자 정보 저장이 아니라 계산까지 담당하게 되어 책임이 불필요하게 커질 위험이 있었다.
따라서 연산 로직은 별도의 클래스에서 처리하고, Enum
은 연산자 정보를 관리하는 역할로만 활용했다.
addNum()
메서드 기능 확장package level3;
import java.util.Scanner;
public class Input {
private final Filter filter = new Filter();
private final Scanner sc = new Scanner(System.in);
/**
* 숫자 입력 메서드
*/
public <T extends Number> T addNum(String order) {
// 필터링 구간
System.out.print(order + " 숫자를 입력하세요: ");
return filter.checkedPositiveNum();
}
/**
* 연산자 입력 메서드
*/
public OperatorType addOperator() {
char operator;
while (true) {
System.out.print("사칙연산 기호를 입력하세요: ");
operator = sc.next().charAt(0);
try {
return OperatorType.verifyOperator(operator);
} catch (IllegalArgumentException e) {
System.out.println("잘못된 연산자입니다. 사칙연산 기호를 다시 입력하세요.");
}
}
}
}
package level3;
import java.util.Scanner;
public class Filter {
Scanner sc = new Scanner(System.in);
/**
* 필터링 메서드 - 입력값이 정수인지 실수인지 판별하고, 문자열 입력을 방지
*/
@SuppressWarnings("unchecked")
public <T extends Number> T checkedPositiveNum() {
while (true) {
if (sc.hasNextInt()) {
System.out.println("정수");
int num = sc.nextInt();
return (T) Integer.valueOf(num);
} else if (sc.hasNextDouble()) {
System.out.println("실수");
double num = sc.nextDouble();
return (T) Double.valueOf(num);
} else {
System.out.println("올바른 숫자를 입력하세요.");
sc.next();
}
}
}
/**
* 나눗셈 검증 메서드 - 분모에 0이나 음수 X
*/
public <T extends Number> T checkedDivide(T secondNum, OperatorType operator) {
while (operator == OperatorType.DIV && secondNum.doubleValue() <= 0) {
System.out.println("나눗셈 연산에서 분모(두 번째 숫자)에 0이나 음수가 올 수 없습니다. 다시 입력하세요.");
secondNum = checkedPositiveNum();
}
return secondNum;
}
}
1) 숫자 입력 메서드
Generic
적용2) 연산자 입력 메서드
Enum
에 선언된 상수 사용try-catch
구문을 사용해 연산자 일치 여부 확인 및 검증 수행3) 필터링 메서드
Generic
적용4) 나눗셈 검증 메서드
Generic
적용Lambda & Stream
을 적용한 필터링 메서드 case "2":
System.out.print("기준 값을 입력하세요: ");
double inputValue = sc.nextDouble();
System.out.println("저장된 연산 결과: " + calc.filteredResult(inputValue));
continue;
/**
* 연산 결과 필터링 메서드 - 입력 값보다 큰 연산 결과만 출력
*/
public List<Double> filteredResult(double inputValue) {
return getResults().stream()
.filter(result -> result > inputValue)
.collect(Collectors.toList());
}
메뉴에서 2번 입력 후 기준 값을 입력할 시 현재 저장된 연산 결과 중 기준값보다 큰 값만 출력
트러블슈팅이라고 하기엔 애매하지만 Generic
의 활용이 애매하게 느껴졌다. 기존의 코드에선
public <T extends Number> T checkedPositiveNum(Class<T> type)
파라미터에 클래스 타입 변수를 받도록 선언했었는데, 이렇게 하니 메서드 호출 시
checkedPositiveNum(Integer.class)
checkedPositiveNum(Double.class)
이런 식으로 호출했어야 했고, if-else
조건문 어느 구간에 들어가는지 확인 해보니 정수, 실수의 입력에 상관없이 명시된 클래스 타입의 조건문 구간으로만 들어가는 것이였다.
Class<T>
를 파라미터로 받도록 하면, 호출할 때 명시적으로 타입을 지정해야 한다.
하지만 이렇게 하면 입력값의 실제 타입과 무관하게 호출된 타입으로만 처리되므로, Generic
의 장점인 '입력값에 따라 유연하게 타입을 결정하는 기능'이 사라진다.
따라서 클래스 타입을 받지 않고 입력값을 동적으로 판별하는 본문 3에 나온 방식으로 개선했다.
아직도 어떤 방법이 정답인지는 모르겠다. Generic
의 기준이 어디까지인지 감이 잘 안 온다.
튜터님께 여쭤보니 내가 구현한 방식이 맞다고 하셔서 고민은 해결됐었다.
연산 과정까지 Generic
을 유지하는 방법도 고려했지만, 결국 최종 결과를 출력할 때는 모든 숫자를 double
로 변환해야 한다. 그렇다면 연산 결과를 Generic
으로 유지하는 의미가 크지 않다고 판단해, 연산 후 double
값을 반환하는 방식으로 정했다.
내가 구현한 Generic
이 올바르게 작성된 것인지 확신하지 못하는 개발자가 아니라
나도 튜터님처럼 확실히 알고 명확한 답을 줄 수 있는 개발자가 되어야겠다.