객체 생성 방식 중 하나인 생성자(constructor) 대신 정적 팩터리 메서드(static factory method)를 고려해야 하는 이유에 대해 쉽고 자세하게 설명드리겠습니다. 이 개념은 Joshua Bloch의 저서 Effective Java에서 자세히 다루어졌으며, Java 프로그래밍에서 객체 생성의 유연성과 효율성을 높이는 중요한 패턴입니다.
예제:
public class User {
private String name;
private int age;
// 생성자
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter 생략
}
예제:
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 생략
}
정적 팩터리 메서드는 단순한 객체 생성 이상의 이점을 제공합니다. 다음은 주요 이유들입니다.
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); // 기본 이름 설정
}
}
위 예제에서 createWithName과 createWithAge는 각각 이름 또는 나이를 기준으로 객체를 생성합니다. 이는 생성자를 사용하는 것보다 더 명확한 의도를 전달합니다.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 인스턴스를 반환하며, 이는 객체 생성을 줄이고 메모리를 절약합니다.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 인터페이스에만 의존하게 되어, 구현 클래스의 변경에 영향을 받지 않습니다.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()를 호출하여 복잡한 초기화 과정을 신경 쓰지 않고 객체를 생성할 수 있습니다.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> 타입을 추론하여 반환하며, 생성자를 직접 사용할 때보다 더 간결합니다.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);
이렇게 여러 가지 정적 팩터리 메서드를 통해 다양한 방식으로 객체를 생성할 수 있습니다.아래는 생성자와 정적 팩터리 메서드의 주요 차이점과 각각의 장단점을 비교한 표입니다.
| 특징 | 생성자(Constructor) | 정적 팩터리 메서드(Static Factory Method) |
|---|---|---|
| 이름 지정 가능성 | 클래스 이름과 동일해야 함 | 의미 있는 이름을 가질 수 있음 |
| 인스턴스 재사용 | 매번 새로운 객체를 생성함 | 이미 생성된 인스턴스를 재사용하거나 캐싱 가능 |
| 서브클래스 반환 가능성 | 항상 자신의 클래스 타입을 반환함 | 서브클래스 객체를 반환할 수 있음 |
| 복잡한 객체 생성 로직 | 복잡한 로직을 생성자에 포함시키기 어려움 | 생성 로직을 메서드 내에 캡슐화 가능 |
| 타입 매개변수 추론 지원 | 제네릭 타입을 사용할 때 명시적 타입 지정 필요 | 제네릭 타입을 자동으로 추론 가능 |
| 가독성 및 명확성 | 간단하지만, 여러 생성자가 있을 경우 혼란스러울 수 있음 | 의미 있는 메서드 이름으로 의도를 명확히 전달 가능 |
| 불변 객체 생성 | 생성자만으로도 가능하지만, 정적 팩터리 메서드가 더 유연함 | 객체의 불변성을 더욱 쉽게 유지하고, 관련 로직을 추가할 수 있음 |
정적 팩터리 메서드를 사용하는 것이 특히 유용한 상황은 다음과 같습니다.
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()를 통해 명확하게 객체를 생성할 수 있습니다.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 인스턴스를 반환합니다. 이는 객체 생성을 줄이고 메모리를 절약합니다.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)에 의존하지 않습니다.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();
물론, 정적 팩터리 메서드가 항상 더 좋은 선택은 아닙니다. 몇 가지 단점도 존재합니다.
Java 표준 라이브러리에서도 정적 팩터리 메서드를 널리 사용하고 있습니다. 몇 가지 대표적인 예를 들어보겠습니다.
java.util.OptionalOptional 클래스는 값을 감싸는 컨테이너로, of(), ofNullable(), empty() 등의 정적 팩터리 메서드를 제공합니다.Optional<String> opt1 = Optional.of("Hello"); // null이 아닌 값을 감쌉니다.
Optional<String> opt2 = Optional.ofNullable(null); // null일 수 있는 값을 감쌉니다.
Optional<String> opt3 = Optional.empty(); // 빈 Optional을 생성합니다.
java.time.LocalDateLocalDate 클래스는 불변의 날짜 객체로, of(), now(), parse() 등의 정적 팩터리 메서드를 통해 객체를 생성합니다.LocalDate date1 = LocalDate.of(2024, Month.APRIL, 27);
LocalDate date2 = LocalDate.now();
LocalDate date3 = LocalDate.parse("2024-04-27");
java.util.CollectionsCollections 클래스는 다양한 정적 팩터리 메서드를 제공하여 불변 리스트, 세트, 맵 등을 생성할 수 있습니다.List<String> list = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
Set<String> set = Collections.singleton("A");
Map<String, String> map = Collections.emptyMap();
정적 팩터리 메서드는 생성자에 비해 여러 가지 이점을 제공하며, 객체 생성의 유연성과 효율성을 높일 수 있는 강력한 도구입니다. 특히 다음과 같은 상황에서 정적 팩터리 메서드를 고려하는 것이 유리합니다.
물론, 정적 팩터리 메서드에도 단점이 있으므로, 상황에 따라 적절히 사용해야 합니다. 그러나 객체 생성의 유연성과 효율성을 높이고자 할 때, 정적 팩터리 메서드는 강력한 대안이 될 수 있습니다.
핵심 요약:
정적 팩터리 메서드를 적절히 활용함으로써, 더 견고하고 효율적인 객체 지향 설계를 구현할 수 있습니다.