접근 제어자가 필요한 이유
접근 제어자를 학습하기 전 한 가지 예시를 통해 접근 제어자가 필요한 이유를 알아보자.
Speaker 객체 설계와 접근 제어자의 필요성
`Speaker` 클래스는 음량을 관리하는 객체로, 메서드를 통해 음량을 조절하도록 설계되었습니다. 다음은 `Speaker` 객체를 생성하고, 초기 음량을 설정한 뒤, 음량을 높이는 메서드를 호출하는 예제입니다.
요구사항
• 스피커의 음량은 절대 100을 넘으면 안 된다. (100을 넘어가면 스피커의 부품들이 고장 난다.)
스피커 객체 설계(클래스)
• 데이터(필드) : 음량
• 기능(메서드) : 음량을 높이고, 내리고, 현재 음량을 확인할 수 있는 단순한 기능
• 요구사항대로 스피커의 음량은 100까지만 증가할 수 있다.
package access;
public class Speaker {
int volume;
Speaker(int volume) {
this.volume = volume;
}
void volumeUp() {
if (volume >= 100) {
System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
return;
}
volume += 10;
System.out.println("음량을 10 증가합니다.");
}
void volumeDown() {
volume -= 10;
System.out.println("음량을 10 감소합니다.");
}
void showVolume() {
System.out.println("현재 음량: " + volume);
}
}
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
설계에 따라 음량은 최대값(100)을 초과하지 않으며, 의도한 대로 잘 동작합니다.
문제점: 필드에 직접 접근 가능
그러나, `Speaker` 클래스의 설계에는 중요한 결함이 있습니다. 외부에서 `volume` 필드에 직접 접근하여 값을 변경할 수 있다는 점입니다. 예를 들어, 아래 코드처럼 `volume` 필드를 직접 수정하면 의도치 않은 값이 설정될 수 있습니다.
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
`volume` 필드가 외부에서 직접 수정 가능하기 때문에, 설계 의도와 다르게 동작하며, 스피커가 고장 나는 등의 문제를 유발할 수 있습니다.
해결 방법: 접근 제어자 활용
이 문제를 해결하려면, `volume` 필드에 `private` 접근 제어자를 적용해야 합니다. 이렇게 하면 클래스 외부에서는 `volume` 필드에 직접 접근할 수 없으며, 메서드를 통해서만 값을 변경하도록 강제할 수 있습니다.
public class Speaker {
private int volume; //private 사용
...
}
`private`는 해당 필드가 선언된 클래스 내부에서만 접근 가능하게 제한합니다. 이로 인해 외부에서의 부적절한 접근이 차단되며, 객체의 무결성을 유지할 수 있습니다.
접근 제어자
이제 본격적으로 접근 제어자를 알아보자. 접근 제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것이며, 자바에서 접근 제어자는 다음과 같이 4가지 종류를 가지게 된다.
- `private` : 클래스 내부에서만 접근 가능하며, 외부 호출은 불가능하다.
- `default` (package-private): 동일한 패키지 안에서는 접근 가능하나, 다른 패키지에서는 접근할 수 없다.
- `protected` : 같은 패키지 안에서는 접근 가능하며, 다른 패키지에서도 상속 관계라면 접근할 수 있다.
- `public` : 모든 패키지, 모든 클래스에서 접근 가능하다.
순서대로 `private` - > `default` -> `protected` -> `public` 허용범위가 늘어나게 된다.
참고로, `defalut`의 경우 위에 첫 예시처럼 접근 제어자를 명시하지 않으면 자동으로 `defalut` 접근 제어자가 적용된다.
접근 제어자는 필드와 메서드, 생성자에 선언가능하며, 추가로 클래스 레벨에도 일부 접근 제어자가 사용가능하다.
접근 제어자의 사용 - 필드, 메서드
필드, 메서드, 생성자 레벨의 접근 제어자는 private, default, protected, public 4가지 모두 사용된다.
객체의 메서드 내부에서는 동일 클래스의 접근 제어자 상관없이 모든 멤버에 접근할 수 있다. 따라서 private로 선언된 멤버라도 제약 없이 접근이 가능하다.
다음 예시로 확인해보자. `access.a`라는 패키지에 여러 접근 제어자 선언된 필드와 메서드들이 있다.
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();
}
}
다음으로 같은 `access.a`패키지 내에서 해당 클래스를 객체화 시켜 테스트를 해보자.
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`으로 선언된 필드와 메서드는 모두 접근이 가능하다. `defalut`의 경우도 같은 패키지 접근이 허용되므로 문제없이 동작한다.
단, `private`선언되 필드와 메서드의 경우 외부 클래스에 접근이 허용안되므로 선언 시 예외가 발생하게 된다.
`innerAccess()`메서드의 경우 `public`으로 선언되어 있으면 호출에는 문제가 없다. 그리고 처음에 설명한 것처럼 클래스 내부 메서드는 클래스 내부의 모든 멤버에 접근이 가능하다.
이제 다른 패키지 내에서 호출시 예제를 확인해보자.
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();
}
}
다른 패키지에서 선언됨에 따라 `defalut`접근 제어자로 선언된 필드와 메서드가 선언이 안되는 것을 확인할 수 있다.
참고로, 생성자도 접근 제어자 관점에서 메서드와 동일하다.
접근 제어자 사용 - 클래스 레벨
위에서 잠깐 언급하거와 같이 클래스 레벨로 왔을 경우 일부 접근 제어자만 사용이 가능하다. `public` , `default` 만 사용이 가능하며 다음과 같은 특징을 갖게 된다.
- `private` , `protected`는 사용할 수 없다.
- `public` 클래스는 반드시 파일명과 이름이 같아야 한다.
- 하나의 자바 파일에 `public` 클래스는 하나만 등장할 수 있다.
- 하나의 자바 파일에 `default` 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.
`public` 클래스는 반드시 파일명과 같아야 한다. 그 이유는 Java가 컴파일 시 각 파일을 단위로 클래스를 생성하기 때문이다. 특히 `public` 클래스는 프로그램의 진입점이 될 가능성이 높은 중요한 클래스이므로, 컴파일러와 JVM이 이를 명확히 찾기 위해 파일 이름과 클래스 이름을 일치시키도록 규칙이 정해져 있다.
반면 `default` 접근 제어자를 사용하는 클래스는 파일 이름과 일치할 필요가 없으므로, 하나의 파일에 여러 개의 `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 {
}
단, 주의할 점은 무한정 생성된 `default` 클래스의 경우, 동일 패키지에서만 접근 가능하며, 다른 패키지에서는 접근이 불가능하다는 것이다.
캡슐화(매우 중요)
캡슐화는 객체 지향 프로그래밍의 핵심 개념 중 하나로, 데이터와 이를 처리하는 메서드를 하나로 묶고 외부 접근을 제한하는 것을 의미한다. 이를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있다.
쉽게 말해, 캡슐화는 속성과 기능을 하나로 묶어 외부에 꼭 필요한 기능만 노출하고, 나머지는 내부로 숨기는 것이다.
접근 제어자를 사용하는 이유는 속성과 기능을 외부로부터 감추기 위함이다. 이런 관점에서 접근 제어자는 캡슐화와 밀접한 관련이 있다. 나아가, 캡슐화를 안전하게 완성할 수 있는 중요한 장치가 바로 접근 제어자다.
1. 데이터를 숨겨라
객체는 속성(데이터)과 기능(메서드)을 가진다. 이 중 캡슐화에서 가장 중요한 원칙은 속성(데이터)을 숨기는 것이다.
예를 들어, Speaker 클래스의 volume 필드를 외부에서 직접 접근할 수 있다면, 클래스 내부 로직을 무시하고 데이터를 변경할 수 있어 안전망이 무너진다. 이는 캡슐화가 깨지는 결과를 초래한다.
우리는 자동차를 운전할 때 엔진 내부를 열어 속도를 직접 조절하지 않는다. 대신, 제공된 엑셀 페달을 밟아 자동차가 알아서 속도를 제어하도록 한다.
마찬가지로, 음악 플레이어를 사용할 때도 내부의 전원부나 볼륨 데이터를 직접 조작하지 않는다. 우리는 단지 버튼을 눌러 전원을 켜거나 볼륨을 조절할 뿐이다. 전원이나 볼륨의 상태를 변경하는 구체적인 작업은 음악 플레이어 내부에서 처리한다.
객체의 데이터는 객체가 제공하는 메서드를 통해서만 접근해야 한다. 이 방식은 데이터 무결성을 유지하고, 객체 내부 구현을 외부로부터 안전하게 보호하는 데 중요한 역할을 한다.
2. 기능을 숨겨라
객체의 기능 중에는 외부에서 사용하지 않고, 내부에서만 필요한 기능들이 있다. 이런 기능은 외부에 노출하지 않고 숨기는 것이 좋다.
예를 들어, 자동차를 운전할 때 복잡한 엔진 조절이나 배기 시스템의 작동 방식을 사용자가 알 필요는 없다. 우리는 단지 엑셀과 핸들 같은 간단한 기능만 사용하면 된다. 만약 이런 내부 기능까지 사용자에게 공개한다면, 사용자는 자동차를 제대로 사용하기 위해 너무 많은 것을 알아야 할 것이다.
사용자 입장에서 꼭 필요한 기능만 외부에 노출하고, 나머지는 모두 내부로 숨기자. 이것이 좋은 캡슐화의 핵심이다. 정리하면, 데이터는 모두 숨기고, 기능은 필요한 최소한만 공개하는 것이 바람직하다.
은행 계좌 기능과 캡슐화
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("유효하지 않은 금액입니다.");
}
}
// public 메서드: withdraw
public void withdraw(int amount) {
if (isAmountValid(amount) && balance - amount >= 0) {
balance -= amount;
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드: getBalance
public int getBalance() {
return balance;
}
// private 메서드: isAmountValid
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());
}
}
BankAccount 클래스는 접근 제어자를 적절히 사용하여 캡슐화를 잘 구현하고 있다. 주요 특징은 다음과 같다:
1. private로 데이터를 보호
- balance 필드: 외부에 직접 노출하지 않음으로써, 데이터의 무분별한 변경을 방지한다. 잔고는 반드시 BankAccount가 제공하는 메서드(deposit, withdraw)를 통해서만 변경할 수 있다.
- isAmountValid() 메서드: 금액 검증은 내부 로직에 필요한 기능이므로 private으로 지정하여 외부에서 호출할 수 없게 했다.
2. public으로 필요한 기능만 노출
- 외부에서 사용할 수 있는 메서드는 deposit(), withdraw(), getBalance() 세 가지로 제한되어 있다. 이로써 BankAccount를 사용하는 개발자는 필요한 기능만 알면 되고, 내부의 복잡한 로직은 신경 쓰지 않아도 된다.
3. 캡슐화를 통해 개발자 혼란 방지
- 만약 isAmountValid()가 외부에 공개된다면, 개발자는 입금이나 출금 전에 이 메서드를 직접 호출해야 하는지 혼란스러워질 것이다.
- 만약 balance가 공개된다면, 외부에서 직접 값을 수정하거나 검증 없이 사용할 가능성이 생긴다. 이는 검증 로직을 무시하고 잔고를 비정상적으로 변경하는 문제를 일으킬 수 있다.
4. 캡슐화의 효과
- 데이터 무결성과 클래스의 책임이 유지된다.
- 외부 사용자의 복잡도를 낮춰 사용 편의성을 높인다.
- 클래스 내부 로직 변경 시에도 외부에 미치는 영향을 최소화할 수 있다.
결론
BankAccount 클래스는 접근 제어자를 활용하여 캡슐화를 구현하고, 데이터를 안전하게 보호하며, 외부 사용자가 필요한 최소한의 기능만 제공하고 있다. 이로써 데이터 무결성을 유지하고, 사용 편의성과 유지 보수성을 높이는 좋은 설계를 보여준다.
참고
'Language > Java' 카테고리의 다른 글
[Java] 자바의 메모리 구조와 Static (스택, 힙, 메서드 영역) (0) | 2025.01.10 |
---|---|
[Java] 생성자가 필요한 이유 (0) | 2025.01.09 |
[Java] null과 NullPointerException (0) | 2025.01.07 |
[Java] 변수의 초기화 (0) | 2025.01.07 |
[Java] 기본형 vs 참조형 (0) | 2025.01.04 |