티스토리 뷰
지난 회고록에서 개인 목표를 아래와 같이 설정하였다.
구현 방식에 모든 가능성을 열어두고, 직접 적용해 본 후에 나의 기준으로 선택의 이유를 설명할 수 있도록 하자
코드리뷰를 받으며, 내가 당연하게 생각했던 코드 작성 방식에 대해 고민해본 적이 없다는 것을 느끼고 위와 같은 목표를 설정하였다. 그래서 이번 주차에는 내가 작성하는 모든 코드를 의심하며, 왜 이런 방식이 권장된다고 배웠는지 직접 경험해보고자 하였다.
따라서, 이번 주차에서는 각 기능 구현에 필요한 다양한 접근 방식을 비교 및 분석하고, 그 과정에서 체감한 장단점을 근거로 실제 구현 방안을 결정하였다. 따라서 이 시행착오 과정을 녹여보고자 한다.
간단한 2주차 과제 소개
이번 주차의 과제는 자동차 경주였다. 경주에 참여할 자동차와 라운드를 입력하고, 랜덤 값을 기반으로 한 전전 조건을 바탕으로 우승자를 결정하는 간단한 CLI 어플리케이션이다.
새롭게 시도한점
1. 기능 명세를 사용자 흐름이 아닌, 기능 종류 중심으로 작성하자.
지난 1주차 과제에서는 사용자 흐름을 중심으로 기능 명세서를 작성하였다. 이 방식은 기능 간의 조율과 전체적인 연계 흐름을 이해하기에는 용이했지만, 절차지향적인 코드 작성으로 편향되어 도메인 객체를 분리하는 데 어려움이 있었다
이에 따라, 이번 주차는 기능의 종류를 기반으로 명세서를 작성하였다. 이는 객체 분리의 용이성을 높이기 위한 선택으로, 기능을 책임 단위로 나누어 명세화하였다.
이 접근 방식 덕분에 구현 단계에서는 도메인 내부에 비즈니스 로직을 캡슐화하기 훨씬 쉽게 느껴졌다. 그러나 한편으로는 명세의 틀에 지나치게 얽매이다 보니, 구현 도중 변경이 필요할 때 구조를 유연하게 수정하기 어렵다는 한계도 느꼈다. 즉, 기능 명세 단계에서 이미 암묵적으로 클래스 구조를 결정한 채 개발을 시작하게 되어, 변경이 발생할 때 즉각적인 대응이 쉽지 않았다.
이처럼 두 가지 방식 모두 명확한 장단점이 존재함을 직접 체감할 수 있는 계기가 되었다. 결국, 이론으로만 들었을 때는 느끼기 어려운 점들을 직접 시도하고 경험함으로써, 기능 명세의 방향이 구현 구조와 변경 유연성에 미치는 영향을 직접 체감할 수 있었다.
이와 같은 나만의 기준을 직접 경험을 통해 하나씩 추가해가는 과정은 정말 중요한 것 같다.
2. 객체의 의미를 먼저 분석하고, 구현 방식을 선택하자
지난 1주차 코드리뷰에서 다음과 같은 피드백을 받았다.
InputValidator를 정적 클래스이면서 유틸성 클래스로 구현할 필요가 있을까?

InputValidator는 검증에 필요한 메소드들만 모아두고, 여러 클래스에서 공통적으로 사용된다고 판단하여 유틸성 클래스로 구현하였다.
처음에는 단순히 인스턴스를 만들기 부담스럽다는 이유로 static 메서드 형태를 선택했지만, 이 질문을 계기로 객체의 의미부터 다시 고민하게 되었다. InputValidator의 내부 메소드를 보면, 도메인 규칙이 포함된 검증 로직들이 있었다.

예를 들어, validatorHeader()는 이 어플리케이션의 입력 값에 대한 길이 검증과 포멧 검증이 포함되어 있다. 이는 입력 값 자체를 검증할때 사용되는 도메인 특화 검증 로직으로 보인다. (결국, 모든 도메인에 완전히 독립적인 메소드가 아닌 것으로 보였다.)
따라서, 해당 객체는 입력 값에서 사용되는 검증 로직만 지닌 일반 클래스로 구현하는 것이 더 올바르다는 판단을 하였다. 즉 어플리케이션 전역에서 사용되는 메소드를 모아둔 클래스가 아닌, 특정 도메인 클래스에서만 사용되는 로직인 것이다. 결국, static 키워드로 어플리케이션 전반에 공유될 필요가 없다.
이에 따라, 이번 과제에서는 Validator가 도메인 규칙을 판단한다면 도메인에 속해야 하고, 전체 도메인에서 공통적으로 사용되는 단순 참 거짓 판단 로직이라면 독립적인 유틸성 클래스로 분리하는 것이 적절하다는 기준을 세웠다.

