접근 제어자

2024. 2. 5. 20:05Java

◎ 접근 제어자

-> 자바는 public, private, protected, default라는 접근 제어자를 제공한다.

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

 

-> 예제

- 스피커 클래스를 통해 스피커의 음량을 조절하는 기능을 구현한다.

- 스피커의 음량은 100을 넘을 수 없다.

package access;

public class Speaker {
    int volume;

    // 생성자로 volume 초기화
    public 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("볼륨 10 감소");
    }

    void showVolume() {
        System.out.println("현재 음량 : " + volume);
    }
}

- volumeUp메서드를 통해 스피커의 음량을 높이는데 volume의 값을 확인하여 100이상이면 더 이상 음량을 높이지 않도록 했다.

package access;

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

        speaker.showVolume();

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

-> 결과

- 음량을 증가 시키다가 100 이상이 되면 "최대 음량!"을 출력하고 더 이상 음량을 높이지 않는다.

 

-> volumeUp()메서드로 volume을 체크하여 음량을 조절하였다. 그런데 만약 SpeakerMain 클래스의 main메서드에서 volumeUp()메서드가 아닌 volume 필드에 직접 접근하여 volume을 조절한다면 어떤 결과가 나올까?

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

        speaker.showVolume();

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

        // 필드에 직접 접근
        System.out.println("volume 필드에 직접 접근");
        speaker.volume = 200; // private 필드에 직접 접근 불가
        speaker.showVolume();
    }
}

- speaker.volume = 200; 은 Speaker 인스턴스의 volume 필드에 직접 접근하여 값을 200으로 설정하였다.

-> 결과

-> volume 필드에 직접 접근을 하면서 200으로 값을 설정하였고 결국 음량이 200이 되어버렸다.

-> 스피커의 볼륨은 100을 넘을 수 없다는 초기 요구사항을 위반했다.

-> volumeUp()메서드로 volume 값을 확인하여 볼륨을 조절을 하여 volumeUp()메서드를 통해 볼륨을 높이면 정상적으로 잘 동작하지만, 필드로 직접 접근을 해버리면 volume 값이 어떤 값이 되든 그대로 저장이 되기 때문에 문제가 발생한다.

-> 이 문제를 해결하려면 volume 필드를 변경할 수 없게 하면 될 것이다.

 

-> Speaker 클래스의 volume 필드 앞에 private를 붙인다.

-> private는 모든 외부 호출을 막는다. private이 붙은 경우 해당 클래스 내부에서만 사용할 수 있다.

-> Speaker 클래스의 volume 필드에 private을 붙인 후 Speaker 클래스를 보면 volume필드에 직접 접근한 코드에 에러가 발생한다.

-> Speaker 클래스의 volume은 private 접근제어자로 Speaker 클래스 내부에서만 사용가능하다. 따라서 외부 클래스인 speakerMain클래스에서는 volume에 접근이 불가능하다. volume을 조절하려면 Speaker 클래스의 볼륨 조절 메서드(volumeUp(), volumeDow())을 이용해 조절해야 한다.

 

-> 이렇게 접근 제어자를 통해 필드나 메서드의 접근을 허용하거나 제한하여 데이터가 외부에서 임의로 수정되지 않도록한다.

 

◎ 접근 제어자의 종류

-> 접근 제어자는 private, default, protected, public으로 4가지가 있다.

-> private : 모든 외부의 호출을 막는다.

-> default : 같은 패키지 내에서 호출은 허용한다.(필드나 메서드에 접근제어자를 따로 붙이지 않으면 기본적으로 default다.)

-> protected : 같은 패키지 내에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.

-> public : 모든 외부 호출을 허용한다.

-> 접근 제어자는 필드, 메서드, 생성자에 사용된다.

 

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

 

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

-> 예제

package access.a;

public class AccessData {
    public int publicField; // public
    int defaultField; // default
    private int privateField; // private 

    public void publicMethod(){ // public
        System.out.println("publicMethod 호출 : " + publicField);
    }

    void defaultMethod() { // default
        System.out.println("defaultMethod 호출 : " + defaultField);
    }

