1. 정적 팩토리 메서드

개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성하는 디자인 패턴.

〔 정적 팩토리 메서드를 사용해야 하는 이유 〕

1) 이름을 가질 수 있다.

반환될 객체의 특정을 쉽게 묘사 가능.

class Book {
    private String title;

    private Book(String title) { this.title = title; }

    public static Book titleOf(String title) {
        return new Book(title);
    }
}

public static void main(String[] args) {
    // 매개인자로 입력한 값이 어떤 값인지 알기 어려움.
    Book book1 = new Book("어린왕자");

    // title 값을 받아서 객체 생성한다는 것을 알 수 있음.
    Book book2 = Book.titleOf("어린왕자"); 
}


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

*불변 클래스는 인스턴스를 미리 생성해 두고 필요할 때 반환하는 식으로 설계된다.

불변 클래스란?

  • 인스턴스의 내부 값을 수정할 수 없는 클래스를 의미한다.

클래스를 불변으로 만들기 위한 규칙:

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다. (final 키워드 사용)
  3. 클래스의 모든 필드를 final로 선언한다.
  4. 클래스의 모든 필드를 private로 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.


상속될 가능성이 거의 없는 유틸리티 클래스Value Object 객체, 상수 객체, DTO를 불변 클래스로 선언하는 경우가 많다.

예시1 Boolean 클래스:

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

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

예시2 사용자 정의 Color 클래스:

public final class Color {
    private final int red;
    private final int green;
    private final int blue;

    // 미리 생성된 인스턴스들
    public static final Color RED = new Color(255, 0, 0);
    public static final Color GREEN = new Color(0, 255, 0);
    public static final Color BLUE = new Color(0, 0, 255);
    public static final Color BLACK = new Color(0, 0, 0);
    public static final Color WHITE = new Color(255, 255, 255);

    // private 생성자: 외부에서 객체 생성 불가
    private Color(int red, int green, int blue) {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    public int getRed() {
        return red;
    }

    public int getGreen() {
        return green;
    }

    public int getBlue() {
        return blue;
    }

    @Override
    public String toString() {
        return "Color{" + "red=" + red + ", green=" + green + ", blue=" + blue + '}';
    }
}


3) 새로 생성한 인스턴스를 캐싱하여 재활용.

객체 생성 비용이 높은 경우나 동일한 값을 가지는 인스턴스를 자주 생성해야 하는 경우에 효과적.

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;

        //...

        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}
@IntrinsicCandidate  
public static Integer valueOf(int i) {  
    if (i >= IntegerCache.low && i <= IntegerCache.high)  
        return IntegerCache.cache[i + (-IntegerCache.low)];  
    return new Integer(i);  
}
  • IntegerCache 클래스는 내부적으로 -128에서 127 사이의 Integer 객체들을 저장하는 배열을 가지고 있다.
    이 범위 내의 Integer 객체는 생성 시점에 한 번만 만들어지고, 그 후 동일한 객체가 재사용된다.
    IntegerCache 클래스는 Integer.valueOf() 메서드가 호출될 때마다 이 배열에서 값을 찾아서 해당 객체를 반환한다.
  • Integer.valueOf()가 내부적으로 사용하는 캐시의 생명주기는 JVM의 실행 동안.
  • 이 캐시는 JVM의 IntegerCache라는 클래스에 의해 관리되며, 이는 -128에서 127까지의 Integer 객체를 정적으로 저장한다.
  • -128 ~ 127 범위를 캐싱하는 이유:
    이 범위는 byte 타입의 범위와 관련이 있는데, byte-128에서 127까지의 값을 가질 수 있기 때문에, 컴퓨터 시스템에서 자주 다뤄지는 숫자 범위로 간주되었다.

사용자 정의 클래스 Person:

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

public final class Person {
    private final String name;

    // 캐싱된 인스턴스를 저장할 Map
    private static final Map<String, Person> cache = new HashMap<>();

    // private 생성자: 외부에서 객체 생성 불가
    private Person(String name) {
        this.name = name;
    }

    // 정적 팩토리 메서드: 캐싱된 인스턴스를 반환하거나 새로 생성
    public static Person valueOf(String name) {
        if (!cache.containsKey(name)) {
            cache.put(name, new Person(name));
        }
        return cache.get(name);
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "'}";
    }
}

