의인화 & 정보 전문가 패턴 & 리팩터링 _feat(우테코 - 로또)
우테코 2단계 level 2 로또 미션 중에 나는 매우 재미있는 경험을 했다.
이전에는 책에서 봤던 이론적인 내용을 말로는 표현할 수 있었지만, 나의 코드로는 적용하기가 어려웠다.
나는 무언가를 안다는 것은 아래 세가지 단계로 나뉜다고 생각한다. (내 생각이 아니라, 어디서 봤던 것 같기도 하고...)
- 개념을 알고 설명한다.
- 그 개념을 똑같이 사용할 수 있다.
- 그 개념을 다르게 활용할 수 있다.
나는 책에서 보았던 내용을 1단계 정도밖에 알지 못하는 것 같았다.
내가 읽었던 책은 조영호 저자의 객사오('객체지향의 사실과 오해') 와,
'오브젝트 - 코드로 이해하는 객체지향 설계',
그리고 마틴 파울러 저자의 '리팩터링 2판' 이다.
의인화 - 객사오
이 책의 66, 67 페이지에서는 아래와 같은 내용이 나온다.
객체지향 세계는 현실 세계의 단순한 모방이 아니다... (중략)
소프트웨어 객체를 창조할 때 우리는 결코 현실 세계의 객체를 모방하지 않는다.
객체 지향 세계를 구축할 때 현실에서 가져온 객체들은 현실 속에서는 할 수 없는 어떤 일이라도 할 수 있는 전지전능한 존재가 된다.
현실의 객체보다 더 많은 일을 할 수 있는 소프트웨어 객체의 특징을 의인화(antropormophism) 이라고 부른다... (중략)
객사오에서는 객체지향 프로그래밍에서는 결국 자율적인 객체들의 협력을 통해 시스템을 구성한다고 한다.
즉, 객체들이 자율성을 가진다는 것도 굉장히 중요하다!
이것은 약 8개월 전에 읽었던 내용이었다.
하지만 내 코드에 어떻게 적용해야 하는지 몰랐다.
step1 의 내 코드 구조는 이랬다.
`LotteryGenerator`, `LotteryRankEvaluator`, `LotteryResultEvaluator`, `ProfitStatusDecider` 는 지금 보았을 때 굉장히 이상한 클래스로 보인다.
이 객체들은 또 아무런 프로퍼티를 가지고 있지 않다.
또 `Lottery`, `LotteryNumber`, `Lotteries` 등의 객체들은 거의 데이터를 들고만 있어서 전혀 자율적이지 않다.
즉, 내 설계는 전혀 객체지향적이지 않다!!
깨달음
나는 우테코 프리코스 때부터 `LotteryGenerator` 와 같은 객체들의 네이밍에 굉장히 불만이 있었다.
이 객체는 결국 로또를 만들어야 하므로, `generateLottery` 라는 메서드를 가질 것이다.
즉, 클라이언트 코드에서는 `LotteryGenerator().generateLottery(...)` 의 모습을 가지게 된다.
나는 이 코드가 굉장히 중복적이라고 생각했으며 마음에 안 들었다.
그래서 (어떤 맥락이었는지는 기억이 잘 나지 않지만) 수업 중, 제이슨에게 "`LotteryGenerator` 라는 네이밍이 별로인 것 같아요. 너무 중복인 것 같아요!" 라고 이야기 했던 것 같다.
당연히 제이슨은 어떠한 해결책도 제시해주지는 않았다. (우리 스스로 알아내는 것이 바람직하기에...)
step1 이후, 리팩토링을 수행하다가, 옆 크루원의 코드가 잠깐 눈에 들어왔고, 갑자기 객사오의 의인화 라는 키워드가 생각이 났다!
그래서 급하게 책을 찾아보고 나서 어떻게 코드를 변경해야 할지 어렴풋이 감이 잡혔다.
오브젝트 - 코드로 이해하는 객체지향 설계
객체를 자율적인 존재로 만들기 위해, 객체에게 어떠한 책임을 객체에게 할당해야 했다.
그리고, 나는 어떤 객체에게 책임을 할당해야 할지 고민했다.
어...? 어떤 객체에게 책임을 할당해야 할지.. 라고???
나는 급하게 또 책을 찾아보았다.
오브젝트라는 책의 138, 139 페이지에서 아래와 같은 내용이 나온다.
책임을 객체에 할당하는 일반적인 원리는 무엇인가?
책임을 정보 전문가, 즉, 책임을 수행하는 데 필요한 정보를 가지고 있는 객체에게 할당하라.
GRASP 에서는 이를 INFORMATION EXPERT(정보 전문가)패턴이라고 부른다.
INFORMATION EXPERT 패턴은 객체가 자율적인 존재여야 한다는 사실을 다시 한번 상기시킨다....(중략)
이 구절을 다시 읽고 나서 빠르게 리팩터링을 이어나갔다.
`ProfitStatusDecider` 는 수익 상태를 결정한다.
수익 상태는 수익률을 통해 결정되기 때문에 이러한 책임은 수익률을 알고 있는 `ProfitRate` 에게 할당하면 된다.
로또 6자리와 당첨 번호 6자리를 비교해서 몇개가 일치하는 지 알아내는 책임 또한 `~~~Matcher` 나 `~~~MatchCounter` 가 할 필요가 없다.
그 역할을 수행하기 위한 정보를 가장 잘 알고 있는 `Lottery` 가 하면 된다.
이러한 식으로 어떠한 책임을 할당할 때 그 책임에 필요한 정보 전문가에게 책임을 할당해 나갔다.
그러다 보니 거짓말처럼 객체 간의 의존성이 간결해지고, 객체들이 자율성을 가지게 되었다.
리팩터링 2판
리팩터링을 하던 도중에 로또를 생성하는 `Lottery` 와 한 자리의 로또 번호를 포장하는 `Lotterynumber` 에서 로또 번호 범위 상수 1 ~ 45 를 알아야 한다는 것이 보였다.
나는 이 부분에서 불편함을 느꼈다.
자율적인 객체로 잘 설계되었다면 상수 정보를 한 객체에서만 알고 있을 수 있지 않을까? 라는 생각을 했다.
그래서 이 부분을 어떻게 리팩터링할 수 있을지 힌트를 얻기 위해서 마틴 파울러 저자의 '리팩터링 2판' 을 조금 찾아보았다.(당연히 다 읽지는 않았다.)
3.10 절 122 페이지에 이러한 내용이 나온다.
데이터 항목들은 ... 서로 어울려 노는 걸 좋아한다....
이렇게 몰려다니는 데이터 뭉치는 보금자리를 따로 마련해줘야 마땅하다....
새로운 클래스를 만들었다면, 이어서 그 클래스로 옮기면 좋을 동작은 없는지 살펴보자.
이러한 연계 과정은 종종 상당한 중복을 없애고 향후 개발을 가속하는 유용한 클래스를 탄생시키는 결과로 이어지기도 한다....
내 코드에서 당첨 로또 번호를 저장하는 `Lottery` 객체(`winningLottery`)와 보너스 번호를 저장하는 `LotteryNumber` 객체(`bonusNumber`) 가 계속 같이 나타나는 것이 보였다.
이것을 한 객체(클래스)로 묶어보니, 이 클래스로 옮기기 좋은 동작이 보였다.
data class WinningLottery(val lottery: Lottery, val bonusNumber: LotteryNumber)
이 객체는 당첨에 대한 모든 것을 알고 있기 때문에, 당첨 결과를 구하는 동작을 이 객체가 하면 되었다.
사실 이것 때문에 객체를 만든 것은 아니었는데... 다른 부분을 고민하다가 이 부분이 리팩터링되어서 구조가 더 깔끔해졌다!
그리고 '자율적인 객체로 잘 설계되었다면 상수 정보를 한 객체에서만 알고 있을 수 있지 않을까?' 에 대한 불편함은 결국 해결되지 않았다.
한 객체 내부에서 이를 상수로 관리하고 다른 객체에서 이 상수를 참조하도록 만들었다.
기존 코드 VS 리팩터링이 마무리된 코드
먼저 기존 코드 깃허브 링크이다.
위에서 코드를 작성했을 때 로또 미션의 핵심 도메인의 패키지 구조와 다이어그램을 보였다.
그리고 이번에는 리팩터링이 마무리된 코드 깃허브 링크와 핵심 도메인만을 표현한 다이어그램이다.
`~~er` 네이밍의 클래스가 제거된 모습이다.
실제로 '엘레강트 오브젝트' 라는 객체 지향 서적 1장에 클래스 이름에서 `~~ er`을 지향하라는 내용이 있다.
물론 논란이 많은 내용이지만, 로또 미션과 같은 작은 프로젝트에서 책의 내용을 적용해보는 노력은 객체지향적인 사고와 설계에 큰 도움이 될 것이라고 생각한다.
코치님의 말을 빌리면, "사실 지금 단계에서의 모든 구현에서 DTO, Service Layer 등을 하나도 필요없다" 라고 하셨다.
끝으로
제가 리팩터링을 완료한 코드가 완벽한 코드도 아니고, 그러한 코드가 있는지도 모르겠지만..
만약 우테코 미션을 진행하고 있는 분이 이 글을 읽는다면, 제 코드를 참조하지 않고 책의 내용을 발췌독한 후에, 직접 적용해보고 차이를 느껴보는 것을 추천드립니다.
성장의 기회 측면에서도 그것이 도움이 되겠지만,
무엇보다도 제가 리팩터링을 하면서 했던 경험이 제게는 너무나도 즐거웠던 경험이었기 때문이에요!!!
객체지향적인 사고와 설계에 재미를 느끼셨으면 좋겠다는 마음으로 글을 마무리하겠습니다. 모두 화이팅! 😆😆😆