Skip to content

Item 1&2. 정적 팩터리 메서드와 빌더의 비교 #4

@daminzzi

Description

@daminzzi

들어가며

우리는 아이템 1, 2를 읽으면서 정적 팩터리 메서드와 빌더 패턴에 대해서 알아볼 수 있었다. 둘 모두 객체를 생성하는 패턴이라는 것에 공통점을 가지지만, 이 둘을 어떨 때 사용해야 하는지 명확히 파악하기가 어려웠다. 따라서 이번 이슈를 통해 정적 팩터리 메서드와 빌더 패턴을 간략하게 정리하고, 둘을 비교하면서 어떤 상황에 어떤 패턴을 써야 하는지 알아보자.

정적 팩터리 메서드

정의

객체를 인스턴스화 할때 직접적으로 생성자(Constructo)를 호출하여 생성하는 것이 아닌, 별도의 객체 생성의 역할을 하는 클래스 메서드를 통해 간접적으로 객체 생성을 유도하는 것.

특징

  • 객체 생성과 관련된 복잡한 논리를 감추고, 명확하고 간결한 메서드를 제공하여 사용자가 객체를 생성하는 과정을 단순화한다.
  • 이름을 가진 메서드를 통해 객체를 생성할 수 있으며, 생성하는 객체의 타입에 따라 다양한 정적 팩터리 메서드를 정의할 수 있다.
public class Car {

    private final String name;
    private final int oil;

    public static Car createCar(String name, int oil) {
        return new Car(name, oil);
    }

    public static Car createNoOilCar(String name) {
        return new Car(name, 0);
    }

    private Car(String name, int oil) {
        this.name = name;
        this.oil = oil;
    }
}

//in main
public static void main(String[] args) {
        Car fullOilCar = createCar("car1", 10);
        Car noOilCar = createNoOilCar("car2");
}

장점

  1. 이름을 가질 수 있다.

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

    → 불필요한 객체 생성을 피할 수 있다.

    ex) String의 valueof

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

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

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

    ex) 서비스 제공자 프레임워크 JDBC

    • 역시 나보다 똑똑한 사람들이 책의 예제에 대해서 잘 설명해주었다.
    • 서비스 인터페이스 : 커넥션 객체를 생성하는 인터페이스를 정의. 각 DB회사는 이 인터페이스를 구현하여 JDBC에 등록하도록 함.
    • 제공자 등록 API : 각 DB회사가 DbDriver 객체를 열심히 구현했지만, 이 구현체를 등록해주려면 PR을 리뷰하고, 레포지토리에 해당 코드를 병합하고, 드라이버를 미리 세팅하는 코드를 추가하고, 재배포 해주어야 한다. 이런 일을 막으려면 서비스 제공자가 구현체를 구현만 해놓으면 자동으로 등록할 수 있는 메커니즘을 만들어야 함. java 6부터 지원되는 공용 서비스 제공자 프레임워크 ServiceLoader는, 서비스 제공자가 구현한 서비스 jar파일 내 META-INF/services 폴더에 서비스 인터페이스의 구현 위치를 등록해놓으면 자동으로 찾을 수 있는 look up 메커니즘을 지원!
    • 서비스 접근 API : 사용자가 서비스 제공자의 구현체를 얻을 수 있게 해주는 API. 우리의 작고 귀여운 JDBC 예제에서 DB Driver를 직접 얻을 수 있는 API는 없지만, getConnection(String dbName) API에서 “나만의DB”라고 넘기면 MyDb의 Connection 객체를 얻을 수 있으므로 이를 서비스 접근 API라고 이해할 수 있을 것 같다.

단점

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

빌더 패턴

정의

빌더 패턴은 복잡한 객체의 생성을 단순화하기 위한 디자인 패턴으로, 객체의 생성 과정을 단계적으로 처리할 수 있도록 한다.

구현 방법

  • 필요한 객체를 직접 만드는 대신,

    1. 필수 매개변수만으로 생성자를 호출해서 빌더 객체를 얻고

    2. 빌더 객체가 제공하는 일종의 세터 메서드들로 원라는 선택 매개변수들을 설정

    3. 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.

public class NutritionFacts {
	private final int servingSize;
	private final int servings;
	private final int calories;
	private final int fat;
	private final int sodium;
	private final int carbohydrate;

	public static class Builder {
		//필수 매개변수
		private final int servingSize;
		private final int servigs;

		//선택 매개변수
		private int calories = 0;
		private int fat = 0;
		private int sodium = 0;
		private int carbohydrate = 0;

		public Builder(int servingSize, int servings) {
			this.servingSize = servingSize;
			this.servings = servings;
		}

		public Builder calories(int val)
			{ calories = val;    return this; }

		public Builder fat(int val)
      { fat = val;    return this; }

    public Builder sodium(int val)
      { sodium = val;    return this; }

    public Builder carbohydrate(int val)
      { carbohydrate = val;   return this; }

		public NutritionFacts build() {
				return new NutritionFacts(this); //여기서의 this는 Builder
		}
		
		private NutritionFacts(Builder builder) { //생성자
				servingSize = builder.servingSize;
        servings = builder.servings;
				calories = builder.calories;
				fat = builder.fat;
				sodium = builder.sodium;
				carbohydrate = builder.carbohydrate;
    }

	}
}

