Java - 정적 팩토리 메서드 패턴

Terror·2024년 9월 17일
0

사전적 정의

  • 개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체 생성 디자인 패턴 이다
  • 별도의 "객체 생성 역할을 하는 클래스 메서드"를 통해 간접적으로 객체 생성을 하는 방식이다
  • 이것을 통칭 "정적 팩토리 메서드 패턴" 이라고 한다
package 정적팩토리메서드패턴;

public class Book {
    private Book() {}
    
    public static Book of(){
        return new Book();
    }
}

package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        Book book = Book.of();
    }
}
  • 정적 팩토리 메서드는 이름에서도 알 수 있듯이, GOF의 "팩토리 메서드","추상 팩토리" 패턴의 개념을 따와 심플하게 변형시킨 팩토리 변형 패턴 종류라고 볼 수 이다
  • 그럼 "왜?" 만들었는가?
  • 정적 팩토리 메서드는 단순히 생성자의 역할을 대신 이행하는것 뿐 아니라, 개발자가 좀 더 가독성 좋은 코드를 작성하고 객체 지향적으로 프로그래밍 할 수 있게 도와준다
  • 또한 생성자의 본질적인 문제점을 극복하기 위해서 나오기도 하였다, 이에 대해 자세히 알아보자!

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

  • 자바 프로그래밍의 간판이라고도 하는(그렇군) 조유사 블로크의 저서 [이팩티브_자바] 에서 " 생성자 대신 정적 팩토리 메서드를 고려하라" 이다
  • 무슨 연유에서 일까 ?

정적 팩토리 메서드 특징

1. 생성 목적에 대한 이름 표현이 가능하다

  • 지금까지 클래스를 설계할때 다양한 타입의 객체를 생성하기 위해, 생성 목적에 따라 생성자를 오버로딩하여 구분하여 사용해 왔다
  • 하지만 문제는 이러한 객체를 new 키워드를 통해 생성자로 생성하려면, 개발자는 해당 생성자의 인자순서와 내부구조를 알고 있어야 목적에 맞게 객체를 생성할 수 있는 번거로움이 있다
  • 예를들어 다음과같은 Car 클래스가 있다고 가정하자
  • Car의 color의 기본옵션은 black이고 브랜드명은 무조건적으로 기재 해야한다
  • color는 옵션이기때문에 선택에따라 색상이 달라질 수 있다
package 정적팩토리메서드패턴;

public class Car {
    private String brand;
    private String color = "black";

    public Car(String brand, String color) {
        this.brand = brand;
        this.color = color;
    }

    public Car(String brand) {
        this.brand = brand;
    }
}

package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car("테스트 브랜드1");
        Car car2 = new Car("테스트 브랜드1","blue");
    }
}
  • 위와 같은 식으로 생성이 가능할것이다
  • 하지만 이러한 방식의 생성자 방식에는 문제가 있다
  • 우리가 프로그래밍을 할때 중요시 여기는것중 하나는 "가독성","편의성","명시성" 정도이다
  • new 키워드를 이용한 생성자 방식은 단순히 생성자를 생성하는 것뿐이고, 어떠한 이유도 주어지지 않는다
  • 즉, 생성자로 넘기는 매개변수 만으로는 "반환될 객체의 특성을 제대로 표현하기가 어렵다" 라는 것이다
  • 하지만 이를 정적 메서드를 통해 메서드 네이밍을 해주게 된다면, 객체의 특성을 한번에 이해하기가 쉬울것이다, 다음 예시를 봐보자
package 정적팩토리메서드패턴;

public class Car {
    private String brand;
    private String color = "black";

    // 생성자 숨기기
    private Car() {}

    // 생성자 숨기기
    private Car(String brand, String color) {
        this.brand = brand;
        this.color = color;
    }

    // 매개 변수 하나는 from 네이밍
    public static Car brandFrom(String brand){
        return new Car(brand,"black");
    }
    
    // 매개변수 2개이상은 of 네이밍
    public static Car brandColorFrom(String brand, String color){
        return new Car(brand,color);
    }
}

package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        Car car1 = Car.brandFrom("테스트 브랜드1");
        Car car2 = Car.brandColorFrom("테스트 브랜드1","blue");
    }
}
  • 확실히 아까보다는 동일하게 생성자를 만들지만, 확실한 네이밍을 통하여 어떤것을 만들지 명확해진 모습이다

2. 인스턴스에 대해 통제 및 관리가 가능하다

  • 메서드를 통해 한단계 거쳐 간접적으로 객체를 생성하기 때문에, 기본적으로 전반적인 객체 생성 및 통제 관리를 할 수 있게된다
  • 즉, 필요에 따라 항상 새로운 객체를 생성해서 반환할 수 도있고 객체 하나만 만들어두고 공유하며 반재사용하는등의 유연한 활용이 가능하다는 것이다
  • 대표적인 예시로 "싱글톤 패턴"을 예시로 들 수 있다, 코드로 봐보자
package 정적팩토리메서드패턴;

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if ( instance == null ) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 이런식으로 이미 만들어져있는 Singleton이 있으면 그걸쓰고, 없으면 새롭게 만드는 방식이다
package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

  • 이렇게하면 동일한 객체에 대해 메모리에 계속 올라가지않고 하나만 올라가니, 메모리측면으로 이득을 볼 수 있다
  • 이런식으로 인스턴스의 생성에 관여하여, 생성되는 인스턴스의 수를 통제할 수 있는 클래스를 "인스턴스 통제" 클래스 라고한다

3. 하위 자료형 객체 반환 가능

package 정적팩토리메서드패턴;

class Galaxy implements SmartPhone {}
class IPhone implements SmartPhone {}
class Huawei implements SmartPhone {}

public interface SmartPhone {
    static Galaxy getGalaxy(){
        return new Galaxy();
    }

    static SmartPhone getApplePhone() {
        return new IPhone();
    }

    static SmartPhone getChinesePhone() {
        return new Huawei();
    }
}
package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        SmartPhone galaxy = SmartPhone.getGalaxy();
        SmartPhone iPhone = SmartPhone.getApplePhone();
        SmartPhone huawei = SmartPhone.getChinesePhone();
    }
}
  • 이런식으로도 활용 가능하다

4. 인자에 따라 다른 객체를 반환하도록 분기할 수 있다 (인스턴스 생성을 조작할 수 있는데에서 비롯된 이점인듯)

    static SmartPhone getRandomPhone(){
        int price = 10000;
        if (price >= 5000) {
            new Galaxy();
        } else if (price >= 7000) {
            new IPhone();
        } else {
            new Huawei();
        }
        return null;
    }
package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        SmartPhone galaxy = SmartPhone.getRandomPhone();
    }
}
  • 이런식으로 활용 가능하다 !

5. 객체 생성을 캡슐화 할 수 있다

  • 생성자를 사용하는 경우 외부에 내부 구현을 드러내야 하는데, 정적 팩토리 메서드는 구현부를 외부로부터 숨길 수 있어 캡슐화 및 정보 은닉의 이점이 있다
  • 또한 노출하지 않는다는 특징은 정보 은닉을 가지기도 하지만, 동시에 사용하고 있는 구현체를 숨겨 "의존성 제거"의 장점도 있다

정적 팩토리 메서드 네이밍 규칙

  • 정적 팩토리 메서드와 다른 정적 메서드와 역할을 구분짓기 위해 독자적인 네이밍 컨벤션이 존재한다
  • 단, 정적 팩토리 메서드에서의 네이밍은 단순히 선호도 의미를 넘어서 거의 "법칙 정도로 사용"되는 부분이니 확실하게 알아두도록하자
  • 정적 팩토리 메서드에서의 네이밍 규칙은 아래와같다
    • from: 하나의 매개 변수를 받아서 객체를 생성
    • of: 여러개의 매개 변수를 받아서 객체를 생성
    • getInstance | instance: 인스턴스를 생성, 이전에 반환했던 것과 같을 수 있음
    • newInstance | create: 항상 새로운 인스턴스를 생성
    • get[OrderType]: 다른 타입의 인스턴스를 생성, 이전에 반환했던 것과 같을 수 있음
    • new[OrderType]: 항상 새로운 다른 타입의 인스턴스를 생성

우리는 알게 모르게 이미 사용해오고 있었다

Optional의 of()

  • 자바 8버젼 부터 지원하는 Optinal 객체를 얻기 위해서 아래와 같이 new 키워드 대신, of() 메서드를 이용하여 객체를 얻도록 설계 되어있다

List의 of()

  • 이 부분은 하위 자식 객체 반환 및 캡슐화에 대한 실전 구현체의 예시라고 볼 수 있다
  • 자바 8버젼 부터 지원하는 List 인터페이스의 of() 메서드는 인터페이스를 반환하는 정적 팩토리 메서드이다.
  • 사용자 입장에선 반환되는 클래스가 어떤 타입인지 알 필요없이, 그저 of() 메서드의 기능을 알고 호출하기만 하면되는것이다

Integer의 valueOf()

  • 이 부분은 정적 팩토리 메서드의 "인스턴스를 캐싱하여 관리 가능"의 실제 자바 구현체 예시이다
  • 실제로 primitive 타입의 Wrapper 클래스들은 값을 캐싱하여 자바 프로그램을 최적화 하도록 설계 되어있다
  • 아래의 Integer 클래스의 value() 메서드는 입력값이 -128 ~ 127 범위의 정수라면 캐싱을 이용하도록 설계되어있다
  • 즉 -128 ~ 127 범위의 정수라면 원래 있던 객체를 내뱉고, 아니면 새로운 객체를 내뱉는 식이다


package 정적팩토리메서드패턴;

public class Main {
    public static void main(String[] args) {
        Integer i1 = Integer.valueOf(100);
        Integer i2 = Integer.valueOf(100);
        System.out.println(i1 == i2);
        Integer i3 = Integer.valueOf(128);
        Integer i4 = Integer.valueOf(128);
        System.out.println(i3 == i4);
    }
}

  • 지식이 늘었다

Lombok으로 정적 팩토리 메서드 만들기

  • 이런식으로도 만들 수 있다

단점?

  • 생성자를 보통 private으로 선언하다보니, 자식 클래스에서 접근이 불가하거나, 확장이 불가한 경우들을 뽑을 수 있다

API 문서 에서의 불편함

  • 생성자는 하나의 자바 프로그래밍 언어의 스펙이기 때문에 JavaDoc 같은 문서에서 상단에 정의되어있어 빠르게 검색 할 수 있다
  • 정적 팩토리 메서드의경우 개발자가 임의로 만든 메서드 이기때문에, 하나하나 찾아야하는데 이래서 네이밍 컨벤션을 통해 그 역할을 빠르게 유추 할 수 있다

참조 블로그

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%EB%8C%80%EC%8B%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90

profile
테러대응전문가

0개의 댓글