클래스와 멤버에 대한 접근은 최소화하라

정보은폐는 시스템을 구성하는 모듈 사이의 의존성을 줄여, 각 모듈을 따로 개발하고, 테스트 하고, 최적화하고, 쓰고, 이해하고, 수정할 수 있게 해주기 때문에 매우 중요한 개념이다.


private
package-private
protected
public
[published] - SRP


public 클래스는 public 인스턴스 변수를 가지지 않아야 한다. (쓰레드에 대해 안전하지 않다.) 접근성은 최대한 줄여야 한다. 아주 조심스럽게 최소의 public API를 설계한 다음, 쓸데없는 클래스, 인터페이스, 멤버가 API의 일부가 되지 않도록 해야 한다. public static final 필드가 아니라면 클래스에 public 필드가 있어선 안된다. 또, public static final 필드라 해도 이 필드는 기본타입이거나 불변 객체만 참조해야 한다.

public static final String[] VALUES = {"1", "2", "3"} ;
이를테면 위와 같은 코드는 실제로 final 하지 않다. 배열은 레퍼런스 참조이므로 실제 값이 변동되는걸 막을 수는 없다.

private static final String[] PRIVATE_VALUE = {"1", "2", "3"} ;
public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUE)) ;
만약 정말로 final 하게 유지하려면 위와 같이 unmodifiableList 를 사용해야 한다.


상속보다 컴포지션을 써라

상속은 비록 강력한 기능이긴 하지만 캡슐화를 위반하기 때문에 문제를 발생시킬 수 있다. 상위클래스와 하위 클래스가 정말 서브타입 관계가 있을 때만 상속을 쓸 수 있다. 하지만 이런 경우라도 하위 클래스와 상위 클래스가 다른 패키지에 있거나 상위 클래스가 상속을 위해 설계되지 않았다면 상속받은 하위 클래스에 문제가 생기기 쉽다. 컴포지션과 포워딩을 쓰면 상속의 폐해를 막을 수 있다. 특히 적절한 인터페이스가 있어서 Wrapper 클래스를 구현할 수 있다면 금상첨화이다.

Case1)

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

public class InstrumentHashSet extends HashSet {
  private int addCount = 0;

  public InstrumentHashSet() {
  }

  public InstrumentHashSet(Collection c) {
    super(c);
  }

  public InstrumentHashSet(int initCap, float loadFactor) {
    super(initCap, loadFactor);
  }

  public boolean add(Object o) {
    addCount++;
    return super.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return super.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }
  
  public static void main(String[] args) {
    InstrumentHashSet s = new InstrumentHashSet() ;
    s.addAll(Arrays.asList(new String[]{"A""B""C"})) ;
    System.out.println(s.getAddCount()) ;
  }
}

위의 main함수를 실행시키면 기대와는 달리 3이 아니라 6이 나온다. 왜냐하면 Hashset의 addAll은 Collection의 수만큼 반복하여 add함수를 실행시키기 때문에 count가 2씩 올라가기 때문이다. 이렇게 인터페이스가 아닌 컨크리트 클래스의 상속을 사용하면 슈퍼클래스의 구현에 대해 알고 있지 않으면 이처럼 실수를 하기 쉽다.


Case2) Wrapper Class(Decorator Pattern)

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class InstrumentHashSet implements Set {

  private final Set s;
  private int addCount = 0;

  public InstrumentHashSet() {
    s = new HashSet();
  }

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

  public boolean add(Object o) {
    addCount++;
    return s.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return s.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }

  // foward method
  public void clear() {
    s.clear();
  }
  
  ......
  

  public static void main(String[] args) {
    InstrumentHashSet s = new InstrumentHashSet();
    s.addAll(Arrays.asList(new String[] { "A""B""C" }));
    System.out.println(s.getAddCount());
  }
}

좀더 좋은 구현은 위와 같다. Case2)는 인터페이스를 구현해서 자신이 구현할 메소드는 직접 구현하고 나머지 메소드는 컴포짓을 사용해서 위임을 사용한다.

정말 모든 B가 A인가?는 LSP 이야기에서 나오겠지만 지키기기 생각만큼 쉽지는 않다. Java API에도 이런 실수가 보이는데 이를테면 Properties는 Hashtable을 상속받고 있다. Properties의 경우 String만 key와 value로 받아들인다는 불변규칙이 있지만 상위 클래스인 hashtable에는 이런 조건이 없다.

