[Java 기초] 접근제어자 종류와 캡슐화
1. 접근제어자가 필요한 이유
접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다. 접근제어자가 왜 필요한가?
스피커에 들어가는 소프트웨어 프로그램을 작성하며 알아보자. 이 스피커의 음량은 절대로 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을 넘게 되면 음량이 더이상 증가하지 않는다.
SpeakerMain
Speaker speaker = new Speaker(90);
speaker.showVolume();
speaker.volumeUp();
speaker.showVolume();
speaker.volumeUp();
speaker.showVolume();
초기 음량 값을 90으로 지정했고, 기대한 대로 음량은 100 이상 넘지 않았다.
그리고 오랜 시간이 지난 후, 기존의 요구사항을 모르는 새로운 개발자가 소리를 더 올리면 좋겠다고 생각하여 Speaker
클래스의 volume
필드를 직접 사용하여 volume
값을 200으로 설정하고 코드를 실행한 순간, 스피커 부품들에 과부하가 걸리면서 폭발했다.
speaker.volume = 200;
speaker.showVolume();
현재 코드로는, Speaker
객체를 사용하는 사용자가 volume
필드와 메서드에 모두 접근할 수 있다. 그래서 volumeUp
메서드로 음량이 100이 넘지 못하도록 기능을 개발하였음에도 volume
필드에 직접 접근해서 원하는 값을 설정할 수 있으므로, 소용없다는 것을 알 수 있다.
이런 문제를 근본적으로 해결하기 위해서는 volume
필드의 외부 접근을 막을 수 있는 방법이 필요하다. 이것을 접근제어자로 해결하룻 있다.
2. 접근제어자 private
public class Speaker {
private int volume;
}
private
접근 제어자는 모든 외부 호출을 막는다.
해당 클래스 내부에서만 호출할 수 있다. volume
에 private을 사용하여 Speaker
내부에 숨긴 셈이다. 이제 Speaker
내부에서만 volume
필드에 접근할 수 있다.
Speaker
클래스를 개발하는 개발자가 처음부터 private
을 사용해서 외부 접근을 막아두었다면, 새로운 개발자도 volume
필드에 직접 전근하지 않고 메서드를 통해 접근했을 것이다. 결과적으로 Speaker
가 폭발하는 문제는 발생하지 않았을 것이다.
3. 접근 제어자의 종류
private
: 모든 외부 호출을 막음default
(package-private) : 같은 패키지 안에서 호출은 허용protected
: 같은 패키지 안에서 호출은 허용. 패키지가 달라도 상속 관계의 호출은 허용public
: 모든 외부 호출을 허용
priavte -> default -> protected -> public
순으로 가장 많이 차단하는 것에서 가장 많이 허용하는 것으로 정리할 수 있다.
package-private
- 접근 제어자를 명시하지 않으면
default
접근 제어자가 적용됨 - 동일 패키지 내에서만 접근 가능하기 때문에,
package-private
가 더 정확한 표현임
접근 제어자 사용 위치
- 필드, 메서드, 생성자, 클래스 레벨
public class Speaker { //클래스 레벨
private int volume; //필드
public Speaker(int volume) {} //생성자
public void volumeUp() {} //메서드
public void volumeDown() {}
public void showVolume() {}
}
접근 제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것이다.
- private: 나의 클래스 안으로 속성과 기능을 숨길 때 사용
- default: 나의 패키지 안으로 속성과 기능을 숨길 때 사용
- protected: 상속 관계로 속성과 기능을 숨길 때 사용
- public: 기능을 숨기지 않음
4. 접근 제어자 사용-필드, 메서드
클래스 내에서 호출하기
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();
}
}
private
외에 다른 메소드는 접근 가능하다
다른 패키지에서 호출하기
package access.b;
import access.a.AccessData;
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();
}
}
default
,private
접근제어자가 붙은 것은 접근할 수 없다.
5. 접근제어자-클래스 레벨
클래스 레벨의 접근 제어자 규칙
- 클래스 레벨의 접근 제어자는
public
,default
만 사용할 수 있다 public
클래스는 반드시 파일명과 이름이 같아야 한다- 하나의 자바 파일에
public
클래스는 하나만 등장 가능 - 하나의 자바 파일에
default
클래스는 여러개 가능
- 하나의 자바 파일에
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 {
}
같은 패키지 내에서 사용하기
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.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();
//DefaultClass2 class2 = new DefaultClass2();
}
}
default
클래스는 접근할 수 없다.
6. 캡슐화
캡슐화(Encapsulation)는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것이다. 캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있다.
- 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 내부로 숨김
이 캡슐화를 안전하게 완성할 수 있게 하는 장치가 접근제어자다.
1) 데이터를 숨겨라
- 객체의 속성(데이터)과 기능(메서드) 중 필수로 숨겨야 할 것은 속성(데이터)다.
- 객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 로직을 무시하고 데이터를 변경할 수 있게 된다. (캡슐화 깨짐)
객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.
2) 기능을 숨겨라
- 객체의 기능 중에서도 내부에서만 사용하는 기능이 있는데, 이런 것들도 모두 감추는 것이 좋다.
- 사용자에게 이런 기능까지 모두 알려준다면, 사용자가 자동차에 대해 너무 많은 것을 알아야 한다. 사용자 입장에서 꼭 필요한 기능만 외부에 노출하자.
데이터는 모두 숨기고, 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화다.
잘 캡슐화된 코드 예제
은행 계좌 프로그램
package access;
public class BankAccount {
private int balance;
public BankAccount() {
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("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드
public int getBalance() {
return balance;
}
// private 메서드
private boolean isAmountValid(int amount) {
// 금액이 0보다 커야함
return amount > 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
balance
: 데이터 필드 노출 XisAmoundValid()
: 내부에서만 사용하는 기능임
public
deposit()
: 입금 기능withdraw()
: 출금 기능getBalace()
: 잔고 확인 기능
내부 메소드를 외부에 노출하면?
- 개발자 입장에서 알아야 할 메서드가 하나 더 늘게 된다.
필드를 외부에 노출하면?
- 개발자 입장에서 이 필드를 직접 사용해도 된다고 생각할 수 있다. 그래서 모든 검증과 캡슐화가 깨지게 되고, 기능이 망가지는 문제가 발생할 수 있다.
정리
접근제어자와 캡슐화를 통해 데이터를 안전하게 보호할 수 있고, 개발자 입장에서도 해당 기능을 사용하는 복잡도도 낮출 수 있다.