4장. 클래스와 인터페이스

4.1 클래스와 멤버에 대한 접근을 최소화 하라.(12항목)

가장 중요한 법칙은 클래스나 클래스의 멤버에 대한 접근을 가능하면 최소화 하라는 것이다.

  • private -- 해당 멤버를 선언한 클래스 내부에서만 접근할 수 있다.
  • package-private -- 해당 멤버를 선언한 클래스와 이 클래스와 같은 패키지에 있는 모든 클래스에서 접근할수 있다. 접근수정자를 붙이지 않으면 정해지는 기본 접근수준 이다.
  • protected -- 해당멤버를 선언한 클래스, 이 클래스의 하위 클래스, 이클래스와 같은 패키지에 있는 모든 클래스에서 접근할 수 있다.
  • public -- 모든 클래스에서 접근할 수 있다.


먼전 public API를 설계 하고 나머지 멤버들은 습관적으로 private로 만든다. 반드시 필요한 경우에만 private을 package-private으로 바꾼다.

  • 기억해야할 요점은 private 이나 package-private멤버는 패키지 상세구현의 일부이고 외부에 제공하는 api는 아니지만 이 멤버를(그러니까 private로 선언된 멤버를)
    가진 클래스거 Serializable을 implements 하게 된다면, 이 필드들은 외부에 제공하는 API가 되어 밖으로 유출 되므로 한번 결정한 직렬화 형태를 영원히 지원 해줘야 한다.
    (p.290참조)


public 클래스는 public 필드를 갖지 말아야 한다.

  • 어떤 필드가 final 이거나 혹은 final이 아니더라도 가변객체 를 참조 하면서 public 이면 이필드의 변경을 제한 할수 있는 방법이 없다.
    가변 객체를 참조하는 public 멤버 필드를 가진 클래스는 당연히 스레드에 안전하지 않다.
    예외 적인 경우가 있다면 public static final 필드로 상수를 외부에 들어내는 경우가 있다(ex. public static final int STUDENT_GRADE = 1; )
    물론 이런 필드드? 불변객체에 대한 참조타입 이거나 기본타입 이어야 한다.
    가변객체를 참조하는 필드는 아무리 final로 선언을 하더라고 final이 아닌것과 마찬가지 이다. 참조가 가르키는 객체자체는 변할수 없지만 이객체의 내용은 변할수 있기 때문이다.
    객체의 내용이 변한다면 무시무시한 일이 일어 날것이다.(이해가 잘 안되면 그림을 그려 보아라)

//엄청난 보안허점을 야기한다.
public static final Type[] VALUES = {...}
//public static final String [] VALUES = {}; 
//혹시나 해서 써놓는데..Type 이란 타입은 없다.
//외부에 제공되는 API는 배열의 복사본을 넘기는 방법이 좋다.


private static final Type[] PRIVATE_VALUES = {...};
public static final Type[] Values() {
 return (Type[]) PRIVATE_VALUES.clone();
}

4.2 불변 클래스를 써라.(13항목)

불변 클래스는 그 인스턴스의 내용을 절대로 바꿀수 없는 클래스다. 이 클래스의 각 인스턴스가 저장하는 정보는 인스턴스를 생성할때 단 한번 만든후에
인스턴스가 소멸할때 까지 바뀌지 않는다. (ex. String 과 기본타입에 대한 래퍼 클래스)

① 객체를 변경하는 메소드를 제공하지 않는다
② 재정의할 수 있는 메소드를 제공하지 않는다.
③ 모든 필드를 final로 만든다.
④ 모든 필드를 private으로 만든다.
⑤ 가변 객체를 잠조하는 필드는 배타적으로 접근해야 한다. (가변 객체를 참조하는 힐드가 있다면 클라이언트가 이 객체에 대한 참조를 얻지 못하게 한다.)

이후 책 99 페이지 까지는 지극히 당연한 내용이 나온다.
간단한 4칙 연산을 하는 예제 클래스를 제공해 주고 예제 클래스에서 2개의 정수가 들어올 경우 더하고 빼고 곱하고 나눈 결과를
내부 멤버로 처리 하는 것이 아니라 생성자를 통해 새로 생성 하고 있다.
이는 당연한 내용이 아닌가? 불변 클래스이므로 필요시점에 새로운 객체를 생성하는건 당연한 것이다.

두번재로 불변 객체는 원래부터 다중 스레드 환경에서 안전하기 때문에 동기화할 필요가 없다 라는 내용이 나오는데..
이또한 당연한 내용이다. 변화하지 않는 객체를 굳이 동기화할 필요가 있는가?? 여러 곳에서 참조 한다고 해도 불변 객체는 유일하므로
가져다가 써도 아무~ 문제 업다.

머 자주 스는 객체를 상수로 제공 하는 방법도 소개 된다.
그것은 다음과 같다.

