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

이성민·2025년 10월 17일

woowacourse

목록 보기
1/12
post-thumbnail

드디어 우테코 프리코스 1주차가 시작되었다.
그동안 회고를 하더라도 단순히 머릿속으로만 정리하거나 노션에 짧게 메모하는 수준이었다.
하지만 이번 프리코스가 시작되면서 벨로그를 통해 ‘제대로 된 회고록’을 써보자고 마음먹었다.
이 회고록을 통해 스스로를 돌아보는 힘을 키워, 한 단계 더 성장하는 개발자가 되고 싶다.


1주차 과제 : 문자열 덧셈 계산기

1주차의 미션은 문자열 덧셈 계산기였다.
프리코스의 진행 방식과 기능 요구사항은 아래와 같다.


우테코 프리코스를 시작하기 전, 『객체지향의 사실과 오해』를 읽으며 설계 감각을 키웠고, 이전 기수 문제를 풀며 연습을 해왔다.
그런데 진행 방식을 읽던 중

기능 단위로 커밋하는 방식으로 진행한다.

라는 문장에 눈이 딱 멈췄다. ‘이거다’ 싶었다.
이전에는 객체지향 설계에만 몰두하느라 커밋 관리가 엉망이었고, 커밋 타이밍을 잡지 못해 히스토리를 엉성하게 남겼다.
그 경험이 후회로 남아 있었기에, 이번 프리코스 1주차를 시작하면서 '기능 단위 커밋을 지키며 객체지향적으로 설계하자!!'는 목표를 세웠다.

과정을 돌아보며

기능 단위 커밋을 실천하기 위해 우선 기능 목록을 작성했다. (물론 완벽하지 않다... 나중에 이유가 나옴...)

처음엔 “그냥 기능 하나 만들고 커밋하면 되겠지”라고 생각했다.
그래서 가장 단순한 기능인 ‘빈 문자열 입력 시 0 반환’을 먼저 구현했다.

package calculator.domain;

public class Calculator {

    public int calculate(String input) {
        String resultValue = convertEmptyToZero(input);
        return Integer.parseInt(resultValue);
    }

    private String convertEmptyToZero(String input) {
        if (input.isEmpty()) {
            return "0";
        }
        return input;
    }
}

그런데 막상 커밋을 하려는데 문득 의문이 들었다.
“기능 단위 커밋은 좋지만, 코드가 제대로 동작하는지도 확인하지 않고 커밋하는 게 맞을까?
그 순간, 나는 테스트 코드를 작성해야 한다는 생각에 이르렀다.

@Test
    void 빈문자열을_입력하면_0을_반환한다() {
        Calculator c = new Calculator();
        assertThat(c.calculate("")).isEqualTo(0);
    }

기능 단위 커밋을 지키려는 시도가 결국 테스트 코드 작성으로 이어졌다.
이때 소름이 돋았다.
기능 단위 커밋을 지키다 보니, 자연스럽게 어렵게만 보였던 TDD의 첫걸음을 밟고 있구나..”

물론 완벽한 TDD는 아니었다.
하지만 테스트 코드를 작성하며 기능 단위로 검증하고, 확신을 가지고 커밋할 수 있었다.

또 하나 놀라운 점은, 기능 단위로 커밋하다 보니 예외나 빠진 기능이 자연스럽게 떠오른다는 것이었다.
그래서 README를 여러 번 수정했고, 처음엔 이게 맞나 싶었지만
나중에 찾아보니 이것이 바로 문서 주도 개발(Doc-driven Development) 이었다.

문서도 코드처럼 살아 움직여야 한다.

결국, 기능 단위 커밋을 실천하면서 자연스럽게
테스트 코드와 문서 주도 개발이라는 두 가지 습관까지 얻을 수 있었다.

그리고 한 가지 더 고민했던 부분이 있었다.
바로 입출력 흐름을 어디서 처리할지에 대한 문제였다.
이번 과제는 규모가 작기 때문에 굳이 Controller를 분리하지 않고, Application 클래스에서 입출력과 도메인 흐름을 함께 처리했다.

package calculator;

import calculator.domain.Calculator;
import calculator.domain.Delimiter;
import calculator.view.IO;

public class Application {
    public static void main(String[] args) {
        IO io = new IO();
        Delimiter delimiter = new Delimiter();
        Calculator calculator = new Calculator(delimiter);

        String input = io.input();
        int result = calculator.calculate(input);
        io.printResult(result);
    }
}

예외는 도메인 내부에서 충분히 검증되도록 구현했기에, 별도로 try-catch로 잡을 필요는 없다고 판단했다.
다만, 이 과제에서 이 구조가 정말 최선인지에 대해서는 아직 확신이 없다. 예외를 명확히 전달하고 구조를 분리하는 관점에서 보면 Controller를 도입하는 게 더 좋을 수도 있을 것 같기도 하다. 다음 과제에서는 Controller를 만들어 직접 비교해볼 예정이다.

+ 리펙터링 메모 - Delimiter ↔ Operand 책임 재정의

