클래스에 유일한 메서드가 인자로 전달된 객체에 뭔가를 하는 메서드인 경우, 그런 메서드 하나뿐인 객체는 해당 메서드의 포인터 구실을 하며, 이러한 객체를 함수 객체라고 부른다.


 StringLengthComparator 객체는 문자열을 비교하는 데 사용될 수 있는 실행 가능 전략이다. 이러한 클래스들은 대부분 무상태 클래스이다. 따라서 아래처럼 싱글턴 패턴을 따르면 쓸데없는 객체 생성은 피할 수 있다.

이러한 실행 가능전략을 인자로하는 메서드들은 Comparator 인터페이스를 자료형을 받으며, 실행전략 클래스들은 Comparator 인터페이스를 구현해야 한다.

 

 실행 가능 전략 클래스는 익명 클래스로 정의하는 경우도 많다. 하지만 아래와 같이 사용하면 sort를 호출할 때마다 새로운 객체가 만들어진다. sort 함수가 여러 번 수행되는 클래스라면 함수객체를 private static final 필드에 저장하고 재사용하는 것을 고려해 보길 바란다.


private static final 필드를 활용한 클래스.


요약

함수 객체의 주된 용도는 전략 패턴을 구현하는 것이다. 자바로 이 패턴을 구현하기 위해서는 전략을 표현하는 인터페이스를 선언하고, 실행 가능 전략 클래스가 전부 해당 인터페이스를 구현하도록 해야 한다. 실행 가능 전략이 한 번만 사용되는 경우에는 보통 그 전략을 익명 클래스로 구현하며, 반복적으로 사용된다면 private static 멤버 클래스로 전략을 표현한 다음, 전략 인터페이스가 자료형인 public static final 필드를 통해 외부로 노출하는 것이 바람직하다.


하나의 클래스가 두 가지 이상의 기능을 가지고 있으며, 그 중 어떤 기능을 제공하는지 표시는 태그가 달린 클래스를 만날 때가 있다. 

위의 클래스에는 다양한 문제점이 있다. enum 선언, 태그 필드 , switch문 등의 상투적인 코드가 반복되며, 서로 다른 기능을 위한 코드가 한 클래스에 모여있으니 가독성도 떨어진다. 객체를 만들때마다 필요없는 필드도 함께 생성되므로 메모리도 낭비다. 즉 태그 기반 클래스는 너저분하고 오류 발생 가능성이 높고 효율적이지 않다. 이러한 클래스는 계승을 이용하여 리팩토링 해야한다.


 먼저 태그 값에 따라 달리 동작하는 메서드를 추상 메서드로 선언하는 추상 클래스를 정의한다. 그 추상 클래스를 클래스 계층 맨 위에 둔다. 그리고 태그 값에 좌우되지 않는 메서드, 데이터 필드를 모두 그 추상클래스에 둔다. 그 다음으로 태그 기반 클래스가 제공하던 각각의 기능을 방금 만든 최상의 클래스의 객체 생성 가능 하위 클래스로 정의하는 것이다.


인터페이스를 구현하는 클래스를 만들게 되면, 그 인터페이스는 해당 클래스의 객체를 참조할 수 있는 자료형 역할을 하게 된다. 인터페이스를 구현해 클래스를 만든다는 것은, 해당 클래스의 객체로 어떤 일을 할 수 있는지 클라이언트에게 알리는 행위이다.


 안좋은 예로 소위 상수 인터페이스 라는 것이 있다. 인터페이스에 메서드가 없고 static final 필드만 있다.

위의 예시 인터페이스는 인터페이스를 잘 못 사용한 것이다. 클래스가 어떤 상수를 어떻게 사용하느냐 하는 것은 구현 세부사항인데 인터페이스에 이러한 구현 세부사항을 정의하게 되면, 구현 세부사항이 API로 노출되기 때문이다.

상수를 API일부로 공개하고 싶을 때는 더 좋은 방법이 있다. 해당 상수가 이미 존재하는 클래스에 강하게 연결되어 있을 때는 해당 클래스에 상수를 정의하면 된다.

요약

인터페이스는 자료형을 정의할 때만 사용해야 한다. 특정 상수를 API의 잉ㄹ부로 공개할 목적으로는 적절치 않다.



