#007 클래스와 인터페이스 세번째

자바에서는 클래스와 인터페이스를 설계하는데 사용할 수 있는 강력한 요소들을 제공한다. 이 장에서는 우리가 만드는 클래스와 인터페이스가 쓸모있고 강력하며 유연성이 있도록 하기 위해 도움을 주는 지침을 설명한다.


태그 (tagged) 클래스보다는 클래스 계층을 사용하자

태그(tagged) 클래스란 인스턴스들이 두 개 이상의 특성으로 분류되고 그런 특성을 나타내는 태그(tag) 필드를 갖는 클래스를 말한다.

// Tagged class - 클래스 계층보다 매우 조악하다!
class Figure {
    enum Shape { RECTANGLE, CIRCLE };
 
    // Tag field - 이 도형의 형태
    final Shape shape;
 
    // 이 필드는 shape가 RECTANGLE일 때만 사용된다.
    double length;
    double width;
 
    // 이 필드는 shape가 CIRCLE일 때만 사용된다.
    double radius;
 
    // circle 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }
 
    // rectangle 생성자
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }
 
    double area() {
        switch(shape) {
          case RECTANGLE:
            return length * width;
          case CIRCLE:
            return Math.PI * (radius * radius);
          default:
            throw new AssertionError();
        }
    }
}

태그(tagged) 클래스는 단점 투성이이다.
열거형(enum) 선언, 태그 필드들, swith문 등을 포함해서 각종 진부한 코드들로 혼란스럽게 구성되어 있다.
다른 종류의 인스턴스에 속하는 부적절한 필드를 가지고 있어서 필요없는 메모리의 할당과 해지가 증가한다.
다른 종류의 인스턴스를 태그 클래스에 추가하려면 모든 switch문의 case를 추가해야 한다.
이런 문제를 해결하기 위해서 태그 클래스를 클래스 계층으로 변환한다.


태그 클래스를 클래스 계층으로 변환하는 방법

1. 태그 값에 따라 동작이 달라지는 각 메소드를 추상 메소드로 만든다.
– Figure 클래스의 area 메소드
2. 태그 값과 관계없이 동작하는 메소드가 있다면 추상 클래스에 넣는다.
– Figure 클래스에는 그런 메소드가 없다.
3. 모든 종류의 인스턴스들이 사용하는 필드들이 있다면 그것들도 추상 클래스에 넣는다.
– Figure 클래스에는그런 필드가 없다.
4. 태그 클래스의 각 인스턴스 종류를 루트 클래스의 서브클래스로 정의한다.
– Circle, Rectangle
5. 루트 클래스의 각 추상 메소드를 구현하는 메소드를 각 서브 클래스에 만든다.
– area 메소드
// 태그 클래스를 대체하는 클래스 계층 코드 - 루트 클래스
abstract class Figure {
    abstract double area();
}
// 루트 클래스의 서브 클래스(실체 클래스)
class Circle extends Figure {
    final double radius;
 
    Circle(double radius) { this.radius = radius; }
 
    double area() { return Math.PI * (radius * radius); }
}
// 루트 클래스의 서브 클래스(실체 클래스)
class Rectangle extends Figure {
    final double length;
    final double width;
 
    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    double area() { return length * width; }
}

클래스 계층 코드는 앞에 나온 태그 클래스의 모든 단점을 해소시킨다.
또 다른 장점은, 타입들 간의 자연적인 계층관계를 반영할 수 있다는 것이다.
정사각형은 직사각형의 특별한 종류이므로(둘 다 불변이라고 가정하면), 클래스 계층에 포함될 정사각형 서브 클래스 코드는 다음과 같다.

class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

앞에 나온 클래스 계층(Figure) 코드의 필드들은 접근자 메소드를 사용하지 않고 직접 접근된다는 점에 주목하자. 코드를 간결하게 만드느라 그런 것이므로, 클래스가 public이라면 그렇게 하면 안될 것이다(항목 14).

요약 : 태그 클래스는 적합하지 않으므로, 만일 태그 클래스를 갖는 클래스를 작성하고 싶다면, 태그를 없앨 수 있는지, 그리고 태그 클래스를 클래스 계층로 교체할 수 있는지에 대해 심사숙고하자.

전략을 표현할 때 함수 객체를 사용하자

What is a ‘Function-Object’?
자바는 객체지향을 구현하기 위해 모든 것을 클래스라는 일종의 인스턴스 템플릿으로 구현하도록 되어 있다.
단순한 유틸성 기능을 구현할 때에도 XXXUtils이라는 뭔가 우주에서 날아온 듯한 이름으로 클래스를 만들고 그 안에 static을 사용한 메서드로 기능을 구현한다.
자바는 언어 차원에서 모든 걸 클래스화 해야만 해서 여러 상황에서 불편한 점이 있을때가 많다.
간단한 예를 들어보자.
책에도 나와있듯이, 공식 API의 Camparator 는 정렬을 위한 함수 객체 인터페이스이다. 자바에서는 정렬 전략으로 사용하기 위해 저 클래스를 풀로 구현해야 한다.
이런 식으로 말이다.

