본문 바로가기

Java

[Java] 접근 제어자

자바는 public, private 같은 접근 제어자(access modifier)를 제공합니다.

접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있습니다.

그렇다면 자바에서 접근 제어자가 왜 필요할까요? 예제를 들어 접근 제어자가 필요한 이유에 대해 알아봅시다!

 

여러분은 스피커에 들어가는 소프트웨어를 개발하는 개발자라고 합시다.

스피커의 음량은 절대로 100을 넘으면 안된다는 요구사항이 있습니다. (100을 넘어가면 스피커의 부품들이 고장난다)

 

스피커 객체를 먼저 만들어볼까요?

스피커는 음량을 높이고, 내리고, 현재 음량을 확인할 수 있는 단순한 기능을 제공하고 있습니다.

요구사항에 맞춰 스피커의 소프트웨어를 만들어봅시다!

 

public class Speaker {

    int volume;

    Speaker(int volume) {
        this.volume = volume;
    }
    
    void volumeUp() {
        if (volume >= 100) {
            System.out.println("음량을 증가할 수 없습니다. 최대 음량 입니다.");
        } else {
            volume += 10;
            System.out.println("음량을 10 증가합니다.");
        }
    }
    void volumeDown() {
        volume -= 10;
        System.out.println("volumeDown 호출");
    }
    void showVolume() {
        System.out.println("현재 음량: " + volume);
    }
}

 

생성자를 통해 초기 음량 값을 지정할 수 있습니다.

volumeUp( ) 메서드를 보면 음량은 10씩 증가하고, 음량이 100이 넘게되면 더는 음량을 증가시키지 않습니다.

 

public class SpeakerMain {

    public static void main(String[] args) {
        Speaker speaker = new Speaker(90);
        speaker.showVolume();

        speaker.volumeUp();
        speaker.showVolume();

        speaker.volumeUp();
        speaker.showVolume();
    }
}

 

초기 음량 값을 90으로 지정하였습니다. 그리고 음량을 높이는 메서드를 여러번 호출해봤습니다.

코드를 작성한대로 음량은 100을 넘지 않았고, 프로젝트는 성공적으로 끝났습니다.

 

오랜 시간이 흘러서 업그레이드 된 다음 버전의 스피커를 출시하게 되었습니다.

이때는 새로운 개발자가 급하게 기존 코드를 이어받아서 개발을 하게 되었습니다.

참고로 새로운 개발자는 기존 요구사항을 잘 몰랐습니다.

코드를 실행해보니 이상하게 음량이 100이상 올라가지 않았고, 소리를 더 올리면 좋겠다고 생각한 개발자는

Speaker 클래스를 보니 volume 필드 값을 200으로 설정하고 코드를 실행했더니

스피커의 부품들에 과부화가 생기면서 망가져버렸습니다..

public class SpeakerMain {

    public static void main(String[] args) {
        Speaker speaker = new Speaker(90);
        speaker.showVolume();

        speaker.volumeUp();
        speaker.showVolume();

        speaker.volumeUp();
        speaker.showVolume();

        // 필드에 직접 접근해서 수정
        System.out.println("volume 필드 접근 수정");
        speaker.volume = 200;
        speaker.showVolume();
    }
}

Speaker 객체를 사용하는 사용자는 Speaker volume 필드와 메서드에 모두 접근할 수 있었습니다.

앞서 volumeUp( ) 메서드를 만들어서 음량이 100을 넘지 못하도록 기능을 개발했지만 소용이 없었습니다.

왜냐하면 Speaker 를 사용하는 입장에서는 volume 필드에 직접 원하는 값을 설정할 수 있기 때문입니다.

volume 필드의 외부 접근을 막을 수 있는 방법이 필요할 것 같습니다..

 

이런 문제를 근본적으로 해결하는 방법은 volume 필드를 Speaker 클래스 외부에서는 접근하지 못하게 막는 것 입니다.

public class Speaker {
    private int volume;
    . . .
}

 

private 접근 제어자는 모든 외부 호출을 막는다. 따라서 private이 붙은 경우 해당 클래스 내부에서만 호출할 수 있다.

 

그림을 보면 volume 필드를 private을 사용해서 Speaker 내부에 숨겼습니다.

외부에서 volume 필드에 직접 접근할 수 없게 막은 것 입니다.

volume 필드는 이제 Speaker 내부에서만 접근할 수 있습니다.

 

이제 SpeakerMain 코드를 다시 실행해보았더니 컴파일 오류가 발생했습니다.

 

만약 타임머신을 타서 Speaker 클래스를 개발하는 개발자가 처음부터 private을 사용해서 volume 필드의 외부 접근을

막아두었다면 어땠을까요? 새로운 개발자도 volume 필드에 직접 접근하지 않고, volumeUp( )과 같은 메서드를 통해서

접근했을 것 입니다. 결과적으로 Speaker가 망가지는 문제도 막을 수 있었겠죠?

 

접근 제어자 종류

자바는 4가지 종류의 접근 제어자를 제공합니다.

  • private: 모든 외부 호출을 막는다.
  • default (pakage--private): 같은 패키지안에서 호출은 허용한다.
  • protected: 같은 패키지 안에서 호출은 허용한다. 패키지가 달라도 상속관계의 호출은 허용한다
  • public: 모든 외부 호출을 허용한다.

순서대로 private 이 가장 많이 차단하고, public이 가장 많이 허용됩니다.

private => default => protected => public

 

 pakage-private 

접근 제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default 접근 제어자가 적용된다.
default 라는 용어는 해당 접근 제어자가 기본적으로 사용되기 때문에 붙여진 이름이지만,
실제로는 pakage-private 가 정확한 표현이다. 왜냐하면 해당 접근 제어자를 사용하는 멤버는 동일한 패키지 내의
다른 클래스에서만 접근이 가능하기 때문이다. 

 

 

접근 제어자 사용위치

 

접근 제어자는 필드와, 메서드, 생성자에 사용됩니다.

추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있습니다.

 

접근 제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것입니다.

  • private 은 나의 클래스 안으로 속성과 기능을 숨길 때 사용, 외부 클래스에서 해당 기능을 호출할 수 없다.
  • default 는 나의 패키지 안으로 속성과 기능을 숨길 때 사용, 외부 패키지에서 해당 기능을 호출할 수 없다.
  • protected 는 상속 관계로 속성과 기능을 숨길 때 사용, 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.
  • public 은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.

 

접근 제어자 사용 - 필드, 메서드

 

다양한 상황에 따른 접근 제어자를 확인해보겠습니다.

package access.a;

public class AccessData {

    public int publicField;
    int defaultField;
    private int privateField;

    public void publicMethod() {
        System.out.println("publicMethod 호출" + publicField);
    }
    void defaultMethod() {
        System.out.println("defaultMethod 호출" + defaultField);
    }
    private void privateMethod() {
        System.out.println("privateMethod 호출" + privateField);
    }
    
    public void innerAccess(){
        System.out.println("내부 호출");
        publicField = 100;
        defaultField = 200;
        privateField = 300;
        publicMethod();
        defaultMethod();
        privateMethod();
    }
}

 

  • 패키지 위치는 pakage access.a 이다.
  • 순서대로 public, default, private 을 필드와 메서드에 사용했다.
  • 마지막에 innerAccess( ) 메서드로 내부 호출을 보여준다. 내부 호출은 자기 자신에게 접근하는 것이다. 따라서 private를 포함한 모든 곳에 접근할 수 있다.

 

이제 외부에서 이 클래스를 접근해보겠습니다.

package access.a;

public class AccessInnerMain {

    public static void main(String[] args) {

        AccessData data = new AccessData();
        // public 호출 가능
        data.publicField = 1;
        data.publicMethod();
        // default 같은 패키지 호출 가능
        data.defaultField = 2;
        data.defaultMethod();
        // private 호출 불가능
        // data.privateField = 3;
        // data.privateMethod();
        
        data.innerAccess();
    }
}

 

실행 결과

publicMethod 호출 1
defaultMethod 호출 2
내부 호출
publicMethod 호출 100
defaultMethod 호출 200
privateMethod 호출 300

 

  • 패키지 위치는 package access.a 이다.
  • public 은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근 가능하다.
  • default 는 같은 패키지에서 접근할 수 있다. AccessMainAccessData 와 같은 패키지이다. 따라서 default 접근제어자에 접근할 수 있다.
  • private AccessData 내부에서만 접근할 수 있다. 따라서 호출 불가능하다.
  • AccessData.innerAccess() 메서드는 public 이다. 따라서 외부에서 호출할 수 있다. innerAccess( ) 메서드는 외부에서 호출되었찌만 innerAccess( ) 매서드는 AccessData 에 포함되어 있다. 이 메서드는 자신의 private 필드와 메서드에 모두 접근할 수  있다.

 

이번엔 다른 패키지에서 호출해보겠습니다.

