Skip to Content

이번 문서의 목표: 캡슐화가 “왜” 필요한지 설계 관점에서 설명하고, 네 가지 접근 제어자(public, protected, default, private)가 클래스·패키지·상속 관계에서 각각 어디까지 접근을 허용하는지 표와 코드로 정확히 구분할 수 있게 된다.

왜 필드를 숨기는가

클래스를 설계할 때 모든 필드를 public으로 열어 두면 어떤 일이 생길까요? 예를 들어 은행 계좌 클래스의 잔액(balance) 필드가 public이라면, 클래스 바깥의 어떤 코드든 account.balance = -1000000;처럼 검증 없이 직접 값을 바꿀 수 있습니다. 잔액이 음수가 되어서는 안 된다는 규칙을 클래스가 강제할 방법이 없어지는 것입니다.

캡슐화(encapsulation, 캡슐화)는 객체의 내부 데이터(필드)를 직접 건드리지 못하게 감추고(정보 은닉, information hiding), 오직 그 클래스가 제공하는 메서드를 통해서만 데이터에 접근하거나 바꾸도록 강제하는 설계 원칙입니다. 03편에서 이미 객체지향 4대 특성 중 하나로 소개했는데(자세한 내용은 03편), 이번 편에서는 이를 실제로 구현하는 문법인 접근 제어자(access modifier, 접근 제어자)를 깊게 다룹니다.

쉽게 말하면: 캡슐화는 “약 상자에 뚜껑을 덮고 정해진 창구로만 약을 내주는 것”입니다. 아무나 상자를 열어 마음대로 약을 꺼내 가게 두지 않고, 정해진 절차(메서드)를 거치게 합니다.

1. 접근 제어자 네 가지

Java는 클래스, 필드, 메서드, 생성자 앞에 붙일 수 있는 접근 제어자 네 가지를 제공합니다. 이 중 public, protected, private은 키워드를 직접 쓰고, 아무것도 쓰지 않으면 네 번째인 default(package-private, 디폴트)가 적용됩니다.

접근 제어자같은 클래스같은 패키지다른 패키지의 자식 클래스다른 패키지의 무관한 클래스
private허용불가불가불가
default(생략)허용허용불가불가
protected허용허용허용불가
public허용허용허용허용

해석: 표를 왼쪽에서 오른쪽으로 읽으면 허용 범위가 점점 넓어집니다. private이 가장 좁고 public이 가장 넓습니다. 시험에서 자주 헷갈리는 지점은 default와 protected의 차이인데, 두 접근 제어자는 “같은 패키지”까지는 동일하게 허용하지만, 다른 패키지에 있는 자식 클래스까지 허용하느냐가 갈림길입니다. default는 패키지 경계를 절대 넘지 못하고, protected는 상속 관계라면 패키지를 넘어서도 접근을 허용합니다.

2. private 필드 + public getter/setter — 표준 캡슐화 패턴

가장 흔한 캡슐화 패턴은 필드를 private으로 감추고, 값을 읽는 getter(게터, 접근자 메서드)와 값을 바꾸는 setter(세터, 설정자 메서드)를 public으로 제공하는 것입니다. 이렇게 하면 값을 바꾸는 유일한 통로가 setter가 되므로, setter 안에 검증 로직을 넣어 잘못된 값이 들어오는 것을 막을 수 있습니다.

