Effective Java : 아이템16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.

    핵심정리

    • 클라이언트 코드가 필드를 직접 사용하면 캡슐화의 장점을 제공하지 못한다. 
    • 필드를 변경하려면 필드를 사용하는 모든 API를 변경해야 한다. (변경 범위가 넓다)
    • 필드에 접근할 때 부수 작업을 할 수 없다. (Validation 등의 작업)
    • package-private 클래스 또는 private 중첩 클래스라면 데이터 필드를 노출해도 문제가 없다. 
      • 그럼에도 불구하고 필드에 바로 접근하는 것은 좋지 않다.

    내용 정리

    클래스에서 사용하는 필드는 상수 필드 (public static final)을 제외하고는 public으로 사용하지 않는 것이 좋다. 아래 코드를 예시로 들면서 이해해보고자 한다. 

    public class Point {
        public double x, y;
    
        public static void main(String[] args) {
            Point point = new Point();
            point.x = 10;
            point.y = 20;
    
            System.out.println(point.x);
            System.out.println(point.y);
        }
    
        private double getX() {
            System.out.println("add-on function");
            return this.x;
        }
    }

    Point 클래스에는 x, y 필드가 public으로 선언되어있다. public으로 선언되어있기 때문에 클래스 외부에서 자유롭게 접근할 수 있게 된다. 클래스 외부에서 자유롭게 접근할 수 있다는 것은 캡슐화가 잘 되지 않은 것을 의미한다. 이것은 개발속도와 프로그램의 확장 가능성에 큰 영향을 미친다. 어떻게 public 변수가 프로그램의 확장 가능성에 많은 영향을 미칠까? 

    필드를 변경하려면 필드를 사용하는 모든 API를 변경해야 한다. (변경 범위가 넓다)

    x의 이름을 xDimension으로 바꾸는 경우를 생각해보자. 그러면 Point 인스턴스에서 point.x로 접근하고 있는 모든 API에서 point.x → point.xDimension으로 수정해야한다. public으로 공개되어있는 녀석이기 때문에 적게는 수십 개, 많게는 수백, 수천 개의 코드를 고쳐야 할 수 있다. 

     

    필드에 접근할 때 부수 작업을 할 수 없다. (Validation 등의 작업)

    만약 사용하고 있는 필드들에 값을 주거나 가져올 때 일정부분의 제약이 필요하다고 가정해보자. 만약 point.x, point.y 형태로 변수에 접근한다면, 이 변수를 사용하는 개발자들이 일일이 '제한'을 자신의 코드에 구현해야 한다. 예를 들어 point.x는 반드시 5보다 커야 한다고 가정해보자. 그러면 point.x를 사용하는 개발자는 모두 아래 형식으로 코드를 작성해야 할지도 모른다.

    public static void main(String[] args) {
        Point point = new Point();
        point.x = 10;
        point.y = 20;
    
    	// 제약조건 확인
        assert point.x > 5;
        
        System.out.println(point.x);
        System.out.println(point.y);
    }

     


    좋은 방법은? → 접근자 메서드 사용 

    그렇다면 어떤 방법이 좋은 방법일까? 좋은 방법은 접근자 메서드를 이용하는 것이다. 가장 대표적인 메서드로는 Getter / Setter가 있다. 이 방법을 이용하면 앞서 이야기 했던 두 가지 방법을 모두 해결할 수 있게 된다. 

    public class Point {
    
        private double x, y;
        
        public static void main2(String[] args) {
            Point point = new Point();
            point.setX(5);
    
            System.out.println(point.getX());
        }
        
        public void setX(double x) {
            assert x > 5;
            this.x = x;
        }
        public double getX() {
            System.out.println("add-on function");
            return this.x;
        }
    }

    위와 같이 getter / setter를 이용하면 필요한 기능들을 캡슐화 할 수 있다. 위의 setX() 메서드를 보면 제약조건인 x가 5보다 커야 한다는 조건을 메서드 내에서 구현한다. 따라서 이 클래스를 사용하는 개발자들은 불변성을 코드에 따로 고민하지 않아도 되게 된다. 

    또한, 변수명의 변화에도 수정해야 할 지점이 굉장히 적어진다. 현재 Point 클래스의 x는 getX()를 통해서 받아오고 있다. 그런데 앞에서처럼 변수 x를 xDimension으로 바꾼다고 가정해보자. 모든 클래스는 private x에 접근할 수 없고, getX()로만 접근하고 있다. 따라서 변경지점은 getX()만 되게 되므로 getX()만 추후에 수정해주면 된다. 

    여기에 대해 점진적인 변경까지 가능해진다. 예를 들어 변수명을 바꾸면서 새로운 메서드를 사용하고 싶은 경우가 있다고 가정해보자. 그러면 다음 두 단계로 점진적으로 일을 진행할 수 있게 된다. 

    1. getter 메서드를 수정한다.
    2. 한참 개발하다가, 이후에 필요한 시점에 새로운 메서드를 도입해서 이걸 사용하도록 수정한다. 

     

    package-private 클래스 또는 private 중첩 클래스라면 데이터 필드를 노출해도 문제가 없다. 

    package-private, private 중첩 클래스를 사용한다면 이 클래스는 package 내부에서만 사용하거나, 해당 클래스에서만 사용된다. 즉 사용 지점이 매우 한정적이기 때문에 public 필드를 사용하더라도 변경지점이 충분히 적을 수 있다는 것이다. 그럼에도 불구하고 package-private, private 중첩 클래스라 할지라도 public 필드보다는 private 필드를 이용하면 접근 메서드를 사용하도록 하는 것이 더 좋다. 

    public class Circle {
    
        public Circle() {
            Point point = new Point();
            point.setX(5);
        }
    	
        // 내부 중첩 private class
        private class Point {
            private double x, y;
    
    
            public void setX(double x) {
                assert x > 5;
                this.x = x;
            }
            public double getX() {
                System.out.println("add-on function");
                return this.x;
            }    
        }
    }

    방금 이야기 한 내용은 '변경 지점이 적다'만을 의미하지, '부가 기능 캡슐화'도 극복하지는 못한다. 따라서 변경 지점을 최소화하고 부가 기능까지 캡슐화 해주는 접근자 메서드를 사용하는 것이 더 낫다. 

    // 코드 16-3 불변 필드를 노출한 public 클래스 - 과연 좋은가? (103-104쪽)
    public 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("minutes:" + minute);
            }
            this.hour = hour;
            this.minute = minute;
        }
        
        // 나머지 코드 생략
    }

     

    public 변수의 또 다른 문제 → 성능 문제 야기

    public final 변수는 그나마 괜찮다. 왜냐하면 처음 인스턴스를 생성했을 때, 변수가 final로 정해지면 외부에서 수정할 수 없기 때문에 변수가 변경되는 걱정을 하지 않아도 된다. 반면 public 변수를 노출하는 경우라면, public 변수는 함수 scope이 바뀔 때마다 언제 값이 바뀌게 될지 알 수 없다. 따라서 불안한 코드가 작성될 수 있기 때문에 이것을 미연에 방지하기 위해서 매번 함수 Scope마다 객체를 복사해서 사용하게 된다. 수많은 객체가 생성된다면, 이것이 결국은 성능 문제를 야기시킨다. 

    public class PointCopy {
        public double x, y;
    
        public static void main(String[] args) {
            PointCopy point = new PointCopy();
            point.x = 10;
            point.y = 20;
    
            // doSomething 이후 값이 바뀔 수도 있음.
            doSomething(point);
    
            System.out.println(point.x);
            System.out.println(point.y);
        }
    
        public static void doSomething(PointCopy pointCopy) {
            // 객체 복사 → 성능 문제 야기
            PointCopy localPoint = new PointCopy();
            localPoint.x = pointCopy.x;
            localPoint.y = pointCopy.y;
            
            // 아래 비즈니스 로직
        }
    
    }

    정리하자면 final이 아닌 public 변수를 쓰려고 한다면, 그 변수를 쓰는 곳에서는 항상 변수를 복사해서 사용해야한다. 필드는 언제든지 가변 가능하기 때문에 견고하지 못한 코딩을 야기하고, 이를 방지하기 위해서 변수를 복사해서 사용한다. 그렇지만 이것은 성능 문제를 야기시킨다. 

    댓글

    Designed by JB FACTORY