본문 바로가기

[우아한테크코스] 8기 프리코스

[우아한테크코스] 8기 프리코스 - 1차 미션 회고록

🧭 우테코 1차 미션 회고록 — 문자열 덧셈 계산기 구현기

1️⃣ 프로젝트 개요

이번 미션의 목표는 “입력된 문자열을 분석해 숫자를 덧셈하는 계산기”를 직접 구현하는 것이었다. 단순한 산술 연산보다는 입력 파싱, 커스텀 구분자 처리, 예외 처리, 타입 분기, 리팩토링 등을 경험하는 것이 핵심이었다.

문자열 입력에서 시작해, 정수·실수 구분, 구분자 처리, 계산 로직 분리까지 전 과정을 설계하고 구현했다.


2️⃣ 개발 과정 요약

단계 주요 작업 학습 포인트
1단계 ConsoleScanner를 이용한 입력 구조 설계 입력 스트림 처리의 제약 이해
2단계 CustomInputStream, CustomScanner 제작 시도 FilterInputStream과 Reflection 실험
3단계 Reflection을 통한 Console 내부 필드 접근 시도 Java 모듈 시스템 이후 접근 제약 확인
4단계 정규식 기반 구분자 탐색 시도 실패 → 인덱스 기반 파싱 전환 정규식의 Java 런타임 차이 분석
5단계 String.split() 기반 계산기 프로토타입 완성 Scanner 기반 대비 간결함 검증
6단계 타입 분리 (Integer, BigDecimal) 및 팩토리 도입 다형성과 확장성 고려한 설계 도입
7단계 리팩토링 및 테스트 역할 분리, 예외 처리 정교화

3️⃣ 문제 해결 과정과 시행착오

(1) Reflection의 유혹과 한계

초기에는 camp.nextstep.edu.missionutils.Console의 내부 Scanner 객체에 직접 접근해 useDelimiter()를 재정의하려 했다.

그러나 Java 9 이후 모듈 시스템 도입으로 IllegalAccessException이 빈번히 발생했다.

또한 Reflection은 유지보수성과 테스트 안정성을 해친다는 점을 명확히 체감했다.

교훈: “프레임워크 내부 동작을 바꾸기보다, 명시적으로 주어진 API를 활용하는 방향이 장기적으로 안전하다.”


(2) 정규식과 Matcher 클래스의 함정

정규식을 활용해 "//""\n" 사이의 custom 구분자를 추출하려 했지만, Java 런타임 환경에서 Matcher.find()가 항상 실패했다.

  1. 콘솔창 출력 및 입력 받기
  2. 구분자 문자열 구하기 : “//” 과 “\n” 사이

→ 정규식으로 구하려고 하는 데 해결이 안됨

        String todoRegex = "^//(.*)\\n";

        Pattern pattern = Pattern.compile(todoRegex);
        String regex = pattern.pattern();
        log.info(regex);
        Matcher matcher = pattern.matcher(input);
       try {
           matcher.find();
           String delimiter = matcher.group();
       }
       catch (IllegalStateException e) {
           log.info(e.getMessage());
       }
        log.info("구분자: " + matcher.group());
        return matcher.group();

 

정규식 테스트 사이트에서는 통과되지만, 실제 코드에서는 실패했다.

→ 원인은 이스케이프 문자 처리("\\n")와 입력 문자열 인코딩의 불일치였다.

결국 단순하고 확실한 방법으로 인덱스 기반 substring 추출 방식으로 전환했다.

이 과정에서 “복잡한 정규식보다 명시적 인덱스 탐색이 디버깅과 유지보수에 유리하다”는 점을 배웠다.

public String concatDelimiter(String input) {
        // 구분자 마지막 인덱스
        int end = input.indexOf("\\n") - 1;
        // 구분자의 글자 수가 1 일때
        if (end == 3) {
            char delimiter = input.charAt(2);
            return Character.toString(delimiter);
        }
        return input.substring(2, end + 1);
    }

(3) Scanner vs String.split()

Scanner는 입력 스트림 단위로 처리할 수 있어 매력적이었지만, 구분자 정규식을 동적으로 교체하거나 순서를 검증하는 데 어려움이 있었다.

반면, String.split()은 문자열 전처리에 강했다.

→ 두 방식을 비교하며, “입력 스트림 제어와 문자열 조작은 서로 다른 책임”임을 인식했다.

Scanner는 입력 흐름을 다루고, split은 문자열을 다룬다.

따라서 최종 구현에서는 Scanner를 파싱 전용 객체(NumberScanner)로 추상화해, 두 접근의 장점을 결합했다.

public class NumberScanner {
    private final Scanner scanner;

    NumberScanner(Scanner scanner) {
        this.scanner = scanner;
    }

    public void setDelimiter(String delimiter) {
        // delimiter 변경(, 또는 : 또는 custom)
        if (delimiter.equals(".")) {
            this.scanner.useDelimiter("[,.:]");
            return;
        }
        this.scanner.useDelimiter(",|:|" + delimiter);
    }

    public void setDelimiter() {
        // delimiter 변경(, 또는 :)
        this.scanner.useDelimiter("[,:]");
    }

        // 제네릭 사용해 덧셈하는 메서드
    public <T extends Number> List<T> parseList(Function<String, T> mapper) {
        ArrayList<T> list = new ArrayList<>();
        try {
            while (this.scanner.hasNext()) {
                String num = this.scanner.next();
                list.add(mapper.apply(num));
            }
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Invalid Input");
        }
        return list;
    }
}

(4) 타입 분기 설계 — Integer vs BigDecimal

  • "1.5,2.0"BigDecimal 계산기로
  • "1,2,3"Integer 계산기로

이를 위해 PatternMatcher.hasDecimalPoint() 메서드를 통해 실수 존재 여부를 판단했고,

덧셈 계산기지만, 입력에 소수점이 포함될 가능성이 있었다.

CalculatorFactory를 통해 타입별 계산기를 주입받는 구조로 리팩토링했다.

확장성: 나중에 “곱셈 계산기”, “나눗셈 계산기” 등을 손쉽게 추가할 수 있게 되었다.

안정성: 부동소수점 오차를 피하기 위해 BigDecimal을 사용했다.

public class CalculatorFactory   {
    public final static String INTEGER = "Integer";
    public final static String BIG_DECIMAL = "BigDecimal";

    public static <T extends Number> Calculator calculate(String type) {
        return switch (type) {
            case INTEGER -> new IntegerCalculator();
            case BIG_DECIMAL -> new BigDecimalCalculator();
            default -> throw new IllegalArgumentException("Invalid Type");
        };
    }
}

(5) 예외 처리와 입력 유효성 검증

입력값이 비어 있거나, 숫자와 구분자의 순서가 어긋난 경우 예외를 던지도록 했다.

if (!scanner.next().matches(regex)) {
    throw new IllegalArgumentException("Invalid Order");
}

 

또한 공백은 0으로 처리해 예외를 최소화했다.

결과적으로 사용자 경험과 프로그램 안정성을 모두 확보했다.

4️⃣ 최종 구조와 리팩토링 포인트


클래스 역할
Application 프로그램의 진입점. 입력 분기 및 결과 출력
PatternMatcher 정규식 기반 판별 (hasCustomDelimiter, hasDecimalPoint)
DelimiterParser 커스텀 구분자 추출 및 입력 문자열 정제
NumberScanner Scanner를 래핑해 숫자 리스트로 파싱
CalculatorFactory 타입별 계산기 생성 (IntegerCalculator, BigDecimalCalculator)
Calculator (인터페이스) 공통 덧셈 로직 추상화

 

→ 각 책임이 명확하게 분리되었고, 테스트 가능성이 높아졌다.

→ “입력 → 파싱 → 계산 → 출력” 흐름이 단방향으로 깔끔하게 이어진다.


5️⃣ 실전 예시로 본 동작 흐름

입력 처리 단계 출력
1,2,3 기본 구분자(,) 사용 → IntegerCalculator로 합산 결과 : 6
//.\n1.5.2.3.5 커스텀 구분자(.) 감지 → 정수 계산기 사용 결과 : 11
//;\n1.2;3.4 구분자 ;, 실수 포함 → BigDecimalCalculator로 처리 결과 : 4.6
(빈 문자열) 예외 없이 0 처리 결과 : 0

6️⃣ 리팩토링을 통해 얻은 통찰

① 단일 책임 원칙(SRP)

  • DelimiterParser, PatternMatcher, NumberScanner 등 각 클래스가 하나의 책임만 가진다.

② 개방-폐쇄 원칙(OCP)

  • 새로운 계산 타입을 추가할 때 기존 로직 수정 없이 팩토리만 확장하면 된다.

③ 의존성 역전 원칙(DIP)

  • 구체적인 계산기(IntegerCalculator, BigDecimalCalculator)가 아니라, 인터페이스(Calculator)에 의존한다.

7️⃣ 배운 점과 다음 단계

주제 배운 점 다음 목표
Reflection 내부 구조 조작보다 명시적 주입 설계가 유지보수에 유리 Reflection은 테스트 목적 외 사용 자제
정규식 복잡한 패턴보다 인덱스 기반 탐색이 더 안정적일 수 있음 패턴 매칭 로직을 별도 유틸로 정리
타입 분기 입력 형태 기반으로 동적 타입 결정 설계 경험 연산자(+,-,*,/) 확장 고려
예외 처리 사용자 입력 불안정성을 예외로 포착 커스텀 예외 클래스 추가 계획
리팩토링 클래스 간 책임 분리가 설계 품질을 결정 단위 테스트 도입으로 안정성 확보

8️⃣ 결론 — “과정의 코드가 내 성장의 기록이었다”

이번 미션은 단순한 계산기 구현이 아니라 “문자열 파싱과 객체 설계의 본질”을 체감한 시간이었다.

처음에는 콘솔을 조작하는 기술적인 방법에 집중했지만, 결국 중요한 것은 명확한 책임 분리와 안전한 구조 설계였다.

리플렉션, 정규식, 입력 처리, 타입 분기, 예외 처리까지 — 하나의 기능을 완성하는 과정에서 수많은 선택과 포기를 경험했다.

마지막으로, 이번 미션에서 느낀 핵심 한 줄 요약은 다음과 같다.

“기술적 트릭보다 설계적 명료함이 유지보수성과 성장의 핵심이다.”


🧩 다음 개선 목표 체크리스트

체크 항목 설명
Calculator 인터페이스에 다른 연산(곱셈, 나눗셈) 추가 확장성 테스트
PatternMatcher에 정규식 예외 감지 강화 입력 오류 정확한 메시지 제공
NumberScanner 단위 테스트 작성 숫자 파싱 검증 자동화
DelimiterParser 리팩토링 인덱스 탐색 로직 단순화
로깅 개선 (java.util.logging → SLF4J) 유지보수성과 로그 레벨 관리 강화

이 회고는 단순히 완성된 코드가 아니라, “한 줄 한 줄의 시행착오가 나를 성장시킨 기록”이었다.

문제를 해결하는 과정에서 배운 원칙들은 앞으로의 모든 프로젝트에 적용될 나침반이 될 것이다