[엘레강트 오브젝트] 3장 - Employment (2)

HyeBin, Park·2022년 6월 22일
0
post-thumbnail

3.5.2 좋은 의도, 나쁜 결과

  • getter와 setter는 캡슐화 원칙을 위반하기 위해 설계되었습니다.
    • 클래스를 자료구조로 바꾸기 위해 도입이 되었다.
    • 객체처럼 보이지만 수동적인 자료구조를 만들기 위해 getter, setter가 필요했다.
  • java에서도 객체의 프로퍼티들을 public으로 만들면 클래스를 자료구조로 바꿀 수는 있습니다.
  • 하지만 이 방식은 Java 프로그래밍의 기본 규칙을 심각하게 위반하며 프로그래머가 OOP를 제대로 이해하지 못했다고 생각할 것 입니다.

    나는 DFS나 BFS 알고리즘 문제를 풀 때 NODE 클래스를 만들어서 활용하는데 그때 마다 필드를 public으로 만들어줬다. DFS, BFS에서도 getter, setter를 만들어서 사용한다는게 굳이라는 생각이 들어서 public으로 만들어준건데.. 한편으로는 이렇게 생각할 수도 있구나 싶어서 문제를 풀 때도 private로 가져가야하나 싶다 ...

  • 겉으로는 메서드처럼 보이지만 , 실제로는 우리가 데이터에 직접 접근하고 있다는 불쾌한 현실을 가리고 있을 뿐입니다.

    getter와 setter에 대해서는 데브코스 초반에 팀원들이랑 얘기를 나눠본 적이 있었다. 그때도 데이터에 직접 접근하고 그대로 노출하는 점에서 getter와 setter는 없어야하는게 아닌가? 라는 얘기를 나눴지만, 현재 getter를 사용하고 있다. (setter는 지양하려한다.) getter 없이 개발을 한 다는게 가능하다고 생각하지 않는다. 저자의 이상과 현실은 다르다.

3.5.3 접두사에 관한 모든 것

  • getter/setter 안타 패턴에서 유해한 부분은 두 접두사인 get과 set이라는 사실이 중요합니다.

    • 접두사는 객체가 진짜 객체가 아니고, 어떤 종중도 받을 가치가 없는 자료구조라는 사실을 명확하게 전달합니다.
  • 객체는 대화를 원하지 않고 그저 우리가 어떤 데이터를 객체 안에 넣어주거나 다시 꺼내주기를 원할 뿐입니다.

  • getDollars()는 "데이터 중에 dollars를 찾은 후 반환하세요"

  • dollars()는 "얼마나 많은 달러가 필요한가요?" 객체를 데이터의 저장소로 취급하지 않고, 객체를 존중합니다.

    앞서 말한 팀원들과 나눴던 대화에서도 나왔던 얘기이다. 메서드 명을 변경하는 걸로 간다면 눈가리고 아웅 아니냐? 라는 얘기를 나눴었는데, 이 예시를 보니까 조금 이해가 간다. getDollars()는 너가 가지고 있는 dollars 필드의 값을 반환해라. 인데 dollars()라는 네이밍은 해당 객체에 dollars라는 필드가 있는지 알 수 없다. 즉 내부 구조에 대해서는 노출하지 않고 있다. 네이밍에 관련된 부분은 이제 이해를 했는데 앞의 3.5.2에서는 애초에 해당 메서드 자체가 잘못된 패턴이라고 말을 했던게 아닌가..? 헷갈린다.

