본문 바로가기

Language/JAVA

[이펙티브 자바][아이템 1] 생성자 대신 정적 팩토리를 고려해라 - 컴도리돌이

728x90

자바에서 객체를 생성하는 전통적인 방법은 'new' 키워드를 사용하는 생성자 호출이에요. 그러나 이번 주제에서는 생성자 대신 정적 팩토리 메서드(static factory methods)를 고려할 것을 권장하는 것을 다뤄보려고 합니다. 

 

1. 명명된 생성자를 통한 가독성 향상

자바에서 객체를 생성할 때, 일반적으로 사용하는 방법은 'new' 키워드를 사용하여 생성자를 호출합니다. 근데 이런 방식은 추후에 다른 개발자가 해당 코드를 사용할 때, 조금 많이 힘든 경우가 있어요 🥲

 

생성자는 클래스 이름과 동일해야 하며, 여러 인자를 받는 경우에도 생성자 자체에 이름을 지정할 수 없죠. 이로 인해 생성자의 목적이나 인자들이 무엇을 의미하는지 명확하지 않을 수 있어요. 

 

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

    public User(String name, int age, boolean isActive) {
        this.name = name;
        this.age = age;
        this.isActive = isActive;
    }
}

 

위에 코드처럼 User를 생성할 때, 세 개의 인자를 받아서 생성합니다. 이 생성자를 호출할 때는 다음과 같이 사용할 수 있습니다. 

 

User user = new User("Comdoliol-i", 30, true);

 

여기서 문제는 이 생성자 호출만으로는 각 인자의 무엇을 의미하는지 명확하지 않다는 것입니다. 물론 첫 번째는 이름을 나타내고, 두 번째는 나이를 나타내겠죠? 하지만 마지막 인자는 어떤 의미를 담고 있는지 직관적으로 알기 어렵습니다. 😓

또한 새로운 생성자를 추가하게 되면 각 생성자의 의미를 기억하기 더더욱 어려워지겠죠..

 

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

    private User(String name, int age, boolean isActive) {
        this.name = name;
        this.age = age;
        this.isActive = isActive;
    }

    public static User activeUser(String name, int age) {
        return new User(name, age, true);
    }

    public static User inactiveUser(String name, int age) {
        return new User(name, age, false);
    }
}

 

정적 팩토리 메서드를 사용하면 생성자의 의도를 명확히 드러낼 수 있습니다. 정적 팩토리 메서드는 클래스 메서드이므로, 우리가 원하는 이름을 붙여 사용할 수 있죠. 위에 코드는 User클래스를 생성하는 코드를 정적팩토리 메서드 방식으로 사용한 코드입니다. 

 

위와 같이 코드를 작성하면 객체를 생성할 때 따음과 같이 명확한 메서드 이름을 사용할 수 있습니다. 

 

User activeUser = User.activeUser("Comdoliol-i", 30);
User inactiveUser = User.inactiveUser("Binary", 26);

 

위 코드에서 메서드 이름이 각각의 객체가 활성 상태인지 비활성 상태인지 명확히 드러내 줍니다. 더 이상 true, false 같은 값을 보고 그 의미를 추측할 필요가 없게 된 거죠 😆

 

Java 21에서는 불변 데이터 클래스를 쉽게 정의할 수 있는 Record 기능을 활용할 수 있습니다. Record는 내부적으로 정적 팩토리 메서드 패턴과 잘 어울리며, 단순한 데이터 클래스에서 객체를 생성할 때 사용할 수 있습니다.

 

public record Point(int x, int y) {
    public static Point of(int x, int y) {
        return new Point(x, y);
    }
}

 

여기서 of 메서드는 정적 팩토리 메서드로, Point 객체를 생성합니다. 이 방식은 생성자보다 명명 가능성(naming flexibility)을 제공하고, 객체 생성 로직을 더욱 명확하게 표현할 수 있습니다.

 


2. 객체 생성을 관리하고 캐싱할 수 있음

정적 팩토리 메서드는 생성자와 달리 객체 생성에 대한 완전한 통제권을 제공합니다. 이를 통해 객체의 생성을 관리하거나, 필요에 따라 동일한 객체를 개싱하여 재사용하는 기능을 구현할 수 있습니다. 

 

2-1 객체 캐싱

