Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 200 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,200 @@
# java-string-calculator4
# 문자열 계산기

### 요구사항
- 사용자가 입력한 문자열 값에 따라 사칙연산을 수행할 수 있는 계산기를 구현
- 사칙연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다.
즉, 수학에선 곱셉,나눗셈이 덧셈,뺄셈 보다 먼저 계산해야 하지만 이를 무시한다.
- 예를 들어 "2 + 3 * 4 / 2"와 같은 문자열을 입력할 경우 2 + 3 * 4 / 2 실행결과인 10을 출력해야 한다.

---

# 프로젝트 요약

### 목표
⭐️ 최대한 객체지향 ⭐️

### 사용 기술

#### 1. 객체지향 설계
- 책임 분리
- 각 클래스가 하나의 역할만 가짐

| 클래스 | 책임 |
| ---------------------- | ---------- |
| `InputProcessor` | 입력 |
| `FormulaSplitter` | 문자열 파싱 |
| `StringCalculator` | 계산 로직 |
| `Operator` | 연산자 행위 캡슐화 |
| `OutputProcessor` | 출력 |
| `StringCalculatorMain` | 흐름 제어 |

#### 2. enum을 이용한 다양성
- enum 상수별 메서드 오버라이딩
- 조건문 if,switch 제거

#### 3. 캡슐화
- "private final String message;" 연산자 기호를 외부에서 직접 접근 불가
- 연산자 변환은 반드시 "conversion()" 을 통해서만 가능

#### 4. 문자열 파싱
- "formula.split(" ")"

#### 5. 예외 처리 설계
- catch (NumberFormatException e) : 숫자 문제
- catch (ArrayIndexOutOfBoundsException e) : 계산식 구조 문제

#### 6. 순차 계산 로직 구현
- "for (int i = 1; i < values.length; i += 2)" : 입력 순서대로 계산

---

# 설계 및 구조

### UML
- UML은 Unified Modeling Language의 약자로, 통합 모델링 언어입니다.
- 모델을 만들고 설명하는 표준 언어(약속)
- UML을 알아야 하는 이유
- 다른 사람들과의 협업에 필요합니다.
UML 형식을 차용해 의사소통하면, 의미가 명확하고 설계에 대한 논의가 순조롭게 이루어질 수 있습니다.
- 전체 시스템의 구조와 클래스의 의존성을 파악합니다.
다이어그램을 분석하면, 시스템의 구조는 물론 클래스 간의 의존성도 파악하기 쉽습니다.
- 유지보수를 위한 백엔드 문서로 사용합니다.
위의 항목과 이어지는 방법입니다. 구조적으로 효율적이지 않거나, 모듈화 또는 구체화해야하는 작업이 필요하다고 생각한다면 UML을 먼저 작성해보고 구조를 수정하는 것이 좋겠습니다.

- UML 다이어그램은 구조 다이어그램과 행위 다이어그램으로 나누어 집니다.

- 구조 다이어그램 (Structure Diagram) 은 각 요소들의 정적인 면을 보기 위한 다이어그램입니다.
따라서 시스템의 개념, 관계 등의 측면에서 요소들을 나타냅니다.
클래스 다이어그램도 구조 다이어그램에 속합니다.
- 행위 다이어그램 (Behavior Diagram) 은 요소들의 동적인 면을 보기 위한 다이어그램입니다.
시퀀셜한 표현을 위한 다이어그램이라고 설명하기도 합니다.


### 클래스 다이어그램

```plaintext
+------------------------+
| StringCalculatorMain |
+------------------------+
| + main(String[]):void |
+-----------+------------+
|
| uses
v
+------------------------+ +------------------------+
| InputProcessor | | FormulaSplitter |
+------------------------+ +------------------------+
| - scanner: Scanner | | |
+------------------------+ +------------------------+
| + input(): String | | + split(String):String[]|
+------------------------+ +------------------------+

+------------------------+
| StringCalculator |
+------------------------+
| |
+------------------------+
| + calculate(String[]): |
| int |
+-----------+------------+
|
| uses
v
+------------------------+
| <<enum>> Operator |
+------------------------+
| - message: String |
+------------------------+
| + conversion(String) |
| # operation(int,int) |
+------------------------+

+------------------------+
| OutputProcessor |
+------------------------+
| |
+------------------------+
| + output(int): void |
+------------------------+
```



### 의존성 흐름

