1. 소개
Java에서 접근 제어자는 집의 잠금 장치와 같습니다. 누가 어디에서 여러분의 방(혹은 클래스의 필드/메서드)에 “들어올” 수 있는지를 결정합니다. 모든 문이 열려 있으면 누구나 와서 무언가를 바꿀 수 있습니다. 모든 것이 잠겨 있으면 아무도 아무것도 망가뜨릴 수 없지만, 어느 순간 여러분 자신도 갇힐 수 있습니다.
Java에는 네 가지 기본 접근 수준이 있습니다:
| 접근 제어자 | 클래스 내부에서 접근 가능 | 패키지 내에서 접근 가능 | 하위 클래스에서 접근 가능 | 다른 패키지에서 접근 가능 |
|---|---|---|---|---|
|
✔ | |||
| (package) | ✔ | ✔ | ||
|
✔ | ✔ | ✔ | (상속을 통해) |
|
✔ | ✔ | ✔ | ✔ |
(package) — 명시적으로 제어자를 지정하지 않은 경우를 의미합니다. 이런 클래스 멤버는 동일한 패키지 안에서만 보입니다.
2. 접근 제어자에서 흔한 실수
오류 1: 기본 접근 수준 (package-private)의 필드와 메서드
가장 흔한 초보자의 실수는 접근 제어자를 지정하지 않는 것입니다. 그 결과, 원치 않았는데도 필드나 메서드가 패키지 전체에서 접근 가능해집니다. 이렇게 되면 같은 패키지의 다른 클래스(여러분의 코드와 무관한)가 객체의 내부 상태를 변경할 수도 있습니다.
// 오류: 필드 name이 보호되지 않았습니다!
class User {
String name; // package-private!
}
그 결과, 이 패키지의 아무 클래스나 다음과 같이 작성할 수 있습니다:
User user = new User();
user.name = "Vasya"; // 제한 없음!
오류 2: 캡슐화 위반 — 공개 필드 (public)
두 번째로 흔한 실수는 클래스의 필드를 public으로 선언하는 것입니다. 학습 중이거나 짧은 예제를 작성할 때는 편할 수 있지만, 실제 프로젝트에서는 거의 항상 좋지 않습니다. 누가, 어떻게 데이터를 변경하는지에 대한 통제력을 잃게 됩니다.
public class Account {
public double balance; // 위험합니다!
}
이제 어떤 코드든 다음과 같이 할 수 있습니다:
Account acc = new Account();
acc.balance = -1000000; // 이제 누구 책임인가요?
오류 3: getter와 setter가 없음
때로는 개발자가 필드를 private으로 만들고도 이를 제어할 메서드를 추가하지 않습니다. 그 결과, 그게 적절한 상황임에도 값을 얻거나 변경할 수 없게 됩니다.
public class Product {
private String name;
// getName()도 setName()도 없음
}
오류 4: 다른 클래스에서 private 멤버에 접근 시도
필드나 메서드를 private으로 선언했다면, 같은 패키지에 있어도 다른 클래스에서 접근할 수 없습니다. 초보자는 종종 왜 필드가 “안 보이는지” 의아해합니다.
public class User {
private String password;
}
public class UserService {
public void resetPassword(User user) {
// user.password = "123"; // 컴파일 오류!
}
}
오류 5: protected 관련 실수
많은 사람이 protected를 “상속만 있으면 어디서나 보인다”로 이해합니다. 하지만 Java에서 패키지 밖의 protected 멤버는 상속을 통해서만, 그리고 하위 클래스에서만 접근 가능합니다. 놓치기 쉬운 미묘한 차이입니다.
package animals;
public class Animal {
protected void sleep() {}
}
package zoo;
import animals.Animal;
public class Dog extends Animal {
public void test() {
sleep(); // OK — 하위 클래스
}
}
public class NotADog {
public void test() {
Animal a = new Animal();
// a.sleep(); // 오류: 하위 클래스가 아님!
}
}
3. 올바른 방법: 모범 사례
규칙 1: 기본적으로 필드는 private로
캡슐화의 핵심 원칙입니다. 필드는 해당 클래스 외부에서는 숨겨져야 합니다. 접근이 필요하다면 getter/setter를 사용하세요.
public class Book {
private String title;
private int pages;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
규칙 2: 필요한 메서드만 공개하세요
메서드를 외부에서 사용할 필요가 있다면 public으로 만드세요. 패키지 내에서만 필요하다면 package-private을 유지하세요. 오직 하위 클래스만 사용해야 한다면 protected를 사용하세요.
규칙 3: 가시 범위를 최소화하세요
가시 범위가 작을수록 우발적 실수와 “예상치 못한 손님”의 가능성이 줄어듭니다. 필요하지 않다면 메서드와 필드를 public으로 만들지 마세요.
규칙 4: 접근 제어를 위해 getter/setter를 사용하세요
이는 필드 읽기/쓰기 시 추가 로직(예: 검증)을 넣을 수 있게 해 줍니다.
public class Account {
private double balance;
public void setBalance(double balance) {
if (balance < 0) {
throw new IllegalArgumentException("잔액은 음수가 될 수 없습니다!");
}
this.balance = balance;
}
public double getBalance() {
return balance;
}
}
규칙 5: 내부 구현을 노출하지 마세요
필드로 배열이나 리스트가 있다면, 이를 getter로 직접 반환하지 말고 복사본을 반환하거나 필요한 메서드만 제공하세요.
public class Team {
private List<String> members = new ArrayList<>();
// 올바른 예:
public List<String> getMembers() {
return new ArrayList<>(members); // 복사본을 반환
}
}
4. 실전 예시
예를 들어, 도서관 사용자를 나타내는 LibraryUser 클래스가 있다고 합시다.
잘못된 구현 예
public class LibraryUser {
public String name;
public int borrowedBooks;
}
이 상태에서는 어떤 코드든 객체에 마음대로 할 수 있습니다:
LibraryUser user = new LibraryUser();
user.name = null;
user.borrowedBooks = -10; // 논리요? 무슨 논리죠?
캡슐화를 적용한 올바른 구현 예
public class LibraryUser {
private String name;
private int borrowedBooks;
public LibraryUser(String name) {
this.name = name;
this.borrowedBooks = 0;
}
public String getName() {
return name;
}
public int getBorrowedBooks() {
return borrowedBooks;
}
public void borrowBook() {
borrowedBooks++;
}
public void returnBook() {
if (borrowedBooks > 0) {
borrowedBooks--;
}
}
}
이제 외부 코드는 대출 도서 수나 사용자 이름을 직접 변경할 수 없습니다. 모든 것은 클래스의 메서드를 통해서만 제어됩니다.
5. 구현상의 특징과 뉘앙스
가끔은 많은 getter와 setter를 작성하는 것보다 필드를 public으로 만드는 것이 더 쉬워 보일 수 있습니다. 하지만 그것은 함정입니다! 공개 필드는 열린 집 문과 같습니다. 편하긴 해도 안전하지 않습니다.
또 다른 뉘앙스는 모든 필드에 항상 getter와 setter가 필요하지 않다는 점입니다. 객체 생성 후 값이 바뀌면 안 되는 필드라면, getter만 만들고 필드를 final로 두세요:
public class Passport {
private final String number;
public Passport(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
}
또한 기억하세요: 클래스가 public으로 선언되었다면 파일 이름은 클래스 이름과 동일해야 합니다! 접근 제어자와 직접적인 관련은 아니지만, 초보자가 아주 자주 하는 실수입니다.
6. 접근 제어자 작업 시 흔한 실수
오류 №1: 필드나 메서드에 접근 제어자를 지정하지 않음. 그 결과, 의도하지 않았는데도 필드나 메서드가 패키지 전체에서 접근 가능해집니다. IDE가 경고하지 않더라도 항상 명시적으로 접근 제어자를 지정하세요.
오류 №2: 모든 필드를 public으로 선언. 이는 캡슐화를 파괴하고 코드를 취약하고 예측 불가능하게 만듭니다. “간단함”을 위한 예제에서의 습관을 실전 코드에 들여오지 마세요.
오류 №3: 다른 클래스에서 private 필드에 접근하려 함. Java는 이를 허용하지 않습니다 — 컴파일러가 여러분을 보호합니다. 만약 리플렉션으로 이를 “우회”하고 싶어졌다면, 왜 그런 필요가 생겼는지 먼저 고민해 보세요.
오류 №4: protected 멤버가 상속이 있는 곳이면 어디서나 접근 가능하다고 기대함. 실제로 패키지 밖에서는 protected 멤버에 하위 클래스에서만, 그리고 this 또는 하위 클래스의 객체를 통해서만 접근할 수 있습니다.
오류 №5: 내부 컬렉션을 getter로 그대로 반환. 내부 배열이나 리스트의 참조를 반환하면 외부 코드가 이를 변경해 클래스의 불변식을 깨뜨릴 수 있습니다.
오류 №6: setter에서 값 설정 시 검증 부재. 입력 값을 검증하지 않으면 객체가 잘못된 상태(예: 음수 잔액)가 될 수 있습니다.
오류 №7: 메서드의 가시 범위를 지나치게 넓게 설정. 패키지나 클래스 내부에서만 필요한 메서드인데도 public으로 만드는 경우가 있습니다. 이는 불필요한 API를 공개하여 유지 보수를 복잡하게 합니다.
GO TO FULL VERSION