객체 캐싱은 동일한 객체를 여러 번 생성할 필요가 없을 때 사용됩니다. 대표적인 예로, Boolean 클래스의 valueOf 메서드를 들어보겠습니다. 자바에서 Boolean 클래스는 truefalse 값을 각각 TRUE, FALSE라는 상수로 제공하며, 이 값들을 캐싱하여 재사용합니다. 

 

public class BooleanFactory {
    private static final Boolean TRUE = new Boolean(true);
    private static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return b ? TRUE : FALSE;
    }
}

 

 

위 코드에서 valueOf 메서드는 new Boolean()을 통해 매번 새로운 객체를 생성하지 않고, 미리 정의된 TRUE, FALSE 객체를 반환해 줍니다. 이렇게 하면 불필요한 객체 생성을 피하고, 메모리 사용량을 줄일 수 있는 장점이 생기게 되죠!

 

2-2 불변 객체의 재사용

불변 객체는 상태가 변하지 않으므로, 동일한 상태를 가진 객체를 여러 번 생성할 필요가 없죠. 이러한 불변 객체를 캐싱하면, 객체 생성 비용을 줄일 수 있는 장점이 있습니다. 

 

public final class Point {
    private final int x;
    private final int y;

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public static Point of(int x, int y) {
        return new Point(x, y);
    }
    
    public static Point polar(double radius, double theta) {
        return new Point((int) (radius * Math.cos(theta)), (int) (radius * Math.sin(theta)));
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

 

 

여기서 Point 클래스는 x와 y 값을 가지며, 생성된 후에는 이 값들이 변경되지 않습니다. of 정적 팩토리 메서드를 사용하여 객체를 생성할 수 있으며, 생성자는 private으로 선언되어 위부에서 접근할 수 없게 되죠! 이는 객체의 불변성을 보장하게 됩니다. 

 

polar 메서드는 극좌표계를 사용해 객체를 생성합니다. 두 메서드는 모두 불변 객체를 반환하며, 클라이언트는 객체 생성 방법에 따라 적절한 팩토리 메서드를 선택할 수 있게 되죠 👍

 

자바에서는 List, Set, Map 등의 불변 컬렉션을 생성할 수 있는 기능일 잘 갖추어져 있는데, 이를 정적 팩토리 메서드에서 활용하여 불변 객체를 쉽게 생성할 수 있어요.

 

public class Person {
    private final String name;
    private final List<String> hobbies;

    private Person(String name, List<String> hobbies) {
        this.name = name;
        this.hobbies = List.copyOf(hobbies); // Immutable copy
    }

    public static Person of(String name, String... hobbies) {
        return new Person(name, List.of(hobbies));
    }

    // Other methods...
}

 

위에 코드에서 hobbies 리스트를 불변으로 복사하여 Person 객체를 생성해 줄 수 있어요. 또한 List.copyOf 메서드는 불변 리스트를 반환하므로, 객체의 내부 상태가 안전하게 유지됩니다.

 

 

 

Java 10 List.copyOf(), Set.copyOf() And Map.copyOf() Methods

Java 10 copyOf() methods with examples, Java 10 immutable collections, Java 10 List.copyOf(), Set.copyOf() and Map.copyOf() methods...

javaconceptoftheday.com

 

2-3 플라이웨이트 패턴(Flyweight Pattern) 

플라이웨이트 패턴은 객체를 가능한 한 공유하여 메모리 사용량을 최소화하는 디자인 패턴이에요. 이 패턴은 대량의 작은 객체를 관리할 때 유용하며, 정적 팩토리 메서드를 사용하면 이 패턴을 쉽게 구현할 수 있어요. 플라이웨이트 패턴에 대해서는 아래 글 참고하시면 좋을 거 같아요 😆

 

 

[디자인 패턴] 플라이웨이트 패턴(Flyweight Pattern)에 대해서 - 컴도리돌이

다수의 유사한 객체를 생성하면 메모리 사용량이 급격히 증가할 수 있어요. 😓이러한 문제를 해결하기 위해 플라이 웨이트 패턴은 동일한 객체를 공유하여 메모리 낭비를 줄이고 성능을 향상

comdolidol-i.tistory.com

 

 

import java.util.HashMap;
import java.util.Map;

public class CharacterFactory {
    private static final Map<Character, Character> cache = new HashMap<>();

    public static Character getCharacter(char c) {
        cache.putIfAbsent(c, c);
        return cache.get(c);
    }
}

 

여기서 CharacterFactory.getCharacter 메서드는 요청된 문자를 캐싱하고, 동일한 문자를 요청할 때는 캐싱된 객체를 반환합니다. 이러한 방법은 특히 대량의 객체를 다룰 때 메모리 사용을 최소 하는 데 매우 효율적이에요 👍

 

2-4 인스턴스 제한

정적 팩토리 메서드는 객체의 인스턴스 개수를 제한하는 데도 사용될 수 있습니다. 예를 들어, 특정 클래스의 인스턴스를 하나만 유지해야 하는 싱글톤 패턴이나, 제한된 개수만큼 인스턴스를 생성해야 하는 경우에도 유용합니다. 

 

public class LimitedInstanceClass {
    private static final int MAX_INSTANCES = 3;
    private static int instanceCount = 0;
    private static final LimitedInstanceClass[] instances = new LimitedInstanceClass[MAX_INSTANCES];

    private LimitedInstanceClass() {
        // Private constructor to prevent external instantiation
    }

    public static LimitedInstanceClass getInstance() {
        if (instanceCount < MAX_INSTANCES) {
            instances[instanceCount] = new LimitedInstanceClass();
            instanceCount++;
        }
        return instances[(instanceCount - 1) % MAX_INSTANCES];
    }
}

 

이 클래스에서는 최대 3개의 인스턴스만 생성할 수 있으며, 그 이상 요청이 들어오면 이미 생성된 인스턴스 중 하나를 반환하게 돼요.

 


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");
    }
}

 

