본문 바로가기
Language/Java

[Java] 객체 지향의 4대 특성 (캡슐화:정보 은닉)

by jaee_ 2021. 9. 26.
본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다. 

 

캡슐화 : 정보 은닉

자바에서 정보 은닉(information hiding)이라고 하면 접근 제어자인 private, [default], protected, public이 생각날 것이다. 

 

간단하게 접근 제어자에 대해 설명을 하고 넘어가자면 접근제어자는 private -> default -> protected -> public 순으로 보다 많은 접근을 허용한다.

  • public : 누구나 접근할 수 있을 때 사용한다.
  • protected : 같은 패키지 내에 있거나 상속을 받은 경우에만 접근할 수 있다.
  • default : 접근 제어자를 별도로 설정하지 않는다면  default 또는 package-private이라고도 불린다. 같은 패키지 내에 있을 때만 접근이 가능하다
  • private : private 이 붙은 변수, 메소드는 해당 클래스에서만 접근이 가능하다.
  해당 클래스 안에서 같은 패키지에서 상속 받은 클래스에서 import한 클래스에서
public O O O O
protected O O O X
default O O X X
private O X X X

 

접근 제어자가 객체 멤버(인스턴스 멤버)와 쓰일 때와 정적 멤버(클래스 멤버)와 함께 쓰일 때를 비교해서 살펴보자.

 

1. 객체 멤버의 접근 제어자

자신의 멤버가 아닌 다른 객체의 멤버에 접근하는 경우에는 다른 객체를 생성한 후 접근해야 한다. 

UML표기법에서 -표시는 private 접근 제어자를, ~ 표시는 [default] 접근 제어자를, #표시는 protected 접근 제어자를, +표시는 public 접근 제어자를 나타낸다. 속성이나 메소드 아래에 _(밑줄)을 사용한 경우는 정적 멤버를 나타낸다.

 

ClassA를 자바 코드로 변환하면 아래와 같이 표현할 수 있다.

public class ClassA {

    private int pri;
    int def;
    protected int pro;
    public int pub;

    void runSomething() {
    }

    static void runStaticThing() {
    }

 

ClassA의 runSomething() 메소드에 접근할 수 있는 ClassA의 속성에는 어떤 것들이 있을까? 혹은 위의 UML표기법에 나타난 클래스 중 접근 가능한 속성들이 어떤 것들이 있는지 직접 코드로 확인해보자.


ClassA 에서 접근 (같은 패키지 같은 클래스)

public class ClassA {

    private String pri = "private";
    String def = "default";
    protected String pro = "protected";
    public String pub = "public";

    void runSomething() {
        System.out.println("RunSomething Method");
    }

    static void runStaticThing() {
        System.out.println("RunStaticThing Method");
    }

    public static void main(String[] args) {
        ClassA a = new ClassA();
        System.out.println(a.pri);
        System.out.println(a.def);
        System.out.println(a.pro);
        System.out.println(a.pub);

        a.runSomething();
        // a.runStaticThing();
        runStaticThing(); // static메소드이기 때문에 객체 생성 없이 메소드 실행 가능

    }

}

// 결과
private
default
protected
public
RunSomething Method
RunStaticThing Method

 

ClassB애서 접근 (같은 패키지, 다른 클래스)

public class ClassB {

    public static void main(String[] args) {
        ClassA a = new ClassA();
        // System.out.println(a.pri); // 오류 : private은 본인 클래스 내에서만 접근 가능
        System.out.println(a.def);
        System.out.println(a.pro);
        System.out.println(a.pub);

        a.runSomething();
        a.runStaticThing();
        ClassA.runStaticThing(); // static메소드이기 때문에 객체생성을 하지 않아도 접근 가능
    }
}

//결과
default
protected
public
RunSomething Method
RunStaticThing Method
RunStaticThing Method

 

ClassAA에서 접근 (같은 패키지, 다른 클래스, ClassA를 상속받음)

public class ClassAA extends ClassA{
    public static void main(String[] args) {
        ClassA a = new ClassA();
        // System.out.println(a.pri); // 오류 : private은 본인 클래스 내에서만 접근 가능
        System.out.println(a.def);
        System.out.println(a.pro);
        System.out.println(a.pub);

        a.runSomething();
        a.runStaticThing();
        ClassA.runStaticThing(); // static메소드이기 때문에 객체생성을 하지 않아도 접근 가능
    }
}

// 결과
default
protected
public
RunSomething Method
RunStaticThing Method
RunStaticThing Method

 

ClassAB에서 접근 (다른 패키지, 다른 클래스, ClassA 상속받음)

public class ClassAB extends ClassA{

    public static void main(String[] args) {
        ClassA a = new ClassA();
//        System.out.println(a.pri); // 오류 : private은 본인 클래스 내에서만 접근 가능
//        System.out.println(a.def); // 오류 : 동일 패키지 내에서만 접근 가능
//        System.out.println(a.pro); // 오류 : 상속/같은 패키지 내의 클래스에서 접근 가능
        System.out.println(a.pub);

//        a.runSomething(); // 오류 : runSomething()메소드가 [default]이기 때문에 접근 불가
//        a.runStaticThing(); // 오류 : runStaticThing()메소드가 [default]이기 때문에 접근 불가
    }
}

// 결과
public

ClassAB는 ClassA의 상속을 받았지만 다른 패키지라 protected가 접근이 불가능

 

 

ClassC에서 접근 (다른 패키지, 다른 클래스)

public class ClassC {

