[이펙티브 자바] 객체의 생성과 파괴 Item1 - 생성자 대신 정적 팩터리 메서드를 고려하라

이성훈·2022년 3월 11일
4

이펙티브 자바

목록 보기
2/17
post-thumbnail

이펙티브 자바의 첫 시작은 객체를 생성하고 파괴하는 것에 대한 고찰이다.

"2장 - 객체의 생성과 파괴" 는 다음과 같은 기준으로 맥락을 잡고 있다.

  • 객체를 만들어야 할 때는 언제인가
  • 객체를 만들지 말아야 할 때는 언제인가
  • 올바른 객체 생성 방법은 무엇인가
  • 객체의 불필요한 생성을 피하는 방법은 무엇인가
  • 객체를 제 때에 파괴시키는 방법은 무엇인가
  • 파괴 전에 수행해야 할 정리 작업을 관리하는 요령이 있는가

위와 같은 맥락을 계속 기억하며 공부하자.


  • Item1. 생성자 대신 정적 팩터리 메서드를 고려하라.
  • Item2. 생성자에 매개변수가 많다면 빌더를 고려하라.
  • Item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
  • Item4. 인스턴스화를 막으려거든 private 생성자를 사용하라.
  • Item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.
  • Item6. 불필요한 객체 생성을 피하라.
  • Item7. 다 쓴 객체 참조를 해체하라.
  • Item8. finalizer와 cleaner 사용을 피하라.
  • Item9. try-finally 보다는 try-with-resources를 사용하라.




<"생성자 대신 정적 팩터리 메서드를 고려하라">


Item1의 제목을 먼저 살펴보자.

생성자, 정적 팩토리 메소드 라는 말을 일단 알아볼 필요가 있겠다.



#  생성자와 정적 팩토리 메소드


1. 생성자 (Constructor)


생성자(Constructor) - 클래스로부터 객체를 생성할 때의 호출하는 메소드.


개발 과정에서 너무나도 당연하게 사용하는 거라 오히려 단어로 보면 생소하지 않나 싶다. (너무 오랜만에 봐서;)

다음의 간단한 코드를 보면서 기억하자.



  • 생성자의 예시
public class Laptop
{
  private String brand;
  public Laptop(String brand)
  {
  	this.brand = brand;
  }
}
public class Application
{
  public static void main(String[] args)
  {
  	Laptop laptop = new Laptop("apple");
  }
}

위와 같이 Laptop이라는 Class를 정의할 때 같은 이름의 메서드로 생성자를 구성하게 되며,
main 함수에서 Laptop이라는 객체를 만들 때, 그 생성자를 통하게 된다.



  • 생성자의 특징
  1. new 연산자를 통해서 객체 생성 시, 반드시 호출 되고 제일 먼저 실행된다.
  2. 객체의 초기화(Initialize) 역할을 한다.
  3. 생성자의 이름은 클래스의 이름과 동일해야 한다.
  4. 만약 생성자를 생성하지 않으면, 컴파일러가 자동으로 Default Constructor를 생성, 주입한다.
  5. 만약 파라미터 구성이 같은 생성자는 두개 이상 만들 수 없다.
    (예를 들어, String 변수를 하나 받는 생성자가 두개일 수는 없다.)



2. 정적 팩토리 메서드 (Static Factory Method)


정적 팩토리 메서드를 설명하기 전, 정확한 이해를 하는데에 도움이 될 만한 단어 및 개념을 먼저 정리해놓는다.
(기초가 탄탄하면 굳이 안봐도 되긴한다.)


  • GoF Pattern - "Gang Of Four"의 줄임말로, 네명의 개발자가 정리한 Software Design Pattern.

  • Software Pattern - 소프트웨어를 설계하는 단계에서 참고할 수 있는 Problem & Solving 을 정리해 놓은것.

  • Factory - GoF 패턴에 등장하는 기법 중 하나로, 메소드 하나를 두어 객체 생성의 역할만을 하게 한다는 것. (Expert, Low Coupling, High Cohesion)

  • Expert - GoF 패턴에 등장하는 기법 중 하나. 한 객체가 한 기능을 최대한 담당하는 것이 좋다는 이론이다.

  • Low Coupling - GoF 패턴에 등장하는 기법 중 하나이며 OOP의 대원칙. 객체와 객체 사이에 최대한 결합도를 낮게 해야한다는 것.

  • High Cohesion GoF 패턴에 등장하는 기법 중 하나이며 OOP의 대원칙.  한 기능은 최대한 한 객체에 응집되어 있어야 한다는 것.



  • Factory란 무엇인가?

우리는 일단 Factory라는 것이 무엇인지 이해해볼 필요가 있다.
여기서의 Factory라는 단어는 GoF Pattern에서 유래하고 있다.

OOP (Object Oriented Programming : 객체 지향 프로그래밍)에서 객체의 생성이라는 것은 꽤나 중요한 논점이다.

개발을 하다보면 하나의 클래스에서 여러 객체를 생성하기도 하고 여러 기능적인 로직까지 추가하기도 한다.

이는 OOP의 대원칙 중 하나인 High Cohesion (높은 응집도) 에 어긋난다.

High Cohesion을 이해하기 위해, 가령 이런 예시를 들어볼 수 있다.
(나는 OOP를 생각할 때 레고 놀이를 떠올리곤 한다.)

예를 들어, 나는 A부품을 머리로, B부품을 몸통으로, C부품을 다리로 각자 가져와 조합하고 싶었다.

그런데 A부품은 그 통째로 사람 하나이기 때문에 머리를 떼어올 수가 없다.
결국 머리만 담당할 다른 부품을 만들어야 하는 상황인 것이다.

만약 A부품은 머리만 담당하고 B부품은 몸통만 담당하고 C부품은 다리만 담당한다면 어떤 조합에서도 유연하게 재사용할 수 있지 않았을까?


그런데 문제는 한 클래스당 로직이 안들어갈 수는 없는 일이다.
(적어도 A.java라는 파일은 A라는 로직을 수행해야 할 것이 아닌가?)

그런데 또 클래스에는 생성자 또한 필요한 법이다.
(A라는 객체를 써먹으려면 생성은 해야하지 않냐는 말이다!)


그래서 GoF Pattern에서는 차라리 여러 클래스들의 생성만을 전문적으로
(Expert Pattern) 담당하는 것을 하나 만드는 것이 어떠냐고 제안한다.

마치 공장에서 부품을 찍어내듯이 말이다. (Factory Pattern)


위 그림은 Factory의 구현 예시이다.
그림에서 볼 수 있듯, Shape이라는 클래스와 그것을 추상화한 Circle과 Box라는 클래스 모두 자체적으로 생성 기능이 없다.

아예 Factory에서 생성 기능을 전담하고 있는 것이다.



  • 정적 팩토리 메서드 (Static Factory Method) 예시

이제 필요한 배경 지식 설명은 끝난것 같으니 다시 본론으로 돌아오자.

필자는 객체의 생성을 일반적인 생성자(Constructor)를 통하지 않고,
생성을 위한 Method를 별도로 만들어서 하는건 어떤가하고 제안하고 있다.


사실 여기까지만 읽으면 진짜 이해가 잘 안갔다.

위에서 생성자의 예시로 들었던 간단한 코드를 이번엔 정적 팩토리 메서드로 작성해보고 다시 이야기해보자.

public class Laptop
{
  private String brand;
  private Laptop(String brand)
  {
  	    this.brand = brand;
  }
  public static Laptop withBrand(String brand)
  {
  		return new Laptop(brand);
  }
}
public class Application
{
  public static void main(String[] args)
  {
  	Laptop laptop = Laptop.withBrand("apple");
  }
}

위처럼 단순한 예시도 있고, 우리에게 보다 친숙할 예시도 있다.
public final class Optional<T>
{
	private Optional(T value)
	{
	    this.value = Objects.requireNonNull(value);
	}
	public static <T> Optional<T> of(T value)
	{
		return new Optional<>(value);
	}
}
public class Application
{
  public static void main(String[] args)
  {
  		Optional<Long> value = Optional.of(1L);
  }
}

자바 1.8에서 등장한 Optional은 대표적인 정적 팩토리 메서드로 구현되어 있다고 한다.

여튼 예시를 보았는데 어떤가? 말은 생성자를 안쓸것 같이 말하지만 생성자는 똑같이 있고, 정적 팩터리 메서드 내부에서 어차피 다시 new 연산을 통해 생성자를 사용하고 있다.

솔직히 코드만 봐서는 오히려 바로 생성자를 통하는 것보다 더 번거로워 보인다.

그럼 대체 뭐가 다르다는 것일까? 이제부터 그 차이 장점과 단점을 통해 알아보고자 한다.






