Java/effective java

[Effective Java] Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라.

mint* 2025. 1. 5. 18:32
728x90

Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라.

정적 팩토리 메서드

클래스의 인스턴스를 반환하는 정적 메서드

  • 클라이언트는 클래스의 인스턴스를 public 생성자로 얻는다.
  • 생성자와 별도로, 정적 팩터리 메서드를 제공하여 인스턴스를 반환할 수 있다.
public class Car {

    private final String make;
    private final String model;
    private final int year;
    private final String color;
    private final boolean isElectric;

    // 생성자
    public Car(String make, String model, int year, String color, boolean isElectric) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.color = color;
        this.isElectric = isElectric;
    }

    // 정적 팩토리 메서드들
    public static Car createSedan(String make, String model, int year) {
        return new Car(make, model, year, "Silver", false);
    }

    public static Car createElectricCar(String make, String model, int year) {
        return new Car(make, model, year, "White", true);
    }

    public static Car createNextYearModel(Car currentCar) {
        return new Car(currentCar.make, currentCar.model, currentCar.year + 1, 
                       currentCar.color, currentCar.isElectric);
    }

    @Override
    public String toString() {
        return year + " " + make + " " + model + " (" + color + ", " + 
               (isElectric ? "Electric" : "Gas") + ")";
    }
}

public class CarFactory {
    public static void main(String[] args) {
        // 생성자 사용
        Car customCar = new Car("Toyota", "Camry", 2023, "Blue", false);
        System.out.println("Custom Car: " + customCar);

        // 정적 팩토리 메서드 사용
        Car sedan = Car.createSedan("Honda", "Accord", 2023);
        System.out.println("Sedan: " + sedan);

        Car electricCar = Car.createElectricCar("Tesla", "Model 3", 2023);
        System.out.println("Electric Car: " + electricCar);

        // 기존 차량으로부터 새 모델 생성
        Car nextYearSportsCar = Car.createNextYearModel(sportsCar);
        System.out.println("Next Year Sports Car: " + nextYearSportsCar);
    }
}

 

정적 팩토리 메서드 특징

1. 이름을 가질 수 있다.

  • 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
    • 생성자에 넘기는 매개 변수와 생성자 자체 만으로는 반환될 객체의 특성을 제대로 설명하기 어렵다.
public class Pizza {
    private int size;
    private boolean cheese;
    private boolean pepperoni;
    private boolean bacon;

    // 생성자
    public Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) {
        this.size = size;
        this.cheese = cheese;
        this.pepperoni = pepperoni;
        this.bacon = bacon;
    }

    // 정적 팩터리 메서드들
    public static Pizza smallVeggie() { // 이름 생성 가능
        return new Pizza(10, true, false, false);
    }

    public static Pizza mediumMeatLovers() {
        return new Pizza(12, true, true, true);
    }

    public static Pizza largeCheese() {
        return new Pizza(14, true, false, false);
    }
}

public class PizzaOrder {
    public static void main(String[] args) {
        // 생성자 사용
        Pizza customPizza = new Pizza(12, true, false, true);

        // 정적 팩터리 메서드 사용
        Pizza veggiePizza = Pizza.smallVeggie();
        Pizza meatLoversPizza = Pizza.mediumMeatLovers();
        Pizza cheesePizza = Pizza.largeCheese();
    }
}
  • 하나의 시그니처로는 생성자를 하나만 만들 수 있지만, 정적 팩토리 메서드는 제약이 없다.
    => 시그니처가 같은 생성자가 여러 개 필요하다면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.

 

2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

    • 불필요한 객체 생성을 피할 수 있다.
      • 불변 클래스라면 인스턴스를 미리 만들어 놓거나 새로 생성된 인스턴스를 캐싱하여 재활용할 수 있다.
        • ex) Integer.valueOf(100);
    • 인스턴스 통제 : 언제 인스턴스를 살아있게 할지 통제할 수 있다.
캐싱을 통해 성능을 끌어올릴 수 있다. 플라이웨이트 패턴도 이와 비슷한 기법이라 할 수 있다.

 

Flyweight 패턴

  • 객체의 공통된 상태를 공유하여 메모리 사용을 최적화하는 패턴
  • 객체 공유: 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 공유하도록 한다.
  • 내부 상태와 외부 상태
    • 내부 상태: 여러 객체 간에 공유될 수 있는 불변의 데이터
    • 외부 상태: 각 객체마다 고유한, 변할 수 있는 데이터
  • 팩토리: Flyweight 객체를 생성하고 관리하는 팩토리를 사용한다.
import java.util.HashMap;
import java.util.Map;

// Flyweight 인터페이스
interface Shape {
    void draw();
}

class Circle implements Shape {
    private String color; // 내부 상태
    private int x, y, radius; // 외부 상태

