반응형

ITEM 4 "인스턴스화를 막으려거든 private 생성자를 사용하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 인스턴스를 막기 위해 private 생성자를 사용할 것을 권장하고 있다.

 

가끔 유틸 클래스처럼 정적 메서드와 정적 필드만 있는 클래스를 설계할 때가 있다. 유용한 메서드들을 한 곳에 모아둘 때 사용하곤 하는데 객체 생성 없이 사용할 수 있어 객체지향적 사고는 아니다. 이러한 클래스는 굳이 생성자가 필요하지 않기 때문에 생성자 생성을 제한하는 장치가 필요하다.

 

public abstract class UtilClass {
    public static String getName() { return "gold-egg"; }

    static class AnotherClass extends UtilClass {

    }

    public static void main(String[] args) {
        // abstract 추상 클래스에서는 인스턴스를 만들지 못 한다.
        UtilClass utilClass = new UtilClass();
        // 그 대신 상속해서 사용이 가능하다.
        AnotherClass anotherClass = new AnotherClass();
        // anotherClass.getName() 이 불가하다.

        UtilClass.getName();
    }
}

 

위 코드와 같이 생성자를 만들지 않으면 컴파일러가 자동으로 public 생성자를 만들어 준다. 사용자는 생성자가 자동 생성 된 것인지, 컴파일러가 만들어 준 것인지 알 수 없다. 이렇게 생성된 생성자를 사용할 경우, 잘못된 side effect 를 초래할 수 있다.

 

보통 Spring 프레임워크처럼 abstract 추상화 클래스를 선언하여 인스턴스 생성을 방지한다. 추상화 클래스를 상속하여 하위 클래스에서 인스턴스를 만들 수 있기 때문에 완벽한 금지 방법은 아니다. 또한, abstract 클래스는 원래 상속을 권장하기 위해 만들어진 클래스이므로 오해할 가능성이 있다. 그래서 책에서는 private 생성자를 명시하여 인스턴스화를 막는 것을 권장하고 있다.

 

클래스가 정말 정적 멤버(static 변수와 static 함수) 로만 이루어져 있다면 설령 자식 클래스로 인스턴스를 만든다고 하더라도 해당 static 함수를 사용할 수가 없다. anotherClass.getName() 부분이 불가한 것을 볼 수 있다. 인스턴스를 생성하더라도 못 쓰는 것이다. 아마도 Spring 프레임워크에서도 이 이유 때문에 abstract 클래스만 선언하여 사용하는 것으로 추정된다.

 

그럼에도 불구하고 하위 클래스에서도 생성자 생성을 금지하고 싶고 생성자를 호출하지 않게 하려면 아래와 같이 private 생성자를 만들고 접근 시 AssertionError 예외를 발생시키라고 권고하고 있다.

 

public class Test {
	private Test() { throw new AssertionError(); }
}

 

반응형
반응형

ITEM 3 "private 생성자나 열거 타입으로 싱글톤임을 보장하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 private 생성자나 열거 타입으로 싱글톤임을 보장해야 한다고 서술하고 있다.

 

싱글톤 패턴이란, 객체의 인스턴스가 오직 1개만 생성이 되는 패턴을 말한다.

최초 한 번만 메모리에 할당이 되고 그 인스턴스를 어디에서든지 참조할 수 있도록 할 수 있다. 어플리케이션 로딩 개의 인스턴스 생성만 필요한 경우에 패턴을 사용한다. 설정 파일이나 데이터베이스 연결 등이 예이다.

 

싱글톤 클래스를 사용할 , 생성된 객체를 공용으로 사용하고 별도로 생성하는 것을 금지한다고 약속할 있다. 하지만 사람이기 때문에 실수할 있다. 한 번만 생성해야 하는 객체를 여러 번 생성할 수 있다.

 

그래서 클래스 선언 외부에서 인스턴스를 생성하지 못하게 별도의 장치를 설정하는데 이번 장에서 소개할 private 생성자와 열거 타입이 주인공이다.

 

번째 방법은,

private 생성자를 사용하여 클래스 내부에 public static final 필드로 객체를 생성해두어 인스턴스를 반환하는 방법이다. final 필드이기 때문에 초기화 번만 호출된다.

 

// 선언부
public class Test {
    public static final Test INSTANCE = new test();
    private Test(){}
}

// 사용부
Test test1 = Test.INSTANCE;
Test test2 = Test.INSTANCE;

 