3.6 부 생성자 밖에서는 new를 사용하지 마세요

  • 위의 코드는 의존성에 문제가 있는 코드의 전형적인 모습을 잘 보여주고 있습니다.
  • euro() 메서드 안에서는 new 연산자를 이용해 Exchange 인스턴스를 생성하고 있습니다.
    • 문제를 일으킨 범인의 이름은 '하드코딩된 의존성' 입니다.
    • Cash 클래스는 Exchange 클래스에 직접 연결되어 있기 때문에, 의존성을 끊기 위해서는 Cash 클래스의 내부 코드를 변경할 수 밖에 없습니다.
  • Cash 클래스의 소스 코드가 없는 상황에서 이 클래스를 사용하는 경우
    • 라이브러리는 바이너리 형태로만 존재하고, 우리는 이 라이브러리를 사용해야만 합니다.
  • Cash를 사용하는 코드 부분을 보면 테스트를 실행할 때마다 매번 뉴욕 증권 거래소서버와 네트워크 통신이 발생합니다.
    • 저는 five.euro()의 내부 동작에는 전혀 관심이 없고 실행 결과만이 필요합니다.
    • 현재 설계에서는 Cash가 NYSE 서버와 통신하지 않게 만들 수 없습니다.
      => Cash와 Exchange간의 연결을 끊을 수 없습니다.
    • 결합을 끊기 위해서는 Cash의 소스 코드를 수정해야만하며, 클래스가 작다면 큰 문제가 아니지만 더 큰 규모에서는 하드 코딩된 의존성이 소프트웨어를 테스트하고 유지보수하기 어렵게 만듭니다.

문제의 근본 원인은 new 연산자 입니다.

  • 객체들이 언제 어디서나 다른 객체를 인스턴스화할 수 있도록 허용해 놓고, 정작 객체들이 마음대로 생성하려고 하면 못 마땅해하는 이유는 무엇일까요
  • Cash가 Exchange의 인스턴스를 직접 생성한 부분이 문제입니다.
  • 만약 new 연산자를 사용할 수 없도록 금지 했다면? 새로운 객체를 생성자의 인자로 전 받아 private 프로퍼티 안에 캡슐화할 수 밖에 없을 것입니다.

수정한 코드

  • 수정한 코드에서는 생성자의 두 번째 인자에 Exchange 인스턴스를 전달해야합니다.
  • Exchange 인스턴스를 직접 생성할 수 없고 오직 생성자를 통해 제공된 Exchange와 협력할 수 있습니다.
  • 의존성을 제어하는 주체가 Cash가 아니라 우리 자신입니다.
  • Cash는 USD를 EUR로 변환하는데 필요한 환율을 얻을 위치를 직접 결정하지 않고 우리의 결정에 의지하고, 우리가 제공하는 객체와 협력합니다.
  • 즉, 객체가 필요한 의존성을 직접 생성하는 대신 우리가 생성자를 통해 의존성을 주입합니다.

어떤 객체라도 훌륭하게 설계할 수 있는 간단한 규칙

  • 부 생성자를 제외한 어떤 곳에서도 NEW를 사용하지마세요.
  • 객체들은 상호간에 충분히 분리되고 테스트 용이성과 유지보수성을 크게 향상시킬 수 있습니다.

    이번 주제에 동의합니다. new 연산자를 통해서 하드코딩 하게된다면 위에서 말했듯이 의존성을 끊기 위해서는 클래스 내부 코드가 변경되어야하는데 OOP와 맞지 않다고 생각합니다.

3.7 인트로스펙션과 캐스팅을 피하세요

  • 기술적으로 java의 instanceof 연산자와 Class.cast() 메서드, 다른 언어에서 동일한 기능을 수행하는 연산자들이 모두 이 범주에 포함됩니다.
  • 타입 인트로스펙션은 리플렉션이라는 더 포괄적인 용어로 불리는 여러 가지 기법들 중하나입니다.
  • 리플렉션
    • 메서드, 명령어, 구문, 클래스 등을 변경할 수 있습니다. CPU가 이 요소들에 접근하기 전에 쉽고 간다한게 코드를 수정할 수 있습니다.
    • 매우 강력한 기법이지만 동시에 코드를 유지보수하기 어렵게 만드는 매우 너저분한 기법입니다.
    • 코드가 런타임에 다른 코드에 의해 수정된다는 사실을 항상 기억해야하며 코드를 읽기가 매우 어렵습니다.

