본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 15 - 클래스와 멤버의 접근 권한을 최소화하라 - 핵심 정리

by 개발인생 2022. 11. 8.
반응형

아이템 15 - 클래스와 멤버의 접근 권한을 최소화하라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

정보 은닉의 장점

정보 은닉의 장점은 다음과 같다.

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

정보 은닉을 하려다보면 자연스럽게 인터페이스 를 설계하게 된다.

어떤 인터페이스의 설계를 마치고 나면 그 인터페이스를 사용하는 쪽은 그 인터페이스에 맞게 개발을 하면 된다.

그 인터페이스를 구현하는 쪽 역시 해당 인터페이스에 정의되어 있는 대로 동작할 수 있는 클래스를 개발하면 된다.

이렇게 인터페이스를 사용하는 쪽과 제공하는 쪽이 동시에 개발 진행이 가능하다.

동시에 여러 모듈을 개발하는 것도 가능하다.

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

인터페이스를 통해 각 컴포넌트를 더 빨리 파악할 수 있기 때문이다.

캡슐화가 잘 되어있다는 가정하에 다른 컴포넌트로 변경하기도 쉽고, 문제가 생겼을 때 디버깅하기도 수월하다.

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

직접적인 도움은 아니지만 정보 은닉이 잘되어있을 때 성능의 병목지점 을 찾기 용이하다.

  • 소프트웨어의 재사용성을 높일 수 있다.

어떤 컴포넌트가 다른 프로젝트에도 사용될 수 있는 모듈이라면 해당 모듈을 그대로 사용할 수 있다.

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

큰 시스템을 만들기위해 모듈 단위로 나누어 개발하여 큰 문제를 해결할 수 있다.


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

다음의 원칙대로 클래스와 인터페이스의 접근 제한자를 사용한다면

조금 더 쓰기 편하고 견고하고, 유연하게 만들 수 있다.

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

톱 레벨 클래스나 인터페이스란 어떤 파일의 최상단 에 선언하는 클래스나 인터페이스이다.

톱 레벨에 붙일 수 있는 접근 제한자는 package - private 이나 public 두개 중 하나이다.

package - privatedefault 접근 지시자라고도 한다.

package - private 을 사용하면 내부 구현체가 되고 public 을 사용하면 API 가 된다.

숨길 것인지 공개할 것인지에 따라 결정한다.

패키지 내부에서 사용하거나 외부에서 사용하지 않는 클래스나 인터페이스라면 package - private 을 사용한다.

public 을 사용하는 순간부터 해당하는 API 는 하위 호완성을 유지하려면 영원히 관리해야 한다.

public 으로 공개한 클래스나 인터페이스를 변경하게되면 클라이언트 코드에서도 바뀐 코드에 맞게 변경해야한다.

하위 호환성에 너무 억매이다보면 급진적인 변화를 주기가 어렵다는 단점이 있다.

public interface MemberService {
   // 탑레벨에는 package - private 아니면 public 만 가능하다.   
}

공개되어도 괜찮고, 영원히 유지보수해도 괜챃다면 public 으로 선언한다.

class DefaultMemberService implements MemberService {

}

구현체같은 경우는 내부 클래스에서만 알면 되기 때문에 package - private 이 적절하다.

클라이언트 코드에서는 공개된 인터페이스만 알면되고, 구체적인 구현체는 굳이 알 필요가 없기 때문이다.

구현체 같은 경우는 의존성 주입이나 서비스 로더를 통해 제공받으면 되기 때문에 공개된 인터페이스만 알아도 된다.

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

interface MemberRepository {

}

패키지 내부에서만 사용하는 MemberRepository 라는 인터페이스가 있다.

class DefaultMemberService implements MemberService {

   MemberRepository memberRepository;

   public Member getMemeber() {
      return memberRepository.findById();
   }
}

package - private 으로 선언했지만 해당 패키지 내에서 한 클래스 에서만 사용이 된다면

class DefaultMemberService implements MemberService {

   private static class MemberRepository {

   }
}

private static 으로 해당 클래스에 중첩시킨다.

그렇다면 왜 private static 을 사용해야할까?

class DefaultMemberService implements MemberService {

   private String name;

   private static class PrivateStaticClass {

   }

   private class PrivateClass {

   }

   public static void main(String[] args) {
      Arrays.stream(PrivateClass.class.getDeclaredFields()).forEach(System.out::println);
   }
}

private class 는 자신을 감싸고 있는 외부 인스턴스를 참조 한다.

private static class 는 외부 인스턴스를 참조하지 않는다.

class DefaultMemberService implements MemberService {

   private String name;

   private class PrivateClass {
      void doPrint() {
         System.out.println(name);
      }
   }
}

private class 는 자신을 감싸고 있는 바깥 클래스의 멤버들에 대한 접근이 수월하다.

