[☕JAVA] 접근 제어자와 캡슐화
접근 제어자가 왜 필요한지, 캡슐화 2대 원칙이 무엇인지, 그리고 static과 어떻게 조합되는지 코드 레벨로 정리합니다.
캡슐화는 OOP의 핵심 원칙이지만, 실제로 어떻게 구현하는지 흐릿한 경우가 많다. 접근 제어자는 단순한 문법이 아니라 최소 권한 원칙을 코드 레벨에서 강제하는 장치다. Speaker 폭발 사례부터 BankAccount, ShoppingCart까지 — 왜 private이 필요한지, static과 어떻게 다른 축인지 완전히 정리해보자.
What this post covers
- 왜 접근 제어자가 필요한가 (Speaker 폭발 사건)
- 4가지 접근 제어자의 범위와 차이
- 클래스 레벨 규칙 (
publicvsdefault) - 캡슐화 2대 원칙과 실전 예제
static은 접근 제어자와 어떻게 다른 축인가
1. 왜 접근 제어자가 필요한가?
1.1 Speaker 폭발 사건 — 문제의 시작
스피커 소프트웨어 개발 시나리오를 생각해보자. 요구사항: 음량은 절대 100을 넘으면 안 된다 (100 초과 시 부품 고장).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package access;
public class Speaker {
int volume; // 접근 제어자 없음 → default (package-private)
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()에 검증 로직이 있으므로 정상적으로 사용하면 100을 넘지 않는다:
1
2
3
4
5
현재 음량: 90
음량을 10 증가합니다.
현재 음량: 100
음량을 증가할 수 없습니다. 최대 음량입니다.
현재 음량: 100
1.2 문제 발생 — 필드 직접 접근
새 개발자가 기존 요구사항을 모르고 필드에 직접 접근:
1
2
speaker.volume = 200; // ❌ 검증 로직 완전 우회!
speaker.showVolume(); // 현재 음량: 200 → 스피커 폭발!
volume 필드가 외부에 노출되어 있으면, volumeUp()의 검증이 무용지물이다.
1.3 해결 — private으로 차단
1
2
3
4
public class Speaker {
private int volume; // ✅ private 사용!
// ... 나머지 동일
}
이제 외부에서 speaker.volume = 200 시도 시:
1
volume has private access in access.Speaker // 컴파일 에러!
“좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약을 제공하는 프로그램이다.”
1.4 Principle of Least Privilege
OS에서 프로세스 A가 프로세스 B의 메모리 공간에 접근할 수 없는 것처럼, 접근 제어자는 최소 권한 원칙(Principle of Least Privilege)을 OOP에서 구현한 것이다. 이는 Secure Coding의 Attack Surface 최소화와 직결된다.
2. 접근 제어자 4가지 종류
차단 강도 순서: private → default → protected → public
2.1 접근 범위 요약표
| Modifier | Same Class | Same Package | Subclass (다른 패키지) | World |
|---|---|---|---|---|
private | O | X | X | X |
default (키워드 없음) | O | O | X | X |
protected | O | O | O | X |
public | O | O | O | O |
2.2 각 제어자의 의미
private — 나의 클래스 안으로 숨김
- 모든 외부 호출을 막는다
- 해당 클래스 내부에서만 접근 가능
- 캡슐화의 핵심 도구
default (package-private) — 나의 패키지 안으로 숨김
- 접근 제어자를 명시하지 않으면 적용됨
- 같은 패키지 안에서만 접근 가능
- package-private이 더 정확한 표현
protected — 패키지 + 상속 관계로 숨김
- 같은 패키지 안에서 호출 허용
- 패키지가 달라도 상속 관계의 호출은 허용
public — 모든 외부 호출 허용
- 어디서든 접근 가능
- 한 번 public으로 열면 되돌리기 어려움 (이미 다른 코드가 의존할 수 있음)
3. 접근 제어자 사용 — 필드, 메서드
3.1 접근 제어자 사용 위치
접근 제어자는 필드, 메서드, 생성자에 사용된다. 추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있다.
1
2
3
4
5
6
7
public class Speaker { // 클래스 레벨
private int volume; // 필드
public Speaker(int volume) {} // 생성자
public void volumeUp() {} // 메서드
public void volumeDown() {}
public void showVolume() {}
}
3.2 AccessData 예제 — 내부 vs 외부 접근
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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; // ✅ 자기 자신 → private도 접근 가능!
publicMethod();
defaultMethod();
privateMethod();
}
}
3.3 같은 패키지에서 접근 (access.a)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package access.a;
public class AccessInnerMain {
public static void main(String[] args) {
AccessData data = new AccessData();
data.publicField = 1; // ✅ public
data.publicMethod();
data.defaultField = 2; // ✅ 같은 패키지이므로 default 접근 가능
data.defaultMethod();
// data.privateField = 3; // ❌ private은 어디서든 직접 접근 불가
// data.privateMethod(); // ❌
data.innerAccess(); // ✅ public 메서드를 통해 내부 private에 간접 접근
}
}
실행 결과:
1
2
3
4
5
6
publicMethod 호출 1
defaultMethod 호출 2
내부 호출
publicMethod 호출 100
defaultMethod 호출 200
privateMethod 호출 300
3.4 다른 패키지에서 접근 (access.b)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package access.b;
import access.a.AccessData;
public class AccessOuterMain {
public static void main(String[] args) {
AccessData data = new AccessData();
data.publicField = 1; // ✅ public은 어디서든 접근 가능
data.publicMethod();
// data.defaultField = 2; // ❌ 다른 패키지이므로 default 접근 불가!
// data.defaultMethod(); // ❌
// data.privateField = 3; // ❌
// data.privateMethod(); // ❌
data.innerAccess(); // ✅ public 메서드이므로 외부 호출 가능
}
}
실행 결과:
1
2
3
4
5
publicMethod 호출 1
내부 호출
publicMethod 호출 100
defaultMethod 호출 200
privateMethod 호출 300
innerAccess()는 외부에서 호출되었지만, 이 메서드는AccessData내부에 있으므로 자신의private필드와 메서드에 모두 접근할 수 있다. 이것이 캡슐화의 작동 원리다.참고: 생성자도 접근 제어자 관점에서 메서드와 같다.
4. 접근 제어자 사용 — 클래스 레벨
4.1 클래스 레벨 규칙
- 클래스 레벨에는
public과default만 사용 가능 (private,protected불가) public클래스는 반드시 파일명과 이름이 같아야 함- 하나의
.java파일에public클래스는 하나만 가능 default클래스는 같은 파일에 여러 개 가능
4.2 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 { } // default → 같은 패키지에서만 접근
class DefaultClass2 { } // 같은 파일에 여러 개 가능
같은 패키지(access.a)에서 접근:
1
2
3
4
5
6
7
8
9
package access.a;
public class PublicClassInnerMain {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass(); // ✅ public
DefaultClass1 class1 = new DefaultClass1(); // ✅ 같은 패키지
DefaultClass2 class2 = new DefaultClass2(); // ✅ 같은 패키지
}
}
다른 패키지(access.b)에서 접근:
1
2
3
4
5
6
7
8
9
10
package access.b;
import access.a.PublicClass;
public class PublicClassOuterMain {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass(); // ✅ public 클래스
// DefaultClass1 class1 = new DefaultClass1(); // ❌ 다른 패키지, default 접근 불가
// DefaultClass2 class2 = new DefaultClass2(); // ❌
}
}
5. 캡슐화 (Encapsulation)
5.1 정의
캡슐화(Encapsulation) = 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것.
쉽게: 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것.
접근 제어자는 캡슐화를 안전하게 완성할 수 있게 해주는 장치이다.
5.2 캡슐화 2대 원칙
원칙 1 — 데이터를 숨겨라
객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 모든 검증 로직을 무시하고 데이터를 변경할 수 있다. MusicPlayer 예제에서 volume, isOn을 직접 수정하는 대신 on(), volumeUp() 메서드만 쓰는 것과 같다 — JAVA(3) OOP 도입편 참고.
→ 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.
원칙 2 — 내부 기능도 숨겨라
외부에서 사용하지 않고 내부에서만 사용하는 기능도 감춘다. MusicPlayer의 내부 상태 관리 로직을 밖에서 알 필요 없듯이, 외부 인터페이스는 꼭 필요한 것만 열어둔다.
→ 사용자 입장에서 꼭 필요한 기능만 외부에 노출하고, 나머지 기능은 모두 내부로 숨기자.
정리: 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화.
5.3 BankAccount — 캡슐화 실전 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package access;
public class BankAccount {
private int balance; // ✅ 원칙 1: 데이터 숨김
public BankAccount() { balance = 0; }
public void deposit(int amount) { // public: 외부에 노출
if (isAmountValid(amount)) {
balance += amount;
} else {
System.out.println("유효하지 않은 금액입니다.");
}
}
public void withdraw(int amount) { // public: 외부에 노출
if (isAmountValid(amount) && balance - amount >= 0) {
balance -= amount;
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
public int getBalance() { return balance; } // public: 외부에 노출
private boolean isAmountValid(int amount) { // ✅ 원칙 2: 내부 기능 숨김
return amount > 0;
}
}
사용자는 deposit(), withdraw(), getBalance() 단 3가지 메서드만 알면 된다.
접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 동시에, 사용자 입장에서 복잡도도 낮출 수 있다.
5.4 Secure Coding 연결
private필드 → Attack Surface 최소화: 외부 노출 인터페이스가 줄어들수록 공격 지점 감소public메서드의 입력 검증 → Input Validation: getter/setter를 통해 모든 입력에 검증 강제isAmountValid()를private으로 숨김 → 필요 이상의 정보를 노출하지 않는 보안 원칙private final→private이 접근 차단,final이 변경 차단 담당
6. 문제와 풀이
6.1 MaxCounter — 최대 카운터와 캡슐화
요구사항:
- 최대값을 지정하고 최대값까지만 값이 증가하는 기능 제공
- 접근 제어자를 사용해서 데이터를 캡슐화
- 해당 클래스는 다른 패키지에서도 사용할 수 있어야 함
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}
실행 결과:
1
2
최대값을 초과할 수 없습니다.
3
정답:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}
}
6.2 ShoppingCart — 객체 간 캡슐화 협력
1
2
3
4
5
6
7
8
9
10
11
12
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();
}
}
실행 결과:
1
2
3
4
장바구니 상품 출력
상품명:마늘, 합계:4000
상품명:상추, 합계:12000
전체 가격 합:16000
Item 클래스 정답:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 getTotalPrice() {
return price * quantity; // ✅ 자기 데이터를 자기가 계산!
}
}
price와 quantity를 외부에 반환하여 외부에서 곱하는 대신, getTotalPrice()에서 자기 데이터를 자기가 직접 계산하여 결과만 제공.
ShoppingCart 클래스 정답:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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.getTotalPrice());
}
System.out.println("전체 가격 합:" + calculateTotalPrice());
}
private int calculateTotalPrice() { // ✅ 내부에서만 사용 → private
int totalPrice = 0;
for (int i = 0; i < itemCount; i++) {
totalPrice += items[i].getTotalPrice();
}
return totalPrice;
}
}
calculateTotalPrice()가 private인 이유: displayItems() 내부에서만 사용되는 유틸리티 메서드이므로 외부에 노출할 필요가 없다.
7. static — 인스턴스가 아닌 클래스에 소속
접근 제어자가 “누가 볼 수 있는가”를 결정한다면, static은 “이 멤버가 어디에 소속되는가“를 결정한다. 이 두 개념은 독립된 축(orthogonal axes)이다.
7.1 메모리 관점
| 구분 | 저장 위치 | 생명 주기 | 개수 |
|---|---|---|---|
| 인스턴스 변수 | Heap | 객체 생성 ~ GC 수거 | 인스턴스마다 하나 |
static 변수 | Method Area (Metaspace) | 클래스 로딩 ~ 프로그램 종료 | 클래스당 딱 하나 |
| 지역 변수 | Stack | 메서드 호출 ~ 리턴 | 호출마다 하나 |
7.2 코드 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Student {
private String name; // 인스턴스 변수: 학생마다 다름
private static int totalCount; // static 변수: 모든 Student가 공유
public Student(String name) {
this.name = name;
totalCount++; // 생성될 때마다 카운트 증가
}
public static int getTotalCount() {
return totalCount;
// this.name ← ❌ 컴파일 에러! static 메서드에서 인스턴스 변수 접근 불가
}
}
7.3 핵심 규칙
규칙 1: static 메서드에서 인스턴스 멤버 접근 불가
static 메서드는 특정 인스턴스에 바인딩되지 않으므로 this 사용 불가. “누구의 name?” — 대답할 수 없다.
규칙 2: static은 오버라이드되지 않음 (Method Hiding)
1
2
3
4
5
6
7
8
9
class Parent {
static void greet() { System.out.println("Parent"); }
}
class Child extends Parent {
static void greet() { System.out.println("Child"); } // hiding, not overriding
}
Parent p = new Child();
p.greet(); // "Parent" 출력! — 컴파일 타임에 결정 (early binding)
7.4 접근 제어자 + static 조합
| 조합 | 의미 | 대표 예시 |
|---|---|---|
private static | 클래스 내부에서만 쓰는 공유 데이터 | 싱글톤의 instance 필드 |
public static | 어디서든 접근 가능한 클래스 레벨 멤버 | Math.PI, Integer.MAX_VALUE |
public static final | 상수 (변경 불가 + 어디서든 접근) | public static final double PI = 3.14159 |
private final | 인스턴스별 불변 데이터 | 불변 객체의 필드 |
Key takeaways
- 접근 제어자는 캡슐화를 안전하게 완성하는 장치
private: 같은 클래스 내부에서만. 캡슐화의 핵심default: 같은 패키지에서만. 키워드 없이 적용protected: 같은 패키지 + 상속 관계public: 어디서든 접근 가능
- 캡슐화 2대 원칙
- 데이터(속성)는 모두
private으로 숨겨라 - 기능(메서드)도 외부에 필요한 것만
public으로 열어라 - 내부 유틸리티 메서드는
private으로 숨겨라
- 데이터(속성)는 모두
- 클래스 레벨 규칙
- 클래스에는
public과default만 사용 가능 public클래스 = 파일명과 동일, 파일당 하나
- 클래스에는
- static은 접근 제어자와 독립된 축
- 인스턴스가 아닌 클래스에 소속 → Method Area에 딱 하나만 존재
- static 메서드에서 인스턴스 멤버 접근 불가
private final조합private= 외부 접근 차단 (캡슐화)final= 변경 차단 (불변성)- 둘의 조합 = 불변 객체의 핵심 규칙
Reading flow
- Concept foundation: [☕JAVA] 객체 지향 프로그래밍(OOP) - JAVA 3 — 캡슐화의 why/what
- Previous: [☕JAVA] 제네릭(Generic) - JAVA 6
- Next: 불변 객체와
private final- JAVA 8 - Series:
/series/
