본문 바로가기
☕️ JAVA/🦋 Effective Java

객체 생성할 때 '생성자' 대신 '정적 팩터리 메서드'를 써볼까? 🏭

by kukim 2022. 1. 15.

*이 글은 책 '이펙티브 자바 3판 Item 1'과 하단 Reference 참고했습니다. 잘못된 내용이 있다면 편하게 말씀해주세요 🙏🏻

 

주제

객체 생성 : 인스턴스 생성 방법두 가지가 있다 하나는 전통적인 public 생성자 방법과 또 다른 하나는 정적 팩터리 메서드(static factory method) 방법이다.

 

예제

// 두 가지 인스턴스 생성 방법
public class Book {
    private String title;
    private String author;

		// 1. public 생성자
    public Book(String title) { 
        this.title = title;
    }
		
		// 2. 정적 팩터리 메서드
    public static Book createBookWithTitle(String title) {
        Book book = new Book();
        book.title = title;
        return book;
    }

		// 사용 방법
    public static void main(String[] args) {
        Book book = new Book("이펙티브자바"); // public 생성자 활용하여 인스턴스 생성
        Book book2 = Book.createBookWithTitle("이펙티브자바"); // 정적 팩터리 메서드 생성 방법
    }
}

 

결론

인스턴스 생성 시 습관적으로 public 생성자만 고려하지 말고 정적 팩터리 메서드도 고려하자! (보통 정적 팩터리 메서드를 사용하는 게 유리한 경우가 더 많다. 하지만 그게 100% 옳다고는 할 수 없겠지만)

정적 팩터리 메서드는 public 생성자에 비해 어떤 장단점이 있을까?

정적 팩터리 메서드 장점

✅  이름을 가질 수 있다.

정적 팩터리 메서드는 말 그대로 메서드이기 때문에 이름을 가질 수 있다. 이름을 활용하여 인스턴스 생성 시 사용자에게 리턴할 인스턴스의 이름이나 인스턴스 생성에 필요한 매개변수의 의미를 쉽게 파악할 수 있도록 도와준다.

public static void main(String[] args) {
    Book book = new Book("이펙티브자바"); 
    Book book2 = Book.createBookWithTitle("이펙티브자바"); // 의미파악, 매개변수 이해 Good
}

 

✅  동일한 매개변수의 인스턴스 생성자(시그니처)는 하나만 가능하지만 팩터리 메서드는 제약이 없다.

public class Book {
    private String title;
    private String author;

    public Book(String title) {
        this.title = title;
    }

    // 똑같은 생성자 생성할 수 없음
//    public Book(String author) {
//        this.title = author;
//    }

}
public class Book {
    private String title;
    private String author;

	public static Book createBookWithTitle(String title) {
        Book book = new Book();
        book.title = title;
        return book;
    }
	// 가능
    public static Book createBookWithAuthor(String author) {
        Book book = new Book();
        book.title = author;
        return book;
    }
}

 

✅ 호출될 때 마다 인스턴스를 생성하지 않아도 된다.

인스턴스 할당이 자주 반복된다고 할 때, 매번 인스턴스를 생성해주면 메모리상 불리할 수 있다. 하지만 팩터리 메서드를 사용하여 이 과정을 인스턴트 캐싱을 하여 클래스의 static final 인스턴스를 만들고, 요청이 들어올 때 매번 리턴해주면 된다. 아래 예는 실제 Boolean 클래스의 valueOf 메서드이다. 이는 매개변수 b가 true라면 static final 객체인 TRUE를 리턴하고 false라면 FALSE를 리턴한다. 

public final class Boolean implements java.io.Serializable, Comparable<Boolean> {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
	// 생략
}

 

✅ 반환 타입이 하위 타입 객체를 반환할 수 있는 능력이 있다.

구현 클래스 전체를 공개하지 않아도 객체를 반환할 수 있다. 반환할 객체 클래스를 선택할 수 있다는 것은 '유연성'을 가질 수 있다. 이는 사용자 입장에서 구현체를 일일이 몰라도 쉽게 사용할 수 있다. 예를 들어 java.util.collections의 정적 팩터리 메서드에서 하위 타입 객체 UnmodifiableRandomAccessList 나 UnmodifiableList 등 하위 타입 객체를 반환할 수 있다. 

