본문 바로가기
IT/김영한의실전JAVA-기본_inFlearn

6. 접근 제어자 - 접근 제어자 이해1~2 / 접근 제어자 종류 / 접근 제어자 사용(필드, 메서드) / 접근 제어자 사용(클래스 레벨) / 캡슐화 / 문제와 풀이

by for-learn 2025. 2. 11.

6-1. 접근 제어자 이해1

접근 제어자(access modifier)를 사용하면,

해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한 가능

 

스피커에 들어가는 소프트웨어를 개발할 때, 스피커의 음량이 100을 넘어가면 부품들이 고장난다.

Speaker.java (volume 100 제한)

package access;

public class Speaker {
    int volume;

    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);
    }
}

SpeakerMain.java

package access;

public class SpeakerMain {
    public static void main(String[] args) {
        Speaker speaker = new Speaker(90);
        speaker.showVolume();

        speaker.volumeUp();
        speaker.showVolume();
        
        speaker.volumeUp();
        speaker.showVolume();
    }
}

 

새로운 개발자가 다음 버전의 스피커를 출시하게 되었다.

기존 코드를 이어 받아 개발하는 개발자는 기존 요구사항을 잘 몰랐다. 

Speaker 클래스를 보니 volume 필드를 직접 사용할 수 있어,

volume 제한을 200으로 높이고 실행하니 부품들이 과부하걸려 고장났다.

SpeakerMain.java (필드에 직접 접근해 코드 추가)

   ...
   // 필드에 직접 접근
   System.out.println("volume 필드 직접 접근 수정");
   speaker.volume = 200;
   speaker.showVolume();
}

  • Speaker 객체를 사용하는 사용자는 Speaker의 volume 필드와 메서드에 모두 접근 가능
  • volumeUp() 메서드 같이 음량이 100을 넘지 못하도록 기능을 개발했지만
    => volume 필드에 직접 접근해 원하는 값 설정 가능

 

6-2. 접근 제어자 이해2

Speaker.java (volume 접근 제어자를 private로 수정)

package access;

public class Speaker {
    // private: Speaker 클래스 안에서만 접근 가능
    private int volume;
    ...
}
  • private 접근 제어자는 모든 외부 호출을 막는다. 

volume 필드를 private을 사용해 Speaker 내부에 숨겼다.

 

SpeakerMain.java (필드에 직접 접근해 코드 추가 => 컴파일 오류 => 주석처리)

    // 필드에 직접 접근
    System.out.println("volume 필드 직접 접근 수정");
    speaker.volume = 200;
    speaker.showVolume();
}

 

6-3. 접근 제어자의 종류

  • private : 모든 외부 호출을 막는다.
  • default (package-private): 같은 패키지안에서 호출은 허용한다.
    필드에 접근 제어자가 없으면 모두 default (기본값)
  • protected : 같은 패키지안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
  • public : 모든 외부 호출을 허용한다
private ▶ default ▶ protected ▶ public

 

package-private

  • 접근 제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default 접근 제어자가 적용된다.
  • default 라는 용어는 해당 접근 제어자가 기본값으로 사용되기 때문에 붙여진 이름이지만,
  • 실제로는 package-private 이 더 정확한 표현이다. (두 용어를 함께 사용)

 

접근 제어자 사용 위치

  • 필드, 메서드, 생성자에 사용한다. (지역 변수에는 사용 불가)
  • 클래스 레벨에도 일부 접근제어자 사용 가능 => 뒤에서 따로 설명
public class Speaker { // 클래스 레벨

    private int volume; // 필드
    
    public Speaker(int volume) {} // 생성자
    
    public void volumeUp() {} // 메서드
    public void volumeDown() {}
    public void showVolume() {}
}

 

접근 제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것이다.

  • private 은 나의 클래스 안으로 속성과 기능을 숨길 때 사용, 외부 클래스에서 해당 기능을 호출할 수 없다.
  • default 는 나의 패키지 안으로 속성과 기능을 숨길 때 사용, 외부 패키지에서 해당 기능을 호출할 수 없다.
  • protected 는 상속 관계로 속성과 기능을 숨길 때 사용, 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.
  • public 은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.

 

6-4. 접근 제어자 사용 - 필드, 메서드

패키지 위치 주의

AccessData.java (public, default, private)

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;
        publicMethod();
        defaultMethod();
        privateMethod();
    }
}

AccessInnerMain.java (패키지 a)

package access.a;

public class AccessInnerMain {
    public static void main(String[] args) {
        AccessData data = new AccessData();
        // public 호출 가능
        data.publicField = 1;
        data.publicMethod();

        // 같은 패키지 default 호출 가능
        data.defaultField = 2;
        data.defaultMethod();

        // private 호출 불가
        // data.privateField = 3;
        // data.privateMethod();

        data.innerAccess();
    }
}

AccessOuterMain.java (패키지 b)

package access.b;

import access.a.AccessData;

public class AccessOuterMain {
    public static void main(String[] args) {
        AccessData data = new AccessData();
        // public 호출 가능
        data.publicField = 1;
        data.publicMethod();

        // 다른 패키지 default 호출 불가
        // data.defaultField = 2;
        // data.defaultMethod();

        // private 호출 불가
        // data.privateField = 3;
        // data.privateMethod();

        data.innerAccess();
    }
}

 

※ 생성자도 접근 제어자 관점에서 메서드와 같다.

 

6-5. 접근 제어자 사용 - 클래스 레벨

