Sparta/Projects

[Project] 계산기를 만들자

syuare 2025. 4. 18. 15:38

 


개발 기획서

 

프로젝트 목표

- 두 정수를 입력 받아 사칙연산(+,-,*,/)이 가능한 계산기를 만들자

- LV1: 클래스 없이 계산기를 만들기

- LV2: 클래스를 생성하여 계산기를 만들기

- LV3: 심화된 기술을 계산기 코드에 적용해보기

 

개발환경

- Intelli J, Java, JDK 17버전, Github


Calculator 구조 / 흐름


 

Lv1. 클래스 없이 계산기 만들기

더보기

요구사항

 1. 양의 정수(0포함)만 입력 받도록 하기 

 2. 사칙연산 기호(+,-,*,/) 를 입력 받기  

 3. 입력받은 양의 정수 2개 + 사칙연산 기호 사용 > 연산 결과 출력   

 4. 반복문 사용 > 반복 종료 키워드를 "exit" 문자열 입력해야 종료되도록, 해당 문자열이 아니면 무한 반복 계산


 

예외 처리 필요

 입력해야하는 정수 값을 음의 정수 값으로 입력 시 > 오류 메시지 추가

✅ 잘못된 사칙연산 기호 입력 시 오류 메시지 처리 

 사칙연산 기호 '/' && 분모값이 0 입력 시 > 오류 메시지 추가

 정수 입력란에 문자나 문자열을 입력할 경우 프로그램이 종료되는 현상 해결 

 

결과값

 뺄셈 > 결과값 마이너스 (-) 일 경우 > 그대로 결과값으로 출력

 나눗셈 결과값에 소수점이 생성될 경우 소수점까지 결과값으로 출력 > 결과값 변수 double형 형변환

 

반복문

  while 선택

 - for의 경우 조건식 3개 작성의 번거로움, 증감식 인자를 어떻게 표현해야할지 모르겠음

 - 쉽게 escape라는 boolean형 변수를 선언하여 true이면 종료되도록 처리

 

[트러블 슈팅]

1. 결과값 소수점 0 처리를 하고 싶다.

원인: 결과값을 저장하는 변수 타입을 double형으로 선언한 탓에 결과값을 조회할 경우 정수 뒤에 소수점.0 이 같이 출력되었다.

 

해결 방법

  • 소수점 처리할 수 있는 객체/메서드 활용 (BigDecimal)
  • (추가) 소수점 15자리 이상될 경우 비정상적인 값 출력 (부동소수점 때문인듯..) > 15자리 까지만 보이도록 함
// 소수점 처리 방법 적용 > BigDecimal 을 사용 > trailing zero 제거
// -> 소수점 뒤가 0인 것들을 안보이게 출력값에 제거
// 소수점 15자리까지 출력 > 소수점 15자리 초과 시 비정상적인 값으로 출력되어 소수점 자리 지정

BigDecimal bdresult = new BigDecimal(result).setScale(15, RoundingMode.HALF_UP);
bdresult = bdresult.stripTrailingZeros();
System.out.println(v1 + " " + operation + " " + v2 + " = " + bdresult);

 

1-1 (추가) 결과값의 1의 자리가 0으로 끝날 경우 지수(E)+@가 포함되어 값이 출력됨

  • 예시 입력값: value1: 95125 / operator: + / value2: 74155
  • 예상 출력값: 169280
  • 실제 출력값: 1.6928E+5
(수정 전) BigDecimal로 처리한 결과값(bdresult) 값 출력 시 지수(E)가 포함되어 있음

원인: BigDecimal.stripTrailingZeros() 을 사용할 경우 > 소수점을 모두 없앤 뒤 scale이 0보다 작을 경우 지수(E) 형태로 변환(과학적 표기법)으로 출력된다고 한다.

 > 실제 예시의 결과 값은 169280.000이며, 소수점이 없고, 오히려 10의 자리부터 시작하기 때문에 scale이 -1값이 된다.

 

