[윤성우의 열혈 Java 프로그래밍] Chapter 09 - 정보 은닉 그리고 캡슐화

Update:     Updated:

카테고리:

태그:

09-1. 정보 은닉(Information Hiding)

자바에서 말하는 ‘정보’는 클래스의 ‘인스턴스 변수’를 의미한다.

데이터에 대해 클래스나 인스턴스 ‘외부’에서 접근하는 것을 막겠다는 뜻이다.(내부에서만 허용하겠다.)
데이터는 가리고 ‘메소드’를 통해서만 접근 가능하도록 해야한다.

- 정보를 은닉해야 하는 이유

다음 예시를 보고 문제가 무엇인지 파악해보자.

class Circle{
  double rad = 0;
  final double PI = 3.14;

  public Circle(double r){
    setRad(r);  // 아래에 정의된 setRad 메소드 호출을 통한 초기화
  }

  public void setRad(double r){
    if(r < 0){  // 반지름은 0보다 작을 수 없으므로
      rad = 0;
      return; // 이 위치에서 메소드 빠져 나감
    }
    rad = r;
  }

  public double getArea(){
    return (rad * rad) * PI;  // 원의 넓이 반환
  }
}
class UnsafeCircle{
  public static void main(String args[]){
    Circle c = new Circle(1.5); // 위에서 정의한 Circle 클래스의 인스턴스를 생성
    System.out.println(c.getArea());

    c.setRad(2.5);
    System.out.println(c.getArea());

    c.setRad(2.5);
    System.out.println(c.getArea());

    c.rad = -4.5; // 옳지 않은 접근 방법, 그리고 문제가 되는 부분(멤버에 직접 접근하였다.)
    System.out.println(c.getArea());
  }
}

setRad 메소드를 보면 Circle 클래스를 정의한 ‘개발자의 의도’를 읽을 수 있다.
-> 반지름의 길이 rad에 0보다 작은 값이 저장되는 일이 발생하지 않도록 하겠다.

이렇듯 인스턴스 변수에 저장되는 값의 종류와 범위는 해당 클래스를 정의한 사람이 가장 정확히 안다.

만약 멤버 변수에 직접적인 접근을 허용하면, 컴파일 과정에서 드러나지 않는 중대한 실수가 발생할 수 있다.
따라서, ‘정보 은닉’을 적용한 클래스의 설계가 필요하다.

- 정보의 은닉을 위한 private 선언

정보 은닉을 적용한 코드를 보자.

class Circle{
    private double rad = 0; // member 변수 rad에 대해 private선언을 넣어줌!
    final double PI = 3.14;

    public Circle(double r){
        setRad(r);
    }
    public void setRad(double rad) {
        if(rad < 0){    // 0보다 작은 반지름이 전달되었을 시
            this.rad = 0;   // 0으로 초기화
        }
        this.rad = rad;
    }

    public double getRad() {
        return rad;
    }

    public double getArea() {
        return (rad * rad) * PI;
    }
}

public class InfoHideCircle {
    public static void main(String[] args) {
        Circle circle = new Circle(1.5);
        System.out.println("반지름: " + circle.getRad());
        System.out.println("반지름: " + circle.getArea() + '\n');

        // circle.rad = 3.4;    // 컴파일 오류 발생!
        circle.setRad(3.4);
        System.out.println("반지름: " + circle.getRad());
        System.out.println("반지름: " + circle.getArea());
    }
}

다음 처럼 private 선언을 통해 클래스 내에 정의된 메소드 내에서의 접근만 허용해주었다.

setRad와 getRad처럼 ‘값의 설정’과 ‘값의 참조’를 위한 메소드를 SetterGetter라고 부른다.

  • Getter
    • 인스턴스 변수의 값을 참조하는 용도로 정의된 메소드
    • 변수의 이름이 name이라면, 메소드의 이름은 getName으로 짓는 것이 관례이다.
  • Setter
    • 인스턴스 변수의 값을 설정하는 용도로 정의된 메소드
    • 변수의 이름이 name이라면, 메소드의 이름은 setName으로 짓는 것이 관례이다.

09-2. 접근 수준 지시자(Access-level Modifiers)

image