번째 방법은,

번째 방법과 유사한데 public static final 필드를 private static final 필드로 바꾸고, 정적 팩토리 메서드를 제공하는 것이다.

 

public class Test {
    private static final Test INSTANCE = new test();
    private Test() {}
    public static Test getInstance() { return INSTANCE; }
}

 

방법 모두 항상 같은 객체를 반환하므로 안전해보인다. 책에서는 Reflection API 통해 setAccessible 사용해 권한을 획득하고 private 생성자를 호출할 있다고 하는데 사실 협업 과정에서 저렇게까지 코딩하지는 않을 같다. ㅎㅎㅎ;;; 싱글톤처럼 전역적으로 사용하는 객체는 안전하게 제공해야 의무가 있기 때문에 책에서 제안하는 것처럼 생성자에 번째 생성자를 생성할 Exception 예외를 던지자.

 

// 생성자 내부에서 count 변수 체크 후 1 이상이면 예외 출력
static int count;
private Test(){
    count++;
    if (count != 1) {
        throw new IllegalStateException("this object should be singleton");
    }
}

 

번째 방법은 간결하고 해당 클래스가 싱글톤이라는 점을 명백히 알려준다.

public static final 이니 절대로 다른 객체를 참조할 없다.

 

번째 방법은 싱글톤이 아니게 변경할 있다는 장점이 있다. 팩터리 메서드에서 반환하던 싱글톤 객체를 다른 객체로 변경이 가능하다. 제네릭 싱글톤 팩터리로 만들어 있다. 마지막으로 정적 팩터리의 메서드 참조를 공급자로 사용할 있다. getInstance Supplier<객체> 사용하는 식이다.

이러한 장점들이 있지만 싱글톤이 아니게 변경될 있다는 유연한 구조 때문에 번째 방법의 장점을 사용하지 않을 것이라면 가급적 번째 방법을 권장하고 있다.
 

안전하게 싱글톤 객체를 만들기 위해 가지 경우를 생각해야 한다. Java 에서는Serializable 클래스로 직렬화를 제공하고 있는데 역직렬화할 때 가짜 객체를 생성한다. 가짜 객체 대신 진짜 객체를 반환하기 위해서 readResolve 함수를 재정의해야 한다. (모든 필드를 transient 선언해야 한다는 점도 잊지 말아야 한다.) 아래는 Java 에서 Serializable 클래스를 사용했을 직렬화/역직렬화 구조이다.

 

 

앞서 소개한 방법은 리플렉션이나 직렬화 문제 때문에 보완적인 코드들을 많이 생성해야 한다. 마지막 번째 방법, Enum 열거를 활용한 방법도 있다. 복잡한 직렬화 상황이나 리플렉션 구조에도 완벽히 방어할 있으며 간단한다. 하지만 상속은 못 한다는 단점이 있다.

 

// 선언부
public enum Test {
    INSTANCE;
    public String getName() {
        return "gold-egg";
    }
}

// 사용부
String name = Test.INSTANCE.getName();

 

반응형
반응형

ITEM 2 "생성자에 매개변수가 많다면 빌더를 고려하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 2편 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 생성자에 매개변수가 많다면 빌더 사용을 권장하고 있다.

 

public class Pizza {
    private final String name;
    private final String size;
    private final String[] toppings;

    // 이름만 가지는 생성자
    public Pizza(String name) {
        this.name = name;
    }

    // 이름과 크기를 가지는 생성자
    public Pizza(String name, String size) {
        this.name = name;
        this.size = size;
    }

    // 모든 필드를 가지고 있는 생성자
    public Pizza(String name, String size, String[] toppings) {
        this.name = name;
        this.size = size;
        this.topings = toppings;
    }
}

 

여기 피자에 대한 간략한 정보를 가지는 클래스가 있다.

피자라는 객체를 만드려고 할 때, 이름 매개변수만 필요한 경우가 있을 수 있고, 이름과 크기를 모두 가지는 경우, 필드 모두가 필요한 경우가 있을 수 있다. 그 때마다 보통 위 코드와 같이 점층적으로 생성자를 만들곤 한다.

 

그런데 이런 점층적 생성자 패턴은 아래와 같은 단점을 가지고 있다.

1 . 매개변수 개수가 많아지면 클라이언트 코드 부분에서 작성하다가 실수할 수도 있고 읽기도 어렵다.

