클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public
생성자다.
public class Car {
private String brand;
public Car(String brand) {
this.brand = brand;
}
}
// 사용
Car car = new Car("BMW");
하지만 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.
→ 그 클래스의 인스턴스를 반환하는 단순한 정적 메서드
1. 예시
```java
public class Car {
private String brand;
// private 생성자
private Car(String brand) {
this.brand = brand;
}
// 정적 팩터리 메서드
public static Car createCar(String brand) {
return new Car(brand);
}
}
// 사용
Car car = Car.createCar("BMW");
```
클래스는 클라이언트에 public 생성자 대신(혹은 생성자와 함께) 정적 팩터리 메서드를 제공할 수 있다. 이 방식에는 장점과 단점 모두 존재.
```java
//생성자 제약 예시
public class User {
private String name;
private boolean isAdmin;
// 생성자: 일반 사용자와 관리자 구분
public User(String name, boolean isAdmin) {
this.name = name;
this.isAdmin = isAdmin;
}
@Override
public String toString() {
return "User{name='" + name + "', isAdmin=" + isAdmin + "}";
}
public static void main(String[] args) {
// 일반 사용자 생성
User regularUser = new User("Alice", false);
// 관리자 생성
User adminUser = new User("Bob", true);
System.out.println(regularUser); // 출력: User{name='Alice', isAdmin=false}
System.out.println(adminUser); // 출력: User{name='Bob', isAdmin=true}
}
}
```
```java
// 정적 팩터리 메서드에서 생성자의 한계 극복 예시
public class User {
private String name;
private boolean isAdmin;
// private 생성자
private User(String name, boolean isAdmin) {
this.name = name;
this.isAdmin = isAdmin;
}
// 정적 팩터리 메서드: 일반 사용자 생성
public static User createRegularUser(String name) {
return new User(name, false);
}
// 정적 팩터리 메서드: 관리자 생성
public static User createAdminUser(String name) {
return new User(name, true);
}
@Override
public String toString() {
return "User{name='" + name + "', isAdmin=" + isAdmin + "}";
}
public static void main(String[] args) {
// 정적 팩터리 메서드를 사용한 객체 생성
User regularUser = User.createRegularUser("Alice");
User adminUser = User.createAdminUser("Bob");
System.out.println(regularUser); // 출력: User{name='Alice', isAdmin=false}
System.out.println(adminUser); // 출력: User{name='Bob', isAdmin=true}
}
}
```
**차이점 요약**
1. **생성자의 제약**
1. 동일한 시그니처를 가진 추가 생성자를 정의할 수 없음.
2. 매개변수(false, true)의 의미를 코드만으로 명확히 알기 어려움.
2. **정적 팩터리 메서드의 장점**
1. createRegularUser, createAdminUser처럼 이름으로 객체의 특성을 설명 가능.
2. 가독성이 높아지고, 반환되는 객체의 의미를 명확히 전달 가능.
불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용
대표적 예인 Boolean.valueOf(boolean) 메서드 → 객체 생성X
public static Boolean valueOf(boolean b){
return b? Boolean.TRUE : Boolean.FALSE;
}
Integer
String
Double
Long
특히 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능이 상당히 끌어올려준다.
플라이웨이트(Flyweight Pattern) 패턴도 이와 비슷한 기법이라 할 수 있다.
import java.util.HashMap;
import java.util.Map;
// 플라이웨이트 인터페이스: 공유 객체의 공통 동작 정의
interface Flyweight {
void display(int x, int y); // 외부 상태를 받아서 객체를 동작시킴
}
// ConcreteFlyweight: 공유 객체의 실제 구현
class Character implements Flyweight {
private final char symbol; // 내부 상태 (공유 가능한 고정 데이터)
private final String font; // 내부 상태 (글꼴 정보)
public Character(char symbol, String font) {
this.symbol = symbol;
this.font = font;
}
@Override
public void display(int x, int y) {
// 외부 상태(x, y 좌표)에 따라 달라지는 행동
System.out.println("Character: " + symbol + ", Font: " + font + ", Position: (" + x + ", " + y + ")");
}
}
// Flyweight Factory: 공유 객체를 관리하고 생성
class FlyweightFactory {
private final Map<String, Flyweight> flyweights = new HashMap<>();
// Flyweight 객체를 반환 (필요하면 생성)
public Flyweight getCharacter(char symbol, String font) {
String key = symbol + font; // 공유 객체를 고유하게 구분할 키
if (!flyweights.containsKey(key)) {
// 객체가 없으면 새로 생성
flyweights.put(key, new Character(symbol, font));
System.out.println("Created new Flyweight: " + key);
}
return flyweights.get(key); // 기존 객체 반환
}
}
// 클라이언트 코드
public class FlyweightPatternExample {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
// 'A' 문자 객체 생성 및 재사용
Flyweight a1 = factory.getCharacter('A', "Arial");
Flyweight a2 = factory.getCharacter('A', "Arial"); // 이미 생성된 객체 재사용
// 'B' 문자 객체 생성
Flyweight b = factory.getCharacter('B', "Arial");
// 객체 동작 (외부 상태: x, y 좌표)
a1.display(10, 20); // 출력: Character: A, Font: Arial, Position: (10, 20)
a2.display(30, 40); // 출력: Character: A, Font: Arial, Position: (30, 40)
b.display(50, 60); // 출력: Character: B, Font: Arial, Position: (50, 60)
// 동일 객체 재사용 여부 확인
System.out.println(a1 == a2); // true (같은 객체를 공유)
}
}
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다.
import java.util.HashMap;
import java.util.Map;
// 인스턴스 통제 클래스: 정적 팩터리 메서드를 사용하여 객체를 통제
public class InstanceControlledClass {
// 내부적으로 관리되는 Map (불변 값 클래스를 위한 예시)
private static final Map<String, InstanceControlledClass> instances = new HashMap<>();
private final String value; // 불변 값 (내부 상태)
// private 생성자: 외부에서 직접 객체 생성 불가
private InstanceControlledClass(String value) {
this.value = value;
}
// 정적 팩터리 메서드: 동일한 값의 인스턴스는 단 하나만 존재하도록 통제
public static InstanceControlledClass getInstance(String value) {
// 이미 존재하는 객체가 있으면 반환, 없으면 새로 생성
instances.computeIfAbsent(value, InstanceControlledClass::new);
return instances.get(value);
}
// 값 반환 메서드
public String getValue() {
return value;
}
@Override
public String toString() {
return "InstanceControlledClass{" + "value='" + value + '\'' + '}';
}
public static void main(String[] args) {
// 동일한 값으로 객체 생성 요청
InstanceControlledClass obj1 = InstanceControlledClass.getInstance("A");
InstanceControlledClass obj2 = InstanceControlledClass.getInstance("A");
InstanceControlledClass obj3 = InstanceControlledClass.getInstance("B");
// 출력
System.out.println(obj1); // InstanceControlledClass{value='A'}
System.out.println(obj2); // InstanceControlledClass{value='A'}
System.out.println(obj3); // InstanceControlledClass{value='B'}
// 동일한 값 객체는 동일성을 보장
System.out.println(obj1 == obj2); // true (동일 객체)
System.out.println(obj1 == obj3); // false (다른 객체)
}
}
API를 만들때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지 할 수 있다
→ 인터페이스를 정적 팩터리 메서드의 반환 타임으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술
```java
// 인터페이스: 공개 API의 일부
interface Shape {
void draw();
}
// 구현 클래스 1: 구체적인 구현 (Circle)
class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing a Circle with radius: " + radius);
}
}
// 구현 클래스 2: 구체적인 구현 (Rectangle)
class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing a Rectangle with width: " + width + " and height: " + height);
}
}
// 정적 팩터리 메서드 제공 클래스
class ShapeFactory {
// 정적 팩터리 메서드: 인터페이스 타입으로 객체를 반환
public static Shape createCircle(double radius) {
return new Circle(radius); // Circle 객체를 반환
}
public static Shape createRectangle(double width, double height) {
return new Rectangle(width, height); // Rectangle 객체를 반환
}
}
// 클라이언트 코드
public class InterfaceBasedFactoryExample {
public static void main(String[] args) {
// Circle 객체 생성 (Shape 인터페이스 타입으로 반환됨)
Shape circle = ShapeFactory.createCircle(5.0);
// Rectangle 객체 생성 (Shape 인터페이스 타입으로 반환됨)
Shape rectangle = ShapeFactory.createRectangle(10.0, 20.0);
// 인터페이스를 통해 메서드 호출
circle.draw(); // 출력: Drawing a Circle with radius: 5.0
rectangle.draw(); // 출력: Drawing a Rectangle with width: 10.0 and height: 20.0
}
}
```
자바 8전에는 인터페이스에 정적 메서드를 선언할 수 없었다.
// Java 8 이전: 유틸리티 클래스를 사용한 정적 메서드
public class MathUtils {
private MathUtils() {
// 인스턴스화 방지
}
public static int add(int a, int b) {
return a + b;
}
public static int multiply(int a, int b) {
return a * b;
}
}
자바 8부터 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸다.
// 인터페이스: 정적 메서드를 포함할 수 있음
public interface MathUtils {
// Java 8: 인터페이스에 정적 메서드 선언 가능
static int add(int a, int b) {
return a + b;
}
static int multiply(int a, int b) {
return a * b;
}
// Java 9: private 정적 메서드 (재사용을 위해)
private static void validateInput(int a, int b) {
if (a < 0 || b < 0) {
throw new IllegalArgumentException("Inputs must be non-negative.");
}
}
// Java 9: private 정적 메서드 호출 예제
static int subtract(int a, int b) {
validateInput(a, b); // private 정적 메서드 호출
return a - b;
}
}
// 클라이언트 코드
public class StaticMethodInInterfaceExample {
public static void main(String[] args) {
// Java 8: 인터페이스의 정적 메서드 호출
int sum = MathUtils.add(5, 10); // 15
int product = MathUtils.multiply(5, 10); // 50
// Java 9: private 정적 메서드를 사용하는 subtract
int difference = MathUtils.subtract(15, 5); // 10
// 출력
System.out.println("Sum: " + sum); // Sum: 15
System.out.println("Product: " + product); // Product: 50
System.out.println("Difference: " + difference); // Difference: 10
}
}
클라이언트는 이 두 클래스의 존재를 모른다.
만약, 원소가 적을 때, RegularEnumSet을 사용할 이점이 없어진다면, 다음 릴리스 때는 이를 삭제해도 아무문제가 없다.
비슷하게 성능을 더 개선한 3,4 번째 클래스를 다음 릴리스에 추가 할 수 있다.
→ 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고, 알 필요도 없다. EnumSet의 하위 클래스이기만 하면 된다.
```java
import java.util.*;
// Enum 타입: 열거형 상수
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// EnumSet의 상위 클래스 (공개된 API)
abstract class CustomEnumSet<E extends Enum<E>> {
// 정적 팩터리 메서드
public static <E extends Enum<E>> CustomEnumSet<E> of(EnumSet<E> elements) {
if (elements.size() <= 64) {
return new RegularEnumSet<>(elements); // 64개 이하일 때
} else {
return new JumboEnumSet<>(elements); // 65개 이상일 때
}
}
// 추상 메서드: 하위 클래스에서 구현
public abstract void displayElements();
}
// RegularEnumSet: 64개 이하의 원소를 효율적으로 관리
class RegularEnumSet<E extends Enum<E>> extends CustomEnumSet<E> {
private final EnumSet<E> elements;
RegularEnumSet(EnumSet<E> elements) {
this.elements = elements;
}
@Override
public void displayElements() {
System.out.println("RegularEnumSet: " + elements);
}
}
// JumboEnumSet: 65개 이상의 원소를 관리
class JumboEnumSet<E extends Enum<E>> extends CustomEnumSet<E> {
private final EnumSet<E> elements;
JumboEnumSet(EnumSet<E> elements) {
this.elements = elements;
}
@Override
public void displayElements() {
System.out.println("JumboEnumSet: " + elements);
}
}
// 클라이언트 코드
public class EnumSetExample {
public static void main(String[] args) {
// EnumSet 생성 (원소 64개 이하)
EnumSet<Day> smallSet = EnumSet.of(Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY);
// EnumSet 생성 (원소 65개 이상을 위해 더미 데이터로 확장)
EnumSet<Day> largeSet = EnumSet.allOf(Day.class);
// 정적 팩터리 메서드로 CustomEnumSet 생성
CustomEnumSet<Day> smallEnumSet = CustomEnumSet.of(smallSet);
CustomEnumSet<Day> largeEnumSet = CustomEnumSet.of(largeSet);
// 원소 출력
smallEnumSet.displayElements(); // 출력: RegularEnumSet: [MONDAY, TUESDAY, WEDNESDAY]
largeEnumSet.displayElements(); // 출력: JumboEnumSet: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
}
/*
출력 결과
RegularEnumSet: [MONDAY, TUESDAY, WEDNESDAY]
JumboEnumSet: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
*/
}
```
1. **정적 팩터리 메서드의 유연성**
1. 입력 매개변수에 따라 다른 클래스 객체를 반환할 수 있음.
2. 반환 타입의 하위 클래스이기만 하면 어떤 클래스 객체를 반환하든 클라이언트 코드에는 영향을 주지 않음.
2. **구현 세부사항 캡슐화**
1. 클라이언트는 내부적으로 어떤 클래스가 사용되는지 알 필요가 없음.
2. 내부 구현이 변경되어도 클라이언트 코드는 수정할 필요가 없음.
3. **미래 확장성**
1. 새로운 요구사항이나 최적화가 필요할 때, 새로운 하위 클래스를 추가하거나 기존 클래스를 대체 가능.
서비스 제공자 프레임워크를 만드는 근간
ex) JDBC
1. ‘서비스 제공자 프레임워크’에서의 ‘제공자’는 서비스의 구현체
2. 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제, 클라이언트를 구현체로부터 분리
**쉽게 설명한다면, JDBC가 “서비스 제공자 프레임워크”의 대표적 예시인 이유는**
**JDBC는** **클라이언트가 직접 데이터베이스 드라이버를 알 필요 없이, 프레임워크가 적절한 드라이버를 찾아 연결을 만들어주기 때문이다.**
JDBC는 **데이터베이스와 클라이언트를 연결하는 서비스 제공자 프레임워크**.
1. **프레임워크**: DriverManager
1. 클라이언트의 요청에 따라 적절한 데이터베이스 드라이버를 찾아서
연결을 제공한다
2. **서비스 제공자**: 데이터베이스 드라이버
1. MySQL, PostgreSQL, H2 등 다양한 데이터베이스 드라이버가 서비스 제공자 역할을 한다.
3. **클라이언트**: 애플리케이션 개발자
1. 클라이언트는 단순히 데이터베이스 URL, 사용자 이름, 비밀번호를 DriverManager에 전달하면, 적절한 드라이버가 선택되어 연결을 제공한다.
2. 어떤 드라이버가 사용되는지는 몰라도 된다.
서비스 제공자 프레임워크는 ‘3개의 핵심 컴포넌트’로 이루어짐
클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있다.
조건명시 x시, 기본 구현체 or 지원하는 구현체들을 하나씩 돌아가며 반환
→ 유연한 정적 팩터리의 실체
3개의 핵심 컴포넌트와 더불어 종종 ‘서비스 제공자 인터페이스’라는 네 번째 컴포넌트가 쓰이기도 함.
역할: 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명해줌.
// 1. 서비스 인터페이스: 음악 스트리밍 서비스의 공통 동작 정의
interface MusicService {
void play(String songName);
void stop();
}
// 2. 제공자 구현체: Spotify 구현체
class SpotifyService implements MusicService {
@Override
public void play(String songName) {
System.out.println("Playing '" + songName + "' on Spotify.");
}
@Override
public void stop() {
System.out.println("Stopping Spotify.");
}
}
// 2. 제공자 구현체: YouTube Music 구현체
class YouTubeMusicService implements MusicService {
@Override
public void play(String songName) {
System.out.println("Playing '" + songName + "' on YouTube Music.");
}
@Override
public void stop() {
System.out.println("Stopping YouTube Music.");
}
}
// 3. 제공자 등록 API:
import java.util.HashMap;
import java.util.Map;
// 제공자 관리 및 등록
class MusicServiceRegistry {
private static final Map<String, MusicService> providers = new HashMap<>();
// 제공자 등록
public static void registerProvider(String name, MusicService provider) {
providers.put(name, provider);
}
// 서비스 접근 API
public static MusicService getService(String name) {
MusicService service = providers.get(name);
if (service == null) {
throw new IllegalArgumentException("No music service found with name: " + name);
}
return service;
}
// 기본 제공자 반환
public static MusicService getDefaultService() {
if (providers.isEmpty()) {
throw new IllegalStateException("No music services registered.");
}
return providers.values().iterator().next();
}
}
//4. 클라이언트 코드
public class MusicServiceExample {
public static void main(String[] args) {
// 제공자 등록
MusicServiceRegistry.registerProvider("Spotify", new SpotifyService());
MusicServiceRegistry.registerProvider("YouTubeMusic", new YouTubeMusicService());
// 특정 제공자 사용
MusicService spotify = MusicServiceRegistry.getService("Spotify");
spotify.play("Shape of You"); // 출력: Playing 'Shape of You' on Spotify.
spotify.stop(); // 출력: Stopping Spotify.
// 기본 제공자 사용
MusicService defaultService = MusicServiceRegistry.getDefaultService();
defaultService.play("Bohemian Rhapsody"); // 출력: Playing 'Bohemian Rhapsody' on Spotify.
}
}
코드 설명
상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다
정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
→ 그래서 쓰는 정적 팩터리 메서드에서 흔히 사용하는 명명 방식:
핵심 정리:
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용할 것.
그렇지만, 정적 팩터리를 사용하는게 유리한 경우가 더많으므로
무작정 public 생성자를 제공하던 습관이 있다면 고치자.