- 네 가지 종류의 '접근 수준 지시자'

접근 수준 지시자(Access-level Modifiers)는 이름 그대로 접근의 허용 수준을 결정할 때 선언하는 키워드이다.

public, protected, private, default

이러한 키워드 선언은 할 수 있는 대상이 정해져있다.

  • 클래스 대상 정의 대상: public, default
  • 인스턴스 변수와 메소드 대상: public, protected, private, default

- 클래스 정의 대상의 public과 default 선언이 갖는 의미

public class AAA{
  
}

class ZZZ{

}
  • public
    • 어디서든 인스턴스 생성이 가능하다.
    • 클래스 이름과 소스파일 이름이 같아야 한다. public 자체가 외부에서 공개를 할 필요가 있어서 선언한 것이므로 아예 소스파일명과 같게 만든다.
    • 하나의 소스파일엔 하나의 public class만 존재해야한다.
  • default
    • 동일 패키지로 묶인 클래스 내에서만 인스턴스 생성을 허용한다.


예시를 보며 이해해보자.
image

package zoo;

class Duck{ // default로 선언되었으므로 동일 패키지 내에서만 인스턴스 생성 가능
    // 빈 클래스
}

public class Cat {  // public으로 선언되었으므로 어디서든 인스턴스 생성 가능
    public void makeCat() {
        Duck quack = new Duck();    // Duck과 같은 패키지로 묶여있기 때문에 인스턴스 생성 가능
    }
}
package animal;

public class Dog {
    public void makeCat(){
        zoo.Cat yaong = new zoo.Cat();  // Cat은 public으로 선언되었으므로 어디서든 인스턴스 생성 가능
    }

    public void makeDuck() {
        zoo.Duck quack = new zoo.Duck();    // Duck은 default로 선언되었으므로 이 위치에서 인스턴스 생성 불가
    }
}

- 인스턴스 멤버 대상의 public, protected, private, default 선언

class AAA{
  public int num1;  // 인스턴스 변수의 public 선언
  protected int num2; // 인스턴스 변수의 protected 선언
  private int num3; // 인스턴스 변수의 private 선언
  int num4; // 인스턴스 변수의 default 선언

  public void md1(){ }  // 인스턴스 메소드의 public 선언
  protected void md1(){ } // 인스턴스 메소드의 protected 선언
  private void md1(){ } // 인스턴스 메소드의 private 선언
  void md1(){ } // 인스턴스 메소드의 deault 선언
}

- 인스턴스 멤버의 public과 default 선언이 갖는 의미

  • public: 어디서든 접근이 가능하다.
  • default: 동일 패키지로 묶인 클래스 내에서만 접근이 가능하다.
package zoo;

public class Cat {
    public void makeSound() {   // public 메소드, 어디서든 호출 가능
        System.out.println("yaong(>_<)");
    }

    void makeHappy() {  // default 메소드, 동일 패키지로 묶은 클래스 내에서 호출 가능
        System.out.println("smile(^_^)");
    }
}
package animal;

public class Dog {
    public static void main(String[] args) {
        zoo.Cat cat = new zoo.Cat();
        welcome(cat);
    }

    public static void welcome(zoo.Cat cat){
        cat.makeSound();    // 호출 가능! 컴파일 성공!
        // cat.makeHappy(); // 호출 불가! 컴파일 오류!
    }
}

- 인스턴스 멤버의 private 선언이 갖는 의미

image

동일 클래스에 정의된 메소드 내에서만 호출이 가능하다.

- 인스턴스 멤버의 protected 선언이 갖는 의미

서로 다른 패키지에 위치하는 두 클래스를 보자.

// AAA.java
package alpha;

public class AAA{
  protected int num;  // 상속 관계에 있는 클래스에서 접근이 가능하다.
}
// ZZZ.java
public class ZZZ extends alpha.AAA{
  public void init(int n){
    num = n;  // 상속된 변수 num의 접근!
  }
}

protected로 선언된 멤버는 상속 관계에 있는 다른 클래스에서 접근 가능하다.

- 인스턴스 멤버를 대상으로 하는 접근 수준 지시자에 대한 정리

image

09-3. 캡슐화(Encapsulation)

