Effective Java 2강 ITEM1 생성자 대신 정적 팩터리 메서드를 고려하라 - 1

park geonwoo·2024년 10월 21일

Effective Java

목록 보기
2/56

객체 생성 방식 중 하나인 생성자(constructor) 대신 정적 팩터리 메서드(static factory method)를 고려해야 하는 이유에 대해 쉽고 자세하게 설명드리겠습니다. 이 개념은 Joshua Bloch의 저서 Effective Java에서 자세히 다루어졌으며, Java 프로그래밍에서 객체 생성의 유연성과 효율성을 높이는 중요한 패턴입니다.


1. 생성자와 정적 팩터리 메서드란?

1.1 생성자(Constructor)

  • 정의: 클래스의 새로운 인스턴스를 생성할 때 사용하는 특별한 메서드입니다.
  • 특징:
    • 클래스 이름과 동일한 이름을 가집니다.
    • 반환 타입이 없습니다.
    • 객체를 초기화하는 역할을 합니다.

예제:

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

    // 생성자
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter 생략
}

1.2 정적 팩터리 메서드(Static Factory Method)

  • 정의: 객체를 생성하여 반환하는 정적(static) 메서드입니다. 클래스의 생성자 대신 사용할 수 있습니다.
  • 특징:
    • 클래스 이름과 다를 수 있는 이름을 가집니다.
    • 반환 타입이 클래스 타입일 필요가 없습니다 (서브클래스의 객체를 반환할 수도 있습니다).
    • 객체 생성 로직을 메서드 내부에 캡슐화할 수 있습니다.

예제:

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

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

    // 정적 팩터리 메서드
    public static User of(String name, int age) {
        return new User(name, age);
    }

    // Getter 생략
}

2. 생성자 대신 정적 팩터리 메서드를 고려해야 하는 이유

정적 팩터리 메서드는 단순한 객체 생성 이상의 이점을 제공합니다. 다음은 주요 이유들입니다.