Pizza("치즈", "L", "오이, 불고기"); 이렇게 작성했을 때 각각이 가지고 있는 필드가 어떤 의미를 나타내는지 정확히 알 수 없다.

2 . 사용자가 설정하길 원하지 않는 매개변수도 강제로 포함해서 값을 지정해주어야 한다.

예를 들어 사이즈는 상관없는 피자를 만들고 싶은데 강제로 사이즈 값을 넣어주어야 한다. 선택적인 매개변수가 중간에 위치해 있는 경우, 필수 매개변수가 되어버린다.

 

그렇다면 어떻게 해야 필수 매개 변수와 선택적인 매개 변수를 적재적소에 넣어 셋팅할 수 있을까?

 

대안 1 자바 빈즈 패턴

매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 호출해서 원하는 매개변수의 값을 설정하는 방식이다.

 

Pizza myPizza = new Pizza();
myPizza.setName("컴비네이션");
myPizza.setToppings("치즈, 올리브");

 

이렇게 지정하면 중간에 원하지 않은 매개변수는 피해서 셋팅할 수 있고 읽기 쉬운 것처럼 보인다.

하지만, 객체 하나를 만드려고 메서드를 여러 개 호출해야 되고 객체가 완성이 되기 전까지는 일관성이 무너진다. 중간에 다른 코드에서 setter 함수가 불릴 수 있으며 불변 객체로 만들 수 없다. 또 이런 코드는 Thread-Safe 하지 않다.

이런 문제점을 해결하고자 자바스크립트 같은 언어에서는 freeze 라는 메서드를 지원하고 있지만, 모든 언어 체계에서 지원하는 것은 아니다. 설령 이 방법을 쓴다고 하더라도 프로그래머가 이 객체에 freeze 하는 구간이 어디 있는지 소스코드를 보며 찾아야 한다는 단점이 있다.

 

대안 2 빌더 패턴

필수 매개 변수만으로 생성자를 호출해 빌더 객체를 얻고 그 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수들을 설정하는 방법이다. 마지막에는 build() 라는 메서드를 호출해 하나의 체인 형태로 구성한다.

 

Pizza myPizza = new Pizza.Builder("콤비네이션")
	.size("L")
    .toppings("불고기, 치즈, 감자")
    .build();

 

빌더의 생성자나 메서드에서 여러 매개 변수들을 혼합해서 유효성 검사도 할 수 있다는 장점이 있으니 잘못된 매개변수를 검증하는 코드도 추가하자. 보통 매개변수를 객체로 복사해온 다음 확인하고, 검증에 실패하면 IllegalArgumentException 에러 메세지를 던져 어떤 매개변수가 잘못 되었는지 확인한다.

 

이 빌더패턴을 활용하면 클래스 계층 구조를 잘 활용할 수 있다. 아래와 같이 추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서 추상 클래스를 상속받으면, 각 하위 클래스에서 추상 빌더를 상속받아 만들 수 있다.

 

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, SAUSAGE
    }

    final Set<Topping> toppings;

    abstract static class Builder<T extends  Builder<T>> { // `재귀적인 타입 매개변수`
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }

}

 

추상 빌더에서 재귀적인 타입 매개변수를 사용하고 self 라는 메서드를 통해 자기 자신을 호출하는 부분이 흥미롭다.

하위 클래스에서 build 메서드의 리턴 타입으로 자기 자신을 리턴하는 Covariant 리턴 타이핑을 사용하면 클라이언트 코드에서 타입 캐스팅을 하지 않아도 된다.

 

public class NyPizza extends Pizza {

    public enum Size {
        SMALL, MEDIUM, LARGE
    }

    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }


        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

 

이렇게 설계해두면 가변 인자 매개변수를 여러 개 사용할 수 있다는 장점도 있고, addTopping 메서드를 여러 번 호출해서 전달받은 매개변수들을 하나의 필드에 모아두는 것도 가능하다. 매번 생성하는 객체를 조금씩 다르게 변화를 줄 수도 있다.

 

이 책에서는 빌더 패턴은 생성자 이전에 빌더 객체를 만들어야 하므로 성능 이슈를 가져올 수도 있다고 하는데 그렇게 크지 않아 보인다. 점차 요구사항이 늘어나면 매개변수가 많아질 가능성이 많으니 빌더 패턴을 잘 학습하고 알아두었다가 앞으로 늘어날 가능성이 있는 클래스 생성자에 적극적으로 활용해보자.

 