public static final Complex ZERO = new Complex(0,0);
public static final Complex ONE = new Complex(1,0);
public static final Complex TWO = new Complex(0,1);

이거에서 한걸음 더 낳아가 상수가 아니라 스태틱 팩토리를 통하여 제공할수도 있다. 예제는
HeaddFirst 디자인 패턴의 팩토리 패턴을 참조 하자. 링크는 다음과 같다. 링크


//스태틱 펙토리 메소드를 생성자 대신 쓰는 불변 클래스
public class Complex {
 private final float re;
 private final float im;

 Complex(float re, float im) {
   this.re = re;
   this.im = im;
 }
 
 public static Complex valueOf(float re, float im) {
   return new Complex(re, im);
 }
 ..
}



불변객체의 단점으로는 각각의 값에 대해 서로 다른 객체가 필요하다는 사실이다.
부호비트만 다르고 나머지는 동일한 인스턴스가 있다고 가정 했을때
이 인스턴스가 100메가 라고 가정해 보자.
단지 1비트가 다른 인스턴스를 만들기 위해 100메가를 복사해야 한다면 이또한 엄청난 낭비일 것이다.
그럼에도 불구 하고 ... 모든 클래스는 특별한 이유가 ?다면 불변 클래스로 만들어야 한다.

다음은 페이지 102~103에 나온 간단한 방어 복사의 예시문이다.
BigInteger 나 BigDecimal 클래스가 초기에 잘못된 설계에 의해서 전부 재정의가 가능하게 설계 되었다.
하위 호환성을 포기 할수 없는 클래스이기에 클라이언트가 상속을 받을 경우 제대로된 인자가 넘어 왔는지
확인해야 하며 이에 대한 방어 복사를 구현해야 한다.


import java.math.BigInteger;


public class NewBigInteger extends BigInteger{

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public NewBigInteger(String s) {
		super(s);
	}
	
	
	public void foo(BigInteger b) {
		if (b.getClass() != BigInteger.class) {
			b = new BigInteger(b.toByteArray());
			System.out.println("BigInteger value : " + b);			
		}else{
			System.out.println("out of 안중");
		}
	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub

		BigInteger bint = new BigInteger("123456789");
		NewBigInteger nb= new NewBigInteger("987654321");
		nb.foo(bint);
		nb.foo(nb);		
		
	}

}

4.3 상속보다 컴포지션을 써라.(14항목)

우리는 이전에 헤드퍼스트디자인 패턴을 통해서 확인한 이야기 이다.
다른 설명 보다는 책에서 나온 코드를 중심으로 살펴 보자.

{code:java}
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

public class InstrumentedHashSet extends HashSet {

private int addCount = 0;

public InstrumentedHashSet() {
// TODO Auto-generated constructor stub
}

public InstrumentedHashSet(Collection arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}

public InstrumentedHashSet(int arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}

public InstrumentedHashSet(int arg0, float arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}

public boolean add(Object o) {
System.out.println("add() 메소드 호출");
addCount++;
return super.add(o);
}

public boolean addAll(Collection c) {
System.out.println("addAll() 메소드 호출");
addCount += c.size();
return super.addAll©;
}

public int getAddCount() {
return addCount;
}

}

 | {code:java}
import java.util.Arrays;
import junit.framework.TestCase;


public class InstrumentHashSetTest extends TestCase {


	private InstrumentedHashSet ih;

	@Override
	protected void setUp() throws Exception {
		this.ih = new InstrumentedHashSet();
	}
	
	//천번째 테스트 케이스로 정해진 HashSet에 문자열을 넣어서 Size를 받아온다.
	//3개의 문자열을 넣어서 3개의 사이즈를 기대하지만 잘못된 결과가 나오는걸 확인 한다. 
	
	public void testAddAll() {
		
		ih.addAll(Arrays.asList(new String[] {"Snap","Crackle","Pop"}));
		assertEquals(6, ih.getAddCount());
		assertEquals(3, ih.getAddCount());
		ih.clear();
	}
}

|

위 코드를 실행해 보면 처음 상속을 통하여 얻고자 한 결과가 제대로 나오지 않는걸 확인할수 있다.
그리고 이 이유에 대한 해결책으로
첫번째 allAll메소드를 재정의 하지 않고 사용한다고 나와있는데(테스트 해보자)
이는 전적으로 부모클래스에 의존한 구현 방식이다. 제공되던 클래스가 수정이 없을때는 상관이 없지만
변경 될경우 상속받은 클래스까지 모두 구현을 변경하라는 사실은 어불성설이요 너무 무책임한 태도다.
두번째 방식인 addAll 메소드에서 인자로 받은 Collection 을 하나씩 순회 하면서 add 메소드를 호출하게
재정의 하는 방법인데.. 이럴거면 머하로 상속 받았는가? 부모로 부터 상속받은 메소드 전부를 재정의 할것인가?
이는 엄청난 자원 낭비임이 틀림없다.
물론 지금 까지 설명한 내용과 책(p108)에 제시된 몇가지 예제들은 너무 비관적으로만 보는게 아닐까 하고 책을 읽으면서
생각하기도 했다.(애초에 상속자체를 만들지 말던지...그렇다 되도록이면 상속을 쓰지 말라는 이야기 이다.)

