정적 팩토리 메서드와 빌더 패턴

JeongJun Min·2024년 9월 20일

JAVA

목록 보기
1/7
post-thumbnail

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

보통 클래스의 인스턴스(객체)를 얻는 방법은 public 생성자이다. 하지만 클래스는 생성자와 별도로 정적 팩토리 메서드(static factory method)를 제공할 수 있다.

예시)

public enum Gender {
	MALE, FEMALE
}
...
Gender aGender = Gender.valueOf("MALE");
Gender bGender = Gender.valueOf("FEMALE");

Enum.valueOf 는 괄호의 해당 문자열을 Enum 객체로 변환시키는 역할을 한다.
각각 타입마다 존재하는 정적 메서드

그렇다면 생성자를 두고 왜 정적 팩토리 메서드를 사용하지?

정적 팩토리 메서드의 장점 5가지가 있다.

  • 이름을 가질 수 있다.
  • 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  • 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

1. 이름을 가질 수 있다.

생성자와 파라미터를 통해 객체를 생성하면 프로그래머는 해당 생성자의 내부 구조를 알고 있어야하며, 생성자 자체만으로는 반환될 객체의 특성을 알 수 없다. 이때 정적 팩토리 메서드를 사용하면 이름에 따라 반환될 객체의 특성을 묘사할 수 있다.

report객체

생성자 방식

public class Report {
	
	private final String username;
	private final ReportType reportType;
	private final String reason;
	
	public Report(String username, ReportType reportType, String reason) {
		this.username = username;
		this.reportType = reportType
		this.reason = reason;
	}
	  
	public Report(String username, String reason) {
		this.username = username;
		this.reportType = ReportType.기타;
		this.reason = reason;
	}
	
}

첫 번째 생성자는 usernamereportType, reason 파라미터로 받아 신고자와 신고 유형, 신고 사유가 담긴 Report 객체를 생성한다.

두 번째 생성자는 username, reason 파라미터로 입력 받아 기타로 분류되고, 신고 사유가 담긴 Report 객체를 생성한다.

public static void main(String[] args) {
        Report bugReport = new Report("Lookie", ReportType.버그 , "뒤로가기 버그 발생");
        Report etcReport = new Report("Lookie", "뒤로가기 버튼 안보임");
}

위 처럼 생성자만으로 Report 클래스 내부에서 어떻게 동작하는지 명확하지 않다.

정적 팩토리 메서드

public class Report {
	
	private final String username;
	private final ReportType reportType;
	private final String reason;
	
	private Report(String username, ReportType reportType, String reason) {
		this.username = username;
		this.reportType = reportType
		this.reason = reason;
	}
	
	public static Report createReport(String username, ReportType reportType, String reason) {
		return new Report(username, reportType, reason);
	}
	
	public static Report createEtcReport(String username, String reason) {
		return new Report(username, ReportType.기타, reason);
	}
	  
}
public static void main(String[] args) {
        Report bugReport = Report.createReport("Lookie", "뒤로가기 버그 발생");
        Report etcReport = Report.createEtcReport("Lookie", "뒤로가기 버튼 안보임");
}

위 코드는 메서드명을 보고 쉽게 어떤 신고 유형으로 분류 되는지 알 수 있다.

이처럼 여러 생성자가 필요한 클래스의 경우 정적 펙토리 메서드로 바꾸어 명확하게 표현하면 호출하는데 실수도 줄일 수 있고, 코드를 읽는 사람도 어렵지 않게 이해할 것이다.

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

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

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 정적 팩토리 메서드
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public static void main(String[] args) {
    Singleton s1 = Singleton.getInstance();
    Singleton s2 = Singleton.getInstance();

    System.out.println(s1 == s2);
}

console: true

매번 객체를 자주 요청되는 상황에서 객체를 아예 생성하지 않기 때문에 성능이 항샹 된다.

또한 정적 팩토리 방식의 클래스는 인스턴스가 살아 있게 할지 통제할 수 있다. 이를 통해 싱글톤 패턴으로 만들 수도 있고, 인스턴스화 불가로 만들 수도 있다. 또한 인스턴스의 동일성을 보장할 수 있다.

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

public class LevelClass {

	public static LevelClass of(int score) {
		
		if(score >= 90) {
			return new A();
		} else if(score >= 80) {
			return new B();
		} else if(score >= 70) {
			return new C();
		} else {
			return new D();
		}
		...
	}
}

정적 팩토리 메서드는 반환할 객체의 클래스를 자유롭게 선택하여 반환 해 줄 수있다.
위 예제 처럼 점수에 따라 Class를 나눈다고 할 때, 분기식에 맞게 각 하위 타입의 객체를 반환할 수 있다.

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

EnumSet 클래스의 경우 public 생성자 없이 오직 정적 팩토리만 제공하는데, 원소가 64개 이하면 원소들을 long 변수 하나로 관리하는 RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환한다.

