3주차 미션은 지금까지 진행한 과제 중 가장 어려웠습니다.
단순히 기능을 구현하는 것이 아니라, 좋은 설계란 무엇인지에 대해 스스로 답을 찾아야 했던 시간이었다고 생가합니다.
2주차에 내렸던 여러 결정들을 다시 돌아보고, “왜 그렇게 설계했는가”, “더 나은 방향은 무엇일까”를 깊이 고민했습니다.
그 과정에서 객체지향 설계 원칙을 조금 더 이해가 아닌 실천의 단계로 옮길 수 있었습니다.
2주차 미션에서는 InputView, OutputView를 모두 static 메소드로 구성된 유틸리티 클래스로 설계했습니다.
당시에는 “View는 상태가 없고 단순히 입출력만 담당하니, 객체 생성은 불필요한 비용”이라고 생각했습니다.
하지만 이번 주차에서는 이 설계가 가진 한계를 느꼈습니다.
static 메소드는 구체적인 클래스에 직접 의존하게 되므로, 이는 의존관계 역전 원칙(DIP)을 위반합니다.
Controller가 특정 View 구현체에 묶이는 구조였습니다.
인스턴스로 변경한 후에는 의존성을 주입받아 유연하고 테스트 가능한 구조로 바꿀 수 있었습니다.
뿐만 아니라, 인스턴스 클래스는 상태를 가질 수 있어 “라운드 결과를 누적해 한 번에 출력”하는 새로운 요구사항에도 대응할 수 있게 되었습니다.
생명주기 관리 측면에서도 명확한 차이를 느꼈습니다.
static은 애플리케이션 전체에서 유지되지만, 인스턴스는 생성과 소멸 시점이 명확합니다.
Console.close()처럼 리소스를 해제해야 하는 상황에서 Controller가 책임을 명확히 질 수 있게 되었습니다.
결국 단순 static을 없앤 것이 아니라, 유연한 의존성 주입 구조와 생명주기 관리 책임을 명확히 한 객체지향적 개선이었다고 생각합니다.
2주차의 선택이 틀렸다고 보진 않지만, 이번 주차를 통해 더 유지보수하기 좋고 확장 가능한 코드란 무엇인지 고민할 수 있었습니다.
애플리케이션의 예외 메시지를 일관되게 관리하기 위해 처음에는 public static final String 상수로 정의된 유틸리티 클래스를 사용했습니다. 하지만 이 방식은 문자열 타입의 한계 때문에 컴파일러가 의도치 않은 문자열과 구분하지 못하고, [ERROR] 접두사를 변경할 때 모든 상수를 수정해야 하는 불편함이 있었습니다.
이를 해결하기 위해 enum을 도입했습니다.
enum은 단순한 상수가 아니라, 상태와 행위를 함께 가지는 객체이기 때문입니다.
이전에는 각 메시지가 접두가 [ERROR]를 직접 포함했지만, 이제는 enum의 getMessage() 메소드가 접두사 처리를 담당합니다.
이로써 메시지 포맷 변경은 한 곳만 수정하면 되고, 코드의 의도도 명확해졌스빈다.
throw new IllegalArgumentException(ErrorMessage.INVALID_UNIT.getMessage());
이 한 줄만으로도 “INVALID_UNIT 에러를 발생시킨다”는 의도를 코드 자체로 표현할 수 있었습니다.
결과적으로 enum 도입은 단순한 구조 변경을 넘어, 유지보수성과 안정성을 모두 높이는 설계적 진전이었다고 느꼈습니다.
이번 주차에서 가장 오랜 시간 고민했던 부분이 바로 이 질문이었습니다.
“View가 Domain 객체를 알아도 되는가?”
계층형 아키텍처 원칙에 따르면 View는 Domain에 의존해서는 안 됩니다.
그래서 처음에는 Controller가 Domain 객체를 받아 DTO로 변환해 View에 전달하는 방식을 설계했습니다.
하지만 이 방식은 규모가 작은 현재 프로젝트에서는 오히려 불필요한 복잡성을 유발했습니다.
결국 저는 “제한된 규칙 하에 제어된 의존성은 허용하자”는 결론에 도달했습니다.
즉, View는 Domain의 상태 조회용 메소드만 사용하고, 상태 변경이나 비즈니스 로직 호출은 금지하는 규칙을 세웠습니다.
이 방식은 DTO를 만들 필요 없이 간결한 구조를 유지하면서도, Domain의 로직이 View로 새어 나오지 않게 방어할 수 있었습니다.
무조건적인 분리보다, 프로젝트의 규모와 복잡도에 맞는 균형 잡힌 설계가 중요했습니다.
LottoRank enum을 설계하면서 “등수를 판별하는 로직을 어디에 둘 것인가?”를 고민했습니다.
처음에는 valueOf 메소드 내부에 모든 if문을 작성했지만, 이 방식은 코드가 비대해지고 개방-폐쇄 원칙(OCP)을 위반했습니다.
그래서 각 enum 상수가 자신의 판별 로직을 직접 가지도록 설계했습니다.
이를 위해 BiPredicate<Integer, Boolean>을 활용하여 일치 개수와 보너스 여부를 판단하도록 했습니다.
이렇게 구현하니, 새로운 규칙이 추가되어도 기존 코드를 수정할 필요 없이 새로운 상수만 추가하면 되었습니다.
각 Rank가 “등수 정보”뿐 아니라 “자신이 등수가 되는 조건”까지 알고 있게 되어, 응집도 높은 객체 설계를 이룰 수 있었습니다.
이제 마지막 미션은 오픈미션입니다.
정말 고민이 많아지지만… 지치지 않고 꾸준히 앞으로 나아가려고 노력중입니다.
코드: https://github.com/giwoong01/java-lotto-8/tree/giwoong01
PR: https://github.com/woowacourse-precourse/java-lotto-8/pull/307