여기서 shape 인터페이스를 구현한 여러 클래스가 존재할 수 있습니다. 이제 정적 팩토리 메서드를 사용하여 클라이언트가 구체적인 클래스에 의존하지 않도록 할 수 있습니다.  

public class ShapeFactory {
    public static Shape getShape(String type) {
        if ("circle".equalsIgnoreCase(type)) {
            return new Circle();
        } else if ("square".equalsIgnoreCase(type)) {
            return new Square();
        }
        throw new IllegalArgumentException("Unknown shape type");
    }
}

 

클라이언트 코드는 이제 구체적인 클래스를 알 필요 없이, 단지 ShapeFactory를 통해 Shape 객체를 얻어 사용할 수 있습니다. 

 

Shape shape1 = ShapeFactory.getShape("circle");
Shape shape2 = ShapeFactory.getShape("square");

 

이 패턴은 클라이언트 코드의 변경을 최소화하면서도 구현 클래스를 자유롭게 교체하거나 확장할 수 있게 되는 거죠. 

 

정적 팩토리 메서드는 추상 팩토리 패턴의 개념을 단순화하여 하나의 메서드에서 여러 타입의 객체를 반환할 수 있게 해줘요. 특히 반환 타입을 구체적인 클래스에 의존하지 않고, 인터페이스나 추상 클래스를 통해 유연하게 처리할 수 있죠. 

 

 

[Design Pattern] 추상 팩토리(Abstract Factory)에 대해서 - 컴도리돌이

추상 팩토리 디자인 패턴(Abstract Factory Design Pattern)은 객체 생성에 관련된 일련의 인터페이스를 제공하여, 관련 객체들의 생성을 캡슐화하고 클라이언트 코드가 구체적인 클래스의 인스턴스를

comdolidol-i.tistory.com

 

자바 17에서 도입된 패턴 매칭과 switch 표현식은 객체의 타입에 따라 동작을 간결하게 정의할 수 있습니다. 이는 복잡한 조건 로직을 처리할 때 매우 유용하며, 불변 객체와의 조합에서도 유용합니다. 

 

 

[JAVA 21] Pattern Matching for switch - 컴도리돌이

스위치문과 null (switches and null) 전통적으로, switch 문과 표현식은 선택기 표현식이 null을 검증할 때, NullPointerException을 throw 합니다. 따라서 null을 테스트하기 위해 switch 바깥에서 테스트해야 했습

comdolidol-i.tistory.com

 

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}

public double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square s -> s.side() * s.side();
        default -> throw new IllegalStateException("Unexpected value: " + shape);
    };
}