4) 인스턴스 통제 클래스

객체의 인스턴스를 생성하거나 관리하는 역할을 담당하는 클래스.

주요 목적:

  • 인스턴스 생성 제한:
    특정 클래스의 인스턴스를 하나만 생성하거나, 정해진 방식으로만 인스턴스를 생성할 수 있도록 제한한다.
  • 객체 생성 방식 제어:
    인스턴스의 생성 방법을 통제하여 필요한 경우 객체 생성 시점이나 객체의 수를 제어한다.
  1. 싱글톤 패턴 (Singleton Pattern):
    인스턴스를 하나만 생성하고, 이 인스턴스를 전역적으로 공유하는 패턴. 객체의 중복 생성 방지와 전역적 접근을 제공하는 클래스에서 인스턴스 통제를 구현한다.
public class Singleton {
    // 1. 자신을 하나만 가지고 있게끔 static 필드로 선언
    private static Singleton instance;

    // 2. private 생성자로 외부에서 인스턴스를 생성하지 못하게 함
    private Singleton() {
        // 초기화 작업
    }

    // 3. static 메서드를 통해서만 인스턴스를 가져오도록 통제
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();  // 인스턴스가 없을 경우에만 생성
        }
        return instance;  // 항상 같은 인스턴스를 반환
    }
}

싱글톤 패턴은 애플리케이션 내에서 단 하나의 인스턴스만 존재해야 하는 경우에 사용된다. 이 패턴은 주로 전역적인 접근이 필요하고, 객체의 중복 생성을 방지해야 할 때 유용하다.

싱글톤 패턴을 사용하는 주요 상황:

  1. 애플리케이션 설정 관리
    • 설정 클래스는 애플리케이션 전역에서 동일한 설정 값을 참조할 수 있어야 하므로 싱글톤 패턴을 사용한다.
    • ex) 데이터베이스 연결 정보, API 키, 파일 경로 등과 같은 설정을 애플리케이션 전역에서 공유하는 경우.
  2. 전역 상태 관리
    • 상태가 하나로 고정되어야 하는 객체를 전역에서 관리할 때 유용하다.
    • ex) 애플리케이션에서 전역적으로 하나의 로그 객체만 필요하거나, 동일한 설정을 여러 곳에서 참조할 때 사용.
  3. 자원 관리
    • 데이터베이스 커넥션 풀이나 파일 핸들러처럼(=자원 관리 객체) 자원을 하나의 인스턴스에서 관리해야 할 때도 싱글톤 패턴이 적합하다. 여러 인스턴스를 생성하는 것보다 하나의 인스턴스를 관리하는 것이 효율적이다.
  4. 인스턴스 풀 (Object Pool):
    사용할 수 있는 객체들을 미리 생성해놓고, 필요할 때마다 재사용하는 방식으로 인스턴스를 관리하는 패턴.
    ex) 데이터베이스 연결이나 스레드 풀
public class DatabaseConnectionPool {
    private static final int POOL_SIZE = 10;
    private static final DatabaseConnectionPool instance = new DatabaseConnectionPool();
    
    private List<Connection> connectionPool;
    
    private DatabaseConnectionPool() {
        connectionPool = new ArrayList<>(POOL_SIZE);
        // 미리 10개의 데이터베이스 연결을 생성하여 풀에 넣음
        for (int i = 0; i < POOL_SIZE; i++) {
            connectionPool.add(new Connection());
        }
    }

    public static DatabaseConnectionPool getInstance() {
        return instance; // 항상 동일한 객체를 반환
    }

    public Connection getConnection() {
        if (connectionPool.isEmpty()) {
            // 새로운 연결을 만들어서 반환 (예시)
            return new Connection();
        }
        return connectionPool.remove(0); // 풀에서 하나를 꺼내서 반환
    }

    public void releaseConnection(Connection connection) {
        connectionPool.add(connection); // 풀에 연결을 반환
    }
}