    public static void main(String[] args) {
        ClassA a = new ClassA();
//        System.out.println(a.pri); // 오류 : private은 본인 클래스 내에서만 접근 가능
//        System.out.println(a.def); // 오류 : 동일 패키지 내에서만 접근 가능
//        System.out.println(a.pro); // 오류 : 상속/같은 패키지 내의 클래스에서 접근 가능
        System.out.println(a.pub);

//        a.runSomething(); // 오류 : runSomething()메소드가 [default]이기 때문에 접근 불가
//        a.runStaticThing(); // 오류 : runStaticThing()메소드가 [default]이기 때문에 접근 불가
    }
}

// 결과
public

 

 

 


2. 정적 멤버와 접근 제어자

이번엔 ClassA가 정적 속성인 priStr, defSt, proSt, pubSt를 가지고 있다고 가정하고 코드를 실행해보자.

 

ClassA 에서 접근

public class ClassA {

    // 정적 멤버와 접근 제어자
    private static String priSt = "private";
    static String defSt = "default";
    protected static String proSt = "protected";
    public static String pubSt = "public";


    void runSomething() {
        System.out.println("RunSomething Method");
    }

    static void runStaticThing() {
        System.out.println("RunStaticThing Method");
    }

    public static void main(String[] args) {
        System.out.println(priSt);
        System.out.println(defSt);
        System.out.println(proSt);
        System.out.println(pubSt);

        ClassA a = new ClassA();
        a.runSomething();
        runStaticThing();
    }

}

// 결과
private
default
protected
public
RunSomething Method
RunStaticThing Method

정적 멤버는 객체를 생성하지 않더라도 접근이 가능한 것을 볼 수 있다. 주석 (1)은 정적 메소드가 아니기 때문에 객체를 생성해서 메소드를 호출해야 한다.

 

ClassB에서 접근 (같은 패키지, 다른 클래스)

public class ClassB {

    public static void main(String[] args) {
        ClassA a = new ClassA();
        // System.out.println(ClassA.priSt); -- 오류
        System.out.println(ClassA.defSt);
        System.out.println(ClassA.proSt);
        System.out.println(ClassA.pubSt);

        a.runSomething();
        ClassA.runStaticThing();
    }
}

// 결과
default
protected
public
RunSomething Method
RunStaticThing Method

 

ClassAB 에서 접근 (같은 패키지, 다른 클래스, ClassA 상속받음)

public class ClassAA extends ClassA {

    public static void main(String[] args) {
        ClassA a = new ClassA();
        // System.out.println(priSt); // 오류 : private은 본인 클래스 내에서만 접근 가능
        System.out.println(defSt);
        System.out.println(proSt);
        System.out.println(pubSt);

        a.runSomething();
        runStaticThing();
    }
}

// 결과
default
protected
public
RunSomething Method
RunStaticThing Method

 

ClassAB에서 접근 (다른 패키지, 다른 클래스, ClassA 상속받음)

public class ClassAB extends ClassA{

    public static void main(String[] args) {
        ClassA a = new ClassA();
//        System.out.println(ClassA.priSt); // 오류 : private은 본인 클래스 내에서만 접근 가능
//        System.out.println(ClassA.defSt); // 오류 : 동일 패키지 내에서만 접근 가능
        System.out.println(proSt); // 접근 가능
        System.out.println(pubSt);

//        a.runSomething(); // 오류 : runSomething()메소드가 [default]이기 때문에 접근 불가
//        a.runStaticThing(); // 오류 : runStaticThing()메소드가 [default]이기 때문에 접근 불가
    }
}

// 결과
protected
public

위의 예제와는 다르게 protected 접근제어자가 접근이 가능한 것을 볼 수 있다.

 

ClassC에서 접근 (다른 패키지, 다른 클래스)

public class ClassC {

    public static void main(String[] args) {
//        System.out.println(ClassA.priSt); // 오류 : private은 본인 클래스 내에서만 접근 가능
//        System.out.println(ClassA.defSt); // 오류 : 동일 패키지 내에서만 접근 가능
//        System.out.println(ClassA.proSt); // 오류 : 상속받지 않아서 접근 불가능
        System.out.println(ClassA.pubSt);

//        ClassA.runSomething(); // 오류 : runSomething()메소드가 [default]이기 때문에 접근 불가
//        ClassA.runStaticThing(); // 오류 : runStaticThing()메소드가 [default]이기 때문에 접근 불가
    }
}


// 결과
public

 

위의 예제로 알 수 있는 점은 아래의 두 가지로 정리할 수 있다. 

 