클래스 레벨의 접근 제어자 규칙

  • 클래스 레벨의 접근 제어자는 public , default 만 사용 가능
  • private , protected 는 사용 불가
  • public 클래스는 반드시 파일명과 이름이 동일해야 함.
    • 하나의 자바 파일에 public 클래스는 하나만 등장
    • 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 등장 가능

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 {
}
class DefaultClass2 {
}

PublicClassInnerMain.java

package access.a;

public class PublicClassInnerMain {
    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass();
        DefaultClass1 class1 = new DefaultClass1();
        DefaultClass2 class2 = new DefaultClass2();
    } // 모두 접근 가능
}

PublicClassOuterMain.java (package b)

package access.b;

//import access.a.DefaultClass1;
import access.a.PublicClass;

public class PublicClassOuterMain {
    public static void main(String[] args) {
        PublicClass publicClass = new PublicClass();
        // 다른 패키지 접근 불가
        //DefaultClass1 class1 = new DefaultClass1();
        //DefaultClass2 class2 = new DefaultClass2();
    }
}

 

6-6. 캡슐화

캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나

데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것

속성과 기능을 하나로 묶고, 외부에는 꼭 필요한 기능만 노출 시킴

캡슐화를 통해 데이터의 직접적인 변경을 방지/제한 가능

 

1. 데이터를 숨겨라

객체에는 속성(데이터)와 기능(메서드)이 있다. 

캡슐화에서 가장 필수로 숨겨야하는 것은 속성(데이터)

 

객체 내부의 데이터를 외부에서 함부로 접근하게 두면,

클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있다.

결국 모든 안전망을 다 빠져나가게 된다. 따라서 캡슐화가 깨진다.

 

"객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다."

(대부분의 데이터들은 private로 막아둔다.)

 

2. 기능을 숨겨라

객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들도 모두 감추는 것이 좋다.

사용자 입장에서 꼭 필요한 기능만 외부에 노출하자.

(그렇지 않으면, 사용자가 자동차에 대해 너무 많은 것을 알아야 한다. )

정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화

 

BankAccount.java

package access;

public class BankAccount {
    private int balance;

    // Genergate 단축키 => alt + insert
    public BankAccount() {
        balance = 0;
    }

    // public 메서드: deposit
    public void deposit(int amount) {
        if (isAmountValid(amount)) {
            balance += amount;
        } else {
            System.out.println("유효하지 않은 금액입니다.");
        }
    }

    // public 메서드: withdraw
    public void withdraw(int amount) {
        if (isAmountValid(amount) && balance - amount >= 0) {
            balance -= amount;
        } else {
            System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
        }
    }

    // public 메서드: getBalance
    public int getBalance() {
        return balance;
    }
    
    // private 메서드: isAmountValid
    private boolean isAmountValid(int amount) {
        // 금액이 0보다 커야 함
        return amount > 0;
    }
}
  • Genergate 단축키 => alt + insert

BankAccountMain.java

package access;

public class BankAccountMain {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(10000);
        account.withdraw(3000);
        System.out.println("balance = " + account.getBalance());
    }
}

private

  • balance: 데이터 필드는 외부에 직접 노출하지 않음. BankAccount 가 제공하는 메서드를 통해서만 접근
  • isAmountValid(): 입력 금액을 검증하는 기능은 내부에서만 필요한 기능이여서 private 을 사용

public

  • deposit(): 입금
  • withdraw(): 출금
  • getBalance(): 잔고

 

6-7. 문제와 풀이

MaxCounter.java

package access.ex;

public class MaxCounter {
    private int count;
    private int max;

    public MaxCounter(int max) {
        this.max = max;
    }

    public void increment() {
        if (max > count) {
            count++;
        } else {
            System.out.println("최대값을 초과할 수 없습니다.");
        }
        /* 강사님 코드
        // 검증로직
        if (count >= max) {
            System.out.println("최대값을 초과할 수 없습니다.");
            return;
        }
        // 실행로직
        count++;
         */
    }

    public int getCount() {
        return count;
    }
}

 

 

Item.java

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;
    }

    String getName(){
        return name;
    }

    int getTotal(){
        return price * quantity;
    }
}

ShoppingCart.java (장바구니 용량 3으로 줄임)

package access.ex;

public class ShoppingCart {
    private Item[] items = new Item[3];
    private int itemCount;

    public void addItem(Item item) {
        if (itemCount >= items.length) {
            System.out.println("장바구니가 가득 찼습니다.");
            return;
        }

        /* 강사님 코드
        items[itemCount] = item;
        itemCount++;
        */

        for (int i = 0; i < items.length; i++) {
            if (items[i] == null) {
                items[i] = item;
                itemCount++;
                break;
            }
        }
    }

    public void displayItems(){
        int totalPrice = 0;
        System.out.println("장바구니 상품 출력");
        for (Item item : items) {
            if (item != null){
                System.out.println("상품명:"+item.getName()+", 합계:"+item.getTotal());
                totalPrice += item.getTotal();
            }
        }
        System.out.println("전체 가격 합:"+totalPrice);
    }

    /* 강사님 코드 => 기능을 세부적으로 분리
    private int calculateTotalPrice() {
        int totalPrice = 0;
        for (int i = 0; i < itemCount; i++) {
            Item item = items[i];
            totalPrice += item.getTotalPrice();
        }
        return totalPrice;
    }
    */
}

  • calculateTotalPrice() : 이 메서드 내부에서만 사용되므로 private 접근 제어자를 사용