이 판단을 바탕으로, 이번 주에는 도메인 규칙을 배제한 순수 검증 메소드만을 담은 Validator를 유틸성 클래스로 구현하고, 도메인 규칙은 도메인 내부로 캡슐화하였다.(이에 따라 클래스명도 도메인 독립적으로 InputValidator > Validator로 수정하였다.)
이 과정을 통해, static이냐, 인스턴스냐의 문제가 아닌, “이 객체가 어떤 역할을 해야 하는가”를 먼저 고민하고 구현 방식을 선택하는 것이 더 좋은 방향의 흐름이라고 느낄 수 있었다. 이전에는 구현 방식이 먼저 떠올랐다면, 지금은 객체의 의미와 역할을 명확히 정의하고 구현 방식을 찾아가는 과정이 중요함을 깨달은 경험이었다.
3. 도메인 객체를 VO로 구현할 것인가, Entity로 구현할 것인가?
사실 이번 과제의 가장 핵심 고민거리는 도메인 객체를 VO로 구현할지 Entity로 구현할지 선택하는 과정이었다.
먼저 VO 객체에 대해 알아보자. VO의 가장 큰 장점은 무엇일까? 바로 객체의 불변성을 보장할 수 있다는 점이다. VO객체는 한번 생성되면 그 어떤 필드도 더 이상 수정될 수 없다. 수정을 하기 위해서 메소드를 작성한다 하더라도, 내부 멤버변수를 수정한 이후 새로운 객체를 생성하여 반환한다. 즉, 생성자를 통한 객체 생성은 불가하며, 내부 메소드를 통해 객체를 조작하고 새로운 객체를 반환받는 방식이다. 이 방식은 결국, 런타임 도중 떠다니는 각 인스턴스는 절대 수정될 수 없음을 보장할 수 있고, 이는 다른 협업 개발자들도 신뢰할 수 있기에 용의하다.
VO는 또한, 식별자를 기반으로 객체를 비교하지 않는다. 고유 ID가 같은 객체를 같은 객체로 보지 않고, 같은 값을 가진 객체들끼리 동일한 객체라고 간주한다. (이를 동등성이라 한다.) 따라서, 객체에 고유 ID가 필요 없고 값이 같은 객체를 동일한 객체로 볼 수 있는 경우에 도입을 고려할 수 있다.
첨언. 나도 이번 기회에 처음으로 VO에 대해 학습해보았는데, 가장 명료했던 글을 첨부한다. VO(Value Object)란 무엇일까?
그렇다면 나는 왜, VO로 Car 객체를 구현하고자 하였을까?
요구사항을 다시 읽어보자. 요구 사항에 따르면 종료 시점에 각 라운드의 결과값을 모두 출력해야한다.
이 요구사항을 읽고 나는 각 라운드마다 자동차의 상태를 유지할 필요가 있다고 느꼈다. 즉, 각 라운드가 끝난 Car 하나 하나를 독립적으로 보고, VO로 구현해 각 라운드의 자동차 상태를 Cars 컬렉션에 저장하려고 하였다. 이는 결국 5대의 자동차가 5라운드동안 경주한다면, 총 25개의 자동차 객체를 런타임 도중 생성하게 된다. 이 구현 방식의 근거는 "요구사항에 따라, 각 라운드의 상태를 유지하는 것이 중요하다" 라는 관점에서 나온 결정이다.
그렇다면 왜 최종적으로 Car객체를 Entity로 구현하였는가?
첨언. 여기서 내가 말하는 Entity는 DB 테이블과 매핑되는 객체를 의미하는 것이 아니다. 비즈니스 로직을 담을 수 있으며, 메소드에 의해 런타임 도중 상태가 변화 가능한 객체를 가리키는 개념이다.
하지만 최종적으로 나는 Entity로 Car 객체를 구현하게 되었다. 구현하다보니, 요구사항에는 다음과 같은 문장이 있었다. "각 자동차는 각 라운드마다 조건에 따라 전진하거나 머무른다."
이 요구사항에 따르면, 자동차 객체는 매 라운드마다 위치 상태가 변화한다. 즉, 이로 인해 꼭 불변성의 이점을 챙기려고 VO로 구현할 필요가 있을까 하는 의심이 들게 되었다. 고민 끝에 이 도메인(Car)의 핵심은 불변성보다 상태의 변화 자체를 관리하는 것이라고 판단하였고, 이에 따라 Entity로 전환하였다. 다만 이로 인해 각 라운드의 상태 값을 저장하기 위한 응답 객체의 복잡성이 올라갔다는 단점도 느낄 수 있었다. (자세히 설명하면, Result라는 DTO를 작성하여 출력 view로 반환하였는데 vo로 설계하였을 때보다, 해당 객체의 복잡도가 올라갔다.)
결론적으로 무엇이 요구사항의 중심인가에 따라 전혀 다른 설계와 구현이 이뤄질 수 있음을 깨달았다.
결국 요구사항의 우선순위를 명확히 정의하고, 그 안에서 현실적인 타협점을 찾아가는 과정을 경험하였고, 모든 방식에는 정답이 없음을 깨닫게 되었다.
그리고 미션이 끝나고 나서 디스코드 글을 올렸다. (아래에는 해당 포스트에 대한 2분의 의견이 담겨있다.)