public class BankAccount { private String owner; private long balance; // private: 클래스 바깥에서 직접 접근 불가 public BankAccount(String owner, long balance) { this.owner = owner; this.balance = balance; } // getter: 값을 읽기만 허용 public long getBalance() { return balance; } // setter: 값을 바꾸되, 검증을 거쳐야만 허용 public void deposit(long amount) { if (amount <= 0) { System.out.println("입금액은 0보다 커야 합니다. 요청 거부: " + amount); return; } balance += amount; System.out.println(owner + "님 입금 완료. 현재 잔액: " + balance); } public void withdraw(long amount) { if (amount > balance) { System.out.println("잔액 부족으로 출금 거부. 요청액: " + amount + ", 잔액: " + balance); return; } balance -= amount; System.out.println(owner + "님 출금 완료. 현재 잔액: " + balance); } public static void main(String[] args) { BankAccount acc = new BankAccount("지훈", 10000); acc.deposit(5000); acc.withdraw(50000); // 잔액 초과 시도 acc.deposit(-100); // 음수 입금 시도 // acc.balance = -999999; // private이라 컴파일 오류(주석 처리) System.out.println("최종 잔액: " + acc.getBalance()); } }
지훈님 입금 완료. 현재 잔액: 15000 잔액 부족으로 출금 거부. 요청액: 50000, 잔액: 15000 입금액은 0보다 커야 합니다. 요청 거부: -100 최종 잔액: 15000

해석: balance 필드는 private이므로 acc.balance = -999999;처럼 클래스 바깥에서 직접 접근하면 컴파일 오류가 발생합니다(예제에서는 주석 처리해 컴파일이 되게 함). 잔액을 바꾸는 유일한 방법은 deposit, withdraw 메서드를 거치는 것뿐이고, 이 메서드들이 “0보다 큰 금액만 입금 가능”, “잔액을 초과해 출금 불가” 같은 규칙을 강제합니다. 만약 balancepublic이었다면 이런 규칙을 우회해 값을 마음대로 바꿀 수 있었을 것입니다.

쉽게 말하면: getter는 “창구 직원에게 물어보고 답을 듣는 것”, setter는 “창구 직원에게 요청서를 내고 심사를 거쳐 처리되는 것”입니다. 손님(외부 코드)이 금고(필드)에 직접 손을 넣을 수는 없습니다.

자주 틀리는 점: “모든 필드에 getter/setter를 기계적으로 다 만들면 캡슐화가 잘 된 것이다”는 흔한 오해입니다. 검증 로직 없이 public void setBalance(long balance) { this.balance = balance; }처럼 필드 값을 그대로 통과시키는 setter는 사실상 public 필드와 다를 것이 없어, 캡슐화의 목적(불변식 보호)을 달성하지 못합니다. 캡슐화의 핵심은 “숨기고 메서드로만 열어준다”는 형식이 아니라, 그 메서드 안에서 규칙을 강제한다는 내용에 있습니다.

3. default(package-private) — 패키지 경계가 곧 접근 범위

접근 제어자를 아무것도 쓰지 않으면 default가 적용되며, 이는 같은 패키지 안에서만 접근을 허용합니다(패키지 개념은 07편에서 자세히 다룸). 아래는 두 클래스가 같은 패키지(app)에 있는 경우와 다른 패키지에 있는 경우를 비교한 표입니다.

// 파일: app/Counter.java package app; class Counter { // 클래스 자체도 default(공개하지 않음) int count; // 필드도 default void increase() { // 메서드도 default count++; } }
접근하는 코드의 위치Counter 클래스 접근count 필드·increase() 접근
같은 패키지(app) 안의 다른 클래스가능가능
다른 패키지(other)의 클래스불가(컴파일 오류)애초에 클래스에 접근이 안 되므로 논의 자체가 불가

해석: class Counter처럼 클래스 선언 자체에도 접근 제어자를 생략하면(default), 그 클래스는 같은 패키지 밖에서는 아예 존재를 알 수 없는 것처럼 취급됩니다. other 패키지의 코드가 import app.Counter;를 쓰더라도 Counter 클래스 자체가 default이므로 사용할 수 없습니다.

4. protected — 상속을 위해 패키지 경계를 넘는 예외

protected는 “같은 패키지”까지는 default와 같지만, 여기에 더해 다른 패키지에 있더라도 그 클래스를 상속한 자식 클래스에서는 접근을 허용합니다. 이는 부모 클래스를 설계할 때 “이 필드·메서드는 남에게는 안 보여주고 싶지만, 내 자식 클래스에게는 물려주고 싶다”는 의도를 표현하는 문법입니다.

// 파일: shapes/Shape.java package shapes; public class Shape { protected String color; protected void printColor() { System.out.println("색상: " + color); } }
// 파일: app/Circle.java package app; import shapes.Shape; public class Circle extends Shape { // 다른 패키지지만 상속 관계 public Circle(String color) { this.color = color; // protected 필드: 자식 클래스이므로 접근 가능 } public static void main(String[] args) { Circle c = new Circle("빨강"); c.printColor(); // protected 메서드: 자식 클래스이므로 접근 가능 } }
색상: 빨강

해석: Circleshapes 패키지가 아닌 app 패키지에 있지만, Shape상속했기 때문에 protectedcolor 필드와 printColor() 메서드에 접근할 수 있습니다. 만약 CircleShape를 상속하지 않고 그냥 별도의 클래스로 Shape 객체를 참조만 하는 관계였다면, 다른 패키지이므로 protected 멤버에 접근할 수 없습니다.

자주 틀리는 점(시험 핵심 함정): “protected는 상속 관계라면 무조건 다른 패키지에서도 인스턴스를 통해 자유롭게 접근할 수 있다”는 것은 정확하지 않습니다. 다른 패키지의 자식 클래스가 protected 멤버에 접근할 수 있는 경우는 자기 자신이나 자신의 자식 타입의 객체를 통해 접근할 때로 제한됩니다. 즉 Circle 안에서 this.color나 상속받은 필드로 접근하는 것은 되지만, Circle 클래스 안에서 부모 타입인 Shape의 임의의 다른 객체(자신이 상속받은 인스턴스가 아닌 별도의 Shape 객체)의 protected 멤버에 접근하는 것은 다른 패키지에서는 허용되지 않습니다. 이 세부 규칙은 실제 시험에서 “옳지 않은 것 고르기” 유형으로 자주 등장하므로, protected를 “상속하면 무조건 다 열린다”로 단순화해 외우지 않도록 주의해야 합니다.

5. 접근 제어와 캡슐화의 관계 정리

목적흔히 쓰는 접근 제어자
필드를 완전히 숨기고 클래스 내부에서만 사용private
getter/setter처럼 외부에 공개할 통로public
같은 패키지 안 협력 클래스끼리만 공유default(생략)
상속받은 자식 클래스에게는 물려주되 남에게는 숨김protected

해석: 클래스를 설계할 때는 “이 멤버를 누가 써야 하는가”를 먼저 묻고, 그 답에 맞는 가장 좁은 접근 제어자를 선택하는 것이 원칙입니다. 이를 최소 권한 원칙(principle of least privilege, 최소 권한 원칙)이라 부르며, 필요 이상으로 넓은 접근 범위(예: 굳이 private이어도 될 필드를 public으로 여는 것)를 주지 않는 것이 캡슐화가 잘 된 설계입니다.

핵심 정리