"생성자와 팩토리 메서드에 매개 변수가 많을 경우, 빌더 패턴을 고려하자"

반응형
반응형

ITEM 1 "생성자 대신 정적 팩토리 메서드를 고려하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 1편 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 생성자 대신 정적 팩토리 메서드를 사용하는 것을 권장하고 있다.

 

public class Point {
	double x, y;
    
    // 생성자
    public Point(double x, double y) {
    	this.x = x;
        this.y = y;
    }
    
    // 정적 팩토리 메서드
    public static Point asPolar(double rho, double phi) {
    	double x = rho * Math.cos(phi);
        double y = rho * Math.sin(phi);
        return new Point(x, y);
    }
    
    // 이렇게 생성할 수 없음. 위 생성자와 타입과 인자의 갯수가 같으므로
    public static Point(double rho, double phi) {
    	this.x = rho * Math.cos(phi);
        this.y = rho * Math.sin(phi);
    }
}

 

여기 위와 같이 직교좌표나 극좌표에서 하나의 점을 표시하는 클래스가 있다.

클래스 이름과 같은 함수를 생성자라고 하고, "public static" 키워드가 붙고 클래스 객체를 생성하는 함수를 정적 팩터리 메서드라고 한다. 생김새만 다를 뿐 생성자와 같은 역할을 하는데 정적 팩터리 메서드를 권장하고 있다.

 

정적 팩터리 메서드는 아래와 같은 장단점을 가지고 있다.

 

장점 1 이름을 가질 수 있다.

생성자는 클래스 이름과 동일하게 함수 이름을 작성해야 한다는 규칙이 있다. 그래서 매개변수와 생성자의 이름만으로 반환될 객체의 특성을 제대로 설명하지 못 한다는 특징이 있다. 하지만 정적 팩토리 메서드는 위 예제와 같이 극좌표계에서 쓰일 객체를 "asPolar" 라는 이름으로 잘 설명해주고 있다.

 

또 타입의 순서와 갯수가 같은 동일한 생성자를 만들 수 없다. 자바에서는 오버로딩을 지원하고 있지만 어디까지나 매개변수 타입의 순서와 갯수가 같아야 한다. 위 코드에서 극좌표계 생성자를 만들다가 실패한 코드가 그 예제이다. 

 

장점 2 새로운 객체를 생성할 필요는 없다.

생성자는 이름 그대로 반드시 하나의 객체를 생성해야 한다. 그렇지만 정적 팩토리 메서드는 프로그래머가 임의로 작성한 메서드이기 때문에 제약이 없다. 불변한 객체를 미리 만들어 놓고 그 객체를 재사용하는 식으로 코드를 작성할 수 있다. 객체가 불변함이 보장된다면 굳이 하나의 객체를 생성해야 하는 생성자는 메모리 비용이 많이 드는 나쁜 선택이다.

 

public class Point {
	
    private static Point pointInstance;
	private static final Point SINGLE_POINT_OBJECT = new Point();

    public static Point getPoint() {
        return SINGLE_POINT_OBJECT;
    }

    public static Point getPointWithSingleTon() {
        if (pointInstance == null) {
        	pointInstance = new Point();
        }
        return pointInstance;
    }
}

 

이렇게 객체 생성을 컨트롤 할 수 있어 인스턴스 통제 클래스라고 부르며 언제, 어디까지 인스턴스를 살게 할 수 있을지 결정할 수 있다. 싱글톤 패턴을 만들 때에도 유용하고 불변 클래스를 만들 때에도 사용된다.

 

장점 3 반환 타입의 하위 타입 객체를 반환할 수 있다.

말 그대로 상속받은 자식 클래스의 객체를 반환할 수 있다는 뜻이다. 자식 클래스의 객체를 반환할 수 있게 해준다면 그 자식 클래스를 공개하지 않고도 객체를 사용할 수 있다는 것이다. 명시한 인터페이스만 다른 시스템에 노출시키고 구현체는 숨김으로써 객체지향의 OCP 원칙을 잘 지키며 설계할 수 있다. 이를 인터페이스 기반 프레임워크라고 부른다.

 

다른 시스템에 노출 시키려면 모두 public 으로 제한자를 풀어야 되는데 구현체는 private 로 숨기고 API 만 public 으로 만들고 그 인터페이스 정적 팩토리 메서드에서 구현체들을 반환만 해주는 것이다.

 

