Item15 - 클래스와 멤버의 접근 권한을 최소화하라

2023. 11. 27. 23:21java/이펙티브 자바

 

어설프게 설계된 컴포넌트와 잘 설게된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다.

 

 잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다. 오직 API를 통해서만 다른 컴포넌트와 소통하며 서로으 내부 동작 방식에는 전혀 개의치 않는다. => 캡슐화

 

다음은 정보 은닉의 장점이다.

 

 

구현과 API를 분리하는 "은닉화" 의 장점

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

 => 여러 컴포넌트를 병렬로 개발

 

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

 => 컴포넌트를 빨리 파악할 수 있음

 

 - 성능 최적화에 도움을 준다.

 => 프로파일링을 통해 최적화할 컴포넌트를 찾고 다른 컴포넌트에 영향을 주지 않으면서 해당 컴포넌트를 개선할 수 있음

 

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

 => 독자적인 컴포넌트라면

 

 - 시스템 개발 난이도를 낮춘다.

 => 전체를 만들기 전에 개별 컴포넌트를 검증할 수 있기 때문

 

 

 

 

자바는 정보 은닉을 위한 다양한 장치를 제공한다. 그중 접근 제어 메커니즘은 클래스, 인터페이스, 멤버의 접근성을 명시한다.(public, protected, default, private) 이 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이다.

 

다음은 기본 원칙이다.

 

 

클래스와 인터페이스의 접근 제한자 사용 원칙

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

 

 - 톱레벨 클래스와 인터페이스에 package-private 또는 public을 쓸 수 있다.

   => public으로 선언하면 API가 되므로 하위 호환성을 유지하려면 영원히 관리해야 한다.

   => 패키지 외부에서 쓰지 않을 클래스나 인터페이스라면 package-private(default)으로 선언한다.(내부 구현체) 그러면            클라이언트에 아무런 피해 없이 다음 릴리스에서 수정, 교체, 제거할 수 있다.

 

public interface MemberService {
}

=> 인터페이스는 패키지 외부에서 쓰일 일이 있다고 생각해서 접근제한자를 public으로 두었다.

============================================================================================

class DefaultMemberService implements MemberService{
}

=> 구현체는 패키지 내부에서만 쓰이지 외부에서 구현 코드를 알 필요가 없으므로 package-private
   으로 두었다.

 

 - 한 클래스에서만 사용하는 package-private 클래스나 인터페이스는 해당 클래스에 private static으로 중첩 시키자.

//구현체는 내부 패키지내에서 있으면 충분하지 않을까?
class DefaultMemberService implements MemberService{

    private String name;

    //DefalutMemberService 안에서만 쓰이니까 private static으로 중첩시킨다.
    private static class PrivateStaticClass {
        public static void findById() {
        }

        //DefaultMemberService와 남남이다.
    }

    private class PrivateClass {
        //내부에 DefalutMemberService를 참조하는 필드가 자동으로 생긴다.

        void doPrint() {
            System.out.println(name);
        }
    }
    public Member getMember() {
        PrivateStaticClass.findById();
        return null;
    }
}

 

내부 클래스

  • 외부 클래스의 필드 및 메서드에 모두 직접 접근 가능하다.
  • 외부 클래스의 인스턴스를 생성해야만 인스턴스 생성이 가능하다. (외부참조: 메모리 누수 가능성)

정적 멤버 클래스

  • 외부 클래스의 static 멤버/메서드만 직접 접근이 가능하다.
  • 외부 클래스의 인스턴스를 생성할 필요가 없다. (독립적)
  • static이 붙어 있지만, 각각 생성한 두 객체는 다른 참조를 갖는다

 

 

 

클래스의 공개 API를 세심히 설계한 후, 그 외의 모든 멤버는 private으로 만들자. 그런 다음 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여 package-private 으로 풀어준다.

 권한을 풀어 주는 일을 자주 한다면 컴포넌트를 더 분해해야 하는 것은 아닌지 고민해본다.

 

클래스와 멤버의 접근 권한을 최소화하라

 - private과 package-private은 내부 구현

 - public 클래스의 protected와 public은 공개 API => 적을 수록 적다.

 - 코드를 테스트 하는 목적으로 private을 package-private으로 풀어주는 것은 허용할 수 있다. 하지만 테스트만을 위해서       멤버를 공개 API로 만들어서는 안된다.(테스트를 같은 패키지에 만든다면 그럴 필요도 없다.)

public class ItemService {
    private MemberService memberService;

    boolean onSale;

    protected int saleRate;

    public ItemService(MemberService memberService) {
        if(memberService == null) {
            throw new IllegalArgumentException("MemberService should not be null.");
        }

        this.memberService = memberService;
    }

    //Test를 위해서 getter 메서드를 package-private하게 만드는 건 괜찮다.
    MemberService getMemberService() {
        return memberService;
    }
}

 

 

 - public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.

   => 필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드와 관련된 모든 것은 불변          임을 보장할 수가 없다. 또한 필드가 수정될 때 다른 작업(락 획득)을 할 수 없게 되므로 스레드 안전하지 않다. 필드가           fianl이면서 불변이더라도 내부 구현을 바꾸고 싶을 때 public 필드를 없애는 방식으로는 리팩터링 할 수 없다.(외부에           서 참조 할 수도 있기 때문)

 

 - 하지만 해당 클래스가 표현하는 추상 개념을 완성하는 데 곡 필요한 구성요로써 상수라면 괜찮다.

 

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

   => 참조가 불변한 것이지 배열 내부 값은 변경 가능하다

   => 해결책 1. 배열을 private으로 만들고 public 불변 리스트 추가

                    2. 배열을 private으로 만들고 그 복사본을 반환하는 public 메서드 추가

 

1.첫 번째

private final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = 
	Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
    
2.두 번째

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