Post

[우테코 프리코스 1주차 회고] 구조 설계 고민

다시 시작하는 우테코 프리코스, 후회 없도록 하자!

작년에 우테코 7기 프리코스에 참여했었다. 그래서인지 이번 8기 프리코스에 다시 참여하기로 마음먹은 순간, 이번엔 후회 없이 해내고 싶다는 다짐이 가장 먼저 들었다.

정답이 없는 과제 속에서 더 나은 코드를 고민하고, 시도하고, 부딪히며 지금의 나보다 더 나은 나를 만들고 싶었다.
아직 1주차가 끝났을 뿐인데도 전보다 고민의 깊이가 달라졌음을 느낀다.
구조 설계에 더 많은 시간을 쓰고, “이 방식이 더 나을까?”를 계속 스스로에게 묻는 과정이 즐겁다.

또한 7기 때는 스터디 참여를 망설였지만, 이번에는 스터디에 참여하고 싶었다.
같은 목표를 가진 사람들과 함께 고민을 나누고, 서로의 코드를 리뷰하며 배우고 싶다.
스터디와 코드리뷰 모두 적극적으로 참여할 것이다!

이제 막 1주차가 끝났을 뿐인데, 시간이 참 빠르게 흘러간다.
앞으로 남은 주차 동안에도 매 순간 최선을 다해 프리코스가 끝났을 때 “정말 후회 없었다”고 말할 수 있길 바란다.


일간 회고

매일 매일 어떤걸 작업했고, 느낀점에 대해 정리하며 과제를 진행하였다.

10/14(화) 1일차

  • 기능을 구현하기 전에 기능 목록 작성하기

이전 기수분의 블로그를 참고하며, 기능 명세서를 어떻게 작성하는지 살펴보았다.

각각의 객체들이 어떤 역할과 책임을 수행하는지 할당하자.

  1. 어떤 객체가 필요할지 떠올린다.
  2. 프로그램의 목적을 수행하기 위해 객체들이 서로 어떤 명령을 내려야 할지 정한다.

나는 이 블로그 글(기술명세서 작성법)을 참고하였다.
이를 바탕으로 각 객체의 역할과 기능, 예외 상황까지 떠오르는 대로 정리하였다.

기술명세서에 작성한 코드 흐름

사용자는 문자열 덧셈 계산기를 통해 문자열을 입력하고, 계산결과를 돌려받는 구조로 기능 명세서를 설계하였다.

내일부터는 오늘 작성한 명세서를 기반으로, 하나씩 기능을 구현해 나갈 계획이다.


10/15(수) 2일차

  • 핵심 기능 구현하기
    • 입력받은 문자열에서 숫자를 추출하는 기능

“문자열 덧셈 계산기”의 핵심 기능을 직접 정의하고 구현해보았다.
핵심 기능을 구현하기 위해 우선적으로 구현되어야하는 것은 두 가지이다.

  1. 사용자가 입력한 문자열에서 구분자를 판단하는 로직
  2. 판단된 구분자로 숫자 부분 문자열을 분리하는 로직

처음에는 빠르게 전체 흐름을 검증하고 싶어서 Application 클래스 안에 모든 로직을 한번에 구현했다.
테스트 코드를 작성하면서 로직의 정확성을 확인했는데, 여기서 예상치 못한 문제가 발생했다.

줄바꿈 기준으로 문자열이 분리되지 않아 //;\n1;2;3 입력이 "1", "2", "3"으로 처리되지 않았던 것이다.
이 부분을 해결하기 위해 input = input.replace("\\n", "\n"); 코드를 추가했다.

기능이 올바르게 동작함을 확인한 후, 한 클래스에 몰려 있던 로직을 역할에 따라 분리했다.
controller, util, view, dto 패키지를 새로 구성했다.
기능을 나누는 과정에서 “이 책임은 어디까지가 적절할까?”를 계속 고민하였다.

오늘 하루는 잘 동작하는 코드를 만드는 과정 속에서
작은 버그 하나가 왜 생기는지를 이해하고, 객체의 역할을 기준으로 리팩토링을 해본 날이었다.
내일은 이 구조 위에 새로운 기능을 확장하면서 테스트를 통해 안정적인 코드로 다듬어 나가려 한다.

p.s 백엔드 코드 리뷰 스터디에 참여했다. 앞으로 매주 금요일마다 팀원분들과 함께 코드 리뷰를 진행하며, 내가 놓치고 있던 부분이나 개선할 점들을 꾸준히 발견해나갈 계획이다. 단순히 ‘잘 짠 코드’를 목표로 하기 보다는 객체지향적인 사고와 더 나은 구조를 탐구하는 과정이 될 것 같다.


10/16(목) 3일차

  • 숫자 리스트로 변환한 것을 검증한 뒤, 더하는 로직 구현하기
    • 숫자 검증, 계산 로직 (더하기)

Number, Numbers 도메인 객체를 새로 추가하면서 각각의 역할을 분리하였다.

Number는 숫자 하나를 표현하고 스스로 유효성을 검증하도록 했으며,
Numbers는 여러 Number를 구성하고 합계를 계산하는 일급 컬렉션으로 설계하였다.

DelimiterExtractor에서 구분자 추출과 검증 로직을 분리하며 단일 책임 원칙을 적용했다.
이 과정에서 “메서드는 한 가지 일만 해야한다”를 지키려 노력했고, 덕분에 각 메서드가 어떤 일을 하는지 이해가 쉬워졌던 것 같다.

오늘 가장 많은 시간을 쏟은 부분은 예외 처리와 리팩토링이었다.
단순히 동작하는 코드를 넘어, 예외 상황에서도 일관된 동작을 보장하려고 노력했다.

드디어 문자열 덧셈 계산기의 전반적인 기능이 완성되었다.
내일은 남은 예외 케이스들을 다시 점검하고, 아직 테스트하지 못한 코드의 테스트들을 보완할 계획이다.
그리고 전체 로직이 어떻게 흘러가는지 정리하면서 내가 설계한 구조가 잘 설계되었는지 확인해보고자한다.


10/17(금) 4일차

  • 남은 예외 케이스들을 다시 점검하고, 아직 테스트하지 못한 코드의 테스트코드 작성하기
    • [테스트코드 작성] 지정한 구분자가 아닌 다른 구분자가 입력될 경우
    • [테스트코드 작성] (기본/커스텀) 구분자 이외의 문자를 사용했을 경우
    • [테스트코드 작성] 입력한 문자열에 양수가 아닌 음수, 문자열이 입력될 경우
    • 전체 로직이 어떻게 흘러가는지 정리하기

총 3가지 상황에 대해 테스트를 진행했다. 테스트를 진행하면서 예외 케이스가 생각보다 많다는 것을 느꼈다.
여러 가지 예외 상황을 떠올리고, 테스트 코드로 확인할 수 있다는 점이 좋았다.

하지만 테스트를 작성하면서 한 가지 깨달은 점이 있다.
테스트 코드에서는 내가 임의로 넣은 입력 값이 예상한 예외를 던지는지에 집중하게 되는데, 실제 Application.main()을 실행해보면 내가 생각하지 못한 예외 메시지가 나타나는 경우가 있었다.

이를 통해 예외처리를 하는 그 클래스 테스트만으로는 예외 메시지의 실제 동작을 완전히 확인할 수 없다는 점을 깨달았다.
이 부분은 내일 리팩토링하면서 개선하고자 한다.


기술명세서에 작성한 코드 흐름

그리고 전체 로직의 흐름을 정리해보았다. 정리하고 나니까 현재 내 코드가 어떻게 동작하는지 다시 돌아볼 수 있었고, 특히 DelimiterExtractor 클래스가 많은 역할을 담당하고 있다는 것을 확인했다.
이 부분 역시 리팩토링 대상이라고 생각이 들었다.