이 채널에서 가장 인상 깊었던 코멘트는 엔티티인지의 여부는 내부 상태 변화 가능 여부와 관계 없을 것 같다는 의견이었다. 또한, 간결함이 선택의 지표가 될 수도 있다는 점이었다.
결국, VO를 사용해서 얻는 이점인 불변과 동등성 비교가 해당 어플리케이션에서는 굳이 필요하지 않기에, 자연스럽고 직관적인 상태 변경 가능한 타입으로 작성하는 것이 더 이점이 있다는 점이었다. 같은 맥락으로 정답에 가까운 방향은 간결성에 있다라는 의견도 공감할 수 있었다. 내가 Entity를 택했었던 이유를 가장 명확하게 표현해주신 용어 같았다.
마지막으로..
마지막으로 이번 회고부터는 계속 유지할 점(Keep)에 대해서도 작성하며, 잘한 점에 대해서도 복기해보고자 한다. 결국 내가 가진 강점도 잘 유지하는 것도 중요하니까.
1. 의심하는 힘
이번 주차의 큰 방향성은 내가 기존에 베스트 프렉티스로 알고 있던 코드에 대해 의심하고 검증하는 과정이었다. 때로는 내가 알고 있던 것들이 나의 주관이 아닌, 어디서 주워들은 내용에 그친다는 사실을 프리코스를 통해 깨달았다. 결국 내가 생각하고 주장하는 모든 내용이 진정으로 나의 의견인지, 충분한 근거가 있는지 항상 의심하며 접근해보려고 한다.
2. 직접해보기
아무리 이론적인 내용이라도, 내가 직접 경험해보지 않으면 설명하기 어렵다. 기존에 디스코드 포스트를 작성할 때도 마찬가지였다. 내가 직접 경험을 통해 체득한 근거가 아닌, 이론 학습을 통해 얻어낸 근거라면 나 스스로도 설명하기 어렵다는 점을 느꼈다. 결국은 상반된 두가지 주장을 직접 경험해보며 요구사항에 맞게 유연하게 무기를 선택하는 힘이 더 중요함을 느끼고 있다.
마무리
무언가 회고록이 내가 무엇을 했냐의 경험에 포커싱 된 것 같다. 하지만 이 나름의 과정을 복기해보는 것만으로도 다시 한번 체화시키는데 도움이 되었다고 생각한다. 오늘도 한걸음 나아가고 내 생각을 명확하게 정리할 수 있었다는 것만으로도 감사하게 생각하며 다음 과제를 시작해보려한다.
(읽어주셔서 감사합니다! 좋은 하루 보내세요~)
'우아한테크코스' 카테고리의 다른 글
| 우아한테크코스 8기 프리코스 3주차 회고록 (0) | 2025.11.10 |
|---|---|
| 도메인 검증 프레임워크 Aegis 구현기 2편: 프레임워크의 부트스트랩이란? (0) | 2025.11.10 |
| 도메인 검증 프레임워크 Aegis 구현기 1편: 기능 명세 정의 (0) | 2025.11.08 |
| 우아한테크코스 8기 프리코스: 오픈미션 프리뷰 - 도전과 주제 정의하기 (0) | 2025.11.07 |
| 우아한테크코스 8기 프리코스 1주차 회고록 (0) | 2025.10.23 |