    private void privateMethod() { // private 
        System.out.println("privateMethod 호출 : " + privateField);
    }

    public void innerAccess(){ // public
        System.out.println("내부 호출 ");
        publicField = 100;
        defaultField = 200;
        privateField = 300;

        publicMethod();
        defaultMethod();
        privateMethod();
    }
}

-> 해당 클래스의 패키지 위치는 access.a다.

package access.a;

public class AccessInnerMain {
    public static void main(String[] args) {
        AccessData accessData = new AccessData();

        // public 호출 가능
        accessData.publicField = 1;
        accessData.publicMethod();

        // 같은 패키지 내에서 default 호출 가능
        accessData.defaultField = 2;
        accessData.defaultMethod();

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

        accessData.innerAccess();
    }
}

-> AccessData 클래스에 접근하는 클래스다. 해당 클래스의 패키지 위치는 access.a다.

-> 결과

-> public은 모든 외부에서 호출이 가능하기 떄문에 publicField와 publicMethod에 접근이 가능하다.

// public 호출 가능
accessData.publicField = 1;
accessData.publicMethod();

-> AccessData 클래스와 AccessInnerMain 클래스 모두 access.a에 위치하기 때문에 defaultField와 defaultMethod에 접근이 가능하다.

// 같은 패키지 내에서 default 호출 가능
accessData.defaultField = 2;
accessData.defaultMethod();

 

-> AccessData 클래스의 privateField와 privateMethod의 접근제어자는 private로 외부 클래스인 AccessInnerMain에서 접근이 불가능하다.

-> 그런데 결과를 보면 privateMethod가 호출된 것을 볼 수 있다.  

-> AccessData클래스의 메서드인 innerAccess()메서드를 통해 호출했다.

-> innerAccess()는 AccessData 클래스 내부에 선언된 메서드다. 따라서 innerAccess()메서드에서 같은 클래스에 있는 privateField와 privateMethod에 접근을 할 수 있다. 또한 innerAccess()는 public 접근 제어자로 외부에서 호출이 가능하다. innerAccess()에서 privateField를 초기화하고, privateMethod를 호출하는 코드를 구현하고 이를 외부에서 호출한 것이다.

 

-> 다음은 access.b 패키지에 위치한 AccessOuterMain클래스다.

package access.b;

import access.a.AccessData;

public class AccessOuterMain {
    public static void main(String[] args) {
        AccessData accessData = new AccessData();

        // public 호출 가능
        accessData.publicField = 1;
        accessData.publicMethod();

        // 다른 패키지에서 default 호출 불가
//        accessData.defaultField = 2;
//        accessData.defaultMethod();

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

        accessData.innerAccess();
    }
}

-> access.b에 위치한 AccessOuterMain에서는 access.a에 위치한 AccessData클래스의 default 접근 제어자를 가진 defaultField와 defaultMethod에 접근할 수 없고, private 접근 제어자인 privateField와 privateMethod에 접근할 수 없다.

-> publicField, publicMethod, innerAccess()는 모두 public 접근 제어자를 가졌기 때문에 접근이 가능하다.

-> 결과

 

◎ 접근 제어자 활용 - 클래스

-> 클래스 레벨에서 접근 제어자는 public과 default만 사용할 수 있다. private와 protected는 사용할 수 없다.

-> public 클래스는 반드시 파일명과 이름이 같아야 한다.

-> 하나의 자바 파일에 public 클래스는 하나만 등장한다.

-> 하나의 자바 파일에 default 클래스는 무한정 만들 수 있다.

ackage access.a;

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

class DefaultClass1{

}

class DefaultClass2 {

}

-> access.a 패키지에 있는 PublicClass 클래스다.

-> PublicClass는 public 접근 제어자를 가진다. 해당 클래스는 외부에서 접근 가능하다.

-> DefaultClass1,2는 모두 default 접근 제어자를 가진다. 동일 패키지 내에서 접근 가능하다.

※ 참고로 public 클래스는 하나의 자바 파일에 하나만 등장한다고 했는데 PublicClass에 public 클래스를 하나 더 정의해봤다.