package access.b;

import access.a.AccessData;

public class AccessOuterMain {

    public static void main(String[] args) {
        AccessData data = new AccessData();
        // public 호출 가능
        data.publicField = 1;
        data.publicMethod();
        // default 다른 패키지 호출 불가능
        //data.defaultField = 2;
        //data.defaultMethod();
        // private 호출 불가능
        // data.privateField = 3;
        // data.privateMethod();
        
        data.innerAccess();
    }
}
  • 패키지 위치는 package access.b 이다.
  • public 은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근할 수 있다.
  • default 는 같은 패키지에서 접근할 수 있다. access.b AccessOuterMain은 access.a.AccessData 와 다른 패키지이다. 따라서 default 접근 제어자에 접근할 수 없다.
  • private은 AccessData 내부에서만 접근할 수 있다. 따라서 호출 불가능하다.
  • AccessData.innerAccess( ) 메서드는 public 이다. 따라서 외부에서 호출할 수 있다. innerAccess( ) 메서드는 외부에서 호출되었지만 해당 메서드 안에서는 자신의 private 필드와 메서드에 접근할 수 있다.

실행 결과

publicMethod 호출 1
내부 호출
publicMethod 호출 100
defaultMethod 호출 200
privateMethod 호출 300

 

 

접근 제어자 사용 - 클래스 레벨

 

클래스 레벨의 접근 제어자 규칙

  • 클래스 레벨의 접근 제어자는 public, default 만 사용할 수 있다. (private, protected 는 사용할 수 없다)
  • public 클래스는 반드시 파일명과 이름이 같아야 한다.
  • 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있다.
  • 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.
package access.a;

public class PublicClass {

    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass();
        DefaultClass1 defaultClass1 = new DefaultClass1();
        DefaultClass2 defaultClass2 = new DefaultClass2();
    }
}
class DefaultClass1 {

}
class DefaultClass2 {

}

 

  • 패키지 위치는 package access.a 이다.
  • PublicClass 라는 이름의 클래스를 만들었다. 이 클래스는 public 접근 제어자다. 따라서 파일명과 이 클래스의 이름이 반드시 같아야 한다. 이 클래스는 public 이기 때문에 외부에서 접근할 수 있다.
  • DefaultClass1, DefaultClass2default 접근 제어자다. 이 클래스는 default 이기 때문에 같은 패키지 내부에서만 접근할 수 있다.
  • PublicClass main( )을 보면 각각의 클래스를 사용하는 예를 보여준다.
  • PublicClasspublic 접근 제어자다. 따라서 어디서든 사용할 수 있다. DefaultClass1, DefaultClass2 는 같은 패키지에 있으므로 사용할 수 있다.
package access.a;

public class PublicClassInnerMain { // 패키지는 같고 다른 클래스

    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass();
        DefaultClass1 class1 = new DefaultClass1();
        DefaultClass2 class2 = new DefaultClass2();
    }
}
  • 패키지 위치는 package access.a 이다. 
  • PublicClass 는 public 클래스이다. 따라서 외부에서 접근할 수 있다.
  • PublicClassInnerMainDefaultClass1, DefaultClass2 는 같은 패키지이다. 따라서 접근할 수 있다.
package access.b;

import access.a.PublicClass;

public class PublicClassOuterMain { // 패키지가 다른 클래스

    public static void main(String[] args) {

        PublicClass publicClass = new PublicClass();
        //다른 패키지 접근 불가능
        //DefaultClass1 class1 = new DefaultClass1();
        //DefaultClass2 class2 = new DefaultClass2();
    }
}
  • 패키지 위치는 package access.b 이다.
  • PublicClass public 클래스이다. 따라서 외부에서 접근할 수 있다.
  • PublicClassOuterMainDefaultClass1, DefaultClass2 는 다른 패키지이다. 따라서 접근할 수 없다.

 

캡슐화

캡술화(Encapsulation)은 객체 지향 프로그래밍의 중요한 개념중 하나입니다. 캡슐화는 데이터와 해당 데이터를

처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말한다. 캡슐화를 통해 데이터의 직접적인 변경을

방지하거나 제한할 수 있습니다. 캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것 입니다.

 

 

1. 데이터를 숨겨라

 

객체에는 속성(데이터) 기능(메서드)이 있습니다. 캡슐화에서 가장 필수로 숨겨야 하는 것은 속성(데이터) 입니다.

Speakervolume을 떠올려봅시다. 객체 내부의 데이터를 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 모든

