[Java 기본]Section 6 - 접근제어자

우영·2024년 2월 17일

JAVA 기본편

목록 보기
6/13

접근 제어자


자바는 public, private 같은 접근 제어자를 제공하다. 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드가 접근 하는 것을 허용하거나 제한할 수 있다.

개념 정리


클래스: 객체를 생성하기 위한 설계도,사용자 정의 자료형, 객체를 생성하기 위한 필드와 메소드의 집합

💡클래스의 구성요소


  • 필드: 메소드 밖, 클래스에 선언한 변수
    • 변수와 비슷한 형태로, 고유 데이터, 현재 상태정보(속성), 다른 객체를 저장

  • 메서드: 객체의 기능에 해당하는, 수행문의 코드블록

  • 생성자: 클래스의 이름과 동일한 이름을 가진 메소드

    • 반환 타입이 없다. (void도 아님, 반환타입 자체가 없는 것. 반환타입이 있을 경우 생성자메소드가 아니다.)

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

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

  1. 100이상 올라가지 않고 잘 만듦.
  2. 그 후에 업그레이드 된 다음 버전의 스피커를 출시하게 되고 새로운 개발자가 이어받아서 개발을 하게 되었다. 기존 요구사항을 몰랐고 실행해보니 100이상 올라가지 않았다. Speaker 클래스를 보니 volumn 필드를 직접 사용할 수 있는 걸 알고 volumn 필드 값을 200으로 설정하고 이 코드를 실행한 순간 스피커의 부품들에 과부하가 걸리면서 폭발하게 됐다.

결론

Speaker 객체를 사용하는 사용자는 Speaker의 volumn 필드와 메서드에 모두 접근 할 수 있다. 100을 넘는 기능을 만들었지만 직접 값을 설정할 수 있어 소용이 없다. 외부 접근을 막을 수 있는 방법이 필요했다.

private


  • private 접근 제어자는 모두 외부 호출을 막는다. 따라서 해당 클래스 내부에서만 호출할 수 있다.

결론


Speaker 외부에서 volumn 필드에 직접 접근하는 것은 불가능하다. 이 경우 자바 컴파일러가 컴파일 오류를 발생시킨다. 외부 접근으로 막아둠으로써 새로운 개발자도 volumn 필드에 직접 접근하지 않고, volumeUp()과 같은 메서드를 통해서 접근 했을 것이다. 결과적으로 폭발하는 문제가 발생하지 않았을 것이다.

접근 제어자 종류


  • private : 모든 외부 호출을 막는다.
  • default (package-private): 같은 패키지안에서 호출은 허용한다.
  • protected : 같은 패키지안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
  • public : 모든 외부 호출을 허용한다.

순서대로 private 이 가장 많이 차단하고, public 이 가장 많이 허용한다.
private -> default -> protected -> public

package-private


= default 접근 제어자

접근 제어자 사용 위치

  • 접근 제어자는 필드와 메서드, 생성자에 사용된다.

    • 추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있다. 이 부분은 뒤에서 따로 설명한다.
  • 지역변수에는 쓸 수 없다.(어차피 지역 안에서만 쓸 수 있는 변수들이라서)

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

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

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


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();
 }
}
  • 패키지 위치는 package access.a 이다. 패키지 위치를 꼭 맞추어야 한다. 주의하자.
  • 순서대로 public , default , private 을 필드와 메서드에 사용했다.
  • 마지막에 innerAccess() 가 있는데, 이 메서드는 내부 호출을 보여준다. 내부 호출은 자기 자신에게 접근하는것이다. 따라서 private 을 포함한 모든 곳에 접근할 수 있다.
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();

    }
}
  • 패키지 위치는 package access.a 이다. 패키지 위치를 꼭 맞추어야 한다. 주의하자.
  • public 은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근 가능하다.
  • default 는 같은 패키지에서 접근할 수 있다. AccessInnerMain 은 AccessData 와 같은 패키지이다. 따라서 default 접근 제어자에 접근할 수 있다.
  • private 은 AccessData 내부에서만 접근할 수 있다. 따라서 호출 불가다.
  • AccessData.innerAccess() 메서드는 public 이다. 따라서 외부에서 호출할 수 있다.
  • innerAccess() 메서드는 외부에서 호출되었지만 innerAccess() 메서드는 AccessData 에 포함되어
    있다. 이 메서드는 자신의 private 필드와 메서드에 모두 접근할 수 있다.
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();

    }
}
  • 패키지 위치는 package access.b 이다. 패키지 위치를 꼭 맞추어야 한다. 주의하자.
  • public 은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근할 수 있다.
  • default 는 같은 패키지에서 접근할 수 있다.
  • access.b.AccessOuterMain 은 access.a.AccessData 와 다른 패키지이다. 따라서 default 접근 제어자에 접근할 수 없다.
  • private는 AccessData 내부에서만 접근할 수 있다. 따라서 호출 불가다.
  • AccessData.innerAccess() 메서드는 public 이다. 따라서 외부에서 호출할 수 있다.
  • innerAccess() 메서드는 외부에서 호출되었지만 해당 메서드 안에서는 자신의 private 필드와 메서드에 접근할 수 있다.

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

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


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

  • 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 {
}
  • 패키지 위치는 acess.a이다. 패키지 위치를 꼭 맞추어야 한다.
  • PublicClass라는 이름의 클래스를 만들었다 . 이 클래스는 public 접근 제어자다. 따라서 파일명과 이 클래스의 이름이 반드시 같아야 한다. 이 클래스는 public이기 때문에 외부에서 접근할 수 있따.
  • DefaultClass1, DefaultClass2는 default 접근 제어자다. 이 클래스는 default이기 때문에 같은 패키지 내부에서만 접근할 수 있다.
  • PublicClass의 main()을 보면 각각의 클래스를 사용하는 예를 보여준다.
    • PublicClass는 public 접근 제어다. 따라서 어디서든 사용할 수 있다. DefaultClass1, DefaultClass2 와는 같은 패키지에 있으므로 사용할 수 있다.

  package access.a;
