[우아한테크코스 8기] 프리코스 1주차 회고

우아한테크코스 8기에 지원했다.

약 5주간의 프리코스를 진행하며 고민했던 부분들을 적어나가려 한다.

 

1주차 미션은 문자열 계산기다.

 

기능 목록 작성

과제 진행 요구 사항

  • 미션은 문자열 덧셈 계산기 저장소를 포크하고 클론하는 것으로 시작한다.
  • 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
  • Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
  • 자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.

 

기능을 구현하기 전 구현할 기능 목록을 정리하라는 말이 눈에 띄었다.

요구사항에 대해 어떤 부분을 고려하면 좋을지, 스스로 생각해보라는 우테코의 가치관이 담겨 있는 것 같았다.

 

또한 이는 곧 요구사항 명세라는 생각이 들었다.

실제 서비스를 개발할 때, 사용자의 요구사항을 파악하듯이 미션을 수행해보려고 한다.

 

Git의 커밋 단위 또한 기능 목록 단위로 추가하며, AngularjS 문서를 참고해 커밋 메시지를 작성하라는 요구사항이 있었다. 여러 번의 팀프로젝트를 진행하며 Git Commit 컨벤션이 어느 정도 익숙해진 상태이지만, 꼭 정답이 아닐 수 있기에 다시 한 번 점검을 거쳤다. 이후 아래와 같은 결론을 내렸다.

  • feat : 사용자가 사용할 수 있는 새로운 기능을 추가했을 때
  • fix : 버그 수정
  • docs : 코드 변경 없이 주석, README, JavaDoc만 수정했을 때
  • style : 로직 변경 없이 공백, 들여쓰기, 세미콜론 등만 수정했을 때
  • refactor : 기능 변경 없이 코드 구조를 개선했을 때
  • test : 테스트 코드만 추가하거나 수정했을 때
  • chore : 빌드 설정, 의존성 추가, 설정 파일 변경 등

 


 

기능 요구사항

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

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

입출력 요구 사항

입력

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

출력

  • 덧셈 결과
결과 : 6

실행 결과 예시

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


사실 위 요구사항을 봤을 때, 많이 어렵진 않겠다는 생각이 들었다. 하지만 이는 섣부른 생각이었다. 요구사항을 여러 번 읽으며 기능 목록을 정리할수록, 구체적으로 명시되지 않은 부분들이 존재했다.

 

커스텀 구분자는 숫자일 수도 있다.

요구사항에서 커스텀 구분자는 '문자'라고 명시되어 있었다. 하지만 곧 '숫자도 문자이기 않은가?' 라는 생각이 들었다. 이에 대한 답은 나무위키에서 확신을 지을 수 있었다. 따라서 커스텀 구분자는 숫자도 가능하다는 결론을 내렸다.

 

숫자 - 나무위키

 

커스텀 구분자는 여러 자일 수도 있다.

커스텀 구분자는 1자가 아닌 여러 자일 수도 있다는 생각이 들었다. 하지만 이 역시 요구사항에서 커스텀 구분자는 '문자'라고 명시되어 있었고, 문맥의 흐름상 커스텀 구분자는 1자인 것으로 범위를 좁혔다.

 

이어서 예외 상황들도 발견할 수 있었다.

 

입력된 값이 존재하지 않을 수 있다.

말 그대로 입력값이 ""이거나 " "일 수 있다.

 

숫자가 양수가 아닐 수 있다.

-1 또는 0일 수 있다. 이는 입력 요구사항에 명시되어 있던 '양수'가 아니다.

 

잘못된 형식의 입력값이 들어올 수 있다.

구분자만 연속으로(",:") 입력될 수도 있다. 또한 기본 구분자가 쉼표(,) 또는 콜론(:)이 아닌 경우가 존재할 수 있다. 뿐만 아니라 구분자만 입력되거나, 숫자만 입력되는 경우도 존재할 수 있다.

 

위와 같은 경우는 잘못된 형식의 입력에 대한 예외상황이라는 생각이 들었고, 기능 목록에 이를 명시했다.

 


 

기본 구분자를 이용한 숫자 추출

기본 구분자와 커스텀 구분자를 이용해, 숫자를 추출하는 기능은 곧 이번 과제의 핵심이라고 생각한다. 어떻게 해결할 수 있을까? 다양한 방법이 떠올랐다.

 

