본문 바로가기

프로그래머/Java Managed Programming

[개체지향 프로그래밍] 접근 제어자 | getter/setter | 캡슐화 | 추상화

개체는 자신의 상태를 스스로 책임져야 함!

  • 즉, 개체 외부에서 개체의 상태에 직접 접근하는 것을 막아야 함
  • 개체의 상태를 변경하는 주체는 개체 자신인 게 이상적
  • 접근 제어자(access modifiedr)를 통해 이런 일을 할 수 있음

접근 제어자

  • 어떤 외부자들이 개체 속에 접근할 수 있는지 정의
  1. public: 누구나 접근 가능
  2. protected: 자식들만 접근 가능
  3. 생략할 경우: 같은 패키지에 속한 클래스들만 접근 가능
    • default 혹은 package 접근 제어자라고 부름
  4. private: 외부 접근 금지

접근 제어자: private

  • 외부자들은 접근할 수 없음
  • 클래스 내부에서만 접근 가능함
  • 클래스의 경우 내포(nested) 클래스에 한 해 붙일 수 있음

private 멤버 변수

public class Human {
    private String name;
    private int age;
    private Sex sex;
    private Citizenship citizenship;

    public void walk(){
        this.age += 1;  // ok
    }
    // ...
}

Human adam = new Human("Adam", 20, Sex.MALE, Citizenship.KOREA);
adam.age = 0;   // compile error

일반적인 접근 제어자

  • 보통 다음과 같은 접근 제어자를 사용
    • 멤버 변수: private(또는 protected)
    • 메서드: public
  • 멤버 변수 접근은 메서드를 통해서만!
    • 캡슐화: 외부에서 캡슐을 뚫고 들어오지 못함.
    • 추상화: 속에 데이터가 무엇이 있는지 밖에서는 전혀 모름. 추측만 가능.

private 메서드는 클래스 안에서만 호출할 수 있음.
고로 클래스 안에서 중복되는 코드를 막는데 사용

private과 생성자

  • 생성자도 멤버 함수이므로 똑같은 규칙 적용
  • new를 못하니 적어도 생성자 하나는 public인 것이 보통

private 접근 제어자

  • 외부에서 접근을 시도하면 컴파일 오류
  • 내부에서는 접근 가능

    내부란 클래스 내부를 의미!
    클래스 내부 != 개체 내부
    같은 클래스에 속한 개체끼리는 private 멤버에 접근 가능

public class Human{
    private int age;
    // ...
    public void punch(Human enemy){
        enemy.age -= 1; // ok
        this.age += 2;  // ok
    }
}

Human adam = new Human();   // age: 20
Human james = new Human();  // age: 30

adam.punch(james);          // adam age : 22
                            // james age : 29 

접근 제어자를 안 붙일 경우

  • 기본(defalut) 또는 패키지(package) 접근 권한
  • 같은 패키지 안에 있는 클래스끼리 서로 접근 가능
    • 같은 패키지: public처럼 작동
    • 다른 패키지: private처럼 작동
  • OOP에서 흔히 말하는 접근 제어자는 아님
  • C#에서도 internal이라는 비슷한 접근 제어자가 있음

패키지 접근 제어자 용도

  1. public 대신 패키지 접근 제어자를 사용할 수 있다면 그리할 것
    • 어떤 패키지 안에서만 사용되는 클래스
  2. 'public이 아닌 내포 클래스'를 최상위 클래스로 바꿀 때 쓸 것
    • 내포 클래스는 가독성 문제가 생길 수 있음
    • 따라서 별도의 클래스로 분리시키는 것이 요즘 트렌드
    • 이 때 접근 권한을 패키지 내로 제한하는 것이 public보다 나음

getter / setter

public class Human {
    private String name;
    // ...
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
    // ...
}

개체 스스로 자신을 책임지도록 하는 게 OOP의 정신
그 클래스를 작성한 프로그래머가 그 클래스에 대해 가장 잘 안다

함수를 통한 데이터 접근의 객관적인 장점

  1. 멤버 변수를 저장하지 않고 필요할 때마다 geter에서 계산 가능
    • 예: 질량과 중력 멤버 변수로부터 무게를 계산
  2. setter에서 추가적인 로직을 실행할 수 있음
    • 예: 음수의 나이가 인자로 들어올 경우 무시
  3. 상속을 통한 다형성 구현 가능

