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) 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
*불변 클래스는 인스턴스를 미리 생성해 두고 필요할 때 반환하는 식으로 설계된다.
불변 클래스란?
- 인스턴스의 내부 값을 수정할 수 없는 클래스를 의미한다.
클래스를 불변으로 만들기 위한 규칙:
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다. (final 키워드 사용)
- 클래스의 모든 필드를 final로 선언한다.
- 클래스의 모든 필드를 private로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
상속될 가능성이 거의 없는 유틸리티 클래스나 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) 인스턴스 통제 클래스
객체의 인스턴스를 생성하거나 관리하는 역할을 담당하는 클래스.
주요 목적:
- 인스턴스 생성 제한:
특정 클래스의 인스턴스를 하나만 생성하거나, 정해진 방식으로만 인스턴스를 생성할 수 있도록 제한한다. - 객체 생성 방식 제어:
인스턴스의 생성 방법을 통제하여 필요한 경우 객체 생성 시점이나 객체의 수를 제어한다.
- 싱글톤 패턴 (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; // 항상 같은 인스턴스를 반환
}
}
싱글톤 패턴은 애플리케이션 내에서 단 하나의 인스턴스만 존재해야 하는 경우에 사용된다. 이 패턴은 주로 전역적인 접근이 필요하고, 객체의 중복 생성을 방지해야 할 때 유용하다.
싱글톤 패턴을 사용하는 주요 상황:
- 애플리케이션 설정 관리
- 설정 클래스는 애플리케이션 전역에서 동일한 설정 값을 참조할 수 있어야 하므로 싱글톤 패턴을 사용한다.
- ex) 데이터베이스 연결 정보, API 키, 파일 경로 등과 같은 설정을 애플리케이션 전역에서 공유하는 경우.
- 전역 상태 관리
- 상태가 하나로 고정되어야 하는 객체를 전역에서 관리할 때 유용하다.
- ex) 애플리케이션에서 전역적으로 하나의 로그 객체만 필요하거나, 동일한 설정을 여러 곳에서 참조할 때 사용.
- 자원 관리
- 데이터베이스 커넥션 풀이나 파일 핸들러처럼(=자원 관리 객체) 자원을 하나의 인스턴스에서 관리해야 할 때도 싱글톤 패턴이 적합하다. 여러 인스턴스를 생성하는 것보다 하나의 인스턴스를 관리하는 것이 효율적이다.
- 인스턴스 풀 (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
orJumboEnumSet
)를 반환한다.
// 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)
,JDBC
의Connection
): 구현체의 동작 정의. 2) 제공자 등록 API(JDBC
의DriverManager.registerDrive
): 제공자가 구현체를 등록할 때 사용. 3) 서비스 접근 API(JDBC
의DriverManager.getConnection
): 클라이언트가 서비스의 인스턴스를 얻을 때 사용. 4) 서비스 제공자 인터페이스(SPI(Service Provider Interface)
,JDBC
의Driver
): 선택적. 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명해준다. - 의존성 주입 프레임워크도 서비스 제공자이다.
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
사용 시의 동작 흐름:
ServiceLoader.load(Logger.class)
:Logger
인터페이스를 구현한 모든 클래스를 찾습니다.META-INF/services/com.example.Logger
파일에 등록된 구현체들을 동적으로 로드하여Logger
인터페이스의 인스턴스를 생성합니다.- 구현체 중 첫 번째 클래스를 반환합니다. (실제 사용에서는 로깅 구현체를 환경 설정에 따라 선택할 수 있습니다.)
예시:
-
서비스 인터페이스
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); } }
-
각 로깅 구현체들을
META-INF/services
폴더에 등록. -
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"); } }
-
런타임에 적합한 로깅 구현체를 동적으로 로드.
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);