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

2025. 3. 17. 21:32Effective-Java

오늘은 그동안 이펙티브 자바를 공부하면서 정리한 내용을 복습할겸 글을 작성해볼 것이다.

 

item1의 내용은 단순히 생성자를 쓰기보다 정적 팩토리 메서드를 사용하는것을 고려하라는 것인데 그 이유에 대해 알아보자

 

일반적으로 우리는 객체를 생성할 때 생성자를 사용한다.

자바를 처음 배울 때부터 자연스럽게 사용하는 방식이며 익숙한 방법이다.

그렇다면 왜 정적 팩토리 메서드를 고려하는 걸까?

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

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

정적 팩토리 메서드는 객체를 생성하는 역할을 하는 정적 메서드이다. 생성자 대신 아래와 같이 정적 팩토리 메서드를 정의 할 수 있다.

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

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


    //생성자 대신 정적 팩토리메서드로 무슨 역할을 하는지 더 정확히 나타낼 수 있음
    public static Person of(String name, int age) {
        return new Person(name, age);
    }
}

이 방식이 생성자보다 좋은 이유는 이름을 가질 수 있기 때문이다.

예를 들어 Person.of(name, age) 라고 하면 이 객체를 생성하는 메서드 라는 것을 직관적으로 알 수 있다.

하지만 이 정도 이유만으로 굳이? 정적 팩토리 메서드를 사용해야하나? 라는 생각이 들 수 있다.

그래서 정적 팩토리 메서드가 유용한 경우 5가지를 살펴보자.

 

- 정적 팩토리 메서드 사용이 좋은 상황

1. 객체 생성의 이름을 통해 의도를 명확히 해야 할 경우

(위의 코드에서 충분히 보여줬으므로 생략)

 

2. 객체 생성 로직이 복잡하거나 추가 작업이 필요할 경우

public static User createWithValidation(String name, int age) {
    if (age < 0) {
        throw new IllegalArgumentException("나이는 0 이상이어야 합니다.");
    }
    return new User(name, age);
}

생성자와 달리 이런 예외처리 같은 검증 로직을 추가하기 좋다.

 

3. 같은 매개변수로 특정 객체를 재사용해야 할 경우 (캐싱)

public class Person {
    // 캐싱을 활용한 동일 인스턴스 반환
    private static final Map<String, Person> CACHE = new HashMap<>();
    private String name;
    private int age;

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

    public static Person of(String name, int age) {
        String key = name + age;
        if (!CACHE.containsKey(key)) {
            CACHE.put(key, new Person(name, age));
        }
        return CACHE.get(key);
    }
}

 

Person p1 = Person.of("Alice", 25);
Person p2 = Person.of("Alice", 25);
System.out.println(p1 == p2); // true (같은 객체를 반환)

이러한 방식으로 같은 인스턴스를 재사용하여 메모리 사용량과 불필요한 객체 생성을 줄일 수 있다.

 

4. 싱글턴 도는 불변 객체를 생성해야 할 경우

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

getInstance()를 사용하여 항상 같은 객체를 반환해 객체 생성을 제한할 수 있다.

 

5. 하위 클래스나 인터페이스를 반환해야 할 경우

// 인터페이스 정의
public interface Shape {
    void draw();
}

// 인터페이스 구현체 1 - 원
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

// 인터페이스 구현체 2 - 사각형
public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

// 정적 팩토리 메서드 활용
public class StaticFactoryMethodEx3 {
    /*
     * 인터페이스(Shape) 타입을 반환하지만,
     * 실제로는 Circle 또는 Rectangle 객체를 반환할 수 있음
     */
    public static Shape createShape(String type) {
        if (type.equals("circle")) {
            return new Circle();
        } else if (type.equals("rectangle")) {
            return new Rectangle();
        }
        throw new IllegalArgumentException("정의되지 않은 도형");
    }

    public static void main(String[] args) {
        Shape circle = createShape("circle");
        Shape rectangle = createShape("rectangle");

        circle.draw(); // "원을 그립니다."
        rectangle.draw(); // "사각형을 그립니다."
    }
}

생성자를 사용했다면 고정된 타입을 반환해야하지만 정적 팩토리 메서드를 이용하기 때문에 유연하게 타입 반환이 가능하다.

또한 createShape() 메서드를 확장하면 새로운 도형을 추가해도 기존 코드에는 영향이 가지 않는다. 즉 OCP원칙을 준수한다.

 

이렇게 좋은 장점만 있으면 반드시 정적팩토리 메서드만 써야할까?

당연히 생성자가 유리한 부분도 있다.

 

- 생성자 사용이 좋은 상황

1. 단순 객체 생성

2. 클래스 설계가 단순하고 확장 필요 없을 경우

3. 새로운 객체 생성이 항상 보장되어야 할 경우

 

이러한 상황에는 생성자를 사용하는것이 낫다.

하지만 대부분의 경우는 정적 팩토리 메서드가 유리한점이 많으므로 무작정 public 생성자를 이용하기보다는 한번 생각하고 정적팩토리 메서드를 이용할 생각을 해보는게 나을 것이다.

 

item 1의 내용은 여기까지이다.

 

- 프로젝트에 어떻게 적용할 것인가?

내 생각을 조금 넣어보자면 우선 나는 spring을 이용한 백엔드 개발자니까 이 부분을 어떻게 사용하면 좋을까를 조금 고민해봤다.

프로젝트를 진행하면서 리팩토링 과정에서 entity -> dto 또는 dto -> entity 로 변환하는 과정을 map을 이용하기보다 정적 팩토리 메서드를 쓰면 코드를 재사용 할 수 도 있고 의도가 분명하기에 더 편리한 것 같다. 또한 엔티티를 생성하는 과정에서 무작정 public 생성자를 사용하기보다 빌더패턴과 결합해서 사용하면 더욱 편리하게 entity를 만들 수 있다는걸 느꼈다. 물론 이 과정에는 jpa가 내부적으로 기본 생성자를 필요로 하기 때문에 private으로 제한할 수 없어 protected를 사용해야 한다. 상황에 따라 엔티티의 필드를 반환해 줄 수도 있고 여러모로 편리한 점이 많은것 같다.