Q : 인터페이스를 잘게 쪼개다 보면 인터페이스가 너무 많아집니다. 해당 파일 트리 관리나 implements 에 수많은 인터페이스가 들어가는 상황이 생길텐데, 관리를 어떻게 할 것인가요?
A : 복합기 interface를 생각해볼게요. 인터페이스를 쪼개면 인쇄, 스캔 등 메소드로 쪼갤 수 있을 겁니다. 더 잘게 쪼개다보면 빨간 잉크 인쇄, 파란 잉크 인쇄 등 아주 잘게 나눌수도 있겠지만, 그렇게 된다면 너무 과도하게 많은 인터페이스가 생겨날거 같아요. 적절한 수준에서 쪼개는 것이 중요하다고 생각합니다.
C : 또 다른 측면으로 보자면 생산성이 향상되니 그 정도는 감안할 수 있을 것 같아요. ISP를 적용하지 않는다면 interface의 책임이 두 가지일 경우 책임 중 한가지가 변경될때 다른 책임도 영향을 받게 되고, 해당 소스를 재컴파일 하는 시간이 늘어나게 될거에요. 성능 상에서 충분한 이득이 있을 것이기 때문에, 잘게 쪼개어 주는 것이 의미가 있다고 생각합니다.
Q :인터페이스를 분리하는 기준이 모호하네요. 어떤 기준으로 해야할까요?
D : 클라이언트에 따라 달라질겁니다. 클라이언트가 요구하는 책임 단위로 분리하는게 좋지 않을까요?
B : 변경되는 시점을 기준으로 분리해야하지 않을까요? SRP와도 관련이 있다고 생각해요.
1. Q : OCP를 잘 하면 리스코프도 지키는 것 아닌가요? 그러면 필요 없는 규칙이 아닐까요
E : OCP를 추상클래스를 통해 구현한다고 해도, 상위 객체가 하위객체 역할을 대신해준다는 내용이 LSP로 알고 있습니다. OCP와는 관계가 없지 않나요? C : LSP에 대해서 알고 계신걸 설명해 주실 수 있나요? E : 부모 객체를 자식 객체로 교체해도 프로그램이 제대로 돌아가야 한다, 로 알고 있습니다. 이해를 위해 덧붙이자면 부모와 자식 상속 개념도 우리를 헷갈리게 하는 것 같아요. 할아버지 - 아버지 - 딸을 생각해볼게요. 아버지와 딸은 부모자식 관계이지만 사실 클래스의 관점에서 생각해보면 아버지의 역할을 딸이 대신할 수는 없을겁니다. 상속 개념은 자식 is a 부모로 이해하여야 하는게 아니라 , 자식 kind of 부모로 이해해야 해요.
A : LSP를 구현하기 위해 오버라이드를 막는다는 이야기가 나왔는데, 만약에 오버라이드를 못한다면 상속을 하는 의미가 없지 않나요? E : 오버라이드 자체를 막는게 아니라, 다른 예외처리나 해당 책임을 못하게 하는 경우가 문제인 것 같아요. 해당 책임을 다하고 있다면 오버라이드 자체는 문제 없다고 생각합니다. C : 요구사항과 관련이 있는 것 같아요. 프로그래밍 언어적으로 보는걸 넘어, 부모의 명세적인 내용을 자식이 그대로 따라주어야 한다는 의미에서 요구사항 명세서와 비슷하단 느낌이 듭니다. 또 책에서 본 자료를 공유할게요
리스코프 치환 원칙은 계약과 확장에 대한 것, 아래는 흔한 LSP를 위반 사례 → 명시된 명세에서 벗어난 값을 반환 → 명시된 명세에서 벗어난 exception 발생 → 명시된 명세에서 벗어난 기능을 수행
G : 어떻게 LSP를 구현하려나요? 클래스를 새로 만들어서 진행한다고 말씀하셨는데, 어떤 방식으로 가능할까요? E : 템플릿 메소드 패턴이 있지 않을까요?
DIP의 장점으로 컴파일 타임에 하지않고 런타임에 한다는 것이 있을텐데요, 뭐가 주입될지 모를텐데 어떤게 수행될지 모르는 것은 위험하지 않을까요?
G : DIP를 잘 설계해놓으면 주입이 어떻게 할지에 대한 정보를 IOC 컨테이너 객체에 책임을 몰아줄 수 있습니다. 그렇게 되면 주입에 대한 정보를 오히려 일목요연하게 볼 수 있게 되는 장점이 있어요. DIP가 적용되어있지 않다면 구현체로 주입이 된 것이 뭔지 확인하려면 해당 클래스 소스를 집적 확인해야 할거에요. H : 어플리케이션이 커지면 IOC컨테이너가 너무 커지지는 않을까요? G : 도메인 별로 세분화해주고, 나중에 합성을 통해 관리하면 체계적으로 관리할 수 있다고 생각해요.
너무 먼 미래를 보고 코딩하는게 아닌가요?
G : TDD를 수행하는 입장으로서 product뿐만 아니고 test 코드생산에도 신경을 써야하는데 product, test가 각각 클라이언트라고 생각 할 수 있을 것 같아요. 벌써 두가지 클라이언트에 대응할 수 있는 코드를 작성하게 되므로, 그리 먼 미래라고 생각하지 않습니다.
OCP 이야기 도중...
SRP 이야기 도중...
C : utils 패키지에 스태틱 메소드로 구현하는 경우가 많은데, 그럴 때 SRP가 원칙적으로 지켜지지 않지만 상황에 맞게 잘 사용해야 할거 같습니다.
F : static은 객체지향을 오염시킨다는 이야기도 있습니다
B : 어떤 방식으로 오염되는지 예시를 들어주실 수 있나요?
G : 정적 메서드(유틸리티 클래스)는 결국 명령을 위해 나온 메서드에요! 무엇무엇을 해라! 하지만 객체지향은 명령형 프로그래밍에서 벗어나 선언형 프로그래밍을 향해 나아가는 것인데 그렇게되면 객체지향의 원리에 위반하는 것이에요! 상태를 가지지 않는 메서드는 바로바로 그것에 대한 값을 리턴해줘야하고 나온 값을 더욱 양념하고 싶다면 결국 호출한 쪽에서 그것에 대해 추가구현을 해줘야하는데 이것 자체가 객체지향에도 위반한다고 생각합니다.
D : static은 동시성 이슈도 야기할 수 있어요. 게임이 끝나는지 판단하는 static 변수를 만들었는데, 리뷰어 께서 여러 유저가 이 프로그램에 접근하는 경우를 생각해보라고 하셨다. static을 사용하지 말아야 할 이유를 여기서도 찾을 수 있을 것 같아요.
E : 하지만 객체의 상태를 나타내는게 아닌, 공통적으로 활용하는 변하지 않는 요소에 대해 static을 사용하는 건 장점이 있다고 생각합니다.
C : 반드시 객체지향만이 정답은 아니라고 생각해요. 성능도 생각해야 하기 때문에, 적절히가 중요하다고 생각해요. 예를들면 원시타입인 int를 Integer로 박싱하는데도 꽤나 성능이 들어가서 들어가기 때문에 mapToInt라든지 IntStream같은 기본형 스트림을 제공해 주는 거거든요. 따라서 객체지향적 설계만을 중점으로 생각하면 성능에도 문제가 있을거라 생각해요.