General/Pattern
[구현패턴] 클래스
croute
2012. 1. 21. 21:33
4장까지의 내용이 구현패턴에 대한 기본 개념들과 프로그래밍적 관점에 대한 설명이었다면, 5장부터는 실제로 패턴들을 설명합니다. 5장은 클래스를 중심으로한 구현패턴에 대해서 설명되어 있는데요. 클래스의 이름을 정하는 방법부터, 인터페이스, 조건문, 구현자에 이르기까지 많은 내용들을 다룹니다.
클래스는 서로 관련이 있는 상태를 묶어놓은 것이다.
클래스는 잠재적으로 여러 세부 사항을 내포할 수 있으므로, 클래스의 사용은 커뮤니케이션을 위한 프로그램에 있어 매우 중요하다. 실제 구현 패턴에서 가장 많은 양을 차지하는 것이 클래스 구현 패턴이다. 널리 알려져 있는 디자인 패턴은 클래스 간의 관계를 다루는 것이다.
5장에서 다루는 패턴
- 클래스: "이 데이터들은 함께 사용되는데, 그에 관련된 로직이 이것이다."라는 이야기를 하고 싶을 때 클래스를 사용한다.
- 단순한 상위 클래스 이름: 클래스 계층의 최상위에 위치하는 클래스 이름은 단순하게 짓는다.
- 한정적 하위 클래스 이름: 상위 클래스와의 유사점과 차이점을 분명히 드러내는 이름을 사용한다.
- 추상 인터페이스: 인터페이스와 구현을 분리한다.
- 인터페이스: 자주 변하지 않는 추상 인터페이스에는 자바 인터페이스를 사용한다.
- 버전 인터페이스: 하위인터페이스를 사용해 기존 인터페이스를 안전하게 확장한다.
- 추상 클래스: 자주 바뀔 것 같은 추상 인터페이스에는 추상 클래스를 사용한다.
- 값 객체: 산술 값처럼 동작하는 객체를 사용한다.
- 특화: 관련된 연산 사이의 유사점 및 차이점을 분명하게 나타낸다.
- 하위 클래스: 1차원적 변화는 하위클래스를 사용해서 표현한다.
- 구현자(implementor): 연산 내용이 바뀌었다면 기존 메소드를 오버라이드해서 사용한다.
- 내부 클래스: 클래스 내부에서 유용하게 사용할 수 있는 코드를 모아 전용 클래스로 사용한다.
- 인스턴스별 행동(instance-specific behavior): 인스턴스에 따라 로직에 변화를 준다.
- 조건문: 명시적 조건에 따라 로직에 변화를 준다.
- 위임(delegation): 여러 종류의 객체 중 하나에 위임해서 로직에 변화를 준다.
- 플러그인 선택자(pluggable selector): 리플렉션을 이용한 메소드 호출로 로직에 변화를 준다.
- 익명 내부 클래스: 필요한 메소드에서 한두 개의 메소드만 오버라이드 하는 객체를 만들어서 사용한다.
- 라이브러리 클래스: 마땅히 들어갈 곳이 없는 기능들을 묶어서 정적 메소드로 표현한다.
클래스
- 객체를 사용해 효과적인 프로그래밍을 하기위해서는 로직을 클래스 단위로 어떻게 구성해야 하는지 로직 사이의 차이점을 어떻게 효과적으로 표현해야 하는지 배워야한다.
- 클래스 계층 구성은 일종의 압축 기법을 사용하는 것이다.
- 효과적 객체지향 프로그래밍을 위해 선별적 상속 필요하다.
- 클래스는 값이 비싼편이므로 의미있는 작업에만 클래스를 사용해야한다.
- 다른 클래스의 크기를너무 비대하게 하지 않으면서 클래스의 수를 줄이는 것이 프로그램을 개선한 것이라 할 수 있다.
단순한 상위 클래스 이름
- 메타포(metaphor)를 사용: 메타포를 사용하면 단어 하나만으로도 연상작용을 통해 여러 관련 정보와 내포된 의미를 전달할 수 있다.
- 중요한 클래스에 대해서는 한 단어로 된 이름을 사용하는 것이 좋다.
한정적 하위 클래스 이름
- 하위 클래스 이름은 상위 클래스와의 유사점과 차이점을 나타내야 한다.
- 하위 클래스가 상위 클래스의 메커니즘을 빌려서 사용할 뿐, 그 자체로 프로그램의 중요한 개념을 의미하는 경우라면, 이런 하위클래스는 다른 하위클래스의 이름과는 차별화되는 단순한 상위 클래스의 이름을 부여받을 자격이 있다.
- 클래스 이름은 커뮤니케이션을 하기 위해서도 필요하다. 이름 사이에 연관성이 없는 일련의 클래스는 이해하기도, 기억하기도 어렵게 된다. 클래스 이름은 코드의 내용을 반영해야 한다.
추상 인터페이스
- 대부분 코드가 컬렉션 인터페이스를 사용한다면, 구상 클래스(concrete class)를 나중에 얼마든지 바꿀 수 있다. 구상 클래스는 컴퓨터가 실제 연산을 하기 전에만 결정하면 된다.
- 여기서 '인터페이스'란 '구현이 빠진 여러 연산의 집합'을 의미한다. 자바에서는 자바 인터페이스나 상위클래스를 사용해서 이러한 목적을 달성할 수 있다.
자바가 제공하는 추상 인터페이스에 대한 두 가지 메커니즘인 상위 클래스와 자바 인터페이스는 소프트웨어 수정 시 서로 다른 형태로 비용이 발생한다.
소프트웨어 개발에 관한 오랜 경언 중 하나로, 구현이 아니라 인터페이스에 맞춰 코딩하라는 말이 있다. 이는 설계상의 결정을 필요이상으로 노출하지 말라는 뜻이다.
인터페이스
- 자바 인터페이스를 사용하는 것은 "여기까지가 내가 원하는 것이고, 이외의 내용은 상관하지 않는다." 라고 이야기하는 것과 같다.
- 인터페이스는 다중 상속의 유연성을 제공하면서도 복잡성과 모호성을 갖고 있지 않은 적절하게 균형이 잡힌 메커니즘이다.
- 하나의 클래스는 여러 인터페이스를 구현한다고 선언할 수 있다. 인터페이스는 필드를 배제하고 연산만을 나타내므로 사용자는 구현이 변경되더라도 신경 쓸 필요가 없다.
추상 클래스
- 자바에서 추상 인터페이스(abstract interface)와 실제 구현(concrete implementation)의 차이를 나타내는 다른 방법은 상위 클래스를 이용하는 것이다. 여기서 상위 클래스는 런타임에 어떤 하위클래스로 교체될지도 모른다는 의미에서(자바의 abstract를 사용한 추상 클래스인지에 관계없이) 추상적이다.
- 추상 클래스와 자바 인터페이스의 장단점은 인터페이스 수정의 용이성과 단일 클래스가 여러 인터페이스를 지원할 수 있는지 여부로 귀결된다.
- 추상 클래스는 기본 구현을 사용할 수 있는 길이 열려있는 한, 기존 설계를 망가뜨리지 않고 새로운 연산을 얼마든지 추가할 수 있다. 추상 클래스의 단점은 단 1개의 상위 클래스만을 지정할 수 있다는 것이다.
인터페이스 계층과 클래스 계층은 서로 배타적인 것이 아니다. 인터페이스를 통해 "이 기능은 이렇게 사용하세요"라고 전달하고, 상위 클래스를 통해 "이 기능을 이렇게 구현해봤습니다"라고 전달하는 것도 가능하다.
버전 인터페이스
- 인터페이스에 어떤 연산을 추가하고 싶을 때, 연산을 추가하면 기존 인터페이스를 구현한 클래스가 동작하지 않게 되므로 함부로 연산을 추가하기 어렵다.
- 그러나 새로운 인터페이스를 선언해서 기존 인터페이스를 확장(상속)한 후, 새로운 연산을 추가할 수는 있다.
- 새로운 연산을 사용하는 경우, 반드시 객체의 타입을 확인한 후 새로운 타입으로 다운캐스트해서 사용해야 한다는 점에 주의하자.
인터페이스의 다운캐스트(상위 인터페이스를 하위 인터페이스로 캐스트하는것)과 이를 위한 instanceof의 사용에 주의하자.
값 객체
- 상태와 객체를 사용한 프로그래밍은 연산에 대한 사고의 틀을 마련해준다.
- 어떤 숫자에 1을 더하는 것은 값을 바꾸는 것이 아니라, 새로운 값을 생성하는 것이다. 대부분 객체에서 값을 변경하는 것은 가능하지만, 0이라는 값 자체를 바꾸는 것은 불가능하다.
- 함수형 스타일(functional style) 연산은 상태를 변화시키지 않으며 새로운 값을 생성한다. 일시적이라도 고정적인 상황을 표현하고 싶다면 함수형 스타일(functional style)이 적절하고, 상황이 변하는 경우라면 상태(state)를 사용하는 편이 낫다.
특화
- 연산 간의 유사점과 차이점을 부각시키는 방향으로 코드를 작성하면, 프로그램을 읽고 사용하고 수정하기 쉬워진다.
- 가장 간단한 변형은 상태만 바꾸는 것이다. "abc"와 "def"는 전혀 다르지만, 두 문자열에 대한 연산 알고리즘은 완전히 같다.
- 가장 복잡한 변형은 로직 자체를 완전히 바꾸는 것이다. 적분 프로그램과 수식 편집기는 같은 입력을 사용할지 몰라도 프로그램의 로직은 완전히 다르다.
- 대부분의 프로그래밍은 두 가지 극단적 변형(다른 데이터를 사용하는 동일한 로직, 동일한 데이터를 사용하는 다른 로직)사이에 존재한다.
하위 클래스
- 하위 클래스를 선언하는 것은 "이 객체는 상위클래스와 같다. ... 이 부분만 제외하면 ..."이라고 말하는 것과 같다.
- 적당한 메소드를 오바라이드 할 경우 간단한 코드 몇 줄이면 기존 연산과 다른 변형을 만들어 낼 수 있다.
- 하위 클래스의 여러 문제점을 알고 있다면, 하위 클래스는 다양한 변형을 나타낼 수 있는 도구다. 하위 클래스를 올바르게 사용하기 위한 키 포인트는 상위 클래스의 로직을 여러 개의 메소드로 잘게 쪼개는 것이다.
- 변화하는 로직을 나타낼 때는 하위 클래스 보다는 조건문이나 위임을 사용하라.
하위 클래스의 문제점
- 하위 클래스 사용 후 하위 클래스에 종속되는 코드 - 변형을 효과적으로 사용 못함
- 하위 클래스를 이해하기 위해 상위 클래스를 이해해야 함
- 하위 클래스가 상위 클래스의 세부 구현 특성에 의존할 수 있으므로, 상위 클래스의 수정이 위험해짐
- 클래스 상속 계층이 복잡해지면서 이 모든 문제가 심화됨
- 동적으로 변화하는 로직을 나타낼 수 없음
구현자
- 객체 지향적 프로그램에서는 선택을 표현하기 위해 주로 다형적 메시지(polymorphic message)를 사용한다. 메시지로 선택을 표현하기 위해서는 여러 종류의 다양한 객체가 메시지를 받아서 처리하게 된다.
- 다형적 메시지는 여러 가지 변형을 수용할 수 있다. 추상 클래스 Socket을 사용하면 호출하는 코드에 영향을 주지 않고, 소켓의 구현을 자유롭게 바꿀 수 있다.
- 명시적이고 폐쇄적인 조건문을 사용하는 프로시저 표현에 비해 객체와 메시지를 사용하는 방식은 프로그래머의 의도와 구현을 분리해서 좀더 명확히 프로그래머의 의도를 나타낸다.
내부 클래스
- 어떤 연산을 표현하기 위한 클래스가 필요하지만, 새로운 파일에 완전히 새로운 클래스를 만들고 싶지는 않을때가 있다. 내부 클래스를 사용하면 클래스 사용에 따른 비용을 지불하지 않으면서도 클래스의 장점을 대부분 취할 수 있다.
- 내부 클래스는 내부 클래스를 감싼 클래스(enclosing class)에 대한 정보를 암묵적으로 전달받는다. 이는 클래스 간의 관계를 명시적으롤 정하지 않으면서도 감싼 클래스의 데이터를 접근할 수 있는 유용한 기법이다.
인스턴스별 행위
- 이론상 클래스의 인스턴스들은 모두 같은 로직을 공유한다. 하지만 이런 제약사항을 완화하면 새로운 스타일의 표현이 가능하다.
- 인스턴스별로 다른 행동을 보이는 경우, 특정 인스턴스의 행동을 이해하기 위해서는 실례를 보거나 데이터의 흐름을 분석해야만 한다. 인스턴스 생성후에는 인스턴스별 행동을 변화시키지 않는 편이 좋다.
조건문
- 가장 단순한 인스턴스별 행동의 형태는 if/then과 switch 구문을 이용하는 것이다. 조건문을 사용하면 데이터에 따라 각 인스턴스는 다른 로직을 수행하게 된다.
- 프로그램의 각 수행 경로에는 오류가 있을 수 있다. 수행 경로가 다양한 프로그램이 그렇지 않은 프로그램에 비해 결함을 갖고 있을 확률이 높다.
- 조건문이 복사되는 경우, 이런 문제가 더 심각해 진다. 이 문제는 하위 클래스나 위임 중 더 적합한 방법을 사용해서 조건문을 메시지로 바꾸면 해결할 수 있다.
위임
- 각 인스턴스에서 다른 로직을 수행하도록 하는 다른 방법으로는 위임(몇 가지 객체 중 하나를 선택해서 작업을 미루는 것)이 있다. 공통으로 사용되는 로직은 위임 클래스를 참조하는 클래스에 들어있지만, 변형은 여러 객체에 각각 구현된다.
- 위임을 사용해서 자기 자신에게 메시지를 보낸다면, '자기 자신'이 무엇인지 분명치 않다. 메시지는 윔임자 객체에 전달되거나 위임 클래스에 전달된다.
위임을 통해 유연성을 부여한 코드
위임을 응용해서 위임자 클래스(delegator)를 위임 메소드(delegated method)에 인자로 넘겨주는 방식
위임 클래스를 위임 메소드에 파라미터로 넘겨주지 않고, 생성자를 통해 필드에 저장하는 방식
(유연성이 중요하지 않은 경우라면, 필드를 통해 참조하는 편이 간편하다.)
플러그인 선택자
- 한두개의 메소드에서만 인스턴스별 행동이 필요하며, 모든 로직이 하나의 클래스 않에 들어가도 좋은 경우, 메소드 이름을 필드에 저장해 두고, 리플렉션(reflection)을 통해 메소드를 호출하는 것도 좋다.
익명 내부 클래스
- 인스턴스별 행동을 위해 자바가 지원하는 다른 기법은 익명 내부 클래스다. 익명 내부 클래스의 기본 아이디어는 한 곳에서만 사용되는 클래스를 생성해서 일부 메소드를 오버라이드 한 후, 지역적으로만 사용하는 것이다. 특정 지역에서만 사용되므로 이러한 클래스는 이름이 필요없다.
라이브러리 클래스
- 어떤 객체에도 적합하지 않은 기능은 빈 클래스를 하나 만들어서 정적 메소드로 구현하는 것이다. 라이브러리 클래스는 인스턴스화가 불가능한, 라이브러리 메소드만을 갖고 있는 클래스다.
- 가능하면 라이브러리 클래스는 객체로 변환하는것이 좋다. 라이브러리 클래스를 점진적으로 객체로 바꾸기 위해서는 정적 메소드를 인스턴스 메소드로 바꾸면 된다.