Post

우테코 7기 프리코스 1주차 회고

🎬 Intro

우테코 프리코스 1주차 회고를 적어봅니다~

1️⃣ 요구 사항 파악하기

요구 사항들을 읽어보면서 눈에 들어왔던건 총 4가지였습니다. 🥸


✅ 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

  • 해당 조건 덕분에 여러 예외 상황을 고민해볼 수 있었습니다

✅ AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.

  • 커밋 메세지를 쓸때는 type만 적었었는데 해당 컨벤션은 scope도 명시할 것을 권장하고 있었습니다.
    • type 만 명시 : feat: 로그인 로직 구현 완료
    • type & scope 명시 : feat(Login, Member): 로그인 로지 구현 완료
  • scope를 함께 명시하면 더욱 명확하게 작업 내용을 전달할 수 있다는걸 알게 되었습니다.
  • 이외 CHANGELOG.md도 알게 되었는데, 프로젝트의 커밋 로그를 type 및 scope 별로 변경내역을 한눈에 확인할 수 있는 파일이었습니다.
  • 미션에 적용해보고싶어서 generate-changelog라는 CHANGELOG.md 자동생성 tool을 설치하였습니다.

✅ JDK 21 버전에서 실행 가능해야 한다.

  • 이번 기회에 JDK 21도 설치해보았습니다~

✅ 자바 코드 컨벤션은 Java Style Guide를 원칙으로 한다.

  • 감사하게도 크루원 중 한분께서 커뮤니티에 인텔리제이 자바 코드 컨벤션과 우테코 코드 스타일을 설정하는 정보를 공유해주셔서 손쉽게 설정할 수 있었습니다 ㅎㅎ

2️⃣ 문제 분석하기

기본 구분자와 커스텀 구분자를 기준으로 문자열을 파싱하여 합 연산을 하는 로직을 구현하는 것이었습니다.


✅ 커스텀 구분자 자리수 정하기

  • 여러 자리수의 커스텀 구분자를 사용할 수 있도록 하였습니다.

✅ 커스텀 구분자 빈문자열 허용 여부

  • 빈문자열을 허용할 경우 사용자에게 혼돈을 줄 것이라 판단하여 허용하지 않도록 하였습니다.

✅ 커스텀 구분자 파싱하는법 정하기

  • 처음에는 문자열에 인덱스로 접근하는 로직을 떠올렸습니다. 하지만 추후에 커스텀 구분자를 설정하는 패턴이 변경되게 되면 수정이 많이 일어날것 같다는 생각이 들었습니다.
  • 그래서 유지보수 용이성, 가독성 등을 고려하여 정규표현식을 사용해 파싱하는걸로 결정하였습니다.

✅ 예외상황 생각하기

  • 커스텀 구분자 설정 //;\n1;2;3
  • 입력은 구분자와 양수로 구성된 문자열

이 2가지를 요구 사항을 기반으로 예외상황들을 판단해보았습니다.

  • 문자열에 숫자 와 구분자외에 다른 문자가 존재할 경우
  • 숫자가 양의 정수가 아닐 경우
  • 숫자가 Integer.MAX_VALUE를 초과할 경우
  • 커스텀 구분자 설정 패턴이 일치하지 않을 경우
  • 커스텀 구분자 설정 패턴이 맨앞에 위치하지 않을 경우
  • 커스텀 구분자가 빈문자열일 경우

이렇게 총 6가지 정도의 예외상황을 정의하였습니다.

3️⃣ 기능 구현 목록 작성

기능 요구 사항은 비개발자가 읽어도 이해하기 쉽게끔 작성할려고 노력하였습니다.


✅ 진행 과정

1. 사용자에게 문자열을 입력 받는다.

  • 입력은 구분자와 양수로 구성된 문자열만 가능하다.
    • 기본 구분자 : 쉼표(,) 또는 콜론(:)
    • 커스텀 구분자 : 문자열 앞부분의 //\n 사이에 위치하는 문자
    • 예시 : "", 1,2, 1,2:3, //;\n1;2;3, 1,3:4
  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException가 발생하고 애플리케이션이 종료 된다.
    • 양수가 아닌 경우 : -1,2,3, a,1,2
    • 기본 구분자외에 커스텀하지 않은 구분자를 사용한 경우 : 1;2;3
    • 커스텀 구분자를 잘못사용한 경우 : /;\n, \\;\n, /;\a, n//;\