해결 방법: toPlainString()를 사용하여 해결하였다.

  • BigDecimal.toPlainString(): BigDecimal 객체의 값을 지수(과학적 표기법) 없이 “있는 그대로” 출력해 주는 메서드
  • 쉽게 말하면 항상 일반 숫자 형태로 반환해주는 메서드라고 이해하면 될 것 같다.
(수정 후) .toPlainString() 코드 추가

 

 

[Java] double 형 값의 소수점 처리

double형은 실수형 자료형 타입을 말하며, 소수점이 있는 실수는 대체로 double형을 통해 값을 저장한다. 이 double 형 값을 출력할 경우 소수점이 출력되는데, 소수점 끝이 0인데, 0이 그대로 출력되

syuare.tistory.com


2. 프로그램 종료 여부를 위해 입력을 받아야 하는데, scanner.nextline(); 을 추가했음에도 따로 입력받지 않고 바로 반복문 처음으로 가는 문제가 있다.

 

원인: nextLine()은 말그대로 한줄 단위로 인식함.

  • enter를 입력할 경우 개행문자(\n)이 입력되고, 이는 한줄을 차지하는 것이기 때문에, nextLine()은 개행문자를 인식한다.
  • 정수(nextInt())를 입력받을 때 숫자 + enter를 입력함에 따라 개행문자(\n)도 함께 입력이 된다.
  • nextLine()은 개행문자(\n)도 인식이 되어, 이전에 입력한 개행문자(\n)이 그대로 남아있었고, 프로그램 종료 확인 여부를 입력할 때 nextLine()을 그대로 사용하다보니, 이전에 입력하여 남아있던 개행문자(\n) 그대로 입력받은 것으로 인식하여 아무것도 입력하지 않았음에도 바로 다음 코드를 실행하는 것으로 확인하였다.

해결 방법: 

  • 숫자일 경우 (숫자1,2 입력 시)
    • scanner.nextInt() 입력 받은 이후 scanner.nextLine(); 추가 & escapeKey를 입력받을 때는 next()만 사용하는 것으로 해결
  • 문자일 경우 (연산 기호, 종료 키워드 입력 시)
    • next() 메서드는 개행문자를 무시하고 입력을 받기 때문에, 공백 전까지만 입력 받는다.
  • 공통: nextLine()을 사용하여 내부 버퍼를 초기화할 수 있도록 코드 추가
 

[Java] next() vs nextLine()

Scanner를 사용할 때 문자열을 입력받고 싶을 때는 next()와 nextLine()을 사용한다 이 둘은 어떤 점이 다르고, 어떻게 사용해야할까? 결론부터 얘기하면 읽어드리는 단위와 동작 방식의 차이가 있어,

syuare.tistory.com


3. 정수 입력란에 정수가 아닌 문자 혹은 문자열을 입력할 경우 프로그램이 종료된다.

 

 hasNextInt()  사용으로 해결

  • 해당 오류는 "InputMismatchException" 으로, 입력한 값이 명시된 타입(int)와 매치가 되지 않아 발생하는 오류)
  • nextInt()는 입력값을 내부 버퍼에 저장하고, 바로 반환한다.
  • hasNextInt()는 입력값을 내부 버퍼에 저장까지만 하고, 반환을 하지 않는다.

이제 프로그램이 종료되지 않도록 이를 이용해서,

  • Scanner 객체의 메소드 호출 시 hasNextInt()로 입력값을 받아서
  • 제어문 if문으로 원하는 조건을 확인하고, true라면 nextInt()로 반환되도록 하여 해결하였다.

(참고) try–catch > 3-1 강의에서 예외 처리 방법을 try-catch로 사용했던 것 같은데, 이건 Lv2 할때 사용해보도록 하자

 

4. 연산 기혹 입력 중 첫글자는 연산기호인데, 이후 문자열이 같이 입력된 경우(예: +adfasf) 에도 계산 과정으로 넘어가는 문제가 있음

 

원인: 연산 기호를 입력받을 때 해당 값을 at 변수에 저장했는데, 저장값을 입력한 값의 첫 번째 문자(chatAt(0)만 저장하도록 하여 발생한 문제로 보임

char at = scanner.next().charAt(0);

 

기대 결과: 연산 기호를 정확히 입력해야 계산을 진행하도록 하며, 정확히 입력하지 않을 경우 정확한 값을 입력하도록 함

 

해결 방법: 

  • if문으로 입력받은 값의 길이를 확인 > 1인 경우에만 연산 기호 확인하는 if문으로 이동되도록 함
  • 연산 기호 입력은 1개의 기호만 받기 때문에 변수 operation 를 char형으로 선언함
  • 다만 입력 받을 때 next()로 입력받다보니 기호 + 문자열 값 까지 입력받게 되는 것 같음
  • 변수 operation 에 입력된 데이터 값의 길이가 1인 경우에만 연산 기호 확인하는 if문으로 진행되도록 처리

 

LV1 - 참고 문서


LV2. 클래스를 적용해 계산기 만들기

더보기

요구사항

1. 사칙연산을 수행 후, 결과값 반환 메서드 구현&연산 결과를 저장하는 컬렉션 타입 필드를 가진 Calculator 클래스를 생성

 사칙연산을 수행한 후, 결과값을 반환하는 메서드 구현
 연산 결과를 저장하는 컬렉션 타입 필드를 가진 Calculator 클래스를 생성
1) 양의 정수 2개(0 포함)와 연산 기호를 매개변수로 받아 사칙연산(➕,➖,✖️,➗) 기능을 수행한 후

2) 결과 값을 반환하는 메서드연산 결과를 저장하는 컬렉션 타입 필드를 가진 Calculator 클래스를 생성합니다.

 

2. Lv 1에서 구현한 App 클래스의 main 메서드에 Calculator 클래스가 활용될 수 있도록 수정

연산 수행 역할은 Calculator 클래스가 담당
연산 결과는 Calculator 클래스의 연산 결과를 저장하는 필드에 저장
소스 코드 수정 후에도 수정 전의 기능들이 반드시 똑같이 동작해야합니다.

 

3. main 메서드에서 Calculator 클래스의 연산 결과를 저장하고 있는 컬렉션 필드에 직접 접근하지 못하도록 수정 (캡슐화)

간접 접근을 통해 필드에 접근하여 가져올 수 있도록 구현합니다. (Getter 메서드)
간접 접근을 통해 필드에 접근하여 수정할 수 있도록 구현합니다. (Setter 메서드)
위 요구사항을 모두 구현 했다면 App 클래스의 main 메서드에서 위에서 구현한 메서드를 활용 해봅니다.

4. Calculator 클래스에 저장된 연산 결과들 중 가장 먼저 저장된 데이터를 삭제하는 기능을 가진 메서드를 구현한 후

    App 클래스의 main 메서드에 삭제 메서드가 활용될 수 있도록 수정

- 컬렉션에서 ‘값을 넣고 제거하는 방법을 이해한다.’가 중요합니다!


예외 처리 코드 수정 (if문 > try-catch)

  • Lv1에서는 if문으로 예외 처리 하던 것들을 Lv2에서는 체크 예외 호출(throw) + try-catch 코드로 수정
  • 예외 처리로 throw할 수 있는 명령어 종류 공부 필요
    • IllegalArgumentExpection, InputMismatchException, ArithmeticException

[트러블 슈팅]

 

1. 결과값을 저장할 수 있는 컬렉션(List)를 생성했는데 값을 double로 저장했더니 결과값 형식을 변경한 것이 반영되지 않음

  • 결과값 컬렉션 변수를 String으로 수정하여 해결하였다.

2. 컬렉션 필드에 직접 접근하지 못하도록 캡슐화가 필요

 