```plaintext
StringCalculatorMain
├─ InputProcessor
├─ FormulaSplitter
├─ StringCalculator
│ └─ Operator
└─ OutputProcessor
```

- Main이 모든 객체를 조립하고 입력 → 파싱 → 계산 → 출력 흐름으로 의존성이 단방향으로 흐르도록 설계 했습니다.
- 연산 로직은 enum 다형성을 통해 캡슐화했습니다.
- 단방향 흐름 장점
- 코드 가독성,유지보수 유리
- 변경 영향 범위가 적음
- 리팩토링,확장에 유리


---

# 고민의 흔적

- 이건 그냥 두서없이 제 생각들을 적어보겠습니다.
- 문자열 계산기란 무엇인지 생각 해봤을때 사용자가 "2 + 3 * 4 / 2" 라는 입력을 하면 일반 계산기는 숫자를 그대로 입력받아 계산을 해주지만 문자열 계산기는 숫자나 연산자들을 문자열로 받아 계산을 합니다.
그래서 입력받은 문자열을 숫자로 변환해서 계산 해주어야하고 계산 조건인 기본 사칙연산이 아니라 입력받은 순서대로 계산을 해야 합니다.

(1). 구조자료를 배열로 선택한 이유는 문자열 계산기의 계산 조건인 입력된 순서대로 연산을 해야되서 사용자의 계산식이 입력 되었을때 그 식을 배열로 생성해 for문을 사용해서 인덱스 0번부터 차례대로 계산을 해야된다고 생각했습니다.
그리고 숫자와 연산자들을 구분하기엔 배열의 인덱스를 통해 접근하기가 가장 쉽다고 생각했습니다.

(2). for문을 사용할 때 숫자와 연산자들을 따로 구분해야합니다.
배열의 인덱스가 짝수는 연산자, 홀수는 숫자이기 때문에 문자열을 반환할 때 2라는 문자열은 숫자로 변환, +라는 문자열은 enum 상수로 변환을 해야해서 for문에선 두가지 작업을 하게 됩니다.
⭐️ 계산기에서 인덱스 0번은 무조건 숫자가 입력되기 때문에 for문에서 int i = 0으로 시작하게 되면 아무런 작업을 할 수가 없습니다.
그래서 인덱스 0번을 기준값(시작상태)을 정해두고 인덱스 1번(홀수)인 연산자부터 반복문을 실행해야합니다.
쉽게 말하자면 계산식은 "숫자 연산자 숫자"가 되어야하는데 숫자부터 실행이 된다면 계산을 시작할 수가 없습니다.
그래서 초기값 int number = Integer.parseInt(values[0])을 설정했습니다. ⭐️

(3). for루프 안
Operator operator = Operator.conversion(values[i])
Operator의 conversion 메서드는 입력받은 문자열의 연산자가 enum 상수와 같은지 확인을 해주는 메서드 입니다.
conversion 메서드가 Operator안에 있는 이유는 Operator가 자기 역할에 대한 책임을 스스로 지게 하기 위해서 입니다.
conversion 메서드의 반환타입을 Operator로 설계했기 때문에 연산자 문자열을 받게되면 enum 상수를 바로 호출해 비교할 수 있습니다.

(4). number = operator.operation(number, nextNumber);
enum 상수가 담겨있는 operator에 operation 메서드를 호출 했습니다.
operation 메서드는 int a, int b가 매개변수로 있고 여기에 number(기준값)와 nextNumber를 넣었습니다.

(5). abstract int operation(int a, int b)
operation을 추상 메서드로 설계한 이유는 enum 상수들이 이 메서드를 오버라이딩해 각 연산자마다 다른 연산을 재정의 할 수 있게 하기 위해서 입니다.
이렇게 메서드를 선언해 놓으면 호출하는 입장에선 +를 호출하면 enum에서 연산 동작을 하기때문에 다형성 확보와 불필요한 if문,switch문을 제거할 수 있습니다.
또, 새로운 enum상수를 추가 했을때 반드시 오버라이딩을 해야하는 적절한 규약이 있어 타입 안전성이 높아집니다.

(6). 입력, 출력, 문자열 분리, 계산 로직을 각각 별도의 클래스로 분리하여 각 클래스가 하나의 책임만 가지도록 설계했습니다.
객체지향의 단일 책임원칙을 충족했고 유지보수와 확장에 유리하고 변경사항이 있을 때 최소화 할 수 있습니다.