오늘은 여러 고민을 하며 코드의 예외 처리와 구조를 깊이 있게 돌아본 하루였다.
내일은 오늘 느낀 부분들을 바탕으로 리팩토링하고, 필요하다면 코드를 수정할 계획이다.

p.s 디스코드에 ‘스스로 만들기’ 카테고리가 오픈 되었다. 오늘도-즐거운-모각코 채널이 만들어져서 과제를 하는 동안, 들어가있었는데 함께 하고 있는 느낌이 들어서 좋았다. 앞으로도 종종 과제를 진행하는 동안 들어가있어야겠다는 생각을 했다.


10/18(토) 5일차

  • 단순히 코드가 동작하는지 확인하는 것을 넘어
    ‘구조적으로 더 나은 방식은 없을까?’를 고민하며 리팩토링 진행하기

가장 먼저 눈에 들어온 건 DelimiterExtractor(구분자 추출기) 클래스였다.
이 클래스의 extract() 메서드는 입력 정규화, 커스텀/기본 구분자 판별, 유효성 검증, 구분자 추출 및 결과 생성까지 너무 많은 일을 동시에 수행하고 있었다.

즉 하나의 메서드가 여러 역할을 맡고 있었고, 만약 새로운 구분자 규칙이 추가된다면 if문으로 조건을 늘려야 하는 구조였다. 이건 명확히 확장에는 닫혀있고, 수정에는 열려 있는 형태였다. (OCP 위반)

OCP(개방-폐쇄 원칙) 이란?

새로운 기능이 추가할 때 기존 코드 수정하지 않고 확장할 수 있다.
기능을 확장할 때 기존의 코드는 변경되지 않아야 한다.


그래서 떠올린 것이 전략 패턴이었다.
DelimiterExtractor는 단순히 입력을 받아 ‘누가 처리할지’를 결정하지 않고,
DelimiterStrategy가 스스로 내가 처리할 수 있는 입력인지를 판단(judgment())하고,
가능하다면 직접 구분자를 추출(extract())하도록 역할을 위임했다.

DelimiterStrategy 동작 흐름

1
2
3
4
public interface DelimiterStrategy {
  boolean judgment(String input); // 내가 처리할 수 있는 입력인지 판단
  DelimiterResult extract(String input); // 구분자 추출 로직
}

이렇게 책임을 나누자 구조가 한결 명확해졌다.

리팩토링 후에는 예외 처리 로직을 점검했다.
특히 예외 메시지가 올바른 상황에 대응하지 않는 부분이 있어서 어떤 입력이 어떤 예외를 유발하는지 올바르게 구분되도록 개선하였다. 이 과정에서 예외 처리는 단순히 에러를 던지는 게 아닌, 사용자에게 명확한 피드백을 주는 중요한 역할이라는걸 깨달았다. 그래야 사용자는 어떤 부분이 에러인지 알 수 있기 때문이다.

다만 아직 아쉬운 점이 있다. 테스트 코드를 통해 실제 사용자 입력이 흘러가는 전체 흐름(입력 → 구분자 추출 → 숫자 검증 → 합계 계산 → 결과 출력)이 잘 동작하고 있는지 검증하지 못했다는 점이다. 내일은 이 부분을 고민해보고 해결해봐야겠다.

p.s 참고한 전략패턴 설명한 블로그 글 : 전략(Strategy) 패턴 - 완벽 마스터하기


10/19(일) 6일차

  • 구조적인 개선과 테스트를 통해 전체 흐름 검증하기

사용자 입력부터 결과 출력까지의 전반적인 흐름을 담당하는 건 CalculatorController였다.
테스트를 진행하려고 보니, 입출력 로직까지 함께 호출하는 것은 불필요하다고 느꼈다.