-> 에러가 발생한다.

 

-> 다음은 PublicClass를 사용하는 PublicClassInnerMain클래스다.

package access.a;

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

-> access.a 패키지에 위치한다.

-> PublicClass에 접근 가능하고, PublicClass와 같은 패키지에 있기 때문에 DefaultClass1,2에 모두 접근 가능하다.

-> 다음은 access.b에 위치한 PublicClassOuterMain 클래스다.

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

-> public 접근 제어자를 가진 PublicClass에는 접근이 가능하지만 default 접근 제어자를 가진 DefaultClass1,2는 다른 패키지에 존재하기 때문에 접근할 수 없다.

 

◎ 캡슐화(Encapsulation)

-> 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어 외부에서 접근 하는 것을 제한하는 것이다.

-> 캡슐화를 통해 데이터의 직접적인 변경을 막을 수 있다.

-> 이전에 봤던 Speaker 클래스애서 Speaker 클래스의 volume 필드를 private으로 설정하여 volume 필드에 직접 접근하는 것을 막고, volumeUp/Down()메서드를 통해서만 볼륨을 조절할 수 있도록 한 것이 캡슐화다.

-> Speaker를 사용하는 곳에서는 볼륨을 조절하는 기능만 알면 되지 볼륨 데이터 자체를 알 필요가 없다.

-> 객체의 데이터는 객체에서 제공하는 메서드를 통해서 접근해야 한다.

-> 객체의 데이터(필드 등)를 외부에서 접근하게 한다면 클래스 내에 구현한 로직을 무시하고 데이터가 변경이 될 수 있기 떄문에 메서드를 통해 접근하는 것이 안전하다.

-> 또한 객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능이 있는데 이 역시 외부로부터의 접근을 막는 것이 좋다.

-> 예를 들어 자동차를 운전할 때 운전자는 시동 on/off, 액셀, 브레이크 정도의 기능만 알면 되지 엔진 조절 기능 까지 알 필요가 없다. 이런 기능까지 알기엔 너무 많은 것을 알아야 한다.

-> 데이터는 숨기고 필요한 기능만 노출하는 것이 좋은 캡슐화다.

 

-> 캡슐화 예제

-> 아래는 BankAcount 객체로 잔고를 필드로 가지고 있고, 입금, 출금, 잔액 조회, 입금 금액 검증 기능을 가지고 있다.

package access;

public class BankAccount {
    private int balance; // 잔고

    public BankAccount() {
        this.balance = 0;
    }

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

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

    // public 메서드 - 잔액 조회
    public int getBalance(){
        return balance;
    }

    // 입금 금액 검증 로직
    private boolean isAmountValid(int amount) {
        // 금액 > 0
        return amount > 0;
    }
}

-> 여기에서 사용자가 사용할 기능은 입금, 출금, 잔액 조회다.  입금 잔액 조회는 입금 기능을 제대로 수행하기 위해 검증하는 로직일 뿐 사용자가 필요로 하는 기능이 아니다 따라서 해당 메서드에는 private 접근 제어자를 붙였다.

package access;

public class BankAccountMain {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(10000);
        account.withdraw(3000);
        System.out.println("balance = " + account.getBalance());
    }
}

 

-> 결과

-> 사용자가 account.deposit(10000)을 통해 입금을 하면 해당 참조값 위치에 있는 Account 클래스의 deposit()메서드로 접근하고, 매개변수로 받은 수가 입금을 정상적으로 수행할 수 있는 값인지 isAmountValid()메서드를 통해 확인한 후 입금 로직을 수행한다. 

-> 또한 balance를 private로 설정하여 외부에서 접근할 수 없게 막아 balance가 로직을 무시하는 음수와 같은 유효하지 않은 값이 들어가는 상황을 방지한다.

 

 

★ 참고

김영한의 실전 자바 - 기본편

'Java' 카테고리의 다른 글

final  (0) 2024.02.07
자바 메모리 구조 & static  (0) 2024.02.06
생성자  (0) 2024.02.01
객체 지향 프로그래밍  (0) 2024.01.31
기본형 & 참조형  (0) 2024.01.30