Book Review/effective-java

[ITEM16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

jay Joon 2021. 2. 12. 00:13

ITEM 16

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

서론

public 필드를 통한 데이터의 접근은 캡슐화의 이점을 제공해주지 못한다.

또한 불변 식을 보장할 수 없으며 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다.

접근자 메서드를 통해 접근하자!

public 클래스라면 패키지 바깥에서 접근할 수 있는 getter 메서드를 만들어주자.

만약 public 필드로 노출하게 되면 다음과 같은 문제점이 발생한다.

  • 캡슐화의 이점을 제공해줄 수 없다.
  • API를 수정하지 않고는 내부 표현을 바꿀 수 없다.(해당 문구는 Setter를 이용하면 유연하게 변경할 수 있다는 뜻인 것 같다.)
  • 불변식을 보장할 수 없다.(이 부분도 public 필드로 들어갈 값들에 대한 검증을 할 수 없다. 하지만 Setter 메서드를 사용한다면 필드 값들에 대해 어느 정도 제약을 줄 수 있다.)
  • 외부에서 필드에 접근할 때 부수작업을 추가하지 못한다.

package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출해도 하등의 문제가 없다.

  • package-private 클래스인 경우 패키지 내부에서 사용하는 코드이다.(즉 패키지 바깥 코드는 전혀 손대지 않고 데이터 표현방식을 변경 가능)
  • private 중첩 클래스 인경우 더욱 수정 범위가 좁아 이 클래스를 포함하는 외부 클래스까지로 제한된다.

결론

public 클래스는 절대 가변 필드를 직접 노출해서는 안된다.

불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.

하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을 때도 있다.

예시 코드

public class User { //public class 일때.

    private String name; //해당 클래스의 필드의 맴버를 private 으로 감추자.
    private int age;
    private String phone;


    public String getName() { // getter 메서드를 통해 데이터를 주자.
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getPhone() {
        //기존 저장형식이 01012345678 인 경우
        // 클라이언트에게 넘겨줄때는 010-0000-0000으로 넘겨줄수있다.
        return this.phone.replaceFirst("(^[0-9]{3})([0-9]{3,4})([0-9]{4})$", "$1-$2-$3");
    }

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

    // setter 를 쓰게되면 불변객체로 보장을 해주지못한다는데..?
    // 하지만 제공하지 않아도 얼마든지  클라이언트 측에서 리플렉션을 이용하여 얼마든지 조작이가능하긴한데?
    // 해당 주제에서는 벗어나니 일단지나가자.
    public void setAge(int age) {
        if(age<0) {
            throw new IllegalArgumentException("음수값을 가질 수 없습니다"); //해당코드 처럼 불변식을 보장해줄수 있다
        }
        this.age = age;
    }

    public void setPhone(String phone) {
        if(!phone.matches("[0-9]{11}")){
            throw new IllegalArgumentException("11자리수를 맞춰주세요"); //간단하게 자리수 만 확인..
        }
        this.phone = phone;
    }
}

Test 코드

class Item16Test {
    @Test
    void userExceptionTest(){
        User user = new User();

        assertThatThrownBy(() -> user.setAge(-10))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("음수값을 가질 수 없습니다");
    }

    @Test
    void userPhone(){
        User user = new User();
        assertThatThrownBy(()->user.setPhone("010"))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("11자리수를 맞춰주세요");
    }

    @Test
    void userGetPhone(){
        User user = new User();
        user.setPhone("01000000000");

        assertThat(user.getPhone()).isEqualTo("010-0000-0000");
    }
}