아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

Yunes·2023년 10월 21일
0

이펙티브 자바

목록 보기
1/2
post-thumbnail

정적 팩터리 메서드란?

정적 static, 팩토리 factory, 메서드 method

여기서 팩토리는 GoF 디자인 패턴중 팩토리 패턴에서 유래하였는데 객체를 생성하는 역할을 분리하겠다는 취지가 담겨있다.

정적 팩터리 메서드는 객체의 생성을 담당하는 클래스 메서드이다.

정적 팩터리 메서드는 클래스의 생성자 대신 객체를 생성시, 명시적인 생성자 호출을 대체하는 방식으로 활용할 수 있다.

예로 Integer 의 valueOf 동작 과정을 살펴봤다.

Integer value = Integer.valueOf(42); // 정적 팩토리 메서드를 사용하여 Integer 객체 생성
@IntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

valueOf 메서드는 Integer 클래스의 정적 메서드로 객체를 생성하고 반환해준다. 즉, new Integer 를 명시적으로 하는 것이 아니라 내부적으로 valueOf 라는 정적 팩터리 메서드를 사용해서 내부에서 new Integer 를 사용하는 방식이다. 생성자 함수가 있는데 굳이 왜 정적 팩터리 메서드를 사용해서 내부적으로 생성자 함수를 호출할까? 책에서는 아래에 소개하고 있는 다섯가지 정적 팩터리 메서드의 장점을 들며 그 이유를 소개하고 있었다.

// constructor
Person person = new Person("Alice", 30);

// static factory method
Person person = Person.createPerson("Alice", 30);

1인 개발자라면 객체의 생성자함수에 전달하는 파라미터들이 각각 무슨 요소를 전달해야 하는지 알 것이다. java 의 경우 메서드 오버로딩이 지원되니 각각의 파라미터가 무엇을 의미하는지 잘 알고 있어야 한다. 그런데 협력을 하는 경우 new 만 호출해서는 각각이 무엇을 의미하는지 알 수가 없다.

반면에 정적 팩터리 메서드를 사용시 이름을 붙일 수 있기에 명시적으로 어떤 동작을 하는지 알 수 있고 내부적으로 어떻게 돌아가는지 알 필요 없이 이름만 보고도 추상화된 동작을 수행할 수 있다.

static 정적 팩터리 메서드는 객체를 생성하기 위해 클래스의 인스턴스를 생성하지 않아도 된다.

+) 팩토리가 무엇인지 감이 잘 잡히지 않아서 Factory Method Pattern 에 대해 더 찾아봤다.

팩터리 메서드 패턴이란?

팩토리 메서드 패턴은 객체 생성을 공장 (Factory) 클래스로 캡슐화하여 대신 생성하게 하는 디자인 패턴이다. - Inpa Factory Method Pattern

  • 팩토리 메서드 패턴을 사용하면 객체 간 결합도가 낮아지고 유지보수가 용이해진다.
  • 객체의 유형과 종속성을 캡슐화시켜 정보를 은닉 처리할 수 있다.
  • 기존 객체를 재구성하는 대신 기존 객체를 재사용하여 리소스를 절약할 수 있다.
  • 객체 생성의 책임을 서브클래스로 분산하여 확장성을 제공할 수 있다.

팩터리 메서드 패턴은 상속을 기반으로 하여 슈퍼 클래스에서 추상 클래스를 선언하고, 서브 클래스에서 이를 구현함으로써 객체 생성을 조절한다.

팩터리 메서드 패턴의 장점

  • 생성자와 구현 객체 간의 강한 결합을 피할 수 이싿.
  • 팩토리 메서드를 통해 객체 생성 후 공통으로 할 일을 수행하도록 지정할 수 있다.
  • 캡슐화, 추상화를 통해 생성되는 타입을 감출 수 있다.
  • SOLID 중 S 에 해당하는 단일 책임 원칙을 준수하여 객체 생성 코드를 한 곳에서 유지보수, 관리할 수 있다.
  • SOLID 중 O 에 해당하는 개방/폐쇄 원칙을 준수하여 기존 코드를 수정하지 않고 확장성 있는 전체 프로젝트를 구성할 수 있다.
  • 생성에 대한 인스턴스 부분과 생성에 대한 구현 부분을 나누어 여러 개발자가 협업을 통해 개발하기 수월하다.\

팩터리 메서드 패턴의 단점

  • 각 구현체별 팩토리 객체를 모두 구현해야 해서 구현체가 늘어날 때마다 서브 클래스 수가 굉장히 늘어난다.
  • 코드가 복잡하다.

팩터리 메서드 패턴을 사용하지 않는 경우

