반응형

규칙 13에서는 소프트웨어 설계의 기본적인 원칙 가운데 하나인 캡슐화에 관해서 설명한다.

 

캡슐화는 다른 모듈 간 커플링을 최소화하기 위하여 구현 세부사항을 전부 API 뒤쪽에 숨긴다. 그리고 모듈들은 이 API를 통해서만 서로를 호출한다. 디커플링을 하게 되면 서로 간에 의존성이 낮아지기 때문에 한쪽이 변경되어도 다른 모듈에 영향을 끼칠 걱정이 없다. 또한 다른 개발에서도 유용하게 쓰일 수 있다는 장점이 있다. 자바는 이런 장점을 사용하기 위해 캡슐화를 지원하며 그중 대표적으로 접근 제어(Access Control) 기능을 제공한다.

 

원칙은 단순하다. 가능한 한 다른 곳에서 접근 불가능하도록 만들어라. 클라이언트에게 제공하는 API만 public 또는 protected로 하라.

 

 

1. 클래스와 인터페이스에 public을 붙이지 않을 수 있는지 확인해보자.

public으로 한번 릴리스한 클래스와 인터페이스는 호환성을 보장하기 위해 계속 제공해야 한다. (대부분 지키지 않지만)

 

예를 들어 자바에서는 Enum에 특화된 Set을 제공하는 EnumSet을 제공하는데, EnumSet을 public으로 제공하여 누구나 사용할 수 있다. 하지만 실제로 정적 팩토리 메서드가 제공하는 객체는 RegularEnumSet, JumboEnumSet이 있는데 이 둘은 직접적으로 사용할 수 없도록 package private 접근 제어자를 사용한다. 즉 jdk 개발자는 RegularEnumSet 클래스를 변경하던, 삭제하던 호환성에 대해 걱정할 필요 없다. 

// EnumSet은 public으로 추상 클래스 제공
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
	...
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
	...
}

// package private
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
	...
}

 

2. 클래스의 멤버 변수들은 최대한 package-private까지만 지원하자. protected도 광범위하다.

객체 필드변수는 절대로 public으로 선언하면 안 된다. 필드를 public으로 선언하면 필드에 저장될 값을 제한할 수 없게 되어 그 필드에 관계된 불변식을 강제할 수 없다. 그러므로 getter와같은 접근자 메서드를 사용하는 게 좋다.

 

단, 상수는 public static final로 선언될 수 있다. 아래는 BigDeimal의 상수 예시이다.

    // Cache of common small BigDecimal values.
    private static final BigDecimal ZERO_THROUGH_TEN[] = {
        new BigDecimal(BigInteger.ZERO,       0,  0, 1),
        new BigDecimal(BigInteger.ONE,        1,  0, 1),
        new BigDecimal(BigInteger.TWO,        2,  0, 1),
        new BigDecimal(BigInteger.valueOf(3), 3,  0, 1),
        new BigDecimal(BigInteger.valueOf(4), 4,  0, 1),
        new BigDecimal(BigInteger.valueOf(5), 5,  0, 1),
        new BigDecimal(BigInteger.valueOf(6), 6,  0, 1),
        new BigDecimal(BigInteger.valueOf(7), 7,  0, 1),
        new BigDecimal(BigInteger.valueOf(8), 8,  0, 1),
        new BigDecimal(BigInteger.valueOf(9), 9,  0, 1),
        new BigDecimal(BigInteger.TEN,        10, 0, 2),
    };

	public static final BigDecimal ZERO = zeroThroughTen[0];
	public static final BigDecimal ONE = zeroThroughTen[1];
	public static final BigDecimal TEN = zeroThroughTen[10];

 

하지만 상수라도 기본 자료형 값들을 갖거나, 변경 불가능 객체를 참조하게 해야 한다.

public static final Thing[] VALUES = { ... };

위 코드와 같이 길이가 0이 아닌 배열 상수 필드는 배열 레퍼런스는 변경하지 못하지만 배열 레퍼런스 안에 존재하는 값은 변경할 수 있다. 따라서 public static final 배열 필드를 두거나, 배열 필드를 반환하는 접근자를 정의하면 안 된다.

 

따라서 클라이언트에게는 배열 필드의 상수 값을 복사해서 넘겨주는 방식으로 이 문제를 해결한다.

방법 1.

	private static final String[] UNMODIFY_NUM_LIST = { "1", "2", "3", "4" };
	public static final List<String> NUM_LIST_2 = Collections.unmodifiableList(Arrays.asList(UNMODIFY_NUM_LIST));

방법 2.

	private static final String[] CLONE_NUM_LISE = { "1", "2", "3", "4" };
	public static final String[] NUM_LIST_3() {
		return CLONE_NUM_LISE.clone();
	}

 

3. 클래스의 메서드도 최대한 접근 권한을 줄이자.

클래스의 메서드도 최대한 접근 권한을 줄여야 한다. 하지만 접근 권한을 줄일 수 없는 경우가 있다. 상위 클래스 메서드를 오버라이딩할 때는 원래 메서드의 접근 권한보다 낮은 권한을 설정할 수 없다.

 

 

결론은 접근 권한은 가능한 낮춰라.

 

반응형

+ Recent posts