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

@023· January 09, 2025 · 7 min read

캡슐화의 이점을 제공하지 못하는 클래스

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

위와 같은 코드는 데이터 필드에 대한 직접적인 접근을 허용한다. 이러한 코드는 캡슐화의 이점을 제공하지 못한다.(아이템15와 이어지는 맥락)

API를 수정하지 않고는 내부 표현을 바꿀 수 없다.

public 필드로만 구성되어 있기 때문에 내부 표현을 변경하기 위해서는 외부 API를 변경해야 한다.

책에서는 다음과 같이 Point 클래스에 접근자 메서드를 추가하는 방법을 제시한다.

public getx() { 
    return x; 
}
public gety() { 
    return y;
}

불변식을 보장할 수 없다.

public 필드는 클라이언트가 필드에 접근하여 값을 변경할 수 있기 때문에 불변식을 보장할 수 없다.

public static void main(String[] args) {
    Point point = new Point(1, 1);
    System.out.println(point.x); //1
    point.x += 1; // x값 변경
    System.out.println(point.x); //2
}

외부에서 필드에 접근할 때 부수 작업을 수행할 수 없다.

위 코드에서의 point.x 같이 필드에 직접 접근하는 경우, 1차원적인 접근만 가능해 필드에 접근할 때 추가적긴 연산 로직 같은 부수 작업을 삽입할 수 없다.

접근자와 수정자 메서드를 사용한 캡슐화

필드를 모두 private으로 선언하고, public한 접근자(getter)와 수정자(setter) 메서드를 사용하여 필드에 접근하도록 한다.

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;
    }
}

이렇게 패키지 외부에서 접근할 수 있는 클래스 접근자(getter,setter)를 제공하게 구현을 하면 클래스 내부 표현 방식을 변경해도 API를 수정하지 않고도 변경할 수 있다. 이러한 클래스의 내부 표현 방식으로 언제든지 내부표현을 유연하게 변경할 수 있다.

package-private 클래스나 private 중첩 클래스에서는 public 필드를 사용해도 된다.

책에서는 또 다른 유용한 예로, package-private 클래스나 private 중첩 클래스을 들고 있다.

package-private 클래스는 같은 패키지 내에서만 접근이 가능한 클래스이다. private 중첩 클래스는 해당 클래스를 포함하는 클래스 내에서만 접근이 가능한 클래스이다.

package-private 클래스나 private 중첩 클래스라면, 필드를 노출해도 클래스가 표현하려는 추상 개념만 문제가 되지 않는다.

다음 코드는 private 중첩 클래스에서 public 필드를 사용한 예시이다.

public class Shape {
    private static class Point {
        private final int x;
        private final int y;

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

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }
    }

    public static Point createPoint(int x, int y) {
        return new Point(x, y);
    }
}

위 코드를 보면 Point 클래스는 Shape 클래스 내부에 캡슐화되어 외부에서 직접적인 접근할 수 없다. 하지만 Shape 클래스에서는 얼마든지 Point 객체를 생성하고, Point 객체의 필드에 접근할 수 있다. 이렇게 함으로써 처음 제시된 3가지 문제점을 해결할 수 있고, 클래스와 필드를 선언하는 면에서나 클라이언트 코드면에서나 접근자 방식보다 깔끔하게 표현이 된다.

private 중첩 클래스는 같은 패키지 내에서만 해당 클래스에 대한 접근이 가능하다면 값 변경 시 생성한 객체를 통해서만 접근 및 변경이 가능하다. 그래서 package-private 클래스보다 private 중첩 클래스가 더 제한적이다.

자바 라이브러리에서 public 클래스의 필드를 직접 노출시킨 사례

자바 라이브러리에서도 public 클래스의 필드를 직접 노출하지 말라는 규칙을 어기는 경우가 있다. 책에 서는 이러한 경우를 통해 타산지석으로 삼아야 한다고 한다며, 대표적인 예로 java.awt.package 패키지의 PointDimension 클래스를 들고 있다.

java.awt.Point 클래스 내부

public class Point extends Point2D implements Serializable {
    public int x;
    public int y;
}

java.awt.Dimensoin 클래스 내부

public class Dimension extends Dimension2D implements Serializable {
    public double width;
    public double height;
}

불변 필드를 노출한 public 클래스

그럼 final 키워드로 선언된 불변 필드를 노출하는 경우는 어떨까? 책에서는 public 클래스에서 final 키워드로 선언된 불변 필드를 노출하는 것은 괜찮다고 한다. 하지만 API를 변경하지 않고는 내부 표현을 바꿀 수 없다는 문제점과 부수 작업을 수행할 수 없다는 문제점은 여전히 남아있어 추천하진 않는다.

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);
        }
        if (minute < 0 || minute >= MINUTES_PER_HOUR) {
            throw new IllegalArgumentException("분: " + minute);
        }
        this.hour = hour;
        this.minute = minute;
    }
}

정리

public 클래스는 절대로 가변 필드를 직접 노출해서는 안된다. 불변 필드라도 public으로 선언하면 해당 필드를 수정할 수 없다는 보장이 없다. 그럼에도 필드를 public으로 선언해야 하는 경우라면 package-private 클래스나 private 중첩 클래스에서 사용하는 방법을 사용하자.

@023
focus and hustle