목표
이번 3주차 미션에서의 개인 목표는 기능을 구현하는 이유와 방식에 대한 명확한 설명이 가능한 이유 있는 설계를 지향하는 것이었습니다. 개인 목표를 추구하다보니 공통 목표 또한 자연스럽게 사고할 수 있는 시간이 되었습니다.
이유 있는 설계를 위한 노력
이번 3주차 미션에서는 단순히 기능을 구현하는 데서 그치지 않고, '이유 있는 설계'를 하기 위해 여러 도전을 진행했습니다. 설계 과정에서 겪은 시행착오를 하면서, 객체의 책임을 명확히 하고 코드의 응집도를 어떻게 하면 높일 수 있는지 스스로 생각해보는 시간이 되었습니다.
도메인 모델 패턴 적용
저는 이번 미션에서 2주차의 목표였던 ‘이유 있는 설계’를 실천하기 위해 여러 디자인 패턴을 학습한 후, 그중 핵심 구조를 구성하는 패턴으로 '도메인 모델 패턴'을 적용했습니다. 도메인 모델 패턴은 비즈니스 규칙과 로직을 객체로 모델링하여 도메인 중심으로 애플리케이션을 설계하는 방식입니다. 이는 서비스 계층에 비즈니스 로직이 집중되는 구조를 피하고, 각 객체가 스스로의 상태와 행위를 책임지도록 구성할 수 있습니다. 2주차에서 MVC 패턴을 적용했을 때, 서비스 계층과 도메인의 역할이 충돌하는 문제를 경험했습니다. 이를 개선하기 위해 이번 미션에서는 도메인 내부에 행위를 직접 정의해 객체 스스로가 상태를 변경하고, 유효성 검증 및 규칙을 관리하도록 설계했습니다. 이렇게 하여 객체 간 신뢰할 수 있는 협력을 증가시키고, 비즈니스 로직의 응집도를 높일 수 있었습니다.
재시도 패턴에 while문 사용
그리고 사용자의 입력이 잘못된 경우 예외를 발생시키고, 에러 메시지를 출력한 뒤 동일한 단계부터 다시 입력을 받을 수 있도록 구현하는 방법에 대해서도 고민했습니다. 이를 구현하는 방법에는 두 가지가 있는데, 예외 발생 시 자기 자신을 다시 호출하는 재귀 방식과 while 반복문을 이용하는 방식이 있습니다. 이때 재귀 방식을 사용하면 호출 스택이 계속 쌓이게 되어 반복적인 예외 입력 상황에서 StackOverflowError가 발생할 위험이 있습니다. 반면 while문은 단일 스택 프레임 내에서 반복 처리가 가능하기 때문에 메모리 부담이 적고, 프로그램의 안정성을 확보할 수 있습니다. 따라서 입력 검증과 재시도 로직에는 while문을 사용하는 것이 더 안전한 선택이라 판단하여 while문을 이용해 재시도 패턴을 구현했습니다.
생성자 대신 정적 팩토리 메서드 패턴 사용
마지막으로, 생성자를 통해 도메인 객체를 생성하려 했지만 사용자의 입력값을 기반으로 객체를 만들 때 한 가지 문제를 발견했습니다. 바로 객체를 생성하기 전에 입력값의 유효성을 검사해야 했는데, 이를 객체 외부에서 수행하는 것은 객체 지향의 핵심 원칙인 캡슐화와 단일 책임 원칙에 어긋나는 문제였습니다. 객체는 자신의 불변 조건과 유효성을 스스로 보장해야 하며, 외부에서 그 검증을 담당하는 것은 책임이 분산되고 코드의 일관성을 해치게 된다 생각했습니다. 따라서 생성자를 private으로 감추고, 대신 from()이라는 정적 팩토리 메서드를 두어 객체 생성 전 필요한 검증을 내부에서 처리하도록 설계했습니다. 이렇게 하면 객체 생성 과정이 명확히 캡슐화되어 외부에서는 검증이나 로직을 신경 쓸 필요 없이, Lotto.from(String input)처럼 직관적인 형태로 안전하게 객체를 생성할 수 있습니다. 그리고 정적 팩토리 메서드는 생성자보다 메서드명으로 생성 의도를 명확히 표현할 수 있다는 장점이 있습니다. 결과적으로 from() 메서드를 사용함으로써 객체 생성의 책임을 도메인 내부로 옮겨, 코드의 응집도를 높이고 객체 스스로 일관된 상태를 유지하도록 할 수 있었습니다.
아쉬웠던 점
첫 번째로 기초 설계가 부족하여 리팩토링에서 큰 비용이 발생했습니다. 초반에는 LottoMachine 클래스에 로또 발행, 당첨 통계 계산, 수익률 계산 등 여러 책임을 한꺼번에 부여했습니다. 그러나 구현 과정에서 당첨 번호와 보너스 번호가 필요한 시점보다 로또 발행이 먼저 이루어져야 하는 문제를 발견했습니다. 이 문제를 해결하기 위해 LottoMachine에는 오직 ‘로또 발행’의 책임만 남기고, 당첨 결과와 수익률을 계산하는 책임은 새로운 클래스인 LottoAnalyzer로 분리했습니다. 이를 통해 각 클래스가 명확한 역할을 가지게 되었고, 코드의 흐름과 유지보수성이 한층 개선되었습니다. 처음에 프로그램의 흐름을 정확히 이해하고 설계했더라면 일어나지 않았을 일이라 이번 일을 통해 기초 설계을 더욱 단단히 해야겠다고 생각했습니다.
두 번째로 객체 간의 유기적인 연결을 고려하는 사고가 얼마나 중요한지도 깨달았습니다. 초반에는 프로그램 전체 구조를 바라보기보다 눈앞의 기능 구현에만 집중하며 필요한 도메인만을 생각했습니다. 하지만 이런 방식으로 개발을 진행하다 보니 후반부로 갈수록 객체 간 결합도가 높아지고, 공통 상수를 중복 정의하는 등 코드 관리가 복잡해졌습니다. 이후 리팩토링을 통해 각 객체의 관계를 재정의하여 공통 상수를 별도로 관리하면서 구조를 정돈했습니다. 이 과정을 통해 단순히 기능을 완성하는 것이 아니라 객체 간 협력과 책임 분리를 고려한 설계가 장기적인 유지보수 비용을 줄이는 핵심이라는 점을 배웠습니다.
세 번째로 이번 미션에서도 TDD를 적용하며 이전보다 한층 체계적인 개발 프로세스를 확립할 수 있었습니다. TDD의 원칙을 충실히 지키기 위해 테스트 작성, 테스트 통과를 위한 기능 구현, 커밋의 순서를 엄격히 따랐으며, 그 결과 총 154개의 커밋이 쌓였습니다. 이러한 과정을 통해 테스트 주도 개발의 흐름을 몸에 익히고, 각 단계에서 무엇이 부족한지를 직접 체감할 수 있었습니다. 다만 테스트 단위별로 세밀하게 커밋을 관리하다 보니, 구현의 세부적인 완성도나 초기 설계에 집중할 에너지가 다소 분산되었다는 아쉬움도 있었습니다. 이번 경험을 통해 TDD를 단순히 절차로 따르는 것이 아니라, 설계와 구현의 균형을 고려하며 적용하는 것이 중요하다는 점을 배웠습니다.
개선 방향과 다음 목표
3주차 목표였던 이유 있는 설계를 하자는 목표에 도전하면서 저는 기초 설계의 중요성에 대해 많이 느꼈습니다. 디자인 패턴은 설계를 돕는 유용한 도구이지만, 그것만으로는 충분하지 않다는 것도 느꼈습니다. 객체들의 협력을 이용해 큰 그림을 그리기 위해서는 큰 그림에서 어떤 것들이 중요한건지 파악해야 하고, 프로그램의 시작부터 끝까지 어떤 것들이 필요한지 코드로 옮기기 전에 완성해야한다는 것을 알았습니다.
따라서 4주차에는 구현에 앞서 객체들의 협력 관계와 필요한 구성 요소를 먼저 정의하고 정리하는 연습을 집중적으로 할 계획입니다. 이를 통해 기능 구현 이전에 전체 흐름을 명확히 설계하고, 더 나아가 저만의 일관된 개발 프로세스와 스타일을 확립하는 것을 목표로 삼고자 합니다.