구현 중 뒤늦게 보니, Operand를 만들어두고도 숫자 검증을 Delimiter에서 하고 있었다.
VALIDATE_DEFAULT0-9 범위까지 포함해 숫자/음수 여부를 사실상 판단하고 있었던 것이다.

Before (문제 코드 부분)

private static final String DEFAULT_DELIMITER = "[" + BASE_DELIMS + "]";
private static final String VALIDATE_DEFAULT = ".*[^0-9" + BASE_DELIMS + "].*";

private List<String> splitByDefaultDelimiter(String inputs) {
        if (inputs.matches(VALIDATE_DEFAULT)) { // 숫자/구분자 외 문자면 예외
            throw new IllegalArgumentException("기본 구분자(" + BASE_DELIMS + ")만 허용됩니다.");
        }
        validateDelimiterSequence(inputs);
        return Arrays.stream(inputs.split(DEFAULT_DELIMITER)).toList();
}

After (개선 코드, 핵심만)

// 기본 구분자(, :) 외 기호 금지 (하이픈은 허용해 음수는 Operand에서 판별)
private static final String HAS_NON_DEFAULT_DELIMS =
    ".*[\\p{Punct}&&[^,:\\-]].*";

// 공백/기본 구분자 제외 토큰 + 올바른 배치(선행/후행/연속 금지)
private static final String TOKEN = "[^\\s,:]+";
private static final String WELL_FORMED = "^" + TOKEN + "(?:[, :]" + TOKEN + ")*$";

private void assertOnlyDefaultDelims(String s) {
    if (s.matches(HAS_NON_DEFAULT_DELIMS)) {
        throw new IllegalArgumentException("정해진 구분자 외 문자는 사용할 수 없습니다.");
    }
}

private void assertWellFormed(String s) {
    if (!s.matches(WELL_FORMED)) {
        throw new IllegalArgumentException("구분자 사용이 올바르지 않습니다. (선행/후행/연속 금지)");
    }
}

정책 정리

  • Delimiter: “구분자 형식/배치 검증 + 분리”만 한다
    - 기본 구분자 , :만 허용 및 하이픈 -는 토큰 문자로 허용
    - 구분자 배치 규칙: TOKEN ( DELIM TOKEN )* (선행/후행/연속 금지)
    - 커스텀 헤더 "//" + "\\n" 지원, 커스텀 구분자는 ,로 정규화

  • Operand: “숫자/음수 검증” 전담

테스트 검증

// Delimiter 단위
assertThatThrownBy(() -> delimiter.split("1;2;3"))  // 기본 구분자 외 문자
    .isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> delimiter.split("1,,2"))   // 연속 구분자
    .isInstanceOf(IllegalArgumentException.class);
assertThat(delimiter.split("//;\\n1;2,3"))
    .containsExactly("1", "2", "3");                // 커스텀 정상 + 정규화

// Operand 단위
assertThatThrownBy(() -> new Operand("-2"))         // 음수
    .isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new Operand("a"))          // 비숫자
    .isInstanceOf(IllegalArgumentException.class);

Delimiter가 숫자/음수까지 검증하던 문제를 확인하고 리펙터하였다.
“형식/배치”는 Delimiter, “숫자/음수”는 Operand로 책임을 분리하고, 정책상 기본 구분자(, :) 외 기호는 차단하되 하이픈(-)은 토큰 문자로 허용했다.
덕분에 "1,-2,3"은 Delimiter를 통과하고 Operand에서 음수로 예외가 나며, 테스트 책임이 명확해졌다.

다음부터는 입력 정책(무엇을 허용/금지할지)과 책임 경계(누가 무엇을 검증할지)를 사전 체크리스트로 확정하고, 이를 기준으로 설계·리팩터링을 수행하겠다.

배운점

이번 1주차에서 가장 크게 배운 것은 두 가지다.

  1. 테스트 기반 개발의 시작점

    • 기능 단위 커밋을 위해 테스트 코드를 작성했고, 그 과정에서 TDD의 개념을 체감했다.
  2. 문서 주도 개발의 중요성

    • README를 지속적으로 업데이트하며 문서가 코드의 흐름과 함께 진화해야 함을 느꼈다.

처음엔 단순히 “기능 단위 커밋을 잘 해보자”에서 출발했지만,
결과적으로 테스트와 문서화까지 이어진 커다란 배움을 얻었다.

아쉬운 점

  1. 테스트 코드 작성이 아직 미숙해, 단위 테스트의 개념을 더 공부할 필요가 있다.

  2. 커밋 시 ‘기능 커밋’과 ‘테스트 커밋’을 분리하지 못했다.

  3. Controller 분리에 대한 판단이 남았다.

  4. 구현 전 입력 정책과 책임 경계를 확정하지 못했다.

마무리

1주차는 단순한 과제 이상의 경험이었다.
‘잘 작동하는 코드’보다 ‘읽기 좋은 코드’의 중요성을 체감했고,
기능 단위 커밋이라는 습관이 생각보다 강력한 학습 도구임을 알게 되었다.

다음 주에는 이번에 부족했던 점을 보완하며,
더 읽기 쉽고 협력하기 좋은 코드를 만드는 개발자로 성장하고 싶다.

profile
BE 개발자

0개의 댓글