이펙티브 자바

[아이템 4] 인스턴스화를 막으려거든 private 생성자를 사용하라

에드박 2021. 4. 20. 17:55

이따금 단순히 static 메서드와 static 필드만을 담은 클래스를 만들고 싶을 때가 있습니다.

(객체 지향적이지 못해서 남용하면 좋지않지만.. 나름의 쓰임새가 있다!)

 

java.lang.Math 와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있습니다.

 

Java Arrays document(출처 : https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html)

 

java.util.Collections 처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드 혹은 정적 팩터리 메서드를 모아놓을 수 있습니다.

(자바 8부터 이런 메서드를 인터페이스에 모아둘 수 있습니다.)

Java Collections document(출처 : https://docs.oracle.com/javase/7/docs/api/java/util/Collections.html)

 

final 클래스와 관련한 메서드들을 모아놓을 때도 사용합니다.

-> final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가능하기 때문입니다.

 

정리하면

static 메서드와 static 필드만 담을 클래스를 사용하는 경우

  • 기본 타입 값이나 배열 관련 메서드들을 모아놓고 싶은 경우
  • 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드 혹은 정적 팩터리를 모아놓는 경우
  • final 클래스와 관련한 메서드들을 모아놓고 싶은 경우

보통 이렇게 만든 클래스가 유틸리티 클래스입니다.

 

유틸리티 클래스는 인스턴스로 만들어서 사용하려고 설계한것이 아니기 때문에 생성자의 존재가 필요없습니다.

그렇다고 생성자를 명시하지 않는걸로 끝낸다면?

  • 컴파일러가 친절하게 기본 생성자를 추가해줍니다.

아래는 생성자를 명시하지 않은 클래스 입니다.

package item4_20210420;

public class PrivateConstructorEx1 {

    void doSomething() {
        System.out.println("뭔가를 실행");
    }
}

 

위의 코드를 컴파일한 뒤

-> 바이트 코드를 디컴파일한 파일을 보면 아래와 같이 기본 생성자가 추가되어 있습니다.

 

추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없습니다. 하위 클래스를 만들어서 인스턴스화 하면 그만입니다.

package item4_20210420;

public class PrivateConstructorEx2 {
    public static void main(String[] args) {
        MyAbstractClass myAbstractClass = new MyChildClass();
    }
}

abstract class MyAbstractClass {

    void doSomething() {
        System.out.println("뭔가를 할겁니다.");
    }
}

class MyChildClass extends MyAbstractClass {

    @Override
    void doSomething() {
        System.out.println("자식이 뭔가를 합니다.");
    }
}

 

더불어 추상클래스를 본 사용자는 상속해서 쓰라는 뜻으로 오해할 수도 있습니다.(아이템 19)

 

따라서 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니

간단하게 기본 생성자를 private 생성자로 추가하면 클래스의 인스턴스화를 막을 수 있습니다.

 

package item4_20210420;

public class UtilityClass {

    // 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용)
    private UtilityClass() {
        throw new AssertionError();
    }
    
    void doSomething() {
        System.out.println("뭔가를 실행함");
    }
}

 

기본 생성자의 접근자를 private 로 명시했으니 클래스 바깥에서는 기본 생성자에 접근할 수 없습니다.

즉, 클래스 외부에서는 인스턴스를 생성할 수 없습니다.

 

꼭 AssertionError()를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 해줍니다.

 

그런데 "생성자가 분명 존재하는 데 호출할 수 없다니? " 라고 착각할 수 있고 직관적이지 않을 수 있습니다.

그래서 위의 코드처럼 적절한 주석을 달아두도록 합니다.( 주석도 싫지만.. )

 

이 방식은 상속을 불가능하게 하는 효과도 있습니다.

모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데 ( 예시. super() ) 

상위 클래스의 생성자가 private 로 선언되어 있으니 하위 클래스가 접근할 길이 막혀있습니다.