켄트백의 구현 패턴 - 1. 클래스

15 May 2016

아직 읽는중,,

객체 지향 프로그래밍은 프로그램을 클래스객체로 구성한 것이다. 여기서 클래스는 비슷한 성질을 가진 것을 총칭하는 용어이며 객체는 클래스가 구체화 된 것이다.

클래스

“이 데이터들은 함께 사용되는데, 그에 관련된 로직이 이것이다”라는 이야기를 하고 싶을 때 클래스를 사용한다.

클래스 선언의 의미

  • 클래스 내의 로직과 데이터는 함께 사용된다.
  • 로직은 데이터에 비해 변경될 가능성이 낮다.
  • 클래스 내부의 데이터는 관련 로직에 의해 변경된다.
  • 클래스 내부에서 사용하는 데이터들의 변경될 가능성은 비슷하다.

여기서 데이터의 변경될 가능성이 로직에 비해 높다는 것은 절대적이지 않다. 로직은 데이터에 의존적으로 변경될 수도 있고, 더 높은 레벨에서 변경되기도 한다. 또한 연산 과정에서 데이터가 변경되지 않는 경우도 있다.

효과적인 객체 지향 프로그래밍을 하기 위해서는 클래스를 어떻게 구성해야 하는지, 로직 사이의 차이점을 어떻게 효과적으로 표현해야 하는지를 배워야 한다.

클래스 계층을 사용하는 것의 의미

클래스 계층을 구성하는 것은 일종의 압축 기법을 사용하는 것이다. 상위클래스의 코드를 모두 하위클래스에 붙여 넣는 것과 같은 효과를 가져온다. 압축 기법과 마찬가지로 코드의 분량은 줄어들지만, 코드를 읽기 어려워진다. 하위클래스를 이해하기 위해서는 상위클래스 또한 이해해야 한다.

하위클래스를 생성하는 것의 의미

하위클래스를 생성하는 것은 “이 클래스는 상위클래스와 비슷하지만, 약간 다르다”라고 이야기하는 것이다. - 여기서 Override라는 단어 선정이 아쉽다고 언급된다. 부분클래스에서 전체클래스의 메소드를 덮어버린다는 것이..

클래스는 OOP의 설계 요소 중 비용이 비교적 높으므로, 뭔가 의미 있는 작업에만 클래스를 사용해야 한다. 다른 클래스들의 크기를 너무 비대하게 하지 않으면서, 클래스의 개수를 줄이는 것은 프로그램을 개선한 것이라고 할 수 있다.

상위 클래스 이름은 단순하게

클래스 계층의 최상위에 위치하는 클래스의 이름은 단순하게 짓는다. 클래스 이름을 지을 때는 간결성과 표현성 사이의 딜레마에서 고민하게 된다. 클래스 이름은 대화 중에도 많이 사용된다. 가급적 짧으면서도 핵심을 찔러야 하지만, 좀 더 정확한 의미를 전달하기 위해 몇 개의 단어를 나열해야 하는 경우도 있다. 좋은 방법은 메타포를 사용하는 것이다. 메타포를 사용하면 단어 하나만으로도 연상작용을 통해 여러 관련 정보와 내포된 의미를 전달할 수 있다.

Drawing 프레임워크를 만들 때, 인쇄된 책에 대한 메타포를 사용할 수 있다. 드로잉을 책의 한 페이지로 생각해본다면, DrawingObject는 페이지 내의 Figure라고 볼 수 있다. Figure라는 이름은 DrawingObject 보다 짧음에도 불구하고 메타포를 통해 더욱 풍부하고 정확한 의미를 전달해준다.

하위클래스 이름은 제한적으로

하위클래스의 이름은 상위클래스와의 유사점과 차이점을 분명하게 나타내어야 한다. 여기서도 간결성과 풍부한 표현성 사이의 고민이 생긴다. 하위클래스는 대화 중에 사용되는 경우가 드물다. 따라서 어느 정도 간결성은 포기하더라도 표현성을 택하는 편이 낫다. 보통은 상위클래스 이름에 한두개의 수식어를 붙여서 하위클래스 이름을 정하는 경우가 많다.

멀티레벨 계층인 경우는 위임을 통해 멀티레벨 클래스 계층을 제거하는 편이 좋다. 멀티레벨 계층을 유지해야 하는 경우는 좋은 이름에 대해 고민해야 한다. 바로 위의 상위클래스 이름에 수식어를 붙이기 보다는, 가장 유사한 상위클래스의 이름을 바탕으로 짓는 것이 좋다.

추상 인터페이스

  • 여기서의 인터페이스는 ‘구현이 빠진 여러 연산의 집합’을 의미한다. 이 중 하나의 형태가 자바의 interface 키워드이나, 자바의 interface 키워드만을 지칭하는 것은 아니다.

인터페이스와 구현을 분리한다. 구현이 아니라 인터페이스에 맞춰 개발한다. 이는 설계상의 결정을 필요 이상으로 노출하지 말라는 뜻이다. 예를 들어 Collection 인터페이스를 사용한다면 어떤 concrete 클래스를 사용할지는 나중에 얼마든지 바꿀 수 있다.

하지만 인터페이스 추가에는 비용이 발생한다. 유연성이 필요할 것이라고 예측되는 부분에 인터페이스를 사용해야 한다. 단 어떤 부분에 유연성이 필요할지 예측하기란 쉽지 않으므로, 실제로 필요해지는 경우에만 시스템에 유연성을 부여하자는 결론에 이른다.

인터페이스

자주 변경되지 않는 추상 인터페이스에는 자바의 인터페이스를 사용한다. 자바 인터페이스를 사용하는 것은 “여기까지가 내가 원하는 것이고, 이외의 내용은 상관하지 않는다.”라고 이야기하는 것과 같다.

인터페이스를 사용하면 구현을 바꾸는 것은 쉽지만, 인터페이스 자체를 바꾸기는 쉽지 않다. 인터페이스를 조금이라도 수정하면 그 인터페이스를 구현했던 모든 클래스들이 수정되어야 하기 때문이다. 따라서 인터페이스가 이미 널리 사용되고 있어서 모든 구현을 바꾸는 것이 쉽지 않은 경우라면 전체 설계를 개선하기는 어렵다.

인터페이스의 모든 메소드는 public 이다..

인터페이스를 ‘구현이 빠져있는 클래스’로 간주하는 경우, 클래스와 같은 방법을 사용해서 이름을 지으면 된다. - 인터페이스 이름이 File, 구현 클래스 이름은 ConcreteFile 혹은 FileImpl .. 인터페이스를 사용했는지 상위클래스 상속을 사용했는지 전달하는 것이 그다지 중요하지 않을때..

구현 클래스의 이름을 간결하게 짓는 것이 중요한 경우, 인터페이스의 이름에 I를 붙인다. - 인터페이스 이름이 IFile, 구현 클래스 이름은 File …

TBC

추상 클래스

자주 변경될 것 같은 추상 인터페이스에는 자바의 추상 클래스를 사용한다.

자바에서 추상

버전 인터페이스

하위 인터페이스를 사용해 기존 인터페이스를 안전하게 확장한다.

값 객체

산술값처럼 동작하는 객체를 사용한다.

특화

관련된 연산 사이의 유사점 및 차이점을 분명하게 나타낸다.

하위 클래스

1차원적잉ㄴ 변화는 하위클래스를 사용하여 나타낸다.

Implementor

연산의 내용이 바뀌었다면 기존 메소드를 오버라이드해서 사용한다.

내부 클래스

클래스 내부에서 유용하게 사용할 수 있는 코드를 모아서 전용 클래스로 사용한다.

인스턴스별 행동 (instance-specific behavior)

인스턴스에 따라 로직의 변화를 준다.

조건문

명시적 조건에 따라 로직의 변화를 준다.

위임

여러 종류의 객체 중 하나에 위임하여 로직의 변화를 준다.

pluggable selector

리플렉션을 이용한 메소드 호출로 로직의 변화를 준다.

익명 내부 클래스

필요한 메소드에서 한두개의 메소드만 오버라이드하는 객체를 만들어서 사용한다.

라이브러리 클래스

마땅히 들어갈 곳 없는 기능들을 묶어서 정적 메소드로 표현한다.

Git - 커밋 합치기 (Squash) 서버 A가 서버 B와 통신할 수 없는 경우