    public Circle(String color) {
        this.color = color;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing Circle[ color: " + color + 
                           ", x: " + x + ", y: " + y + 
                           ", radius: " + radius + " ]");
    }
}

// Flyweight Factory
class ShapeFactory {
    private static final Map<String, Shape> circleMap = new HashMap<>();

    public static Shape getCircle(String color) {
        Circle circle = (Circle)circleMap.get(color);

        if(circle == null) { // 없으면 생성
            circle = new Circle(color);
            circleMap.put(color, circle);
            System.out.println("Creating circle of color : " + color);
        }
        return circle; // 존재하면 기존 객체 반환
    }
}

// 클라이언트
public class FlyweightExample {

    private static final String[] colors = { "Red", "Green", "Blue", "White", "Black" };

    public static void main(String[] args) {
        for(int i=0; i < 20; ++i) {
            Circle circle = (Circle)ShapeFactory.getCircle(getRandomColor());
            circle.setX(getRandomX());
            circle.setY(getRandomY());
            circle.setRadius(100);
            circle.draw();
        }
    }

    private static String getRandomColor() {
        return colors[(int)(Math.random() * colors.length)];
    }

    private static int getRandomX() {
        return (int)(Math.random() * 100);
    }

    private static int getRandomY() {
        return (int)(Math.random() * 100);
    }
}
각 색깔마다 한번씩만 객체가 생성되므로 메모리 사용량이 감소한다.

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

  • 반환할 객체의 클래스를 자유롭게 선택할 수 있다.
    • 정적 팩토리는 public으로 공개하고 인터페이스를 반환하여 구현 클래스를 숨길 수 있다.
    • 클라이언트의 코드가 인터페이스에 의존하여 클라이언트는 구현 세부사항을 알 필요가 없다.
인터페이스를 중심으로 프레임워크를 만드는 핵심 기술이다.
// 인터페이스
public interface Animal {
    void makeSound();
}

// 구현 클래스 (비공개)
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

// 구현 클래스 (비공개)
class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

// 팩토리 클래스
public class AnimalFactory {
    // 정적 팩토리 메서드
    public static Animal createDog() { // 반환 타입 : 인터페이스
        return new Dog();
    }

    public static Animal createCat() {
        return new Cat();
    }
}

// 클라이언트
public class Main {
    public static void main(String[] args) {
        Animal dog = AnimalFactory.createDog(); 
        Animal cat = AnimalFactory.createCat();

        // 클라이언트는 구현 클래스를 알 필요 없이 Animal 인터페이스만 알면 된다.
        dog.makeSound(); // Woof! 
        cat.makeSound(); // Meow!
    }
}

 

package-private 클래스

버전 별 변천 과정

  1. Java 8 이전
    인터페이스는 정적 메서드를 가질 수 없다.
    동반 클래스(companion class)를 만들어 정적 메서드를 구현했다.
// 인터페이스
public interface OldCollection<E> {
 void add(E element);
}

// 동반 클래스
public class OldCollections {
    public static OldCollection unmodifiableCollection(OldCollection c) {
    }
}

 

2. Java 8 이후
인터페이스에 public 정적 메서드를 직접 정의할 수 있게 되었다.

public interface ModernCollection<E> {
    void add(E element);

    public static <E> ModernCollection<E> unmodifiableCollection(ModernCollection<E> c) {
    }
}

 

  1. Java 9 이후

인터페이스에 private 정적 메서드도 정의할 수 있게 되었다.

public interface Java9Collection<E> {
    void add(E element);

    public static <E> Java9Collection<E> unmodifiableCollection(Java9Collection<E> c) {
        return new UnmodifiableCollection<>(c);
    }

    private static <E> Java9Collection<E> createUnmodifiable(Java9Collection<E> c) {
    }
}
  1. 현재
  • 인터페이스 안에 public 정적 메서드private 정적 메서드를 가질 수 있다.
  • 인터페이스에서는 정적 필드와 정적 멤버 클래스public이어야 한다.
  • 복잡한 구현 로직이나 private 정적 필드가 필요한 경우, package-private 클래스를 사용해야 한다.
public interface ModernInterface {

    // public 정적 필드만 가능
    public static final int CONSTANT = 42;

    // public 정적 메서드
    public static void publicStaticMethod() {
        privateStaticMethod();
        int count = HelperClass.getCounter(); // private 정적 필드
        // ..
    }

    // private 정적 메서드 (Java 9+)
    private static void privateStaticMethod() {
    }

    // 정적 멤버 클래스는 public만 가능
    public static class NestedClass {
    }
}

// package-private 클래스
class HelperClass {

    private static int counter = 0; // private 정적 필드를 가질 경우 default 클래스 생성해야한다.

    static void complexImplementation() {
	    counter++;
    }
    
	static int getCounter() { return counter; }
}

 

4. 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있다.

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.

interface PaymentMethod {
    void processPayment(double amount);
}

// 신용카드 결제
class CreditCardPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

// 암호화폐 결제
class CryptoPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing cryptocurrency payment of $" + amount);
    }
}

// 결제 팩토리 클래스
class PaymentFactory {
    // 정적 팩토리 메서드
    public static PaymentMethod getPaymentMethod(String paymentType) {
        switch (paymentType.toLowerCase()) {
            case "credit":
                return new CreditCardPayment();
            case "crypto":
                return new CryptoPayment();
            default:
                throw new IllegalArgumentException("Unknown payment type: " + paymentType);
        }
    }
}

// 클라이언트
public class PaymentExample {
    public static void main(String[] args) {
        double amount = 100.00;

        PaymentMethod creditPayment = PaymentFactory.getPaymentMethod("credit");
        creditPayment.processPayment(amount);

        PaymentMethod cryptoPayment = PaymentFactory.getPaymentMethod("crypto");
        cryptoPayment.processPayment(amount);
    }
}

 

5. 정적 팩터리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 된다.

서비스 제공자 프레임워크

ex) JDBC

  • 제공자 : 서비스의 구현체
  • 프레임워크 : 구현체를 클라이언트에 제공하는 역할을 통제하여 클라이언트를 구현체로부터 분리한다.
    • 클라이언트는 구현체를 직접 다루지 않고 프레임워크를 통해 얻는다.
  • 3가지 핵심 컴포넌트
    • 서비스 인터페이스 : 구현체의 동작 정의
    • 제공자 등록 API : 제공자가 구현체를 등록할 때 사용
    • 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용, 유연한 정적 팩터리
    • 서비스 제공자 인터페이스 : 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체
      • 서비스 제공자 인터페이스가 없다면 각 구현체를 인스턴스로 만들때 reflection을 사용해야한다.

서비스 인터페이스 (Connection):

public interface Connection {
    Statement createStatement() throws SQLException;
    PreparedStatement prepareStatement(String sql) throws SQLException;
}

서비스 제공자 인터페이스 (Driver)

public interface Driver {
    Connection connect(String url, Properties info) throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
}

제공자 등록 API (DriverManager.registerDriver)

public class DriverManager {
    public static void registerDriver(Driver driver) throws SQLException {
    }
}

 

서비스 접근 API (DriverManager.getConnection)

public class DriverManager {
    public static Connection getConnection(String url) throws SQLException {
    }
}

 

정적 팩토리 메서드를 사용하여 클라이언트는 구현체를 직접 다루지 않고 JDBC 프레임워크가 제공하는 인터페이스만을 사용한다.

 

Driver 구현 예시

public class MySQLDriver implements Driver {
    static {
        try {
            DriverManager.registerDriver(new MySQLDriver());
        } catch (SQLException e) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        if (!acceptsURL(url)) return null;
        return new MySQLConnection(url, info);
    }

    @Override
    public boolean acceptsURL(String url) throws SQLException {
        return url.startsWith("jdbc:mysql:");
    }
}

class MySQLConnection implements Connection {
}

 

클라이언트

public class JDBCClient {
    public static void main(String[] args) {
        try {
            // 드라이버 로드
            Class.forName("com.mysql.jdbc.Driver");

            String url = "jdbc:mysql://localhost:3306/mydb";
            Connection conn = DriverManager.getConnection(url, "username", "password");

            // 연결 사용
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM users");

            while (rs.next()) {
                System.out.println(rs.getString("name"));
            }

            // 리소스 정리
            rs.close();
            stmt.close();
            conn.close();

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
    }
}

 

서비스 제공자 프레임워크 변형

공급자가 제공하는 것보다 더 풍부한 서비스 인터페이스를 클라이언트에 반환할 수 있다.

  1. 브리지 패턴 : 추상화와 구현을 분리하여 둘을 독립적으로 변형할 수 있는 디자인 패턴
// 기본 서비스 인터페이스 (공급자가 제공)
interface BasicDrawAPI {
    void drawShape();
}

// 확장된 서비스 인터페이스 (클라이언트에 제공)
interface AdvancedDrawAPI extends BasicDrawAPI {
    void setColor(String color);
    void resize(int percent);
}

// 구체적인 구현 클래스
class CircleAPI implements BasicDrawAPI {
    public void drawShape() {
        System.out.println("Drawing a circle");
    }
}

// 브리지 클래스 (확장된 기능 구현)
class AdvancedDrawAPIImpl implements AdvancedDrawAPI {
    private BasicDrawAPI basicAPI;
    private String color = "black";
    private int size = 100;

    public AdvancedDrawAPIImpl(BasicDrawAPI basicAPI) {
        this.basicAPI = basicAPI;
    }

    public void drawShape() {
        System.out.println("Drawing in " + color + " at " + size + "% size");
        basicAPI.drawShape();
    }