부모 클래스에 추가된 메소드로 인하여 자식클래스에서는 보안 및 안정성을 확신할수 없다.
(헤드퍼스트의 첫장인 오리를 기억하라 단지 부모에 날수있다 라는 행동을 부여했더니.. 고무오리도 날게 되었다.)
또 언제 무슨 문제가 발생 할지 알수 없기 때문에 우리는 상속을 지양 하고 컴포지션을 통하여 문제에 접근을 해야 한다.

  • 컴포지선 - 기존 클래스에 관한 인스턴스(객체) 멤버를 private로 갖는 형태

위 클래스를 컴포지션을 이용해서 처리해 보자

{code:java}
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public class InstrumentSet implements Set{

private final Set s;
private int addCount = 0;

public InstrumentSet(Set s) {
this.s = s;
}

public boolean add(Object arg0) {
addCount ++;
System.out.println("add Call()");
return s.add(arg0);
}

public boolean addAll(Collection arg0) {
addCount = arg0.size();

System.out.println("addAll Call()");
return s.add(arg0);
}

public int getAddCount() {
return addCount;
}

public void clear() {
// TODO Auto-generated method stub
s.clear();
}

public boolean contains(Object arg0) {
// TODO Auto-generated method stub
return s.contains(arg0);
}

public boolean containsAll(Collection arg0) {
// TODO Auto-generated method stub
return s.contains(arg0);
}

public boolean isEmpty() {
// TODO Auto-generated method stub
return s.isEmpty();
}

public Iterator iterator() {
// TODO Auto-generated method stub
return s.iterator();
}

public boolean remove(Object arg0) {
// TODO Auto-generated method stub
return s.remove(arg0);
}

public boolean removeAll(Collection arg0) {
// TODO Auto-generated method stub
return s.removeAll(arg0);
}

public boolean retainAll(Collection arg0) {
// TODO Auto-generated method stub
return s.retainAll(arg0);
}

public int size() {
// TODO Auto-generated method stub
return s.size();
}

public Object[] toArray() {
// TODO Auto-generated method stub
return s.toArray();
}

public Object[] toArray(Object[] arg0) {
// TODO Auto-generated method stub
return s.toArray(arg0);
}

}

 | {code:java} 
import java.util.Arrays;
import java.util.HashSet;

import junit.framework.TestCase;


public class InstrumentSetTest extends TestCase {
	

	
	public void testAddAll() {
		InstrumentSet s = new InstrumentSet(new HashSet());
		s.addAll(Arrays.asList(new String[] {"Snap","Crackle","Pop"}));
		assertEquals(3, s.getAddCount());
	}
}

|

4.4 상속받을수 있도록 설계하고 문서화 하라. 아니면 상속을 금지하라.(15항목)

페이지 113쪽과 116쪽에 걸쳐서 문서를 만들때 어떻게 만들어야 하며 왜 만들어야 하는지 설명 한다.

  • 상속을 위해 설계한 클래스는 직접적이든 간접적이든, 생성자에서 재정의 가능한 메소드를 절대로 호출해선 안된다.

위 사항을 위반할 경우 생기는 문제 코드를 살펴보자

{code:java}
/**
* @author Administrator
*
*/
public class Super {
public Super() {
// TODO Auto-generated constructor stub
System.out.println("Super Class Construct Call()");
m();
}

public void m() {

System.out.println("Super Class M Method Call()");
}
}

|{code:java}
/**
 * @author Administrator
 *
 */
public class Sub extends Super {

	private final Date date;
	/**
	 * 
	 */
	
	public Sub() {
		System.out.println("Sub Class Construct Call()");
		date =  new Date();				
	}

	public void m() {		
		System.out.println("Sub Class M Method Call()");
		System.out.println(date);
	}

	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
//		Sub s = new Sub();
//		s.m();
		Super s = new Sub();
		s.m();
	}

}

|

예상과 다른 결과를 보여주는 화면을 보게 될것이다.
책 에서는(p.118) final 필드의 상태가 2개가 될수 있다는 것에 주목 하라고 했는데... 이게 무슨 소리냐 하면은
메인 메소드 실행과 함게 만들어진 1개의 Thread 안에서 그려지는 과정을 머리속으로 그리면 답이 나온다.
이는 발표를 하면서 보여주도록 하겠다.