자바 8 이전에는 인터페이스에서 정적 메서드를 만들 수가 없었다고 한다. 그래서 인스턴스화가 불가한 동반 클래스로 만들어 그 안에 정의했다고 한다.

 

장점 4 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

장점 3의 연장선 상의 개념이다. 조건에 따라 하위 타입의 객체를 반환할 수 있게 설계가 가능하다.

이 책에서는 EnumSet 클래스를 예로 들며 이 클래스는 public 생성자가 없고 원소의 갯수에 따라 객체를 반환하는 메서드만 있다고 한다. 원소가 64개 이하면 RegularEnumSet 객체를, 65개 이상이라면 JumboEnumSet 객체를 반환한다.

Intellij 에서 직접 확인해봤다.

 

 

맨 마지막 EnumSet 생성자 보면 접근 제한자가 없다. 같은 패키지 내에서만 호출될 수 있다.

 

 

그리고, noneOf 이라는 정적 팩토리 메서드에서 원소의 개수에 따라 하위 클래스의 객체를 반환하는 것을 볼 수 있다.

이 클래스를 사용하는 프로그래머 입장에서는 저런 세세한 구현을 알지 못하더라도 공통적인 기능의 EnumSet 을 사용할 수 있게 된다. 장점 3에서 이야기 하였듯이 실제 구현은 공개하지 않았는데도 명세서만 보고 개발이 가능하다.

 

장점 5 정적 팩토리 메서드를 작성할 때 반환할 객체의 클래스가 존재하지 않아도 된다.

책에서는 서비스 프로바이더 프레임워크를 예시로 들면서 설명하고 있는데 그 개념을 모르는 사람은 어렵기도 하고 이해가 가지 않을 수 있다. 쉽게 설명하면 "구현""정의" 그리고 "사용" 관점에서 나누어 설계되어 있는 프레임워크라고 보면 된다. 정의부는 이 시스템에서 제공하고자 하는 명세서를 작성하는 담당을 하고, 구현부는 실제 그 기능을 만드는 구현체이다. 사용부는 사용자가 이 시스템에 접근해서 서비스를 요청하는 담당을 한다. 여기서 사용부는 어떻게 구현되었는지 알 필요가 없다. 무엇을 요청할지만 알면 된다. 그래서 사용부의 API 는 정의 인터페이스만 반환할 뿐이다.

정의 인터페이스와 연결만 되어 있으니 반환할 객체의 클래스가 존재하지 않아도 개발이 가능하다.

 

단점 1 상속이 불가능하다. == 하위 클래스를 만들지 못 한다.

상속을 하려면 public 이나 protected 생성자가 필요한데 정적 팩토리 메서드가 구현된 클래스는 생성자가 불필요하기 때문에 private 으로 보통 선언한다. 따라서 상속이 불가능하며 하위 클래스를 만들지 못한다.

나중에 "상속보다 합성", "불변 클래스" 라는 말을 많이 듣게 될텐데 상속이 불가능하다는 것은 단점이라기 보다는 장점으로 보일 수 있다.

 

단점 2 프로그래머가 정적 팩터리 메서드를 찾기가 어렵다.

단점이라기 보다는 불친절에 가깝다. 자바독을 보면, 생성자는 설명을 잘 해두지만 정적 팩터리 메서드는 직접 개발자가 소스코드를 뒤지며 찾아야 한다. 안 써있다. 앞으로 개발자들이 생성자와 같은 역할을 하는 정적 팩터리 메서드도 잘 문서화하면 문제가 없을 것이다. 그 이전에 작성해두었던 정적 팩토리 메서드는 어떻게 찾아야 될까?

흔히 사용되는 정적 팩터리 메서드 명명 방식들을 눈에 익혀두고 생각이 안 나면 이 접두사들을 보고 찾자.

 

from 매개변수가 하나일 때 사용
of 여러 매개변수를 받을 때 사용
valueOf from 과 of 의 더 자세한 버전
getInstance / instance 매개변수로 명시한 인스턴스 반환
newInstance / create getInstance/instance 와 기능은 같지만, 매번 새로운 인스턴스를 생성해서 반환
getType 여기서 Type 은 다른 클래스 이름. 다른 클래스의 객체를 반환
newType newInstance 와 같지만, 다른 클래스의 객체를 반환
type getType 과 newType 의 간결한 버전

 

"정적 팩토리 메서드를 고려하고 항상 문서화하자."

반응형

+ Recent posts