  • 캡슐화는 필드를 숨기고 메서드를 통해서만 접근하게 해, 클래스가 정한 규칙(불변식)을 강제하는 설계 원칙이다.
  • 접근 범위는 private < default(생략) < protected < public 순으로 넓어진다.
  • default는 같은 패키지까지만, protected는 같은 패키지에 더해 다른 패키지의 자식 클래스까지 허용한다.
  • getter/setter는 형식이 아니라 그 안에 담는 검증 로직이 캡슐화의 실질적 효과를 만든다.
  • protected의 다른 패키지 접근은 상속받은 자기 자신(또는 그 하위 타입)을 통한 접근으로 제한된다.
  • 최소 권한 원칙: 필요한 만큼만 열고, 나머지는 최대한 좁게 감춘다.

마무리 복습

문제 14지선다
다음 중 접근 허용 범위가 가장 넓은 접근 제어자는?
문제 24지선다
필드를 private으로 감추고 public 메서드로만 값을 바꾸게 하는 설계의 가장 핵심적인 목적은?
문제 34지선다
접근 제어자를 아무것도 쓰지 않았을 때(default) 적용되는 접근 범위는?
문제 44지선다
protected 멤버가 default와 다른 점으로 옳은 것은?
문제 54지선다
Shape 클래스(shapes 패키지)의 protected 필드 color를, 다른 패키지(app)에서 Shape를 상속하지 않고 단순히 Shape 객체를 참조만 하는 별도 클래스에서 접근하려 하면 어떻게 되는가?
문제 64지선다
최소 권한 원칙(principle of least privilege)을 접근 제어자 선택에 적용한 예로 가장 적절한 것은?

참고 자료

Last updated on