원인: getResultList 메서드 생성 > 결과값을 직접 접근하지 못하게 하고, 메서드를 호출해서 볼 수 있도록 하였는데, 생각해보니 retrun을 컬렉션 필드인 resultList를 그대로 반환할 경우 직접 접근하는 것이랑 다르지 않다고 생각하였다.

 

해결 방법

  • 새로운 변수 선언 > resultList값을 그대로 가진 새로운 컬력신 필드를 생성 > 이걸 반환하도록 하였다.
  • main 메서드에서 직접 해당 컬렉션에 값을 add할 수 없도록 add 메서드 생성 > 호출해서 사용하도록 하였다.

3. 갑자기 곱셉(*)을 할 경우 음수(-) 값으로 출력되는 문제

문제: 

  • 예시 : 4564564564 * 456456456 
  • 기대값: 20835249804990558...
  • 출력값: -1620125584

 

(테스트 할 때는 문제가 없었는데, 왜 그럴까?)

원인: 확인해보니 두 값의 연산이 int 범위를 넘어버림에 따라 overflow가 발생한 것이 원인

  • Integer.MAX_VALUE(범위): 2.1*10^9 까지
  • 위의 예시 계산값은 2.0847x10^14 의 값으로 범위를 훨씬 넘어가는 걸 확인할 수 있다.

32비트 정수 범위(int) 를 벗어난 결과값은 특정 형태로 잘려서 저장되며, 음수 비트 패턴을 가지게 되고, 이로 인해 결과값이 양수의 곱셈임에도 음수(-) 값을 가지게 되고 이걸 double 자료형 타입의 변수에 반환되어서 음수값으로 저장되는 것으로 보인다.

 

해결 방법:

int 범위를 확장하도록 하여 곱셈 시 (double) value1 * value2 로 수행하도록 하였다. (64비트 부동소수점 곱셈)

 

다른 해결 방법:

int보다 더 큰 정수 타입형인 long으로 입력값을 받는 value1, value2를 수정해주면 된다.

LV3: 심화된 기술을 활용하여 계산기 만들기 (Enum, Generic, Lambda&Stream)

더보기

1. 4가지 연산 타입 정보를 별도로 관리해보자

 enum 타입을 활용하여 구현하기

 

2. 실수(double) 타입의 값을 입력받아도 연산이 수행되도록 만들어보자

 generic을 활용하여 구현하기

 

3. 저장된 연산 결과 중 Scanner로 입력받은 값보다 큰 결과값들을 출력해보자 

- lambda & stream을 활용하여 구현하기

 


[트러블 슈팅]

1. generic을 선언 이후 계산 기능을 가진 클래스에서 계산해야하는 값을 가진 변수 value1, value2가 계속 오류가 발생

 

value1, value2가 generic으로 선언된 상황

generic은 자료형 타입을 알 수 없기 때문에, 산술 연산자인 +, -, *, / 가 조합될 시 오류가 발생하는 것으로 보임

 

해결 방법

value1, value2가 generic으로 선언되어 있지만 숫자 값이라는 것을 프로그램에 인식시킬 필요가 있음

클래스에 제네릭 선언할 때 와일드 카드를 사용

public class ArithmeticCalculator <T extends Number>

- generic으로 선언되어 있지만 T 타입에는 Number를 포함, 상속하는 Interger, Double, Long 등 타입이 사용 가능

- 객체나 메소드 호출 시 T는 위와 같은 지정된 타입으로 반환됨을 명시

 

계산 기능 클래스 인 public double calculate() 내 value1, valu2 값이 double 값임을 별도 명시

double val1 = value1.doubleValue(); // generic 으로 선언된 value1을 double형 변수 val1로 변환
double val2 = value2.doubleValue(); // generic 으로 선언된 value1을 double형 변수 val1로 변환

 

해결된 내용

 

 

[참고 문서 - 전문 문서/기술블로그/정리글 등 ]

더보기

 

예외 처리 

 

0407 JAVA - 예외처리