// 도형 클래스
class Shape {
    public void draw() {
        // 도형을 그리는 로직
    }
}

// 원 클래스
class Circle extends Shape {
    @Override
    public void draw() {
        // 원을 그리는 로직
    }
}

// 사각형 클래스
class Rectangle extends Shape {
    @Override
    public void draw() {
        // 사각형을 그리는 로직
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape = new Circle(); // 직접 생성
        shape.draw();
        
        Shape shape2 = new Rectangle(); // 직접 생성
        shape2.draw();
    }
}

팩터리 메서드 패턴을 사용하는 경우

// 도형 클래스
abstract class Shape {
    public abstract void draw();
}

// 원 클래스
class Circle extends Shape {
    @Override
    public void draw() {
        // 원을 그리는 로직
    }
}

// 사각형 클래스
class Rectangle extends Shape {
    @Override
    public void draw() {
        // 사각형을 그리는 로직
    }
}

// 팩토리 메서드 인터페이스
interface ShapeFactory {
    Shape createShape();
}

// 원을 생성하는 팩토리
class CircleFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Circle();
    }
}

// 사각형을 생성하는 팩토리
class RectangleFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Rectangle();
    }
}

public class Main {
    public static void main(String[] args) {
        ShapeFactory circleFactory = new CircleFactory();
        Shape circle = circleFactory.createShape();
        circle.draw();
        
        ShapeFactory rectangleFactory = new RectangleFactory();
        Shape rectangle = rectangleFactory.createShape();
        rectangle.draw();
    }
}

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다.

생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 그런 반면 정적 팩터리 메서드는 이름을 지어 객체의 특성을 쉽게 묘사할 수 있다.

// 생성자

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
    }
}
// 정적 팩터리 메서드

public class Person {
    private String name;
    private int age;

    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static Person create(String name, int age) {
        return new Person(name, age);
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = Person.create("Alice", 30);
    }
}

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

정적 팩토리 메서드의 이 특성 덕분에 불변 클래스의 인스턴스는 미리 만들어 두거나 생성한 인스턴스를 캐싱하여 재활용하여 불필요한 객체 생성을 피할 수 있다.

public class Singleton {
    private static Singleton instance;

    // 생성자를 private로 선언하여 외부에서 직접 생성할 수 없도록 한다.
    private Singleton() {
    }

    // 정적 팩토리 메서드를 사용하여 인스턴스를 생성하거나 반환한다.
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

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

정적 팩토리 메서드는 반환할 객체의 클래스를 자유롭게 선택할 수 있다.

java 8 버전 이전까지는 Collections 클래스를 Collection 인터페이스의 동반 클래스로 만들어줘야 했는데 java 8 이후 버전부터는 인터페이스가 정적 메서드를 가질 수 있게 되어 동반 클래스 개념이 더이상 필요 없이 인터페이스에 정적 팩토리 메서드를 선언할 수 있게 되었다.

단, java 9 에서 private 정적 메서드까지 인터페이스에 쓸 수는 있으나 정적 필드와 정적 멤버 클래스는 여전히 public 이어야 한다.

java 7 까지 동반 클래스를 사용하는 예시코드

interface Car {}

class Sedan implements Car {}

class SUV implements Car {}

class Cars { // Cars 는 인터페이스 Car 의 동반 클래스, 인터페이스가 java 7 까지는 정적 메서드를 가질 수 없어 동반 클래스인 Cars 에서 정적 메서드를 만들어줬다.
    public static Car createSedan() {
        return new Sedan();
    }

    public static Car createSUV() {
        return new SUV();
    }
}

public class CarFactoryExample {
    public static void main(String[] args) {
        Car sedan = Cars.createSedan();
        Car suv = Cars.createSUV();
    }
}

java 8 이후 인터페이스에서 바로 정적 팩토리 메서드를 사용하는 사례

interface Car {
    static Car createSedan() {
        return new Sedan();
    }

    static Car createSUV() {
        return new SUV();
    }
}

class Sedan implements Car {}

class SUV implements Car {}

public class CarFactoryExample {
    public static void main(String[] args) {
        Car sedan = Car.createSedan();
        Car suv = Car.createSUV();
    }
}

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

3번 장점의 확장사례라고도 볼 수 있는데 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다.

클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴지인지 알 수도 없고 알 필요도 없다. 반환 타입의 하위 타입이기만 하면 된다.

<이펙티브 자바 3/E 11p>

예시코드

interface Shape {
    void draw();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Circle is drawn.");
    }
}

class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Square is drawn.");
    }
}

class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Triangle is drawn.");
    }
}