이럴경우....
  Hashtable hs = new Properties();
  hs.put(new Integer(1), Calendar.getInstance()) ;

와 같은 코드 작성시 혼란이 올수 있다. 이때  hs는 Properties 임에도 불구하고 key와 value로 Object를 받아버린다.


컨크리트 클래스보다는 추상클래스를, 추상클래스보다는 인터페이스를 써라

이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 것은 어려운 일이 아니다. 하지만 이미 존재하는 클래스가 새로운 추상 클래스를 상속받도록 고치는 것은 이미 배포된 프레임워크에서는 거의 불가능한 일이다. Sun도 Properties가 Hashtable을 상속받는건 잘못이라는 걸 알았지만 이미 고칠수가 없다.

다양한 구현이 가능한 타입을 정의할때 인터페이스를 쓰는 것이 가장 좋다. 단, 쉽게 기능을 추가할 수 잇는 것이 유연성과 강력함보다 더 중요한 경우에는 추상 클래스를 쓰는 것이 좋다. 하지만 그 한계를 이해하고 수용할 수 있을때만 추상 클래스를 사용해야 한다. 외부에 제공하는 중요한 인터페이스에 대한 기본 뼈대 구현을 제공하는 것이 좋다. public 인터페이스는 신중하게 설계해야 하고, 다양하게 구현을 통한 철저한 시험을 거친후 발표해야 한다. 인터페이스와 추상 클래스의 장점만 결합해서, 외부에 제공하는 중요한 인터페이스들에 대해 기본 뼈대 구현(Skeletal implementation_을 해놓은 추상 클래스를 제공할 수 있다.(AbstractCollection, AbsgtractSet, AbstractList....)

여기서 본질적인 질문을 해보자.
추상클래스와 인터페이스는 무슨 차이가 있는가 ? 문법상으로 추상 메소드만 있다면 인터페이스와 비슷한 역할을 할 수 있다. 인터페이스를 쓰면 Mixin 타입을 정의할 수 있고 계층 구조가 없는 타입 프레임 워크를 만들 수 있지만 그런 차이를 넘어서 본질적인 차이는 무엇일까? 인터페이스를 써야할까 추상 클래스를 써야 할까 를 고민할때 선택하는 기준은 무엇인가? 극히 개인적인 의견이긴 하지만 클래스 이름이 명사는 추상클래스를 동사와 형용사는 인터페이스를 사용한다. 명사는 보통 속성을 가지고 있고 실체가 있는 경우가 보통이지만 동사와 형용사는 보통 실체가 없는 동작이나 관점이기 때문에 변할 소지가 다분하다.


인터페이스 타입으로 객체를 참조하고 리턴해라

// better
public List newSubscriber(){
    return new Vector() ;
}

// bad
public Vector newSubscriber(){
    return new Vector() ;
}

적절한 인터페이스가 없다면, 인터페이스 타입이 아닌 클래스 타입으로 객체를 참조해도 괜찮다. 예를 들어 String이나 BigInteger와 같은 값 클래스가 다양한 구현체를 가진다는 것은 상상하기 어렵다. 이 클래스들은 대부분 final이고, 해당하는 인터페이스를 가진 경우는 겨의 없다. 따라서 값 클래스는 인자, 변수, 리턴타입으로 쓸수 있다.

어떤 프레임워크의 기반타입이 인터페이스가 아니라 클래스라면 클래스 타입으로 객체를 선언할 수 밖에 없을 것이다. 하지만 이런 클래스 기반 프레임워크에 속하는 객체라도 구체적인 클래스 타입이 아닌 추상 클래스인 기반 클래스 타입으로 참조하는 것이 좋다. 


클래스의 상태를 요약하는 toString()을 작성해주면 편하다.

public String toString(){
   return "a:" + a + ", b:" + b + ", c:" + c;
}
대부분의 JVM은 최적화 작업을 해주기 때문에 위의 String 연산은 보이는만큼 많이 일어나지 않는다.

# Bad
public String toString(){
   String s = "a:" + a ;
             s += ", b:" + b ;
             s += ", c:" + c ;
   return s ;
}
그러나 위 코드는 JVM이 최적화 작업을 해주기 사실상 어렵다.



 

'Framework > 아키텍쳐 일반' 카테고리의 다른 글

아키텍쳐 패턴 - Pipes and Filter 패턴  (0) 2009.03.11
아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
Posted by bleujin

댓글을 달아 주세요