Post

[JAVA] 접근 제어자와 캡슐화 알아보기

접근 제어자

김영한의 실전 자바 기본편을 보며 정리해보는 자바의 접근 제어자

접근 제어자란 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한하는 것을 뜻한다.

아마 객체지향 언어를 한 번이라도 본 적이 있다면 public이나 private 키워드가 낯이 익을 것이다. 이런 키워드가 바로 접근 제어자, 혹은 접근 제한자다.

private와 public

접근 제어자를 어떻게 사용하는지 알아보기 전에, 자바가 이런 기능을 제공하게 됐는지 생각해보자. 왜 외부에서 특정 필드나 메서드를 사용하지 못하게 할 필요가 있었을까?

왜❔ 접근 제어자가 필요할까

이유를 알아보기 위해 지금부터 휴대폰 객체를 한번 만들어 보자.

지금부터 만들 휴대폰 객체는 다음과 같은 조건을 가지고 있다.

1
2
1. 휴대폰은 전원 버튼과 배터리만 가지고 있다.
2. 휴대폰은 100% 이상 충전되면 대참사가 일어난다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Phone {
    int battery = 100;

    void charge() {
        if (battery >= 100) {
            System.out.println("이미 완전히 충전되었습니다.");
            return;
        }
        battery += 20;
    }

    void showBatteryStatus() {
        System.out.println("현재 배터리는 " + battery + "% 입니다.");
    }
}

과충전 되면 일어날 수 있는 대참사를 막기 위해, charge() 메서드에서 이미 완전히 충전되었을 경우 배터리를 더 충전시키지 않도록 제약을 걸어두었다. 정상적으로 동작하는지 한 번 실행시켜 보자.

1
2
3
4
5
6
7
public class PhoneMain {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.showBatteryStatus();
        phone.charge();
    }
}

console output - 1

배터리의 기본 값은 100이고, 더 충전시키려고 하니 충전하지 못하도록 막고 있다.

우리는 이로써 대참사를 막을 수 있었다!

그러나, 어느 날 친구 개발자가 찾아와 여러분의 소중한 휴대폰을 구경하던 도중이었다.

alt text

alt text

‘어? 배터리? 배터리는 많으면 많을 수록 좋으니까 내가 올려줘야겠다!’

alt text

alt text

alt text

여러분의 휴대폰은 폭발하고 말았다.

charge() 메서드에 안전장치를 걸어두었지만, Phone을 사용하는 입장에서 battery 필드에 직접 접근해 원하는 값을 설정할 수 있다면 이런 안전장치는 제대로 동작하지 않는다.

따라서 클래스 내부의 값에 외부에서 직접 접근하지 못하도록 하는 방법이 필요하게 된 것이다.

1
private int battery = 100;

Phone 클래스의 battery 변수 접근 제어자를 private로 설정해주면 batteryPhone 클래스에서만 수정할 수 있게 된다.

alt text

따라서, 다른 개발자가 PhoneMain 클래스에서 배터리를 변경할 수 있는 수단은 charge() 메서드만 남게 되므로 휴대폰이 과충전되는 일은 없어진다.

이렇게 외부에서 내부 변수에 접근할 경우 발생할 수 있는 예외적인 상황(버그)를 막기 위해 접근 제어자가 필요해진 것이다.

어떻게❔ 접근 제어자를 사용할까

접근 제어자의 종류

자바는 4가지 종류의 접근 제어자를 제공한다.

가장 많이 차단하는 순서로 나열하면 다음과 같다.

  • private
    • 모든 외부 호출을 막는다.
    • 나의 클래스 안으로 속성과 기능을 숨길 때 사용한다.
  • default(package-private)
    • 같은 패키지 안에서 호출은 허용한다.
    • 나의 패키지 안으로 속성과 기능을 숨길 때 사용한다.
    • 접근 제어자를 명시하지 않으면 해당 접근 제어자가 적용된다.
  • protected
    • 같은 패키지안에서 호출은 허용한다.
    • 패키지가 달라도 상속 관계의 호출은 허용한다.
    • 상속 관계로 속성과 기능을 숨길 때 사용하며 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.
  • public
    • 모든 외부 호출을 허용한다.
    • 즉 모든 기능을 어디서든 사용할 수 있게 공개한다.