입력은 문자열로 들어오니, for문으로 문자열을 순회하며, 숫자인지 구분자인지 확인할 수도 있다. String API에서 제공하는 split, substring, indexOf 등의 메서드를 활용할 수도 있다. 어떤 방법이 좋을지 고민되어 직접 String 클래스를 파헤쳐봤다. 이 때, 못보던 메서드를 발견할 수 있었다.

 

splitWithDelimiters()

splitWithDelimiters() 메서드는 Java 21에서 업데이트 된 내용이다. 기존의 split() 메서드처럼 문자열 타입의 regex를 이용해 문자열을 분리하는 것은 동일하지만, 구분자도 결과에 포함되어 반환한다. 여러 개의 구분자가 들어와도, 문자열을 분리하는 것이 가능하기에 해당 방식을 사용하기로 결정했다. 기본 구분자를 이용해 숫자를 추출하는 로직은 아래와 같이 구현했다.

 

input.splitWithDelimiters("[,:]", 0);

 


 

커스텀 구분자를 이용한 숫자 추출

splitWithDelimiters()

커스텀 구분자를 이용해 숫자를 추출하는 건 기본 구분자보다 훨씬 까다로워 보였다. "\\"와 "/n" 사이에 있는 문자가 곧 커스텀 문자이기에, 이를 추출하는 방법을 먼저 찾아야 했다. 우선 기본 구분자를 이용해 숫자를 추출할 때 사용했던 splitWithDelimiters()를 동일하게 적용해보고자 했다.

 

String[] tokens = input.splitWithDelimiters("(//(.)\\n)", 0);

String delimiter = tokens[2];
String numbers = tokens[4];

 

위와 같은 방식으로 숫자를 추출할 수 있었다.

 


 

Pattern, Matcher

미션을 해결하며 정규표현식을 학습하면서 Java의 regex 패키지에서 제공하는 Pattern, Matcher 클래스를 알게 되었다. 해당 방식 또한 동일하게 적용해보고 싶었다. 관련 내용은 아래 링크에서 따로 정리했다.

 

https://jihyun-devstory.tistory.com/59

 

[Java] 정규표현식(Regular Expression)

1. 정규표현식이 왜 필요할까?개발을 하다보면 사용자의 입력을 검증해야하는 순간이 온다. 그 형식은 다양하다. 전화번호 형식이라든지, 이메일 형식이라든지. 예를 들면 아래와 같다. 아래와

jihyun-devstory.tistory.com

 

Pattern pattern = Pattern.compile("//(.)\\n");
Matcher matcher = pattern.matcher(input);

String delimiter = "";
while (matcher.find()) {
       delimiter = matcher.group(1);
}

String numbers = input.substring(input.indexOf("\n") + 1);

 


 

splitWithDelimiters()와 Pattern/Matcher 성능 비교

 

코드 길이 측면에서는 splitWithDelimiters()가 훨씬 간결했다. 이에 그치지 않고 성능도 비교해보았다. 반복 횟수를 5회로 두고, 성능 테스트를 수행한 결과 아래와 같은 결과가 나왔다.

 

  • splitWithDelimiters()
    • 5회 평균 : 62.5ms
  • Pattern, Matcher
    • 5회 평균 : 61.85ms

 

둘의 성능 차이는 크지 않았다. 하지만 Pattern/Matcher를 사용했을 때는 splitWithDelimiters를 사용했을 때보다 매직 넘버가 없고, 정규식 패턴으로 문자열을 추출하는 의도가 더 명확하게 드러난다고 판단해 Pattern/Matcher를 사용하기로 결정했다.

 


 

리팩토링

요구사항을 만족하는 최소한의 코드를 작성한 후, PR 체크리스트와 자바 컨벤션 가이드를 살펴보며 코드를 다시 검토했다. 초기 코드는 모든 로직이 하나의 클래스에 집중되어 있어 책임이 명확하지 않았고, 유지보수가 어려웠다. 이를 개선하기 위해 MVC 패턴을 도입하고 아래와 같은 고민을 진행했다.

 

MVC 패턴 도입

