Readable Code: 읽기 좋은 코드를 작성하는 사고법 - Section 6 : 리팩토링 연습

연습 프로젝트 소개




리팩토링 (1) - 추상화 레벨






- StudyCafePassType을 이미 선택했기 때문에 이걸로 filter 해주고 중복제거 해주면 된다.



- 사물함 이용권을 고르는 로직을 메서드로 추출해보자.

- 사용자가 선택한 고정석 날짜에 따라 그에 맞는 락커패스를 필터링 하기 위한 로직이다.



- 파일 핸들러는 필드로 가지고 있어도 될 것 같다.




- lockerSelection을 selectLockerPass에서 했으면 좋겠다.





- outputHandler.showPassOrderSummary() 메서드에서 StudyCafeLockerPass를 필요로 하지 않으면 두번째 파라미터를 null로 하고 있다.
-> 이를 해결하기 위해 일단 lockerPass를 if문 밖으로 꺼내자.

- 두번 째 인자값으로 lockerPass를 넣고 selectedLockerPass 메서드를 수정해주자.



- 패스권을 얻어오기 위한 일련의 과정을 메서드로 추출하자.









- null을 반환하는 것은 좋지 않다. 반환해야 하는 상황이라면 Optional을 사용하자.





- Optional을 인자로 받는 것은 안티패턴이다.
-> 밖에서 해소해줘야 한다.

- if 문을 없앴는데 Optional을 사용하면서 다시 분기문이 생겨버렸다.

- ifPresentOrElse를 사용해서 분기문을 제거했다.
- outputHandler.showPassOrderSummary() 메서드 두번 째 인자에 null을 넣어주는 것이 마음에 걸린다.

- 객체에 메세지를 보낼 수 있는 부분을 수정하자.






- 이것도 getter로 꺼내오는 과정이다.
-> 객체한테 요청하는게 어떨까?






- 고정 좌석 타입이 아닌가? 는 직관적으로 받아드려지지 않는다.
-> 더 높은 추상화 레벨로 바꾸는게 좋아보인다.
-> 사물함 옵션을 사용할 수 있는 타입이 아닌가? 라는 추상화가 적당한 추상화 레벨이다.

- 이렇게 추상화하니까 Fixed 말고도 나중에 여러 가지 이용권들이 추가된다면 바뀌어야 할 것이다.
-> 그러면 애초에 사물함을 사용할 수 있는 패스 타입들을 관리해보자.

- Set이 Pass에 있을지 LockerPass에 있을지 고민이 된다.
-> 그러면 차라리 PassType에 있는게 어떨지 고려해보자.



리팩토링 (2) - 객체의 책임과 응집도


- 아웃풋 핸들러에 어떤 메시지를 출력하는 것과 인풋 핸들러를 통해서 출력한 메세지에 대한 사용자 응답을 받는 부분은 각각의 별개의 과정이 아니고 사실은 하나의 과정이다.
- StudyCafeIOHandler에서 인풋, 아웃풋 핸들러를 가지고 있게 하자.

























- 일급 컬렉션의 장점중 하나는 컬렉션에 대한 가공 로직이 일급 컬렉션이 담당하게 된다는 장점이 있다.


- findPassBy에 대한 테스트 코드 작성도 가능해진다.
- List<LockerPass>에 대한 일급 컬렉션을 만들자.









- 일급 컬렉션에서 null을 반환하고 있다.
-> 좋은 구조는 아니다.






- StudyCafePass에 있는 display() 로직을 OutputHandler로 옮기자.





- 비슷한 로직이 LockerPass에도 있다.
-> 일단은 옮겨 보자.






- 옮긴 두개의 display() 로직이 똑같다.
-> 스터디카페 패스랑 락커패스랑 스터디카페에서 사용하는 패스권이라는 개념으로 추상화 해보면 어떨까?
-> 인터페이스로 뽑으면 어떨까? 하는 생각이 든다.
- 이용권이라는 개념 하위에 좌석 이용권과 사물함 이용권이 있다고 생각이 들기 때문에 변경해보도록 하자.
- 스터디 카페 패쓰라는 이름을 인터페이스 이름으로 사용하고 싶기 때문에 기존에 사용하고 있떤 스터디 카페 패쓰를 좌석 이용권 즉, 스터디 카페 시트 패스로 변경해보자.







- 출력을 담당하는 아웃풋 핸들러에서 계산에 대한 로직까지 담당하고 있다.
- 주문에 대한 계산은 굉장히 중요한 로직이다.
- showPassOrderSummary()라고 Order라는 개념을 사용하고 있다.
-> 사용자가 선택한 모든 선택지를 모은 Order, 주문이라는 개념을 만들어보자.








- LockerPass는 넣어줄 때 orElse(null)로 너어줬기 때문에 null일 수도 있다.
-> Optional로 감싸서 반환하자.





- DiscountPrice()의 로직들은 seatPass의 것이다.
-> seatPass로 넘겨주자.





리팩토링 (3) - 관점의 차이로 달라지는 추상화



- 파일에서 읽어오는 것이 너무 구체, 구현쪽이다.
-> 추상화해서 DIP, 의존성 역전 원칙을 적용해보자.

