접근 제한자 (public, private, protected, default)
자바에는 클래스, 메서드 필드(변수), 생성자의 접근 제한을 위해 사용한 접근 제한자(public, private, protected, default)가 있다.
1. public
적용 범위 : 클래스, 메서드, 필드, 생성자
설명 : public으로 선언된 멤버는 어디서든 접근할 수 있다. 즉, 모든 클래스에서 해당 멤버에 접근할 수 있다. 일반적으로 공개 API를 구성하는데 사용된다.
// PublicClass.java 파일
public class PublicClass {
// Public 메서드
public void show() {
System.out.println("PublicClass의 show 메서드 호출됨.");
}
// Public 정적 메서드
public static void staticShow() {
System.out.println("PublicClass의 정적 staticShow 메서드 호출됨.");
}
}
이 예시에서 PublicClass 클래스와 그 안의 show() 메서드는 public으로 선언되어 있어서, 이 클래스와 메서드는 어디서든 접근할 수 있다. 이는 프로젝트 내의 다른 패키지에서도 이 클래스의 인스턴스를 생성하고, 이 메서드를 호출할 수 있음을 의미한다.
// 다른 패키지에 속한 AnotherClass.java 파일
import packageName.PublicClass;
public class AnotherClass {
public static void main(String[] args) {
PublicClass publicObject = new PublicClass();
publicObject.show(); // 다른 패키지에서 public 메서드 호출
PublicClass.staticShow(); // 정적 메서드 호출
}
}
AnotherClass는 PublicClass를 import하여 PublicClass의 인스턴스를 생성하고, show() 메서드와 staticShow() 정적 메서드를 호출한다. PublicClass와 그 메서드들이 public으로 선언되었기 때문에, 다른 패키지에서도 접근할 수 있다.
2. private
적용 범위 : 메서드, 필드
설명 : private으로 선언된 멤버는 해당 클래스 내부에서만 접근할 수 있다. 다른 클래스에서는 접근할 수 없다. 이를 통해 클래스의 내부 구현을 숨기고, 외부에서의 불필요한 접근을 방지하여 캡슐화를 강화할 수 있다.
public class BankAccount {
// private 필드: 계좌 잔액
private double balance;
// 생성자
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// private 메서드: 잔액 업데이트
private void updateBalance(double amount) {
balance += amount;
}
// 공개 메서드: 입금
public void deposit(double amount) {
if (amount > 0) {
updateBalance(amount);
System.out.println("입금 완료: " + amount);
} else {
System.out.println("입금 금액이 유효하지 않습니다.");
}
}
// 공개 메서드: 출금
public void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
updateBalance(-amount);
System.out.println("출금 완료: " + amount);
} else {
System.out.println("출금 금액이 유효하지 않거나 잔액이 부족합니다.");
}
}
// 공개 메서드: 잔액 조회
public double getBalance() {
return balance;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000); // 초기 잔액 1000
account.deposit(500); // 500 입금
account.withdraw(200); // 200 출금
System.out.println("현재 잔액: " + account.getBalance()); // 잔액 조회
}
}
이 예시에서 BankAccount 클래스는 balance라는 private 필드를 가지고 있다. 이 필드는 계좌의 잔액을 나타내며, 클래스 외부에서 직접 접근할 수 없다. 대신, 잔액을 변경하기 위해 deposit(double amount)와 withdraw(double amount) 같은 공개 메서드를 통해 간접적으로 접근할 수 있다.
또한, updateBalance(double amount) 메서드는 private으로 선언되어 있어, 이 메서드는 BankAccount 클래스 내부에서만 호출할 수 있다. 이 메서드는 deposit과 withdraw 메서드에 의해 잔액을 업데이트하는 데 사용되며 간접 접근만이 가능합니다.
이와 같이 private 메서드와 필드를 사용함으로써, BankAccount 클래스는 내부 상태와 행위를 외부로부터 보호하고, 사용자가 클래스를 예상 가능하고 안전하게 사용할 수 있도록 인터페이스를 제공한다.
3. protected
적용 범위 : 메서드, 필드, 생성자
설명 : protected로 선언된 멤버는 같은 패키지 내의 다른 클래스 또는 다른 패키지에 속한 자식 클래스에서 접근할 수 있다. 이는 주로 상속 구조에서 부모 클래스의 멤버를 자식 클래스가 사용할 수 있도록 하기 위해 사용된다.
public class ParentClass {
// protected 필드
protected String name;
// protected 생성자
protected ParentClass(String name) {
this.name = name;
}
// protected 메서드
protected void display() {
System.out.println("이름: " + name);
}
}
public class ChildClass extends ParentClass {
// 자식 클래스의 생성자
public ChildClass(String name) {
super(name); // 부모 클래스의 protected 생성자 호출
}
// 부모 클래스의 protected 메서드를 오버라이드
@Override
protected void display() {
System.out.println("자식 클래스에서의 이름: " + name);
super.display(); // 부모 클래스의 protected 메서드 호출
}
public static void main(String[] args) {
ChildClass child = new ChildClass("Java");
child.display(); // 오버라이드된 메서드 호출
}
}
이 예시에서는 ParentClass라는 부모 클래스가 protected 접근 제한자를 사용하여 name 필드, 생성자, 그리고 display 메서드를 선언하고 있다. ChildClass는 이 ParentClass를 상속받아 ParentClass의 name 필드와 display 메서드에 접근할 수 있다. ChildClass에서는 display 메서드를 오버라이드하여 부모 클래스의 display 메서드를 호출하고 있으며, 이를 통해 상속과 메서드 오버라이딩을 통한 기능 확장이 어떻게 이루어지는지 보여준다.
protected 접근 제한자는 이처럼 상속 관계에서 부모 클래스의 멤버를 보호하면서도 자식 클래스가 이를 사용하고 확장할 수 있도록 하는 데 유용하다. 위에서 언급했듯이, 같은 패키지 내의 다른 클래스에서의 접근 역시 유용하다.
4. default (package-private)
적용 범위 : 클래스, 메서드, 필드, 생성자
설명 : 명시적인 접근 제한자를 사용하지 않은 경우, 자바는 기본적으로 default 접근 제한자를 적용한다. 이는 같은 패키지 내의 클래스들만 접근할 수 있게 한다. 다른 패키지에서는 접근할 수 없다. 이를 통해 패키지 내부에서만 사용되는 내부 클래스나 멤버를 정의할 때 사용된다.
package mypackage;
// default 접근 제한자를 사용하는 클래스
class MyClass {
// default 접근 제한자를 사용하는 필드
int defaultNumber;
// default 접근 제한자를 사용하는 메서드
void display() {
System.out.println("Default Access: " + defaultNumber);
}
}
package mypackage;
public class SamePackageClass {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.defaultNumber = 10;
myClass.display(); // 같은 패키지 내에서 접근 가능
}
}
이 예시에서 MyClass와 그 멤버들은 default 접근 제한자를 사용하므로, mypackage 패키지 내의 SamePackageClass에서는 MyClass의 인스턴스를 생성하고, 그 필드와 메서드에 접근할 수 있다.
그런데 가만히 살펴보면, 클래스는 private과 protected를 사용하지 못하고, 생성자는 private을 사용하지 못한다.
그 이유가 무엇일까?
먼저 클래스에 대해 정확히 말하자면 아예 사용하지 못하는게 아니다. 내부 클래스만 사용이 가능하다.
최상위 클래스는 다른 클래스들이 이를 참조할 수 있어야 하므로, public 또는 기본 접근 제한자를 사용하여 다른 패키지 또는 같은 패키지의 클래스들이 이에 접근할 수 있게 한다. 최상위 클래스에 private 또는 protected를 사용하면, 이 클래스를 다른 클래스에서 접근할 수 없게 되어, 클래스의 사용성이 매우 제한적이 된다.
내부 클래스의 경우, private, protected, public, 기본 접근 제한자 모두 사용할 수 있다. 내부 클래스에 대해서 private 또는 protected 접근 제한자를 사용하는 것은, 해당 내부 클래스를 감싸고 있는 외부 클래스 내부에서만 사용하거나, 상속받는 클래스에서만 사용하도록 제한하기 위함이다. 이러한 접근 제한자는 내부 클래스의 캡슐화와 정보 은닉을 강화하는 데 도움을 준다.
(최상위 클래스에 대해서 private과 protected는 애초에 문법상 허용하지 않는다)
class OuterClass {
// private 내부 클래스 정의
private class PrivateInnerClass {
private String secret = "내부 비밀 메시지";
private void showSecret() {
System.out.println("PrivateInnerClass의 비밀: " + secret);
}
}
public void accessInner() {
// 내부 클래스의 인스턴스 생성 및 메서드 호출
PrivateInnerClass inner = new PrivateInnerClass();
inner.showSecret();
}
}
public class Main {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.accessInner(); // 외부 클래스를 통해 내부 클래스의 메서드에 접근
}
}
이 예시에서 OuterClass는 PrivateInnerClass 인스턴스를 생성하고, 그 인스턴스의 showSecret 메서드를 호출하는 accessInner 메서드를 제공한다. 이 방식으로 OuterClass는 자신의 private 내부 클래스 PrivateInnerClass에 대한 완전한 제어를 유지하면서, 필요한 기능을 외부에 노출할 수 있다.
특정 내부 클래스의 구현 세부 사항을 외부로부터 숨기고 싶을 때 유용하게 사용된다. 이를 통해 클래스의 내부 구현을 캡슐화하고, 외부에서의 불필요한 접근을 제한하여 안정성과 유지보수성을 높일 수 있다.
다음으로, 생성자에 대해 정확히 말하자면 private을 사용할 수는 있다. 하지만 특정한 디자인 패턴과 목적에 한정되기 때문에 일반적인 클래스 설계에서는 그다지 흔하지 않다.
주요 사용 이유
싱글톤 패턴: 클래스의 인스턴스가 하나만 생성되어야 할 때 사용된다. 싱글톤 패턴을 구현하는 클래스는 private 생성자를 통해 외부에서의 인스턴스 생성을 막고, 내부적으로 자신의 인스턴스를 생성하여 관리한다. 이 패턴은 전역 상태를 관리하거나 공유 리소스에 대한 접근을 제어하는 데 유용하다.
유틸리티 클래스: 인스턴스화할 필요가 없는 정적 메서드만을 포함하는 클래스다. private 생성자를 사용하여 유틸리티 클래스의 인스턴스화를 방지함으로써, 클래스가 의도한 용도로만 사용되도록 한다.
사용이 드문 이유
한정된 사용 사례: 대부분의 클래스는 여러 인스턴스를 생성할 수 있도록 설계되며, 객체 지향 프로그래밍의 핵심 원칙 중 하나는 인스턴스화를 통해 객체의 상태와 행동을 캡슐화하는 것이다. private 생성자는 이러한 원칙과는 다른, 특정한 목적(예: 싱글톤, 유틸리티 클래스)을 가진 경우에만 사용된다.
객체 지향 설계와의 충돌: 객체 지향 프로그래밍은 상속, 다형성, 캡슐화 등을 통해 코드의 재사용성, 확장성, 유지보수성을 높이는 것을 목표로 한다. private 생성자는 클래스의 확장과 인스턴스 생성을 제한함으로써, 이러한 목표와는 어느 정도 상충될 수 있다.
유연성 제한: private 생성자를 사용하는 클래스는 테스트하기 어려울 수 있으며, 다른 디자인 패턴이나 설계 요구사항에 적응하기 어려울 수 있다. 예를 들어, 싱글톤 클래스는 테스트에서 모의 객체(mock objects)를 사용하거나 다양한 설정으로 인스턴스를 생성하는 것이 어려울 수 있다.
결론적으로, private 생성자는 특정한 사용 사례와 디자인 패턴에는 매우 유용할 수 있지만, 일반적인 클래스 설계에서는 그 사용이 제한적이다.
아래는 싱글톤과 유틸리티 클래스의 간략한 예시다.
public class SingletonExample {
// 싱글톤 인스턴스를 저장할 private static 변수
private static SingletonExample instance;
// 생성자를 private으로 선언하여 외부에서의 인스턴스화를 방지
private SingletonExample() {}
// 인스턴스에 접근하기 위한 public static 메서드
public static SingletonExample getInstance() {
if (instance == null) {
instance = new SingletonExample();
}
return instance;
}
public void displayMessage() {
System.out.println("Hello from SingletonExample!");
}
public static void main(String[] args) {
SingletonExample singleton = SingletonExample.getInstance();
singleton.displayMessage();
}
}
public class UtilityClass {
// 생성자를 private으로 선언하여 인스턴스화 방지
private UtilityClass() {
throw new AssertionError("Utility class cannot be instantiated");
}
// 유틸리티 메서드는 static으로 선언
public static void utilityMethod() {
System.out.println("Utility method called");
}
public static void main(String[] args) {
UtilityClass.utilityMethod(); // 클래스 이름으로 직접 메서드 호출
}
}
최종적으로 정리하자면 아래와 같다.
Public : 프로젝트 전체 접근 가능
Protected : 같은 패키지 내의 다른 클래스 또는 다른 패키지에 속한 자식 클래스에서 접근 가능
default : 같은 패키지 내의 클래스들만 접근 가능
Private : 해당 접근 제한자를 포함하는 클래스 내에서만 접근 가능 (가장 가까운 중괄호 안에서만 가능)
(접근 영역 : Public > Protected > default > Private)
Public | Private | Protected | default | |
클래스 | O | O (단, 내부 클래스만) | O (단, 내부 클래스만) | O |
메서드 | O | O | O | O |
필드(변수) | O | O | O | O |
생성자 | O | O (단, 싱글톤 패턴, 유틸리티 클래스만) | O | O |