접근 제어자는 속성과 기능을 외부로부터 숨김과 동시에 프로그래머로 하여금 해당 기능과 변수를 어디까지 사용해야 하는지 알려주는 메시지의 역할도 함께 한다.

즉, 어떤 변수가 public으로 선언되어 있다면 “이 변수는 아무데서나 쓸 수 있는 변수야.” 하고 알려주는 것과 같다.

이런 접근 제어자는 필드와 메서드, 생성자에 사용된다. 클래스 레벨에도 일부 접근 제어자를 사용할 수 있다.

클래스 레벨의 접근 제어자는 public, default만 사용할 수 있다.

단, public 클래스는 반드시 파일명과 이름이 같아야 한다.

하나의 자바 파일에 public 클래스는 하나만 등장할 수 있고, default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.

접근 제어자로 알아보는 캡슐화

💊 캡슐화(Encapsulation)란?

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

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

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

클래스와 생성자를 통해 데이터와 데이터를 처리하는 메서드를 하나로 모았다면, 접근 제어자를 통해 외부에 필요한 기능만 노출하고 데이터는 내부로 숨길 수 있게 된다.

✔ 데이터를 숨겨야 한다

아까 만들었던 휴대폰을 생각해 보자. 사용자가 배터리를 직접 수정하면 충전 메서드에 걸어두었던 안전 장치를 무시하게 됐다. 이런 코드는 변수를 초래하고 예상치 못한 버그를 발생시킨다.

따라서 데이터를 숨기고, 객체 내부의 데이터는 객체가 제공하는 기능인 메서드를 통해서만 접근하도록 해야 한다.

✔ 내부에서만 사용하는 기능을 숨겨야 한다

전원 버튼을 눌렀을 때 내부에서 변수가 어떻게 변하는지, 어떤 기능들이 실행되는지는 사용자에게 불필요한 정보일 뿐이다. 배터리가 어떤 식으로 과충전을 막고 있는지, 전원이 켜지면 내부에서 어떤 일이 일어나는지는 알 필요가 없다.

코드로 알아보는 캡슐화

이제 캡슐화를 코드에 직접 적용해 보며 마무리 짓자.

아까 만든 휴대폰을 좀 더 업그레이드 해보겠다.

추가된 조건은 다음과 같다.

1
2
3
4
5
6
7
8
9
1. 전화를 걸 수 있다.
    - 단, 전화번호부에 등록되어 있지 않은 번호라면 '등록되어 있지 않은 번호입니다.' 메시지를 출력한다.
    - 전화번호부에 등록된 번호라면 '통화를 시작합니다.' 메시지를 출력한다.
    - 전화를 걸 때마다 배터리가 30%씩 소모된다.
    - 전화를 걸 때 배터리가 10% 이하면 '충전이 필요합니다' 메시지를 출력한다.
2. 충전은 마찬가지로 100% 까지만 할 수 있다.
    - 배터리는 100%까지만 충전된다.
    - 배터리가 100%인 상태에서 충전하려고 하면 '이미 완전히 충전되었습니다.' 메시지를 출력한다.
    - 배터리 값은 외부에서 변경할 수 없다.

Phone.java

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.Arrays;

public class Phone {
    private int battery = 100;
    private String[] phoneDirectory = {"01012345678", "01011111111"};

    // 휴대폰 충전
    public void charge() {
        if (battery >= 100) {
            System.out.println("이미 완전히 충전되었습니다.");
            return;
        }
        battery = Math.min(battery + 20, 100);
    }

    // 전화 걸기
    public void call(String phoneNumber) {
        // 만약 전화번호부에 등록되어 있지 않은 번호라면
        if (!isPhoneNumberInDirectory(phoneNumber)) {
            System.out.println("등록되어 있지 않은 번호입니다.");
            return;
        }

        System.out.println("통화를 시작합니다.");
        reduceBattery();
    }

    // 전화번호가 전화번호부에 있는지 검증
    private boolean isPhoneNumberInDirectory(String phoneNumber) {
        return Arrays.stream(phoneDirectory).anyMatch(phoneNumber::equals);
    }

