정보은닉
정보 은닉이란 무엇일까요?
내부의 “데이터” , “ 구현 " 을 외부로부터 얼마나 잘 숨겼는가.
이를 숨겨줌으로서, A 는 자신의 역할만을 잘 수행해주면 된다. A 를 사용하는 B 에서는 A 가 내부적으로 어떻게 수행되고 있는지는 알 필요 없다.
정보은닉은 왜 중요할까요
- 외부에서, 내부 구현에 의존하고 있다면, 내부 구현을 변경하는 것이 힘들어지겠죠?
변경으로 인한 영향의 범위가 넓어지니 까요.
- 정보은닉이 잘 되어있다면, 외부에 영향을 주지 않고 내부 구현을 변경하는게 가능해집니다
예시혈소판이 충분하지 않은 상태에서 헌혈을 하는 것은 건강에 좋지 않다. 따라서 우리는 현재 Person이 충분한 혈소판을 가지고 있는지 확인해봐야한다.아래와 같이 해 볼 수 있을 것이다if(person.getPlatelets() >= 150000 ) // true 면 헌혈 대상자다
이 코드는 과연 정보 은닉이 제대로 되어있는 걸까??헌혈 대상자인지 판단하기 위해 과연 내부 데이터를 노출시켜줘야만 할까??아래와 같이 코드를 작성할 수는 없을까?if(person.isEnoughToDonate(150000)) // true 이면 헌혈 대상자다
person 이 가진 데이터를 노출시키지 않고도 충분한 혈소판을 가지고 있는지에 대해서 알려주는 것이 가능하다.외부에서는 그저 person 에서 전달해준 “이 사람은 현재 헌혈 하기에 충분한 혈소판을 가졌다”라는 메시지 만을 받고, 외부의 동작을 하면 되는 것이다.
정보 은닉을 통해 얻을 수 있는 것?
정보은닉이 중요한 이유를 바탕으로, 정보은닉을 통해 얻을 수 있는 것은 무엇이 있는지 알아봅시다
- 컴포넌트들을 서로 독립시켜 각 부분을 개별적으로 개발, 테스트, 최적화 할 수 있다.
- 컴포넌트들 사이의 결합도가 낮기 때문
- 즉, 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화 가능
- 다른 컴포넌트로 교체하는 부담이 적다.
- API 를 통해서만 소통 하면 된다. ( 위에서 처럼, isEnoughToDonate 라는 메소드를 통해 소통 하면 된다 )
- 내부 동작 방식을 알 필요 없다.
- 내부 구현과 API 를 분리
- 재사용성
- 외부에 의존하지 않은 컴포넌트이기 때문
- 큰 시스템 개발시에 용이
- 개별 컴포넌트 단위로 동작을 검증 가능하기 때문
Java 의 접근메커니즘과 정보은닉
이러한 이유에서 “정보은닉” 은 중요합니다.
“접근제어 메커니즘”은 Java 에서 이러한 “정보은닉” 을 위해 제공하는 장치 중 하나 입니다.
- 클래스, 인터페이스, 멤버 에 대한 접근성을 명시
- 각 요소에 대한 접근성을 결정하는 요소 : 선언된 “위치”(탑클래스? public 클래스에 선언된 멤버?), 접근 제한자
기본 원칙 : 모든 클래스와 멤버의 접근성을 “가능한 한 좁혀”야 한다. ( 올바르게 동작하는 한 가장 낮은 접근 수준 )
어떤 접근성을 갖도록 해야할까?
선언된 위치에 따라 접근성이 달라질 수 있다 했다
- 톱 레벨 클래스,인터페이스 가 가질 수 있는 접근성: package - private, public
public interface Mappable{} // publicinterface Mappable{} // package-priavate
- public 클래스는 protected 멤버도 “공개 API” 에 올라간다 → 지속적인 하위호환이 지원해야 하게 된다 ( 따라서 protected 보다 낮은 접근성이 좋겠다 )
해당 패키지에서만 사용하는 경우 → package -private 으로 하는 것을 고려할 수 있다해당 클래스에서만 사용하는 경우 → private static nested class 로 중첩시키는 것을 고려할 수 있다.
- private 정적 중첩 클래스로 할 경우 어떤 차이가 있나??
- 톱 레벨 package-private 은 “같은 패키지의 모든 클래스”가 접근 가능
- private static nested class 은 “바깥 클래스(Outer class)” 하나만 접근 가능
- “멤버(필드, 메소드, 중첩 클래스, 중첩 인터페이스)” : private, package-private, protected, public
- private : 해당 멤버를 선언한 클래스에서만.
- package-private (default ) : 해당 멤버를 선언한 클래스가 속한 패키지
- protected : 해당 멤버를 선언한 클래스를 상속한 클래스에서도 가능 (다른 패키지더라도)
- public : 모든 곳!
자바 문법책을 볼 때 나오는 내용이죠. 오랜만에 정리해 볼까요? ( 아래로 갈 수록 접근성이 넓어집니다 )
- private → package-private → protected .. 이런식으로 일단 접근성을 좁게하여 선언한 후 권한을 점점 풀어주자. 하지만, 권한을 풀어주는 일을 자주 한다면 컴포넌트를 더 분해해야 하는 것은 아닌지 고민해 봐야 한다 > ⭐️ 권한을 자주 풀어야 하는 일이 발생할 때 컴포넌트를 더 분해해야하는 이유?? > > 이러한 상황은 객체지향 설계( 적절한 단위로 컴포넌트를 분리하는 것 )가 적절하지 않게 되어있는 것으로 생각할 수 있다 > - 현재 객체에 너무 많은 책임이 있는 것으로 생각 할 수도 있을 것 같다 > - 다른 객체로 옮겨줘야 하는 멤버일 수도 있다.
- Serializable 구현 클래스에서는 private, package-private 멤버도 공개 API가 될 수 있음에 주의하자.
- 멤버 접근성을 “좁히는 것” 을 방해하는 제약이 존재한다
- 하위클래스에서 재정의하는 메소드의 접근성 ≥ 상위클래스에서의 접근성 ( 더 좁힐 수 없다 )
- 따라서 인터페이스 구현체에서는 public 으로 메소드 선언 ( 인터페이스에는 private static method 까지는 작성가능하지만
public SuperClass { protected void callMethod(){} } public SubClass { @Override default void callMethod(){} // 불가능 } public OtherSubclass { @Override public void callMethod(){} // 가능 }
java > SuperClass instance = new OtherSubclass(); > > instance.callMethod(); > // 상위 클래스에 정의된 메소드라면 호출할 수 있던게, 하위 클래스의 재정의로 인해 호출 할 수 없게 되면 안되겠죠? >
>- 테스트 코드를 위해서 접근성을 넓히는 것은 지양하자
- 어차피 public 인 메소드가 정상 동작한다면, 내부적으로 private, protected 등의 메소드를 호출하는 것이므로, 해당 기능들에 대한 검증 또한 이루어진다.
- 아니면 package-private 메소드로 작성하자. https://junit.org/junit4/faq.html#atests_10
- 테스트 패키지 아래에 동일한 이름의 패키지에서 테스트를 할 경우, 접근이 가능하기 때문
| |--main |. |---my--MyClass |--test |. |---my--MyClassTest
- public class 의 인스턴스 필드, 정적 필드는 되도록 public 이 아니어야 한다.
- 해당 public 필드를 없애고 싶더라도 → 불가능
- 심지어 final 도 아니라면? → 이 필드와 관련된 모든것은 불변식을 잃는다 + 스레드 세이프 하지 못하다
public class MyClass { public final Person[] people = new Person[10]; ..... }
Service A ... myClass.people[3] = new Person("Lee"); ... Service B myClass.people[3] = new Person("Kim"); // 다른곳에서 이렇게 상태를 변경할 수 있다
- 단, public static final 필드가 필요한 경우가 있을 수 있다.
- ex) 공통적으로 사용할 상수를, 인터페이스에 Integer constant, String constant 로 정의 해 놓을 수 있다
- 이 경우 관례가 있는데 1. 대문자 알파벳으로 이루어져야함 2. 단어사이에 _ (underscore)를 사용
public interface SessionConstant{ public static final String SESSION_ATTRIBUTE = "MySession"; ... }
- 이 때 이 필드는 “기본타입 or 불변 객체” 를 참조해야한다. ( final 이어도 가변객체(ex_배열) 참조라면 예상하지 못한 결과를 일으킨다 )
결론
정보은닉(캡슐화) 을 위해 클래스, 인터페이스, 멤버 등의 접근성은 가능한한 “최소”로 하자.
접근성을 가능한 최소로 하기 위해서는, 먼저 가장 최소(private)로 선언한 후 → 점점 풀어주는 방식을 사용하자 . 이 과정에서 접근성을 자주 풀어주게 된다면 컴포넌트의 분리가 제대로 되지 않은 것이라 더 작은 단위로 분리하거나 역할에 맞게 멤버의 위치를 바꿔주는 리팩토링을 할 수 있다.
또한 접근성에 의해 의도치 않게 API 로 공개되는 것에 주의하자. 예를들어 public 클래스 내부의 public 필드, public, protected 멤버도 API 로 공개된다.