일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- 자바로 키오스크 만들기
- 자바 고급2편 - 네트워크 프로그램
- 스프링 입문(무료)
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch12
- 자바 기초
- 자바의 정석 기초편 ch13
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch11
- 스프링 mvc1 - 스프링 mvc
- 자바 고급2편 - io
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch6
- 스프링 트랜잭션
- 자바의 정석 기초편 ch1
- @Aspect
- 자바로 계산기 만들기
- 자바 중급1편 - 날짜와 시간
- 람다
- 자바 중급2편 - 컬렉션 프레임워크
- 자바의 정석 기초편 ch9
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch14
- 스프링 mvc2 - 검증
- 2024 정보처리기사 시나공 필기
- 데이터 접근 기술
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 로그인 처리
- Today
- Total
개발공부기록
자바로 계산기 만들기 - LV3(도전 기능 - Generic과 Enum 함께 사용하기) 개발 이력 및 회고 본문
자바로 계산기 만들기 - LV3(도전 기능 - Generic과 Enum 함께 사용하기) 개발 이력 및 회고
소소한나구리 2025. 2. 27. 23:40계산기 과제 LV1, LV2 개발 회고
- https://nagul2.tistory.com/463
- 위 글의 내용과 이어서 개발합니다.
Lv3. Enum, 제네릭, 람다 & 스트림을 이해한 계산기 만들기
** 참고
- Lv2에서 동일하게 적용되어 있는 기능을 설명을 제외함
v3 요구사항 정의
- 1. Lv2를 작성한 코드와 구분하기 위해 v3패키지를 생성하여 작성하고, 기존 v2코드를 복사하여 리펙토링을 진행
- 2. 연산 타입을 Enum으로 관리하여 ArithmeticCalculator 클래스에 활용해보기(계산기 클래스명 변경)
- 3. 실수(double타입)의 값을 전달 받아도 연산이 수행하도록 수정
- int -> double 타입으로만 변환하는 것이 아니라 제네릭을 활용하여 여러 타입을 받을 수 있도록 기능을 확장
- 4. 저장된 연산 결과들 중 Scanner로 입력받은 값보다 큰 결과값을 출력하는 기능을 구현, 단 이 기능은 Lambda & Stream을 꼭 사용해야 함(람다, 스트림 연습)
기능 개발 설명
1. 고민 포인트 - 다형성 -> 제네릭 개발 방향 변경
public class ArithmeticCalculator {
private List<String> resultHistoryList = new ArrayList<>();
... 기타 코드 생략
}
public class App {
public static void main(String[] args) {
... 기타 코드 생략
Number firstValue = validFirstValue(scanner, INPUT_VALID_REGEXP);
Number secondValue = validSecondValue(operator, scanner, INPUT_VALID_REGEXP);
Number result = calculator.getCalculateResult(operator, firstValue, secondValue);
}
- Enum 클래스들의 개발을 완료하고 다양한 타입을 입력받기 위해 처음에는 접근 포인트를 제네릭이 아닌 다형성을 통해서 접근을 시도하고 개발을 시도하였음
- 제네릭을 활용하든, 다형성을 활용하든 여러 타입의 값을 받도록 구현하면 된다고 생각하여 제네릭의 사용은 배제하고 입력받는 값을 모든 숫자의 조상인 Number 타입을 활용하여 문제를 모두 해결하려고 했었음
- 그러나 개발을 진행하는 도중 과제에서 제네릭을 사용하라는 요구사항이 있어 무조건 제네릭을 적용해야할 것 같다는 생각에 박성규 튜터님께 질문하였음
- 우선 접근 방식은 좋으나 문제해결을 위한 구현방식은 다양할 수 있는데 여기 과제에서는 '제네릭을 어떻게 활용할 수 있는가'도 과제 채점 기준이기 때문에 제네릭을 활용하여 문제를 해결하는 방식도 들어가야 할 것 같다는 코멘트를 얻음
- 그리고 상담 시 이력을 저장하는 resultHistoryList에 ArithmeticCalculator 클래스 <T extends Number>로 타입변수 상한제한을 걸어서 <T> 타입으로 리스트에 여러 타입의 결과를 저장해도 괜찮다는 코멘트 듣고 생각을 달리 하였음
- 처음에는 이 변수의 용도를 단순히 애플리케이션에서 사용되는 단순한 컬렉션이라고 생각하여 컬렉션에는 여러 타입을 담으면 컬렉션에 제네릭이 도입된 이유와 상반된다고 생각하여 제네릭을 활용하지 않아야 한다고 생각했었음
- 그러나 이 resultHistoryList 컬렉션 변수는 단순히 중간에 결과를 담는 컬렉션 용도로 사용한다기 보다 결과를 저장하는 저장소의 개념으로 사용하고 있기 때문에 여러 타입을 입력해도 괜찮을 것 같다는 생각이 들어 제네릭과 다형성을 적절히 활용해 보는 방향으로 개발 방향을 전환하였음
2. Operator - Enum 클래스 개발
package v3;
public enum Operator {
PLUS("+", "+"),
MINUS("-", "-"),
MULTIPLY("*", "✕"),
DIVIDE("/", "÷"),
;
private final String input;
private final String symbol;
Operator(String input, String symbol) {
this.input = input;
this.symbol = symbol;
}
public String getInput() {
return input;
}
public String getSymbol() {
return symbol;
}
public static Operator getValidOperator(String input) {
Operator[] operators = Operator.values();
for (Operator operator : operators) {
if (operator.getInput().equals(input)) {
return operator;
}
}
return null;
}
}
- 입력 받는 연산자를 상수로 관리하기 위해 enum으로 생성
- 실제 사용자가 입력하는 값은 +, -, *, / 이지만 연산 결과를 출력할 때 출력되는 기호는 + , -, ✕, ÷ 가 출력되도록 각각의 필드를 선언
- 객체 지향적인 코드를 최대한 만들어보기 위해 입력받은 연산자와 enum의 연산자가 일치하는지 비교하는 메서드를 enum에 작성함
- 상수이므로 각 필드의 값을 꺼낼 수 있는 getter를 가지고 있음
3. OhterCommand - Enum 클래스 개발
package v3;
public enum OtherCommand {
EXIT("exit"),
HISTORY("his"),
BIG_HISTORY("bighis")
;
private final String command;
OtherCommand(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}
- 특별한 기능은 없이 프로그램 종료, 계산 결과 이력 조회, 입력 값보다 큰 계산 이력조회의 기능을 진입하는 분기문을 휴먼에러를 방지하고 변하지 않기 때문에 enum을 통해 관리
4. ArithmeticCalculator - 제네릭 클래스 개발
public class ArithmeticCalculator <T extends Number> {
// 이력을 저장하는 DB의 역할
private List<T> calculratorRepository = new ArrayList<>();
private void add(T result) {
calculratorRepository.add(result);
}
public T remove() {
return calculratorRepository.remove(0);
}
- 계산기를 실제 동작하는 비즈니스 로직과 계산 이력이 담긴 ArithmeticCalculator 클래스를 제네릭 클래스로 선언하고 타입 변수를 Number 타입으로 상한 제한을 걸어 Number(모든 숫자 타입의 조상)와 그 자식 클래스를 모두 담을 수 있도록 작성
- calculratorRepository는 계산 결과 이력을 담는 저장소라는 의미로 네이밍을 하였으며, 저장소이기에 동일한 타입만 담지 않고 실수, 정수를 모두 담을 수 있도록 타입 변수를 타입으로 지정
public void intCalculate(Operator operator, Integer firstValue, Integer secondValue) {
Integer result = 0;
switch (operator) {
case PLUS:
result = firstValue + secondValue;
break;
case MINUS:
result = firstValue - secondValue;
break;
case MULTIPLY:
result = firstValue * secondValue;
break;
case DIVIDE:
result = firstValue / secondValue;
break;
}
System.out.println("***************** 계산 결과 출력 *****************");
System.out.println("계산 결과: " + firstValue + " " + operator.getSymbol() + " " + secondValue + " = " + result);
System.out.println();
add((T) result);
}
- 클라이언트 (App 클래스)에서 검증된 연산자가 Integer 타입일 경우 호출되는 메서드
- 매개변수로 넘어온 연산자와 입력값들을 switch 문을 통해 계산하고 그 결과를 출력 후 저장소에 저장함
- Lv2에서는 계산하는 메서드와 결과를 출력하는 로직을 분리하였지만 여기서는 각 입력값에 따라 출력 포맷을 다르게 출력해야하여 Integer인 경우와 Double인 경우를 구분하기 위해 이렇게 변경하였음
public void doubleCalculate(Operator operator, Double firstValue, Double secondValue) {
Double result = 0.0;
switch (operator) {
case PLUS:
result = firstValue + secondValue;
break;
case MINUS:
result = firstValue - secondValue;
break;
case MULTIPLY:
result = firstValue * secondValue;
break;
case DIVIDE:
result = firstValue / secondValue;
break;
}
String printFirstValue = printFormat(firstValue);
String printSecondValue = printFormat(secondValue);
String printResultValue = printFormat(result);
System.out.println("***************** 계산 결과 출력 *****************");
System.out.println("계산 결과: " + printFirstValue + " " + operator.getSymbol() + " " + printSecondValue+ " = " + printResultValue);
System.out.println();
if (result % 1 == 0) {
add((T) Integer.valueOf(result.intValue()));
} else {
add((T) result);
}
}
private String printFormat(Double value) {
if (value % 1 == 0) {
return String.valueOf(value.intValue());
}
return String.valueOf(value);
}
- 매개변수의 인자에서 Double 타입이 넘어올 경우 계산한 결과를 저장하고 출력하는 메서드
- 인자가 Double 타입인 경우 사용자가 1.0, 4.0 처럼 일부러 소수점을 0으로 입력할 수 있고 연산 1.5 + 1.5 처럼 연산 결과가 3.0 으로 나올 수 있기 때문에 계산할 값과 계산 결과의 소수점의 자리수가 .0 으로 끝날 경우 정수 자리만 출력될 수 있도록 변환하는 printFormat 이라는 메서드를 추가로 호출
- 어차피 출력은 String 타입으로 출력해도 되기 때문에 String.valueOf() 메서드로 형변환을 진행
- 결과를 저장할 때도 3.0, 4.0 처럼 소수점의 자리가 0으로 끝나는 경우에는 정수로 결과가 저장되도록 if문을 통해 코드를 각각 작성하였음
public void inputThanBigValuePrint(T inputValue) {
if (calculratorRepository.isEmpty()) {
System.out.println("계산 결과 이력이 없습니다.");
System.out.println();
return;
}
System.out.println(inputValue + "보다 큰 계산 이력 출력");
double doubleInputValue = inputValue.doubleValue();
List<T> resultList = calculratorRepository.stream()
.filter(history -> history.doubleValue() > doubleInputValue)
.sorted(Comparator.comparingDouble(history -> history.doubleValue())) // 동일 타입으로 정렬하기 위해 비교자를 double 타입으로 지정
.toList();
System.out.println(resultList);
}
- 도전 과제의 구현해야할 기능 중 하나로, 여러 연산 결과 중에서 입력 한 값 초과되는 값들만 출력하는 메서드
- stream과 lambda를 연습해보려는 목적으로 만들어진 기능으로, 여기에 계산 결과가 없을 때 예외처리되는 코드를 추가하였음
- 해당 저장소에는 Integer, Double 두개의 타입이 저장되어있기 때문에 비교 연산자를 통하여 입력값보다 큰 값을 출력하기 위해서는 Int보다 표현 범위가 넓은 Double 타입으로 변환하는 것이 값 손실이 없으므로 Double 타입으로 변환하여 조건을 비교하였음
- 해당 이력은 계산 이력을 전체적으로 보여줄 필요가 없다고 판단되어 sorted() 중간연산을 통해 정렬을 수행하여 결과를 반환하였고, 마찬가지로 정렬을하기 위해서는 타입이 일치해야 하기 때문에 Comparator.comparingDouble을 통해 double 타입으로 일시적으로 변환하여 값을 비교하여 정렬할 수 있도록 비교자를 인수로 전달하였음)
5. App - 클라이언트 역할 클래스
public class App {
// 음수, 소수점, 숫자만 들어올 수 있는 정규 표현식
static final String INPUT_VALID_REGEXP = "^-?(?:[1-9]\\d*|0)(?:\\.\\d+)?$";
- 계산한 값들을 입력 받을 때 String 타입으로 입력 받아 음수, 양수, 0, 소수점만 입력 가능하도록 검증하기 위한 정규 표현식을 작성
- 정규 표현식에 대해서는 깊게는 잘 모르지만 인터넷에 각 언어와 상황에 따라 자주 쓰는 정규 표현식이 거의 정해져있어서 검색해서 사용해도 괜찮다고 생각함
public static void main(String[] args) {
ArithmeticCalculator<Number> calculator = new ArithmeticCalculator<>();
- 앞서 제네릭 클래스로 만든 ArithmeticCalculator를 사용하기 위해 제네릭 타입을 Number로 지정하여 생성
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("==== 계산기 시작 ====");
System.out.println("- 종료: exit");
System.out.println("- 계산결과 이력 보기: his");
System.out.println("- 입력값보다 큰 계산 결과 이력 보기: bighis");
System.out.println("- 계산 부호 입력 (+ , -, *, /)");
System.out.print("메뉴를 선택해 주세요: ");
- 기능이 추가되어 계산기를 시작할 때 메뉴를 보여주는 방식을 변경하였고 입력값보다 큰 계산 결과 이력을 보기 위해서는 bighis를 입력하면 됨
String operator = scanner.nextLine();
if (operator.equals(OtherCommand.EXIT.getCommand())) { // Enum 적용
System.out.println("계산기 프로그램을 종료합니다.");
break;
} else if (operator.equals(OtherCommand.HISTORY.getCommand())) { // Enum 적용
calculator.historyPrinter();
continue;
} else if (operator.equals(OtherCommand.BIG_HISTORY.getCommand())) {
String inputValue;
while (true) {
System.out.print("조회 하실 값을 입력해 주세요: ");
inputValue = scanner.nextLine();
boolean checkResult = Pattern.matches(INPUT_VALID_REGEXP, inputValue); // 정규식과 비교
if (!checkResult) {
System.out.println("**** 입력값이 올바르지 않습니다. 숫자(소수점포함)만 입력해 주세요.****");
System.out.println();
continue;
}
break;
}
if (inputValue.contains(".")) {
calculator.inputThanBigValuePrint(Double.valueOf(inputValue));
} else {
calculator.inputThanBigValuePrint(Integer.valueOf(inputValue));
}
continue;
}
- 입력받은 연산자를 if문에서 검증할 때 휴먼에러가 발생할 수 있는 문자열을 직접 입력하는 것이 아니라 상수를 통해 값을 비교하여 동작하도록 수정하였음
- bighis(입력 값보다 큰 값 계산 결과 출력)기능은 조회할 값을 입력 받아야하기에 정수 타입, 인티저 타입 둘다 입력받을 수 있도록 nextLine으로 먼저 문자열로 입력 받고, Pattern.matches()메서드를 통해 정규 표현식과 비교하여 어긋나는 입력값이 있는지 먼저 검증을 진행
- 그 다음 입력받은 문자열에 .이 있으면 Double로, 그게 아니라면 Integer로 형변환을 하여 inputThanBigValuePrint()메서드를 호출하여 입력 값보다 더 큰 계산결과를 출력
Operator validOperator = getValidOperator(operator);
if (validOperator == null) { // 연산자 검증로직을 Enum 클래스의 static 메서드에서 검증 -> 객체 지향적인 설계를 위함
System.out.println("**** 연산 부호를 잘못 입력 하셨습니다. 다시 시작합니다 ****");
System.out.println();
continue;
}
- 계산을 위해 입력받은 연산자를 enum에 정의한 값이 맞는지 아닌지를 검증하고 enum타입으로 반환하는 메서드
- 문자열을 직접 다루지 않기 때문에 휴먼에러, 오타에러 등을 방지할 수 있음
// 입력값 검증
String validFirstValueStr = validFirstValue(scanner, INPUT_VALID_REGEXP);
private static String validFirstValue(Scanner scanner, String regExp) {
String firstValue;
while (true) {
System.out.print("숫자를 입력해 주세요(소수점 입력 가능): ");
firstValue = scanner.nextLine();
boolean checkResult = Pattern.matches(regExp, firstValue); // 정규식과 비교
if (!checkResult) {
System.out.println("**** 입력값이 올바르지 않습니다. 숫자(소수점포함)만 입력해 주세요.****");
System.out.println();
continue;
}
break;
}
return firstValue;
}
- 계산을 위한 입력값을 정규표현식을 통해 검증하는 메서드
String validSecondValueStr = validSecondValue(validOperator.getSymbol(), scanner, INPUT_VALID_REGEXP);
private static String validSecondValue(String operator, Scanner scanner, String regExp) {
String secondValue;
while (true) {
if (operator.equals(Operator.DIVIDE.getSymbol())) {
System.out.print("두번째 숫자를 입력해주세요. 나눗셈 연산은 0으로 나눌 수 없습니다.: ");
secondValue = scanner.nextLine();
boolean checkResult = Pattern.matches(regExp, secondValue); // 정규식과 비교
if (!checkResult) {
System.out.println("**** 입력값이 올바르지 않습니다. 숫자(소수점포함)만 입력해 주세요.****");
System.out.println();
continue;
}
if (Double.parseDouble(secondValue) == 0.0) {
System.out.println("**** 나눗셈 연산에서는 0을 입력할 수 없습니다. 다시 입력해 주세요 ****");
System.out.println();
continue;
}
break;
} else {
System.out.print("두번째 숫자를 입력해주세요. 숫자만 가능합니다: ");
secondValue = scanner.nextLine();
boolean checkResult = Pattern.matches(regExp, secondValue); // 정규식과 비교
if (!checkResult) {
System.out.println("**** 입력값이 올바르지 않습니다. 숫자(소수점포함)만 입력해 주세요.****");
System.out.println();
continue;
}
break;
}
}
return secondValue;
}
- 계산을 위한 입력값을 정규표현식을 통해 검증하는 메서드이며 두번째 입력값이므로 나눗셈 연산에서는 0을 입력받을 수 없도록 하는 조건문 로직이 들어가있음
- String으로 입력 받을 때 악의적으로 0.000000000000, 0.0, 0 등으로 입력한 경우 Double 타입으로 형변환 하면 무조건 0.0이 되기 때문에 이렇게 비교하면 다양한 0의 경우의 수를 예외처리할 수 있음
- Lv2와 마찬가지로, 나눗셈 연산자일 때와 아닐 때를 구분하기 위한 로직도 포함되어있음
Number firstValue = inputTypeConverter(validFirstValueStr);
Number secondValue = inputTypeConverter(validSecondValueStr);
private static Number inputTypeConverter(String inputValue) {
if (inputValue.contains(".")) {
return Double.valueOf(inputValue);
} else {
return Integer.valueOf(inputValue);
}
}
- 정규 표현식 검증을 거친 입력값들을 실제 계산을 할 수 있는 타입으로 변경하기 위해 컨버터메서드를 작성
- 검증된 값에 .이 있으면 Double로, 그게 아니라면 Integer로 형변환 하고 다형성을 활용하여 타입별로 동작할 수 있도록 하기 위해 Number타입으로 반환
if (firstValue instanceof Double || secondValue instanceof Double) {
calculator.doubleCalculate(validOperator, firstValue.doubleValue(), secondValue.doubleValue());
} else {
calculator.intCalculate(validOperator, firstValue.intValue(), secondValue.intValue());
}
calculator.historyCountHandler(); // 계산 이력이 10개 이상되면 1개를 삭제하고 계산 이력의 개수를 출력
- Number타입으로 변환 된 계산을 위해 입력받은 두 수를 각각 intanceof로 체크하여 하나라도 Double 타입이 있으면 계산을 위한 메서드에 두 값을 모두 double타입으로 인자를 넘김
- 만약 Double 타입이 아니라면 Integer 타입이므로 int 타입으로 값을 넘겨서 계산 결과를 출력
- historyCountHandler()는 Lv2와 기능이 동일함
6. 실행 결과 스샷
연산자 입력시 다른 값을 입력하면 안내 문구 출력됨
his, bighis 조회시 계산 결과가 없으면 계산 결과 이력이 없다고 출력됨
계산할 값을 입력 시 숫자가 아닌 타입을 입력하면 안내 문구가 출력되며 값을 다시 입력할 수 있도록 재 안내함
결과가 계산되면 계산 결과와 계산 이력이 증가됨
계산을 위한 인자를 소수점을 포함한 값으로 전달하여도 계산 결과의 소수점 자리수가 0으로 끝나면 1.0 처럼 필요없는 소수점이 출력되지 않고 1로 정수 타입으로 출력되는 것을 확인할 수 있음
악의 적으로 소수점의 자리를 00000 으로 입력하여도 계산이 정상적으로 정수로 입력되고 결과도 정수로 출력되는 것을 확인할 수 있음
나눗셈 연산 시 두번째 입력값은 0이 입력될 수 없도록 검증하고 소수점의 결과도 정상적으로 출력되는 됨
마찬가지로 2.5로 나누었음에도 결과의 소수점의 자리수가 0이므로 정수로 출력되는 것을 확인할 수 있음
his를 입력하여 계산 이력을 출력하면 실수, 정수 구분없이 모두 저장소에 잘 담겨있는 것을 확인할 수 있으며 가장 먼저 계산된 순서를 보기좋게 출력해주고 있음
bighis를 입력하여 입력값보다 큰 계산 결과 이력을 보는 기능을 호출해보면 입력값인 4보다 큰 값을 오름 차순 정렬해서 보여주는 것을 확인할 수 있음
계산 이력이 9개인 상태에서 한번더 계산을 진행하면 더이상 계산 결과를 보관할 수 없어 가장 오래된 계산 결과를 삭제한다는 문구와 삭제된 값을 출력해줌
his를 입력해보면 계산 처음 계산했던 3이 삭제되고 새로 계산된 값이 출력되는 것을 확인할 수 있음
** 버그 픽스
public void historyCountHandler() {
if (getSize() > 10) {
T removeValue = remove();
System.out.println("계산 결과를 더이상 보관할 수 없어 가장 오래된 계산 결과가 삭제 되었습니다.");
System.out.println("삭제된 계산 결과: " + removeValue);
System.out.println();
return;
}
System.out.println("계산 이력 " + getSize() + "건" );
}
- 여기에서 원래 10개까지 값을 저장할 수 있도록 로직을 구상하였는데 실제 테스트 시에는 9개까지만 동작하는 것을 확인함
- 10개가 넘어가면 오래된 이력을 삭제하는 조건문의 로직을 getSize() >= 10 에서 getSize() > 10으로 변경함으로 인해 버그를 수정하였음
버그가 수정된 출력 결과
이로서 LV3의 도전 과제의 필수 구현 기능과 각종 생각나는 예외처리를 모두 진행하는 계산기가 완성되었음
개인 강의를 듣고 있는것이 추가적으로 있어서 시간이 더 있을지는 모르겠지만, 시간이 난다면 혼자만의 도전으로 추가적인 기능과 포맷팅까지 탑재한 계산기를 만들어보는 것을 도전해 보고자 함