로직을 무시하고 데이터를 변경할 수 있습니다. 결국 모든 안전망을 다 빠져나가게 되면서 캡슐화가 깨질 것입니다.

 

저희가 자동차를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지는 않습니다.

단지 자동차가 제공하는 엑셀 기능을 사용해서 엑셀을 밟으면 자동차가 나머지는 다 알아서 하는 것 입니다.

 

저희가 일상에서 생각할 수 있는 음악플레이어도 마찬가지 입니다. 음악 플레이어를 사용할 때 그 내부에 들어있는

전원부나, 볼륨상태의 데이터를 직접 수정할 일이 있을까요? 저희는 그냥 음악 플레이어의 켜고, 끄고, 볼륨을 조절하는

버튼을 누를 뿐입니다. 그 내부에 있는 전원부나, 볼륨의 상태 데이터를 직접 수정하지는 않습니다.

전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일입니다.

볼륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 볼륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는

일입니다. 쉽게 이야기해서 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것입니다.

복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하지 않는 것 처럼 말이죠!

 

객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 합니다. (데이터들은 대부분 private으로!)

 

 

2. 기능을 숨겨라

 

객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있습니다. 이런 기능도 모두 감추는 것이

좋습니다. 우리가 자동차를 운전하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 저희가 알 필요가

없습니다. 우리는 단지 엑셀과 핸들 정도의 기능만 알면 됩니다. 만약 사용자에게 이런 기능까지 모두 알려준다면,

사용자가 자동차에 대해 너무 많은 것을 알아야 합니다. 사용자 입장에서 꼭 필요한 기능만 외부에 노출합시다!

사용자 입장에서 꼭 필요한 기능만 외부에 노출하고, 나머지 기능은 모두 내부로 숨깁시다!

(예시: 엔진 조절 기능, 배기 기능)

 

정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화란 점!

 

 

 

아래 코드는 은행 계좌 기능을 다루고 있습니다.

package access;

public class BankAccount {

    private int balance;

    public BankAccount() { 
        balance = 0;
    }
    public void deposit(int amount) {
        if (isAmountValid(amount)){ // 참이면 실행
            balance += amount;
        } else {
            System.out.println("유효하지 않은 금액입니다.");
        }
    }
    public void withdraw (int amount){
        if (isAmountValid(amount) && balance - amount >= 0) {
            balance -= amount;
        } else {
            System.out.println("올바르지 않은 금액입니다. 잔액을 확인해주세요");
        }
    }
    public int getBalance() {
        return balance;
    }
    private boolean isAmountValid(int amount) { // 금액이 0보다 커야 합니다.
        // 금액이 0보다 커야함
        return amount >= 0;
    }
}
package access;

public class BankAccountMain {

    public static void main(String[] args) {

        BankAccount account = new BankAccount();
        account.deposit(2000);
        account.withdraw(2000);
        System.out.println("잔액: " + account.getBalance());
    }
}

 

private

  • balance: 데이터 필드는 외부에 직접 노출하지 않는다. BankAccount 가 제공하는 메서드를 통해서만 접근할 수 있다.
  • isAmountValid( ): 입력 금액을 검증하는 기능은 내부에서만 필요한 기능이다. 따라서 private 을 사용했다.

public

  • deposit( ): 입금
  • withdraw( ): 출금
  • getBalance( ): 잔고

BankAccount 를 사용하는 입장에서는 단 3가지 메서드만 알면 됩니다.

나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있습니다.

 

 

만약 isAmountValid( ) 를 외부로 노출하면 어떻게 될까요? BankAccount 를 사용하는 개발자 입장에서는 사용할 수 있는 메서드가 하나 더 늘었습니다. 여러분이 BankAccount 를 사용하는 개발자라면 어떤 생각을 할까요? 아마도 입금과 출금 전에 본인이 먼저 isAmountValid( ) 를 사용해서 검증을 해야 하나 생각할 것입니다.

 

만약 balance 필드를 외부에 노출 시킨다면 어떻게 될까요? BankAccount 를 사용하는 개발자 입장에서는 이 필드를 직접 사용해도 된다고 생각할 수 있습니다. 왜냐하면 외부에 공개하는 것은 그것을 외부에서 사용해도 된다는 뜻이기

때문입니다. 결국 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생시키는 버그쟁이가

될 수 있습니다.

 

접근 제어자캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount 를 사용하는 개발자 입장에서

해당 기능을 사용하는 부작용도 낮출 수 있는데 쓰지 않을 이유가 있을까요?