(7). 예외 처리
- 입력값이 null이거나 ""같은 공백일 때
- 0으로 나누었을 때
- 숫자가 아닌 문자열을 입력했을 때
- 지원하지 않는 연산자를 입력했을 때
- 연산자 뒤에 숫자를 입력하지 않았을 때

---

# 회고

### 1. 가장 어려웠던 점


- 사칙연산의 기본인 "숫자 연산자 숫자"가 되어야 두 값을 연산할 수 있는데 반복문을 어떻게 사용할지가 생각이 나지 않았습니다.
- for문에서 인덱스 0번을 기준값으로 잡아놔야 한다는 생각을 하기가 힘들었습니다.
- enum 상수를 사용할 때 메서드 오버라이딩이란 생각을 하지 못했습니다.
- 해결과정들은 위의 고민의 흔적에 작성했습니다.

### 2. 다시 돌아간다면 어디서 시간을 줄일 것인가

- 가장 오래 걸렸던 반복문 사용방법에 시간을 줄여야 할 것 같습니다.
- 인덱스 0번을 기준으로 잡고 연산자부터 시작한다. 근데 이거 진짜 내가 빡대가리라서 생각을 못한거는 맞는데 다른 사람들은 검색도 없이 그냥 자기 생각만으로 하는거임??
11 changes: 11 additions & 0 deletions src/stringCalculator/FormulaSplitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package stringCalculator;

public class FormulaSplitter {

public String[] split(String formula) {
if (formula == null || formula.isBlank()) {
throw new IllegalArgumentException("계산식이 비어있거나 null입니다.");
}
return formula.split(" ");
}
}
12 changes: 12 additions & 0 deletions src/stringCalculator/InputProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package stringCalculator;

import java.util.Scanner;

public class InputProcessor {
private final Scanner scanner = new Scanner(System.in);

public String input() {
System.out.print("계산식을 입력하세요(예:2 + 3 * 4 / 5): ");
return scanner.nextLine();
}
}
48 changes: 48 additions & 0 deletions src/stringCalculator/Operator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package stringCalculator;

public enum Operator {
PLUS("+") {
@Override
int operation(int a, int b) {
return a + b;
}
},
MINUS("-"){
@Override
int operation(int a, int b) {
return a - b;
}
},
MULTIPLY("*"){
@Override
int operation(int a, int b) {
return a * b;
}
},
DIVIDE("/"){
@Override
int operation(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
}
return a / b;
}
};

private final String message;

Operator(String message) {
this.message = message;
}

abstract int operation(int a, int b);

public static Operator conversion(String message) { //입력받은 연산자로 enum 찾기
for (Operator op : Operator.values()) {
if (op.message.equals(message)) {
return op;
}
}
throw new IllegalArgumentException("지원하지 않는 연산자: " + message);
}
}
8 changes: 8 additions & 0 deletions src/stringCalculator/OutputProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package stringCalculator;

public class OutputProcessor {

public void output(int number) {
System.out.println("결과: " + number);
}
}
31 changes: 31 additions & 0 deletions src/stringCalculator/StringCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package stringCalculator;


public class StringCalculator {

private int parseNumber(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("숫자가 아닌 값이 포함되어 있습니다." + e);
}
}

public int calculate(String[] values) {
try {
int number = parseNumber(values[0]);

for (int i = 1; i < values.length; i += 2) {
Operator operator = Operator.conversion(values[i]);
int nextNumber = Integer.parseInt(values[i + 1]);

number = operator.operation(number, nextNumber);
}
return number;
} catch (ArrayIndexOutOfBoundsException e) {
throw new IllegalArgumentException("잘못된 계산식입니다. 연산자와 숫자를 올바르게 입력하시오." , e);
}
}
}


16 changes: 16 additions & 0 deletions src/stringCalculator/StringCalculatorMain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package stringCalculator;

public class StringCalculatorMain {
public static void main(String[] args) {
StringCalculator stringCalculator = new StringCalculator();
InputProcessor inputProcessor = new InputProcessor();
OutputProcessor outputProcessor = new OutputProcessor();
FormulaSplitter formulaSplitter = new FormulaSplitter();

String formula = inputProcessor.input();
String[] tokens = formulaSplitter.split(formula);

int result = stringCalculator.calculate(tokens);
outputProcessor.output(result);
}
}
Loading