이 의존하는 클래스를 정적 유틸리티 클래스(아이템 4)로 구현하게 되면 유연하지 않고 테스트 하기 어려운 구조가 된다.
정적 유틸리티의 안좋은 예- 유연하지 않고 테스트 어렵다.
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker() { }
public static SpellChecker INSTANCE = new SpellChecker();
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
싱글톤의 안좋은 예- 유연하지 않고 테스트 어렵다.
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker() { }
public static SpellChecker INSTANCE = new SpellChecker();
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
SpellChecker가 여러 사전을 사용할 수 있도록 하기 위해서 좋지 않은 방법 - 멀티스레드 환경X
public class SpellChecker {
private Lexicon dictionary = ...;
public static void changeDictionary(){
this.dictionary = ...
}
}
클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준 다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
이 자원들을 클래스가 직접 만들게 해서도 안 된다.
대신 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 사용한다.(불변 보장)
이는 의존 객체 주입의 한 형태로, 객체를 생성할 때 의존 객체를 주입해주면 된다. => c.f. Strategy Pattern
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) { // 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨준다
this.dictionary = dictionary;
}
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { }
}
이 패턴의 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다.(Factory Method Pattern)
팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다.
ex) Supplier<T> interface(java8)
Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한해야 한다.
이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.
// 클라이언트가 제공한 팩터리가 생성한 타일(Tile)들로 구성된 모자이크 (Mosaic)를 만드는 메서드
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
예를 들어 어떤 사람의 이름과 생일을 입력해두고 getAge()로 나이를 가져오는 Person 클래스를 만든다고 하자.
package com.example.sypark9646.item5;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class Person {
final String name;
private final LocalDate dateOfBirth;
private final LocalDate currentDate;
public Person(String name, LocalDate dateOfBirth) {
this(name, dateOfBirth, LocalDate.now());
}
public Person(String name, LocalDate dateOfBirth, LocalDate currentDate) {
this.name = name;
this.dateOfBirth = dateOfBirth;
this.currentDate = currentDate;
}
long getAge() {
return ChronoUnit.YEARS.between(dateOfBirth, currentDate);
}
public static void printAge(PersonSupplierConstruct person) {
System.out.println(person.name + " is " + person.getAge());
}
}
위 방법의 경우, getAge ()는 현재 날짜가 아닌 Person 객체가 생성 된 시기를 기반으로 한다.
이 문제는 Supplier <LocalDate>를 사용하면 해결된다. 현재 시간을 Supplier를 이용하여 주입하는 것이다.
package com.example.sypark9646.item5;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.function.Supplier;
public class PersonSupplierConstruct {
final String name;
private final LocalDate dateOfBirth;
private final Supplier<LocalDate> currentDate;
public PersonSupplierConstruct(String name, LocalDate dateOfBirth) {
this(name, dateOfBirth, LocalDate::now);
}
public PersonSupplierConstruct(String name, LocalDate dateOfBirth, Supplier<LocalDate> currentDate) {
this.name = name;
this.dateOfBirth = dateOfBirth;
this.currentDate = currentDate;
}
public long getAge() {
return ChronoUnit.YEARS.between(dateOfBirth, currentDate.get());
}
public static void printAge(PersonSupplierConstruct person) {
System.out.println(person.name + " is " + person.getAge());
}
}
Abstract classes cannot be instantiated, but they can be subclassed.
여기서 나온 예제를 직접 한 번 구현해 보았다.
Shape는 추상 클래스로 두고, Circle과 Rectangle은 이를 extends 해서 구체화 하여 구현할 수 있다.
package com.example.sypark9646.item4;
public abstract class Shape {
protected int x, y;
public Shape() {
System.out.println("Shape 호출");
}
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public abstract String getName();
public void drawCenter() {
System.out.println("x = " + x + ", y = " + y);
}
}
package com.example.sypark9646.item4;
public class Circle extends Shape {
int radius;
public Circle() {
// super(); 묵시적 호출
System.out.println("Circle 호출");
}
public Circle(int x, int y, int radius) {
super(x, y);
this.radius = radius;
}
@Override
public void drawCenter() {
super.drawCenter();
System.out.println("radius = " + radius);
}
@Override
public String getName() {
return "circle" + this.hashCode();
}
}
package com.example.sypark9646.item4;
public class Rectangle extends Shape {
int row, col;
public Rectangle() {
// super(); 묵시적 호출
System.out.println("Rectangle 호출");
}
public Rectangle(int x, int y, int row, int col) {
super(x, y);
this.row = row;
this.col = col;
}
@Override
public void drawCenter() {
super.drawCenter();
System.out.println("row = " + row + ", col = " + col);
}
@Override
public String getName() {
return "rectangle" + this.hashCode();
}
}
추상클래스의 경우 아래와 같이 그냥 Shape 그 자체를 인스턴스화 할 순 없지만,
Circle과 Rectangle은 생성자를 통해 인스턴스화가 가능하고, 이 생성자들을 부르게 되면 상위 클래스 Shape의 생성자를 호출하게 된다.
package com.example.sypark9646.item4;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class AbstractInstantiateTest {
@Test
@DisplayName("추상클래스 인스턴스화 테스트")
public void testInstantiateShape() throws InterruptedException {
Shape circle = new Circle();
Shape rectangle = new Rectangle();
circle.drawCenter();
rectangle.drawCenter();
}
}
위와 같이 Circle, Rectangle 인스턴스를 생성하기 위해서는 반드시 생성자를 호출해야 한다.
하지만 생성자는 상속되지 않고 멤버만 상속된다. 대신 자식 클래스로 인스턴스를 생성하게 되면 부모 클래스의 생성자를 super로 호출한다.
즉, 부모 클래스의 생성자가 호출되어야 자식 클래스를 인스턴스화할 수 있다.
그래서 어떤 클래스의 인스턴스화를 확실히 막는 방법은 private 기본 생성자를 추가하는 것이다.
위에서 제시한 java.lang.Math, java.util.Arrays, 그리고 java.util.Collections 또한 private 생성자로 인스턴스화를 막고 있는 것을 확인할 수 있었다.
public class Collections {
// Suppresses default constructor, ensuring non-instantiability.
private Collections() {}
}
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
}
만약 유틸리티 클래스를 만들게 된다면 아래와 같이 만들면 된다.
public class Utilityclass {
// 기본 생성자가 만들어지는 것을 막는다, 인스턴스화 방지용
private Utilityclass() { throw new AssertionError(); }
}
생성자를 명시적으로 private이니 클래스 바깥에서는 접근하지 못하도록 하고,
꼭 Assertion Error를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 해준다.
즉, 어떤 환경에서도 클래스가 인스턴스화되는 것을 막아 준다.
추가적으로 사용자가 이해하기 쉽도록 생성자가 존재하는데 호출할 수 없다는 내용을 주석으로 달아주면 더 좋다.
이 방식은 상속을 불가능하게 하는 효과도 있다.
Shape 예제에서 볼 수 있듯이 모든 생성자는 명시적이든 묵시적이든 부모 클래스의 생성자를 호출하게 되는데,
이를 private으로 선언 하게 되면 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막혀버리기 때문에 컴파일 타임에 상속이 불가능하다는 것을 알 수 있다.
Abstract class vs Interface
인스턴스화를 막는 방법으로서 private 기본 생성자를 추가하는 예시를 자바 컬렉션에서 찾아보다가 궁금한 점이 생겼다
HashSet의 경우 AbstractSet 추상클래스를 상속하고 있는데, Set 인터페이스또한 implement하고 있다.
그런데 AbstractSet은 Set을 implement하고 있다.
그렇다면 HashSet에서는 Set을 이미 implement하고 있는 AbstractSet만 상속하여 구현하면 될텐데 왜 굳이 두가지를 모두 상속하고 구현한 것일까?
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{...}
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {...}
public interface Set<E> extends Collection<E> {...}
Abstract class가 Interface를 implement하는 이유는 무엇이고 어떻게 동작하는 것일까?
What exactly happens when an abstract class implements an interface? Does this work like inheritance, i.e. all interface methods belong also to the abtract class eventhough they are not implemented in it? Or will only the implemented methods belong to the abstract class? So is there any difference between implements and extends, except that one is used to implement interfaces and the other one is used for inheritance?
이에 대한 답변은 정리하자면 아래와 같다.
if you have an abstract class and implement an interface with it, you have two options for the interface methods.
예를 들어 소리를 낼 수 있는지 여부를 나타내는 인터페이스 CanMakeNoise, 동물을 나타내는 추상클래스 Animal이 있다고 하자
package com.example.sypark9646.item4;
public interface CanMakeNoise {
void makeNoise();
}
1. implement themin the abstract class
package com.example.sypark9646.item4;
public abstract class Animal implements CanMakeNoise {
public abstract void jump();
@Override
public void makeNoise() { // interface 함수 구현
System.out.println("animal noise");
}
}
abstract class에 interface에서 정의한 함수를 구현하면 구체클래스에서는 이를 재 정의할 필요 없다
package com.example.sypark9646.item4;
public class Dog extends Animal implements CanMakeNoise{
@Override
public void jump() {
System.out.println("dog jumps");
}
// @Override
// public void makeNoise() {
// System.out.println("dog noise");
// }
}
물론 makeNoise 함수를 Override하여 구현 해줘도 되긴 하다. 이럴 경우 메소드가 오버라이드 되어서 "dog noise"가 나온다.
2. youleave them abstract, but then some of your more concretechildren need to implementit.
만약 abstract class에서 interface 함수를 구현하지 않고 implement만 한다면
package com.example.sypark9646.item4;
public abstract class Animal implements CanMakeNoise {
public abstract void jump();
}
구체 클래스 Dog에서는 두 abstract 함수를 필수적으로 모두 구현해 주어야 한다.
package com.example.sypark9646.item4;
public class Dog extends Animal implements CanMakeNoise {
@Override
public void jump() {
System.out.println("dog jumps");
}
@Override
public void makeNoise() {
System.out.println("dog noise");
}
}
이 예제의 경우는 Animal은 특정 동물이 어떻게 makeNoise 하는지 알 수 없기 때문에 함수의 구현을 구체 클래스로 남겨 두는 것이 좋다.(2번 방법)
이와 반대로, 인터페이스의 중복된 구현을 추상클래스로 빼서 중복을 방지하는 디자인 패턴이 있을 수 있다.(1번 방법)
타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면, 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.
싱글턴을 만드는 방식
기본적으로 생성자는 private 으로 감춰두고, => 인스턴스 생성 불가, 서브클래스 생성 불가
유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버 를 하나 마련해둔다.
1. Eager Initialization
가장 간단한 형태의 구현 방법으로, 인스턴스를 클래스 로딩 단계에서 객체를 프로그램 시작과 동시에 초기화하는 방법이다.
package com.example.demo.item03;
public class EagerInitializationSingleton {
private static final EagerInitializationSingleton instance = new EagerInitializationSingleton();
private EagerInitializationSingleton() {
if (instance != null) {
throw new InstantiationError("Creating of this object is not allowed.");
}
}
public static final EagerInitializationSingleton getInstance() {
return instance;
}
}
private 생성자는 private static final 필드인 instance를 초기화할 때 딱 한 번만 호출되고, getInstance는 항상 같은 객체의 참조를 반환하므로, EagerInitializationSingleton 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
기본 생성자에서 예외를 리턴하는데, 클라이언트가 자바 리플렉션 API(아이템 65)를 사용하여 private 생성자를 호출할 수도 있다.
이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
이 방법의 장점은 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점이다. 또한, 두 번째 장점은 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다는 점이다(아이템 30). 세 번째 장점은 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다는 점이다. 가령 Singleton::get Instance를 Supplier<Singleton>로 사용하는 식이다(아이템 43, 44).
즉, File System, Database Connection 등 객체 생성에 많은 리소스를 사용하는 싱글톤을 구현할 때는 직접 사용할 때까지 싱글톤 인스턴스를 생성하지 않는 방법이 더 좋다.
//예시 java.util.HashSet
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
...
}
2. Static Block Initialization
Eager Initialization과 유사하지만 static block을 통해서 Exception Handling을 제공한다.
Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하므로, 큰 리소스를 다루는 경우에는 적절하지 않다.
package com.example.demo.item03;
public class StaticBlockInitializationSingleton {
private static StaticBlockInitializationSingleton instance;
private StaticBlockInitializationSingleton() {
if (instance != null) {
throw new InstantiationError("Creating of this object is not allowed.");
}
}
static {
try {
instance = new StaticBlockInitializationSingleton();
} catch (Exception e) {
throw new RuntimeException("Creating of this object is not allowed.");
}
}
public static StaticBlockInitializationSingleton getInstance() {
return instance;
}
}
3. Lazy Initialization > single thread 에서만 사용
앞선 두 방식과는 달리 나중에 초기화하는 방법이다.
public 메소드 getInstance()를 호출할 때 인스턴스가 없는지 확인하고, 없다면 생성한다. 따라서 인스턴스 낭비를 막을 수 있다.
package com.example.demo.item03;
public class LazyInitializationSingleton {
private static LazyInitializationSingleton instance;
private LazyInitializationSingleton() {
if (instance != null) {
throw new InstantiationError("Creating of this object is not allowed.");
}
}
public static LazyInitializationSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializationSingleton();
}
return instance;
}
}
하지만 multi-thread 환경에서 instance==null인 시점일 때 여러 쓰레드가 동시에 getInstance() 를 호출 한다면
thread-safe 하지 않을 수 있다는 치명적인 단점이 있다.
@Test
public void testLazyInitializationSingleton() throws InterruptedException {
int numberOfThreads = 500;
ExecutorService service = Executors.newFixedThreadPool(500);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
HashSet<LazyInitializationSingleton> singletonHashSet = new HashSet<>();
for (int i = 0; i < numberOfThreads; i++) {
service.execute(() -> {
LazyInitializationSingleton lazyInitializationSingleton = LazyInitializationSingleton.getInstance();
singletonHashSet.add(lazyInitializationSingleton);
latch.countDown();
});
}
Assertions.assertEquals(singletonHashSet.size(), 1);
latch.await();
}
lazy initialization singleton 방식만 multi-thread 테스트를 통과하지 못함
4. Thread Safe Singleton
Lazy Initialization의 thread-safe 문제를 해결하기 위한 방법으로, getInstance() 메소드에 synchronized를 걸어두는 방식이다.
synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해 준다.
package com.example.demo.item03;
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {
if (instance != null) {
throw new InstantiationError("Creating of this object is not allowed.");
}
}
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
getInstance() 메소드 내에 진입하는 쓰레드가 하나로 보장받기 때문에 멀티 쓰레드 환경에서도 정상 동작하게 된다.
그러나 synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은경우 성능이 떨어지게 됩니다.
5. Double checked Locking Singleton
synchronized 키워드가 성능상으로 좋지 않기 때문에,
getInstance() 메소드 수준에 lock을 걸지 않고 instance가 null일 경우에만 synchronized가 동작하도록 한다.
package com.example.demo.item03;
public class DoubleCheckingLockingSingleton {
private static DoubleCheckingLockingSingleton instance;
private DoubleCheckingLockingSingleton() {
if (instance != null) {
throw new InstantiationError("Creating of this object is not allowed.");
}
}
public static DoubleCheckingLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckingLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckingLockingSingleton();
}
}
}
return instance;
}
}
6. Bill Pugh Singleton Implementation
Bill Pugh가 고안한 방식으로, inner static helper class를 사용하는 방식이다.
앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식으로,현재 가장 널리 쓰이는 싱글톤 구현 방법이다.
package com.example.demo.item03;
public class InitializationOnDemandHolderIdiomSingleton {
private InitializationOnDemandHolderIdiomSingleton() {
if (instance != null) {
throw new InstantiationError("Creating of this object is not allowed.");
}
}
public static InitializationOnDemandHolderIdiomSingleton getInstance() {
return SingletonLazyHolder.INSTANCE;
}
private static class SingletonLazyHolder {
private static final InitializationOnDemandHolderIdiomSingleton INSTANCE = new InitializationOnDemandHolderIdiomSingleton();
}
}
private inner static class를 두어 싱글톤 인스턴스를 갖게 하는데,
inner class인 SingletonHelper 클래스는 Singleton 클래스가 로드 될 때가 아닌, getInstance()가 호출됐을 때
비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 된다.
(+ synchronized를 사용하지 않기 때문에성능 문제가 해결된다.)
7. Enum 방식
앞선 1~6 방식은 자바 리플렉션 api를 이용하여 싱글톤을 파괴할 수 있다.
package com.example.demo.item03;
public enum EnumSingleton {
INSTANCE;
}
enum 방식은 이와 달리 간결할 뿐만 아니라, 쉽게 직렬화할 수 있고, 제2의 인스턴스를 생성하는 리플렉션 공격도 완벽히 막아준다.
조금 부자연스러워 보일 수는 있으나 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법 일 수 있다.
단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다).
그러나 이 방법 또한 1, 2번과 같이 클래스를 사용하지 않을 경우 메모리 낭비를 한다는 단점이 있다.
싱글턴 클래스의 직렬화
싱글턴 클래스를 직렬화하려면(12장) 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다.
assertSame 은 참조형(Reference Type)인 경우에만 사용해야 한다. 만약 기본형(Primitive Type)을 비교해야 한다면 값을 비교하는 가정문 assertEquals 을 사용해야 한다.
assertSame(Object arg, Object arg2) 의 파라미터 형은 Object 이기 때문에, autoboxing이 일어나서 새로운 객체가 생성되어 같은 값이라도 hashCode 가 다르다 -> false 리턴
추가로, Assert.assertSame(127, 127); // true 자바에서 -128 ~ 127 사이의 값은 미리 저장된 값을 이용하기 때문에 (새로운 객체를 생성하지 않아서) true를 리턴한다
용어
무상태 (stateless) 객체:
인스턴스 변수가 없는 객체
Stateless object is an instance of a class without instance fields (instance variables). The classmayhave fields, but they are compile-time constants (static final).
A very much related term isimmutable. Immutable objects may have state, but it does not change when a method is invoked (method invocations do not assign new values to fields). These objects are also thread-safe.
정적 팩터리 메소드와 생성자는 선택적 매개변수가 많을 떄 적절하게 대응하기 어렵다는 단점이 있다.
만약 있을 수도 없을 수도 있는 필드가 많은 객체가 있다고 가정하자.
1. 옛날에는 이러한 클래스에서 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다.
이 패턴은 필수 매개변수를 받는 생성자와, 선택 매개변수를 하나씩 늘여가며 생성자를 만드는 패턴이다.
=> 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다
class Person {
private String name; // 필수
private int age; // 필수
private String job; // 선택
private String hobby; // 선택
public Person(String name, int age, String job, String hobby) {
this.name = name;
this.age = age;
this.job = job;
this.hobby = hobby;
}
// 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데,
// 어쩔 수 없이 hobby 매개변수에도 ""라는 값을 지정해줘야 한다.
public Person(String name, int age, String job) {
this(name, age, job, "");
}
public Person(String name, int age) {
this(name, age, "", "");
}
}
코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 것이고, 매개변수가 몇 개인지도 주의해서 세어 보아야 한다.
또한 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어 질 수 있다.
클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러 는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 된다(아이템 51).
2. 위 문제점에 대한 대안으로는 자바빈즈 패턴(JavaBeans pattern)이 있다.
매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.
@Setter
class Person {
private String name = ""; // 필수
private int age = 0; // 필수
private String job = ""; // 선택
private String hobby = ""; // 선택
public Person() {
}
}
public class DemoApplication {
public static void main(String[] args) {
Person person=new Person();
person.setName("soyeon");
person.setAge(25);
person.setJob("student");
person.setHobby("exercise");
}
}
자바빈즈 패턴의 경우 장점은 코드가 길어지긴 했지만 인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드가 되었다는 점이다.
하지만 단점은 객체 하나를 만들기 위해 많은 메서드를 호출해야 하며, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다. => 버그가 존재하는 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅이 어려워진다.
점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것이다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변(Item17)으로 만들 수 없으며 스레드 안전성이 없다는 문제도 있다.
3. 마지막 대안은 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattem)[Gamma95]이다.
클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자 또는 정적 팩터리 메서드를 호출해 빌더 객체를 얻는다.
클라이언트는 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
마지막으로 매개변수가 없는 build 메서드를 호출해 드디어 우리에게 필요한 객체를 얻는다.(이 객체는 일반적으로 불변객체이다.)
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어 두도록 한다.
package com.yapp.crew.domain.model;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookMark extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
@Setter(value = AccessLevel.PRIVATE)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
@Setter(value = AccessLevel.PRIVATE)
private Board board;
public static BookMarkBuilder getBuilder() {
return new BookMarkBuilder();
}
public static class BookMarkBuilder { // 정적 멤버 클래스
private User user;
private Board board;
public BookMarkBuilder withUser(User user) {
this.user = user;
return this;
}
public BookMarkBuilder withBoard(Board board) {
this.board = board;
return this;
}
public BookMark build() {
BookMark bookMark = new BookMark();
bookMark.setUser(user);
bookMark.setBoard(board);
return bookMark;
}
}
}
private void saveBookMark(Board board, User user) {
// 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다.(method chaining)
BookMarkBuilder bookMarkBuilder = BookMark.getBuilder();
BookMark bookMark = bookMarkBuilder
.withUser(user)
.withBoard(board)
.build();
user.addBookMark(bookMark);
board.addBookMark(bookMark);
bookMarkRepository.save(bookMark);
}
이때, 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식 (invariant)을 검사하도록 하면 더 좋다. 공격에 대비해 이런 *불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다(Item50).
검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IllegalArgumentException을 던지면 된다(Item75).
빌더 패턴은 계충적= 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하자.
추상 클래스는 추상 빌더를, 구체 클래스(concrete class)는 구체 빌더를 갖게 한다.
다음은 피자의 다양한 종류를 표현 하는 계층구조의 루트에 놓인 추상 클래스다.
용어
불변 vs 불변식
불변(immutable, immutability): 어떠한 변경도 허용하지 않는다는 뜻으로, 주로 변경을 허용하는 가변(mutable) 객체와 구분하는 용도로 쓰인다. ex) String 객체
불변식(invariant): 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 뜻한다. 즉, 변경을 허용할 수는 있으나 주어진 조건 내에서 만 허용한다는 뜻이다.(아이템 50)
ex) 리스트의 크기는 반드시 0 이상 = 만약 한순간 이라도 음수 값이 된다면 불변식이 깨진 것이다.
ex) 기간을 표현하는 Period 클래스에서 start 필드의 값은 반드시 end 필드의 값보다 앞서야 = 두 값이 역전되면 불변식이 깨진 것이다
따라서 가변 객체에도 불변식은 존재할 수 있으며, 넓게 보면 불변은 불변식의 극단적 인 예라 할 수 있다.
public class Person {
private static Person PERSON = new Person();
private Person() { // 외부 생성 금지
}
public static final Person create() { // factory method
return PERSON;
}
}
정적 팩터리 메소드의 장점
1. 이름을 가질 수 있다.
일반 생성자의 경우 매개변수와 생성자 자체 만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.
정적 팩터리 메서드의 경우는 이름을 지으면서 반환될 객체의 특성을 쉽게 묘사할 수 있다.
// BigInteger.probablePrime: 함수 네이밍을 통해 '값이 소수인 Biginteger를 반환한다'는 의미를 잘 전달한다.
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}
또한, 생성자 방식으로 인스턴스를 생성할 경우, 같은 매개변수로는 동일한 생성자를 사용해야하지만
정적 팩터리 메서드는 이름을 가질 수 있으므로 한 클래스에 같은 매개변수의 생성자가 여러개 필요하다면 차이를 잘 드러내는 이름을 지어줌으로써 역할 구분이 가능하다.
2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
*불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용함으로써 불필요한 객체 생성을 피할 수 있다.
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {
// static 자원은 JVM 클래스로더의 초기화 부분에서 할당된다.
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
...
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
같은 객체가 자주 요청되는 상황이고, 이 객체의 생성비용이 크다면 정적팩터리 메소드 방식으로 인스턴스를 생성하도록 한다.
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. - 인터페이스기반 프레임워크(Item20)의 핵심기술
이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 한다.
이를 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
interface Person {
}
class Doctor implements Person {
private Doctor() {
} // 외부 생성 금지
public static final Person create() { // 구체적인 타입을 숨길 수 있다
return new Doctor();
}
}
정적 팩터리 메서드를 사용하는 클라이언트는 얻은 객체를 (그 구현 클래스 가 아닌) 인터페이스만으로 다루게 된다(Item64)
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. (반환 타입의 하위 타입이기만 하면)
정적팩터리 메서드를 사용하면 동적으로 적절한 타입의 객체를 반환할 수 있다.
심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다.
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
// 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
클라이언트는 RegularEnumSet과 JumboEnumSet 클래스의 존재를 모른다.
따라서 RegularEnumSet, JumboEnumSet등을 삭제하거나 다른 클래스들을 추가하더라도 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 모르기 때문에 문제 없다. EnumSet의 하위 클래스이기만 하면 되는 것이다.
5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
ex) JDBC(Java Database Connectivity): 서비스 제공자 프레임워크에서의 제공자(provider)는 서비스의 구현체다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여 , 클라이언트를 구현체로부터 분리해준다.
서비스 제공자 프레임워크의 컴포넌트 3요소
서비스 인터페이스(service interface): 구현체의 동작을 정의
제공자 등록 API (provider registration API): 제공자가 구현체를 등록할 때 사용
서비스 접근 API (service access API): 클라이언트가 서비스의 인스턴스를 얻을 때 사용
그런데 Class.forName으로 드라이버 이름만 호출했는데 어떻게 DriverManager가 커넥션을 만들어서 리턴할 수 있었을까?
1. Class.forName(String name) 메소드에 의해 문자열로 전달되는 "com.mysql.jdbc.Driver"이라는 클래스가 메모리에 로드 된다.
2. 메모리에 로드되면서 "com.mysql.jdbc.Driver" 클래스의static 절이 실행된다. 아래와 같이 DriverManager.registerDriver() 메소드를 통해 자기 자신을 등록한다. 즉, 이러한 이유로 Class.forName("com.mysql.jdbc.Driver") 실행시 JDBC Driver가 자동 등록된다.
자바5 이후는 java.util.ServiceLoader라는 범용 서비스 제공자 프레임워크가 제공되어 프레임워크를 직접 만들 필요가 거의 없어졌다.(Item59).
JDBC는 자바 5 전에 등장한 개념이므로 ServiceLoader를 사용하지 않고 위와 같이 구현되어 있다.
단점
1. 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
상속을 하려면 public이나 protected 생성자가 필요한데, 정적 팩터리 메서드를 사용하는 경우 private 기본생성자를 통해 외부 생성을 막아두기 때문이다. 따라서 정적 팩터리 메서드를 이용하는 대표적인 클래스인 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없 다.
이 제약은 상속보다 컴포지션을 사용(Item18) 하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점이 될 수 있다.
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
생성자처럼 API 설명에 명확히 드러나지 않으므로 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 API 문서를 통해 알아내야 한다.
아래는 정적 팩터리 메서드에서 통용되는 명명 방식이다.
from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> cards = EnumSet.of(JACK, QUEEN, KING);
valueOf: from과 of의 더 자세한 버전
Boolean true = Boolean.valueOf(true);
instance (getlnstance): (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
Calendar calendar = Calendar.getlnstance(zone);
public static Calendar getInstance(TimeZone zone){
return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));
}
create (newlnstance): instance 혹은 getlnstance와 같지만, 매번 새로 운 인스턴스를 생성해 반환함을 보장한다.