클라이언트는 팩토리가 건네주는 객체가 EnumSet의 하위 클래스이기만 하면, 어느 클래스의 인스턴스인지 알 수도, 알 필요도 없다.

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


정적 팩토리 메서드의 단점

  1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
  2. 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.

사용자는 정적 팩토리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다. API문서를 작성하거나 널리 알려진 규약을 통해 메서드명을 지어 문제를 완화시킬 수 있다.


메서드 명명 방식

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
  • valueOf : from과 of의 더 자세한 버전
  • instance 혹은 getInstance : 매개변수를 받는다면 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
  • create 혹은 newInstance : instance 혹은 getInstacne와 같지만. 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. “Type”은 팩토리 메서드가 반환할 객체의 타입이다.
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. “Type”은 팩토리 메서드가 반환할 객체의 타입이다.
  • type : getType과 newType의 간결한 버전

생성자에 매개변수가 많다면 빌더를 고려하라

선택적 매개변수가 많을 경우 생성자나 정적 팩토리 모두 적절히 대응하기 어렵다.

점층적 생성자 패턴

public class Person {
	
	private final int height;
	private final int weight;
	private final int age;
	private final float eyesight;
		  
	public Person(int height, int weight) {
		this(height, weight, 1);
	}
	
		  
	public Person(int height, int weight, int age) {
		this(height, weight, age, 1.0);
	}
	
	public Person(int height, int weight, int age, float eyesight) {
		this.height = height;
		this.weight = weight
		this.age = age;
		this.eyesight = eyesight
	}

}

보통 프로그래머들은 점층적 생성자 패턴을 이용해 선택 매개변수를 전부다 받는 생성자까지 늘려가면서 코드를 작성했다.

이러한 패턴 방식은 매개변수가 많아질 수록 클라이언트 코드를 작성하거나 읽기 어려워진다.

자바빈즈 패턴

public class Person {
	
	private final int height;
	private final int weight;
	private final int age;
	private final float eyesight;
	
	public Person() { }
		  
	public void setHeight(int height) { this.height = height; }
	public void setWeight(int weight) { this.weight = weight; }
	public void setAge(int age) { this.age = age; }
	public void setEyesight(float eyesight) { this.eyesight = eyesight; }

}
Person person = new Person();
person.setHeight(180);
person.setWeight(75);
person.setAge(30);
person.setEyesight(1.2);

두 번째 대안인 자바빈즈 패턴의 경우 매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드들을 통해 원하는 매개변수의 값을 설정하는 방식이다.

이또한 객체가 완전히 생성되기 전까지 여러 메서드를 호출해야하고, 그 전까지는 일관성이 무너진 상태에 놓이게 된다. ( 불변으로 만들 수 없다.)

빌더 패턴

public class Person {
	
	private final int height;
	private final int weight;
	private final int age;
	private final float eyesight;
	
	public static class Person {
		private final int height;  // 필수
		private final int weight;  // 필수
				
		private final int age = 1;        // 선택
		private final float eyesight = 1.0;   // 선택
		
	  public Builder(int height, int weight) {
        this.height = height;
        this.weight = weight;
    }
    
    public Builder age(int age) {
        this.age = age;
        return this;
    }
  
    public Builder eyesight(float eyesight) {
      this.eyesight = eyesight;
      return this;
    }
  }

  private Person(Builder builder) {
    height = builder.height;
    weight = builder.weight;
    age = builder.age;
    eyesight = builder.eyesight;
	}
	
}
Person person = new Person.Builder(180, 75)
	.age(30)
	.eyesight(1.2)
	.build();

세 번째 대안인 빌더 패턴이 있다. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그런 다음 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다. 이후 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.

이렇게 연쇄적으로 메서드를 호출하는 방법을 플루언트 API(fluent API) 또는 메서드 연쇄(method chaining)이라 한다.

불변 : 어떠한 변경도 허용하지 않는다는 뜻, 주로 변경을 허용하는 가변 객체와 구분하는 용도로 쓰인다.

불변식 : 프로그램이 실행되는 동안 반드시 만족해야 하는 조건, Period 클래스의 start는 end 값보다 앞서야 한다. 두 값이 역전되면 불변식이 깨진 것이다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

예를 들어 부모 클래스인 Pizza 클래스가 있고, 하위 클래스로 NyPizza, CalzonePizza가 있다고 가정하자, 각각의 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다.

이러한 기능을 공변 반환 타이핑(convariant return typing)이라 한다.

빌더 패턴은 빌더 하나로 여러 객체를 순회하면서 만들수 있으며, 빌더에 넘기는 파라미터에 따라 다른 객체를 만들 수 있는 유연한 방식이다.


빌더 패턴의 단점

  • 객체를 만드려면 빌더부터 만들어야 한다.
  • 점층적 생성자 패턴보다 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.

보통 시간이 지남에 따라 매개변수가 많아지는 경향에 따라 처음부터 빌더로 시작하는 편이 나을 때가 많다.

profile
개발계발

0개의 댓글