  • 상속을 받지 않았다면 객체 멤버는 객체를 생성한 후 객체 참조 변수를 이용해 접근해야 한다. 
  • 정적 멤버는 클래서명.정적멤버 형식으로 접근하는 것을 권장한다. 

정리 문장 중 두번째에 대해서는 조금 더 생각해볼 필요가 있다. public 정적 속성인 경우엔 각 위치별 객체 멤버 메스드에서 접근할 수 있는 방법이 무려 세 가지나 된다. 아래의 표를 보고 확인해보자.

ClassA ClassA,pubSt pubSt this.putSt
O O O
같은 패키지 상속한 경우 O O O
상속하지 않은 경우 O X X
다른 패키지 상속한 경우 O O O
상속하지 않은 경우 O X X

 

일관된 형식을 위해 클래스명.정적멤버 형식으로 접근해야 한다. 또 다른 이유로는 메모리의 물리적 접근에 따른 이유가 있다. 아래의 그림을 보자. 

 

메모리에서 정적 멤버에 접근하는 방법 2가지

클래스명.정적멤버로 접근하면 바로 해당 클래스로 가는 접근 방식을 취한다. 반면, 객체참조변수명.정적멤버로 접근하면 스택메모리로 접근 후 힙메모리의 객체를 따라서 클래스로 접근하는 방식을 취하므로 클래스명.정적멤버로 접근하는 것을 권장한다. 

 


3. 참조 변수의 복사

기본 자료형 변수를 복사하는 경우에는 Call By Value(값에 의한 호출)에 의해 그 값이 복사되며 두 개의 변수는 서로에게 영향을 주지 않는다. 

// CallByValue
public class CallByValue {

    public static void main(String[] args) {
        int a = 10;
        int b = a;

        b = 20;

        System.out.println(a); // 10
        System.out.println(b); // 20

    }
}

// 결과 
10
20

 

 

Call By Value T메모리 영역

변수 a에 10을 대입한 후, 변수 b가 변수 a가 가진 값을 복사하고, 다시 변수 b에 20을 할당한 것을 볼 수 있다. 이 때 a가 가진 값이 단순히 b에 복사된 것이고 a와 b변수는 아무런 관계가 없는 것을 알 수 있다. 

 

그렇다면 기본 자료형이 아닌 객체 참조 변수를 복사하는 경우는 어떨까? 코드를 보고 확인해보자.

public class CallByReference {

    public static void main(String[] args) {
        Animals ref_a = new Animals();
        Animals ref_b = ref_a;

        ref_a.age = 10;
        ref_b.age = 20;

        System.out.println(ref_a.age);	// 20
        System.out.println(ref_b.age);	// 20
    }
}

class Animals{
    public int age;
}

// 결과
20
20

Call By Reference T메모리 영역

사실 Call By Value와 Call By Rreference는 본질적으로 차이가 없다. 차이라면 기본 자료형변수를 저장하고 있는 값을 그 값 자체로 해석하는 반면, 객체 참조 변수저장하고 있는 값을 주소로 해석한다는 차이가 있다.

 

즉, Call By Value에 의해 변수를 복사하든 Call By Reference에 의해 참조 변수를 복사하든 결국은 변수가 가진 값이 그대로 복사된다. 다만 그 값을 값 자체로 해석(기본자료형)하느냐 아니면 주소값으로 해석(참조자료형)하느냐의 차이가 있다. 

 

주소를 보여주지는 않지만 참조하고 있는 객체에 대한 고유값을 보여주는 다음 코드를 실행해보면 ref_a와 ref_b는 결국 같은 값을 가지고 있음을 알 수 있다. 

public class CallByReference2 {

    public static void main(String[] args) {
        Animals ref_a = new Animals();
        Animals ref_b = ref_a;

        System.out.println(ref_a);  // study.week08.Animals@1b6d3586 - 실행할 때마다 다르다.
        System.out.println(ref_b);  // 바로 위와 같은 값이 출력된다. 


    }
}

ref_a와 ref_b는 완전히 다른 변수다. 다만 같은 값을 가지고 있고 컴퓨터는 그 값을 주소로서 활용한다. 결국 두 변수가 같은 객체를 참조하고 있을 뿐이다. 참조하고 있는 객체가 같으니 참조하고 있는 객체의 변화에 함께 영향을 받을 뿐이다. 

 

결국 Call By Value와 Call By Reference를 다르다고 이해하기보다는 기본 자료형 변수는 저장하고 있는 값을 그 값 자체로 판단하고, 참조 변수는 저장하고 있는 값을 주소로 판단한다고 이해하는 것이 더 쉽다. 여기서는 main()메소드 내부만 살펴봤는데 변수를 메소드의 인자나 반환값으로 사용하는 경우도 동일하다.

 

기억해야할 부분을 정리해보자.

  • 기본 자료형 변수는 값을 그 자체로 판단한다.
  • 참조 자료형 변수는 값을 주소, 즉 포인터로 판단한다.
  • 기본 자료형 변수를 복사할 때 참조 자료형 변수를 복사할 때 일어나는 일은 같다. 즉, 가지고 있는 값을 그대로 복사해서 넘긴다. 

 


참고

 - 스프링 입문을 위한 자바 객체지향의 원리와 이해

댓글