(Effective Java) 생성자 대신 정적 팩터리 메서드를 고려하라

2022. 9. 19. 22:54독서

반응형

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다.

 

 

이 외에도 클래스 인스턴스를 반환하는 다양한 방법이 있고,

 

 

이 문서에서는 <EJ3/E>에 나온 정적 팩터리 메서드에 대해서 소개하려 한다.

 

 

책에서도 언급했지만 디자인 패턴에 있는 팩터리 메서드와는 다르다.

 

 

(팩터리 메서드는 OCP를 목적으로 하고 있고, 정적 팩터리 메서드는 클래스 인스턴스 반환을 목적으로 한다.)

 

 

먼저 팩터리 메서드의 장점에 대해서 알아보자.

 

 

👍 이름을 가질 수 있다


기존의 생성자 코드를 살펴보자.

 

@Getter
class Book {
    private String name;
    private String author;
    
    public Book() {};
    
    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }
    
}


class Main {
    public void static main(String[] args) {
        Book book = new Book("Effective java", "Joshua Bloch");
        System.out.prinln(book.getName()); // Effective java
        System.out.prinln(book.getAuthor()); // Joshua bloch
    }
}

 

위처럼 기존의 코드에서는 public 생성자를 통해서 클래스의 인스턴스를 생성했다.

 

 

Main 클래스의 코드를 보면 생성자는 전통적인 new~~ 방식으로 호출되고 있다.

 

 

이때 정적 팩터리 메서드를 사용하면 이름을 가질 수 있다.

 

 

아래의 정적 팩터리 메서드 코드를 보자.

 

@Getter
class Book {
    private String name;
    private String author;
    
    public static Book createNoArgsBook() {
        return new Book();
    }
    
    public static Book createAllArgsBook(String name, String author) {
        return new Book(name, author);
    }
    
}


class Main {
    public void static main(String[] args) {
        Book book = Book.createAllArgsBook("Effective Java", "Joshua Bloch");
        System.out.prinln(book.getName()); // Effective java
        System.out.prinln(book.getAuthor()); // Joshua bloch
    }
}

 

어떤가?

 

이름을 통해서 충분히 어떤 객체를 리턴하는지 감이 오지 않는가?

 

 

또한 이름을 가질 수 있는 덕분에 이름이 다르면서 같은 역할을 하는 생성자를 여러 개 만들 수 있다.

 

 

👍 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.


이 덕분에 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

 

책에서 나온 예제인 Boolean 예제를 살펴보자.

 

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

 

이 경우는 인스턴스 자체를 생성하지 않는다.

 

 

인스턴스를 생성하지 않는 것이 왜 장점일까? 어떨 때 장점인걸까?

 

 

바로 객체 생성에 많은 비용이 들 경우 이는 분명히 장점으로 작용한다.

 

 

Boolean 같은 경우 많은 메서드를 포함하고 있기 때문에 

 

 

인스턴스를 생성하는 것만으로도 많은 메모리를 필요로 한다.

 

 

이럴 때 정적 팩터리 메서드인 valueOf 같은 것을 사용한다면 그 비용을 줄일 수 있다.

 

 

또 이 인스턴스 통제의 장점은 싱글턴으로 처리할 수도 있다는 것이다.

 

 

아래 코드를 보자.

 

 

public SpringContainer {
    private static final SpringContainer INSTANCE = new SpringContainer();

    private SpringContainer() {
        
    }
    
    public static SpringContainer() {
        return INSTANCE;
    }
}

 

(완벽하게 통제된 싱글턴은 아니다.)

 

 

이렇게 사용하면 정적 팩터리 메서드를 통해서 객체의 인서턴스를 생성하고, 오직 하나의 인스턴스만 리턴 받을 수 있게 된다.

 

 

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


이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다.

 

저자가 '엄청난 유연성'이라고 강조할 만큼 중요하다.

 

 

어쩌면 이어지는 다음 내용인 '매개 변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.'와 일맥상통할 수 있다.

 

 

코드를 살펴보자.

 

 

class Car {
    public static getSportsCar() {
        return new SportsCar();
    }
    
    public static getSUV() {
        return new SUV();
    }
  	
    
    public static getWagen() {
        return new Wagen();
    }
}


class SportsCar extends Car {
....
}


class SUB extends Car {
...
}


class Wagen extedns Car {
...
}

 

위와 같이 Main 코드에서 부를 때, 원하는 하위 인스턴스를 불러 낼 수 있는 능력이 있다.

 

 

그렇기 때문에 엄청난 유연성을 자랑한다.

 

 

꼭 하위 타입이어야 하냐고?

 

 

그렇다 당연히 하위 타입이어야 한다.

 

 

왜냐하면 반환받는 생성되는 변수의 타입은 Car 일 테니 말이다.

 

 

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


위의 내용과 연장선상에 있는 내용이라고 할 수 있다.

 

 

당연히 하위 타입의 인스턴스만 반환할 수 있다.

 

 

코드를 살펴보자.

 

 

public Ticket {
    public static Ticket(int age) {
        if (age < 7) {
            return new FreeTicket();
        } else if (age < 20) {
            return new StudentTicket();
        } else if age < 65) {
            return new adultTicket();
        } else {
            return new elders();
        }
    }
}

 

입력 매개변수에 따라서 다른 타입의 인스턴스를 반환할 수 있다.

 

 

코드는 따로 적지는 않았지만, 당연히 FreeTicket(), StuentTicket() 등 Ticket 종류는 모두 Ticket을 상속받고 있다.

 

 

이렇게 되면 클라이언트는 팩터리 메서드가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없어진다.

 

 

그저 하위 클래스이기만 하면 되기 때문이다.

 

 

👍 반환 객체 타입 미결정


책에서는 이를 JDBC에 빗대어 설명하고 있지만 JDBC 예제로는 이해하기가 어렵다.

 

 

이 설명을 이해하려면 라이브러리에 있는 것을 로딩한다는 전제를 이해하고 있는 게 편할 것이다.

 

아래 코드를 한 번 보자.

 

class Device {
    public static getGadgets() {
        ServiceLoader<AppleDevice> lists = ServiceLoader.load(AppleDevice.class);
        Optional<AppleDevice> deviceOptional = lists.findFirst();
        deviceOptional.ifPresent(d -> {
            System.out.println(d.getName());
        }
   }
}

이런 코드의 경우 AppleDevice라는 클래스가 실제로는 구현되어 있지 않고,

 

 

구현체에 대한 정보만 있으면 정상적으로 작동할 수 있다.

 

 

이때 반드시 구현체에 대한 dependency는 있어야 한다.

 

 

자세한 설명은 백기선 님의 인프런 강의 (맛보기 영상)를 보는 것을 추천한다.

 

 

 

 

지금까지 팩터리 메서드에 대한 장점을 알아봤다.

 

 

그럼 단점을 알아보도록 하자.

👎정적 팩터리 메서드 only => 하위 클래스 생성 불가


어떤 부분에서는 이것이 장점일 수 있다.

 

 

하지만 상속을 필요로 하는 클래스에서는 이는 분명한 단점이다.

 

 

왜 안 될까? 정확히는 어떤 경우에서 안 될까?

 

 

책에서 예시를 들고 있는 Collections 프레임 워크의 클래스를 살펴보자.

 

 

public class Collections {
    private Collections() {
    }
    /.../
}

 

생성자가 존재하면서 기본 생성자가 private이다.

 

 

상속이 가능하려면 상위 클래스에는 public 혹은 protected 생성자가 존재해야 하므로 

 

 

상속이 불가능하다.

 

 

 

👎정적 팩터리 메서드는 프로그래머가 찾기 어렵다


 

간단한 예시로 Date 클래스를 열어서 확인해보자.

 

 

어떤 것이 정적 팩터리 메서드이고 아닌지 구분이 쉽게 되는가?

 

 

그렇지 않다.

 

 

책에서는 Javadoc이 이 역할을 했으면 좋겠다고 했지만 Javadoc으로 구분하기란....

 

 

 

 

🧐마치며


 

정적 팩터리 메서드에 대해서 알아봤다.

 

 

실제로 현업에서도 많이 사용하는 것 같다.

 

 

클래스 종속적이지 않고, 굳이 인스턴스 생성이 필요 없는 경우에 

 

 

사용을 고려하면 좋을 것 같다.

 

 

 

 

반응형