public class Collections {
  
    private Collections() {
    }

    // 정적 펙토리 메서드, 입력 매개변수에 따라 리턴해주는 하위 객체가 다르다.
    public static <T> List<T> unmodifiableList(List<? extends T> list) {
        return (list instanceof RandomAccess ?
                new UnmodifiableRandomAccessList<>(list) :
                new UnmodifiableList<>(list));
    }
    
 }

 

 

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

위의 코드 Collections() 예에서 입력 매개변수에 따라 (RandomAccess 인지 아닌지에) 리턴해주는 조건을 다르게 주고 있다.

아래 예는 마찬가지로 정적 팩터리 메서드를 통해 입력 매개변수 (꼭짓점 개수)에 따라 리턴해주는 하위 객체가 다른 것을 볼 수 있다.

public class Figure {
	// ...

	protected Figure(List<Point> points) {
        this.points = points;
        this.vertex = points.size();
    }

	public static Figure createFigureWithPoints(List<Point> points) {
        int vertex = points.size();

        switch (vertex) {
            case LINE_VERTEX:
                return new Line(points);
            case TRIANGLE_VERTEX:
                return new Triangle(points);
            case SQUARE_VERTEX:
                return new Square(points);
            default:
                return new Polygon(points);
        }
    }
    
	// ...    
}

public class Square extends Figure {

    public Square(List<Point> points) {
        super(points);
    }
    
    // ...
}


public class Main {
    public static void main(String[] args) {
        List<Point> points_4 = init4Point();
        List<Point> points_3 = init3Point();

        Figure square = Figure.createFigureWithPoints(points);
        Figure triangle= Figure.createFigureWithPoints(points); 
    }

 

✅ 정적 팩터리 메서드는 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 된다.

위 말의 의미는 반환할 객체를 펙토리 메서드 방식을 이용하여 만들기 때문에, JDBC에서도 각 상황에 따라서 펙토리 메서드 내용만 바꿔서 연결에 필요한 객체를 얻을 수 있다고 한다. 아직 이해가 어려워 좀 더 살펴봐야겠다. (문장 출처 : 이펙티브 자바 스터디 김민걸님)

 

정적 팩터리 메서드 단점

📍 정적 팩터리 메서드만 사용하면 상속, 하위 클래스를 만들 수 없다.

정적 팩터리 메서드를 사용할 때 private 생성자를 통해 외부 생성을 막는다. 하지만 이는 상속, 하위클래스를 만들 수 없다는 의미이다.

위의 Figure 예에서는 정적 팩터리 메서드만 사용하면 상속, 하위 클래스를 만들어줘야한다. Line, Square, Triangle, Polygon들은 Figure을 상속받고있기 때문에 protected 생성자가 있다. (이 패턴이 좋은지는 모르겠다.)

 

📍 정적 팩터리 메서드는 사용자(프로그래머)가 찾기 힘들다

이는 JavaDoc API 문서에 생성자는 자동으로 생성되지만 정적 팩터리 메서드는 사용자 입장에서 바로 드러나지 않는다. 따라서 JavaDoc 문서 작성시 상위에 정적 팩터리 메서드에 대해 명시해주는 것이 좋다. 또한 팩터리 메서드에 대해 널리 알려진 명명규칙을 쓰는 것도 좋다.

 

명명 규칙

명명 규칙 설명
from() 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드. Data d = Date.from();
of() 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드. Set<Rank> faceCards = Enum.of(JACK, QUEEN, KING);
valueOf() from 과 of 의 더 자세한 버전 BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance(), getInstance() (매개 변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지는 않는다. StackWalker luke = StackWalker.getInstance(options);
create(), newInstance() instance 혹은 getInstance와 같지만 매번 새로운 인스턴스를 생성해 반환함을 보장한다.  Object newArray = Array.newInstance(classObject, arrayLen);
getType() getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. FileStore fs = Files.getFileStore(path);
newType() newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. BufferedReader br = Files.newBufferedReader(path);
type() getType과 newType의 간결한 버전 List<Complaint> litany = Collections.list(legacyLitany);

 


⛓ Reference

http://www.yes24.com/Product/Goods/65551284

https://github.com/Meet-Coder-Study/book-effective-java

https://kukim.tistory.com/58

댓글