- FileHandler에 존재하던 메서들을 추출하자.




- FileHandler라는 구현체는 passReader로 숨겼는데 기존에 사용하고 있던 readStudyCafePasses()가 애매해졌다.
-> 기존에는 FileHandler가 read를 하기 때문에 File로 부터 read하는구나를 알 수 있는데 passReader로 추상화 하니까 어디로부터 read하는지 알 수 없게 되었다.
- 외부에 있는 어떤 데이터를 필요로 해서 데이터를 가져온다고 했을 때 두 가지 관점으로 생각해 볼 수 있다.
-> 1. 어떤 데이터를 필요로 하는가
-> 2. 데이터를 어디로부터 어떻게 가져올 것인가
- 파일 핸들러는 두 번째 관점인 데이터를 어디로부터 어떻게 가져올 것이가에만 초점이 맞춰져 있다.
-> 파일 핸들러로부터 파생된 PassReader 또한 데이터를 어디로부터 어떻게 가져올 것인가에 구현에만 초점이 맞춰져서 추상화가 이루어졌다.
- 두 번째, 즉 방법에 대한 추상화가 아니고 어떤 데이터를 필요로 하는가에 초점을 맞춰서 방법은 아예 모르도록 하고 어떤 데이터를 필요로 하는지에 대한 스펙만 뽑는게 더 나은 추상화이다.

- 나중에 요구사항이 추가돼서 공지사항을 파일로 관리한다고 생각해보자.
- 파일로부터 공지사항들을 가져오게 될텐데 그 로직도 스터디 카페 파일 핸들러에 추가가 될 것이다.
-> 패스 리더도 파일 핸들러를 기반으로 추상화가 됐기 때문에 패스 리더에도 공지사항을 파일로부터 가져오는 스펙이 추가가 될 것이다.
- 패스머신이 점점 발전해감에 따라 파일에서 무엇인가를 읽어오는 모든 행위는 스터디카페 파일 핸들러로 모일 것이다.
- 왜 이렇게 될까?
-> 파일 핸들러, 파일에서 읽는다는 방법에 초점을 맞춰서 객체를 만들었기 때문이다.

- 또 다른 예시를 살펴보자. 만약 파일 핸들러에서 좌석 이용권 말고 락커 이요권, 락커 패스에 대한 것들을 파일이 아니고 구글 시트로부터 가져온다고 생각해보자.
- 이미 파일 핸들러에 수많은 파일에 대한 로직들이 다 모여있는데 락커 패스를 읽어오는 부분만 갑자기 구글 시트로 구현이 바뀔 것이다.
-> 클래스의 이름은 파일 핸들러인데 여기에 구글 시트가 될 것이다.
-> 그러면 또 구글 시트 핸들러 이렇게 만들어서 분리 할 수 있을 것이다.
- 근데 처음부터 파일 핸들러라는 객체 자체가 잘못된 응집이 아닌가라는 생각을 해보자는 것이다.
- 다시 돌아가서 2번, 즉 데이터를 어디서부터 어떻게 가져올 것인가에 대한 방법적인 것에 초점을 맞춰서 객체를 만드는 것이 아니고 어떤 데이터를 필요로 하는가에 대해서만 초점을 맞춰보자.
- 기존에 만들었던 DIP로직을 지우자.
- 어떤 데이터를 필요로 하는지에 대해 초점을 맞춰야 하니 SeatPass를 제공하는 인터페이스를 만들자.


- 구현체를 만들고 파일 핸들러로부터 로직을 가져오자.



- 파일 핸들러 대신에 SeatPassProvier를 통해 SeatPass를 받자.
- 마찬가지로 LockerPassProvider를 만들자.


- 구현체를 만들고 파일 핸들러로부터 로직을 가져오자.





- 파일 핸들러는 객체 자체가 파일에서 읽는다라는 방법에만 초점을 맞춰서 객체가 만들어졌기 때문에 요구사항이 발전하면 발전할수록 이 객체는 굉장히 헤비해질 것이다.
-> 모든 파일과 관련된 로직들이 파일 핸들러로 모이기 때문이다.
- 뭐가 필요해? 락커패스가 필요해
-> 락커패스 프로바이더를 구현하는 방법에 대해서는 락커패스 파일 리더가 담당을 하는 것이다.

- Provider 패키지에 interface를 두고 그 구현체에 대해서는 IO 패키지에 따로 두었다. 이 둘을 분리 두는 것이 중요하다.
- 파일에서 읽는다라는 개념은 하위, 구현에 대한 추상화 레벨이 낮은 개념이고 상위 레벨의 개념은 방법이 무엇이든 패스를 제공해 줄거다라는 개념이라고 별도의 Provider 패키지에 interface라는 스펙을 둔것이다.
- 헥사고날 아키텍처의 포트와 어댑터 개념이 이와 같다.
-> 포트는 인터페이스, 어댑터는 구현체이며 언제든지 포트에 구현체를 바꿔 끼울 수 있는 형태를 의미한다.
-> 이는 OCP를 지키는 방법이기도 하다.