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
102 changes: 12 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,18 @@
# java-calculator

## **학습 목표**

- Git, GitHub, IDE 등 실제 개발을 위한 환경에 익숙해진다.
- 교육 분야에 맞는 프로그래밍 언어를 사용하여 간단한 문제를 해결한다.
# 문자열 덧셈 계산기
> 입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현하다.

---

## **미션 진행 방식**

### **진행 방식**

- 미션은 **과제 진행 요구 사항**, **기능 요구 사항**, **프로그래밍 요구 사항** 세 가지로 구성되어 있다.
- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
- **기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.**

### **미션 제출 방법**

- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다.
- GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고해 제출한다.

### **과제 제출 전 체크 리스트**

- 기능을 올바르게 구현했더라도 **요구 사항에 명시된 출력 형식을 따르지 않으면 0점**을 받게 된다.
- 기능 구현을 완료한 후 아래 가이드에 따라 모든 테스트가 성공적으로 실행되는지 확인한다.
- **테스트가 실패하면 점수가 0점**이 되므로 제출하기 전에 반드시 확인한다.

### **테스트 실행 가이드**

- 터미널에서 `java -version`을 실행하여 Java 버전이 21인지 확인한다. Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 21로 실행되는지 확인한다.
- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고, Windows 사용자의 경우 `gradlew.bat clean test` 또는 `.\gradlew.bat clean test` 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다.

```
BUILD SUCCESSFUL in 0s

```
## Tech Stack & Environment
* **Language:** Java 21
* **Framework:** Spring Boot 3.x
* **Build Tool:** Gradle
* **Test:** JUnit 5, AssertJ

---

# **문자열 덧셈 계산기**

## **과제 진행 요구 사항**
- **기능을 구현하기 전 `README.md`에 구현할 기능 목록을 정리**해 추가한다.
- Git의 커밋 단위는 앞 단계에서 `README.md`에 정리한 기능 목록 단위로 추가한다.
- [AngularJS Git Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)을 참고해 커밋 메시지를 작성한다.
- 자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.

## **기능 요구 사항**

입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다.

- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
- 예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6
- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
- 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.

### **입출력 요구 사항**

### **입력**

- 구분자와 양수로 구성된 문자열

### **출력**

- 덧셈 결과

```
결과 : 6

```

### **실행 결과 예시**

```
덧셈할 문자열을 입력해 주세요.
1,2:3
결과 : 6

```

## **프로그래밍 요구 사항**

- JDK 21 버전에서 실행 가능해야 한다.
- 프로그램 실행의 시작점은 `Application`의 `main()`이다.
- `build.gradle` 파일은 변경할 수 없으며, **제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.**
- 프로그램 종료 시 `System.exit()`를 호출하지 않는다.
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 [Java Style Guide](https://github.com/woowacourse/woowacourse-docs/blob/main/styleguide/java)를 원칙으로 한다.

### **라이브러리**

- `camp.nextstep.edu.missionutils`에서 제공하는 `Console` API를 사용하여 구현해야 한다.
- 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.
## Key Features
* **커스텀 구분자 분석:** `//[구분자]\n[본문]` 형식의 문자열 패턴을 정규식으로 검증 및 추출
* **기본 구분자 분석:** `[숫자][:or,][숫자][:or,]...` 형태의 패턴을 정규식으로 검증 및 추출
* **도메인 중심 설계:** 비즈니스 검증 및 가공 로직을 `Domain` 객체 내부에 응집
* **안전한 데이터 전송:** 계층 간 이동 시 `Static Inner Class` 기반의 DTO 구조 채택
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
8 changes: 3 additions & 5 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/main/java/calculator/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package calculator;

import calculator.controller.CalculatorController;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
CalculatorController calculatorController = new CalculatorController();
calculatorController.run();
}
}
17 changes: 17 additions & 0 deletions src/main/java/calculator/controller/CalculatorController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package calculator.controller;