//in main
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
		.calories(100).sodium(35).carbohydrate(27).build();
  • 이러한 방식을 통해서
    • NutritionFacts 클래스를 불변으로 만들 수 있다.
    • 모든 매개변수의 기본 값들을 한 곳에 모아 둘 수 있다.
    • 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
      • 플루언트 API 혹은 메서드 연쇄(method chaining)
    • 클라이언트 코드가 쓰기 읽고 쓰기 쉬워진다.
  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
  • 유연한 구조를 가지고 있음

단점

  • 객체를 만들기 위해서 빌더가 우선적으로 만들어져야 함
  • 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있음
  • 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.(근데 보통 다 확장되니까 이렇게 만드는 게 좋다)

비교

그럼 정적 팩터리 메서드 방식과 빌더 패턴을 모두 살펴보았는데, 본격적으로 둘을 비교하기에 앞서 앞의 내용을 간단하게 정리해보자.

정적 팩터리 메서드:

  • 정적 팩터리 메서드는 클래스의 인스턴스를 생성하기 위해 클래스 내에 정적(static) 메서드를 사용하는 디자인 패턴
  • 생성자와는 달리 메서드의 이름을 가지고 있으며, 객체 생성과 관련된 복잡한 논리를 캡슐화하고 명확한 인터페이스를 제공할 수 있음.
  • 다양한 정적 팩터리 메서드를 통해 다른 방식으로 객체를 생성할 수 있으며, 유연성과 캡슐화를 제공함.

빌더 패턴:

  • 빌더 패턴은 복잡한 객체의 생성을 단순화하기 위한 디자인 패턴으로, 객체의 생성 과정을 단계적으로 처리.
  • 객체를 구성하기 위해 여러 메서드 호출을 연쇄적으로 사용하여 객체를 구성하고, 마지막에 최종 객체를 생성.
  • 선택적 매개변수를 가진 객체를 생성하거나 복잡한 객체의 생성을 단순화할 수 있으며, 가독성과 유지보수성을 향상.

둘의 주된 차이점을 정리해보자면 다음과 같다.

차이점

  1. 사용 방법:
    • 정적 팩터리 메서드는 클래스 내에 정적 메서드를 사용하여 객체를 생성한다.
    • 빌더 패턴은 메서드 체인을 사용하여 객체를 생성한다.
  2. 목적:
    • 둘 다 객체 생성 방식에서 단순화, 유연성을 지향하는 것은 동일하지만 이를 지원하는 방식에 차이가 있다.
    • 정적 팩터리 메서드는 객체 생성에서 복잡한 논리를 단순화하고, 여러 클래스를 지원하는 방식의 유연성을 제공한다.
    • 빌더 패턴은 복잡하게 얽힌 매개변수를 각각의 함수를 통해 관리해 객체의 생성을 단순화해 가독성을 높이고, 선택적 매개변수에 대한 유연성을 제공한다.

이런 차이점으로 인해 두 패턴은 각각의 특성에 맞춰 선택적으로 이용되어야 한다.

  1. 정적 팩터리 메서드 사용:
    • 복잡한 유효성 검사나 초기화 로직이 필요한 경우
    • 객체 생성이 단순하고 간단한 경우
  2. 빌더 패턴 사용:
    • 여러 개의 선택적 매개변수를 가지고 있는 경우
    • 복잡한 객체(점층적 구조 등)를 구성해야 하는 경우

그렇다면 마지막으로 각각의 특성에 맞춘 예제 코드를 하나씩 살펴보고 마무리해보자.

정적 팩터리 메서드

public class Car {
    private String brand;
    private String model;
    private int year;

    private Car(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }

    public static Car createCar(String brand, String model, int year) {
        // 여기서 복잡한 유효성 검사 또는 로직이 수행될 수 있음
        if (year < 1900) {
            throw new IllegalArgumentException("Invalid year for the car");
        }
        return new Car(brand, model, year);
    }
}

빌더 패턴

public class User {
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String email;
    private final String address;

    public static class Builder {
        private final String firstName;
        private final String lastName;
        private int age;
        private String email;
        private String address;
				
				// 빌더의 생성자를 통해 필수 매개변수를 받고
				// 선택적 매개변수에 대한 초기값을 지정할 수 있음
        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
						this.age = 14;
						this.email = "";
						this.address = "";
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
		//빌더를 통해서 User가 생성되기 때문에 User는 불변객체가 됨.
    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.email = builder.email;
        this.address = builder.address;
    }
}

마무리

우리는 무의식 중에 public 생성자를 통한 객체 생성을 하고 있을지도 모른다. 하지만 이제 정적 팩터리 메서드와 빌더 패턴에 대해서 정리하고 또 그 예시들을 살펴보았으니 앞으로는 상황에 맞게 이러한 패턴을 잘 활용할 수 있길 바란다!

참고자료

서비스 제공자 프레임워크

https://inpa.tistory.com/entry/GOF-💠-정적-팩토리-메서드-생성자-대신-사용하자

https://velog.io/@cjh8746/정적-팩토리-메서드Static-Factory-Method

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions