개발지식 먹는 하마 님의 블로그
[내일배움캠프 11일차] _ 계산기 lv3 구현, 트러블 슈팅 본문
계산기 프로젝트의 level3(도전) 요구사항 구현을 완료했다.
도전 기능의 요구사항을 어떻게 구현했는지에 대한 간략 설명 후,
구현하면서 겪었던 트러블 슈팅에 대해 기록하고자 한다.
📚 도전 기능 구현
❗ 도전 기능 요구사항
- 제네릭을 활용하여 실수를 전달받아도 연산이 수행하도록 하기
- Enum 타입을 활용하여 연산자 타입에 대한 정보를 관리하기
- Lambda와 Stream을 활용하여
저장된 연산 결과들 중 Scanner로 입력받은 값보다 큰 값을 조회 및 출력하기
두 개의 입력값으로 연산을 한다고 할 때,
(정수, 정수) = 정수
(정수, 실수) = 실수
(실수, 정수) = 실수
(실수, 실수) = 정수, 실수
위의 경우의 수와 같이 입력값 중 하나라도 실수라면 결과도 거의 실수로 나오기 때문에 (예외도 있다)
따라서, 변수를 double로 설정하고 계산 결과가 정수인 경우에만 정수형으로 변경해 주는 것이 일반적이다.
그러나, 이번 프로젝트는 Java의 요소를 직접 사용해보는 경험을 위한 것으로
요구사항에 제네릭을 사용하라고 명시되어있기 때문에 그에 따라 제네릭을 활용하였다.
< 제네릭을 활용하여 실수 값을 전달 받아도 연산이 수행되도록 하기 >
🟥 Number Class 사용
입력을 받을 데이터는 Int와 double 두 종류이다.
따라서, Integer와 double의 부모 클래스인 Number를 사용했다.
입력 데이터가 정수 또는 실수로 특정 지어진 경우 nextInt() 또는 nextDouble()을 사용하지만,
두 자료형을 모두 입력 데이터로 받을 경우에는 이것이 불가능하다.
다만 정수를 실수형으로 받아도 무방하기 때문에 2개의 입력을 모두 nextDouble로 받아와 Number 객체에 저장한다.
System.out.print("연산할 첫 번째 값을 입력해주세요 : ");
Number num1 = scanner.nextDouble();
System.out.print("연산할 두 번째 값을 입력해주세요 : ");
Number num2 = scanner.nextDouble();
🟨 제네릭으로 연산
Enum 내부에 위와 같은 추상 메서드를 선언하여 제네릭을 활용한 연산을 하고자 했다.
실제로는 Number 객체가 매개변수로 넘어가지만,
입력받은 두 데이터의 자료형이 서로 다를 수 있다는 점을 고려하여,
제네릭 타입 T1과 T2를 사용해 각각의 자료형을 유연하게 받을 수 있도록 하였다.
public abstract <T1 extends Number, T2 extends Number> Number apply(T1 num1, T2 num2);
제네릭 타입 자체로는 직접 연산을 수행할 수 없다.
따라서, Number를 상속받아 숫자형 데이터로 제한함으로써 연산이 가능하도록 하였다.
< Enum 타입을 활용하여 연산자 타입에 대한 정보를 관리하기 >
🟩 Enum으로 연산자 타입을 관리
ADD, SUB, MUL, DIV를 정의하고, 앞서 선언한 제네릭 추상 메서드를 각 연산에서 오버라이딩 하였다.
doubleValue()를 사용해 연산을 하기 때문에 연산 결과는 무조건 double이지만
정수와 실수를 포괄하는 Number를 반환하도록 하였다.
private static enum OperationType{
ADD('+'){
@Override
public <T1 extends Number, T2 extends Number> Number apply(T1 num1, T2 num2){
return num1.doubleValue() + num2.doubleValue();
}
},
SUB('-'){
@Override
public <T1 extends Number, T2 extends Number> Number apply(T1 num1, T2 num2){
return num1.doubleValue() - num2.doubleValue();
}
},
MUL('*'){
@Override
public <T1 extends Number, T2 extends Number> Number apply(T1 num1, T2 num2){
return num1.doubleValue() * num2.doubleValue();
}
},
DIV('/'){
@Override
public <T1 extends Number, T2 extends Number> Number apply(T1 num1, T2 num2){
if(num2.doubleValue() == 0.0){
throw new ArithmeticException();
}
return num1.doubleValue() / num2.doubleValue();
}
};
}
🟦 Enum으로 연산자 호출하기
주어진 연산 기호에 해당하는 연산 타입을 찾아서 Enum으로 선언된 OperationType의 값을 반환한다.
public static OperationType checkChar(char operator) {
for(OperationType op : OperationType.values()){
if(op.operator == operator){
return op;
}
}
throw new IllegalArgumentException("연산자: " + operator);
}
< Lambda와 Stream을 활용하여 입력값보다 큰 값을 조회 및 출력하기 >
🟪 Lambda와 Stream 활용
public List<Number> selectList(Number point){
return results.stream()
.filter(r -> r.doubleValue() > point.doubleValue())
.collect(Collectors.toList());
}
stream()로 연산 결과가 저장된 results를 스트림으로 변환한다.
*스트림 : 컬렉션을 연산 가능한 형태로 변환
filter()는 조건에 따라 데이터를 필터링한다. 이 때, 조건을 람다식으로 작성하였다.
r -> r.doubleValue() > point.doubleValue()
리스트의 각 요소 r을 입력값 point와 비교한다.
collect()은 조건에 따라 필터링된 결과를 별도의 리스트에 저장 및 반환한다.
⏳ 트러블 슈팅
2개의 어려움을 겪었다.
매우 간단한 문제 하나, 트러블 슈팅이라고 하기에는 조금 애매한 문제 하나이다.
< 저는 아무것도 안했는데 왜 넘어가시죠? >
문제는 계산기 프로그램의 종료 여부를 묻는 부분에서 발생하였다.
❓ 문제 현상 ❓
아래의 예시처럼 아무것도 입력하지 않았지만,
Enter 키를 누를 틈도 없이 다음 연산을 위해 정수를 입력받는 부분으로 넘어가는 현상이 나타난 것이다.
❗ 문제 원인 ❗
원인은 nextInt(), charAt() 사용에 있었다.
nextInt()나 charAt()와 같이 문자열 전체가 아닌 특정 데이터를 받는 메서드는 개행문자를 읽지 않는다.
따라서 입력 버퍼에 개행 문자가 여전히 남아있게 된다.
프로그램의 종료 여부를 묻는 부분의 scanner가 버퍼에 남아있던 개행 문자를 읽게 되면서
사용자가 별다른 입력을 하지 않아도 다음 단계로 넘어가게 되는 것이다.
✅ 해결 방안
다음 입력을 받기 전, scanner.nextLine()으로 입력 버퍼에 남아있는 개행문자를 제거하면 문제가 해결된다.
scanner.nextLine(); //입력 버퍼에 남아있는 개행문자 제거
System.out.println("종료하려면 exit를 입력해주세요: ");
String isEnd = scanner.nextLine();
< 쓸 필요가 없지만 그래도 써야 한다. >
lv3의 도전 기능을 구현하는 전반적인 과정에서 혼란을 겪어야 했다.
❓ 문제 현상 ❓
코드가 길고 복잡해지려고 했다.
초기 구현 과정에서 입력을 String으로 받았다. 입력이 정수인지 실수인지를 구분하기 위해서였다.
String으로 입력 받은 값을 정규표현식을 사용하여 정수인지 실수인지를 판별하였다.
이를 Number 객체에 각각 Integer 또는 Double로 다운캐스팅을 하였다.
public static Number checkNumber(String input) {
if (input.matches("-?\\d+")) { // 정수 판별
return Integer.parseInt(input);
} else if (input.matches("-?\\d+\\.\\d+")) { // 실수 판별
return Double.parseDouble(input);
} else {
throw new InputMismatchException();
}
}
정수와 실수의 구분을 사칙연산에도 적용하려면 이 외에 더 많은 기능이 필요했다.
- 사칙연산 메서드에서 매개변수로 받은 Number 객체가
Integer의 인스턴스인지 Double의 인스턴스인지 instance of로 판별하기 - 모든 사칙연산 메서드마다 1번의 인스턴스 판별 결과에 따라
intValue() 또는 doubleValue를 적용하도록 조건문 사용하기
❗ 문제 원인 ❗
제네릭을 사용하지 않으면 이렇게까지 길어질 필요가 없는 일이었다.
내가 제대로 하고 있는게 맞는지 계속 의문이 들었다.
또, 제네릭을 사용하지 않을 때의 구현 방법이 계속 떠올라 더욱 혼란스럽게 했다.
✅ 해결 방안
튜터님께 질문하기
여러 튜터님께 구현한 코드를 바탕으로 내가 겪은 혼란에 대해 질문드렸다.
공통적인 답변은 "제네릭은 사용에 의의를 두는 것이기 때문에 그렇게까지 깊게 들어갈 필요는 없다!"는 것이었다.
추가적으로 제네릭을 사용하지 않을 때와 혼돈하여 연산 결과를 double로 반환하고
연산 결과 역시 double 형태로 저장하도록 구현한 부분이 있었는데,
이를 제네릭 형식으로 바꾸면 좋을 것 같다는 피드백을 받았다.
피드백을 바탕으로 다음과 같이 코드를 변경하였다.
System.out.print("연산할 첫 번째 값을 입력해주세요 : ");
Number num1 = scanner.nextDouble();
System.out.print("연산할 두 번째 값을 입력해주세요 : ");
Number num2 = scanner.nextDouble();
입력은 위에서 설명한 바와 같이 Number 객체로 선언하되 정수와 실수를 모두 받을 수 있는 nextDouble()로 받아주었다.
public abstract <T1 extends Number, T2 extends Number> Number apply(T1 num1, T2 num2);
private final List<Number> results = new ArrayList<>();
double로 선언했던 요소들을 Number로 변환해주었다.
📸 시연 영상
영상을 통해 아래와 같은 경우의 실행을 순서대로 확인할 수 있다.
- 입력값이 정수나 실수가 아닐 때
- 입력값이 연산기호가 아닐 때
- 0으로 나눌 때
- 사칙연산
- record 입력 시, 연산 결과 기록 출력
- delete 입력 시, 기록 삭제
삭제 개수 -1 입력 시, 재입력 - select 입력 시, 특정 값이상 연산 결과 기록 출력
- 기록 삭제 시, 삭제 개수가 데이터 개수보다 크면 전체 데이터 삭제
- exit 입력 시, 프로그램 종료
'내일배움캠프 (CS25)' 카테고리의 다른 글
[내일배움캠프 13일차] _ GUI 계산기 구현, 트러블 슈팅 (0) | 2025.03.06 |
---|---|
[내일배움캠프 12일차] _ 시소 짝꿍 풀이, GUI 계산기 입력 제한 구현 (0) | 2025.03.05 |
[내일배움캠프 10일차] _ 문자열, Pattern&Matcher, 계산기 lv3 구현 (0) | 2025.02.28 |
[내일배움캠프 9일차] _ 탐욕법, 계산기 lv2 구현, Git 오류 (0) | 2025.02.27 |
[내일배움캠프 8일차] _ 탐욕법, 계산기 GUI (0) | 2025.02.26 |