2. 입력 받은 문자열을 구분자 기준으로 분리한 각 숫자을 반환한다.

  • 예시 : "" => 0, //;\n1;2;3 => 6, 1,3:4 => 8

3. 실행 결과 예시

1
2
3
덧셈할 문자열을 입력해 주세요.
1,2:3
결과 : 6
1
2
3
덧셈할 문자열을 입력해 주세요.
//;\n3,2;3
결과 : 8
1
2
3
덧셈할 문자열을 입력해 주세요.
//;\n,2;3
결과 : 5

✅ 클래스 및 기능 구현 목록

1. domain

Numbers

사용자에게 입력 받은 숫자를 저장하고, 관련 기능을 수행

  • 숫자 배열의 사이즈를 반환
  • 인덱스에 따라 숫자 배열의 요소를 반환
Calculator

숫자 배열의 연산을 수행

  • 숫자 배열의 합을 계산하여 반환

2. util

InputStringConvertor

사용자에게 입력 받은 문자열을 숫자 배열로 변환

  • 구분자를 사용하여 문자열을 양의 정수 배열로 변환
  • StringConvertor 구현체
InputStringParser

사용자에게 입력 받은 문자열을 파싱

  • 커스텀 구분자를 추출하여 반환
  • 커스텀 구분자 형식을 제거
  • StringParser 구현체
InputStringValidator

사용자에 입력 받은 문자열을 검증

  • 문자열 입력 형식이 올바른지 검증
  • 양의 정수의 범위안에 숫자인지 검증
  • StringValidator 구현체

3. view

InputView

사용자가 입력한 값을 읽음

  • 사용자가 입력한 값을 읽어 문자열로 반환
OutputView

사용자에게 메세지 및 결과를 출력

  • 요청 메세지 출력
  • 연산 결과값 출력

4.controller

CalculatorController

애플리케이션의 흐름을 제어

  • 사용자에게 문자열을 요청
  • 사용자에게 연산 결과를 반환

5. constant

Delimiter

기본 구분자와 커스텀 구분자 형식을 정의

  • 기본 구분자 : ,, ;
  • 커스텀 구분자 형식 : //커스텀 구분자\n
ErrorType

에러 타입을 정의

  • 올바르지 않은 입력 형식 (INVALID_INPUT_FORMAT)
  • 양의 정수 범위를 벗어난 숫자 존재 (OUT_OF_RANGE)
NumberRange

허용 가능한 숫자 범위를 정의

  • 최소 : 1
  • 최대 : 2,147,483,647 (Integer.MAX_VALUE)

4️⃣ 구현하기

미션을 단순한 알고리즘 문제 풀이가 아닌 하나의 서비스를 만든다는 관점으로 바라보았습니다. 그래서 다형성, DI등을 활용하여 객체지향적으로 코드를 짤려고 노력하였습니다. 💪

전체 코드


✅ 클래스 다이어그램

Main

img.png

Constant

img.png

✅ 신경쓴 점

객체지향적으로 코드를 구현할려고 노력하였습니다~ 🥸

  • DIP를 위해 인터페이스를 사용하였습니다. 각 객체는 구현체가 아닌 인터페이스를 참조하도록 하였습니다.
  • 제네릭을 사용하여 타입 안정성과 재사용성을 높였습니다.
  • SRP 원칙대로 각 객체는 단 하나의 책임만 갖도록 하였습니다.
  • 각 메서드는 한가지 일만 하도록 구현하였습니다.
  • DI를 활용하여 테스트를 유연하게 하고 객체 간 결합도를 낮췄습니다.
  • 유지보수 용이성과 가독성을 높이기 위해 상수화를 진행하였습니다.
  • 컨트롤러의 코드 복잡성을 줄이기 위해 Facade 패턴을 적용하였습니다.

5️⃣ 트러블 슈팅

✅ NumberFormatException 해결

문제 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private boolean isOutOfRange(final String str) {
    final String[] tokens = stringParser.extractTokens(str);
    return Arrays.stream(tokens)
            .map(BigInteger::new)
            .anyMatch(num -> num.compareTo(MIN) < 0 || num.compareTo(MAX) > 0);
}