- Error(오류)와 Exception(예외사항) Error(오류) 프로그램이 실행될 수 없는 오류 프로그램이 실행중에 처리할 수 없음(코드를 고쳐야 실행 가능) ex.오타 Exception(예외사항) 프로그램 실행 중에 발생하

jiwon38.tistory.com

 


 

Enum

 

[Java] Enum

enum (열거형, eunmerated type)열거형: 서로 연관된 상수들의 집합enum은 자바 1.5부터 문법적으로 지원 시작했다.더보기enum은 어쩌다 등장 / 탄생하게 되었을까 (enum 탄생 배경) enum이 없을 때는 class 내

syuare.tistory.com


Generic

 

 

Difference between Bounded Type parameter (T extends) and Upper Bound Wildcard (? extends)

I know that there was a similar question already posted, although I think mine is somewhat different... Suppose you have two methods: // Bounded type parameter private static <T extends Number...

stackoverflow.com

 

 

[Java] Java Generic(제네릭)

이 글은 "자바 온라인 스터디"에서 공부한 내용을 작성하였습니다. Generic이란? Integer형 배열, String형 배열 등등 배열에 포함되는 원소의 타입마다 추가, 삭제, 정렬과 같은 함수를 정의하고 사용

math-coding.tistory.com

 

 

자바 [JAVA] - 제네릭(Generic)의 이해

정적언어(C, C++, C#, Java)을 다뤄보신 분이라면 제네릭(Generic)에 대해 잘 알지는 못하더라도 한 번쯤은 들어봤을 것이다. 특히 자료구조 같이 구조체를 직접 만들어 사용할 때 많이 쓰이기도 하고

st-lab.tistory.com

 

 

☕ 자바 제네릭(Generics) 개념 & 문법 정복하기

제네릭 (Generics) 이란 자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 객체별로 다른 타입의 자료가 저장될 수 있도록 한다. 자바에서 배

inpa.tistory.com


 

 

 

 

 

 

 

 

 


프로젝트 회고

더보기

Java를 배우고 첫 프로젝트.

 

강의 및 준비된 교재로 공부한 것을 토대로 프로젝트를 진행하다보니

기본 개념이 부족하다는 것을 절실히 느낀 프로젝트였네요.

 

강의를 들은 내용 만으로 프로젝트를 구현하기가 쉽지 않았고, 

예시 코드로만 봤을 때 간단해 보이는 것도 실제로 직접 코드를 작성해보니 많이 헤매었던 것 같습니다.

 

개념이 부족했다고 느꼈던 부분을 조금 정리하자면,

 

예를 들면..

  • 반복문 중 while문을 사용할 때 특정 조건을 만족하면 반복문을 종료하겠금 작성해야하는데 어떤 값을 조건으로 이용해서 만들어야 할까? 부터  고민을 많이 하게 된 것 같습니다.
  • 제어문도 if, switch 에 대한 차이를 자세하게 알기 어려웠었는데, 직접 코드를 작성해보니 차이점이 보이는 것도 개념이 부족했던 것이었겠죠.

처음에 머릿속에서는 계산기를 만들 때는 

1) 사용자한테 입력값을 받고 2)입력값을 계산해서 3) 계산 결과값을 출력

하면 되는 과정 정도이겠구나 정도로 생각하고 다이어그램을 한 번 만들어봐라 해서 만들어봤었는데, 처음에는 그냥 이런 그림이겠구나 정도로만 생각했었으나, 점점 구현한 코드가 길어지고 기능에 따른 클래스를 분리하게 되면서 이 흐름에 대해 진지하게 고민하게 되었던 것 같습니다.

 

실제로 Lv1 단계를 구현할 때는 단순히 코드만 작성하는 것으로도 크게 무리가 없었습니다.

오히려 요구사항 외 추가 기능들을 구현해보면 좋겠다 생각할 정도였으니 말이죠.

 

그러나 점차 하나의 클래스에 내용이 많아지게 되면서 코드 하나 하나 작성하는데 많은 고민이 들었던 것 같습니다.