// 이러한 문법적 껍데기를 반드시 선언해야 한다.
public class StringLengthComparator implements Comparator {
 
        // 우리가 필요한건 이 메소드의 기능 뿐인데도...
	public int compare(String s1, String s2) {
		return s1.length() - s2.length();
	}
}
// 아래와 같이 사용
Arrays.sort(array, new StringLengthComparator());

하지만 일부 언어에서는 함수 포인터(function pointer), 위임(delegate), 람다식(lamda expression) 또는 이와 유사한 기능을 제공하여 프로그램에서 특정 함수의 호출을 저장하거나 전달할 수 있다.

 lang="c">
// C의 함수 포인터 방식
#include 
void hello(char *name) {
    printf ("Hi %s\n", name);
}

int main() {
    // 반환값이 void 이고 매개변수가 캐릭터인 함수 포인터 선언
    void (*Func)(char *);

    // 그 포인터에 hello 가르키게 함
    Func = hello;

    // Func 는 변수로 사용할 수 있지만 본질은 함수이다.
    // 함수 포인터 실행
    Func("test");
}
// C#의 위임 방식
// delegate 타입 선언
public delegate void DoSomething(string command);
 
// 위임할 메서드
public void Action(string direction){
    // 메서드 구현
}
...
...
// delegate 생성
DoSomething done = new DoSomething(Action);
 
// 위임 객체를 통해 변수로서 함수를 취급가능하다.
done("left");
// 자바스크립트의 익명 람다 함수
// each의 람다 함수를 지정
$.each(function(key, value) {
    // each 구현
});

그렇다면, 위 기능을 새로 구현해보자. javascript 에서는 익명 함수를 지원하며, 위와 같은 구현이 아주 간단하다.

// 익명 함수를 사용한 배열 정렬법
arrayObject.sort(function(s1, s2) {
    return s1.length - s2.length;  
})

자바는 위 예제의 자바스크립트처럼 함수(혹은 메서드)만을 생성할 수 없고 반드시 클래스와 쌍으로 움직여야 한다.
그래서 새로운 업무 방식이 생겨 객체에 특정한 알고리즘을 교체하려면 아예 그 객체를 새로 만들거나(설마 이러진 않겠지요…), 그 업무 방식에 맞는 새로운 알고리즘을 구현한 메소드를 포함한 클래스를 생성하여 처리 메서드로 전달해야 한다.
이 굵은 글씨로 표현한 클래스 객체를 함수 객체 라고 부른다.
보통은 메서드를 하나만 가지는 인터페이스 형식으로 구현되나, 이 함수 객체를 사용하는 측에 따라서는 메서드 여러개가 구현되어 있을 수도 있다.


전략 패턴 (Strategy Pattern)

함수 객체가 제일 많이 쓰이는 곳은 역시 전략 패턴이다.
전략 패턴(Strategy Pattern) 은 기본적인 목적인 비슷하지만 흐름에서 변화하거나 교체할 수 있는 부분을 따로 분리하여 쉽게 변화시키고 새로 지정할 수 있게 해주는 패턴이다.
여러 메서드를 하나의 클래스에 집어넣은 것보다 유연성이 좋고 잘 디자인 된 전략은 다른 객체에서도 활용할 수 있다.
자바의 전략 패턴은 함수 객체를 사용해야 가능하다.

위에 예시로 든 Comparator 는 전략 패턴의 모범생 같은 구현이라고 볼 수 있겠다.
이제 예제와 함께 더 살펴보자.
만일 한 어느 학원 수강생 처리시스템에서 모든 등록 수강생의 주소를 새로운 주소로 바꾸는 작업을 해야 한다고 가정한다.
보통은 이렇게 처리할 것이다.

public List<Student> changeNewAddress(List<Student> student) {
    for(Student s : student) {
        String newAddr = getNewAddress(s.getAddress);
        s.setAddress(s);
    }
    return student;
}

수강생의 주소를 바꾸는 일 외에 학생들의 프로필 사진도 전부 섬네일화 해야 하는 작업이 생겼다고 가정해보자.
그렇다면 간단한 구현으로는 새로운 createProfilePhotoToThumb 메소드가 생길 것이다.

하지만 이렇게 접근하지 말고 다른 방법을 써 보자.
일단, 하려는 작업은 둘다 학생에 대한 배치 처리이다. 학생 하나하나를 순회하며 해당 학생의 특정 필드에 대해 작업을 수행한다.

여기서 “새로운 주소 변경” 과 “섬네일 만들기” 는 전략으로 볼 수 있고 위에서 설명한 함수 객체로 구현해볼 수 있다.
그렇다면 먼저 함수 객체 인터페이스는,

interface Transfer<T> {
    T transfer(T value);
}

그렇다면 전략 객체 구현은,

// 들어온 주소 문자열을 새 주소 변경하는 함수 객체 클래스.
class ChangeAdressJob implements Transfer<Student> {
    public Student transfer(Student address) {
        // 구 주소를 새 주소로 바꾸는 로직
    }
}
 
