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 클래스
버전 별 변천 과정
- 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) {
}
}
- 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) {
}
}
- 현재
- 인터페이스 안에 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();
}
}
}
서비스 제공자 프레임워크 변형
공급자가 제공하는 것보다 더 풍부한 서비스 인터페이스를 클라이언트에 반환할 수 있다.
- 브리지 패턴 : 추상화와 구현을 분리하여 둘을 독립적으로 변형할 수 있는 디자인 패턴
// 기본 서비스 인터페이스 (공급자가 제공)
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
'Java > effective java' 카테고리의 다른 글
[Effective Java] Item 5. 자원을 직접 명시하지 말고 의존 객체 주입(DI)을 사용하라. (0) | 2025.01.05 |
---|---|
[Effective Java] Item 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2025.01.05 |
[Effective Java] Item 3. private 생성자나 열거 타입으로 싱글턴임을 보장하라. (0) | 2025.01.05 |
[Effective Java] Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2025.01.05 |
[Effective Java] 2장. 객체 생성과 파괴 - 0. 들어가기 (0) | 2025.01.05 |