-제약 조건들은 반드시 문서화 되어야 한다.
-유효성 검사를 통해 인자가 유효하지 않을 때 적절한 예외를 발생시키면, 빠르고 깔끔하게 메소드를 종료할 수 있다.
-public 메소드의 경우 Javadoc 의 @throws 태그를 써서 인자값에 제약조건을 어겼을 때 예외를 문서화 해야한다.
/**
* (this mod m)의 값을 가지는 BigInteger를 리턴한다.
* 이 메소드는 항상 음수가 아닌 BigInteger를 리턴한다는 점이
* remainder 메소드와 다르다
*
* @param m 나누는 수, 반드시 양수이다.
* @return this BigInteger를 m으로 나눈 몫, 항상 양수이다.
* @throw ArithmeticException m <= 0 일 때 발생한다.
*/
public BigInteger mod(BigInteger m){
if(m.signum() <= 0)
throw new ArithmeticException("Modulus not positive");
...// 실제 계산
}
-public이 아닌 메소드에서는 assertion을 써서 인자값을 검사해야 한다. (jdk 1.4 이상이면 assert명령어 사용)
|
| {code} //================================================================= // 메소드의 인자로 전달된 값을 미리 검증 // - public BigInteger mod(BigInteger m){ assert m.signum() <= 0; ...// 실제 계산 } //================================================================= {code} |
-메소드 안에 직접 쓰이지 않지만 나중에 다른 곳에서 쓰기 위해 필드로 저장해 놓는 인자가 유효한지 검사하는 것은 매우 중요하다.
-클래스의 불변규칙을 어기는 객체가 생성되는 것을 막기 위해 생성자에 전달되는 인자의 유효성을 검사하는 것은 매우 중요하다.
-메소드 수행 과정에서 묵시적으로 인자에 대한 유효성을 검사할 때 잘못된 예외가 발생하는 경우가 있다.
-예외변환(exception translatioin) 구현패턴을 써서 자연스럽게 발생하는 예외를 메소드의 명세문서에 나온 예외로 바꿔야 한다.
-메소드나 생성자를 만들 때 인자에 어떤 제약 조건이 있는지 반드시 검토해야한다.
-제약 조건들을 메소드 본문의 시작부분에서 이 제약조건을 검사해야한다.
| 가짜 불편 클래스 | Period 인스턴스 공격하기 |
|---|---|
| {code} package efective; |
import java.util.Date;
//=================================================================
//가짜 불변 클래스
//
//
/**
//=================================================================
// 두 날짜 사이의 기간 표현
//
if(start.compareTo(end) > 0)
throw new IllegalArgumentException(start + "after" + end);
//
//=================================================================
// 시작일 리턴
//
//=================================================================
// 종료일 리턴
//
}
|
package efective;
import java.util.Date;
public class PeriodMain{
public static void main(String[] args){
//=================================================================
// 객체생성
//
//=================================================================
// Date Log Test
//
!p165.jpg!|
|▲ 이 클래스는 불변 클래스, 시작일이 종료일보다 앞설 수 밖에 없어 보인다.
그러나 Date 클래스가 가변 클래스 이기에 불변규칙은 쉽게 깨진다. |▲ 위와 같은 공격으로 부터 보호하려면, 생성자에 전달되는 변경가능 인자들을 방어복사(defensive copy)해야 하고,
원본대신에 복사본으로 Period 인스턴스를 만들어야 한다. |
h4.
||인자를 방어복사||또 다른 공격||
|
//=================================================================
// 인자를 방어복사한다.
//
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end)>0)
throw new IllegalArgumentException(start + "after" + end);
}
//=================================================================
|
//=================================================================
//Period 인스턴스에 대한 다른 공격
//
|
|인자의 유효성을 검사하기 전에 먼저 복사하고 나서 원본이 아닌 복사본의 유효성을 검사한다.
방어복사에 Date클래스의 Clone 메소드를 쓰지 않는다.
Date클래스는 final이 아니므로 Clone메소드가 리턴하는 객체가 정확히 java.util.Date 클래스의 인스턴스라고
확신할 수 없다. 따라서 하위클래스가 생길 수 있는 방어복사에 Clone 메소드를 쓰지 말아야한다.|▲ Period 인스턴스 접근자 메소를 이용해 가변 객체참조 하기.|
h4.
||*Period 클래스는 완전한 불변 클래스가 되었다.*||
|
package efective;
import java.util.Date;
//=================================================================
//가짜 불변 클래스
//
//
/**
//=================================================================
// 인자를 방어복사한다.
//
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end)>0)
throw new IllegalArgumentException(start + "after" + end);
}
//=================================================================
//=================================================================
// 시작일 내부 필드를 방어복사하여 리턴한다.
//
//=================================================================
// 종료일 내부 필드를 방어복사하여 리턴한다.
//
|
- 접근자에서는 생성자와 달리 clone 메소드를 써서 방어복사한다.
- Period 내부 Date객체는 java.util.Date 클래스의 인스턴스 라는 것이 확실하기 때문에 clone 메소드를 써도 문제가 없다.
- 메소드나 생성자 생성시, 클라이언트에서 제공하는 객체를 내부 구조에 저장해야 할 때, 그 객체가 변경가능한지 생각하고, 변경 가능하다면 객체를 방어복사해서 클래스 내부필드에 원본이 아닌 복사본을 저장해야 한다.
- 길이가 0이 아닌 모든 배열은 언제나 변경가능하다. 클라이언트에게 원본 배열 자체를 리턴하지 말고, 배열을 방어복사해서 리턴해야 한다.
- 방어복사와 같은 귀찮은 작업을 전혀 신경쓰지 않으려면, 불변 객체를 써야 한다.
h2. 항목 25 : 메소드 시그니처를 신중하게 설계하라
- API 설계에 대한 힌트를 모은 것이다.
- API를 배우기 쉽고, 쓰기 쉽고, 오류도 적게 발생하도록 만드는게 목적이다.
h4. 1. 메소드 이름을 신중하게 결정하라
-이름은 표준 작명규칙(standard naming convention)을 따라야한다.
-이해하기 쉽고, 같은 패키지에 있는 다른 클래스 혹은 인터페이스들과 일관성 있는 이름을 짓도록 해야한다.
-적절한 이름을 찾기 어렵다면 자바 플랫폼 라이브러리 API를 참고한다.
-*Patrick Chan*의 *The Java Developers Almanac*도 매우 유용한 자료이다. [http://www.exampledepot.com/egs/]
h4. 2. 편리한 메소드를 제공하기 위해 너무 애쓰지 마라
-메소드가 너무 많으면 클래스를 배우고, 쓰고, 문서화하고, 시험하고, 관리하기 어려워진다.
-행위 하나하나에 대해 완벽하게 기능을 발휘하는 메소드를 제공히라.
-자주 사용하는 경우에만 속기(shorthand) 메소드 를 제공할지 고민하라.
h4. 3. 인자를 너무 많이 받지 마라.
-인자는 적을수록 좋다.
-대부분의 프로그래머는 인자 개수가 3개를 넘으면 기억하지 못한다.
-동일한 타입의 인자가 죽 이어져 있으면 훨씬 더 위험하다.
*인자의 길이를 줄이는 두가지 방법*
-첫째, 인자의 일부만 받는 여러 개의 메소드로 나누는 것이다.
(예 : java.util.List 인터페이스에는 처음과 마지막 요소의 인덱스를 찾는 메소드가 없다. 이런 메소드 구현시 모두 세개의 인자가 필요하다.
대신 indexOf, lastIndexOf를 조합하면 원하는 작업을 처리할 수 있다.)
-둘째, 인자들을 모아서 보관하는 헬퍼 클래스(Helper class)를 사용한다.
(예 : 카드케임 클래스를 만든다고 가정시 숫자와 무늬라는 두개의 인자를 받는 메소드가 계속 생길 것이다. 따라서 숫자와 무늬를 멤버로 가지는
정적멤버클래스를 헬퍼클래스로 생성해두어 두 인자 대신에 이 헬퍼 클래스 타입으로 바꾸면
API와 내부 구조도 깔끔하게 정리된다.꾼다면 가 계속 나오는 것을 발견할 수 있다. )
h4. 4. 인자의 타입으로 클래스보다 인터페이스를 써라
-인자를 정의할 수 있는 적절한 인터페이스가 있다면, 이를 구현한 클래스 보다 인터페이스로 타입을 정하는 것이 좋다.
(예: Hashtable 을 인자로 받는 메소드를 만들 필요가 없다.)
-Map을 인자로 받는 메소드는 Hashtable, HashMap, TreeMap과 같은 현재 구현된 모든 종류의 Map구현체와 앞으로 구현될 모든 Map구현체를 쓸 수 있다.
h4. 5. 기능객체(function object)는 신중하게 써라.
h2. 항목 26 : 메소드를 중복정의할 때는 신중하라
|
package effective;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
//=================================================================
// 틀린 구현 - 메소드 중복정의를 잘못 쓰고 있다.
//
public static String classify(Set s){
return "Set";
}
public static String classify(List l){
return "List";
}
public static String classify(Collection c){
return "Unknown Collection";
}
public static void main(String[] args){
Collection[] tests = new Collection[]{
new HashSet(), // Set
new ArrayList(), // List
new HashMap().values() // Set 도 List도 아닌 것
};
for(int i=0; i<tests.length; i++){
//
|
//=================================================================
// 예상출력 결과
//
!p172.jpg!
- 문제점 : classify 메소드가 중복정의 메소드(overloaded method)라는 것이다.
- 중복정의 메소드 중에서 어떤 것을 호출할지 고르는 것은 컴파일 시점에서 결정난다.
때문에, 실행시점에 달라지는 타입은 중복정의 메소드를 고를 때 아무런 영향을 줄 수 없다.
|
*<중복정의 메소드 선택은 정적으로 결정, 재정의 메소드 선택은 동적으로 결정>*
||메소드를 재정의 하려면 하위클래스와 상위클래스의 메소드가 같은 시그니처를 가져야 한다.||클래스 A에 선언한 name 메소드를 클래스 B와 C가 재정의 한다.||
|
//================================================
// 재정의 메소드
//
class B {
String name() { return "B" ; }
}
class C {
String name() { return "C" ; }
}
public class Overriding {
public static void main(String[] args) {
A[] tests = new A[] { new A(), new B(), new C()}
for(int i=0; i<tests.length; i++) {
System.out.println("---tests["i"].name()---["tests[i].name()"]");
}
}
}
//================================================
|
//================================================
// 예상출력 결과
//
//================================================
// 실제출력 결과
//
|
-부모 클래스와 자식 클래스에 동일한 메소드가 존재할 경우 :
컴파일러가 동일한 메소드가 중복되었다고 판단, 묵시적으로 부모 클래스의 메소드를 숨기고 자식 클래스의 메소드를 실행한다.
||CollectionClassifier 예제에서 classify 메소드를 하나로 합치고 instanceof 를 써서 타입을 검사해보자.||
|
package effective;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
//=================================================================
// 틀린 구현 - 메소드 중복정의를 잘못 쓰고 있다.
//
public static void main(String[] args){
Collection[] tests = new Collection[]{
new HashSet(), // Set
new ArrayList(), // List
new HashMap().values() // Set 도 List도 아닌 것
};
for(int i=0; i<tests.length; i++){
//
|
|!p172_1.jpg!|
-혼란을 주지 않고 메소드를 중복정의 하는 방법?
가장 안전하고 보수적인 방법은 같은 개수의 인자를 갖는 중복정의 메소드를 만들지 않는 것이다.
//================================================
// ObjectOutputStream 클래스를 이용한 예.
//
void writeBolean(boolean) // boolean 값을 출력한다.
void writeInt(int) // int 값을 출력한다.
void writeLong(long) // long 값을 출력한다.
//================================================
▲ 클래스는 기본타입과 일부 참조타입에 대한 다른 이름의 write 메소드들을 가지고 있다.
write 메소드들을 중복정의 하지 않고, writeBolean(..), writeInt(..), writeLong(..) 과 같은 전혀 다른 시그니처를 갖는다.
readBolean(), readInt(), readLong() 와 같이 각 write 메소드에 대응되는 이름을 가진 read 메소드를 제공할 수 있다.
같은 시그니처를 가지면서 리턴 타입만 다른 메소드는 만들 수 없다. 따라서 write 메소드는 중복정의 할 수 있어도 read 메소드는 중복정의가 불가능하다.
같은 수의 인자를 가진 중복정의 메소드나 생성자가 많더라고, 어떤 메소드가 어떤 인자 집합을 처리하는지 명확하기만 하다면, 프로그래머들은 혼란에 빠지지 않을 것이다.
//================================================
ArrayList 클래스를 이용한 예.
//
Comparable 인터페이스가 등장하기 전까지 compareTo 메소드로 같은 타입을 비교해 왔다.
-> public int compareTo(Sting s);
Comparable 인터페이스 등장후 일반 compareTo메소드도 제공한다.
->public int compareTo(Object o);
compareTo(Object)메소드가 중복정의 메소드 지침을 어기고 있지만, 이 메소드들이 정확하게 똑같이 행동한다면 큰 문제가 되지 않는다.
//================================================
// 중복정의 메소드들 똑같이 행동하기.
//
public boolean equals(Object o) {
return o instanceof String && equals((String) o );
}
//================================================
자바 플랫폼 라이브러리의 중복정의 메소드들은 거의 이 장의 지침을 잘 지키고 있다.
//================================================
// String 클래스(중복정의 메소드)
//
Tip
- 정리
- 메소드를 중복정의 할 수 있다는 것을 반드시 중복정의 하라는 것을 받아들이지 말아야 한다.
- 인자 수가 같은 메소드가 여러개 생기도록 중복정의 하지 말아야 한다.
- 기존 클래스가 새로운 인터페이스를 구현하는 것과 같은 상황을 피할 수 없으면, 중복정의 메소드의 행동을 똑같이 만들어야 한다.
*collection : <http://bp2.blogger.com/_pvfHUBL4tpw/R9cwpHfF6rI/AAAAAAAAAFA/CZSpPa4CfIM/s1600-h/java_collection.gif>
h2. 항목 27 : 널(null)이 아닌 길이가 0인 (zero-length) 배열을 리턴하라
//================================================
// Null 대신 길이가 0인 배열 리턴하기
//
//================================================
// 간단하게 처리하리
//
▲ 길이가 0인 배열을 리턴 할 수 있는 곳에서 null을 리턴하는 메소드를 쓸때 마다, 필요 없는 코드가 반복되어야 한다.
* 배열을 생성, 할당에 드는 비용때문에 길이가 0인 배열보다 null을 리턴할 수 있지만 다음과 같은 이유로 잘못 된 것을 알 수 있다.
** 첫째, 진짜 성능에 문제가 되는 부분이 어디인지 프로파일링하지 않고 성능에 대해 말하는 것은 옳지 않다.
** 둘째, 길이가 0인 배열은 불변 객체이고 불변 객체는 항상 자유롭게 공유할 수 있기 때문에 하나만 만들어 놓으면 모든 메소드에서 쓸 수 있다.
//================================================
// Null 대신 길이가 0인 배열 리턴하기
//
class Cheese {
String name;
Chess(String name) {
this.name = name;
}
public static final Chesse STILTON = new Cheese("Stilton");
public static final Chesse CHDDAR = new Cheese("Cheddar");
}
class CheeseShop {
private static Cheese[] ECA = new Chesse[0];
private List cheesesInStock = Collections.singletonList(Cheese.STILTON);
private final static Cheese[] NULL_CHESSE_ARRAY = new Cheese[0];
/**
public class Main {
static CheeseShop shop = new CheeseShop();
public static void main(String[] args) {
Cheese[] cheeses = shop.getCheeses();
if(Arrays.asList(cheese).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
if(!Arrays.asList(cheese).contains(Cheese.CHEDDAR))
System.out.println("Oops, too bad.");
}
}
//================================================
Tip
- 정리
- 배열을 리턴하는 메소드에서 null을 리턴할 이유가 전혀없다. null을 리턴할 상황이라면 길이가 0인 배열을 리턴하면 된다.
h2. 항목 28 : 외부에 제공하는 API의 모든 구성요소에 대해 문서화 주석을 달아라
- 모든 API에는 명세문서가 있어야 한다.
- 자바 프로그래밍 언어에서는 Javadoc이라는 유틸리티를 제공 하므로 API 문서 작업이 쉬워졌다.
- 문서화 주석을 작성하는 규칙은 The Javadoc Tool Home Page[http://java.sun.com/j2se/javadoc/]에 정의되어있다.
h4. 1. API를 문서화하려면, 외부에 제공하는 모든 클래스, 인터페이스, 생성자, 메소드, 필드에 문서화 주석을 달아야 한다.
h4. 2. 메소드의 문서화 주석은 메소드와 클라이언트 사이의 계약을 간명하게 기술해야 한다.
* 메소드가 '무엇을 하는가?'를 설명해야 한다.
** 사전조건(precondition), 사후조건(postcondition), 부작용도 기술해야 한다.
** 부작용: 사후조건을 달성하기 위해 반드시 필요하지도 않으면서, 시스템의 상태를 관찰가능한 정도로 변화시키는 행위
//============================================
// 메소드의 문서화 주석
//
/**
- 문서화 주석은 HTML 태그와 메타문자로 만든다.
- Javadoc 유틸리티는 문서화 주석을 HTML로 변환한다.
- 가장 많이 쓰이는 태그는 <p>, <code>, <tt>, <pre> 이다.
- <, >, & 와 같은 HTML 메타문자는 escape문자열로 바꿔야한다.
- "this"가 인스턴스 메소드의 문서화 주석에 쓰이면, 항상 현재 호출되는 메소드를 가진 객체를 의미한다.
- 문서화 주석의 첫 문장은 주석을 붙인 대상에 대한 요약설명(summary description)이 된다.
- HTML 유효성 검사기로 Javadoc이 만들어낸 HTML을 검사해 볼 수 있다.
Tip
- 정리
- 문서화 주석은 API를 문서화 할 수 있는 가장 훌륭하고 효율적인 방법이다.
- 외부에 제공하는 모든 API에 대해 반드시 문서화 주석을 달아야 한다.
- 작성하는 표준규칙을 지며 일관성있는 문서를 만들어야 한다.
- HTML 메타문자는 반드시 escape 처리해야한다.
h2. 문서에 대하여
* 최초작성자 : [이현숙]
* 최초작성일 : 2008년 04월 11일
* 이 문서는 [HeadFirst Design Patterns|http://book.naver.com/bookdb/book_detail.php?bid=1882446]을 정리한 내용 입니다.
* 이 문서는 [오라클클럽|http://www.gurubee.net] [자바 웹개발자 스터디|제3차 자바 웹개발자 스터디] 모임에서 작성하였습니다.
* 이 문서를 다른 블로그나 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^\^