[JAVA]_Access Modifier(접근제어자)
1. 접근제어자
접근제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있음.
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);
}
}
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
[Speaker, SpeakerMain]
생성자를 통해 초기 음량값을 지정 가능.
volumeUp() 메스드를 보면 음량을 한 번에 10씩 증가하고, 음량이 100을 넘게 되면 더는 음량을 증가하지 않음.
초기 음량 값을 90으로 지정하고, 음량 높이는 메서드를 여러 번 호출했음. 기대한 대로 음량을 100을 넘지 않음.
프로젝트는 성공한 것으로 볼 수 있음.
만약 오랜 시간이 흘러 업그레이드된 다음 버전의 스피커를 출시하게 되었음. 이 때는 새로운 개발자가 급하게 기존 코드를 이어받아 개발을 하게 되고, 참고로 새로운 개발자는 기존 요구사항을 잘 몰랐다고 가정하자.
음량이 100이상 올라가지 않아서 소리를 더 올리면 좋겠다고 생각하여 volume 필드의 값을 200으로 설정하고 이 코드를 실행했더니 순간 스피커의 부품들에 과부하가 걸리면서 폭발했음.
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
Speaker 객체를 사용하는 사용자는 Speaker의 volume 필드와 메서드에 모두 접근 가능.
앞서 volumeUp()과 같은 메서드를 만들어 음량이 100을 넘지 못하도록 기능을 개발했지만 소용이 없었음. 그 이유는 Speaker를 사용하는 입장에서는 volume 필드에 직접 접근해서 원하는 값을 설정할 수 있었기 때문.
이런 문제를 근본적으로 해결하기 위해서는 volume 필드의 외부 접근을 막을 수 있는 방법이 필요.
package access;
public class Speaker {
private 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);
}
}
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; // private 접근 제어로 인해 접근 불가능.
speaker.showVolume();
}
}
volume 접근 제어자를 private으로 수정.
private 접근 제어자는 모든 외부 호출을 막음. 따라서 private이 붙은 경우 해당 클래스 내부에서만 호출 가능.
SpeakerMain을 다시 실행해 보면 speaker.volume = 200 부분에 오류가 발생하는 것을 확인 가능.
volume 필드는 private으로 설정되었기 때문에 외부에서 접근할 수 없다는 요류임.
따라서 오류가 발생하므로 주석으로 처리함.
※ 참고 ※
좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아닌 적절한 제약을 제공하는 프로그램임.
○ 접근 제어자 종류 ○
- private : 모든 외부 호출을 막음. 내부 클래스에서만 사용 가능.
- default (package-private) : 같은 패키지 안에서 호출은 허용 가능.
- protected : 같은 패키지 안에서 호출은 허용함. 패키지가 달라도 상속 관계의 호출은 허용.
- public : 모든 외부 호출 허용 가능.
순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용함.
참고로 protected는 상속 관계에서 자세히 설명.
▶ package-private
접근 제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default 접근 제어자가 적용.
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은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개.
접근제어자 사용_필드, 메서드
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.AccessData]
패키지 위치는 package access.a (패키지 위치 맞추기. 주의!!)
순서대로 public, default, private 필드와 메서드에 사용.
마지막 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();
}
}
실행결과
publicMethod 호출 1
defaultMethod 호출 2
내부 호출
publicMethod 호출 100
defaultMethod 호출 200
privateMethod 호출 300
[AccessInnerMain]
패키지 위치는 package access.a
public은 모든 접근 허용하기 때문에 필드, 메서드 모두 접근 가능.
default는 같은 패키지에 접근 가능하므로 접근 가능. (AccessInnerMain과 AccessData는 같은 패키지)
private은 AccessDate 내부에서만 접근 가능하므로 호출 불가.
AccessData.innerAccess() 메서드는 public이므로 외부에서 호출 가능. 메서드는 외부에서 호출한 거지만 이 메서드는 AccessData에 포함되어 있기 때문에 자신의 private 필드와 메서드에 모두 접근 가능.
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.AccessOuterMain]
패키지위치는 package access.b
public은 모든 접근 허용하기 때문에 필드, 메서드 모두 접근 가능.
default는 같은 패키지에서 접근 가능하므로 access.b.AccessOuterMain은 access.a.AccessData와 다른 패키지이므로 접근 불가능.
priavate는 AccessData 내부에서만 접근 가능하므로 호출 불가능.
AccessData.innerAccess() 메서드는 public이므로 외부에서 호출 가능. 메서드는 외부에서 호출한 거지만 이 메서드는 AccessData에 포함되어 있기 때문에 자신의 private 필드와 메서드에 모두 접근 가능.
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 {
}
[access.a.PublicClass]
클래스 레벨의 접근 제어자 규칙
- 클래스 레벨의 접근 제어자는 public, default만 사용 가능 → private, protected는 사용 불가능
- pubilc 클래스는 반드시 파일명과 이름이 같아야 함.
- 하나의 자바 파일에는 public 클래스는 하나만 등장 가능.
- 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 생성 가능.
패키지 위치는 package access.a
PublicClass라는 이름의 클래스 생성. 이 클래스는 public 접근 제어자이므로 파일명과 클래스 이름이 같아야 하고, public이므로 외부에서 접근 가능.
DefaultClass1, DefaultClass2는 default 접근 제어자이므로, 같은 패키지 내부에서만 접근 가능.
Public Class의 main()을 보면 PublicClass는 public 접근 제어자이므로 어디서든 사용 가능하고, DefaultClass1과 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();
}
}
[access.a.PublicClassInneMain]
패키지 위치는 package access.a
PublicClass는 public 클래스이므로 외부에서 접근 가능
PublicClassInnerMain은 DefaultClass1과 DefaultClass2와 같은 패키지이므로 접근 가능.
package access.b;
/*import access.a.DefaultClass1;
import access.a.DefaultClass2;*/
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();
}
}
[access.b.PublicClassOuterMain]
패키지 위치는 package access.b
PublicClass는 public이므로 외부에서 접근 가능.
PublicClassOuterMain은 DefaultClass1과 DefaultClass2와 다른 패키지이므로 접근 불가능.
○ 캡슐화 ○
캡슐화(Encapsulation)은 객체 지향 프로그래밍의 중요한 개념 중 하나.
데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말함. 캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있음.
쉽게 말해 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것.
어떤 것을 숨기고 어떤 것을 노출해야 하는지 알아보자.
1. 데이터를 숨겨라
객체에는 속성(데이터)과 기능(메서드)이 있음. 캡슐화에서 가장 필수로 숨겨야 하는 것이 속성(데이터) 임.
Speaker 클래스에서 필드 volume을 떠올려보면 객체 내부의 데이터를 외부에서 함부로 접근 시, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터 변경이 가능함. 결국 모든 안정망을 다 빠져나가게 되고, 캡슐화가 깨짐.
자동차를 운전하거나 음악 플레이어를 사용할 때 그 내부에 들어있는 것을 일일이 수정하지 않음. 그 기능을 사용할 뿐임. 쉽게 말해 우리는 제공하는 기능을 통해서 사용하는 것이지, 그 내부를 까서 데이터까지 우리가 직접 사용하는 것은 아님.
객체의 데이터는 객체가 제공하는 기능인 메서드를 통해 접근해야 함.
2. 기능을 숨겨라
객체의 기능 중 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있음. 이런 기능도 모두 감추는 것이 좋음.
예를 들어 자동차를 운전하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리가 알 필요가 없고 우리는 엑셀과 핸들 정도의 기능만 알면 됨. 사용자에게 이런 기능까지 모두 알려주면 사용자는 자동차에 대해 너무 많은 것을 알아야 함.
따라서 사용자 입장에서 꼭 필요한 기능만 외부에 노출하고 나머지 기능은 모두 내부로 숨기는 것이 좋음.
정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화임.
package access;
public class BankAccount {
private int balance;
public BankAccount() {
this.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 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, BankAccountMain]
은행 계좌 기능을 다룸.
▶ private
- balance : 데이터 필드는 외부에 직접 노출하지 않음. BankAccount가 제공하는 메서드를 통해서만 접근 가능
- isAmountValid() : 입력 금액을 검증하는 기능은 내부에서만 필요하기 때문에 private 접근 제어자 사용.
▶ public
- deposit() : 입금
- withdraw() : 출금
- getBalance() : 잔고
BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 되고, 나머지 복잡한 내용은 모두 BankAccount 내부에 숨어 있음.
접근제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론, BankAccount를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도를 낮출 수 있음.
1-1 접근제어자 문제
[MaxCounter, CounterMain]
MaxCounter 클래스 생성 : 이 클래스는 최댓값 지정하고 최댓값까지만 값이 증가하는 기능 제공
- int count : 내부에서 사용하는 숫자. 초기값은 0
- int max : 최대값. 생성자 통해 입력.
- increment() ; 숫자 하나 증가
- getCount() : 지금까지 증가한 값을 반환
요구사항
- 접근 제어자를 사용해서 데이터 캡슐화.
- 해당 클래스는 다른 패키지에서도 사용할 수 있어야 함.
package access.ex;
public class CounterMain {
public static void main(String[] args) {
MaxCounter counter = new MaxCounter(3);
counter.increment();
counter.increment();
counter.increment();
counter.increment();
int count = counter.getCount();
System.out.println(count);
}
}
실행결과
최대값을 초과할 수 없습니다.
3
package access.ex;
public class MaxCounter {
private int count = 0;
private int max;
public MaxCounter(int max) {
this.max = max;
}
public void increment() {
if(count >= max) {
System.out.println("최대값을 초과할 수 없습니다.");
return;
}
count++;
}
public int getCount() {
return count;
}
}
[Item, ShoppingCart, ShoppingCartMain]
요구사항
- 접근제어자를 사용해서 데이터를 캡슐화 하기.
- 해당 클래스는 다른 패키지에서도 사용할 수 있어야 함.
- 장바구니에는 상품을 최대 10개만 담기. 10개 초과 등록 시 "장바구니가 가득 찼습니다." 출력.
package access.ex;
public class ShoppingCartMain {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item item1 = new Item("마늘", 2000, 2);
Item item2 = new Item("상추", 3000, 4);
cart.addItem(item1);
cart.addItem(item2);
cart.displayItems();
}
}
실행결과
장바구니 상품 출력
상품명 : 마늘, 합계 : 4000
상품명 : 상추, 합계 : 12000
전체 가격 합 : 16000
package access.ex;
public class Item {
private String name;
private int price;
private int quantity;
public Item(String name, int price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public String getName() {
return name;
}
public int getTotal() {
return price * quantity;
}
}
[Item]
각각의 Item의 가격과 수량을 곱하면 각 상품별 합계를 구할 수 있음.
price와 quantity를 외부에 반환한 다음에 외부에서 곱해서 상품별 합계를 구해도 되지만, getTotal() 메서드를 제공하면 외부에서 단순히 이 메서드를 호출하면 됨. 이 메서드의 핵심은 자신이 가진 데이터를 사용한다는 점.
package access.ex;
public class ShoppingCart {
private Item[] items = new Item[10];
private int itemCount;
public void addItem(Item item) {
if(itemCount >= items.length) {
System.out.println("장바구니가 가득 찼습니다.");
return;
}
items[itemCount] = item;
itemCount++;
}
public void displayItems() {
System.out.println("장바구니 상품 출력");
for(int i = 0; i < itemCount; i++) {
Item item = items[i];
System.out.println("상품명 : " + item.getName() + ", 합계 : " + item.getTotal());
}
System.out.println("전체 가격 합 : " + calculateTotal());
}
private int calculateTotal() {
int totalPrice = 0;
for(int i = 0; i < itemCount; i++) {
Item item = items[i];
totalPrice += item.getTotal();
}
return totalPrice;
}
}
[ShoppingCart]
여기서 calculationTotal() 메서드는 내부에서만 사용되므로 public이 아닌 private으로 접근제어자 사용함.