그래서 CalculatorController는 오직 흐름 제어만 담당하고, 실제 계산 로직은 CalculatorService가 하도록 분리하였다. CalculatorService는 다음의 역할만 수행하도록 했다.

  • DelimiterExtractor로 구분자 추출
  • StringSplitter로 문자열 분리
  • Numbers를 통해 숫자 변환 및 검증
  • 최종 합계 게산(sum())

CalculatorController는 이 최종 합계 계산값을 단순히 사용자에게 전달하는 역할만 하게 했다.

이렇게 나누고 나니 테스트가 훨씬 수월해졌다.
CalculatorServiceTest에서 입력값만 바꾸면 실제 프로그램의 흐름이 올바른지 검증할 수 있었기 때문이다.
각 구성 요소가 내부에서 무엇을 하는지는 몰라도 결과를 통해 전체 로직이 올바르게 동작함을 보장할 수 있었다.

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
39
40
41
private CalculatorService calculatorService;

@BeforeEach
void setUp() {
  calculatorService = new CalculatorService(new DelimiterExtractor());
}

@Test
@DisplayName("기본 구분자(쉼표, 콜론)로 합계 계산")
void 기본_구분자로_합계_계산() {
  assertCalculatedResult("3:4,5", 12);
}

@Test
@DisplayName("커스텀 구분자 사용 시 합계 계산")
void 커스텀_구분자로_합계_계산() {
  assertCalculatedResult("//#\n3#4#5", 12);
}

@Test
@DisplayName("빈 문자열 입력 시 결과는 0")
void 빈_문자열_입력() {
  assertCalculatedResult("    ", 0);
}

@Test
@DisplayName("커스텀 구분자 두개 이상 입력된 경우 예외 발생")
void 커스텀구분자_두개이상_입력_예외발생() {
  // given
  String input = "//#;\n3#2;4";

  // when & then
  assertThatThrownBy(() -> calculatorService.calculate(input))
        .isInstanceOf(InvalidInputException.class)
        .hasMessage(ErrorMessage.INVALID_CUSTOM_DELIMITER_FORMAT);
}

private void assertCalculatedResult(String input, int expected) {
  int result = calculatorService.calculate(input);
  assertThat(result).isEqualTo(expected);
}


제출하기 전 다시 과제 요구사항을 꼼꼼히 읽어보다가 기본 구분자 외에 커스텀 구분자를 지정할 수 있다라는 문구를 잘못 해석하고 있었다는 것을 깨달았다. 그동안 나는 ‘커스텀 구분자는 어떤 문자든 지정할 수 있다’라고 이해했는데, 사실 이 말은 ‘커스텀 구분자는 기본 구분자(, :)와 겹치면 안된다는 뜻이었다.

이 부분은 InputValidator안에서 처리하도록 수정했다.
예외 처리를 한 곳에 모아두었기 때문에, 수정이 필요할 때 InputValidator 클래스만 변경하면 되었고, 이를 통해 구조적 분리의 유용함을 직접 느낄 수 있었다.

1
2
3
4
5
6
private static final String comma = ",";
private static final String colon = ":";

if (customDelimiter.equals(comma) || customDelimiter.equals(colon)) {
  throw new InvalidInputException(ErrorMessage.DUPLICATE_DEFAULT_DELIMITER);
}


최종 문자열 덧셈 계산기 흐름

리팩토링을 하면서 가장 많이 들었던 고민은 “내가 너무 많은 클래스로 쪼개는건 아닐까?”였다.
폴더가 많아지고, 파일 수도 늘어나면서 순간 불필요하게 복잡해진 것 같다는 생각도 들었다.

하지만 반대로 수정이 필요할 때 “어디를 고쳐야할지” 명확히 보였고,
각 클래스의 역할이 분명하니 전체 구조를 이해하기가 훨씬 쉬워졌다.

아직 완벽하다고는 할 수 없지만, 이 고민 자체가 더 나은 설계로 이끌고 있다고 생각한다. (그렇게 믿고 싶다)


깃허브 레포 링크 : java-calculator-8

© sihyun. Some rights reserved.