import calculator.service.CalculatorService;
import calculator.view.*;
import static calculator.view.InputView.*;

public class CalculatorController {

public void run() {
String input = receiveInput();

CalculatorService calculatorService = new CalculatorService();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run() 할 때만 Service를 주입하는 것 보다는 다른 곳에도 사용할 수 있기 때문에, Controller가 직접 가지고 있는 방식은 어떻게 생각하시나요?

public class CalculatorController {
    
    private final CalculatorService calculatorService;

    public CalculatorController(CalculatorService calculatorService) {
        this.calculatorService = new CalculatorService;
    }
    
    ...
}

@kimse8699 kimse8699 Jun 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인자로 받은 calculatorService가 아닌 새로운 객체를 생성해서 넣는 이유는 뭔가요?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run()이라는 메서드가 생성될 때마다 주입된다면 "객체 생성 비용"이 매 순간 발생하기 때문에 Controller가 생성될 때만 주입되는게 유리하다고 생각해서 입니다.

추가적으로 run()이라는 메서드에서도 말고 새로운 메서드에서 사용될 때에도 사용될 수 있기 때문입니다.

int result = calculatorService.calculateInput(input);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 느끼는 일반 MVC에서는 Controller에서는 흐름을 조합하는 곳이라고 생각합니다.

그래서 어찌보면 calculatoeInput이라는 메서드 네이밍이 어떤 역할을 하는지 잘 이해가 되지않습니다.

현재 서비스를 만들고 있는데, 현재와 같이 작은 규모의 도메인 정도면 Service 없이 진행해보는 것이 더 좋을 것 같습니다.

왜냐하면 이렇다할 비즈니스 규칙이 없다보니 컨트롤러에서 비즈니스 흐름을 다 조합해보는 것도 좋은 방법일 것 같습니다.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MVC에서 Controller 계층과 Service 계층의 관계에 대한 역할 구분이 명확하지 않아서 calculateInput 이라는 네이밍이 나온 것 같습니다. 수정하겠습니다.

제가 공부한 바로는 Service 계층은 서비스 시나리오를 Controller 계층은 외부 통신 흐름 제어의 역할을 맡는다고 알고있습니다. Service 계층이 필요없는건 이해가 가지만 Controller에서 조합한다면 역할이 애매해지지 않을까 싶습니다.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어찌보면 현재 계산기 도메인 내에서는 계산하다라는 비즈니스 로직 하나 즉 서비스 시나리오가 하나 밖에 없기 떄문에, Service를 생성하는 비용이 더 들 것 같다고 생각합니다.

그래서 아싸리 Controller에서만 하도록 하는 것도 좋다고 생각합니다.


OutputView.showResult(result);
}
}
55 changes: 55 additions & 0 deletions src/main/java/calculator/domain/Delimiter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package calculator.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public class Delimiter {

private final String delimiter;
private final Expression expression;

public Delimiter(String input) {
expression = new Expression(input);
if (hasCustomDelimiter()) {
delimiter = expression.getCustomDelimiter();
return;
}
delimiter = null;
}

public boolean hasCustomDelimiter() {
if (expression.isCustomDelimiter()) {
return true;
}
if (expression.isDefaultDelimiter()) {
return false;
}
throw new IllegalArgumentException("올바르지 않은 입력 형식입니다.");
}

public List<Number> parseAsCustomDelimiter(String input) {
String body = expression.getBody();
String[] numbers = body.split(Pattern.quote(delimiter));

List<Number> numberList = new ArrayList<>();
for (String number : numbers) {
Number num = new Number(Integer.parseInt(number));
numberList.add(num);
}
return numberList;
}

public List<Number> parseAsDefaultDelimiter(String input) {
List<Number> numberList = new ArrayList<>();
String[] restResult = input.split(",");
for (String rr : restResult) {
Comment thread
jyt6640 marked this conversation as resolved.
String[] colonResult = rr.split(":");
for (String cr : colonResult) {
Number number = new Number(Integer.parseInt(cr));
numberList.add(number);
}
}
return numberList;
}
}
36 changes: 36 additions & 0 deletions src/main/java/calculator/domain/Expression.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package calculator.domain;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Expression {

private static final Pattern customRegex = Pattern.compile("^//(.)\\\\n(([1-9][0-9]*)(\\1[1-9][0-9]*)*$)");
private static final Pattern defaultRegex = Pattern.compile("([1-9][0-9]*)([, :][1-9][0-9]*)*");

private final String input;
Comment thread
jyt6640 marked this conversation as resolved.

public Expression(String input) {
this.input = input;
}

public boolean isCustomDelimiter() {
return Pattern.matches(String.valueOf(customRegex), input);
}

public boolean isDefaultDelimiter() {
return Pattern.matches(String.valueOf(defaultRegex), input);
}

public String getCustomDelimiter() {
Matcher matcher = customRegex.matcher(input);
matcher.matches();
return matcher.group(1);
}

public String getBody() {
Matcher matcher = customRegex.matcher(input);
matcher.matches();
return matcher.group(2);
}
}
13 changes: 13 additions & 0 deletions src/main/java/calculator/domain/Number.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package calculator.domain;