public class ShapeFactory {
    public static Shape createShape(String shapeType) {
        switch (shapeType.toLowerCase()) {
            case "circle":
                return new Circle();
            case "square":
                return new Square();
            case "triangle":
                return new Triangle();
            default:
                throw new IllegalArgumentException("Invalid shape type: " + shapeType);
        }
    }

    public static void main(String[] args) {
        Shape circle = ShapeFactory.createShape("Circle");
        Shape square = ShapeFactory.createShape("Square");
        Shape triangle = ShapeFactory.createShape("Triangle");

        circle.draw();
        square.draw();
        triangle.draw();
    }
}

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

사실 책에 나온 설명이 몇번을 읽어도 잘 이해가 되지 않았다. 참고한 자료들에서 이 부분에 대해 객체 생성을 캡슐화 할 수 있어 객체 생성과 사용이 분리되어 구현 세부 정보를 알지 못해도 상관 없다는 것을 설명하고 있었다.

public class UserDto {
    private String username;
    private String email;

    public static UserDto from(UserEntity userEntity) {
        return new UserDto(userEntity.getUsername(), userEntity.getEmail());
    }
}

public class Main {
    public static void main(String[] args) {
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername("john_doe");
        userEntity.setEmail("john@example.com");
		
        // 정적 팩토리 메서드
        UserDto userDto = UserDto.from(userEntity);
        
        // 생성자
        UserDto userDto2 = new UserDto(userEntity.getUsername(), userEntity.getEmail())
    }
}

정적 팩토리 메서드를 사용하지 않고 생성자를 사용하면 외부에서 생성자의 내부 구현을 모두 드러내야 한다.

정적 팩터리 메서드의 단점

1. 정적 팩터리 메서드만으로 이루어진 클래스는 상속할 수 없다.

상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공시 하위 클래스를 만들 수 없다

이펙티브 자바 3/E

예시코드

public final class ImmutablePerson {
    private final String name;
    private final int age;

	// 생성자 함수
    private ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

	// 정적 팩토리 메서드
    public static ImmutablePerson create(String name, int age) {
        return new ImmutablePerson(name, age);
    }
}

public class Employee extends ImmutablePerson {
    private final String employeeId;

    public Employee(String name, int age, String employeeId) {
        super(name, age); // ImmutablePerson 의 생성자 함수가 private 이라서 접근할 수가 없다. 그래서 하위 클래스를 만드는 것이 제한된다.
        this.employeeId = employeeId;
    }

    public String getEmployeeId() {
        return employeeId;
    }
}

비교사례

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Employee extends Person {
    private String employeeId;

    public Employee(String name, int age, String employeeId) {
        super(name, age);
        this.employeeId = employeeId;
    }

    public String getEmployeeId() {
        return employeeId;
    }
}

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

생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩토리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다. 이펙티브 자바 3/E

정적 팩터리 메서드 네이밍 컨벤션

  • from : 하나의 매개변수를 받아 객체를 생성

  • of : 여러 매개변수를 받아 객체를 생성

  • valueOf : from, of 의 더 자세한 버전

  • instance | getInstance : 인스턴스를 반환하나 같은 인스턴스임을 보장하지 않음

  • create | newInstance : 인스턴스를 반환하며 매번 새로운 인스턴스를 생성해 반환함을 보장

  • getType : getInstance 와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.

  • newType : newInstance 와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드 정의시 사용

  • type : getTypenewType 의 간결한 버전

// from
import java.util.Date;
import java.time.Instant;

Date date = Date.from(Instant.now());
// of
import java.util.EnumSet;

enum Color {
    RED, GREEN, BLUE
}

EnumSet<Color> colors = EnumSet.of(Color.RED, Color.BLUE);
// valueOf
import java.math.BigInteger;

BigInteger bigInteger = BigInteger.valueOf(100);
// getInstance
import java.util.stream.Stream;

StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
Stream<StackWalker.StackFrame> stackFrames = stackWalker.walk(frames -> frames.skip(1));
// newInstance
import java.lang.reflect.Array;

int[] newArray = (int[]) Array.newInstance(int.class, 5);
// getType
import java.io.IOException;
import java.nio.file.FileStore;
import java.nio.file.FileSystems;

FileStore fileStore = FileSystems.getDefault().getFileStores().iterator().next();
// newType
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

Path filePath = Path.of("example.txt");
BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8);
// type
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;

List<String> list = Collections.list(new ArrayList<>());

정적 팩터리 메서드와 public 생성자를 각각의 쓰임새에 맞게 장단점을 이해하고 사용하자.

레퍼런스

blog
Inpa - 정적 팩토리 메서드 패턴 (Static Factory Method)
Inpa - 팩토리 메서드 패턴
Tecoble 2기 보스독 - 정적 팩토리 메서드는 왜 사용할까?

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글