예시 코드

  • Iterable의 크기를 계산합니다. 메서드는 items에 포함된 요소들에 접근하기 전에 items 객체가 size()메서드를 정의하고 있는 Collection의 인스턴스인지 확인합니다.
  • 런타임에 타입을 확인하고, 타입에 따라 적절하게 행동합니다.
  • 사용하기에 매우 편리하고 성능 관점에서도 최적화된 것처럼 보이지만, 사실은 매우 잘못된 방식입니다.
  • 이 접근 방법은 타입에 따라 객체를 차별하기 때문에 OOP의 기본 사상을 심각하게 훼손시킵니다.
  • 요청을 어떤 식으로 처리할 지 객체가 결정할 수 있도록 하는 대신, 객체를 배재한 상태에서 결정을 내리고, 이를 바탕으로 좋은 객체와 나쁜 객체를 차별합니다.
    -> 사람들의 세계에서 성별, 민족 ,나이에 따라 차별하는 것과도 유사합니다.

문제

  • 런타임에 객체의 타입을 조사하는 것은 클래스 사이의 결합도가 높아지기 때문에 기술적인 관점에서도 좋지 않습니다.
  • size 메서드는 Iterable 인터페이스 하나가 아니라, Iterable과 Collection이라는 두 개의 인터페이스에 의존하고 있습니다. => 의존하는 대상이 늘어날수록 결합도는 높아진다. => 유지보수성에 악영향을 끼친다.
  • 메서드를 효과적으로 사용하기 위해서는 메서드의 내부 동작을 이해할 필요가 있습니다.

개선 - 오버로딩

  • 메서드 오버로딩을 사용하여 더 나은 설계를 할 수있습니다.

가정

  • 기술적으로 두 코드는 거의 동일하게 동작합니다. 최종 결과는 items객체가 Collection이다라는 사실입니다.
  • 배관공에게 저는 당신이 컴퓨터 전문가라고 가정하고 있습니다. 그러니 이컴퓨터를 수리해주세요 라고 이야기하는것과 흡사합니다.
if (items instanceof Collection) {
	return ((Collection) items).size();
}
  • 만약 당신이 컴퓨터 전문가이기도 하다면, 이 프린터를 고쳐주세요 라고 말하는 듯이 보입니다.
  • 아무런 대책 없이 추측만으로 프린터 수리를 요구했던 앞의 예제보다는 개선 되었지만 여전히 좋지 않은 방법입니다.
    => 결합도가 숨겨져 있기 때문입니다.
  • 다음에 새로운 배관공을 파견하려고 할 때, 회사는 여러분이 프린터 수리에 추가 금액을 지불하는다는 사실을 기억하고 있기 때문에 배고나공인 동시에 컴퓨터 전문가인 사람을 찾으려고 시도할 것입니다. 여러분과 수도 배관회사 사이에 체결된 계약은 싱크대 수리와 관련이 있지만 실제로는 배관공이 프린터도 수리하고 있는 것입니다.
  • 나중에 수도배관회사를 바꾸기로 결정한다면 여러분은 싱크대와 프린터를 함께 수리할 수 있는 사람을 다시 요청해야합니다. 이 지식은 계약서 어디에도 명시적으로 문서화되어 있지 않습니다.
  • 방문한 객체에 대한 기대를 문서에 명시적으로 기록하지 않은 채로 외부에 노출해버린것입니다.
  • 어떤 클라이언트는 여러분이 기대하는 바를 학습한 후 더 적절한 객체를 제공하겠지만 어떤 클라이언트는 그럴 수 없을 것입니다.

정리

  • instansceof연산자를 사용하거나 클래스를 캐스팅하는 일은 안티패턴이기 때문에 사용해서는 안됩니다.
  • Java를 포함한 대부분의 OOP언어에서 리플렉션과 함께 이런 기능들을 제공하고 있지만 소프트웨어에는 아무런 도움도 되지 않습니다.

배관공 예시가 이번 주제를 이해하는데 많은 도움이 되었다. 타입 캐스팅은 가능하니까 사용했던거였는데 다른 역할을 기대하고 있었다는 점을 생각하지 못했었다.

0개의 댓글