접근 제어자 이해1

접근 제어자(access modifier)

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

 

예제를 통해 접근 제어자가 필요한 이유를 알아보자.

 

스피커에 들어가는 소프트웨어를 개발해보자. 스피커의 음량은 절대로 100을 넘으면 안되는 요구사항이 있다. (100을 넘으면 스피커의 부품들이 고장난다.)

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

 

Speaker

package javabasic06;

public class Speaker {
    int volume;

    //생성자를 통해 초기 음량값을 지정할 수 있다.
    Speaker(int volume){
        this.volume = volume;
    } 
    //음량을 높인다.
    public void volumeUp(){
        if(volume>=100){
            System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
        } else {
            volume+=10;
            System.out.println("음량을 10 증가합니다.");
        }
    }
    //음량을 낮춘다.
    public void volumeDown(){
        volume-=10;
        System.out.println("음량을 10 감소합니다.");
    }
    //현재 음량을 확인한다.
    public void showVolume(){
        System.out.println("현재 음량: " + volume);
    }
}

 

 

SpeakerMain

package javabasic06;

public class SpeakerMain {
    public static void main(String[] args) {
        Speaker speaker = new Speaker(90); //초기 음량값을 90으로 지정
        speaker.showVolume();

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

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

실행 결과

요구사항대로 스피커의 음량이 100을 넘지 않도록 설계했다. 프로젝트가 성공적으로 끝났다.

 

오랜 시간이 지나 업그레이드 된 다음 버전의 스피커를 출시하게 되었다. 이때 새로운 개발자가 투입되었는데, 소리를 더 올리면 좋겠다고 생각했다. Speaker 클래스를 보니 volume 필드를 직접 사용할 수 있었다. volume 필드의 값을 200으로 설정하고 이 코드를 실행한 순간 스피커의 부품들에 과부하가 걸리면서 폭발했다.

 

SpeakerMain - 필드 직접 접근 코드 추가

package javabasic06;

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 필드의 외부 접근을 막을 수 있는 방법이 필요하다.

 


접근 제어자 이해2

volume 필드를 Speaker 클래스 외부에서 접근하지 못하게 막으면 된다.

package javabasic06;

public class Speaker {
    private int volume; //private 사용
        .
        .
        .

 

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

 

volume 필드를 private을 이용해서 Speaker 내부에 숨겼다.

외부에서 volume 필드에 직접 접근할 수 없게 막은 것이다. volume 필드는 이제 Speaker 내부에서만 접근할 수 있다.

 

이제 SpeakerMain 코드를 다시 실행하면 컴파일 오류가 발생한다.

//필드에 직접 접근
System.out.println("volume 필드 직접 접근 수정");
speaker.volume = 200; //컴파일 오류
speaker.showVolume();

volume 필드는 private으로 설정되어 있기 때문에 외부에서 접근할 수 없다는 오류

 


접근 제어자 종류

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

  1. private: 모든 외부 호출을 막는다.
    나의 클래스 안으로 속성과 기능을 숨길 때 사용한다. 외부 클래스에서 해당 기능을 호출할 수 없다.
  2. default (package-private): 같은 패키지 안에서 호출은 허용한다.
    나의 패키지 안으로 속성과 기능을 숨길 때 사용한다. 외부 패키지에서 해당 기능을 호출할 수 없다.
  3. protected: 같은 패키지 안에서 호출은 허용하며, 패키지가 다르더라도 상속 관계의 호출은 허용한다.
    상속 관계로 속성과 기능을 숨길 때 사용한다. 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.
  4. public: 모든 외부 호출을 허용한다.
    기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.

 

차단 정도의 순서

private이 가장 많이 차단하고, public이 가장 많이 허용한다.

private → default → protected → public

 

package-private

접근 제어자를 명시하지 않으면 default 값으로 default 접근 제어자(같은 패키지 안에서 호출 허용)가 적용된다.

default라는 용어는 해당 접근 제어자가 기본값으로 사용되기 때문에 붙여진 이름이지만,

실제로는 package-private이 더 정확한 표현이다. 왜냐하면 해당 접근 제어자를 사용하는 멤버는 동일한 패키지 내의 다른 클래스에서만 접근이 가능하기 때문이다. 참고로 두 용어를 함께 사용한다.

 

접근 제어자 사용 위치

접근 제어자는 필드, 메서드, 생성자, 클래스 레벨에 사용된다.

※ 클래스 레벨: public, default만 사용 가능

 

접근 제어자 예시

package javabasic06;

class Speaker { //클래스 레벨(default)
    private int volume; //필드(private)

    public Speaker (int volume){ //생성자(public)

    }
    
    //메서드(public)
    public void volumeUp(){} 
    public void volumeDown(){}
    public void showVolume(){}
}

 


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

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();
    }
}
  • 순서대로 public, default, private을 각각 필드와 메서드에 사용했다.
  • 마지막의 innerAccess()는 내부 호출을 보여준다. 내부 호출자기 자신에게 접근하는 것이기 때문에 private을 포함한 모든 곳에 접근할 수 있다.

 

이제 외부에서 이 클래스에 접근해보자.

[AccessInnerMain] (동일 패키지 내, 다른 클래스)

package access.a;

public class AccessInnerMain {
    public static void main(String[] args) {
        AccessData data = new AccessData();
        //public 호출 가능
        data.publicField = 1;
        data.publicMethod();

        //같은 패키지 default 호출 가능 (패키지 access.a)
        data.defaultField = 2;
        data.defaultMethod();

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

        data.innerAccess(); //innerAccess는 public이므로 외부에서 호출할 수 있음
    }
}

실행 결과

 

이번에는 코드는 동일하되, 다른 패키지 내, 다른 클래스에서 메인 코드를 생성해보자.

[AccessOuterMain] (다른 패키지 내, 다른 클래스)

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(); //innerAccess는 public이므로 외부에서 호출할 수 있음
    }
}

