CodeGym /행동 /JAVA 25 SELF /접근 제어자 오류

접근 제어자 오류

JAVA 25 SELF
레벨 23 , 레슨 2
사용 가능

1. 소개

Java에서 접근 제어자는 집의 잠금 장치와 같습니다. 누가 어디에서 여러분의 방(혹은 클래스의 필드/메서드)에 “들어올” 수 있는지를 결정합니다. 모든 문이 열려 있으면 누구나 와서 무언가를 바꿀 수 있습니다. 모든 것이 잠겨 있으면 아무도 아무것도 망가뜨릴 수 없지만, 어느 순간 여러분 자신도 갇힐 수 있습니다.

Java에는 네 가지 기본 접근 수준이 있습니다:

접근 제어자 클래스 내부에서 접근 가능 패키지 내에서 접근 가능 하위 클래스에서 접근 가능 다른 패키지에서 접근 가능
private
(package)
protected
(상속을 통해)
public

(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를 공개하여 유지 보수를 복잡하게 합니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION