SRP (Single Response Principle)

  1. Q : 컨트롤러를 예를 들었을 때 하나의 책임을 지니고 있는건 아니지 않나요? 책임이라는 게 단위가 어떻게 되는 것 인가요?
  2. Q : 책임이란 것은 모호합니다. 음식을 먹는다, 라는 책임을 한가지라고 볼수도 있지만 씹는다, 삼킨다, 녹인다 등으로 세분화할 수도 있기 때문입니다. 추상화 단계에 따라 달라질텐데, 또 책임 기준은 사람마다 다를텐데 이게 모두에게 적용할 수 있는 원칙이 될 수 있을까요?

OCP (Open-Closed Principle)

  1. Q : OCP를 적용하려다보니 데코레이터나 프록시패턴으로 구현하게 되는 경우가 있었는데요, 뎁스가 3단계, 4단계로 불어나며 오히려 프로그램의 복잡도가 증가하는 경험을 했습니다. H : 적절한 단계에서 제어하는게 중요한 것 같아요! 과도한 패턴 사용은 되려 프로그램의 본질을 퇴색할 수 있을 것 같습니다.

ISP (Intefrace Segregation Principle)

  1. Q : 인터페이스를 잘게 쪼개다 보면 인터페이스가 너무 많아집니다. 해당 파일 트리 관리나 implements 에 수많은 인터페이스가 들어가는 상황이 생길텐데, 관리를 어떻게 할 것인가요?

    A : 복합기 interface를 생각해볼게요. 인터페이스를 쪼개면 인쇄, 스캔 등 메소드로 쪼갤 수 있을 겁니다. 더 잘게 쪼개다보면 빨간 잉크 인쇄, 파란 잉크 인쇄 등 아주 잘게 나눌수도 있겠지만, 그렇게 된다면 너무 과도하게 많은 인터페이스가 생겨날거 같아요. 적절한 수준에서 쪼개는 것이 중요하다고 생각합니다.

    C : 또 다른 측면으로 보자면 생산성이 향상되니 그 정도는 감안할 수 있을 것 같아요. ISP를 적용하지 않는다면 interface의 책임이 두 가지일 경우 책임 중 한가지가 변경될때 다른 책임도 영향을 받게 되고, 해당 소스를 재컴파일 하는 시간이 늘어나게 될거에요. 성능 상에서 충분한 이득이 있을 것이기 때문에, 잘게 쪼개어 주는 것이 의미가 있다고 생각합니다.

  2. Q :인터페이스를 분리하는 기준이 모호하네요. 어떤 기준으로 해야할까요?

    D : 클라이언트에 따라 달라질겁니다. 클라이언트가 요구하는 책임 단위로 분리하는게 좋지 않을까요?

    B : 변경되는 시점을 기준으로 분리해야하지 않을까요? SRP와도 관련이 있다고 생각해요.

LSP (Likov Substitution Principle)

1. Q : OCP를 잘 하면 리스코프도 지키는 것 아닌가요? 그러면 필요 없는 규칙이 아닐까요

E : OCP를 추상클래스를 통해 구현한다고 해도, 상위 객체가 하위객체 역할을 대신해준다는 내용이 LSP로 알고 있습니다. OCP와는 관계가 없지 않나요? C : LSP에 대해서 알고 계신걸 설명해 주실 수 있나요? E : 부모 객체를 자식 객체로 교체해도 프로그램이 제대로 돌아가야 한다, 로 알고 있습니다. 이해를 위해 덧붙이자면 부모와 자식 상속 개념도 우리를 헷갈리게 하는 것 같아요. 할아버지 - 아버지 - 딸을 생각해볼게요. 아버지와 딸은 부모자식 관계이지만 사실 클래스의 관점에서 생각해보면 아버지의 역할을 딸이 대신할 수는 없을겁니다. 상속 개념은 자식 is a 부모로 이해하여야 하는게 아니라 , 자식 kind of 부모로 이해해야 해요.

A : LSP를 구현하기 위해 오버라이드를 막는다는 이야기가 나왔는데, 만약에 오버라이드를 못한다면 상속을 하는 의미가 없지 않나요? E : 오버라이드 자체를 막는게 아니라, 다른 예외처리나 해당 책임을 못하게 하는 경우가 문제인 것 같아요. 해당 책임을 다하고 있다면 오버라이드 자체는 문제 없다고 생각합니다. C : 요구사항과 관련이 있는 것 같아요. 프로그래밍 언어적으로 보는걸 넘어, 부모의 명세적인 내용을 자식이 그대로 따라주어야 한다는 의미에서 요구사항 명세서와 비슷하단 느낌이 듭니다. 또 책에서 본 자료를 공유할게요

리스코프 치환 원칙은 계약과 확장에 대한 것, 아래는 흔한 LSP를 위반 사례 → 명시된 명세에서 벗어난 값을 반환 → 명시된 명세에서 벗어난 exception 발생 → 명시된 명세에서 벗어난 기능을 수행

G : 어떻게 LSP를 구현하려나요? 클래스를 새로 만들어서 진행한다고 말씀하셨는데, 어떤 방식으로 가능할까요? E : 템플릿 메소드 패턴이 있지 않을까요?

DIP (Dependency Inversion Principle)

  1. DIP의 장점으로 컴파일 타임에 하지않고 런타임에 한다는 것이 있을텐데요, 뭐가 주입될지 모를텐데 어떤게 수행될지 모르는 것은 위험하지 않을까요?

    G : DIP를 잘 설계해놓으면 주입이 어떻게 할지에 대한 정보를 IOC 컨테이너 객체에 책임을 몰아줄 수 있습니다. 그렇게 되면 주입에 대한 정보를 오히려 일목요연하게 볼 수 있게 되는 장점이 있어요. DIP가 적용되어있지 않다면 구현체로 주입이 된 것이 뭔지 확인하려면 해당 클래스 소스를 집적 확인해야 할거에요. H : 어플리케이션이 커지면 IOC컨테이너가 너무 커지지는 않을까요? G : 도메인 별로 세분화해주고, 나중에 합성을 통해 관리하면 체계적으로 관리할 수 있다고 생각해요.

  2. 너무 먼 미래를 보고 코딩하는게 아닌가요?

    G : TDD를 수행하는 입장으로서 product뿐만 아니고 test 코드생산에도 신경을 써야하는데 product, test가 각각 클라이언트라고 생각 할 수 있을 것 같아요. 벌써 두가지 클라이언트에 대응할 수 있는 코드를 작성하게 되므로, 그리 먼 미래라고 생각하지 않습니다.

번외 1 - SOLID를 잘 지켜야 하는가?

OCP 이야기 도중...

  1. 변화가 없는 코드에도 OCP를 지켜야 할까요? F : 이건 SOLID 전체에 해당되는 이야기라고 생각하는데요, SOLID는 변화하는 프로그램을 전제하고 있다고 생각해요. SOLID를 적용함으로써 재사용성과 유연함을 얻을 수 있는데, 변화하지 않을 코드라고 생각하면 굳이 지킬 필요가 없어질 것 같습니다. C : 모든 분야에 적용되는 정답은 없다고 생각합니다. SOLID도 정도를 지켜서 활용하는 것이 좋다고 생각해요!

번외 2 - static은 객체지향을 오염하는가?

SRP 이야기 도중...

C : utils 패키지에 스태틱 메소드로 구현하는 경우가 많은데, 그럴 때 SRP가 원칙적으로 지켜지지 않지만 상황에 맞게 잘 사용해야 할거 같습니다. F : static은 객체지향을 오염시킨다는 이야기도 있습니다 B : 어떤 방식으로 오염되는지 예시를 들어주실 수 있나요? G : 정적 메서드(유틸리티 클래스)는 결국 명령을 위해 나온 메서드에요! 무엇무엇을 해라! 하지만 객체지향은 명령형 프로그래밍에서 벗어나 선언형 프로그래밍을 향해 나아가는 것인데 그렇게되면 객체지향의 원리에 위반하는 것이에요! 상태를 가지지 않는 메서드는 바로바로 그것에 대한 값을 리턴해줘야하고 나온 값을 더욱 양념하고 싶다면 결국 호출한 쪽에서 그것에 대해 추가구현을 해줘야하는데 이것 자체가 객체지향에도 위반한다고 생각합니다. D : static은 동시성 이슈도 야기할 수 있어요. 게임이 끝나는지 판단하는 static 변수를 만들었는데, 리뷰어 께서 여러 유저가 이 프로그램에 접근하는 경우를 생각해보라고 하셨다. static을 사용하지 말아야 할 이유를 여기서도 찾을 수 있을 것 같아요. E : 하지만 객체의 상태를 나타내는게 아닌, 공통적으로 활용하는 변하지 않는 요소에 대해 static을 사용하는 건 장점이 있다고 생각합니다.
C : 반드시 객체지향만이 정답은 아니라고 생각해요. 성능도 생각해야 하기 때문에, 적절히가 중요하다고 생각해요. 예를들면 원시타입인 int를 Integer로 박싱하는데도 꽤나 성능이 들어가서 들어가기 때문에 mapToInt라든지 IntStream같은 기본형 스트림을 제공해 주는 거거든요. 따라서 객체지향적 설계만을 중점으로 생각하면 성능에도 문제가 있을거라 생각해요.