실행 결과

 


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

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

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

 

[파일명: PublicClass.java]

package access.a;

public class PublicClass { //public class는 파일명과 동일
    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass(); //public - 어디서나 사용 가능
        DefaultClass1 class1 = new DefaultClass1(); //default - 같은 패키지 내부O
        DefaultClass2 class2 = new DefaultClass2(); //default - 같은 패키지 내부O
    }
}

class DefaultClass1 {

}

class DefaultClass2 {

}
  • PublicClass 클래스는 public 접근 제어자를 사용하므로, 파일명과 클래스 이름이 반드시 일치해야 한다.
    (파일명: PublicClass.java // 클래스명:PublicClass)
    이 클래스는 public이기 때문에 외부에서 접근할 수 있다.
  • DefaultClass1, DefaultClass2는 default 접근 제어자를 사용하므로, 같은 패키지 내부에서만 접근할 수 있다. 

 

다른 패키지에 같은 코드를 생성해보자.

package access.b;

import access.a.PublicClass;

public class PublicClassOuterMain {
    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass(); //public - 어디서나 사용 가능

        //다른 패키지 접근 불가
//        DefaultClass1 class1 = new DefaultClass1();
//        DefaultClass2 class2 = new DefaultClass2();
    }
}
  • PublicClass는 public이기 때문에 외부에서 접근할 수 있다.
  • DefaultClass1, DefaultClass2는 PublicClassOuterMain과 다른 패키지에 있기 때문에 접근할 수 없다.

 


캡슐화(Encapsulation)

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

즉, 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것

private 접근 제어자 활용

 

객체 = 속성(데이터) + 기능(메서드)

  1. 데이터를 숨겨라
    객체의 속성(데이터)값은 외부에서 직접 접근할 수 없게 막아야 한다.
    객체의 데이터는 메서드를 통해서만 접근할 수 있다.
  2. 기능을 숨겨라
    객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들은 숨겨야 한다.
    사용자 입장에서 꼭 필요한 기능만 외부에 노출하고, 나머지 기능은 모두 내부로 숨기자.
좋은 캡슐화: 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것

 


잘 캡슐화된 예제

package day06;

public class BankAccount {
    private int balance = 0;

    //public 메서드: deposit, withdraw, getter
    public void deposit(int amount){
        if(isAmountValid(amount)){
            balance += amount;
            System.out.println(amount + "원을 입금했습니다.");
        } else {
            System.out.println("유효하지 않은 금액입니다.");
        }
    }
    public void withdraw(int amount){
        if(!(isAmountValid(amount))){
            System.out.println("유효하지 않은 금액입니다.");
        } else if(balance >= amount){
            balance -= amount;
            System.out.println(amount + "원을 출금했습니다.");
        } else if(balance < amount) {
            System.out.println("통장 잔고가 부족합니다.");
        }
    }

    public int getBalance(){
        return balance;
    }

    //private 메서드: isAmountValid
    private boolean isAmountValid(int amount){
        //금액이 0보다 커야함
        return amount > 0;
    }
}
package day06;

public class BankAccountMain {
    public static void main(String[] args) {
        //10000원 입금
        //3000원 출금
        //잔액 확인
        BankAccount account = new BankAccount();
        account.deposit(10000);
        account.withdraw(3000);
        System.out.println("잔액: " + account.getBalance() + "원");
    }
}

실행 결과

 

은행 계좌 기능을 다룬다. 다음과 같은 기능을 가지고 있다.

 

private

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

 

public

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

BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 된다. 나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있다.

 

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

 

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

 

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

+ Recent posts