public class PublicClassInnerMain {
 public static void main(String[] args) {
 PublicClass publicClass = new PublicClass();
 DefaultClass1 class1 = new DefaultClass1();
 DefaultClass2 class2 = new DefaultClass2();
 }
}
  • public 클래스(외부에서 접근가능)라서 접근 가능하고 나머지 밑에 2개는 같은 패키지기 때문에 된다.


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();
 }
}
  • 패키지 위치는 b이다.
  • public은 모든 접근 허용이기 때문에 필드, 메서드 모두 접근 할 수 있습니다.
  • default는 같은 패키지에서 접근할 수 있습니다. 따라서 접근 제어자에 접근 할 수 없습니다.

캡슐화


  • 객체 지향 프로그래밍의 중요한 개념 중 하나로, 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말한다.
  • 캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있다.
  • 접근제어자의 역할 = 캡슐화를 안전하게 완성할 수 있게 해주는 장치

    쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것.


어떤 것을 숨기고 어떤 것을 노출해야 하는지?

    1. 데이터 숨기기
    • 객체에는 속성(데이터)과 기능(메서드)이 있다. 캡슐화에서 필수로 숨겨야 하는 것은 속성(데이터)이다.
    • 객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 데이터안의 로직을 무시하고 데이터를 변경할 수 있다. 따라서 캡슐화가 깨진다.
    • 예를 들어 자동차를 운전할 때 자동차 부품을 열어서 속도 조절 하진는 않는다. 엑셀을 밟기만 하면 자동차가 알아서 하는 것.

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


    1. 기능 숨기기
    • 객체의 기능 중에서 내부에서만 사용하는 기능들이 있다. 이런 것들은 모두 감추기.
    • 만약 사용자에게 모두 알려준다면, 사용자에 대해 너무 많은 것을 알아야 한다.
    • 사용자 입장에서 꼭 필요한 기능만 외부에 노출하자. 나머지는 숨기자

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


BankAccount

package access;
public class BankAccount {
 private int balance;
 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;
 }
}

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가 제공하는 메서드를 통해서만 접근 가능.
    • isAmountVaid(): 입력 금액을 검증하는 기능은 내부에서만 필요한 기능이다. 따라서 private를 사용했다.
  • public
    • deposit() : 입금
    • withdraw() : 출금
    • getBalance() : 잔고
      BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 된다. 나머진 복잡한 내용은 모두 숨어있다.

만약 isAmountValid()를 외부에 노출하면 어떻게 될까?

BankAccount를 사용하는 개발자 입장에서는 사용할 수 있는 메서드가 하나 더 늘었다. 입금과 출금전에 isAmountValid()를 사용해서 검증을 해야 하나? 라고 의문을 가질 것이다.

만약 balance 필드를 외부에 노출하면 어떻게 될까?

이 필드를 직접 사용해도 된다고 판단하고 쓰면 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생할 수 있다.

private : 쓰면 안됨, public : 갖다 씀.


문제풀이

1 - 최대 카운터와 캡슐화

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

답:

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

핵심

  • 데이터들은 private(숨김), 외부기능들은 public

문제 2 - 쇼핑카트



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();
 }
Item 클래스 
package access.ex;
public class Item {
 private String name;
 private int price;
 private int quantity;
 //TODO 나머지 코드를 완성해라.
}

ShoppingCart 클래스 
package access.ex;
public class ShoppingCart {
 private Item[] items = new Item[10];
 private int itemCount;
 //TODO 나머지 코드를 완성해라.
}

정답

속성과 기능이 근처에 있는 걸 쓰는 게 좋다. 수정할 때도 편하다.

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;
 }
 //else는 검증로직, 정상로직이 검증로직 안에 들어가는 것은 별로 맘에 안든다.
  items[itemCount] = item;
  itemCount++;
 }

// 장바구니 상품 출력
// 상품명:마늘, 합계:4000
// 상품명:상추, 합계:12000
// 전체 가격 합:16000
 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() {
  int totalPrice = 0;
  for (int i = 0; i < itemCount; i++) {
   Item item = items[i];
   totalPrice += item.getTotalPrice();
  }
  return totalPrice;
 }
}
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;
 }
}
  • 캡슐화를 잘하고 설계하면 클라이언트(고객)가 좋다.
  • 메서드 뽑아 놓으면 메서드 내부에서만 고칠 수 있다.

  • 아직 안보고 못품
  • 많이 부족함

0개의 댓글