2.1. 이름을 가질 수 있다

  • 설명: 생성자는 클래스 이름과 동일해야 하지만, 정적 팩터리 메서드는 의미 있는 이름을 가질 수 있습니다. 이를 통해 메서드가 수행하는 역할을 명확하게 표현할 수 있습니다.
  • 예제:
    public class User {
        private String name;
        private int age;
    
        private User(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        // 의미 있는 이름의 정적 팩터리 메서드
        public static User createWithName(String name) {
            return new User(name, 0); // 기본 나이 설정
        }
    
        public static User createWithAge(int age) {
            return new User("Unknown", age); // 기본 이름 설정
        }
    }
    
    위 예제에서 createWithNamecreateWithAge는 각각 이름 또는 나이를 기준으로 객체를 생성합니다. 이는 생성자를 사용하는 것보다 더 명확한 의도를 전달합니다.

2.2. 인스턴스 재사용 및 캐싱 가능

  • 설명: 정적 팩터리 메서드는 필요에 따라 이미 생성된 인스턴스를 재사용하거나, 캐싱된 인스턴스를 반환할 수 있습니다. 이는 메모리 사용을 최적화하고 성능을 향상시킬 수 있습니다.
  • 예제:
    public class BooleanWrapper {
        private final boolean value;
    
        private BooleanWrapper(boolean value) {
            this.value = value;
        }
    
        // 정적 팩터리 메서드를 통해 인스턴스 재사용
        public static final BooleanWrapper TRUE = new BooleanWrapper(true);
        public static final BooleanWrapper FALSE = new BooleanWrapper(false);
    
        public static BooleanWrapper valueOf(boolean value) {
            return value ? TRUE : FALSE;
        }
    
        public boolean getValue() {
            return value;
        }
    }
    
    여기서 BooleanWrapper.valueOf(true)는 항상 동일한 TRUE 인스턴스를 반환하며, 이는 객체 생성을 줄이고 메모리를 절약합니다.

2.3. 서브클래스의 객체를 반환할 수 있다

  • 설명: 정적 팩터리 메서드는 실제로 반환하는 객체의 타입을 숨길 수 있어, 인터페이스 기반의 설계를 구현할 때 유용합니다. 이는 구현을 캡슐화하고, 클라이언트가 실제 구현 클래스에 의존하지 않도록 합니다.
  • 예제:
    public interface Shape {
        void draw();
    }
    
    public class Circle implements Shape {
        @Override
        public void draw() {
            System.out.println("Drawing a circle");
        }
    }
    
    public class Square implements Shape {
        @Override
        public void draw() {
            System.out.println("Drawing a square");
        }
    }
    
    public class ShapeFactory {
        public static Shape getShape(String shapeType) {
            if (shapeType.equalsIgnoreCase("CIRCLE")) {
                return new Circle();
            } else if (shapeType.equalsIgnoreCase("SQUARE")) {
                return new Square();
            }
            throw new IllegalArgumentException("Unknown shape type");
        }
    }
    
    여기서 ShapeFactory.getShape("CIRCLE")Circle 객체를 반환하고, ShapeFactory.getShape("SQUARE")Square 객체를 반환합니다. 클라이언트는 Shape 인터페이스에만 의존하게 되어, 구현 클래스의 변경에 영향을 받지 않습니다.

2.4. 객체 생성 로직을 캡슐화할 수 있다

  • 설명: 복잡한 객체 생성 로직을 정적 팩터리 메서드 내에 숨길 수 있어, 클라이언트는 간단하게 객체를 생성할 수 있습니다. 이는 코드의 가독성과 유지보수성을 높입니다.
  • 예제:
    public class ComplexObject {
        private String partA;
        private String partB;
    
        private ComplexObject(String partA, String partB) {
            this.partA = partA;
            this.partB = partB;
        }
    
        public static ComplexObject createDefault() {
            // 복잡한 초기화 로직을 캡슐화
            String defaultA = "DefaultA";
            String defaultB = "DefaultB";
            return new ComplexObject(defaultA, defaultB);
        }
    }
    
    클라이언트는 ComplexObject.createDefault()를 호출하여 복잡한 초기화 과정을 신경 쓰지 않고 객체를 생성할 수 있습니다.

2.5. 타입 매개변수 추론을 지원

  • 설명: 제네릭 타입을 사용할 때, 정적 팩터리 메서드는 타입 매개변수를 명시적으로 지정하지 않아도 됩니다. 이는 코드의 간결성을 높입니다.
  • 예제:
    public class Box<T> {
        private T content;
    
        private Box(T content) {
            this.content = content;
        }
    
        // 정적 팩터리 메서드
        public static <T> Box<T> of(T content) {
            return new Box<>(content);
        }
    
        public T getContent() {
            return content;
        }
    }
    
    // 사용 예시
    Box<String> stringBox = Box.of("Hello");
    Box<Integer> integerBox = Box.of(123);
    
    여기서 Box.of("Hello")Box<String> 타입을 추론하여 반환하며, 생성자를 직접 사용할 때보다 더 간결합니다.

2.6. 다양한 객체 생성 방식을 제공할 수 있다

  • 설명: 동일한 클래스 내에서 여러 가지 방식으로 객체를 생성할 수 있는 다양한 정적 팩터리 메서드를 제공할 수 있습니다. 이는 객체 생성의 유연성을 높입니다.
  • 예제:
    public class User {
        private String name;
        private int age;
    
        private User(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public static User withName(String name) {
            return new User(name, 0); // 기본 나이
        }
    
        public static User withAge(int age) {
            return new User("Unknown", age); // 기본 이름
        }
    
        public static User of(String name, int age) {
            return new User(name, age);
        }
    }
    
    // 사용 예시
    User user1 = User.withName("Alice");
    User user2 = User.withAge(30);
    User user3 = User.of("Bob", 25);
    
    이렇게 여러 가지 정적 팩터리 메서드를 통해 다양한 방식으로 객체를 생성할 수 있습니다.

3. 생성자와 정적 팩터리 메서드의 비교

아래는 생성자와 정적 팩터리 메서드의 주요 차이점과 각각의 장단점을 비교한 표입니다.

특징생성자(Constructor)정적 팩터리 메서드(Static Factory Method)
이름 지정 가능성클래스 이름과 동일해야 함의미 있는 이름을 가질 수 있음
인스턴스 재사용매번 새로운 객체를 생성함이미 생성된 인스턴스를 재사용하거나 캐싱 가능
서브클래스 반환 가능성항상 자신의 클래스 타입을 반환함서브클래스 객체를 반환할 수 있음
복잡한 객체 생성 로직복잡한 로직을 생성자에 포함시키기 어려움생성 로직을 메서드 내에 캡슐화 가능
타입 매개변수 추론 지원제네릭 타입을 사용할 때 명시적 타입 지정 필요제네릭 타입을 자동으로 추론 가능
가독성 및 명확성간단하지만, 여러 생성자가 있을 경우 혼란스러울 수 있음의미 있는 메서드 이름으로 의도를 명확히 전달 가능
불변 객체 생성생성자만으로도 가능하지만, 정적 팩터리 메서드가 더 유연함객체의 불변성을 더욱 쉽게 유지하고, 관련 로직을 추가할 수 있음

4. 언제 정적 팩터리 메서드를 사용해야 할까?

정적 팩터리 메서드를 사용하는 것이 특히 유용한 상황은 다음과 같습니다.

4.1. 여러 생성자가 있을 때

  • 설명: 클래스에 여러 생성자가 존재하면, 어떤 생성자를 호출해야 하는지 혼란스러울 수 있습니다. 정적 팩터리 메서드를 사용하면 각 메서드에 명확한 이름을 부여할 수 있습니다.
  • 예제:
    public class Vehicle {
        private String type;
        private int wheels;
    
        private Vehicle(String type, int wheels) {
            this.type = type;
            this.wheels = wheels;
        }
    
        public static Vehicle createCar() {
            return new Vehicle("Car", 4);
        }
    
        public static Vehicle createBike() {
            return new Vehicle("Bike", 2);
        }
    }
    
    // 사용 예시
    Vehicle car = Vehicle.createCar();
    Vehicle bike = Vehicle.createBike();
    
    이렇게 하면 Vehicle.createCar()Vehicle.createBike()를 통해 명확하게 객체를 생성할 수 있습니다.

4.2. 객체의 인스턴스를 재사용해야 할 때

  • 설명: 특정 조건에서 이미 생성된 객체를 재사용해야 하는 경우, 정적 팩터리 메서드를 통해 이를 구현할 수 있습니다.
  • 예제:
    public class Status {
        private String code;
    
        private Status(String code) {
            this.code = code;
        }
    
        public static final Status ACTIVE = new Status("ACTIVE");
        public static final Status INACTIVE = new Status("INACTIVE");
    
        public static Status valueOf(String code) {
            switch(code) {
                case "ACTIVE":
                    return ACTIVE;
                case "INACTIVE":
                    return INACTIVE;
                default:
                    throw new IllegalArgumentException("Unknown code: " + code);
            }
        }
    
        public String getCode() {
            return code;
        }
    }
    
    // 사용 예시
    Status active1 = Status.valueOf("ACTIVE");
    Status active2 = Status.valueOf("ACTIVE");
    System.out.println(active1 == active2); // true
    
    여기서 Status.valueOf("ACTIVE")는 항상 동일한 ACTIVE 인스턴스를 반환합니다. 이는 객체 생성을 줄이고 메모리를 절약합니다.

4.3. 반환 타입을 유연하게 하고 싶을 때

  • 설명: 정적 팩터리 메서드는 반환 타입을 유연하게 지정할 수 있습니다. 이는 인터페이스 기반의 설계에서 특히 유용합니다.
  • 예제:
    public interface Connection {
        void connect();
    }
    
    public class DatabaseConnection implements Connection {
        @Override
        public void connect() {
            System.out.println("Connecting to database");
        }
    }
    
    public class ConnectionFactory {
        public static Connection createConnection() {
            return new DatabaseConnection();
        }
    }
    
    // 사용 예시
    Connection connection = ConnectionFactory.createConnection();
    connection.connect();
    
    클라이언트는 ConnectionFactory.createConnection()을 통해 Connection 인터페이스 타입의 객체를 받게 되며, 실제 구현 클래스(DatabaseConnection)에 의존하지 않습니다.

4.4. 객체 생성 로직이 복잡할 때

  • 설명: 객체 생성 시 복잡한 초기화 로직이 필요할 때, 이를 정적 팩터리 메서드 내에 캡슐화할 수 있습니다.
  • 예제:
    public class ComplexObject {
        private String partA;
        private String partB;
    
        private ComplexObject(String partA, String partB) {
            this.partA = partA;
            this.partB = partB;
        }
    
        public static ComplexObject createWithDefaultParts() {
            String defaultA = "DefaultA";
            String defaultB = "DefaultB";
            return new ComplexObject(defaultA, defaultB);
        }
    }
    
    // 사용 예시
    ComplexObject obj = ComplexObject.createWithDefaultParts();
    

5. 정적 팩터리 메서드의 단점과 고려사항

물론, 정적 팩터리 메서드가 항상 더 좋은 선택은 아닙니다. 몇 가지 단점도 존재합니다.

5.1. 서브클래싱 불가능

  • 설명: 정적 팩터리 메서드는 생성자가 아니기 때문에, 서브클래스를 만들 때 호출할 수 없습니다. 이는 상속을 통한 확장이 필요할 경우 불편할 수 있습니다.
  • 해결 방안: 필요한 경우, 정적 팩터리 메서드와 함께 생성자를 적절히 사용할 수 있습니다.

5.2. 코드의 명확성 저하

  • 설명: 클래스에 정적 팩터리 메서드가 많아지면, 어떤 메서드가 어떤 역할을 하는지 파악하기 어려워질 수 있습니다.
  • 해결 방안: 정적 팩터리 메서드에 의미 있는 이름을 부여하고, 문서화하여 사용자가 쉽게 이해할 수 있도록 합니다.

5.3. 도구와의 호환성 문제

  • 설명: 일부 라이브러리나 도구는 객체 생성을 위해 생성자를 기대할 수 있습니다. 정적 팩터리 메서드를 사용하는 경우, 이러한 도구와의 호환성 문제가 발생할 수 있습니다.
  • 해결 방안: 필요한 경우, 생성자도 함께 제공하여 호환성을 유지할 수 있습니다.

6. 실제 사례: Java 표준 라이브러리에서의 사용

Java 표준 라이브러리에서도 정적 팩터리 메서드를 널리 사용하고 있습니다. 몇 가지 대표적인 예를 들어보겠습니다.

6.1. java.util.Optional

  • 설명: Optional 클래스는 값을 감싸는 컨테이너로, of(), ofNullable(), empty() 등의 정적 팩터리 메서드를 제공합니다.
  • 예제:
    Optional<String> opt1 = Optional.of("Hello"); // null이 아닌 값을 감쌉니다.
    Optional<String> opt2 = Optional.ofNullable(null); // null일 수 있는 값을 감쌉니다.
    Optional<String> opt3 = Optional.empty(); // 빈 Optional을 생성합니다.
    

6.2. java.time.LocalDate

  • 설명: LocalDate 클래스는 불변의 날짜 객체로, of(), now(), parse() 등의 정적 팩터리 메서드를 통해 객체를 생성합니다.
  • 예제:
    LocalDate date1 = LocalDate.of(2024, Month.APRIL, 27);
    LocalDate date2 = LocalDate.now();
    LocalDate date3 = LocalDate.parse("2024-04-27");
    

6.3. java.util.Collections

  • 설명: Collections 클래스는 다양한 정적 팩터리 메서드를 제공하여 불변 리스트, 세트, 맵 등을 생성할 수 있습니다.
  • 예제:
    List<String> list = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
    Set<String> set = Collections.singleton("A");
    Map<String, String> map = Collections.emptyMap();
    

7. 결론

정적 팩터리 메서드는 생성자에 비해 여러 가지 이점을 제공하며, 객체 생성의 유연성과 효율성을 높일 수 있는 강력한 도구입니다. 특히 다음과 같은 상황에서 정적 팩터리 메서드를 고려하는 것이 유리합니다.

  1. 명확한 이름을 부여하고 싶을 때: 다양한 생성 방법을 명확하게 표현할 수 있습니다.
  2. 객체의 인스턴스를 재사용하거나 캐싱하고 싶을 때: 메모리 사용을 최적화할 수 있습니다.
  3. 인터페이스 기반의 설계를 구현하고 싶을 때: 구현 클래스를 숨길 수 있습니다.
  4. 복잡한 객체 생성 로직을 캡슐화하고 싶을 때: 클라이언트 코드의 간결성을 유지할 수 있습니다.
  5. 제네릭 타입의 객체를 쉽게 생성하고 싶을 때: 타입 매개변수 추론을 활용할 수 있습니다.

물론, 정적 팩터리 메서드에도 단점이 있으므로, 상황에 따라 적절히 사용해야 합니다. 그러나 객체 생성의 유연성과 효율성을 높이고자 할 때, 정적 팩터리 메서드는 강력한 대안이 될 수 있습니다.

핵심 요약:

  • 정적 팩터리 메서드는 생성자에 비해 더 많은 유연성과 표현력을 제공합니다.
  • 명확한 메서드 이름, 인스턴스 재사용, 서브클래스 반환 등의 이점을 통해 코드의 가독성과 유지보수성을 향상시킵니다.
  • Java 표준 라이브러리에서도 널리 사용되며, 실용적인 예제로 그 효용성을 입증하고 있습니다.

정적 팩터리 메서드를 적절히 활용함으로써, 더 견고하고 효율적인 객체 지향 설계를 구현할 수 있습니다.

0개의 댓글