관리 메뉴

The Nirsa Way

[Effective Java 3/E - 객체 생성과 파괴] Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라 본문

Programming/JAVA

[Effective Java 3/E - 객체 생성과 파괴] Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라

KoreaNirsa 2025. 5. 30. 14:55
반응형
정적 팩터리 메서드란?

정적 팩터리 메서드는 객체를 생성하는 메서드이지만 일반적으로 사용되는 new 연산자를 사용하지 않는 메서드 입니다. 클래스 내부에 static 메서드로 용도에 따라 of(), valueOf(), getInstance(), form()를 생성하여 객체를 반환하는 메서드입니다.

public class Main {
	public static void main(String[] args) {
		Person p = Person.of("Nirsa"); // 정적 팩터리 메서드를 사용하여 객체 생성
		System.out.println(p.getName()); // Nirsa 출력
	}
}

class Person {
	private String name;
	
    	// 외부에서 생성자를 호출하여 객체를 생성할 수 없도록 private으로 접근 제어
    	// 같은 클래스에 있는 정적 팩터리 메서드에서만 생성자 호출 가능
	private Person(String name) {
		this.name = name;
	}
	
    	// 정적 팩터리 메서드
	public static Person of(String name) {
		return new Person(name);
	}
	
	public String getName() {
		return name;
	}
}

위의 코드와 같이 정적 팩터리 메서드 of()는 객체를 반환하기 위한 메서드로써 사용되며, 생성자는 private으로 접근을 제어하여 외부에서 new 연산자를 통해 객체를 생성할 수 없도록 합니다.

정적 팩터리 메서드에서만 생성자를 사용하여 객체 생성이 가능합니다.

메서드명 의미
from() 하나의 매개변수를 받아서 인스턴스 생성
of() 여러개의 매개변수를 받아서 인스턴스 생성
getInstance() 인스턴스를 반환하지만 동일한 인스턴스임을 보장하지 않음 (싱글턴 등)
getXXX() 호출하는 클래스와 다른 타입의 인스턴스를 반환
newXXX() 호출마다 매번 새로운 인스턴스를 반환

 


 

장점1 : 이름을 가질 수 있다.

생성자를 사용하면 "new 생성자명()"을 사용하여 정확히 어떤 용도의 인스턴스를 생성하고 반환하는지에 대한 명확한 의미를 가지기 힘듭니다. 하지만, 정적 팩터리 메서드를 사용하게 되면 메서드명으로도 어떠한 객체를 생성하고 반환하는지 명확하게 확인을 할 수 있어 가독성과 유지보수성이 올라가게 됩니다.

예를 들어, 아래의 코드가 있을 때 정적 팩터리 메서드를 활용하면 각각 과일(Fruit)의 객체이지만 해당 값이 "Apple"인지, "Banana"인지 메서드 이름만으로 파악이 가능해집니다.

이로인해 개발자의 실수로 의도와 다른 것을 호출하는 실수를 방지할 가능성을 높일 수 있습니다.

public class Main {
	public static void main(String[] args) {
        Fruit f1 = Fruit.apple();
        Fruit f2 = Fruit.banana();

        System.out.println(f1); // Apple
        System.out.println(f2); // Banana
	}
}

class Fruit {
    private String name;

    private Fruit(String name) {
        this.name = name;
    }

    public static Fruit apple() {
        return new Fruit("Apple");
    }

    public static Fruit banana() {
        return new Fruit("Banana");
    }

    @Override
    public String toString() {
        return name;
    }
}

 


 

장점2 : 호출할 때 마다 인스턴스를 새로 생성하지 않아도 된다.

아래와 같은 코드가 있을 때 YesNo 클래스에는 static final을 사용한 YES, NO 객체를 미리 생성해둡니다.

이러한 상황에서 정적 팩터리 메서드(valueOf)는 기존에 만들어진 객체를 재사용하게 되며, 미리 만들어둔 객체를 반환함으로써 호출할 때 마다 새로운 인스턴스를 생성하지 않아도 됩니다.

public class Main {
	public static void main(String[] args) {
        YesNo yes1 = YesNo.valueOf(true);
        YesNo yes2 = YesNo.valueOf(true);
        YesNo no1 = YesNo.valueOf(false);

        System.out.println(yes1); // YES
        System.out.println(no1); // NO
        System.out.println(yes1 == yes2); // true (동일한 인스턴스)
	}
}

class YesNo {
    private static final YesNo YES = new YesNo(true);
    private static final YesNo NO = new YesNo(false);

    private boolean value;

    private YesNo(boolean value) {
        this.value = value;
    }

    // 호출할 때 마다 새로운 객체를 생성하지 않고
    // YES, NO의 객체를 반환하며 재사용함
    public static YesNo valueOf(boolean value) {
        return value ? YES : NO;
    }

    @Override
    public String toString() {
        return value ? "YES" : "NO";
    }
}

 


 

장점3 : 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

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

장점 3번과 4번은 정적 팩터리 메서드의 "다형성을 활용할 수 있는" 특징에 의해 생깁니다. 정적 팩터리 메서드는 동일한 상위 타입으로 여러 개의 하위 구현체 클래스 중 상황에 알맞은 객체를 선택하여 반환할 수 있습니다.

아래의 코드는 "Animal" 이라는 상위 타입(interface)을 만들어 두고 각각 "Dog", "Cat" 구현체를 작성하였는데, "AnimalFactory"라는 정적 팩터리 메서드를 제공하는 클래스의 코드를 살펴보면 매개 변수로 어떠한 타입인지 문자열로 입력을 받습니다.

이후 전달받은 문자열이 dog와 일치 하는지, cat과 일치 하는지 확인을 하며 "dog"와 일치하면 Dog 객체를 반환하고 "cat"과 일치하면 Cat 객체를 반환합니다.

정적 팩터리 메서드의 반환 타입은 상위 타입인 Animal 이므로, 다형성에 의해 하위 타입인 Dog, Cat이 모두 반환 가능한 상태이기 때문에 가능한 코드가 됩니다.

이렇게 정적 팩터리 메서드를 사용하면 다형성을 사용하여 다양한 객체를 반환하여 사용할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Animal a1 = AnimalFactory.create("dog");
        Animal a2 = AnimalFactory.create("cat");
        
//        아래의 코드는 llegalArgumentException("알 수 없는 동물 타입") 발생
//        Animal a3 = AnimalFactory.create("pig");   

        a1.sound(); // 멍멍
        a2.sound(); // 야옹
    }
}

// 상위 타입 (인터페이스 정의)
interface Animal {
 void sound();
}

// 하위 타입 (구현체)
class Dog implements Animal {
 public void sound() { System.out.println("멍멍"); }
}

class Cat implements Animal {
 public void sound() { System.out.println("야옹"); }
}


//정적 팩터리 메서드 제공 클래스
class AnimalFactory {
 public static Animal create(String type) {
     if ("dog".equalsIgnoreCase(type)) {
         return new Dog();
     } else if ("cat".equalsIgnoreCase(type)) {
         return new Cat();
     } else {
         throw new IllegalArgumentException("알 수 없는 동물 타입");
     }
 }
}

 


 

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

정적 팩터리를 작성하는 시점에는 실제 구현체가 없더라도 괜찮습니다. 

서비스 제공자는 Payment라는 인터페이스를 생성해둔 후 정적 팩터리 메서드에서는 해당 인터페이스의 타입으로 주입받도록 코드를 작성하고나서 이후 사용자는 Payment 인터페이스를 상속받아 구현체를 만들면 정적 팩터리 메서드를 그대로 사용을 할 수 있습니다.

public class Main {
	public static void main(String[] args) {
		// 사용자가 구현체 작성 후 정적 팩터리 메서드 사용
		Payment payment1 = PaymentFactory.createPayment(new CardPayment());
		Payment payment2 = PaymentFactory.createPayment(new KakaoPay());

		payment1.pay(5000);
		payment2.pay(7000);
	}
}

// *** 서비스 제공자 ***
interface Payment {
	void pay(int amount);
}

class PaymentFactory {
	// 외부에서 구현체를 주입하기 때문에 정적 팩터리 메서드 작성하는 시점에
	// 반환할 실제 구현체가 없어도 작성 가능
	public static Payment createPayment(Payment impl) {
		return impl;
	}
}

// *** 사용자가 구현체 작성 ***
class CardPayment implements Payment {
	public void pay(int amount) {
		System.out.println(amount + "원을 카드로 결제합니다.");
	}
}

class KakaoPay implements Payment {
	public void pay(int amount) {
		System.out.println(amount + "원을 카카오페이로 결제합니다.");
	}
}

 


 

단점1 : 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

위의 예시 코드들을 확인해보면 정적 팩터리 메서드만 제공하면 private 생성자를 사용하므로 하위 클래스가 생성자로 접근할 수 없기 때문에 상속받아 사용할 수 없습니다.

 


 

단점2 : 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

생성자에 비해 API 문서에 설명이 명확히 드러나지 않기 때문에 사용자는 해당 서비스의 정적 팩터리 메서드를 사용할 수 있는 방법을 알아내서 사용해야 합니다.

이러한 특징으로 인해 정적 팩터리 메서드를 작성하는 사람은 메서드명은 널리 알려진 규약으로 작성하고, API 문서를 명확히 할 수 있도록 해야 합니다.

반응형