@Override
public List<Integer> convertToListWithDelimiter(final String str, final String delimiter) {
    final String[] tokens = str.split(delimiter);
    return Arrays.stream(token)
            .map(Integer::parseInt)
            .toList();
}
  • 사용자가 빈문자열을 입력할 경우 tokens가 빈배열이 되고 map이 실행 안될 줄 알았습니다.
  • 하지만 "" 형태의 빈문자열이 요소로 들어가 있었습니다.
  • 구분자에 빈문자열이 없기 때문에 split시 ""이 제거 되지 않았고,따라서 빈문자열을 숫자 타입으로 변환을 시도하여 NumberFormatException이 발생하였습니다.

개선 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 private boolean isOutOfRange(final String str) {
    final String[] tokens = stringParser.extractTokens(str);
    return Arrays.stream(tokens)
            .filter(token -> !token.isEmpty())
            .map(BigInteger::new)
            .anyMatch(num -> num.compareTo(MIN) < 0 || num.compareTo(MAX) > 0);
}

@Override
public List<Integer> convertToListWithDelimiter(final String str, final String delimiter) {
    final String[] tokens = str.split(delimiter);
    return Arrays.stream(tokens)
            .filter(token -> !token.isEmpty())
            .map(Integer::parseInt)
            .toList();
}
  • 스트림에 filter를 추가하여 빈문자열이 아닌 경우에만 map이 실행되도록 하였습니다.

6️⃣ 학습하고 사용한 것들

✅ 정규표현식

정규표현식은 특정 패턴을 통해 문자열을 파싱할 수 있게 해줍니다.

왜 사용했는가??

1. 가독성

정규표현식을 사용할 경우 요구되는 문자열의 패턴을 명확하게 표현할 수 있기 때문에, 코드의 목적과 의도를정확하게 전달할 수 있다고 판단했습니다.

2. 유지보수 및 확장성

요구되는 문자열의 패턴이 추가 또는 변경되었을때, 정규표현식을 업데이트하면 되므로 수정에 유연하게 대처할 수 있을것이라 생각하였습니다.

3. 에러 핸들링 용이

인덱스를 사용할 경우 인덱스 관련 예외도 처리해야 합니다. 반면에 정규표현식은 패턴에만 집중하면 되므로 예외를 핸들링하는데 더 수월할 것이라 생각했습니다.

정규표현식 적용

1
public static final String CUSTOM_DELIMITER_FORMAT = "^//(.+)\\\\n";
  • 첫번째 문자열이 //구분자\n 형식과 일치하는지 확인하는 정규표현식 입니다.
  • Java에서는 "\"를 표현하기 위해서는 "\"를 사용하여 이스케이프가 필요합니다. 따라서 문자 "\""\\"로 표현됩니다.
  • 정규표현식에 \n은 줄바꿈 패턴을 나타냅니다. 따라서 "\" 문자를 사용하여 이스케이프를 해줘야합니다. 따라서 \n 문자패턴은 \\\\n 로 표현됩니다.
  • +는 앞의 정규표현식에 해당하는 문자가 1개 이상 있다는것을 나타냅니다.
  • ^ 문자열의 시작을 나타냅니다.
  • . 는 하나의 문자를 나타냅니다.
  • ()는 캡처를 의미 합니다. 해당 괄호안에 있는 문자열을 매칭한 결과로 저장할 수 있습니다.
  • 즉, 위 정규표현식은 문자열이 //구분자\n 형식으로 시작하는지 확인하고 구분자를 매칭을 의미 합니다.
  • 따라서 //;;a\n1,2,3 , // \n1:2:3 과 같은 문자열이 매칭 됩니다.
