접근 제어자

계리·2024년 3월 19일
0

접근 제어자란

접근 제어자는 외부에서 어떠한 클래스나 해당 클래스의 멤버 변수나 메서드에 접근하는 것을 제한하는 것을 말한다.


접근 제어자 종류

접근 제어자 종류는 4가지가 있다.

  • public : 모든 외부로부터 호출 가능
  • default : 같은 패키지 내에서 호출 가능
  • protected : 같은 패키지 또는 같은 패키지가 아니여도 상속 관계에서는 호출 가능
  • private : 모든 외부로부터 호출 불가능

가장 많이 차단하는 것이 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패키지에 있어야 한다.
  • public은 모든 접근이 허용되기 때문에 호출이 가능하다.
  • default은 같은 패키지 내에서 접근이 가능하다 했는데 AccessData 클래스와 AccessInnerMain 클래스도 서로 같은 access.a패키지에 있어 호출이 가능하다.
  • privates는 AccessData 클래스 내부에서만 접근이 가능하다. AccessInnerMain 클래스는 외부에 있는 클래스이기 때문에 호출이 불가능하다.
  • innerAccess() 메서드는 public으로 선언되어 있기 때문에 호출이 가능하고 메서드 내부에 있는 private는 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 패키지에 생성한다.
  • public은 모든 접근이 가능하기 때문에 같은 패키지에 있지 않아도 호출이 가능하다.
  • default는 같은 패키지 내에 접근이 가능하다고 했는데 AccessData 클래스는 access.a 패키지, AccessOuterMain 클래스는 access.b 패키지에 있으므로 서로 다른 패키지에 있어 호출이 불가능하다.
  • private는 위와 동일하게 호출이 불가능하다.
  • 참고로 생성자도 접근 제어자 관점에서 메서드와 같다.

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

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

  • 클래스 접근 제어자는 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의 경우 모든 접근이 가능하기 때문에 호출이 가능하다.
  • 클래스 레벨에서 default 또한 같은 패키지에서 접근 하기 때문에 호출이 가능하다.

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도 모든 접근이 가능하기 때문에 다른 패키지여도 호출이 가능하다.
  • 클래스 레벨에서 default도 같은 패키지에서만 호출이 가능한데 현재 PublicClassOuterMain 클래스는 다른 패키지에 생성이 됐으므로 호출이 불가능하다.

캡슐화

객체지향 프로그래밍 개념 중 하나인 캡슐화는 해당 클래스에서 선언된 필드나 메서드를 외부로부터 접근하지 못하게 하나로 묶어 감싸고 있다는 표현하여 캡슐화라고 한다.

이렇게 접근 제어자를 통해 캡슐화하여 데이터를 직접적으로 변경을 제한하거나 방지할 수 있다. 외부로부터 사용해야 할 데이터나 기능들은 노출을 시켜주어 사용하면 된다.

  1. 데이터를 숨겨라
    데이터(속성) 같은 경우는 필수로 숨겨야 한다. 해당 데이터에 직접적으로 접근하게 되면 클래스에서 다루는 로직을 무시하고 데이터를 변경하게 될 수 있기 때문이다. 예를 들면 위에 예시 중 Speaker 클래스의 volume 속성 같은 경우 직접적으로 접근하여 클래스 로직에 제한을 둔 로직이 있지만 그 제한을 무시하고 볼륨 값을 올릴 수 있었던 예시가 있다. Speaker 클래스로 예시를 둔 상황처럼 나오지 않게 하려면 객체가 제공하는 기능(메서드)을 통해서 접근해야 한다.

  2. 기능을 숨겨라
    외부로부터 사용하지 않는 기능은 숨겨야 한다. 굳이 외부에서 사용하지 않을 기능까지 제공하게 되면 오히려 복잡해질 수 있기 때문에 내부에서만 사용하는지, 외부에서도 사용하는지 잘 알아보고 제공하는 것이 좋다.


캡슐화가 잘 되어 있는 객체

은행 계좌 기능 예시로 확인해보자.

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메서드를 노출시키면 어떻게 될까? 다른 개발자가 해당 메서드 호출이 가능하다는걸 보면 이 메서드로 검증 로직을 넣어야 하는 것인가? 라고 의문이 생길 수 있다. 의문이 들면 해당 메서드를 찾아가보게 되고 해당 메서드를 확인해보니 해당 메서드에서 이미 검증을 하고 있는 것을 확인하게 된다.

이렇게 이미 해당 메서드가 검증을 하고 있는데 노출을 시켜 다른 개발자에게 혼동을 주는 상황이 나올 수 있다. 그러므로 노출 시키지 않아도 되는 기능들은 캡슐화하여 데이터나 기능들을 안전하게 보호하고 다른 개발자들에게는 복잡도를 낮출 수가 있다.


참고

  • 김영한의 실전 자바 기본편
profile
gyery

0개의 댓글