public class Number {
private final int num;

public Number(int num) {
this.num = num;
}

public int addTo(int sum) {
return sum += num;
}
}
15 changes: 15 additions & 0 deletions src/main/java/calculator/domain/Numbers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package calculator.domain;

import java.util.ArrayList;
import java.util.List;

public class Numbers {

List<Number> numbers = new ArrayList<>();

public void ReturnNumbers(String list) {

}


}
29 changes: 29 additions & 0 deletions src/main/java/calculator/service/CalculatorService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package calculator.service;

import calculator.domain.Delimiter;
import calculator.domain.Number;

import java.util.List;

public class CalculatorService {

public int calculateInput(String input) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller에서도 말했듯이 여러 역할을 담당하고 있는 것이라고 느껴집니다.

기능을 좀 더 분리해볼 수 없을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드를 작성할 당시에 service 계층의 역할을 잘못 이해하고 있었습니다. service 계층을 없애고 domain 계층에서 아예calculator 라는 내부 로직을 조립하는 도메인을 만드는건 어떻게 생각하시나요?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 오히려 좋다고 생각합니다.

calculator라는 도메인 내에서 조립해서 사용한다면 더욱 "객체지향"에 가깝지 않을까 생각합니다.

Delimiter delimiter = new Delimiter(input);
Comment thread
jyt6640 marked this conversation as resolved.

if (delimiter.hasCustomDelimiter()) {
List<Number> numberList = delimiter.parseAsCustomDelimiter(input);
return addNum(numberList);
}
Comment thread
jyt6640 marked this conversation as resolved.

List<Number> numberList = delimiter.parseAsDefaultDelimiter(input);
return addNum(numberList);
}

public int addNum(List<Number> numberList) {
int result = 0;
for (Number number : numberList) {
result = number.addTo(result);
}
return result;
}
}
11 changes: 11 additions & 0 deletions src/main/java/calculator/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package calculator.view;

import static camp.nextstep.edu.missionutils.Console.readLine;

public class InputView {

public static String receiveInput() {
System.out.println("덧셈할 문자열을 입력해 주세요.");
return readLine();
}
}
9 changes: 9 additions & 0 deletions src/main/java/calculator/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package calculator.view;

public class OutputView {

public static void showResult(int result) {

System.out.println("결과 : " + result);
}
}
22 changes: 22 additions & 0 deletions src/test/java/calculator/domain/ExpressionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package calculator.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ExpressionTest {

@Test
@DisplayName("matcher.group(1)이 뭐가 나오는지 테스트")
void 뭐가_나오는지_보자() {
// Given
String input = "//;\\n1;2;3";

// When
Expression expression = new Expression(input);
String customDelimiter = expression.getCustomDelimiter();
System.out.println(customDelimiter);
}

}