5) 반환 타입의 하위 타입 객체를 반환 가능하다.

  • 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있다.
    → 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술.

  • 자바 8 전에는 인터페이스에 정적 메서드를 선언할 수 없었다.
    → 이름이 Type 인 인터페이스를 반환하는 정적 메서드가 필요하면, Types 라는 (인스턴스화 불가인)동반 클래스에 만들어 그 안에 정의하는 것이 관례였다.
    → ex) 자바 컬렉션 프레임워크와 java.util.Collections :
    자바 컬렉션 프레임워크는 핵심 인터페이스들(List, Set, Queue)에 수정 불가나 동기화 등의 기능을 덧붙인 유틸리티 구현체를 제공하고 있고, 이 구현체를 java.util.Collections 에서 정적 팩토리 메서드를 통해 얻도록 했다.

// 수정 불가한 리스트 생성
List<String> unmodifiableList = Collections.unmodifiableList(list);
// Collections의 정적 팩토리 메서드
public static <T> List<T> unmodifiableList(List<? extends T> list) {  
    if (list.getClass() == UnmodifiableList.class || list.getClass() == UnmodifiableRandomAccessList.class) {  
       return (List<T>) list;  
    }  
    return (list instanceof RandomAccess ?  
            new UnmodifiableRandomAccessList<>(list) :  
            new UnmodifiableList<>(list));  
}

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

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

ex) EnumSet 클래스 :

  • public 생성자가 없이 정적 팩토리 메서드만 제공.
  • noneOf 정적 팩토리 메서드에서 조건에 따라 다른 하위 클래스의 인스턴스(RegularEnumSet or JumboEnumSet)를 반환한다.
// public 생성자가 아니다.
EnumSet(Class<E>elementType, Enum<?>[] universe) {  
    this.elementType = elementType;  
    this.universe    = universe;  
}

// 정적 팩토리 메서드
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {  
    Enum<?>[] universe = getUniverse(elementType);  
    if (universe == null)  
        throw new ClassCastException(elementType + " not an enum");  
  
    if (universe.length <= 64)  
        return new RegularEnumSet<>(elementType, universe);  
    else  
        return new JumboEnumSet<>(elementType, universe);  
}

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

이는 서비스 제공자 프레임워크(대표적으로 JDBC)를 만드는 근간이 된다. 서비스 제공자 프레임워크에서 제공자 = 서비스의 구현체. 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

서비스 제공자 프레임워크:

  • 3개의 핵심 컴포넌트 + 선택적 사용 컴포넌트로 구성된다. 1) 서비스 인터페이스(SPI(Service Provider Interface), JDBCConnection): 구현체의 동작 정의. 2) 제공자 등록 API(JDBCDriverManager.registerDrive): 제공자가 구현체를 등록할 때 사용. 3) 서비스 접근 API(JDBCDriverManager.getConnection): 클라이언트가 서비스의 인스턴스를 얻을 때 사용. 4) 서비스 제공자 인터페이스(SPI(Service Provider Interface), JDBCDriver): 선택적. 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명해준다.
  • 의존성 주입 프레임워크도 서비스 제공자이다.

DriverManager.getConnection()는 팩토리 메서드로써 다양한 드라이버 중 적합한 Connection 구현체를 런타임 시점에 동적으로 선택해 반환하는 역할을 한다.
→ 즉, 서비스 제공자 프레임워크가 DriverManager.getConnection() 메서드를 작성하는 시점에 반환할 Connection 구현체가 존재하지 않아도 문제가 되지 않는다. = 문제 발생 시점은 런타임으로 연기된다.


@CallerSensitive  
public static Connection getConnection(String url,  
    String user, String password) throws SQLException {  
    java.util.Properties info = new java.util.Properties();  
  
    if (user != null) {  
        info.put("user", user);  
    }    if (password != null) {  
        info.put("password", password);  
    }  
    return (getConnection(url, info, Reflection.getCallerClass()));  
}

