[Java] 접근 제어자

szlee·2023년 12월 8일
0

Java

목록 보기
15/23

< 김영한의 실전 자바 - 기본편 > 강의를 보고 이해한 내용을 바탕으로 합니다.





접근 제어자

자바는 public, private 와 같은 접근 제어자를 제공한다.
접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다.

만약 스피커를 설계하는데 최대 음량이 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);
    }
}

그러면 의도대로 음량이 100이 넘지 않고 프로젝트는 성공적으로 끝날 것이다.


접근 제어자가 필요한 이유?

그리고 오랜 시간이 흘러 업그레이드 된 다음 버전의 스피커를 출시하게 되었다.
이때는 새로운 개발자가 급하게 기존 코드를 이어받아 개발을 하게 되었다.
그리고 이 새로운 개발자는 기존 요구사항을 잘 몰랐다.
코드를 실행해보니 음량이 100이상 올라가지 않아 소리를 더 올리면 좋겠다고 생각해 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 객체를 사용하는 사용자는 Speakervolume필드와 메서드에 모두 접근할 수 있다.
앞서 volumeUp()과 같은 메서드를 만들어서 음량이 100을 넘지 못하도록 기능을 개발했지만 소용이 없다.
왜냐하면 Speaker를 사용하는 입장에서는 volume필드에 직접 접근해서 원하는 값을 설정할 수 있기 때문이다.
이러한 문제를 근본적으로 해결하기 위해서는 volume필드의 외부 접근을 막을 수 있는 방법이 필요하다.
--> volume필드를 Speaker클래스 외부에서는 접근하지 못하게 막는다!

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

이렇게 volume 필드에 private을 붙이면 모든 외부 호출을 막을 수 있다.
private이 붙은 경우 해당 클래스 내부에서만 호출할 수 있다.

speaker.volume = 200; //java: volume has private access in access.Speaker

만역 Speaker 클래스를 개발하는 개발자가 처음부터 private을 사용해서 volume필드의 외부 접근을 막아두었다면 새로운 개발자도 volume필드에 직접 접근하지 않고 volumeUp()과 같은 메서드를 통해서 접근했을 것이다.
결과적으로 Speaker가 폭발하는 문제는 발생하지 않았을 것이다.



접근 제어자 종류

자바는 총 네가지 종류의 접근 제어자를 제공한다.

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

순서대로 private이 가장 많이 차단하고 public이 가장 많이 허용한다.
private -> default -> protected -> public


package-private

접근 제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default접근 제어자가 적용된다.
default라는 용어는 해당 접근 제어자가 기본값으로 사용되기 때문에 붙여진 이름이지만 실제로는 package-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();
    }

}

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();

    }
}
  • public은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근 가능하다.
  • default는 같은 패키지에서 접근 가능하다.
    • AccessInnerMainAccessData와 같은 패키지이기 때문에 default접근 제어자에 접근할 수 있다.
  • privateAccessData내부에서만 접근할 수 있다. --호출 불가
  • 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; //java: defaultField is not public in access.a.AccessData; cannot be accessed from outside package
        data.defaultMethod(); //java: defaultMethod() is not public in access.a.AccessData; cannot be accessed from outside package

        //private 호출 불가
//        data.privateField = 3;
//        data.privateMethod();

        data.innerAccess();
    }
}
  • access.b.AccessOuterMainaccess.a.AccessData와 다른 패키지이다. 따라서 default 접근 제어자에 접근할 수 없다.

클래스

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

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

import access.a.DefaultClass1;
import access.a.PublicClass;

public class PublicClassOuterMain {
    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass();

        //다른 패키지 접근 불가
        DefaultClass1 class1 = new DefaulClass1(); //java: access.a.DefaultClass1 is not public in access.a; cannot be accessed from outside package
    }
}




캡슐화

캡슐화는 객체 지향 프로그래밍의 중요한 개념 중 하나이다.
⭐️캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것⭐️을 말한다.
즉, 속성과 기능을 하나로 묶고 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것이다.

앞서 데이터와 데이터를 처리하는 메서드를 하나로 모으는 것에 초점을 맞춰 캡슐화를 알아보았는데 여기서 한발짝 더 나아가 캡슐화를 안전하게 완성할 수 있게 해주는 장치가 바로 접근제어자이다.

데이터를 숨겨라

객체에는 속성(데이터)기능(메서드)이 있다.
캡슐화에서 가장 필수로 숨겨야 하는 것은 속성(데이터)이다.
Speakervolume을 떠올려보면 객체 내부의 데이터를 외부에서 함부로 접근하게 두면 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있다.
결국 모든 안전망을 다 빠져나가게된다. --> 캡슐화가 깨진다.

우리가 자동차를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지 않는다. 단지 자동차가 제공하는 엑셀 기능을 사용해서 엑셀을 밟으면 자동차가 나머지는 다 알아서 하는 것이다.

우리가 일상에서 생각할 수 있는 음악 플레이어를 떠올려보자.
음악 플레이어를 사용할 때 그 내부에 들어있는 전원부나 볼륨 상태의 데이터를 직접 수정할 일이 있을까? 우리는 그냥 플레이어의 켜고, 끄고, 볼륨을 조절하는 버튼을 누를 뿐이다. 그 내부에 있는 전원부나 볼륨의 상태 데이터를 직접 수정하지 않는다. 전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일이다. 볼륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 볼륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는 일이다.
즉, 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것이다. 복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하는 것은 아니다.

객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.

기능을 숨겨라

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

우리가 자동차를 운전하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리가 알 필요는 없다. 우리는 단지 엑셀과 핸들 정도의 기능만 알면 된다. 만약 사용자에게 이런 기능까지 모두 알려준다면 사용자가 자동차에 대해 너무 많은 것을 알아야한다.

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

⭐️데이터는 모두 숨기고 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화이다.⭐️




package access;

public class BankAccount {

    private int balance;

    public BankAccount() {
        balance = 0;
    }

    //public 메서드 : deposit
    public void deposit(int amount){
        if(isAmountValid(amount)){
            balance += amount;
        }else{
            System.out.println("유효하지 않은 금액입니다.");
        }
    }

    private boolean isAmountValid(int amount){ //내부에서만 쓸거니까 private
        return amount > 0; //0원보다 커야함
    }

    //public 메서드 : withdraw
    public void withdraw(int amount){
        if(isAmountValid(amount) && balance-amount >= 0){
            balance -= amount;
        }else{
            System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
        }
    }

    //public 메서드
    public int getBalance(){
        return balance;
    }
}

private

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

public

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

만약 isAmountValid()를 외부에 노출하면 어떻게 될까?
BankAccount를 사용하는 개발자라면 아마 입금과 출금 전에 본인이 먼저 isAmountValid()을 사용해서 검증을 해야 하나? 의문을 가질 것이다.

만약 balance필드를 외부에 노출하면 어떻게 될까?
BankAccount를 사용하는 개발자 입장에서는 이 필드를 직접 사용해도 된다고 생각할 수 있다. 외부에 공개하는 것은 그것을 외부에서 사용해도 된다는 뜻이기 때문이다.
결국 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생할 수 있다.

접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도도 낮출 수 있다.

profile
🌱

0개의 댓글