우선 입력, 출력, 비즈니스 로직이 한 곳에 섞여 있었기 때문에,  이를 분리했다. 이 때 Controller, View는 역할이 명확하지만, Service와 Domain의 소속이 불분명했다. 이에 대해 Service와 Domain 모두 Model Layer에 속한다고 결론지었다.

 

  • Service : 비즈니스 로직을 조율 및 흐름 제어
  • Domain : 핵심 도메인 로직과 규칙을 캡슐화

 

이러한 분리로 비즈니스 로직의 복잡도를 계층별로 관리할 수 있었다.

 

검증 로직은 어디에서 담당해야 할까?

사용자로부터 입력을 받는 InputView(View)에서 검증 로직을 담당해야 할지, Calculator(Domain)에서 담당해야 할지에 대한 고민이었다.

 

이에 대해 입력에 대한 검증은 비즈니스 로직에 속한다고 판단했다. 따라서 Calculator 내부에서 검증하도록 구현했다.

 

배열 -> 컬렉션

처음엔 숫자를 추출하는 로직과 관련된 모든 메서드가 배열 타입을 반환하고 있었다. 이 때 자바에서 제공되는 컬렉션 API를 적극 활용해보면 좋겠다는 생각이 들어, String[]에서 List<String> 타입을 반환하도록 수정했다.

 

 

객체의 상태 관리

초반에는 Calculator 클래스 내에서 아무런 필드 없이 메서드만 구현했다. calculate() 메서드 내에서 검증 호출, 숫자 추출, 합을 모두 담당했고 메서드의 역할이 크다는 생각이 들었다. 이와 동시에 '객체는 상태를 가져야 하지 않나?'라는 의문이 생기기 시작했다. 상태를 외부에서 관리하고 있었기 때문이다.

 

이를 해결하기 위해 추출한 숫자를 Calculator 클래스의 필드로 관리하기 시작했다. 또한 정적 팩토리 메서드를 도입해 내부적으로 발생하는 상황을 외부에서 알지 못하도록 캡슐화를 적용했다. 이 때, numbers의 타입을 List<String>에서  List<Integer>로 수정했다. 그 이유는, 구분자를 포함하지 않고 숫자만 Calculator 클래스가 관리하면 된다고 판단했기 때문이다.

 

public class Calculator {
    private final List<Integer> numbers;

    public Calculator(List<Integer> numbers) {
        this.numbers = List.copyOf(numbers);
    }

    public static Calculator from(String input) {
        InputValidator.validate(input);
        List<Integer> extractNumbers = NumberParser.parseNumbers(input);
        return new Calculator(extractNumbers);
    }
}

 

방어적 복사

추출한 숫자를 필드로 관리하기 시작하니, 생성자에서 주입받고 있던 값을 외부에서 변경이 가능했다. 이를 해결하기 위해 List.copyOf()를 활용한 방어적 복사를 도입했다.

 

public Calculator(List<Integer> numbers) {
	this.numbers = List.copyOf(numbers);
}

 

생성자 주입 시 Java의 특성 상 외부에서 변경이 가능하다는 사실을 인식했고, 객체의 불변성을 보장하는 것이 중요하다는 것을 깨달았다.

 

splitWithDelimiters() -> split()

미션을 수행하며 정규표현식에 대해 학습했는데, 이 때 특히 놀라웠던 사실이 있다. 여러 개의 정규식을 활용해서 문자열을 분리하는건 splitWithDelimiters()만 가능하다고 알고있었는데, split() 메서드 또한 가능하다는 사실이다.


두 메서드의 차이는 구분자를 포함해 분리하는지, 포함하지 않고 분리하는지였다.

input.splitWithDelimiters("[,:]");
input.split("[,:]");

 

위 사실에 대해 알게되니 splitWithDelimiters()를 사용하는 것보다 split()을 사용하는게 더 로직이 직관적이고, 간결하다는 것을 깨달았다. 또한, 같은 String API이더라도 필요에 따라 골라서 사용할 수 있다는 점이 재미있고 신기했다.


 

회고

프리코스 목표 설정

1주차를 시작하기 전 이번 프리코스에 내가 얻고 싶은 것이 무엇인지 곰곰히 생각해보았다. 구체적인 목표 설정이 필요하다는 생각이 들었다. 따라서 아래와 같이 목표를 세웠다.

 

1. 매일 6시간씩 프리코스에 투자하기

 