자바에는 여러 가지 구현을 허용하는 자료형을 만드는 방법이 두 가지가 있다. 인터페이스와 추상 클래스가 그것이다. 이 두 방법의 차이는 추상 클래스는 구현된 메소드를 포함할 수 있지만 인터페이스는 그럴수 없으며, 추상클래스가 규정하는 자료형을 구현하기 위해서는 추상 클래스를 반드시 계승해야 한다는 것이다. 


 인터페이스는 믹스인을 정의하는 데 이상적이다. 믹스인은 클래스가 주 자료형 이외에 추가로 구현할 수 있는 자료형으로, 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰인다. (클래스 명 이외의 자료형을 정의)

 인터페이스는 비 계층적인 자료형을 만들 수 있도록 한다. 계층적으로 잘 정리되지 않는 것들이 있을 때 사용하면 유용하다. 

위의 예는 작곡가와 가수를 표현하는 인터페이스들이 있다. 그러나 가수이면서 작곡가인 사람들도 분명히 있다. 이러한 사람을 표현하는 것은 계승보다는 인터페이스를 2개다 구현함으로써 표현이 쉬워진다.


추상 골격 구현클래스를 중요 인터페이스마다 두면, 인터페이스의 장점과 추상 클래스의 장점을 결합할 수 있다. 인터페이스는 자료형을 정의하고, 구현하는 일은 골격 구현 클래스에 맡긴다. 클래스명에 Abstarct를 붙히는 관습이 있다.

위의 골격 구현 클래스는 추상 클래스를 자료형 정의 수단으로 사용했을 때 발생하는 제약사항들을 따르지 않아도 추상 클래스를 구현할 수 있도록 돕는다는 것이 장점이다. 추상 클래스는 객체를 생성하기 위해서는 해당 클래스를 반드시 계승하여서 객체를 생성해야 하지만, 골격 구현 클래스는 그러한 제약사항을 해결하도록 도움을 준다.

추상 클래스가 인터페이스보다 나은 점?

확장성이 좀 더 뛰어나다. 다음 릴리즈때 추상클래스의 공통메서드를 추가하면 추상클래스를 계승한 클래스들은 새로운 메소드를 바로 이용할 수 있지만, 인터페이스에 새로운 메소드를 정의하면 이를 구현한 클래스들은 반드시 이 메소드를 정의해야만 하기 때문이다.


재정의 가능 메서드를 내부적으로 어떻게 사용하는지 반드시 문서에 남기라는 뜻이다.

즉, 재정의 가능 메서드가 호출되는 모든 상황을 문서로 남겨야 한다는 규칙이다. 문서만 제대로 썼다고 계승에 적합한 설계가 되지는 않는다. 너무 애쓰지 않고도 효율적인 하위 클래스를 작성할 수 있도록 하려면 클래스 내부 동작에 개입할 수 있는 훅을 신중하게 고른 protected 메서드 형태로 제공해야 한다. 
 그렇다면 계승을 고려하여 클래스를 설계할 때 protected로 선언할 멤버는 어떻게 정하나? 직접해볼 수 밖에 없단다..ㄷㄷ.. 왜냐하면 널리 사용될 클래스를 계승에 맞게 설계하고, 메서드와 필드를 protected로 선언하는 과정에서 발생하는 결정들은 영원히 고수해야 하기 때문이다. 

계승을 허용하려면 반드시 따라야할 제약사항

1. 생성자는 직접적이건 간접적이건 재정의 가능 메서드를 호출해서는 안된다. 부모 클래스의 생성자가 자식 클래스의 생성자보다 먼저 호출되기 때문에 메서드 동작을 보장할 수 없기 때문이다.

2. clone이나 readObject 메서드 안에서 직접적이건 간접적이건 재정의 가능한 메서드를 호출하지 않도록 주의해야 한다. readObject 메서드 안에서 재정의 가능 메서드를 호출하게 되면 , 역직렬화가 되기전에 해당 메서드가 실행되어 버린다. 그리고 clone메서드의 경우라면 하위 클래스의 clone메서드가 복사본 객체의 상태를 미처 수정하기도 전에 해당 메서드가 실행되어 버린다.

요약

계승에 맞도록 설계하고 문서화하지 않은 클래스에 대한 하위 클래스는 만들지 않는 것이다. 


 계승은 코드 재사용을 돕는 강력한 도구지만, 항상 최선이라고는 할 수 없다. 그리고 해당 클래스가 속한 패키지 밖에서 계승을 시도하는 것은 위험하다. 왜냐하면 메서드 호출과 달리 계승은 캡슐화 원칙을 위반하기 때문이다. 

 예를들어, 하위 클래스가 정상 동작하기 위해서는 상위 클래스의 구현에 의존할 수 밖에 없다. 상위 클래스의 구현은 릴리즈가 거듭되면서 바뀔 수 있는데 이러한 과정속에서 하위클래스의 코드는 수정된 적이 없어도 망가질 수 있다. (캡슐화 원칙 위반)

 이러한 문제를 해결하귀 위해서는 기존 클래스를 계승하는 대신, 새로운 클래스에 기존 클래스 객체를 참조하는 private 필드를 하나 두는 것이다. 이런 설계 방법을 구성이라고 한다.



 계승 매커니즘은 상위 클래스의 문제를 하위 클래스에 전파시킨다. 반면 구성 기법은 그런 약점을 감추는 새로운 API를 설계할 수 있다.