자기 자신을 감싸고 있는 바깥 클래스의 인스턴스를 가지고 있기 때문이다.

class DefaultMemberService implements MemberService {

   private String name;

   private static class PrivateStaticClass {

      void doPrint() {
         System.out.println(name); // 불가능하다.
      }
   }
}

private static class 같은 경우는 바깥 클래스의 멤버들에 대한 접근이 불가능하다.

원래 독립적인 클래스나 인터페이스를 inner class 로 만드는 것이기 때문에

한 클래스에서만 사용하는 package - private 클래스나 인터페이스를

해당 클래스에 중첩할 때는 private static class 이 더 어울리다.

만약 내부 클래스에서 외부 클래스의 필드들을 참조하고 싶다면 private class 로 만들면 된다.


멤버(필드, 메서드, 중첩 클래스/인터페이스)의 접근 제한자 원칙

클래스에서의 맴버란 필드, 메서드, 중첩 클래스 / 인터페이스 를 의미한다.

공개 API 를 만든 이후에는 다른 모든 멤버들의 접근 제한자를 private 으로 만들어야한다.

필요할 시에는 package - private 으로 접근 제한자를 풀어주어도 된다.

package - private 으로 풀어주는 멤버들이 많아진다면 컴포넌트의 구성이 잘못되지는 않았는지, 컴포넌트를 나누어야할지 고민해야한다.

private, package - private 은 내부 구현이다.

숨길 정보에 해당하는 것은 private, package - private 으로 감추어야한다.

밖으로 노출해야하는 정보라면 public 으로 해야한다.

필드에 대한 접급은 상수 일 경우에는 public static final 을 사용해 공개해야 한다.

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

공개할 API 에는 public, protected 를 사용하여 공개한다.

public class ItemService {

    private MemberService memberService;

    boolean onSale;

    protected int saleRate;

    public ItemService(MemberService memberService) {
        this.memberService = memberService;
    }
}

ItemService 를 작성하자.

class ItemServiceTest {

   @Test
   void itemService() {
      ItemService service = new ItemService();
   }
}

테스트 코드를 작성해야하는데 ItemService 에서 사용하는 MemberService 에 대한 참조를 할 수가 없다고 가정하자.

@ExtendWith(MockitoExtension.class)
class ItemServiceTest {

   @Mock // 가짜 객체 생성
   MemberService memberService;

   @Test
   void itemService() {
      ItemService service = new ItemService(memberService);
      assertNotNull(service);
      assertNotNull(service.getMemberService()); // 맴버 서비스에 대한 접근
   }

}

이러한 경우에는 Mocking 을 통해 해결할 수 있다.

이 테스트에서는 ItemService 클래스에 있는 private 맴버 인 MemberService 에 대한 접근이 필요하다.

이럴때는 두가지 방법이 있다.

public MemberService getMemberService() {
        return memberService;
    }

getter 를 통해 접근을 허용하는 방법이다.

테스트 하는 대상이 이미 getter 를 제공하는 경우라면 getter 를 활용하는 것이 좋다.

하지만 getter 가 없는 경우에 테스트를 위해 공개 API 를 만드는 경우보다는

접근 권한을 변경하는 것을 권장한다.

public class ItemService {

    MemberService memberService; // package - private 으로 접근 제한자 변경.

    boolean onSale;

    protected int saleRate;

    public ItemService(MemberService memberService) {
        this.memberService = memberService;
    }
}
@ExtendWith(MockitoExtension.class)
class ItemServiceTest {

   @Mock // 가짜 객체 생성
   MemberService memberService;

   @Test
   void itemService() {
      ItemService service = new ItemService(memberService);
      assertNotNull(service);
      assertNotNull(service.memberService()); // 맴버 서비스에 대한 접근
   }

}

package - private 으로 접근 제한자를 변경해 테스트에서 접근이 가능하도록 한다.

만약 접근할 객체를 private 으로 유지하고 싶다면

MemberService getMemberService() {
        return memberService;
    }

getterpackage - private 으로 제공하는 방법이 있다.

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;
    }

    MemberService getMemberService() {
        return memberService;
    }
}

생성자 메서드에서 파라미터를 확인하는 방법이 이상적인 방법이다.

이처럼 private 으로 만들었지만 package - private 으로 확장하는 것은 괜찮으나

테스트 때문에 굳이 불필요한 public 한 맴버들을 만드는 걸 권장하지 않는다.

public static final String[] NAMES = new String[10]; // 권장하지 않는다.

public static final배열 필드 에 사용하는 것은 권장하지 않는다.

베열 안의 값은 변경이 가능 하기 때문이다.

또한 이러한 필드를 반환하는 메서드를 제공하는 것 역시 권장하지 않는다.

반응형

댓글