Java

[Effective Java] 2-1. 객체 생성과 파괴: 생성자 대신 정적 팩토리 메서드를 고려하라

noahkim_ 2024. 12. 26. 23:29

조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.

 

1. 정적 팩토리 메서드

  • 클래스는 생성자와는 별도로 static factory method 제공이 가능함

 

2. 장점

장점 설명
이름을 가질 수 있음 생성자와 달리 이름으로 의미 전달 가능
동일 시그니처 생성자 문제 해결 매개변수만 다르고 타입이 같으면 컴파일 에러 발생 → 정적 메서드로 해결
인스턴스 캐싱 매번 새 객체를 생성하지 않고, 재사용 가능
인스턴스 생성 통제 싱글턴, 불변 객체, 생성 불가 등 구현 가능
반환 타입을 하위 타입으로 지정 가능 인터페이스 반환 → 실제 구현은 감춤
입력값에 따라 다른 클래스 인스턴스 반환 서브클래스를 숨기고, 조건 따라 인스턴스 선택
반환 객체 클래스가 미리 없어도 됨 정적 팩터리 메서드 작성 시점에 구현체가 없어도 됨
서비스 제공자 프레임워크 구현 가능 추상화된 API + 구현을 분리 → 유연한 확장
인스턴스 생성을 서브클래스에 위임 구현체가 인스턴스를 생성하도록 위임

 

예시) 이름을 가짐

더보기
public static Order primeOrder(Product product) {
    Order order = new Order();
    order.prime = true;
    order.product = product;
    return order;
}

public static Order urgentOrder(Product product) {
    Order order = new Order();
    order.urgent = true;
    order.product = product;
    return order;
}

 

 

예시) 동일 시그니처 생성자가 여러 개 필요할 경우

더보기
public class Order {
    private Product product;
    private boolean prime, urgent;

    public Order(Product product, boolean urgent) {
        this.product = product;
        this.urgent = urgent;
    }

    public Order(Product product, boolean prime) {
        this.product = product;
        this.prime = prime;
    }
}

 

예시) 인스턴스 캐싱

더보기
public class Settings {
    private boolean useAutoSteering;
    private boolean useABS;
    private Difficulty difficulty;

    private Settings() {}

    private static Settings SETTINGS = new Settings();

    public static Settings newInstance() return SETTINGS;
}
  • 매개 변수에 따라 다른 인스턴스 객체를 리턴할 수 있음 (ex. Boolean.valueOf(boolean))
  • 불변 클래스를 다룰 때 유용
  • 플라이웨이트 패턴과 유사함

 

예시) 인스턴스 생성을 철저히 통제할 수 있음

더보기

싱글턴 클래스화

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

    private Singleton() {} // 외부에서 new 불가

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

 

불변 객체

public final class Money {
    private final int amount;

    private Money(int amount) {
        this.amount = amount;
    }

    private static final Map<Integer, Money> CACHE = new ConcurrentHashMap<>();

    public static Money of(int amount) {
        return CACHE.computeIfAbsent(amount, Money::new);
    }
}

 

인스턴스화 불가

public class MathUtils {
    private MathUtils() {
        throw new AssertionError("No MathUtils instance allowed");
    }

    public static int add(int a, int b) {
        return a + b;
    }
}

 

열거 타입

public enum AppSettings {
    INSTANCE;

    public void doSomething() {
        System.out.println("Doing something...");
    }
}
  • 싱글턴을 가장 안전하게 구현할 수 있는 방식
  • 직렬화, 리플렉션 회피가 자동으로 보장됨

 

예시) 입력 매개변수에 따라 매번 다른 클래스의 객체 반환 가능

더보기

전략 패턴

public class EmailSender implements MessageSender {
    public void send(String message) {
        System.out.println("📧 Email: " + message);
    }
}

public class SmsSender implements MessageSender {
    public void send(String message) {
        System.out.println("📱 SMS: " + message);
    }
}

 

팩토리 메서드

public class MessageSenderFactory {
    public static MessageSender getSender(String type) {
        return switch (type.toLowerCase()) {
            case "email" -> new EmailSender();
            case "sms" -> new SmsSender();
            default -> throw new IllegalArgumentException("Unknown sender type");
        };
    }
}

 

예시) 반환할 객체의 클래스가 존재하지 않아도 됨

더보기

추상화 제공

public static void main(String[] args) {
    String driverName = "com.mysql.jdbc.Driver";
    String url = "jdbc:mysql://localhost:3306/test";
    String user = "root";
    String password = "soyeon";

    try {
        Class.forName(driverName);

        // 서비스 접근 API인 DriverManager.getConnection가 서비스 구현체(서비스 인터페이스)인 Connection 반환
        Connection connection = DriverManager.getConnection(url, user, password);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}
  • 클라이언트 코드와의 종속성을 낮추고 결합도를 느슨하게 함
  • 이러한 유연함은 서비스 제공자 프레임워크를 만드는 근간이 됨

 

예) 서비스 제공자 프레임워크

더보기

Logger

public interface Logger {
    void log(String message);
}
public class ConsoleLogger implements Logger {
    public void log(String message) {
        System.out.println("[Console] " + message);
    }
}
Logger logger = LoggerFactory.getLogger(); // 내부에서 구현체 반환
logger.log("안녕 충밤!");
  • 인터페이스를 기준으로, 실제 구현체를 외부에서 플러그인처럼 제공받는 구조
  • 클라이언트와 구현이 느슨하게 연결되어 새로운 구현을 쉽게 교체하거나 확장할 수 있음

 

예) 인스턴스 생성을 서브클래스에게 위임

더보기
abstract class Shape {
    protected DrawingAPI drawingAPI;

    protected Shape(DrawingAPI drawingAPI) { this.drawingAPI = drawingAPI; }
            
    public abstract void draw();
}

class CircleShape extends Shape {
    private double x, y, radius;

    public CircleShape(double x, double y, double radius, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public void draw() { drawingAPI.drawCircle(x, y, radius); }
}
  • 추상화된 부분과 구현 부분을 서로 다른 클래스 계층구조로 분리
  • 서로 영향을 주지 않으면서 독립적으로 확장하도록 하기 위함
  • 브릿지 패턴과 유사

 

3. 단점

단점 설명
하위 클래스 생성 불가 정적 팩터리 메서드만 쓰고 생성자가 private이면 상속이 불가능함
프로그래머가 찾기 어려움 new 키워드 없이 메서드로 객체를 생성하므로 IDE에서 자동으로 찾기 어려움

 

4. 정적 팩토리 메서드 네이밍 규약

메서드 이름 의미 특징/사용 예
from 하나의 매개변수로 인스턴스 생성 Duration.from(TemporalAmount amount)
of 여러 매개변수로 적절한 인스턴스 생성 List.of("a", "b"), EnumSet.of(E e1, E... others)
valueOf from 또는 of보다 더 명시적이고 세부적 Boolean.valueOf(true)
getInstance 재사용 가능한 인스턴스 반환 (동일 인스턴스 아님) Calendar.getInstance()
newInstance 매번 새로운 객체를 반환함을 보장 Class.newInstance()
getType 팩토리 클래스에서 Type 반환 (재사용 가능) FileSystem.getFileSystem()
newType 팩토리 클래스에서 Type 반환 (항상 새 인스턴스) DocumentBuilderFactory.newDocumentBuilder()