    // 전화 시 배터리 감소
    private void reduceBattery() {
        if (battery <= 10) {
            System.out.println("충전이 필요합니다.");
            return;
        }
        battery -= 30;
        if (battery < 0) {
            battery = 0; // 배터리가 음수가 되지 않도록
        }
    }

    // 배터리 잔량 확인
    public void showBatteryStatus() {
        System.out.println("현재 배터리는 " + battery + "% 입니다.");
    }
}

PhoneMain.java

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
public class PhoneMain {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.showBatteryStatus();
        phone.charge(); // 이미 완전히 충전되었습니다.

        phone.call("01012345678");
        phone.showBatteryStatus(); // 배터리 70%

        phone.call("01022222222");
        phone.showBatteryStatus(); // 배터리 70%

        phone.call("01011111111");
        phone.call("01011111111");
        phone.call("01012345678"); // 충전이 필요합니다.
        phone.showBatteryStatus(); // 배터리 10%

        phone.charge(); // 배터리 30%
        phone.charge(); // 배터리 50%
        phone.charge(); // 배터리 70%
        phone.charge(); // 배터리 90%
        phone.showBatteryStatus();
        phone.charge(); // 배터리 100% - 100% 이상 충전되지 않음
        phone.showBatteryStatus();
    }
}

Phone 클래스의 다음 코드를 보자.

1
2
    private int battery = 100;
    private String[] phoneDirectory = {"01012345678", "01011111111"};

배터리와 전화번호부는 외부에서 접근해 직접 변경할 수 없도록 private 접근 제한자를 설정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    private boolean isPhoneNumberInDirectory(String phoneNumber) {
        return Arrays.stream(phoneDirectory).anyMatch(phoneNumber::equals);
    }

    private void reduceBattery() {
        if (battery <= 10) {
            System.out.println("충전이 필요합니다.");
            return;
        }
        battery -= 30;
        if (battery < 0) {
            battery = 0; // 배터리가 음수가 되지 않도록
        }
    }

전화번호부에 해당 번호가 존재하는지 검증하는 기능과 배터리가 감소하는 기능은 사용자가 알 필요가 없다.

사용자는 전화 버튼을 눌렀을 때 전화가 걸리는지, 걸리지 않는지만 알면 되기 때문이다.

따라서 isPhoneNumberInDirectory 메서드와 reduceBatteryprivate로 숨겨준다.

1
2
3
4
5
6
7
8
9
10
    public void call(String phoneNumber) {
        // 만약 전화번호부에 등록되어 있지 않은 번호라면
        if (!isPhoneNumberInDirectory(phoneNumber)) {
            System.out.println("등록되어 있지 않은 번호입니다.");
            return;
        }

        System.out.println("통화를 시작합니다.");
        reduceBattery();
    }

call 메서드는 사용자가 전화 버튼을 눌렀을 때 일어나는 일이다.

숨겼던 기능들은 이곳 내부에서 실행되고 있다.

만약 전화번호부에 등록되어 있지 않은 번호라면 “등록되어 있지 않은 번호입니다.” 메시지를 출력하고, 그렇지 않으면 통화를 시작한 후 배터리를 조건에 맞게 감소시키거나 “충전이 필요합니다.” 메시지를 출력한다.

결과적으로 사용자는 배터리 충전, 전화 걸기, 배터리 잔량 확인 기능만을 사용해 해당 클래스에 접근할 수 있다.

정리

  • 접근 제어자를 사용하는 이유는 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한하기 위해서이다.
  • 이런 제한이 필요한 이유는 외부에서 클래스 내부 데이터에 직접 접근했을 때 발생할 수 있는 예외를 방지하기 위함이다.
  • 접근 제어자를 통해 데이터를 숨기고, 필요한 기능만 제공함으로써 객체지향의 특징인 캡슐화를 구현할 수 있다.
    • 데이터의 무결성을 보장한다.
    • 유지보수성과 재사용성을 향상시킨다.
    • 디버깅과 테스트가 용이해진다.
This post is licensed under CC BY 4.0 by the author.