본문 바로가기
Computer Sience/Java

[Effective Java] 아이템15 클래스와 멤버의 접근 권한을 최소화해라

by 제우제우 2024. 11. 5.

왜 아이템 13, 14는 정리 안 하는지?

아이템13은 최근에 프로토타입 패턴을 공부하면서 Object clone(), Cloneable 인터페이스, shallow copy, deep copy 개념을 정리했기 때문에 pass 한다. 

https://20240228.tistory.com/351

 

[Design Pattern] 프로토타입 (Prototype) 패턴

프로토타입 (Prototype) 패턴기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법프로토타입 패턴은 객체를 생성할 때, 기존 객체를 복사하여 새로운 객체를 만드는 생성 패턴 중 하나

20240228.tistory.com

 

아이템14는 Comparable 관련 내용인데 이미 코테를 준비 하면서 자세하게 알던 내용이었기 때문에 pass한다.

간단 요약하면 해당 클래스가 컬렉션이나 배열 같은 곳에 데이터를 담을 때 특정 순서에 맞게 반환이 필요하면 

Comparable 인터페이스를 구현해서 자연(기본) 오더(순서)를 제공하라는 건데 

기본적인 정렬을 자주 하는 상황이라면 Comparable 인터페이스를 구현하고 아니라면 정렬이 필요할 때 Comparator를  

익명 클래스나 람다 식(Comparator가 함수형 인터페이스여서 가능)으로 제공하면 된다. 

여러가지 정렬이 자주 사용된다면 클래스에 static으로 Comparator를 저장하고 사용하는 방식도 고려하자

잘 설계된 컴포넌트 

어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 정보 은닉 수준이다. 

클래스의 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다.

잘 설계된 컴포넌트는 모든 내부 구현을 완벽하게 숨겨, 구현과 API를 깔끔히 분리한다. 

오직 API를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작 방식에는 전혀 개의치 않는다. 

 

이터레이터 패턴으로 알아보는 잘 구현된 컴포넌트 

https://20240228.tistory.com/384

 

[Design Pattern] 이터레이터(Iterator) 패턴

이터레이터(Iterator) 패턴집합 객체 내부 구조를 노출시키지 않고 순회 하는 방법을 제공하는 패턴집합 객체를 순회하는 클라이언트 코드를 변경하지 않고 다양한 순회 방법을 제공할 수 있다. 

20240228.tistory.com

 

해당 글 내용에서는 이터레이터 패턴을 적용해서 구체 클래스가 바뀌어도 클라이언트의 코드 변경 없이 순회하도록 만들고 있다. 

이처럼 정보 은닉(클래스 내부 컬렉션의 구체 클래스)를 모르도록 추상화된 Iterator에만 의존하도록 하는게 왜 중요한지를 설명한다. 

 

정보 은닉의 장점 

시스템 개발 속도를 높인다. 

여러 컴포넌트를 병렬로 개발할 수 있기 때문이다. 

 

시스템의 관리 비용을 낮춘다.

각 컴포넌트를 더 빨리 파악하여 디버깅이 가능, 다른 컴포넌트로 교체하는 부담이 적기 때문이다. 

 

정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 

최적화할 컴포넌트를 정한 다음 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문이다.

이는 뭔가 분산 시스템의 장점과 유사하다. 

 

소프트웨어 재사용성을 높인다. 

외부에 거의 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트라면 그 컴포넌트와 함께 개발되지 않은 낯선 환경에서도 유용하게 쓰일 가능성이 크다. 

 

큰 시스템을 제작하는 난이도를 낮춘다. 

시스템 전체가 아직 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있기 때문이다.

단위 테스트를 용이하게 만든다. 

접근 허용 범위 

자바는 정보 은닉을 위한 다양한 장치를 제공하는데 그중 접근 제한자가 있다.

 

기본 원칙은 simple

모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다. 

 

톱 레벨(가장 바깥)클래스, 인터페이스에 부여할 수 있는 접근 수준은 package-private(default)와 public 두 가지다.

public으로 선언하면 공개 API가 된다. package-private는 해당 패키지 안에서만 사용 가능하다. 

외부 패키지에서 쓸 이유가 없다면 package-private으로 선언하자.

 

public 선언과 package-private 선언으로 오는 영향 