#  정적 팩토리 메서드의 장점


1. 이름을 가질 수 있다.



  • "객체의 특성을 쉽게 묘사할 수 있다.

앞에서 말했듯 생성자의 이름은 클래스의 이름과 같아야 한다. 이는 생성자의 특징이자 제한사항이다.

만약 정적 팩토리 메서드를 사용한다면, 클래스 이름과는 다른 보다 특징적인 이름을 지어줄 수가 있다.

사실 여기까지 읽어놓고 드는 생각은 "그래서 그게 뭐 어쨌다고..?" 라는 것이다.

나처럼 헤매는 사람들을 위해 (우리 모두 불쌍..) 위에서 작성했던 예시를 다시 가져왔다.

public class Laptop {

	private String brand;
	private String cpu;
	private String ram;
	private String weight;
 
	private Laptop(){
	}

	public static Laptop fromBrand(String brand)
	{
		Laptop laptop = new Laptop();
		laptop.brand = brand;
		return laptop;
	}

	public static Laptop fromCpu(String cpu)
	{
		Laptop laptop = new Laptop();
		laptop.cpu = cpu;
		return laptop;
	}

	public static Laptop fromRam(String ram)
	{
		Laptop laptop = new Laptop();
		laptop.ram = ram;
		return laptop;
	}

	public static Laptop fromWeight(String weight)
	{
		Laptop laptop = new Laptop();
		laptop.weight = weight;
		return laptop;
	}
}
public class Application
{
  public static void main(String[] args)
  {
		Laptop laptop1 = Laptop.fromBrand("Apple");
		Laptop laptop2 = Laptop.fromCpu("Intel");
		Laptop laptop3 = Laptop.fromRam("4GB");
		Laptop laptop4  = Laptop.fromWeight("1.3kg");
  }
}

나는 이 예시를 써놓고서 얼추 감을 찾을 수 있었다.

일반적인 생성자는 클래스 이름과 같은 이름을 가진다는 제약이 있기 때문에,
무슨 의도인지, 무엇으로 만드는지 파악하기가 어렵다.


그런데 위의 예시를 보면,
"아, 브랜드 이름을 가지고 노트북이라는 객체를 만든다는 의미구나"
"아, Cpu 이름을 가지고 노트북이라는 객체를 만들겠다는 거구나"
를 알 수가 있다.

결과적으로는 코드의 가독성을 높여주게 된다.



  • "하나의 시그니처로 생성자를 하나만 만들 수 있다" 라는 제약이 없다.

이건 정말 번역의 문제다. 정말 번역하시는 분이 번역만 했다는걸 알 수 있다.
이 문장만 읽고 이해하는 사람은 천재가 아닐까 싶다.

여튼... 설명해보자.


여기서 시그니처라고 말하는 것은 생성자의 생성 조건이라고 이해하는 것이 마음 편하다.
즉, 생성자를 정의할 때 보통 매개변수의 조합을 말한다.

public class Laptop {

	private String brand;
	private String cpu;
	
	public Laptop (String brand)
	{
  		this.brand = brand;
	}
	public Laptop (String cpu)
	{
  		this.cpu = cpu;
	}
}

위와 같이 일반적인 방법인 public 생성자를 두개 만들어보았다.
그런데 이는 성립이 불가한 코드이다. fromBrand인 생성자와 fromCpu인 생성자의 생성 조건이 String 변수 하나라는 조건으로 같기 때문이다.


그럼 매개변수가 여러개일 때는 어떨까?

public class Laptop {

	private String brand;
	private String cpu;
	
	public Laptop (String brand, int countryCode)
	{
		this.brand = brand;
	}
	public Laptop (String cpu, int spec)
	{
		this.cpu = cpu;
	}
}

이 또한 성립이 불가한 코드이다.
생성자 둘의 생성 조건이 (String, int) 조합으로 같기 때문이다.


이때 생성 조건은 그 순서를 구분하기 때문에 다음과 같이 할 수는 있다.

public class Laptop {

	private String brand;
	private String cpu;
	
	public Laptop (String brand, int countryCode)
	{
		this.brand = brand;
	}
	public Laptop (int spec, String cpu)
	{
		this.cpu = cpu;
	}
}

하지만 이런 방식으로 여러개의 생성자를 만들어낸다고 해도,
개발자의 입장에서는 이름이 모두 똑같은 여러개의 생성자를 보며 멍때리기 쉽상이다.

실제 생성자마다 역할이 다를텐데 엉뚱한걸 호출할 수도 있다.


하지만 정적 팩토리 메소드를 통한다면 이야기가 다르다.
public class Laptop {

	private String brand;
	private String cpu;
	private String ram;
	private String weight;

	private Laptop(){
	}
	
	public static Laptop fromBrand(String brand)
	{
		Laptop laptop = new Laptop();
		laptop.brand = brand;
		return laptop;
	}

	public static Laptop fromCpu(String cpu)
	{
		Laptop laptop = new Laptop();
		laptop.cpu = cpu;
		return laptop;
	}
	
	public static Laptop fromRam(String ram, int spec)
	{
		Laptop laptop = new Laptop();
		laptop.ram = ram;
		return laptop;
	}
	
	public static Laptop fromWeight(String weight, int spec)
	{
		Laptop laptop = new Laptop();
		laptop.weight = weight;
		return laptop;
	}
}

위와 같이 각자의 생성 역할을 하는 (유사 생성자) 메서드가 각 이름을 가질 수 있기 때문에,
"하나의 시그니처로 생성자를 하나만 만들 수 있다" 라는 제약이 사라지게 된다.

<장점1>

  • 각 정적 메서드에게 이름을 부여하여 코드 가독성 및 편의를 높인다.
  • 생성 조건 (파라미터의 조합)이 같은 생성자는 두개 이상 존재할 수 없다는 제약 조건이 사라진다.



2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.



  • "인스턴스 통제 (instance-controlled) 클래스 :
    언제 어느 인스턴스를 살아있게 할 지를 철저히 통제할 수 있는 클래스."

계속 느끼는건데 이 책은 너무 본론만 말한다.
그래서 또 내가 이해한대로 풀어서 설명해본다.


현재까지 개념을 잘 따라오고 있다면 정적 팩토리 메소드란 결국 객체의 생성을 책임지고 있다는 것을 알 것이다.

조금 돌려서 말하면, 정적 팩토리 메소드가 객체의 생성 방식 또한 관리할 수 있다는 말이 된다.

이는 다음과 같은 의미를 내포한다.

  • 필요에 따라 항상 새로운 객체를 생성해서 반환할 수 있다.
  • 필요에 따라 새로운 객체 생성을 금지하고 같은 객체만 반환할 수 있다.
  • 불필요한 객체를 굳이 만들지 않게 하도록 통제할 수 있다.

일반적인 생성자(Constructor)를 사용할 경우 호출할 때마다 항상 새로운 객체를 만들게 된다.
그런데 인스턴스의 내용이 바뀔 일도 없고, 호출 빈도수가 매우 잦은데 생성 비용까지 크다면? 그것은 자원 낭비가 매우 심하다.


만약 정적 팩토리 메소드를 사용한다면,

경우에 따라 어떤 클래스는 객체를 만들지 않도록 하거나,
객체를 오로지 하나만 만들도록 할 수 있기 때문에,

위와 같은 특수적인 상황에서는 효율성이 증대되며 빛을 발한다.

각 예시를 들어보자.


  • "인스턴스를 통제하면 ... 인스턴스화 불가(noninstantiable)"

인스턴스화 불가 (noninstantiable) - 인스턴스의 추가 생성을 통제한다.


보통 클래스에서 인스턴스화 될 때 어떤 일에도 변할 일이 없을 경우에,
이를 불변 클래스 (immutable class) 라고 한다.

그래서 이런 경우에는 인스턴스를 미리 만들어 놓고 있다가,
호출이 오면 새로 만들지 않고 기존 것을 반환하게 하는게 좋다고 한다.

가장 대표적인 예시가 Boolean 이다.

다음 코드는 실제 Boolean 클래스의 일부 코드이다.


public final class Boolean implements java.io.Serializable, Comparable<Boolean>, Constable
{
	public static final Boolean TRUE = new Boolean(true);
  	public static final Boolean FALSE = new Boolean(false);


	private final boolean value;
	
  	public Boolean(boolean value)
  	{
  		this.value = value;
  	}

  	public static Boolean valueOf(boolean b)
  	{
  		return (b ? TRUE:FALSE);
  	}
public class Application
{
  public static void main(String[] args)
  {
  		boolean b = true;
		Boolean myBoolean = Boolean.valueOf(b);
  }
}

위 코드를 살펴보면,
Boolean 클래스는 TRUE 와 FALSE라는 인스턴스를 사전에 생성해두고 있다.

그리고 정적 팩토리 메서드인 valueOf는,
호출될 경우 기존에 생성되어 있는 TRUE 또는 FALSE만을 반환한다.

사실상 계속 같은 인스턴스만을 반환하고 있는 것인데,
Boolean 클래스의 인스턴스는 변형의 여지가 없기 때문에 이렇게 하는거다.
(그래봤자 true 아니면 false를 반환하는 목적의 클래스니까..)


이걸로 불변 클래스 (immutable class)인스턴스화 불가 (noninstantiable) 에 대한 맥락은 잡힌다.

그런데 Boolean 예시는 이미 만들어져 있는거라.. 보다 일반적인 예시가 필요하다.
(나만 그런가..?)

위에서 사용하던 예시를 다시 가져와보자.

public class Laptop {

	public static final Laptop INSTANCE = new Laptop();
 
	private Laptop(){
	}

	public static Laptop getInstance()
	{
		return INSTANCE;
	}
}
public class Application
{
  public static void main(String[] args)
  {
		Laptop laptop = Laptop.getInstance();
  }
}

간단하지만 확실하게 시사하고 있는것 같다.

Laptop 클래스는 INSTANCE 라는 인스턴스를 미리 사전에 만들어두었고,

getInstance()라는 정적 팩토리 메서드를 통하면,
INSTANCE라는 항상 같은 인스턴스를 반환하도록 되어있다.

이러한 방식으로 객체의 생성을 통제할 수 있다는 말이다.



  • "인스턴스를 통제하면 클래스를 싱글턴(singleton)으로 만들 수도.. "

싱글턴 (singleton) - 객체를 단 하나만 생성하게 하고, 생성된 객체를 시스템 어디에서든지 사용할 수 있게 하는 GoF 패턴의 하나.


이번엔 인스턴스를 만들지 못하게 하지는 않지만, 단 하나만 만들도록 하는 제약 조건이다.

사실 이 말 자체가 싱글턴(singleton) 패턴을 뜻하는데,
이에 대해서는 뒤에서 더 다루므로 여기서는 언급만 하도록 한다.

여기서는 이렇게 싱글턴(singleton) 또한 정적 팩토리 메서드로 구현이 가능하며,
다양하게 객체 생성 통제가 가능하다 라는 것에 초점을 맞추고 보면 된다.


예시 코드를 보자.

public class Singleton
{
	private static Singleton singleton = null;

	private Singleton()
  	{
  	}

  	public static Singleton getInstance()
  	{
  		if(singleton == null)
  		{
  			singleton = new Singleton();
  		}
  		return singleton;
  	}
}
public class Application
{
  public static void main(String[] args)
  {
  		Singleton singleton = Singleton.getInstance();
  }
}

코드를 보면, if 문을 사용하여 생성된 객체가 없으면 생성해서 반환하고, 기존에 생성된게 있으면 그것을 반환하도록 되어있다.

결과적으로 단 하나의 인스턴스만을 생성하고 반환하게 되는 것이다.



<장점2>

  • 불필요한 인스턴스의 생성을 막을 수 있다.
    • 인스턴스화 불가 (noninstantiable) 보장.

  • 인스턴스를 단 하나만 생성해서 그것만 사용하게 할 수도 있다.
    • Singleton 패턴 구현 가능.

  • 결론적으로, 상황에 적절하게 객체 생성을 통제하는 것이 가능하다.
    • 인스턴스 통제 클래스 (instance-controlled) 구현 가능



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

< 하위 타입 >
java에서 하위 타입이라는 이야기가 나오면 두 가지를 생각하면 된다.
상속 클래스(Extend)와 추상 클래스(Interface).

이 문장에 대해서는 아무래도 바로 예제 코드를 보는 것이 편할 것 같다.



  • 추상 클래스 (Interface)를 예로 들면

아래 예제 코드를 보자.

public interface Grade 
{
	
	String toText();
	
	public static Grade of(int score)
	{
		if(score >= 90)
			return new A();
		else if(score >=80)
			return new B();
		else
			return new F();
	}
}
public class A implements Grade
{
	@Override
	public String toText()
	{
		return "A";
	}
}

public class B implements Grade
{
	@Override
	public String toText()
	{
		return "B";
	}
}
	
public class F implements Grade
{
	@Override
	public String toText()
	{
		return "F";
	}
}
public class Application 
{
	public static void main(String[] args) 
 	{		
		{
			Grade grade = Grade.of(95);
			System.out.println(grade);
		}
	}
}

코드를 간략히 설명하자면,
Grade라는 추상 클래스를 A,B,C 라는 클래스가 구현하고 있다.
또 정적 팩토리 메서드인 of()의 반환 타입은 Grade이다.

그런데 실제로 of() 메서드를 보면 반환은 A,B,C의 인스턴스로 하고 있다.

이처럼 정적 팩토리 메서드를 사용하면, 반환 타입 (추상 클래스 : Grade)의 하위 타입 (구현 클래스 : A, B, C) 객체를 반환하는게 가능하다.


그렇다면 궁금해지는 것은 저게 왜 장점이 되냐는 것이다.

먼저, 위 코드에서 볼 수 있듯 Grade의 of() 메소드는 조건에 따라 다른 객체를 자유롭게 선택하여 반환한다.
즉, 엄청난 유연성을 보여준다.

또, Grade 클래스 내에 구현체가 있는 것이 아니고,
Grade는 실제 구현체를 연결해주는 역할만을 하기 때문에,
구현 로직은 숨길 수 있으면서, API는 매우 경량화 된다.

개발자의 입장에서도 인터페이스대로 객체를 가져올 것임을 알기에,
굳이 구현체를 찾아볼 필요가 없게된다.
이는 API를 사용하는데에 있어 난이도를 낮추게 된다.


이번 항목은 코드를 통해 학습하면 보다 이해하기가 쉽고,
이해하면 자연스럽게 그 장점 또한 쉽게 받아들여진다.

<장점3>

  • 반환 타입의 하위 타입 객체를 반환할 수 있다.
  • 유연하게 객체를 반환할 수가 있어진다. (높은 유연성)
  • API를 경량화 할 수 있다.
  • 구현체를 숨길 수 있다.
  • 개발자에게 API 사용 난이도를 낮춰준다.



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


사실 장점 3번의 내용과 장점 4번의 내용은 맥락이 서로 같다.

장점 3번은 반환 타입의 하위 객체를 반환할 수 있다는 가능성적인 측면에 초점을 두고있고,
장점 4번은 하위 객체를 반환할 수 있기에 다양한 객체를 선택적으로 반환할 수 있다는 기능적인 측면에 초점을 두고있을 뿐이다.

그렇기에 3번은 이해했다면 4번은 자동으로 이해가 된다.

위에서 봤던 예시를 한번 더 언급해보자.

public interface Grade 
{
	
	String toText();
	
	public static Grade of(int score)
	{
		if(score >= 90)
			return new A();
		else if(score >=80)
			return new B();
		else
			return new F();
	}
}
public class A implements Grade
{
	@Override
	public String toText()
	{
		return "A";
	}
}

public class B implements Grade
{
	@Override
	public String toText()
	{
		return "B";
	}
}
	
public class F implements Grade
{
	@Override
	public String toText()
	{
		return "F";
	}
}
public class Application 
{
	public static void main(String[] args) 
 	{		
		{
			Grade grade = Grade.of(95);
			System.out.println(grade);
		}
	}
}

Grade 추상클래스 내의 of() 메서드를 보면,
매개변수로 score 값에 따라 A, B, F 객체를 선택적으로 반환하고 있는 것이 보인다.


<장점4>

  • 반환 타입의 하위 객체라면, 얼마든지 선택적으로 다양한 객체를 반환할 수 있다.



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


어찌보면 5개의 장점 중 가장 난해할 수 있는 문장이였다.

모두가 나와 같은 생각을 하는 것인지..
다른 여타 블로그들을 많이 찾아봐도 저 문장 자체를 명쾌하게 해설하는건 없었다.

일단 저 문장을 최대한 이해해보려고 노력한 결과,
나만의 해석점을 찾을 수 있었다.

일단 아래 예제 코드를 보면서 설명해보자.





#  정적 팩토리 메서드의 단점


글 내내 정적 팩토리 메서드의 좋은점에 대해서 말했지만,
모든 IT 기술에서는 그 양면이 존재한다.

이제 정적 팩토리 메서드가 안좋은 점에 대해서도 알아보자.



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


그렇게 어려운 말은 아니다.

다음의 예제 코드를 보면서 말해보자.

public class Laptop {

	private String brand;

	private Laptop(){
	}
	
	public static Laptop fromBrand(String brand)
	{
		Laptop laptop = new Laptop();
		laptop.brand = brand;
		return laptop;
	}

}

보통 정적 팩토리 메서드만을 사용하여 생성 기능을 제공하는 클래스의 경우,
외부에서 생성자를 사용하지 못하게 해야하니 private 형태로 선언한다.


하지만 상속(Extend) 기능을 사용하기 위해서는 public이나 protected 형태의 상속자가 필요하다.

다시 말해, 정적 팩토리 메서드 만을 제공하는 클래스의 경우에는
상속을 통한 확장이 불가하다.

하지만 필자는 상속보다 컴포지션을 사용하도록 유도하고,
클래스를 불변 타입으로 만들기 위해서는,
오히려 이 제약이 장점으로 작용할 수 있다고 언급하고 있다.


<단점1>

  • 생성 기능으로 정적 팩토리 메서드만을 제공하는 클래스의 경우, 상속을 통한 확장이 불가하다.
  • 하지만 오히려 이 제약 조건은 장점으로 작용할 수도 있다.



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


일단 다시 위의 예제 코드를 가져와서 살펴보자.

public class Laptop {

	private String brand;

	private Laptop(){
	}
	
	public static Laptop fromBrand(String brand)
	{
		Laptop laptop = new Laptop();
		laptop.brand = brand;
		return laptop;
	}

}

보통 생성자(Constructor)의 경우, 클래스의 최상단부에 위치하게 된다.
뭐 이름 또한 클래명과 같으니,
위치로 보나 이름으로 보나 개발자 입장에서는 찾기가 수월하다.


하지만 정적 팩토리 메서드의 경우,
별다른 위치가 정해지지도 않았을 뿐더러 이름도 별다른 제약조건이 없기 때문에,
언뜻 보면 일반 메서드와 차이를 구별하기 쉽지가 않다.

다시 말해, 개발자가 객체를 생성하는 정적 팩토리 메서드를 보고자 할 때,
그 위치를 한번에 찾기가 쉽지 않다.


<단점2>

  • 정적 팩토리 메서드는 일반 메서드와 같은 형태를 띄고 있기 때문에, 한번에 알아보기가 어렵다.

그렇기에 정적 팩토리 메서드를 쉽게 알아볼 수 있게 하기 위해,
메서드 이름을 지을 때 나름의 Soft Rule이 존재한다. (=Naming Convention)


<정적 팩토리 메서드의 네이밍 컨벤션 (Naming Convention)>

규칙설명예시
from매개변수를 하나 받아서 해당 타입의 인스턴스를 반환
(=형변환 메서드)
Date d = Date.from(instant);
of여러 매개변수를 받아 해당 타입의 인스턴스 반환
(=집계 메서드)
Set faceCards =
EnumSet.of(JACK, QUEEN, KING);
valueOffrom과 of의 더 자세한 버전BigInteger prime =
BigInteger.valueOf(Integer.MAX_VALUE);
Instance,
getInstance
(매개변수를 받는다면) 매개변수로 명시한 인스턴스 반환,
같은 인스턴스임은 보장하지 않음
StackWalker luke =
StackWalker.getInstance(options);
create,
newInstance
instance 혹은 getIntance와 같지만,
매번 새로운 인스턴스를 반환함을 보장.
Object newArray =
Array.newInstance(classObject, arrayLen);
getTypegetInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용.
"Type"은 팩터리 메서드가 반환할 객체의 타입.
FileStore fs =
Files.getFileStore(path);
newTypenewInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용.
"Type"은 팩터리 메서드가 반환할 객체의 타입.
BufferedReader br =
Files.newBufferedReader(path);
typegetType과 newType의 간결한 버전List litany =
Collections.list(legacyLitany)



지금까지 생성자와 정적 팩토리 메서드의 차이점과 그 장단점을 알아보았다.

필자의 코멘트를 마지막으로 글을 마친다.

<Item1 정리>

  • 정적 팩토리 메서드와 public 생성자는 각자의 쓰임새가 있다.
  • 각자의 장단점을 이해하는게 좋다.
  • 하지만 정적 팩토리 메서드를 사용하는게 유리한 경우다 더 많다.
  • 무작성 public 생성자를 사용하는 습관이 있다면 고치자.
profile
IT 지식 공간

2개의 댓글

comment-user-thumbnail
2023년 10월 30일

공부하는데 많은 도움이 됐습니다 정말 감사합니다.

1개의 답글