[Effective Java] Item1. 생성자 대신 팩토리 메서드를 고려하라

최강일·2024년 3월 19일
0

Effective Java

목록 보기
2/9

1장. 들어가기

  • 이 책의 대부분의 내용은 2가지 원칙에서 파생된다.(명료성, 단순성)
  • 정해진 동작이나 예측할 수 있는 동작만 수행해야 한다.
  • 컴포넌트는 가능한한 작되, 그렇다고 너무 작아서는 안된다.
  • 코드는 복사되는게 아니라 재사용되어야 한다.
  • 컴포넌트 사이의 의존성은 최소로 유지해야 한다.

이 규칙들을 맹종하진 말아야 하나, 어거야 할때는 합당한 이유가 있어야 한다.

컴포넌트 : 개별 메서드부터 여러 패키지로 이뤄진 프레임워크까지 재사용 가능한 s/w를 칭함(책 한정)

내 생각

동료가 예측가능한 쉬운 코드를 짜야한다고 생각한다.
대부분은 개발을 동료들과 함께 하기에, 협업 개발자들을 배려하여 작성해야한다.
위의 내용처럼 동료 개발자가 모든 소스코드들을 한줄한줄 읽어가면서 파악하는건 너무 비효율적이다.
때문에 메서드명만 보고도 특정 기능 수행을 예상하도록 작성해야 한다.

컴포넌트는 가능한한 작되, 그렇다고 너무 작아서는 안된다. -> "단일책임원칙"
하지만 어디까지가 책임일까? 생각보다 어렵다.
일관된 규칙을 가지고 만들어 나가고, 많이 경험해봐야 알것같다.

2장. 객체 생성과 파괴

객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법을 다룬다.

생성자 대신 static 팩터리 메서드를 고려하라

클래스는 public 생성자 대신 정적 팩터리 메서드를 제공할 수 있다.

public static Boolean valueOf(boolean b){
	return b ? Boolean.TRUE : Boolean.FALSE;
}

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

생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 static 팩터리 메서드는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.

//ex1) 소수 BigInteger를 리턴하는지 인지하기 쉬운것은?
BigInteger(int,int,Random) vs BigInteger.probablePrime(...)

//ex2) name 매개변수를 넣어줘야겠구나
Foo f = new Foo("foo"); vs Foo.withName("foo");

또한 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 public static 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.

private String name;
private String address;

public Foo(String name){
	this.name = name;
}
//생성자와 같은 시그니처
public Foo(String address){
	this.address = address;
}
//static 팩터리 메서드 생성
public static Foo withAddress(String address){
	Foo f = new Foo();
    f.address=address;
    return f;
}

=> 가독성이 올라간다. 같은 시그니처인 생성자는 2개 이상 존재할 수 없다는 제약 조건을 우회할 수 있다.

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

이 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하여 불필요한 객체 생성을 피한다.

//객체 생성 X
public static Boolean valueOf(boolean b){
	return b ? Boolean.TRUE : Boolean.FALSE;
}

불변 클래스 : 인스턴스화 될 때 어떤 일에도 변할 일이 없을 경우 ex) Boolean

=> 불필요한 객체 생성을 통제할 뿐 아니라, 싱글턴 패턴 구현 등 인스턴스화 통제 가능

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

반환할 객체의 클래스를 자유롭게 선택할 수 있는 "유연함"을 가진다.
api를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 api를 작게 유지할 수 있다. api가 작아진 것은 프로그래머가 api를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮아진다는 것이다.
ex) java.util.Collections

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";
	}
}

//main
Grade grade = Grade.of(95);

=> 유연하게 객체를 반환할 수 있어진다. 또한 구현부를 클라이언트에게 감추기에 api를 경량화할 수 있다.

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

오버라이딩을 활용해서 매개변수가 다르면 다른 서브클래스를 반환할 수 있다.

장점3)의 예제 코드를 참고하면, Grade는 점수가 90점 미만이면 B를, 이상이면 A 인스턴스를 반환한다.

=> 유연하게 객체를 반환할 수 있어진다.

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

인터페이스나 클래스가 만들어지는 시점에서 하위 타입의 클래스가 존재 하지 않아도 나중에 만들 클래스가 기존의 인터페이스나 클래스를 상속 받으면 언제든지 의존성을 주입 받아서 사용이 가능하다.
반환값이 인터페이스가 되며 정적 팩터리 메서드의 변경 없이 구현체를 바꿔 끼울 수 있다.

public class TicketStore { 
	/** TicketSeller는 인터페이스이고 구현체가 없음에도 아래와 같은 메서드 작성이 가능하다.**/ 
	public static List<TicketSeller> getSellers(){ 
		return new ArrayList<>(); 
	} 
}

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

static 팩토리 메서드를 제공하면 외부에서 생성자 사용을 제한해야해서, 생성자를 private 형태로 선언한다.
하지만 상속을 하기위해서는 public 또는 protected 형태의 상속자가 필요하다.
다시 말해, static 팩터리 메서드 만을 제공하는 클래스의 경우에는 상속을 통한 확장이 불가하다.

이 제약은 Composition, 불변 타입을 만들려면 이 제약을 지켜야 한다는 점에서 장점으로 받아들일 수 도 있다.

public class OS {

	private String name;

	private OS(){
	}
	
	public static OS fromName(String name)
	{
		OS os = new OS();
		os.name = name;
		return os;
	}
}

Composition
기존 클래스가 새로운 클래스의 구성요소로 쓰인다.

 public class OS{
 	private Linux l;
    private Mac m;
 }

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

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

생성자는 Javadoc 상단에 모아서 보여주지만 static 팩토리 메소드는 API 문서에서 특별히 다뤄주지 않는다.
또한 일반적인 메서드 형태로 일반적인 메서드와 구별하기 쉽지 않다.

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

static 팩터리 메서드의 네이밍 컨벤션

그래서 혼란을 줄이기 위해 관례적인 네이밍을 몇 가지 활용하기도 한다.

from

매개 변수를 하나 받아서 해당 타입의 인스턴스를 반환

Date d = Date.from(instant);

of

여러 매개 변수를 받아 적합한 타입의 인스턴스를 반환

Set<Rank> faceCards = EnumSet.of(Jack,Queen,King);

valueOf

from과 of의 더 자세한 버전

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

instance || getInstance

(매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.

StackWalker luke = StackWalker.getInstance(options);

create || newInstance

instance || getInstance 와 같지만, 새로운 인스턴스의 생성을 보장

Object newArray = Array.newInstance(classObject, arrayLen);

- getType

getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입

FileStore fs = FileStore.getFileStore(path);

newType

newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스의 팩토리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입

BufferedReader br = Files.newBufferedReader(path);

type

getType과 newType의 간결한 버전

List<Complaint> litany = Collections.list(legacyLitany);

Summary

static 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 장단점을 이해하고 사용하는 것이 좋다.
그렇다고 하더라도 static 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

profile
Search & Backend Engineer

0개의 댓글