10월 15일, 생일 선물처럼 우테코 7기 프리코스가 시작됐다.
1주 차 과제는 문자열 덧셈 계산기.
과제의 요구 사항들을 보자마자 든 생각은 '어.. 별로 안 어려울 것 같은데? 1주 차라 그런가 생각보다 간단한 걸 냈네?'였다. 하지만 이 때는 몰랐지 과제를 진행하면서 이것저것 시도해 보니 '절대 쉽지 않구나' 생각하게 될 줄은..😇
프리코스가 시작되기 3주 전, TDD에 익숙해지기 위해 한별언니와 6기 프리코스 과제 중 숫자 야구 게임, 자동차 경주, 로또를 미리 스터디했다. 그 덕분인지 과제 진행 방식 자체에 대해서는 어느 정도 익숙해진 채 프리코스를 시작할 수 있었다.
상속과 인터페이스
기능 요구 사항에 주어진 '커스텀 구분자'와 '기본 구분자'를 보고 '구분자'로 두 구분자를 통합할 수 있지 않을까 생각했다.
자바에서 활용할 수 있는 건 상속과 인터페이스였고, 그 중 어떤 것을 활용할지 정하기 위해 상속과 인터페이스의 차이점에 대해 알아보았다.
- 상속 : 상속을 받는 객체들이 공통된 상태를 공유하며, 상태를 기반으로 행동이 동작되거나 확장될 때 적합하다. 코드의 중복을 줄일 수 있다는 장점이 있다.
- 인터페이스 : 구현하는 객체들의 행동을 통합하려는 목적일 때 적합하다.
'커스텀 구분자'와 '기본 구분자'가 서로 공통된 '상태'를 가지고 있는가? 에 대한 물음에 아니라고 생각했고, '구분자'들에게 '구분하는 것'의 행동을 강제하는 것이 좋겠다고 생각하여 인터페이스를 활용하기로 했다.
일급 컬렉션
객체지향 생활체조 원칙에 따르면 모든 원시값과 문자열을 포장하는 것이 좋다고 한다.
public class User {
private String id;
private String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
}
위 코드와 같이 User라는 객체가 있다고 가정했을 때, User를 생성할 때 id와 name이 모두 String으로 같은 타입을 가지고 있다. 만약 User를 생성하는 곳에서 id에 name의 값을, name에 id의 값을 넣는 상황을 배제하기 위해 아래와 같이 원시값과 문자열을 포장해 준다.
public class Id {
private String id;
public Id(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
public class Name {
private String name;
public Name(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class User {
private Id id;
private Name name;
public User(Id id, Name name) {
this.id = id;
this.name = name;
}
}
이렇게 원시값과 문자열을 포장한다면, 같은 타입의 id와 name이어도 바꾸어 입력하면 예외가 발생하므로 예외 처리가 가능하게 되어 잘못된 값으로 객체가 생성되지 않는다.
일급 컬렉션도 이와 비슷한 이유 때문에 사용한다고 한다.
커스텀 구분자와 기본 구분자는 그리고 구분자를 나눈 숫자는 여러 개가 될 수 있고, 동일한 유효성 검증을 필요로 하기 때문에 커스텀 구분자, 기본 구분자, 숫자를 일급 컬렉션으로 관리하기로 결정했다.
다만, 일급 컬렉션 내부의 구분자들과 숫자들은 위 예시의 User와 같이 동일한 타입의 필드값을 필요로 하지 않고 구분자는 String, 숫자는 int 뿐이니 일급 컬렉션을 위한 객체를 만들지는 않기로 했다.
유효성 검증
유효성 검증은 언제 하는 것이 좋을까? 처음엔 객체를 생성하기 전에 유효성을 검증하고 객체를 생성하는 것이 좋지 않을까 생각했다. 하지만 유효성 검증은 객체가 생성된 이후(필드가 초기화된 이후)에 하는 것이 안전하다고 한다. 객체의 상태가 완전히 설정된 이후에 검증을 수행해야 로직이 정상적으로 작동되기 때문이라고 한다.
객체 생성 전 검증을 하고 싶다면 '정적 팩토리 메서드'를 활용하여 검증하는 것이 좋다. 다만 이 경우 객체 내부에서 검증을 수행해야 하므로 코드의 중복이 발생할 수 있다.
class가 하는 일도 명확히 구분해주어야 한다고 생각했기 때문에, 객체 내부에서는 객체가 하는 일만 담당하고 유효성 검증은 별도의 클래스로 분리하고, 객체 생성 직후에 유효성 검증을 해주기로 했다.
설계 먼저? 구현 먼저?
상속과 인터페이스, 일급 컬렉션, 유효성 검증 등을 먼저 떠올린 것을 보면 나는 설계를 먼저 한 것이라고 볼 수 있다. 실제로 기능 목록과 테스트 목록은 간단히 작성한 후에 어떤 클래스들과 패키지를 만들지 상세하게 설계했었다.

그런데 이렇게 정해두고 구현을 하다 보니 더 구조가 잡히지 않는 것 같았다. 설계에 대해 생각하다 보니 예외 케이스들만 잔뜩 떠올랐고, 생각이 너무 많아졌다.
그래서 우선, 우테코에서 제공된 기본 케이스들만 만족할 수 있는 간단한 구현을 해보자고 생각했다.
객체고 뭐고 다 필요 없고, 일단 main에 기본 타입들을 가지고 구현을 시작했다.
맨 처음 과제를 봤을 때 생각보다 쉬운데?라고 생각했던 이유가 있긴 했었다. 단순히 돌아가게만 만들어보니 생각보다 금방 구현할 수 있었다. 구현한 내용을 바탕으로 이 타입은 이 객체로, 저 타입은 저 객체로 만들면 되겠다는 생각이 들며 자연스레 설계로 연결시킬 수 있었다.
앞서 3주 간 진행했던 스터디에서는 TDD에 익숙해지자고 테스트 케이스를 먼저 생각하고, 그 테스트 케이스에 따라 기능을 구현하고 설계하는 방식으로 공부했었다. 다만 프리코스를 진행하면서 나는 설계에 대한 부분도 자연스럽게 설계할 수 있도록 익히고 싶기 때문에 앞으로도 주어진 최소한의 테스트 케이스를 만족하는 프로그램을 구현하고, 그 이후에 추가 테스트 케이스를 생각하는 것이 좋겠다.
객체가 하는 일
최소한의 기능 구현을 완료하고 나니 그렇다면 객체에게 일을 어디까지 줘야 할까? 생각하게 됐다. 특히 초기 설계 구조에서 커스텀 구분자에게 너무 많은 일을 준 것 같았다.
- 커스텀 구분자를 포함한 입력 문자를 구분자 부분과 계산 부분으로 나누는 일, 계산 부분을 구분자와 숫자로 나누는 일
기본 구분자처럼 커스텀 구분자도 계산 부분을 구분자와 숫자로 구분하는 일만 하면 안 되는 걸까?
Controller의 역할과 Service를 사용한 이유
커스텀 구분자의 일을 줄여주는 것부터 시작해서 Controller에게도 너무 많은 일을 준 것은 아닐지 걱정이 됐다. 웹으로 생각했을 때, MVC 패턴에서 Controller의 일을 나누어 줄 수 있다면 그게 Service가 되어야 한다.
MVC 패턴은 Model, View, Controller로 구성되며, Model과 View는 서로를 몰라야 하고, 중간에서 Controller를 통해 소통한다. 사용자는 View를 통해 입력을 하고, Controller는 그 입력을 받아 Model에게 전달한다. 그렇다면 사용자의 입력을 Controller가 그대로 전달하지는 않을 것인데, 어디에서 바꾸어 전달해주어야 할까?
Service이다. Service는 사용자로부터 받은 입력값을 Controller로부터 전달받아 Model이 사용하기 좋게 변형한다.
기존 구조에서는 사용자 입력이 커스텀 구분자 객체에 변형 없이 그대로 전달되어 커스텀 구분자 객체가 하는 일이 너무 많았다.
그래서 Service를 도입하고, 사용자로부터 받은 입력값을 Controller에서 Service로 넘겨 Model이 활용할 수 있도록 조작, 변형하도록 했다.
의존성 주입(Dependency Injection, DI)
Controller를 설계할 때, 사용되는 객체들을 어떻게 생성해야 할지 고민하면서 의존성 주입에 대해 찾아보게 되었다. 일반적으로 java를 공부할 때, 객체 생성 = new라고 배운다. 그래서 Controller 내부에서 필요로 하는 객체들을 생성할 때도 아무 생각 없이 new를 사용하여 객체를 생성하고 사용했다.
그런데 문득 의문이 들었다. 스프링으로 프로젝트를 진행했을 땐 의존성 주입을 통해 클래스 간 결합도를 약하게 해 주었는데, 이런 콘솔 프로그램에서도 의존성 주입을 해주어야 하는가?
1주 차의 학습 목표를 다시 한번 돌아보니, "Git, GitHub, IDE 등 '실제 개발'을 위한 환경에 익숙해진다."라고 되어 있었다.
그렇다면 주로 실제 개발 환경이 될 스프링 프레임워크를 사용하는 것처럼 의존성 주입을 해주는 것이 맞지 않을까 생각했다.
new로 객체를 생성하는 경우의 단점은 무엇일까?
- 단위 테스트가 어려워진다.
: new로 객체를 생성할 경우 객체 간 결합도가 증가하므로 테스트가 어려워진다. 실제로 Controller에서 new로 객체를 생성했을 때 단위 테스트를 진행하면 테스트 코드 내부에서 항상 필요한 객체를 새로 생성해주어야 하는 불편함이 있었다. - 유지 보수성이 떨어진다.
: 코드가 변경될 경우 new로 생성한 모든 객체들을 모두 수정해주어야 한다. 의존성 주입을 하게 되면 코드 수정 시 영향을 받는 범위가 줄어든다. - 확장성이 낮아진다.
: 새로운 기능을 추가할 때 필요한 객체가 생긴다면 역시나 객체가 필요한 모든 곳에서 객체를 수정해주어야 한다. 의존성 주입을 활용하면 쉽게 객체를 주입할 수 있다.
의존성 주입의 장점은 new로 객체를 생성할 때의 단점과 완전히 상반된다. 테스트 용이성, 유연성, 확장성. 사용하지 않을 이유가 없다.
각 클래스의 역할을 명확히 하자
초기 설계 단계에서 놓친 것이 클래스의 역할을 명확히 하는 것이었다. 중복을 싫어하기 때문에 하나의 메서드에서 하나의 일만 하게 하는 것은 어느 정도 익숙해진 것 같은데, 클래스에게 역할을 주는 것에 대해서는 중요성을 느끼지 못했었다.
되도록이면 하나의 클래스에게도 한 가지의 역할만 주도록 노력했다.
- CalculatorController : 문자열 계산기 프로그램 전체 흐름 제어
- SeparatorsService : Separators 객체 생성, 커스텀 구분자의 구분자/계산 부분 분리
- NumbersService : Numbers 객체 생성
- Separators : 구분자 인터페이스, 구분자의 행동 제어
- CustomSeparators : 커스텀 구분자 데이터, 커스텀 구분자와 숫자를 분리
- DefaultSeparators : 기본 구분자 데이터, 기본 구분자와 숫자를 분리
- Numbers : 숫자 데이터
- Calculator : 숫자 계산
비즈니스 로직과 관련된 클래스들에게 최소한의 역할만 주도록 노력하니 구조가 명확해지는 느낌을 받았다. 이번주 프리코스 진행 과정에서 이 것을 깨닫기까지 이런저런 시행착오가 있었지만 앞으로의 프리코스에서 적용해 볼 생각을 하니 설렌다.
테스트 코드도 코드다
클래스별로 명확한 일을 주고 나니 테스트 코드도 클래스별로 단위 테스트하기가 훨씬 수월해졌다.
커스텀 구분자로 \(역슬래시)를 사용하지 못하도록 예외를 두었었는데, 정규식에서 사용되는 특수 문자도 커스텀 구분자로 사용할 수 없는 상황이 발생했다. 역슬래시를 허용하고, 정규식에서 사용하는 특수문자를 그대로 인식하도록 Pattern.quote()를 사용하도록 고쳐주었다. 이 과정에서 테스트 코드에도 버그가 있는 것을 확인했다.
실제 프로그램에서는 Service에서 구분자 부분과 계산 부분을 나누어서 커스텀 구분자와 숫자 객체를 생성하게 되는데, 테스트 코드에서는 강제로 나누는 코드를 넣어뒀었다. 그렇다 보니 여러 가지 메서드에서 잘못된 방식으로 구분자를 나누고 있었고, Service의 나누는 메서드를 활용하니 편-안 해졌다. 이게 바로 재사용성인가!
Integer의 범위
'문자열 덧셈 계산기'인데, 계산하는 범위에 대해서는 가장 마지막에 생각하게 되었다. 중요 비즈니스 로직인데, 실수였다.
Integer의 범위에 대해 찾아보니 -2,147,483,648 ~ 2,147,483,647 양수로 따지면 약 21억까지 가능하다.
21억을 넘지 않게 하기 위해서 입력 가능한 한 개의 숫자 범위는 최대 2억, 구분자로 구분할 경우 입력 가능한 숫자의 최대 개수는 10개로 제한했다.
일주일이 어떻게 지나간지 모르게 정말 빠르고 꽉차게 지나갔다.
우테코 지원서에 적었던 프리코스의 목표는 딱 두 가지였다.
- 건강하게 개발하기
- 숲과 나무를 모두 보기
국비를 수료하고 갑자기 아프게 되어서 3개월 동안 신나게 놀다가 다시 공부하려 하니 하고 싶은 게 너무너무 많다. 사실 이번 주를 돌아보면 건강하게 개발하기를 완전히 지키지 못한 것 같기도 하다. 물론 주 3회 이상 운동은 하고 있고, 최소 수면 시간도 지키고 있지만 밤낮이 바뀌어 오후 시간과 밤에 공부를 하고 있다.
남은 프리코스 기간 동안에는 밤낮을 되돌려 아침에 공부하는 시간도 확보하도록 노력해야겠다.
숲과 나무를 모두 보자는 목표는 제공된 요구 사항을 빠짐없이 구현하는 것이었는데, 이 목표는 1주차에 완벽히 지켜내려고 노력했고, 지켰다고 생각한다. 앞으로도 지금처럼만 꾸준히 노력하고 지켜나가도록 해야겠다.

작년엔 Git조차 사용할 줄 몰라서 1주차에서 포기했었다. 그래서 이 과제 제출 화면이 너무너무 보고 싶었는데, 테스트까지 잘 돌아가니 뿌듯한 마음이 가득하다.
생각보다 프리코스 과제에 굉장히 몰입하고 있어서 너무 재미있다. 내일은 어떤 과제가 나올지 기대된다!😆
1주차 프리코스 코드 레포지토리 : https://github.com/ljhee92/java-calculator-7/tree/ljhee92
'study > 우아한테크코스' 카테고리의 다른 글
| 우아한테크코스 7기 프리코스 3주 차 회고+ (부제: 왜요? 제가 0점 처리될 사람처럼 보이세요?) (2) | 2024.11.05 |
|---|---|
| 우아한테크코스 7기 프리코스 3주 차 회고 (9) | 2024.11.04 |
| 우아한테크코스 7기 프리코스 2주 차 회고 (2) | 2024.10.27 |
| 우테코 백엔드 6기 프리코스 포기! (3) | 2023.10.26 |
| 우아한테크코스 웹 백앤드 6기 지원 (0) | 2023.10.15 |