베스트 프랙티스 1: 멤버 함수는 private

  • 대부분의 경우 모두 private
  • 정보 숨기기(informateion hiding)

베스트 프랙티스 2: 새 개체는 유효하도록

  • 개체는 살아있는 동안 언제나 유효한 상태여야 이상적
    • 처음 생성될 때도 마찬가지
    • 그래야 실수를 막을 수 있음
  • 생성자를 통해 이를 강제할 수 있음
    • 매개변수가 틀릴 경우 컴파일 자체가 안 됨
    • 매개변수가 바뀌는 경우도 마찬가지
public Human(String name, Sex sex) { ... }
private Human(String name, int age, Sex sex, Citizenship citizenship) { ... }

Human adam = new Human();   //  compile error
Human james = new Human("James", 31, Sex.MALE)  // compile error
Human gabriela = new Human("Gabriela", 39, Sex.FEMALE, Citizenship.KOREA)   // OK

베스트 프랙티스 3: getter는 자유롭게 추가

  • 사용자가 알 필요 없는 정보는 보여주지 않는 게 정석
  • 그러나 보여줘도 큰 문제는 없으니 getter는 보통 자유롭게
  • 주의: 어떤 개체의 레퍼런스를 반환할 때는 문제 될 수도 있음
public class Family {
    private Human[] parents;
    private Dog pet;
    // ...
    public Dog getPet() {
        return pet;
    }
}

Family adams = new Family();
Family smiths = new Family();

Dog notAdamsDog = smiths.getPet();
adams.setPet(notAdamsDog);

C++는 getter에서 읽기 전용레퍼런스를 반환할 수 있어 이런 문제가 없음

베스트 프랙티스 4: setter는 고민 후 추가

  • 이상적인 개체의 상태 수정법
  1. 그 개체의 사용자가 어떤 동작을 지시
  2. 그 동작의 결과로 개체 안에 있는 어떤 상태가 바뀜
  • 즉, 개체 스스로 상태를 변경
  • setter는 데이터를 직접 바꾸므로 가능한 피하는 게 좋음
    • 단, 언제나 그럴 수는 없기에 많이들 허용
    • 중요한 점: 개체가 불확실한 상태로 되는 경우를 최대한 막자!
  • 무조건 setter/getter를 추가하는 경우도 있음
    • 혹시나 모를 미래를 위해
    • 그냥 아무 생각 없이(...)

이상적인 개체의 상태 수정법 예

public class Classroom {
    private int[] scores;
    private float mean;
    // ...

    public boolean setScore(int index, int score){
        scores[index] = score;
        updateMean();

        return true;
    }

    private void updateMean() {
        this.mean = 계산결과;
    }
    // ...
}

classroom.setScore(1, 100);
classroom.setScore(13, 20);

정리: 캡슐화

  1. 개체의 데이터(=멤버 변수)와 동작(=메서드)을 하나로 묶음
  2. 내부의 데이터를 외부로부터 보호
  • 사용자가 클래스 속을 알 필요가 없음
    • 사용자가 함수 속을 알 필요가 없는 것과 마찬가지
    • 이 개념은 추상화로 이어짐
  • 함수를 분리할 때 적용했던 원칙을 클래스에도 적용할 것!
    • 중복된 코드가 있다면 private 메서드로

정리: 추상화

  • 추상 자료형(abstract data type)쪽 관점
    • 사용자는 클래스를 자료형으로 사용할 수 있음
    • 그 클래스 안에 들어있는 멤버 변수가 정확히 뭔지 몰라도 됨
    • 그냥 클래스로부터 개체 생성 가능
  • 절차적 데이터 추상화(procdeural data abstraction) 쪽 관점
    • 데이터를 직접 조작하는 대신 메서드를 호출
    • OOP라는 용어를 처음 주창했다는 소수설의 관점과 유사
    • 동작적 개체(behavioral objects) 진영이라고 하기도 함(정식 명칭 x)

추상화의 단점

  • 동작 없이 데이터만 있는 클래스는 쓸데 없는 코드만 늘어남
    • 예: 웹 프로그래밍에서 많이 볼 수 있는 DTO(data transfer object)
    • 그래서 이런 경우에는 그냥 public 데이터를 쓰기도 함
  • 어떻게 추상화를 해야하는지 뚜렷한 객관적 기준이 없음
    • 나중에 다형성, 상속, 인터페이스에서 나오는 추상화에서 특히 문제