[이펙티브 자바] 클래스와 인터페이스 Item16 - public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

객체 지향 방법론(Object Oriented Programming : OOP)의 기본 개념은
공통적으로 사용되는 부분을 미리 추상화 해놓는 것이다.

그것을 필요에 따라 구체화시켜 사용하는 것은 코드의 재사용성을 높이며,
그것은 곧 OOP의 지향점이 된다.


자바에서는 추상화의 기본 단위로 클래스(Class)와 인터페이스(Interface) 를 정의하고 있고, 이는 곧 자바의 심장과도 같다.


"4장 - 클래스와 인터페이스" 에서는,
쓰기 편하고, 견고하며, 유연한 클래스와 인터페이스를 만드는 방법에 대해 내용을 서술한다.



  • Item15. 클래스와 멤버의 접근 권한을 최소화하라.
  • Item16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.
  • Item17. 변경 가능성을 최소화하라.
  • Item18. 상속보다는 컴포지션을 사용하라.
  • Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.
  • Item20. 추상 클래스보다는 인터페이스를 우선하라.
  • Item21. 인터페이스는 구현하는 쪽을 생각해 설계하라.
  • Item22. 인터페이스는 타입을 정의하는 용도로만 사용하라.
  • Item23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라.
  • Item24. 멤버 클래스는 되도록 static으로 만들라.
  • Item25. 톱레벨 클래스는 한 파일에 하나만 담으라.




<"public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.">




#   public 클래스의 경우



시작과 함께 먼저 아래의 예시를 봐보자.

class Point {
  public double x;
  public double y;
}

간혹가다 위처럼 인스턴스 필드만을 모아놓는 경우가 생긴다.
(작가는 "퇴보한" 이라는 극적인 워딩을 사용했다....)


로직의 유무를 말하는게 아니다.
일반적으로 엔티티를 정의할 때도 저렇게 필드만 선언되니까 말이다.

문제는 바로 저 필드만 딱 있을때 발생한다.


모두 public으로 선언되어 있기 때문에,
다른 컴포넌트에서 해당 클래스 데이터 필드에 직접 접근이 가능하다.

그 말은, 캡슐화(Encapsulation)가 전혀 안되기 때문에 그에 따른 장점도 얻을 수 없다는 말이다.
사실 장점을 얻을 수 없는 차원이라기 보다, 많은 단점과 위험성이 존재한다고 하는게 맞는것 같다.


<단점>

  • 캡술화의 이점이 없다.
  • API를 수정하지 않고는 내부 표현을 바꿀 수 없다.
  • 불변식을 보장할 수 없다.
  • 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다. (Thread가 불안전하다, Race Condition)

그래서 위 코드는 아래와 같이 바꾸는게 정석이다.

class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { 
  		return x; 
  	}
    public double getY() { 
  		return y; 
  	}

    public void setX(double x) { 
  		this.x = x; 
  	}
    public void setY(double y) { 
  		this.y = y; 
  	}
}

모든 데이터 필드의 접근 가능성을 private으로 바꾼다.
그런 이후에 데이터 필드에 접근할 수 있는 별도의 메서드를 생성한다.


자바에서는 이러한 것을 접근자(Getter, Setter)라고 칭한다.


위와 같이 패키지 바깥에서 접근할 수 있는 클래스에 접근자를 제공하는 방법을 취하면,
클래스 내부 표현 방식을 언제든 유현하게 바꿀수 있게 된다.


<Comment.>

  • 여기서 "내부 표현 방식"이 유연해진다 라는 의미는 "변수명"이라고 바꿔보면 조금 쉽게 이해가 가능했다.
  • 접근자를 이용하지 않고 필드를 직접 노출하게 되면, 클라이언트는 해당 변수명을 그대로 이용하게 된다.
  • 만약 내가 이 변수명을 바꾸려고 하면? 그것을 이용하는 모든 클라이언트가 다 바꿔야 하기 때문에 사실상 바꾸는게 불가능해진다.
  • 그렇지만 클라이언트가 접근자 (Getter, Setter)를 이용하고 있다면 내부적으로 얼마든지 변수명을 바꿀수 있다는 것이다.



#   private-package, private 클래스의 경우


그렇다면 클래스가 public이 아닌, private-package나 private 중첩 클래스라면 어떨까?

이 경우엔 데이터 필드를 노출하던 안하던 별로 상관이 없다.
(어차피 외부에서 접근도, 수정도 못한다.)


이때는 그냥 본분을 다해 그 클래스가 구현하고자 하는 기능만 잘 구현해내면 된다.


실제로 이것은 클래스 선언 면에서나, 클라이언트 코드 면에서나 접근자보다 훨씬 깔끔해진다.
(사실 클라이언트 코드에서야 접근자를 사용하는 것보다 직접 필드를 사용하는게 더 깔끔하다.)


<Comment.>

  • 아마도 이런 의문이 드는 사람이 있을 것이다.
  • "이것도 그럼 변수명 바꾸면 클라이언트도 다 바꿔야 하는데?"
  • public 클래스의 경우 공개 API이기 때문에 전세계의 모든 사람이 클라이언트지만, package-private은 그 패키지 내에서만 클라이언트가 존재한다.
  • 선자는 바꾸는게 불가능하고, 후자는 바꾸는게 어렵지 않다. 아예 가능성 자체가 다르다.


중첩된 private 클래스의 경우는 더 쉽다.

어차피 그 클래스를 사용할 수 있는건 바로 바깥 클래스 하나이기 때문에 클라이언트가 될 수 있는 대상이 훨씬 좁다.

내부 표현 수정이 더욱 용이하다.




#   만약 public 클래스의 필드가 불변이라면?


불변이라면야 직접 노출하는 거에 대해서 단점이 줄어들긴 한다.
불변식을 만족할 수는 있게 되지만 여전히 다른 단점은 존재하지 않는가.

내부 표현 방식을 바꿀수 없고, 필드를 읽을 때 부수 작업도 수행할 수 없다.


다음의 예를 보자.

public final class Time {
    private static final int HOURS_PER_DAY    = 24;
    private static final int MINUTES_PER_HOUR = 60;

    public final int hour;
    public final int minute;

    public Time(int hour, int minute) {
        if (hour < 0 || hour >= HOURS_PER_DAY)
            throw new IllegalArgumentException("Hour: " + hour);
        if (minute < 0 || minute >= MINUTES_PER_HOUR)
            throw new IllegalArgumentException("Min: " + minute);
        this.hour = hour;
        this.minute = minute;
    }

}

위는 입력으로 들어오는 시간이 유효한 시간임을 보장하는 클래스이다.




필자의 코멘트로 글을 마무리 한다.

Item16 정리

  • public 클래스는 절대 가변 필드를 직접 노출해서는 안된다.
  • 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.
  • package-private이나 중첩 private 클래스는 노출하는게 나을 때도 있다.

좋은 웹페이지 즐겨찾기