본문 바로가기
코딩

[ Java ] 실전자바-기본편 6. 접근 제어자

by 모두의 아카이브 2024. 2. 14.
반응형

접근 제어자 이해 - 1 

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

 

필요한 이유

스피커 객체를 만들어볼껀데, 이때 스피커의 음량은 절대로 100을 넘으면 안되는 요구사항이 있다.
(100을 넘어가면 스피커의 부품들이 고장난다.)

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);
 	}
}

생성자를 통해 초기 음량값을 지정할 수 있다.

volumeUp() 메서드를 보자. 음량을 한번에 10씩 증가한다. 단 음량이 100을 넘게되면 더는 음량을 증가하지 않는다.

 

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 클래스를 보니 volume 필드를 직접 사용할 수 있었다. 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 필드 직접 접근 수정

출처 -김영한 실전자바-기본편

 

Speaker 객체를 사용하는 사용자는 Speaker 의 volume 필드와 메서드에 모두 접근할 수 있다.

앞서 volumeUp() 과 같은 메서드를 만들어서 음량이 100을 넘지 못하도록 기능을 개발했지만 소용이 없다.

왜냐하면 Speaker 를 사용하는 입장에서는 volume 필드에 직접 접근해서 원하는 값을 설정할 수 있기 때문이다.

이런 문제를 근본적으로 해결하기 위해서는 volume 필드의 외부 접근을 막을 수 있는 방법이 필요하다.

 

접근 제어자 이해 - 2

이 문제를 근본적으로 해결하는 방법은 volume 필드를 Speaker 클래스 외부에서는 접근하지 못하게 ㅁ가는것이다.

 

private

volume 접근 제어자를 private로 수정

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

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

 

출처 -김영한 실전자바-기본편

 

그림을 보면 volume 필드를 private을 사용해서 Speaker 내부에 숨겼다.

volume필드는 이제 Speaker 내부에서만 접근할 수 있다.

 

좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약을 제공하는 프로그램이다.

접근 제어자 종류

  • private : 모든 외부 호출을 막는다.
  • default (package-private): 같은 패키지안에서 호출은 허용한다.
  • protected : 같은 패키지안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
  • public : 모든 외부 호출을 허용한다

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

private -> default -> protected -> public

 

package-private

접근제어자를 명시하지 않으면 default 접근 제어자가 적용된다.

 

접근 제어자 사용 위치

접근 제어자는 필드와 메서드,생성자에 사용, 추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있다.

 

접근 제어자 예시

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();
 }
}
```

 

외부에세 이 클래스 접근

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();
 }
}
  • 패키지 위치는 package access.a 이다. 패키지 위치를 꼭 맞추어야 한다. 주의하자.
  • public 은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근 가능하다.
  • default 는 같은 패키지에서 접근할 수 있다. AccessInnerMain 은 AccessData 와 같은 패키지이다.
    따라서 default 접근 제어자에 접근할 수 있다.
  • private 은 AccessData 내부에서만 접근할 수 있다. 따라서 호출 불가다.
  • AccessData.innerAccess() 메서드는 public 이다. 따라서 외부에서 호출할 수 있다. innerAccess() 메서드는 외부에서 호출되었지만 innerAccess() 메서드는 AccessData 에 포함되어 있다. 이 메서드는 자신의 private 필드와 메서드에 모두 접근할 수 있다

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

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

  • 클래스 레벨의 접근 제어자는 public,default만 사용 가능
    • private, protected는 사용 불가
  • 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 {
}
  • PublicClass 라는 이름의 클래스를 만들었다. 이 클래스는 public 접근 제어자다. 따라서 파일명과 이 클래스의 이름이 반드시 같아야 한다. 이 클래스는 public 이기 때문에 외부에서 접근할 수 있다.
  • DefaultClass1 , DefaultClass2 는 default 접근 제어자다. 이 클래스는 default 이기 때문에 같은 패키지 내부에서만 접근할 수 있다
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();
 }
}
  • PublicClass 는 public 클래스이다. 따라서 외부에서 접근할 수 있다.
  • PublicClassOuterMain 와 DefaultClass1 , DefaultClass2 는 다른 패키지이다. 따라서 접근할 수 없다

캡슐화

캡슐화는 데이터와 해당 테이터를 처리하는 메서드를 하나로 묶어서 외부에서의 제한하는 것을 말한다.

캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는
것이다.

 

1. 데이터를 숨겨라

객체 내부의 테이터를 외부에서 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있다.

 

ex) 자동차를 운전할 때 자동차 부품을 열어서 그 안에 있는 속도계를 직접 조절하지 않는다. 단지 자동차가 제공 하는 엑셀 기능을 사용해서 엑셀을 밟으면 자동차가 나머지는 다 알아서 하는 것이다.

 

객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.

 

2. 기능을 숨겨라

객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들은 모두 감추는 것이 좋다.

ex) 우리가 자동차를 운전하기 위해 자동차가 제공하는 복잡한 엔진 조절기능, 배기 기능까지 우리가 알 필요는 없다.

우리는 엑셀과 핸들 정도의 기능만 알면 된다.

 

캡슐화 예제

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());
 }
}

 

해당 코드는 은행 계좌 기능을 다룬다.

private

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

public

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

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

 

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

 

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

 

반응형