명료성(clarity)과 단순성(simplicity)
인스턴스는 전통적으로 public 생성자를 통해 생성된다.
그러나 정적 팩터리 메서드를 통해서도 인스턴스를 만들 수 있고 특정 상황에서는 오히려 더 나은 선택지이기도 하다.
정적 팩터리 메서드는 어떤 점에서 좋고 나쁠까?
이름을 가질 수 있다
생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다. 예컨대 생성자인 BigInteger(int, int, Random)과 정적 팩터리 메서드인 BigInteger.
probablePrime 중 어느 쪽이 '값이 소수인 BigInteger를 반환한다'는 의미를 더
잘 설명할 것 같은지 생각해 보면 명확하다. (BigInteger.probablePrime은 자바 4에서 추
가됐다.)
-또한 시그니처가 같은 생성자가 여러개 필요할때, 용도에 맞는 메서드를 만들어주기만 된다는 장점이 있다. (물론, 시그니처
📢 시그니처란?
해당 생성자를 구분할 수 있는 요소들, 생성자의 이름+매개변수리스트(매개변수의 개수, 타입, 순서)의 조합. 따라서, 동일한 이름의 생성자라도 매개변수 리스트가 다르면 서로 다른 생성자로 간주합니다.
호출될 때마다 인스턴스를 새로 생성하지 않아도 된다
📢 플라이웨이트 패턴?
플라이웨이트(Flyweight) 패턴은 GoF(Design Patterns)에서 구조 패턴 중 하나로, 메모리 사용을 줄이고 객체 생성을 최적화하기 위한 패턴입니다. 이 패턴은 공유 객체를 사용하여 동일한 데이터가 여러 번 생성되지 않도록 설계됩니다. 플라이웨이트 패턴의 핵심은 공유 가능한 내부 상태와 외부 상태를 구분하는 것이며, 내부 상태는 공유해서 다시 사용하고 외부 상태는 재사용하지 않고 매번 새로 생성합니다.
반환 타입의 하위 타입 객체를 반환할 수 있게 된다.
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있게 된다.
반환 타입의 하위 타입이면 어떤 클래스의 객체든 반환할 수 있다. 클라이언트는 이를 모르므로 필요에 따라 하위 타입들을 변경할 수 있는 이점도 생기게 된다.
가령 EnumSet 클래스(아이템 36)는 public 생성자 없이 오직 정적 팩터리만
제공하는데, OpenJDK에서는 원소의 수에 따라 두 가지 하위 클래스 중 하나의
인스턴스를 반환한다.
(대다수에 해당하는) 원소가 64개 이하면 원소들을 long
변수 하나로 관리하는 RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열
로 관리하는 JumboEnumSet의 인스턴스를 반환한다.
클라이언트는 이 두 클래스의 존재를 모른다. 만약 원소가 적을 때
RegularEnumSet을 사용할 이점이 없어진다면 다음 릴리스 때는 이를 삭제해도
아무 문제가 없다. 비슷하게, 성능을 더 개선한 세 번째, 네 번째 클래스를 다음
릴리스에 추가할 수도 있다. 클라이언트는 팩터리가 건네주는 객체가 어느 클
래스의 인스턴스인지 알 수도 없고 알 필요도 없다. EnumSet의 하위 클래스이
기만 하면 되는 것이기 때문이다.
정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않아도 기능한다.
- 이러한 특성은 service provider framework를 만드는 근간이 됩니다.
반환할 객체의 클래스가 존재하지 않아도 된다?
// 서비스 인터페이스
public interface Shape {
void draw();
}
// 정적 팩터리 메서드가 포함된 클래스
public class ShapeFactory {
public static Shape getShape(String shapeType) {
// 구현체는 아직 정의되지 않았습니다!
return null;
}
}
이게 무슨 말일까요? 초기에는 위와같이 인터페이스나 추상 클래스만 정의하고, 구체 클래스(구현체)는 나중에 작성해도 된다는 말입니다. 왜냐하면 정적 팩터리 메서드는 런타임에 필요한 구현체를 동적으로 찾아서 반환하기 때문입니다.
// 구체 클래스 1
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
// 구체 클래스 2
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Rectangle");
}
}
// ShapeFactory를 수정하여 구현체를 반환
public class ShapeFactory {
public static Shape getShape(String shapeType) {
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle(); // 나중에 정의된 Circle 클래스
} else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle(); // 나중에 정의된 Rectangle 클래스
}
throw new IllegalArgumentException("Unknown shape type");
}
}
위처럼 나중에 필요시에 Shape 인터페이스의 구현체를 작성하고, 이를 정적 팩터리 메서드에 등록하면 됩니다.
public class Main {
public static void main(String[] args) {
Shape circle = ShapeFactory.getShape("CIRCLE"); //런타임에 구현체 등록
circle.draw(); // "Drawing a Circle"
Shape rectangle = ShapeFactory.getShape("RECTANGLE");
rectangle.draw(); // "Drawing a Rectangle"
}
}
즉, 컴파일 타임에는 상위 타입(인터페이스 또는 추상 클래스)만 정의하면 된다. 왜냐하면 런타임에 동적으로 구현체(구체 클래스)를 등록하고, 정적 팩터리 메서드를 통해 이를 찾아 반환할 수 있기 때문이다.
클라이언트에서의 사용은 위와 같을 것입니다. 보시다시피 정적 팩터리 메서드를 호출하여 사용하할때 3번에서 본 반환 타입의 하위 타입 객체를 반환하는 것도 볼 수 있습니다.
📢서비스 제공자 프레임워크란?
서비스 제공자 프레임워크란?
Provider를 클라이언트 코드와 분리하여 동적으로 제공할 수 있도록 돕는 구조입니다. 클라이언트는 특정 구현체에 직접 의존하지 않고, 프레임워크를 통해 구현체를 동적으로 선택하고 사용합니다.
🖼️ 구성 요소
서비스 인터페이스 (Service Interface)
구현체(Provider)가 따라야 할 동작을 정의하는 인터페이스입니다.
예: JDBC에서 java.sql.Driver 인터페이스가 이에 해당합니다.
제공자 등록 API (Provider Registration API)
구현체를 프레임워크에 등록할 때 사용하는 API입니다.
예: JDBC의 DriverManager.registerDriver() 메서드.
서비스 접근 API (Service Access API)
클라이언트가 서비스의 구현체를 얻기 위해 사용하는 API입니다.
클라이언트는 조건을 명시하여 특정 구현체를 요청하거나, 기본 구현체를 요청할 수 있습니다.
예: JDBC의 DriverManager.getConnection() 메서드.
서비스 제공자 인터페이스(service provider interface)
서비스 구현체를 프레임워크에 등록하기 위한 역할을 수행, 서비스 구현체를 직접 생성하지 않고 제공자가 생성한 객체를 받아서 사용할 수 있게 합니다. 이를 통해 서비스 구현체 생성의 책임을 서비스 제공자에게 위임합니다.
🎪 동작방식
구현체(Provider) 등록
서비스 제공자가 제공자 등록 API를 사용하여 자신의 구현체를 프레임워크에 등록합니다.
클라이언트 요청
클라이언트는 서비스 접근 API를 호출하여 서비스 구현체를 요청합니다.
클라이언트는 구현체를 구체적으로 알 필요 없이, 조건을 명시하거나 기본 구현체를 요청할 수 있습니다.
프레임워크 동작
프레임워크는 등록된 구현체 중에서 클라이언트 조건에 맞는 것을 반환하거나, 기본값을 반환합니다.
🎉용어 정리
Service Interface
서비스를 추상적으로 정의한 인터페이스
예: java.sql.Driver는 JDBC의 서비스 인터페이스로, 데이터베이스 드라이버가 따라야 할 규약을 정의합니다.
Service Implementation
서비스 인터페이스를 구현한 클래스이며, 실제 동작을 수행합니다.
이 클래스의 인스턴스를 service provider 라고 부릅니다.
예: MySQLDriver, PostgreSQLDriver 등이 java.sql.Driver를 구현한 서비스 구현체입니다.
정적 메서드 명명법
• from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메
서드Date d = Date.from(instant);
- of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueof: from과 of의 더 자세한 버전
BigInteger prime = BigInteger.value0f(Integer.MAX_VALUE);
- instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인
스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.StackWalker luke = StackWalker.getInstance(options);
- create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로
운 인스턴스를 생성해 반환함을 보장한다.Object newArray = Array.newInstance(class0bject, arrayLen);
- getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터
리 메서드를 정의할 때 쓴다. "Type"은 펙터리 메서드가 반환할 객체의 타입
이다.FileStore fs = Files.getFileStore(path)
- newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터
리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입
이다.BufferedReader br = Files.newBufferedReader(path);
- type: getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);
이름을 가질 수 있다
객체 생성을 제어할 수 있다
서브클래스 반환 가능
복잡한 초기화 논리 처리
인스턴스 제한
직관적인 사용
상속 가능성
객체 생성이 항상 새로워야 할 때
기본 규칙
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이
해하고 사용하는 것이 좋습니다. 일반적으로 정적 팩터리 메서드를 우선 고려하되, 상속이 필요한 경우나 특정 프레임워크 요구사항이 있을 경우 public 생성자를 제공하는 방식으로 균형 있게 활용하는걸 추천합니다.
이펙티브 자바 https://www.yes24.com/Product/Goods/65551284
서비스 제공자 프레임워크 이해하기 https://sihyung92.oopy.io/java/service-provider-framework