특히 하나가 오류 발생되어서 수정을 하면 다른 곳이 연계되어 오류가 발생하는 걸 보았고, 해당 오류를 찾고 수정하려니까 클래스 내 작성된 코드, 기능이 많아서 어디가 문제, 문제를 해결하려면 어떤 코드를 수정해야할 지 등 파악하는데 쉽지 않았습니다. 그러다 클래스 분리의 필요성을 겪게 되었던 것 같습니다. 

그리고 캡슐화에 대해서도 고민을 많이 하게 된 것 같습니다. 

캡슐화를 했을 때 단순히 해당 클래스의 변수 값을 직접 접근하여 수정하지 못하도록 하는 정도로 생각했었는데, 코드를 작성하고 흐름을 고민하다보니 getter와 setter가 접근제어자로 캡슐화를 해둔 것들이 무용지물로 만들 수 있다는 것으로 귀결되었을 때 '캡슐화를 왜 하는 것이지?'에 대한 개념부터 고민하게 되었습니다.

그러다 이번 프로젝트를 하면서 깨달은 캡슐화의 핵심

  • 외부에서 내부 요소를 직접 접근하게 하는 것도 분명 주요 특성이겠지만,
  • 외부에서 내부 요소가 무엇이 있는지, 어떤 데이터가 있는지를 모르게 하고, 요소를 활용하고 싶으면 메서드를 통해 간접적으로 우회하여 이용하도록 하는 것

즉, 클래스 내부 요소에 대한 정보 은닉의 효과 라는 것을 알 수 있었던 것 같습니다.

 

또한, 컬렉션도 쉽지 않았던 것 같아요. 컬렉션을 어떻게 정의를 해야하는지 부터 활용까지 강의 내용만으로는 이해하는게 쉽지 않았고, 그러다보니 오랜 시간동안 처음 만들어두었던 다이어그램을 보면서 코드의 흐름을 좀 더 고민해봤던 것 같습니다.

처음에 그려보았던 클래스 다이어그램도 처음에는 그려보라 해서 아무런 의미 없이 그렸던 것이지만, 이제는 눈에 조금 들어왔고, 구현하기 전에 이러한 코드 구조라고 해야할까요? 사전 기획을 잘 해야 좋은 코드로 구현할 수 있겠구나 생각했습니다.

enum의 경우 강의 내용에 거의 다루지 않았어서, enum의 탄생부터 접근을 해보고, 어떻게 코드를 작성하고, enum을 어떻게 활용할 수 있는지 등 을 알아가는데만 해도 어려웠던 것 같습니다. (그래서 TIL로 정리)

generic(제네릭)도 타입을 아무거나 쓸 수 있는 만능형인 줄 알았는데, 이것을 활용하려니 쉽지 않았던 것 같습니다.

특히 입력받아 산술에 사용하는 변수를 제네릭 <T>로 선언했었을 때 발생한 오류에 대해 어떤 부분으로 파고들어야할 지 막막한 상황에, 프로젝트 제출 시간이 거의 다 되어가고 있다보니 조급해지는 마음이 컸던 것 같습니다.

다행히 얼마 남지 않은 시간이라 그런지 집중도가 평소보다 훨씬 높아져서 generic의 개념, 활용 방법에 대해 발을 담글 수는 있게 된 것 같았네요.

 

가장 아쉬운 건 Lambda와 Stream을 구현하지 못한 것이었습니다.

조금만 시간이 더 있었다면 구현해볼 수 있었을 것 같은데... 아쉬운 마음에 금주에 혼자 코드를 작성해보아야 겠습니다.

 

아무튼, 전체적으로 Java 기본이 매우 부족하구나를 절실히 느꼈던 프로젝트였습니다.

 

이 아쉬움은 다음 프로젝트을 통해 해소해야할 것 같고, 평소에 개념을 공부하는 것도 중요하지만 해당 개념을 어떻게 활용하는 지에 대해 고민해보고 실제 사용해보는게 중요하다는 것을 절실히 깨달았던 프로젝트 였습니다.