2. 스스로 고민해보기

  • 어떤 방법이 있을까?
  • 이 중에 어떤게 제일 적합할까? 효율적일까?
  • 리팩토링은 항상 마지막에 하기

 

3. 기록하고 공유하는 습관 길들이기

  • 매주 10명 이상 코드 리뷰하기
    • 왜?
      • 내 코드에 대한 다른 사람들의 관점과 피드백을 주고 받을 수 있어서
      • 내 기준 최소의 인원
  • 매주 기술 블로그 공유하기
    • 매 주차마다 내가 가장 몰입해서 학습했던 기술 키워드가 무엇인지?
    • 남들에게 설명할 수 있는만큼 깊이 학습했는지?
    • 나만의 결론을 도출했는지?
  • 매주 회고록 공유하기
    • 일주일동안 어떤 목표를 가지고, 어떤 행동을 했는지, 그 결과 어떤걸 깨달았는지

 

4. 커뮤니티 활발하게 활동하기

  • 평가 기준을 위한 커뮤니티 활동 X
  • 평소 혼자 고민했던 부분이 무엇인지? 지원자들과 함께 토론해보고 싶은 주제가 무엇인지?

 

5. 학습의 필요성을 느낀 부분에 대해 깊게 파보기

  • 학습에 대한 의식적인 연습하기
  • 매 주차마다 깊게 학습했던 하나의 키워드에 대해 기술 블로그 작성해보기
  • 나만의 결론 내보기

 

1주차 - TDD와 시간 재기 도입

7기 프리코스에 참여하며 스터디원과 TDD를 연습했지만, 최종 코딩테스트에서 적용하지 못했던 것이 떠올랐다. 그래서 이번 프리코스 때는 TDD를 적용하며, 조금 더 친해지고 싶었다. 구현 시간을 재는 것도 하고 싶었다. 단순히 5시간 안에 빠르게 구현하기 위함이 아닌 내가 지금 어느 정도의 상태인지 메타인지하기 위함이었다.

 

그래서 위 목표를 토대로 1주차 미션을 수행했다. (TDD+시간 재기)를 병행하며 요구사항을 만족하는 최소한의 코드를 작성했다. 이전에는 머릿속에서 설계에 대한 고민을 먼저 해 TDD가 어렵게만 느껴졌다. 하지만 이전과 달리 주요 로직에 대해, 스스로 세웠던 기능 목록 단위로 테스트를 구현하니, 훨씬 수월하게 느껴졌다.

작성한 테스트 코드에 맞게 클래스와 메서드를 구현한 이후, 테스트 코드가 하나씩 통과할 때마다 기분이 좋아졌다. 마치 조립을 하는 듯한 기분이 들었다. 조금 더 TDD에 자신감을 갖는 계기가 되었다.

 

public class CalculatorTest {
    @Test
    @DisplayName("기본 구분자로 숫자를 추출해 합을 계산한다.")
    public void 기본_구분자로_추출해_계산() {
        String input = "1,2:3";
        Calculator calculator = new Calculator();
        int result = calculator.calculate(input);

        assertEquals(6, result);
    }

    @Test
    @DisplayName("커스텀 구분자로 숫자를 추출해 합을 계산한다.")
    public void 커스텀_구분자로_추출해_계산() {
        String input = "//;\n4;5;6";
        Calculator calculator = new Calculator();
        int result = calculator.calculate(input);

        assertEquals(15, result);
    }
}



MVP 구현 이후, 학습과 병행하며 리팩토링도 진행했다. 미션을 구현하고나니 우선 돌아가는 최소한의 코드를 작성하는데 집중하게 되었고, 구현 과정에서의 학습보다 구현 이후의 학습의 깊이가 더 깊었다. 따라서 MVP 구현 이후, 학습했던 정규표현식 내용을 리팩토링 과정에 반영했다.

 

나의 학습 방향은?

1주차를 마무리하고  '과연 이렇게 학습하는게 맞을까?, 잘하고 있는 걸까?'라는 의문이 들었다. MVP를 구현하면서가 아닌 구현한 이후, 학습의 깊이가 깊어지는 것에 대한 의구심이 들었기 때문이다. 이 부분은 정답이 없다는 것을 알고있다. 이 때 포비가 토론하기에 아래와 같은 글을 올려주셨다.

 

 

