접근 제어자는 외부에서 어떠한 클래스나 해당 클래스의 멤버 변수나 메서드에 접근하는 것을 제한하는 것을 말한다.
접근 제어자 종류는 4가지가 있다.
가장 많이 차단하는 것이 private
이고 가장 많이 접근 가능한 것이 public
이다. 아래는 왼쪽부터 가장 많이 차단하는 순서다.
private --> protected --> default --> public
스피커 객체를 가지고 접근 제어자를 알아보겠다.
Speaker.java
package access;
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을 넘어가면 안되는 요구사항이 있었다고 가정 해본다. 그래서 volumeUp() 메서드를 보면 음량을 10씩 증가하게 하고 100이 넘어가면 음량을 증가할 수 없게 끔 제한을 두었다.
SpeakerMain.java
package access;
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
음량을 10 증가합니다.
현재 음량:100
음량을 증가할 수 없습니다. 최대 음량입니다.
현재 음량:100
메인 클래스에서 초기 음량 90으로 지정했다. 그리고 음량을 여러 번 증가 시켜서 음량 100을 넘어선 순간부터는 제한이 된 것을 확인할 수 있다.
여기서 예를 들어 새로운 개발자가 해당 코드를 이어 받았는데 기존 요구사항에 대해서는 알지 못했다. 그리고 새로운 개발자는 음량 100을 넘기고 싶은데 넘기지 못해서 고민을 하다가 Speaker
클래스의 volume
변수에 직접 접근 할 수 있는 것을 확인하여 직접 접근해서 볼륨을 올려보기로 했다.
SpeakerMain.java 코드 수정
package access;
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();
}
}
실행 결과
현재 음량:90
음량을 10 증가합니다.
현재 음량:100
음량을 증가할 수 없습니다. 최대 음량입니다.
현재 음량:100
volume 필드 직접 접근 수정
현재 음량:200
이렇게 결국 새로운 개발자는 기존 요구사항을 모른채 지키지 못하여 음량을 200으로 올려 해당 프로그램에 문제가 발생했다. 이렇게 데이터에 직접적으로 자유롭게 접근을 하여 프로그램에 문제가 발생할 수 있다. 그래서 접근 제어자를 통해 문제가 발생할 확률을 낮출 수가 있다.
메인 클래스는 건드리지 말고 Speaker
클래스의 volume
변수에 private
접근 제어자를 추가해보자.
public class Speaker {
private int volume;
...
}
그러면 메인 클래스에서 직접 접근했던 speaker.volume = 200;
라인에 빨간 줄이 표시 된 것을 확인할 수 있다. private
접근 제어자로 인해 직접 접근이 제한되어 컴파일 오류가 발생했다.
이렇게 접근 제어자로 데이터에 직접 접근하는 것을 오류로 막아 데이터 오류가 나지 않도록 방지를 해준다.
무한한 자유도를 주는 것보다 이렇게 접근 제어자를 통해 적절한 제약을 제공하는 것이 좋은 프로그램이다.
4가지 접근 제어자를 코드로 확인해보자.
AccessData.java
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();
}
}
AccessInnerMain.java
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
AccessData
클래스와 AccessInnerMain
클래스는 서로 같은 access.a
패키지에 있어야 한다.AccessData
클래스와 AccessInnerMain
클래스도 서로 같은 access.a
패키지에 있어 호출이 가능하다.AccessData
클래스 내부에서만 접근이 가능하다. AccessInnerMain
클래스는 외부에 있는 클래스이기 때문에 호출이 불가능하다.AccessData
클래스 내부에 있기 때문에 호출이 가능하다.AccessOuterMain.java
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();
}
}
실행 결과
publicMethod 호출 1
내부 호출
publicMethod 호출 100
defaultMethod 호출 200
privateMethod 호출 300
access.b
패키지에 생성한다.AccessData
클래스는 access.a
패키지, AccessOuterMain
클래스는 access.b
패키지에 있으므로 서로 다른 패키지에 있어 호출이 불가능하다.클래스 레벨 접근 제어자 규칙
public
default
만 선언 가능하다.protected
private
는 사용이 불가능하다.public
으로 선언 시 파일 명과 클래스 이름이 같아야 한다.public
클래스만 생성할 수 있다.default
클래스를 생성할 수 있다.코드로 확인을 해보자.
PublicClass.java
package 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 {
}
PublicClassInnerMain.java
package access.a;
public class PublicClassInnerMain {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
DefaultClass1 class1 = new DefaultClass1();
DefaultClass2 class2 = new DefaultClass2();
}
}
PublicClass
클래스와 PublicClassInnerMain
클래스는 서로 같은 패키지에서 생성 해준다.public
의 경우 모든 접근이 가능하기 때문에 호출이 가능하다.PublicClassOuterMain.java
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();
}
}
PublicClassOuterMain
클래스는 다른 패키지인 access.b
패키지에 생성했다.public
도 모든 접근이 가능하기 때문에 다른 패키지여도 호출이 가능하다.PublicClassOuterMain
클래스는 다른 패키지에 생성이 됐으므로 호출이 불가능하다.객체지향 프로그래밍 개념 중 하나인 캡슐화는 해당 클래스에서 선언된 필드나 메서드를 외부로부터 접근하지 못하게 하나로 묶어 감싸고 있다는 표현하여 캡슐화라고 한다.
이렇게 접근 제어자를 통해 캡슐화하여 데이터를 직접적으로 변경을 제한하거나 방지할 수 있다. 외부로부터 사용해야 할 데이터나 기능들은 노출을 시켜주어 사용하면 된다.
데이터를 숨겨라
데이터(속성) 같은 경우는 필수로 숨겨야 한다. 해당 데이터에 직접적으로 접근하게 되면 클래스에서 다루는 로직을 무시하고 데이터를 변경하게 될 수 있기 때문이다. 예를 들면 위에 예시 중 Speaker
클래스의 volume
속성 같은 경우 직접적으로 접근하여 클래스 로직에 제한을 둔 로직이 있지만 그 제한을 무시하고 볼륨 값을 올릴 수 있었던 예시가 있다. Speaker
클래스로 예시를 둔 상황처럼 나오지 않게 하려면 객체가 제공하는 기능(메서드)을 통해서 접근해야 한다.
기능을 숨겨라
외부로부터 사용하지 않는 기능은 숨겨야 한다. 굳이 외부에서 사용하지 않을 기능까지 제공하게 되면 오히려 복잡해질 수 있기 때문에 내부에서만 사용하는지, 외부에서도 사용하는지 잘 알아보고 제공하는 것이 좋다.
은행 계좌 기능 예시로 확인해보자.
BankAccount.java
package access;
public class BankAccount {
private int balance;
public BankAccount() {
}
// public 메서드: deposit (입금)
public void deposit(int account) {
if(isAmountValid(account)) {
balance += account;
} else {
System.out.println("유효하지 않은 금액입니다.");
}
}
// public 메서드: withdraw (출금)
public void withdraw(int account) {
if(isAmountValid(account) && balance - account >= 0) {
balance -= account;
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드: getBalance (잔금 표시)
public int getBalance() {
return balance;
}
// private 메서드: isAmountValid
private boolean isAmountValid(int account) {
// account가 0보다 커야함
return account > 0;
}
}
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());
}
}
private
private int balance
: 잔금 데이터 속성인데 private로 선언되어 있다. 잔금 데이터 같은 경우 직접적으로 접근하게 되면 잔금이 있었을 경우 잔금 데이터에 직접 접근하여 잔금이 없어지게 할 수도 또는 잔금을 더 늘릴 수도 있는 상황이 나올 수 있기 때문에 숨겨두었다.isAmountValid
메서드 : 입력 금액을 검증하는 메서드이다. 해당 기능은 굳이 외부로 노출시킬 필요가 없기 때문에 숨겨두었다.public
deposit
: 입금 메서드
withdraw
: 출금 메서드
getBalance
: 잔금 메서드
입금, 출금, 잔금 세 개의 메서드는 public으로 선언했다. 생각을 해봤을 때 입금했을 때 얼마나 잘 입금이 됐는지, 출금했을 때 얼마나 잘 출금이 됐는지, 입금 또는 출금한 후 잘 계산되서 잔금이 남아있는지 확인이 필요하기 때문에 외부에 노출시켰다.
여기서 만약 입력 금액을 검증하는 메서드인 isAmountValid
메서드를 노출시키면 어떻게 될까? 다른 개발자가 해당 메서드 호출이 가능하다는걸 보면 이 메서드로 검증 로직을 넣어야 하는 것인가? 라고 의문이 생길 수 있다. 의문이 들면 해당 메서드를 찾아가보게 되고 해당 메서드를 확인해보니 해당 메서드에서 이미 검증을 하고 있는 것을 확인하게 된다.
이렇게 이미 해당 메서드가 검증을 하고 있는데 노출을 시켜 다른 개발자에게 혼동을 주는 상황이 나올 수 있다. 그러므로 노출 시키지 않아도 되는 기능들은 캡슐화하여 데이터나 기능들을 안전하게 보호하고 다른 개발자들에게는 복잡도를 낮출 수가 있다.
참고