    public void setColor(String color) {
        this.color = color;
    }

    public void resize(int percent) {
        this.size = percent;
    }
}

// 서비스 제공자
class DrawingServiceProvider {
    public static AdvancedDrawAPI getDrawingAPI() {
        return new AdvancedDrawAPIImpl(new CircleAPI());
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        AdvancedDrawAPI api = DrawingServiceProvider.getDrawingAPI();
        api.setColor("red");
        api.resize(150);
        api.drawShape();
    }
}

 

2. 의존관계 주입 프레임워크 : 클래스의 의존성을 외부에서 주입받아 사용한다.

// 기본 서비스 인터페이스
interface DatabaseService {
    void executeQuery(String query);
}

// 로깅 서비스 인터페이스
interface LoggingService {
    void log(String message);
}

// 확장된 서비스 인터페이스
interface AdvancedDatabaseService extends DatabaseService {
    void executeQueryWithLogging(String query);
}

// 구현 클래스
class MySqlService implements DatabaseService {
    public void executeQuery(String query) {
        System.out.println("Executing query on MySQL: " + query);
    }
}

class ConsoleLoggingService implements LoggingService {
    public void log(String message) {
        System.out.println("LOG: " + message);
    }
}

// 의존 객체 주입을 사용한 확장 서비스 구현
class AdvancedDatabaseServiceImpl implements AdvancedDatabaseService {
    private DatabaseService databaseService;
    private LoggingService loggingService;

    public AdvancedDatabaseServiceImpl(DatabaseService databaseService, LoggingService loggingService) {
        this.databaseService = databaseService;
        this.loggingService = loggingService;
    }

    public void executeQuery(String query) {
        databaseService.executeQuery(query);
    }

    public void executeQueryWithLogging(String query) {
        loggingService.log("Executing query: " + query);
        databaseService.executeQuery(query);
        loggingService.log("Query execution completed");
    }
}

// 서비스 접근 API
class ServiceProvider {
    public static AdvancedDatabaseService getAdvancedDatabaseService() {
        DatabaseService databaseService = new MySqlService();
        LoggingService loggingService = new ConsoleLoggingService();
        return new AdvancedDatabaseServiceImpl(databaseService, loggingService);
    }
}

// 클라이언트
public class Client {
    public static void main(String[] args) {
        AdvancedDatabaseService service = ServiceProvider.getAdvancedDatabaseService();
        service.executeQueryWithLogging("SELECT * FROM users");
    }
}

 

정적 팩토리 메서드 단점

1. 클래스에서 정적 팩터리 메서드만 제공하면 하위 클래스(상속)를 만들 수 없다.

  • 상속을 하려면 public이나 protected 생성자가 필요하기 때문이다.
  • 상속보다 컴포지션을 사용하자.
public class Engine {

    private int horsepower;

    private Engine(int horsepower) {
        this.horsepower = horsepower;
    }

    public static Engine create(int horsepower) {
        return new Engine(horsepower);
    }

    public void start() {
        System.out.println("Engine started. Horsepower: " + horsepower);
    }
}

public class Car {

    private Engine engine;  // 컴포지션

    public Car(int horsepower) {
        this.engine = Engine.create(horsepower);  
    }

    public void drive() {
        engine.start();
        System.out.println("Car is driving");
    }
}

// 클라이언트 
public class Main {
    public static void main(String[] args) {
        Car car = new Car(200);
        car.drive();
    }
}
  • 클래스가 불변 타입이라면 오히려 장점일 수 있다.
    • 하위 클래스에서 상태를 변경할 수 있는 메서드를 추가하면 불변성이 깨질 수 있기 때문이다.


2. 정적 팩터리 메서드는 찾기 어렵다.

  • 생성자처럼 API 설명에 명확하게 드러나지 않기 때문이다.
  • API 문서에 명시하고 메서드 이름도 널리 알려진 규약에 따라 지으면 문제를 완화할 수 있다.

 

정적 팩터리 메서드 네이밍 방식

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스 반환
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스 반환
  • valueOf : from과 of의 더 자세한 버전
  • instance 혹은 getInstance : 매개변수로 명시한 인스턴스를 반환 (같은 인스턴스임을 보장하지는 X)
  • create 혹은 newInstance : 매번 새로운 인스턴스를 생성해 반환
  • getType : `getInstance`와 같지만 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용
  • type : 팩터리 메서드가 반환할 객체의 타입
  • newType: newInstance와 같지만 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용
  • type : 팩터리 메서드가 반환할 객체의 타입
    • type: getType과 newType의 간결한 버전
정적 팩터리 메서드와 public 생성자는 상대적인 장단점을 이해하고 사용하는 것이 좋다.
정적 팩터리 메서드를 사용하는게 유리한 경우가 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

 

728x90