캡슐화는 ‘정보 은닉’과 더불어 객체지향 기반의 클래스 설계에 있어 가장 기본이면서 중요한 원칙 중 하나이다.
클래스 안에 ‘무엇을 넣을까’에 대한 이론을 제시하는 내용이다.

각자의 class가 자기 역할을 잘 하도록 Design하는 것

- 캡슐화의 개념

하나의 목적을 이루기 위해 관련 있는 모든 것을 하나의 클래스에 담아 두는 것

무조건 많이 담아서도, 부족해도 캡슐화가 아니다.

- 캡슐화가 이뤄지지 않은 예제

class SinivelCap{
  void take(){
    System.out.println("콧물이 싹~ 납니다.");
  }
}
class SneezeCap{
  void take(){
    System.out.println("재치기가 멎습니다.");
  }
}
class SnuffleCap{
  void take(){
    System.out.println("코가 뻥 뚫립니다.");
  }
}

class ColdPatient{
  void takeSinivelCap(SinivelCap cap){
    cap.take();    
  }
  void takeSneezeCap(SneezeCap cap){
    cap.take();    
  }
  void takeSnuffleCap(SnuffleCap cap){
    cap.take();    
  }
}
class BadEncapsulation{
  public static void main(String[] args){
    ColdPatient suf = new ColdPatient();

    suf.takeSinivelCap(new SnivelCap());  // 콧물 캡슐 구매 후 복용

    suf.takeSneezeCap(new SneezeCap()); // 재채기 캡슐 구매 후 복용

    suf.takeSnuffleCap(new SnuffleCap()); // 코막힘 캡슐 구매 후 복용
  }
}

코감기 증상 완화를 위해 SinivelCap, SneezeCap, SnuffleCap 인스턴스 생성해야 한다.

코감기 약 복용이라는 한 가지 목적의 달성을 위해 프로그래머가 알아야 할 것도 많고 코드상에서 약 복용 과정 또한 복잡하다.

- 캡슐화가 잘 이뤄진 예제

class SinusCap{
  void sniTake(){
    System.out.println("콧물이 싹~ 납니다.");
  }
    void sneTake(){
    System.out.println("재채기가 멎습니다.");
  }
  void sniTake(){
    System.out.println("코가 뻥 뚫립니다.");
  }

  void take(){
    sniTake();
    sneTake();
    snuTake();
  }
}

class ColdPatient{
  void takeSinus(SinusCap cap){
    cap.take();
  }
}

class OneClassEncapsulation{
  public static void main(String[] args){
    ColdPatient suf = new ColdPatient();
    suf.takeSinus(new SinusCap());
  }
}

인스턴스의 생성이 한 번으로 줄어들었고, 복용에 관한 정보(복용 순서, 약의 효능 등등)가 클래스에 모두 들어가있다.
캡슐화는 이처럼 해당 클래스와 관련있는 내용을 하나의 클래스에 모두 담되 부족하게 담아서도 넘치게 담아서도 안된다!

- 포함 관계로 캡슐화 완성하기

한 클래스가 다른 클래스의 인스턴스를 멤버로 가질 수 있는데, 이런 관계를 가리켜 ‘포함 관계’라 한다.

class SinivelCap{   // 콧물 처치용 캡슐
    void take(){
        System.out.println("콧물이 싹~ 납니다.");
    }
}

class SneezeCap{    // 재채기 처치용 캡슐
    void take() {
        System.out.println("재채기가 멎습니다.");
    }
}

class SnuffleCap{   // 코막힘 처치용 캡슐
    void take() {
        System.out.println("코가 뻥 뚫립니다.");
    }
}

class TotalCap{ // 포함 관계가 담긴 캡슐 class
    SinivelCap siCap = new SinivelCap();
    SneezeCap szCap = new SneezeCap();
    SnuffleCap sfCap = new SnuffleCap();

    void take(){
        siCap.take(); szCap.take(); sfCap.take();
    }
}

class Patient{
    void takeSinus(TotalCap cap){
        cap.take();
    }
}

public class CompEncapsulation {
    public static void main(String[] args) {
        Patient patient = new Patient();
        patient.takeSinus(new TotalCap());
    }
}

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기