1
2
3
4
5
private boolean isInvalidFormat(final String str) {
    final String[] tokens = stringParser.extractTokens(str);
    return Arrays.stream(tokens)
      .anyMatch(token -> !token.matches("\\d*"));
}
  • \d 는 0 부터 9까지를 나타내며 [0-9] 정규표현식과 동일한 의미입니다.
  • *는 앞의 정규표현식에 해당하는 문자가 0개 혹은 n개가 있다는것을 나타냅니다.
  • 즉 위 정규표현식은 0 부터 9까지의 숫자를 0개 혹은 n개 매칭을 의미 합니다.
  • 따라서 100, """ , 2 등과 같이 숫자와 빈문자열이 매칭 됩니다.

✅ Facade 패턴

퍼사드 패턴은 복잡한 서브 시스템의 내부 구현을 숨기고, 이를 단순화된 인터페이스로 제공하는 구조 입니다.

왜 사용했는가??

컨트롤러가 너무 많은 유틸 클래스를 주입 받고 있어 이를 일관된 인터페이스로 묶어 복잡도를 줄이기 위해 사용하였습니다.

퍼사드 패턴 적용 전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class CalculatorController {

    private final InputView inputView;
    private final OutputView outputView;
    private final StringConvertor<Integer> stringConvertor;
    private final StringParser stringParser;
    private final StringValidator stringValidator;
    private final Calculator calculator;

    public CalculatorController(final InputView inputView, final OutputView outputView,
                                final StringConvertor<Integer> stringConvertor,
                                final StringParser stringParser, final StringValidator stringValidator,
                                final Calculator calculator) {
        this.inputView = inputView;
        this.outputView = outputView;
        this.stringConvertor = stringConvertor;
        this.stringParser = stringParser;
        this.stringValidator = stringValidator;
        this.calculator = calculator;
    }

    public void run() {
        final String inputString = requestInputString();
        final Numbers<Integer> numbers = Numbers.ofInteger(inputString, stringConvertor, stringParser, stringValidator);
        final BigInteger result = calculator.sumInteger(numbers);
        responseResult(result);
    }

    private void responseResult(final BigInteger result) {
        outputView.printResult(result);
    }

    private String requestInputString() {
        outputView.printAskInputString();
        return inputView.read();
    }

}

퍼사드 패턴 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CalculatorController {

    private final InputView inputView;
    private final OutputView outputView;
    private final CalculatorFacade calculatorFacade;

    public CalculatorController(
            final InputView inputView,
            final OutputView outputView,
            final CalculatorFacade calculatorFacade
    ) {
        this.inputView = inputView;
        this.outputView = outputView;
        this.calculatorFacade = calculatorFacade;
    }

    public void run() {
        final String inputString = requestInputString();
        final BigInteger result = calculatorFacade.calculatorIntegerSum(inputString);
        responseResult(result);
    }

    private void responseResult(final BigInteger result) {
        outputView.printResult(result);
    }

    private String requestInputString() {
        outputView.printAskInputString();
        return inputView.read();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface CalculatorFacade {

    BigInteger calculatorIntegerSum(final String str);
}

public class CalculatorFacadeImpl implements CalculatorFacade {

    private final StringConvertor<Integer> stringConvertor;
    private final StringParser stringParser;
    private final StringValidator stringValidator;
    private final Calculator calculator;

    public CalculatorFacadeImpl(
            final StringConvertor<Integer> stringConvertor,
            final StringParser stringParser,
            final StringValidator stringValidator,
            final Calculator calculator
    ) {
        this.stringConvertor = stringConvertor;
        this.stringParser = stringParser;
        this.stringValidator = stringValidator;
        this.calculator = calculator;
    }

    @Override
    public BigInteger calculatorIntegerSum(final String str) {
        final Numbers<Integer> numbers = Numbers.ofInteger(str, stringConvertor, stringParser, stringValidator);
        return calculator.sumInteger(numbers);
    }
}

  • 유틸 클래스들을 하나의 집합으로 묶고 CalculatorFacade 인터페이스로 감쌌습니다.
  • 컨트롤러는 여러 유틸 클래스가 아닌 CalculatorFacade만 참조함으로써 복잡도를 줄일 수 있었습니다.

✨Summary

1주차 미션은 언뜻 보면 간단해보였지만 “기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.” 라는 말 덕분에 고민해볼 포인트가 많았습니다. 특히 올바르지 않은 입력에 대한 예시가 없어서 예외 상황에 대해서 여러가지 경우의 수를 생각해야 했습니다. 또한 미션 자체를 단순한 알고리즘 문제 풀이가 아닌 하나의 서비스를 만든다는 관점에서 바라보았습니다. 그러다보니 자연스럽게 다형성과 DI등을 활용하게 되었고 객체지향적으로 구현하게 되었습니다~

📋 Todo

  • OOP 공부
  • Junit5 공부
  • java21 업데이트 내용 공부
  • 좋은 정보 커뮤니티에 공유하기
  • 페어구하기
This post is licensed under CC BY 4.0 by the author.