고민하고 있는 부분에 대해 콕 짚어주신 것 같다는 생각이 들었다.

 

  • 내가 성장하고 있는지 어떻게 판단해야 할까?
  • 지금처럼 '이게 맞을까?'라는 생각이 들면 어떤 다른 기준을 세워야 할까?
  • 너무 하드 스킬에만 집중했던 것은 아닐까?
  • 내가 진정 프리코스에서 얻고 싶은 것이 무엇일까?
  • 내가 할 수 있는 것이 아닌 '해본 적이 없어서 도전하고 싶은 일'은 뭘까?

 

계속해서 학습 방향에 대해 고민했다. 돌아보면, 난 프리코스를 할 때 가장 열심히 스스로 학습하는 것 같다. 왜일까? 이에 대한 결론은 아래와 같다.

 

1. 함께하는 열정적인 사람들
2. 늘 혼자서 했던 고민을 입 밖으로 꺼내고 정리해, 다른 사람과 토론하며 자신만의 결론을 내리는 과정
3. 성장하고 싶은 욕구를 이루기 위해 세운 작은 목표들

 

2주차 목표 재정비

1주차를 되돌아보면 프리코스가 시작되기 전 세웠던 '매일 6시간씩 프리코스에 투자하기'를 이루지 못한 것 같다. 또한 요구사항 중 아래 2가지를 지키지 못했다.

 

  • 출력 요구사항 - 입력 안내 문자 출력
  • ""이 입력되면 0을 출력한다.

 

변명일 수 있지만, 3일간 가족 여행으로 더 시간을 쏟지 못했던 것이 원인인 것 같다. 여러 차례 요구사항을 꼼꼼하게 검토하지 못한 명백한 나의 실수였다. 이런 부분에서 실수를 한 스스로에게 화가 나기도 하고, 개인적으로 아쉬웠던 한 주였다.

 

5주간의 프리코스를 참여하며 단순히 합격에 좌지우지 되지 않고, '이번 프리코스에서 내가 진정 이루고 싶은 목표들은 무엇인가?'에 대해 지속적으로 생각해보고, 각 주차별 구체적인 목표를 세워야겠다고 다짐했다. 또한 2주차가 시작되기 전, 매 주차마다 목표를 더 구체적으로 재정비 할 필요가 있다고 느꼈다. 따라서 아래와 같이 목표를 구체화 했다.

 

1. 미션 시작 전과 제출 전 요구사항을 5번씩 읽고, 꼼꼼히 확인하기
2. 최소 1가지 이상 학습하고, 블로깅하여 디스코드에 공유하기
3. 다른 프리코스 참여자분들과 상호 코드 리뷰 진행하기
4. 매 주 마주한 문제상황이나 고민을 기록하고, 회고를 작성하여 공유하기
5. 첫 시도 한 줄 챌린지 참여하기
6. 매일 5시간 이상 프리코스에 투자하기

 

목표를 간결하면서도, 핵심을 담으니 이전보다 내가 프리코스에서 명확히 어떤걸 하고싶은지 보이기 시작했다. 나는 내가 배운 것에 대해 '공유'하는 사람이 되고 싶다. 다른 사람에게 공유하기 위해서는, 내가 해당 지식에 대해 더 깊이있게 학습해야 하고 설명할 줄 알아야 한다. TDD 및 시간 재기를 지금처럼 유지하되, 2주차 때는 재정비 한 목표를 토대로 요구사항을 1순위로 집중해보려 한다. 그리고 빠른 구현이 아닌, 시간이 많이 걸리더라도 의식적인 연습을 통해 '나만의 결론 내리기'를 길들이고 싶다. 2주차가 종료되고, 스스로 성장 방향성을 찾았으면 좋겠다. 후회없도록 최선을 다해 임하자.

 

1주차 PR 링크

https://github.com/woowacourse-precourse/java-calculator-8/pull/203

 

[문자열 덧셈 계산기] 이지현 미션 제출합니다. by Jihyun3478 · Pull Request #203 · woowacourse-precourse/java-

아래 고민한 부분을 중점적으로, 리뷰어분들은 어떤 고민과 선택을 하셨는지 함께 토론해보고 싶습니다! 1. 애플리케이션 구조 (MVC 패턴) 2. 고민한 부분 기본 구분자 및 커스텀 구분자 처리를 어

github.com