계승은 어떠한 경우에 사용해야 할까?

계승은 하위클래스가 상위 클래스의 하위 자료형이 확실한 경우에만 바람직하다. 다시 말해서, 클래스 B는 클래스 A와 "IS-A" 관계가 성립할 때만 A를 계승해야 한다. 


변경 불가능 클래스는 그 객체를 수정할 수 없는 클래스다. 변경 불가능 클래스는 변경 가능 클래스보다 설계하기 쉽고, 구현하기 쉬우며, 사용하기도 쉽다. 오류 가능성도 적고, 더 안전하다

규칙1. 객체 상태를 변경하는 메서드를 제공하지 않는다(setter)

규칙2. 계승할 수 없도록 한다.  

그러면 잘못 작성되거나 악의적인 하위 클래스가 객체 상태가 변경된 것처럼 동작해서 변경 불가능성을 깨뜨리는 일을 막을 수 있다. 클래스에 final을 붙히면 된다.

규칙3. 모든 필드를 final로 선언한다.

그러면 시스템이 강제하는 형태대로 프로그래머의 의도가 분명하게 전달된다. 동기화없이 쓰레드에도 안전하다.

규칙4. 모든 필드를 private로 선언한다.

그러면 클라이언트가 참조하는 변경 가능 객체를 직접 수정하는 일을 막을 수 있다. 

규칙5. 변경 가능 컴포넌트에 대한 독점적 접근권을 보장한다.

클래스에 포함된 변경 가능 객체에 대한 참조를 클라이언트는 획득할 수 없어야 한다. 클라이언트가 제공하는 객체로 초기화해서도 안되고, 접근자 또한 그런 필드를 반환해서는 안된다. 따라서 방어적 복사본을 만들어야 한다.

변경 불가능 객체의 장/단점

장점

변경 불가능 객체는 단순하다. 생성될 때 부여된 한가지 상태만 갖기 때문에 생성자가 불변식을 확실히 따른다면, 해당 객체는 불변식을 절대 어기지 않는다.

변경 불가능 객체는 스레드에 안전하다. 어떤동기화도 필요없다. 그렇기 떄문에 private static final 등을 이용하여 자유롭게 공유할 수 있다. 

변경 불가능 객체는 다른 객체의 구성요로도 훌륭하다. 불변이기 때문에 맵과 집합의 키로 유용하다.

단점

값마다 별도의 객체를 만들어야 한다. 따라서 객체 생성 비용이 높을 가능성이 있다.


요약

변경 가능한 클래스로 만들 타당한 이유가 없다면, 반드시 변경 불가능 클래스로 만들어야 하며, 이렇게 하기 힘들다면 변경 가능성을 최대한 제한하여야 한다. 특별한 이유가 없다면 모든 필드는 final로 선언하며, 생성자 이외의 public 초기화 메서드나 정적 팩터리 메서드를 제공하면 안된다.


 이런 클래스는 데이터 필드를 직접 조작할 수 있어서 캡슐화의 이점을 누릴수가 없다. 불변식도 강제할 수 없고, 필드를 사용하는 순간에 어떤 동작이 실행되도록 만들수도 없다. 이러한 클래스는 private필드와 public 접근자 메서드(getter)로 바꿔야 한다.

 선언된 패키지 밖에서도 사용 가능한 클래스에는 접근자 메서드를 제공하라. 그래야 클래스 내부 표현을 자유로이 수정할 수 있게 된다. 

하지만 package-private 클래스나 private 중첩 클래스는 데이터 필드를 공개하더라도 잘못이라 말할 수 없다. 클래스의 내용을 제대로 기술하기만 한다면, 접근자 메서드보다는 시각적으로 깔끔하다. private 중첩 클래스의 경우는, 그 클래스의 바깥 클래스 외부의 코드는 아무 영향도 받지 않을 것이기 떄문이다.


요약

public 클래스는 변경 가능 필드를 외부로 공개하면 안된다. 변경 불가능 필드인 경우에는 외부로 공개하더라도 많이 위험하진 않겠지만, 그럴 필요가 없다고 생각된다. 하지만 package-private나 private로 선언된 중첩 클래스의 필드는 그 변경 가능 여부와는 상관없이 외부로 공개하는 것이 바람직할 때도 있다.


+ Recent posts