// 들어온 사진을 섬네일화 하는 함수 객체 클래스
class CreateThumbnail implements Transfer<Student> {
    public Student transfer(Student photo) {
        // 프로필 파일을 분석하여 해당 파일을 섬네일을 만듬
    }
}

그렇다면 이제 저 함수 객체를 사용하는 클래스는 다음과 같을 것이다.

class StudentManager {
 
    private final List<Transfer<Student>> transferStrategy = new LinkedList<Transfer<Student>>();
 
    public List<Student> loadStudents() { ... }
 
    // 배치 처리.
    // 학생 리스트에 대해 전략 함수 객체(Transfer 구현체)를 하나하나 실행함.
    public void executeBatch(List<Student> students) {
        for(Student s : students) {
            for(Transfer t : transferStrategy) {
                t.transfer(s);
            }            
        }
    }
 
    // 전략 객체를 전략 셋에 추가한다.
    public void addTransfer(Transfer t) {
        transferStrategy.add(t);
    }
 
    public static void main() {
 
        StudentManager manager = new StudentManager();
        List<Student> students = manager.loadStudents();
 
        manager.addTransfer(new ChangeAdressJob());
        manager.addTransfer(new CreateThumbnail());
 
        manager.executeBatch(students);
    }
}

static 멤버 클래스를 많이 사용하자

중첩(nested) 클래스는 다른 클래스의 내부에 정의된 클래스로, 외곽(enclosing) 클래스를 지원하는 목적으로만 존재해야 한다.
중첩 클래스에는 4가지 종류가 있는데, 이 항목에서는 이런 종류의 중첩 클래스를 언제, 왜 사용하는지 알려줄 것이다.

1. static 멤버 클래스

가장 간단한 중첩 클래스로써, 다른 클래스의 내부에 선언되어 있고 외곽 클래스의 모든 멤버들(private로 선언된 것까지도)을 사용할 수 있는 일반 클래스라고 생각하면 된다.
외곽 클래스의 static 멤버이므로, 다른 static 멤버와 동일한 접근 규칙을 준수해야 한다. (private로 선언되었다면 외곽 클래스의 내부에서만 사용 가능하다.)

2. static이 아닌 멤버 클래스

구문적으로 봐서, static 멤버 클래스와의 차이점은 static이 아닌 멤버 클래스는 static 수식어가 없다는 것이다.
구문적으로는 유사하지만 두 종류의 중첩 클래스는 서로 많이 다르다.
static이 아닌 멤버 클래스의 인스턴스는 자신과 연관되는 외곽 클래스의 인스턴스가 있어야만 생성할 수 있다.

3. 익명(anonymous) 클래스

익명 클래스는 명칭과 같이 이름이 없다. 자신을 포함하는 외곽의 클래스도 아니다.
다른 멤버와 함께 선언되지도 않고 사용 시점에서 선언과 인스턴스 생성이 동시에 이루어진다.
익명클래스는 함수 객체(function object) (항목 21)를 생성하는데 많이 사용된다.

4. 지역(local) 클래스

지역 클래스는 4가지 클래스 중 제일 적게 사용된다.
지역가 선언될 수 있는 곳이면 어디든 선언될 수 있으며, 지역변수와 동일한 유효 범위를 갖는다.
자바의 중첩 클래스 4가지
public final class IHaveMemberClasses {
 
    // 스태틱 멤버 클래스 선언
    public static class StaticMemberClass {}
 
    // 멤버 클래스 선언
    public class MeberClasss {}
 
    // 인터페이스를 받는 메서드
    public static void anonymouseClassMethod(Callable<String> call) {}
 
    public static void main() {
 
        // 지역 클래스 선언
        class LocalClass {};
 
        // 익명 클래스 사용.
        // 메소드 호출 시 바로 생성하여 전달함.
        IHaveMemberClasses.anonymouseClassMethod(new Callable<String>() {
            public String call() {
                return "call!";
            }
        });
 
        // 스태틱 멤버 클래스 생성. 그냥 사용가능
        // 만일 import 구문에 import IHaveMemberClasses.StaticMemberClass 를 써 놓으면
        // new StaticMemberClass() 로 그냥 사용할수도 있다.
        new IHaveMemberClasses.StaticMemberClass();
 
        // 멤버 클래스 생성.
        // 먼저 외곽 클래스가 생성되어야 사용할 수 있다.
        IHaveMemberClasses outer =  new IHaveMemberClasses();
        MeberClasss nonStaticMember = new outer.MeberClasss();
 
        // 로컬 클래스는 선언 메소드 스택 내에서 자유로이 사용 가능
        new LocalClasss();       
    }
}
요약 : 만일 멤버 클래스가 외곽 클래스의 인스턴스를 참조할 필요가 있다면 static이 아닌 멤버 클래스로 만들고, 그렇지 않다면 static 멤버 클래스로 만든다. 클래스가 어떤 메소드에 속한다는 가정하에, 만일 한 곳에서만 그 클래스의 인스턴스를 생성할 필요가 있고, 그 클래서의 특성을 나타내는 타입이 이미 존재한다면, 익명 클래스로 만들고 그렇지 않으면, 지역 클래스로 만든다.

creative by javacafe

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

%d 블로거가 이것을 좋아합니다: