정적 팩토리 메서드 패턴

JINSEON YE·2023년 9월 1일

공부

목록 보기
4/9

정적 팩토리 메서드(Static Factory Method) 패턴이란?

: 개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성하는 디자인 패턴이다.
객체를 인스턴스화할 때 직접적으로 생성자를 호출하여 생성하는 것이 아니라,
객체 생성의 역할을 하는 별도의 클래스 메서드를 통해 간접적으로 객체 생성을 유도하는 것이다.

class Book {
    private String title;
    
    // 생성자를 private화 하여 외부에서 생성자 호출 차단
    private Book(String title) { this.title = title; }
    
    // 정적 팩토리 메서드
    public static Book titleOf(String title) {
        return new Book(title); // 메서드에서 생성자를 호출하고 리턴함
    }
}
public static void main(String[] args) {
	// 정적 메서드 호출을 통해 인스턴스화된 객체를 얻음
    Book book1 = Book.titleOf("어린왕자"); 
}

정적 팩토리 메서드는 GOF의 팩토리 메서드, 추상 팩토리 패턴의 팩토리(Factory) 개념을 따와 심플하게 변형시킨 팩토리 변형 패턴의 종류라고 보면 된다.

멀쩡한 생성자를 냅두고 번거롭게 한단계 거쳐 정적 팩토리 메서드를 통해 객체를 생성하는지에 대한 실용성에 대해 의문을 가질테지만,
정적 팩토리 메서드는 단순히 생성자의 역할을 대신 이행하는 것 뿐만 아니라 개발자가 좀 더 가독성 좋은 코드를 작성하고 객체 지향적으로 프로그래밍 할 수 있게 도와준다.

정적 팩토리 메서드 특징

  1. 생성 목적에 대한 이름 표현이 가능하다
    지금까지 클래스를 설계할때 다양한 타입의 객체를 생성하기 위해, 생성 목적에 따라 생성자를 오버로딩하여 구분하여 사용해왔다. 하지만 문제는 이러한 객체를 new 키워드를 통해 생성자로 생성하려면, 개발자는 해당 생성자의 인자 순서와 내부 구조를 알고 있어야 목적에 맞게 객체를 생성할수가 있다는 번거로움이 있다.
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;
    }
}
public static void main(String[] args) {
    // 검정색 테슬라 자동차 
    Car teslaCar = new Car("Tesla");

    // 빨간색 BMW 자동차
    Car bmwRedCar = new Car("BMW", "Red");
}

그리고, new 생성자 방법은 단지 매개변수의 유형과 개수를 제안할 뿐이지 어떠한 역할 표현이나 편의성을 제공하지 않는다는 면에서 생성자로 넘기는 매개변수 만으로는 반환될 객체의 특성을 제대로 표현하기가 어렵다는 것이다.
따라서 정적 메서드를 통해 적절한 메서드 네이밍을 해준다면 반환될 객체의 특성을 한번에 유추할 수 있게 된다. 왜냐하면 네이밍을 통해 어떤 값을 이용해 객체를 만들려고 하는지 쉽게 설계 의도를 전달할 수 있기 때문이다.

class Car {
    private String brand;
    private String color;

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

    // 정적 팩토리 메서드 (매개변수 하나는 from 네이밍)
    public static Car brandBlackFrom(String brand) {
        return new Car(brand, "black");
    }

    // 정적 팩토리 메서드 (매개변수 여러개는 of 네이밍)
    public static Car brandColorOf(String brand, String color) {
        return new Car(brand, color);
    }
}
public static void main(String[] args) {
    // 검정색 테슬라 자동차 
    Car teslaCar = Car.brandBlackFrom("Tesla");

    // 빨간색 BMW 자동차
    Car bmwRedCar = Car.brandColorOf("BMW", "Red");
}

이처럼 생성자 대신 정적 팩토리 메서드를 호출함으로써 생성될 객체의 특성에 대해 쉽게 묘사할 수 있다는 장점이 있어 코드의 가독성을 높여주게 된다. 생성자로 만드는 것보다 의미를 가진 메소드를 이용하면 객체 생성의 의미를 훨씬 파악하기 쉽기 때문이다.

  1. 인스턴스에 대해 통제 및 관리가 가능하다
    메서드를 통해 한단계 거쳐 간접적으로 객체를 생성하기 때문에, 기본적으로 전반적인 객체 생성 및 통제 관리를 할 수 있게 된다. 즉, 필요에 따라 항상 새로운 객체를 생성해서 반환할 수도 있고, 아니면 객체 하나만 만들어두고 이를 공유하여 재사용하게 하여 불필요한 객체를 생성하는 것을 방지 할 수 있는 것이다.

대표적인 예로 Singleton 디자인 패턴Visit Website 을 들 수 있는데, getInstance() 라는 정적 팩토리 메서드를 사용해 오로지 하나의 객체만 반환하도록 하여 객체를 재사용해 메모리를 아끼도록 유도할 수 있다.

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 정적 팩토리 메서드
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

또다른 예로는 인스턴스에 대한 캐싱(Caching) 절차 구조를 정적 팩토리 메서드로 구현할 수 있다. 인스턴스에 대해 캐싱을 한다면 필요한 인스턴스만 뽑아 재사용하여 메모리를 절약할 수 있게 된다.
인스턴스를 통제하는 것은 인스턴스가 단 하나뿐임을 보장하는 것이고, Flyweight 디자인 패턴Visit Website의 근간이 되게 된다.

class Day {
    private String day;

    public Day(String day) { this.day = day; }

    public String getDay() { return day; }
}

// Day 객체를 생성하고 관리하는 Flyweight 팩토리 클래스
class DayFactory {

	// Day 객체를 저장하는 캐싱 저장소 역할
    private static final Map<String, Day> cache = new HashMap<>();
	
    // 자주 사용될것 같은 Day 객체 몇가지를 미리 등록한다
    static { 
    	cache.put("Monday", new Day("Monday")); 
        cache.put("Tuesday", new Day("Tuesday")); 
        cache.put("Wednesday", new Day("Wednesday")); 
    }

    // 정적 팩토리 메서드 (인스턴스에 대해 철저한 관리)
    public static Day from(String day) {

        if(cache.containsKey(day)) {
            // 캐시 되어있으면 그대로 가져와 반환
            System.out.println("해당 요일은 캐싱되어 있습니다.");
            return cache.get(day);
        } else {
            // 캐시 되어 있지 않으면 새로 생성하고 캐싱하고 반환
            System.out.println("해당 요일은 캐싱되어 있지 않아 새로 생성하였습니다.");
            Day d = new Day(day);
            cache.put(day, d);
            return d;
        }
    }
}

이렇게 인스턴스의 생성에 관여하여, 생성되는 인스턴스의 수를 통제할 수 있는 클래스를 인스턴스 통제 (instance-controlled) 클래스라고 한다.

  1. 하위 자료형 객체를 반환할 수 있다
    클래스의 다형성의 특징을 응용한 정적 팩토리 메서드 특징이다. 메서드 호출을 통해 얻을 객체의 인스턴스를 자유롭게 선택할수 있는 유연성을 갖는 것이다.
interface SmarPhone {
    public static SmarPhone getSamsungPhone() {
        return new Galaxy();
    }

    public static SmarPhone getApplePhone() {
        return new IPhone();
    }

    public static SmarPhone getChinesePhone() {
        return new Huawei();
    }
}
  1. 인자에 따라 다른 객체를 반환하도록 분기할 수 있다
    메서드이니 매개변수를 받을수 있을테고, 메서드 블록 내에서 분기문을 통해 여러 자식 타입의 인스턴스를 반환하도록 응용 구성이 가능하다. 위의 3번에 대한 확장 예제라고 보면 된다.
interface SmarPhone {
    public static SmarPhone getPhone(int price) {
        if(price > 100000) {
            return new IPhone();
        }

        if(price > 50000) {
            return new Galaxy();
        }

        return new Huawei();
    }
}
  1. 객체 생성을 캡슐화 할 수 있다
    생성자를 사용하는 경우 외부에 내부 구현을 드러내야 하는데, 정적 팩토리 메서드는 구현부를 외부로 부터 숨길 수 있어 캡슐화(encapsulation) 및 정보 은닉(information hiding)을 할수 있다는 특징이 있다.
    또한 노출하지 않는다는 특징은 정보 은닉성을 가지기도 하지만 동시에 사용하고 있는 구현체를 숨겨 의존성을 제거해주는 장점도 지니고 있다. 이 예제 역시 위의 3번의 확장 예이다.

아래 예제 코드를 보면, 메인 메서드에서 오로지 GradeCalculator의 정적 팩토리 메서드 of() 를 호출하여 Grade 인터페이스 타입의 객체를 반환할 뿐이지, Grade 인터페이스의 구현체인 A ~ F 객체 존재에 대해서는 모르게 된다.
즉, 구현체를 생성해서 반환할 책임은 정적 팩토리 메서드를 가진 GradeCalculator 이고, 클라이언트는 구현체를 신경쓸 필요없이 제공되는 메서드를 호출만 하면 되어 편리하게 사용이 가능해진다.

interface Grade {
    String toText();
}

class A implements Grade {
    @Override
    public String toText() {return "A";}
}

class B implements Grade {
    @Override
    public String toText() {return "B";}
}

class C implements Grade {
    @Override
    public String toText() {return "C";}
}

class D implements Grade {
    @Override
    public String toText() {return "D";}
}

class F implements Grade {
    @Override
    public String toText() {return "F";}
}
class GradeCalculator {
	// 정적 팩토리 메서드
    public static Grade of(int score) {
        if (score >= 90) {
            return new A();
        } else if (score >= 80) {
            return new B();
        } else if (score >= 70) {
            return new C();
        } else if (score >= 60) {
            return new D();
        } else {
            return new F();
        }
    }
}
public static void main(String[] args) {
    String jeff_score = GradeCalculator.of(36).toText();
    String herryPorter_score = GradeCalculator.of(99).toText();

    System.out.println(jeff_score); // F
    System.out.println(herryPorter_score); // A
}

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

정적 팩토리 메서드 와 다른 정적 메서드와 역할을 구분짓기 위해 독자적인 네이밍 컨벤션(Convention)이 존재한다. 단, 정적 팩토리 메서드에서의 네이밍은 단순히 선호도 의미를 넘어서 거의 법칙 정도로 사용되는 것이니, 각 네이밍의 역할에 대해 알아두는 것은 개념을 아는 것만큼 중요하다. (이를 왜 지켜야 되는지는 정적 팩토리 메서드 문제점 색션에서 후술한다)

정적 팩토리 메서드에서 사용되는 네이밍 단어 종류는 다음과 같다.

from : 하나의 매개 변수를 받아서 객체를 생성
of : 여러개의 매개 변수를 받아서 객체를 생성
getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
newInstance | create : 항상 새로운 인스턴스를 생성
get[OrderType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
new[OrderType] : 항상 다른 타입의 새로운 인스턴스를 생성

// from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
// of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
// valueOf : from과 of의 더 자세한 버전
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
// instance 혹은 getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
StackWalker luke = StackWalker.getInstance(options);

Calendar instance = Calendar.getInstance();
// create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
// getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.
FileStore fs = Files.getFileStore(path);
// newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.
BufferedReader br = Files.newBufferdReader(path);
// type : getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);

정적 팩토리 메서드 문제점

  1. private 생성자일 경우 상속 불가능
    정적 팩토리 메서드로 클래스를 설계를 하면 생성자를 private 접근 제어자로 설정하게 된다. 따라서 정적 팩토리 메서드를 적용하는 경우에는 상속을 이용한 확장이 불가능해진다.
    생성자는 하나의 자바 프로그래밍 언어의 스펙이기 때문에 JavaDoc 같은 문서에서 상단에 정의되어 있기 때문에 빠르게 그에 대한 스펙 검색을 할수가 있다. 반면에 정적 팩토리 메서드는 개발자가 임의로 만든 메서드이기 때문에, 안그래도 그 많은 메서드들 중에서 정적 팩토리 역할을 하는 메서드를 뒤져서 찾아 이해해야 한다.

따라서 클래스 설계자는 API 문서를 깔끔하게 작성할 필요가 있으며, 정적 팩토리 메서드를 작성할때 네이밍 컨밴션을 지킴으로써 문제점을 극복하기도 한다.
다만, 이 부분은 단점이라기 보단 스펙에 가깝다고 얘기할수 있다.
대표적인 예로 자바의 Collections 클래스를 보면 생성자가 private로 되어 있다. 이는 일부로 상속을 하게 하지 않기 위한 설계된건데, Collections는 자바의 컬렉션에 대한 헬퍼 제공 용도이지 상속을 통해 무언가를 확장하게 하기 위한 용도가 아니기 때문이다. 따라서 이러한 제약은 상속(Inheritance) 보단 합성(Composition)을 사용하도록 유도하게 하거나, 불변(Immutable) 객체로 만들고 싶을때 사용되는 하나의 코드 패턴이라고 보면 된다.

  1. API 문서에서의 불편함
    생성자는 하나의 자바 프로그래밍 언어의 스펙이기 때문에 JavaDoc 같은 문서에서 상단에 정의되어 있기 때문에 빠르게 그에 대한 스펙 검색을 할수가 있다. 반면에 정적 팩토리 메서드는 개발자가 임의로 만든 메서드이기 때문에, 안그래도 그 많은 메서드들 중에서 정적 팩토리 역할을 하는 메서드를 뒤져서 찾아 이해해야 한다.

따라서 클래스 설계자는 API 문서를 깔끔하게 작성할 필요가 있으며, 정적 팩토리 메서드를 작성할때 네이밍 컨벤션을 지킴으로써 문제점을 극복하기도 한다.

참고 자료

profile
백엔드 개발자

0개의 댓글