// DriverManager.java
private static Connection getConnection(

// ...

	for (DriverInfo aDriver : registeredDrivers) {  
	    // If the caller does not have permission to load the driver then  
	    // skip it.    if (isDriverAllowed(aDriver.driver, callerCL)) {  
	        try {  
	            println("    trying " + aDriver.driver.getClass().getName());  
	            // 제공자가 구현한 Driver 인터페이스로부터 Connection 구현체 반환받는다.
	            Connection con = aDriver.driver.connect(url, info);  
	            if (con != null) {  
	                // Success!  
	                println("getConnection returning " + aDriver.driver.getClass().getName());  
	                return (con);  
	            }        } catch (SQLException ex) {  
	            if (reason == null) {  
	                reason = ex;  
	            }        }  
	    } else {  
	        println("    skipping: " + aDriver.driver.getClass().getName());  
	    }  
	}
	
)
  • 자바 6 부터는 java.util.ServiceLoader 라는 범용 서비스 제공자 프레임워크가 제공되어 프레임워크를 직접 만들 필요가 거의 없어졌다.

ServiceLoader:
서비스 제공자 프레임워크(SPI)를 구현하기 위한 도구. 주로 **서비스 인터페이스와 그에 해당하는 서비스 제공자(구현체)를 런타임에 동적으로 로드하는 데 사용된다.

ServiceLoader 사용 시의 동작 흐름:

  1. ServiceLoader.load(Logger.class): Logger 인터페이스를 구현한 모든 클래스를 찾습니다.
  2. META-INF/services/com.example.Logger 파일에 등록된 구현체들을 동적으로 로드하여 Logger 인터페이스의 인스턴스를 생성합니다.
  3. 구현체 중 첫 번째 클래스를 반환합니다. (실제 사용에서는 로깅 구현체를 환경 설정에 따라 선택할 수 있습니다.)

예시:

  1. 서비스 인터페이스 Logger 와 구현체 정의.

     // 서비스 인터페이스
     public interface Logger {
         void log(String message);
     }
    
     // Logger 인터페이스를 구현체
     public class LogbackLogger implements Logger {
         @Override
         public void log(String message) {
             System.out.println("[Logback] " + message);
         }
     }
    
     // Logger 인터페이스를 구현체
     public class Log4jLogger implements Logger {
         @Override
         public void log(String message) {
             System.out.println("[Log4j] " + message);
         }
     }
    
  2. 각 로깅 구현체들을 META-INF/services 폴더에 등록.

  3. ServiceLoader 사용.

     import java.util.ServiceLoader;
    
     public class LoggerFactory {
         public static Logger getLogger() {
             ServiceLoader<Logger> serviceLoader = ServiceLoader.load(Logger.class);
             for (Logger logger : serviceLoader) {
                 return logger; // 첫 번째 로깅 구현체를 반환
             }
             throw new IllegalStateException("No Logger implementation found");
         }
     }
    
  4. 런타임에 적합한 로깅 구현체를 동적으로 로드.

     public class Main {
         public static void main(String[] args) {
             Logger logger = LoggerFactory.getLogger();
             logger.log("Hello, ServiceLoader!");
         }
     }
    

〔 정적 팩토리 메서드 단점 〕

1) 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다. (상속을 통한 확장이 불가하다.)

상속을 하려면 public 이나 protected 생성자가 필요하기 때문이다.
예시로, 컬렉션 프레임워크의 유틸리티 클래스인 Collections 의 하위 클래스를 생성할 수 없다.

2) 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.

생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩토리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.
→ 정적 팩토리 메서드 명명 규칙을 준수하는 식으로 완화하도록 한다.

정적 팩토리 메서드 명명 규칙:

  • from : 하나의 매개변수를 사용하여 인스턴스를 생성.
  • of : 여러 매개변수를 사용하여 인스턴스를 생성.
  • valueOf : 매개변수를 변환하여 인스턴스를 생성.
  • instance 또는 getInstance : 인스턴스를 반환. 일반적으로 싱글톤 패턴에서 사용.
  • create 또는 newInstance : 인스턴스를 생성. new 키워드를 대체하는 의미로 사용. 매번 새로운 인스턴스를 생성해 반환함을 보증한다.
  • get[Type] : Type 의 인스턴스를 반환. getInstance와 유사하지만 타입을 명시.
    FileStore store = Files.getFileStore(path);
    
  • new[Type] : 새로운 Type의 인스턴스를 생성. newInstance 와 유사하지만 타입을 명시.
    BufferedReader br = Files.newBufferedReader(path);
    
  • [type] : get[Type], new[Type]의 간결한 버전.
    List<Book> books = Collections.list(oldBooks);