public으로 선언을 하면 하위 호환을 위해 영원히 관리 

package-private으로 선언하면 이들은 API가 아닌 내부 구현이 되어 언제든 수정할 수 있다. 

즉, 클라이언트에게 아무런 피해 없이 다음 릴리스에서 수정, 교체, 제거할 수 있다. 

 

결론 

package-private 접근 제한자는 클래스와 모듈 간의 의존성을 줄이고, 각 컴포넌트가 독립적으로 유지 및 변경될 수 있도록 함으로써 루즈 커플링을 촉진한다.

 

한 클래스에서만 사용하는 클래스

한 클래스에서만 사용하는 클래스를 private static 클래스로 중첩시켜보자 

한 클래스에서만 사용하는 클래스를 톱레벨 클래스나 인터페이스로 정의한다면 package-private으로 하더라도 

같은 패키지내에서는 다른 클래스가 접근이 가능하다.

 

하지만 private static inner 클래스로 사용한다면 해당 클래스만 접근이 가능하다. 

 

Collections - ReverseComparator 

public static <T> Comparator<T> reverseOrder() {
    return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}

private static class ReverseComparator
        implements Comparator<Comparable<Object>>, Serializable {
    
    static final ReverseComparator REVERSE_ORDER
        = new ReverseComparator();
        

    public Comparator<Comparable<Object>> reversed() {
            return Comparator.naturalOrder();
        }     
}
public static void main(String[] args) {
    List<Integer> integers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
    integers.sort(Collections.reverseOrder());
    System.out.println(integers); // 5, 4, 3, 2, 1
    integers.sort(Collections.reverseOrder().reversed());
    System.out.println(integers); // 1, 2, 3, 4, 5
}

 

public 톱 클래스(인터페이스)

톱 레벨 클래스나 인터페이스가 다른 패키지에서도 사용된다면 public으로 설정할 수 밖에 없다.

그러니 내부 구현을 숨기기 위해 멤버 변수와 메소드들은 최대한 private으로 설정하자.

하지만 private이 불가능한 경우가 있으니 여러 케이스에 대해서 설명해 보겠다.

 

public static final 필드 상수는 공개해도 괜찮다. 

값이 변경되지 않기 때문에 안전하게 외부에 공개할 수 있다. 

 

(public) final이 아닌 인스턴스 필드 

final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 읽게 된다. 

또한 필드가 수정될 때 (락 획득) 같은 다른 작업을 할 수 없게 되므로 일반적으로 스레드 not safe 하다. 

 

(public) final인 인스턴스 필드

final인 인스턴스 필드여도 해당 필드가 불변 객체가 아니라면 수정은 가능하다. 

이 또한 final이 아닌 인스턴스 필드의 단점과 동일하다.

 

클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메소드를 공개해서는 안 된다.

@Getter @Setter @AllArgsConstructor @ToString
public class SomeThing {
    private String item;
}
public class Thing {
    public static final SomeThing[] PRIVATE_VALUES = {new SomeThing("A"), new SomeThing("B")};
}

 

외부에서 변경 

SomeThing privateValue = Thing.PRIVATE_VALUES[0];
privateValue.setItem("C");

 

public 제어자로 final 배열을 반환하면 해당 배열을 직접적으로 변경은 못하지만
참조하고 있는 값은 외부에서 충분히 변경이 가능하다.

그러니 private 제어자로 바꾸고 해당 뷰를 반환한다.

public class Thing {
    private static final SomeThing[] PRIVATE_VALUES = {new SomeThing("A"), new SomeThing("B")};

    /**
     * 근데 이 방식은 매번 PRIVATE_VALUES(배열) -> List -> 불변 리스트
     * 이렇게 2번의 과정을 거쳐서 반환한다.
     */
    public static List<SomeThing> privateValuesAsList(){
        return Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
    }

    /**
     * 방어적 복사
     */
    public static SomeThing[] getPrivateValues(){
        return PRIVATE_VALUES.clone();
    }
}

원본 객체를 수정 불가능 

 

public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드를 가져서는 안 된다.

정리

프로그램의 요소의 접근성은 가능한 최소화 

꼭 필요한 부분만 public API로 설계

 

public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드를 가져서는 안 된다.

또한 public static final 필드가 참조하는 객체가 불변인지 확인하자