여지까지 나온 내용을 종합 하면 다음과 같은 결론이 가능하다.

  • 상속을 위해 클래스를 설계하려면 이 클래스에 실제로 제약을 줘야 한다.
  • 상속과 관련된 문제를 해결하는 가장 좋은 방법은 안전하게 상속받을수 있도록 설계 하지도 않고 문서화하지도 않은 클래스는 아예 상속받지 못하게 만드는 것이다.

4.5 추상클래스 보다는 인터페이스를 써라.(16항목)

  • 어떤 클래스가 인터페이스에 정의된 모든 메소드를 구현 하고 인터페이스의 구현계약을 지키면 클래스의 계층 구조와 상관없이 같은 타입이될수 있다.

여지껏 살펴본 바에 의하면 이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 것은 어려운 일이 아니다.
But 이미 존재 하는 클래스가 새로운 추상 쿨래스를 상속 받도록 고치는 것은 거의 불가능 한 일이다.
(그래서 되도록 이면 쓰지 말고.. 만약에 쓰게 되면 각오를 하고 쓰라는 거다.
물론 인터페이스 또한 만들어 져서 공표 되고 불특정 다수의 클래스가 이를 구현한 시점에서는 수정은 불가 하다.)

데코레이터 패턴에서 보아온 거처럼 인터페이스나 혹은 참조 변수를 멤버로 가진 클래스를 구현 하면 안전하고 강력하게 클래스에 새로운 기능을 더할수 있다.

다음 코드를 통하여 인터페이스를 이용한 클래스 설계를 보자. 이코드는 크게 별다른 의미를 지닌것은 아니지만 완전 하게 동작이 가능한 List형태를 가진
정수 배열을 생성하는 역활을 하는 클래스 이다. (팩토리 메소드 패턴을 사용 하였다.)


import java.util.AbstractList;
import java.util.Collections;
import java.util.List;

public class IntList {

	static List intArrayAsList(final int[] a) {
		if (a == null)
			throw new NullPointerException();
		return new AbstractList() {
			public Object get(int i) {
				return new Integer(a[i]);
			}

			public int size() {
				return a.length;
			}

			public Object set(int i, Object o) {
				int oldVal = a[i];
				a[i] = ((Integer) o).intValue();
				return new Integer(oldVal);

			}
		};
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int n = Integer.parseInt("20");
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = i;
		}
		List l = intArrayAsList(a);
		Collections.shuffle(l);
		System.out.println("ListElements : " + l);
		Collections.sort(l);
		System.out.println("ListElements : " + l);
	}
}

코드는 1차원 정수배열을 20개 생성 해서 List타입의로 만든후 Collections 클래스를 통해 섞고 또는 정렬 하는 결과를 보여준다.

4.6 인터페이스는 타입을 정의할 때만 써라.(17항목)

이 항목은 딱히 설명한 내용이 없다. 요약 하자면 상수 집합을 제공하기 위해 굳이 인터페이스를 쓰지 말라

4.7 중첩클래스는 정적 멤버 클래스로 정의 하라.(18항목)

중첩 클래스는 다른 클래스 내부에 정의한 클래스 이고
1. 정정멤버 클랫,
2. 비정적 멤버 클래스
3. 익명 클래스
4. 지역 클래스

로 나뉘어 지는데 이중 정정 멤버 클래스를 제외한 나머지 클래스는 내부 클래스 라고 불리운다.
멤버클래스를 정의 할때 감싼 클래스의 인스턴스에 접근할 필요가 없다면 정적 멤버클래스로 만들고
참조가 필요 하다면 비정적 멤버 클래스로 만든다.(당연하다 비정적 멤버 클래스는 감싼 클래스를 생성 하지 않으면 생성이 안되니까.)

이후 여러 가지 설명이 이어지는데 실제로 기억할 내용은 다음만 보면 된다.
한 메소드 에서만 쓰는게 아니거나 너무 길어서 한 메소드에 넣기 힘들다면 멤버 클래스로 만든다.
자신을 감싼 인스턴스의 참조가 필요한 경우에만 비 정적 멤버 클래스로 만들고 나머지 경우에는 모두 비정적 멤버 클래스로 만든다.

예제로 제공된 18.1 예제는 왜 이렇게 작성 되어야 했는지 대단히 의문스럽다. 저자 본인이 14 15 16 항목에 걸쳐 설명한 내용을 스스로 위반한 예제이다.
차라리 잘 정의된 HashSet 클래스를 가져다가 사용하면 된다. (HashSet --> 이건 대단히 죽여준다..Set 인터페이스를 구현 한거중에 황태자로 지칭해도 좋다.)


발표하면서 작성하자. 힘들다.




문서에 대하여