개발지식 먹는 하마 님의 블로그
[내일배움캠프 13일차] _ GUI 계산기 구현, 트러블 슈팅 본문
GUI 계산기 프로젝트를 구현하였다.
시간 관계상, 입력과 연산이라는 필수 기능 구현까지 끝마친 상태이다.
따라서 구현 방법, 트러블 슈팅, 개선할 점 등을 기록하고자 한다.
📚 주요 기능 구현
❗ 기능 3가지
- GUI 구성
- 버튼을 사용한 수식 입력 제한
- 스택을 이용한 우선순위에 따른 연산
< GUI 구성 >
JFrame의 Border Layout과 Grid Layout을 사용하여 GUI를 구성하였다.
🟥 BorderLayout으로 구성한 상부
계산기 상부에는 record와 exit, textField와 backSpace를 특정 위치에 배치하고 싶어 Border Layout을 사용하였다.
Border Layout인 JPanel을 생성해 West에 record 버튼, East에 exit 버튼을 배치하였다.
또 다른 Border Layout인 JPanel을 생성 후 Center에 수식을 입력받을 textField를, East에 backSpace를 배치하였다.
이를 별도의 패널에서 합친 후, South에 결과를 출력하는 textField를 배치하여 아래의 이미지와 같은 상부를 구성하였다.
🟧 GridLayout으로 구성한 하부
표준 계산기를 기반으로 GUI에 생성할 버튼의 종류를 선정하였다.
C, 괄호, 숫자, 연산자, 소수점., = 을 선정 후, 배열에 저장하였다.
String[] labels = {
"C", "(", ")", "/",
"7", "8", "9", "*",
"4", "5", "6", "-",
"1", "2", "3", "+",
"+/-", "0", ".", "="
};
필요한 버튼을 직접 생성하면 코드가 매우 길어지기 때문에 반복문을 사용하였다.
배열에 저장된 label의 개수만큼 버튼 생성 후, ActionListner를 추가하고 이를 Grid Layout으로 선언된 Panel에 추가한다.
for(String l : labels){
JButton btn = new JButton(l);
btn.addActionListener(new BtnClickListener());
btnPanel.add(btn);
}
이렇게 구성한 계산기의 전체 모습은 다음과 같다.
( +/- 기능은 구현하지 못해 비활성화 해놓았다. )
< 수식 입력 제한 >
textField를 수정 불가능한 상태로 설정해놓았기 때문에 버튼으로만 수식 입력이 가능한 상태이다.
따라서 버튼에 액션 리스너를 추가하고 각 버튼 클릭 시, 적절한 입력을 처리한다.
이 때 입력을 별도의 클래스 InputFunction에서 관리하도록 하였다.
괄호와 소수점도 입력 받기 때문에, 이에 대한 제어를 하면서 입력을 받도록 하였다.
🟨 1) 실수형을 나타내는 온점
- 한 숫자에서 온점이 2개 이상 사용되면 안된다. ex) 0.123.456 불가
boolean형의 isDouble 변수를 선언하여, 실수형을 입력할 수 있는 상태의 여부를 구분했다.
- 빈 텍스트에 온점이 입력된 경우, 앞에 0을 삽입해주어야 한다.
if(result.isEmpty()){ //아무것도 입력되지 않은 상황이라면
result = result + "0" + command; //앞에 0을 생성한 후 온점 추가
}
- 온점을 사용했으나 이 이후 아무 숫자도 입력되지 않은 경우, 뒤에 0을 삽입해주어야 한다.
별도의 메서드를 생성한 후 연산 기호 또는 괄호 입력 시 이를 호출해, 해당 상황을 검사 및 조치를 취하도록 했다.
private String isEndDot(String text){
String result = text;
if(!isDouble){ //이미 온점이 사용된 상태이고
char lastChar = text.charAt(text.length()-1);
if(lastChar == '.'){ //온점 뒤에 숫자가 입력되지 않았다면
result = result + "0";
}
}
return result;
}
🟩 2) 괄호
- 여는 괄호가 존재해야 닫는 괄호도 입력될 수 있다.
boolean형의 isParentheses 변수를 선언하여, 괄호가 열려있는지의 여부를 구분했다.
- 여는 괄호가 사용된 만큼 닫는 괄호도 사용되어야 한다.
정수형의 numParentheses 변수를 선언하여, 여는 괄호가 사용된 횟수를 반영했다.
- 여는 괄호 앞 문자가 숫자라면, 생략된 곱셈 기호를 추가한다.
char lastChar = result.charAt(result.length()-1);
if (Character.isDigit(lastChar)) { //여는 괄호의 바로 앞 문자가 숫자라면
result = result + "*("; //곱하기 연산자를 여는 괄호 앞에 추가
isParentheses = true; //괄호가 열려 있다고 설정
numParentheses++; //닫아야 하는 괄호의 개수 1 증가
isDouble = true;
}
- 닫는 괄호 뒤에 바로 숫자가 올 수 없다.
if(lastChar == ')'){ //닫는 괄호로 끝나 있는 경우
result = result + ""; //숫자 입력 불가
}
🟦 3) BackSpace
- 온점을 지운 경우, 실수형 입력 가능 상태로 변경해주어야 한다.
- 괄호 열림 여부, 개수를 변경해주어야 한다.
🟪 4) = 으로 입력을 마무리 할 때
- 온점 마무리 여부, 온점을 사용했으나 이후 아무 숫자도 입력되지 않은 경우 뒤에 0을 삽입해주어야 한다.
- 괄호가 다 닫히지 않은 경우, 남은 횟수 만큼 닫아주어야 한다.
for(int i = numParentheses; i > 0; i--){
result = inputCloseParentheses(result);
}
< 스택을 이용한 우선순위에 따른 연산 >
숫자 전용 스택 NumbersStack, 연산자 전용 스택 OperatorsStack 총 2개의 스택을 사용한다.
(아래의 예시처럼 스택의 크기가 5로 정해진 것은 아니다.)
알고리즘은 다음과 같다.
🟥 수식 분리하기
먼저 Pattern과 Matcher를 이용해서 숫자, 연산자, 괄호에 따라 문자열을 분리한다.
private void separateInput(String input){
//패턴, 매처, 정규표현식 사용
Pattern pattern = Pattern.compile("(\\d*\\.?\\d+)|([+\\-*/()])");
Matcher matcher = pattern.matcher(input);
inputSeperate.clear();
while(matcher.find()){
inputSeperate.add(matcher.group());
}
}
🟨 분리된 문자열 확인 후 연산
분리된 문자열을 읽으며 해당 문자열이 숫자, 연산자, 괄호 중 어디에 해당하는지 판별한 후,
그에 따라 스택에 값을 push 또는 연산을 수행한다.
private void applyOperation(Stack<Double> numbersStack, String operator) {
if (numbersStack.size() < 2) { //연산할 숫자가 없다면
throw new IllegalArgumentException(); //예외 처리
}
double num2 = numbersStack.pop(); //stack의 맨 위는 나중에 입력된 숫자이기 때문에 num2로 설정
double num1 = numbersStack.pop();
OperationType operation = OperationType.checkOperator(operator); //연산 기호 확인
numbersStack.push(operation.apply(num1, num2)); //연산 후 결과를 numbersStack에 push
}
연산 과정을 별도의 메서드로 구성했다.
두 개의 숫자를 스택에서 꺼내, 마찬가지로 스택에서 꺼낸 연산자에 따라 연산한 후 그 결과를 다시 스택에 push한다.
연산을 해야 하는데 Stack에 저장된 숫자의 개수가 2개 미만이라면 연산을 할 수 없기 때문에 예외를 던진다.
🟩 괄호 우선 연산
닫는 괄호가 인식된 경우, operatorsNumber에 저장된 문자열의 맨 위가 여는 괄호 "("가 나올 때까지
저장되어있는 연산자에 대해 연산한다.
private void processOperatorsUntilParenthesis(Stack<Double> numbersStack, Stack<String> operatorsStack) {
//operatorsStack의 맨 위 데이터가 여는 괄호가 될 때까지, 괄호 내의 연산 수행
while (!operatorsStack.isEmpty() && !operatorsStack.peek().equals("(")) {
applyOperation(numbersStack, operatorsStack.pop());
}
operatorsStack.pop(); // operatorsStack의 맨 위에 있는 여는 괄호 제거
}
🟪 예시로 알고리즘 파악하기
수식이 10 * (9 + 3 * 7) 라고 가정해보겠다.
스택에는 아래와 같이 숫자와 연산자가 push 된다.
+ 연산자가 입력된 경우, operatorsStack에 가장 최근 입력된 기호가 괄호로 우선순위가 낮기 때문에
별다른 연산이 수행되지 않고 + 기호가 저장된다.
이후에 있는 * 연산자도 마찬가지이다.
수식을 끝까지 탐색한 결과는 아래와 같다.
닫는 괄호가 인식되었기 때문에, operatorsStack의 맨 위에 여는 괄호가 올 때까지 연산을 시작한다.
여는 괄호를 pop하며 괄호 내부의 연산이 마무리 되었다.
이제 operatorsStack가 텅 빌 때까지 남아있는 연산자에 대해 연산한다.
더 이상 operatorsStack에 남아있는 연산자가 없으면, numbersStack에 저장되어 있는 숫자를 return한다.
return된 결과를 결과 전용 textField에 출력한 결과는 다음과 같다.
📸 시연 영상
소수점과 괄호를 사용한 연산, 결과 출력 후 버튼을 누를 때 입력 필드가 초기화 되는 등의 기능을 확인할 수 있다.
자세한 코드 또는 프로그램 실행은 아래의 링크를 통해 확인할 수 있다.
https://github.com/crocusia/sparta_spring6_week2/tree/gui
GitHub - crocusia/sparta_spring6_week2
Contribute to crocusia/sparta_spring6_week2 development by creating an account on GitHub.
github.com
⏳ 트러블 슈팅
< 까다로운 입력받기 >
괄호의 닫는 개수, 실수형을 위한 소수점 입력 개수 등
허용되지 않는 수식의 입력을 방지하기 위해 이를 제한하였다.
모든 경우의 수를 제한하는 것이 매우 어렵고 까다로웠다.
직접 경우의 수를 시험하며 제대로 제한되었는지 확인하며 기능을 추가했으나
제한하지 못한 기능 부분도 있다.
실수형을 지운 후, 앞서 입력된 실수형에 대해서 온점이 한 번더 입력되지 않도록 해줘야 하지만
이 부분 까지는 처리하지 못했다.
예시)
지우기 전 입력
: 0.2 + 0.5
지운 후의 상태 가정
: 0.2
이 때 제한하지 못한 입력 상황
: 0.2.
잘못된 수식이 입력된 경우 예외 처리를 하는 코드를 추가할 필요성을 느꼈다.
📚 개선할 점
- 위에서 언급한 것처럼 이미 입력된 소수점을 지운 후,
이전에 실수형으로 표현된 숫자에 소수점이 하나 더 입력될 수 있는 가능성 제어 - 잘못된 수식 입력을 처리하는 예외
- 저장된 연산 기록을 record 버튼 클릭 시, 확인할 수 있도록 하는 기능
- 다음 연산자가 입력되기 전까지 입력되어 있는 수식에 대해 계산 결과를 제공하고 이를 업데이트 하는 기능
- 공학용 기능 추가
등등 개선할 점은 아직 많이 남아있다.
'내일배움캠프 (CS25)' 카테고리의 다른 글
[내일배움캠프 15일차] _ lower_bound vs find (0) | 2025.03.10 |
---|---|
[내일배움캠프 14일차] _ 수학적으로 더 간편하게 풀기, Git 커밋 컨벤션 (0) | 2025.03.07 |
[내일배움캠프 12일차] _ 시소 짝꿍 풀이, GUI 계산기 입력 제한 구현 (0) | 2025.03.05 |
[내일배움캠프 11일차] _ 계산기 lv3 구현, 트러블 슈팅 (1) | 2025.03.04 |
[내일배움캠프 10일차] _ 문자열, Pattern&Matcher, 계산기 lv3 구현 (0) | 2025.02.28 |