반응형

자바는 가비지 컬렉션이 알아서 메모리를 관리해주기 때문에 C와 같은 언어보다 메모리에 대해 생각하지 않고 일반적으로 코딩을 한다.

 

하지만 자바 애플리케이션을 만들어 실행하다 보면 OOM이 떨어지는 경우가 있다.

즉 어디선가 가비지 컬렉션이 청소할 수 없는 객체들이 쌓여서 메모리 누수 (leak)가 발생한 것이다.

 

아래 Stack을 구현한 pop 메서드 예제를 보자.

	/**
	 * This method only decreases the size of the stack.
	 * The object at the corresponding array index becomes an obsolete reference.
	 */
	public Object pop() {
		if (size == 0) {
			throw new EmptyStackException();
		}

		return elements[--size];
	}

 

Stack의 데이터를 꺼내오면서 Stack의 허용치를 1 증가시키기 위해 인덱스를 감소시켰다.

 

위 코드의 문제점은 무엇일까?

pop을 통해 꺼내온 객체는 아직까지도 Stack의 array가 참조하고 있다. 결국 해당 array 위치에 새로운 객체가 할당되지 않는다면 프로그램이 종료될 때까지 가비지 컬렉션은 해당 객체가 가비지인지 알 수 없다.

 

따라서 위 코드는 아래와 같이 명시적으로 null로 만들어 참조를 제거해야 한다.

	public Object popDoThis() {
		if (size == 0) {
			throw new EmptyStackException();
		}
		Object result = elements[--size];
		elements[size] = null; /* to let gc do its work */

		return result;
	}

 

Stack 예시처럼 자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야 한다.

 

java.util.WeakHashMap : key에 대한 메모리 참조가 없으면 자동으로 데이터를 삭제하는 Map

자바에서는 메모리 누수가 발생할 수 있는 자료구조에 대한 몇 가지 해결책을 제공한다. 첫 번째로 java.util.WeakHashMap이다.

 

WeakHashMap은 Map이므로 Key와 Value를 한쌍의 데이터로 관리한다. 이때 Key에 대한 참조가 더 이상 존재하지 않게 되면, Value를 가져올 수 있는 방법이 없다고 판단하여, 해당 Key-Value 쌍은 자동으로 삭제되는 Map이다.

 

아래 예제를 보자.

	/**
	 * We put object reference into a cache and forget that we put it there.
	 * To solve this problem we often implement caches using WeakHaspMap.
	 * A WeakHashMap will automatically remove value when its key is no longer referenced.
	 */
	public static void main(String[] args) {
		WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<Integer, String>();

		Integer key = new Integer(1);
		weakHashMap.put(key, "1");
		key = null;

		// If GC is generated, the output changes to {}.
		while (true) {
			System.out.println(weakHashMap);
			System.gc();
			if (weakHashMap.size() == 0) {
				break;
			}
		}

		System.out.println("End");
	}

Key가 1의 값을 가진 Integer 객체이고, Value를 "1" 로하여 WeakHashMap에 put 하였다. 이후, Key값인 Integer의 참조를 null로 만들어 더 이상 참조가 일어나지 않도록 하였다. 이후 GC를 발생시키면 Key의 대한 참조가 없다고 판단하여, 쌍이 사라진 예제이다.

 

참고로, String 클래스를 Key로 하는 WeakHashMap을 사용하면 의미가 없다. 왜냐하면 규칙 5에서 설명했듯이 String은 내부적으로 한 번 생성된 String에 대해 Constant Pool에 항상 참조가 존재하기 때문이다.

 

java.util.LinkedHashMap : 가장 오래된 데이터를 처리할 수 있는 Map

java.util.LinkedHashMap은 HashMap가 다르게 데이터를 넣은 순서를 알 수 있다. 순서를 알 수 있으므로 LinkedHashMap은 아래와 같은 특별한 메서드를 제공한다.

 

    /**
     * Returns <tt>true</tt> if this map should remove its eldest entry.
     * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
     * inserting a new entry into the map.  It provides the implementor
     * with the opportunity to remove the eldest entry each time a new one
     * is added.  This is useful if the map represents a cache: it allows
     * the map to reduce memory consumption by deleting stale entries.
     *
	 * ....
     *
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns <tt>true</tt>.  If the map was empty prior
     *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   <tt>true</tt> if the eldest entry should be removed
     *           from the map; <tt>false</tt> if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

 

put() 또는 putAll() 메서드를 호출하고 나서 자동으로 호출되는 removeEldestEntry 메서드는 가장 오래된 데이터를 삭제할지를 검사하는 메서드이다.

 

true를 리턴하게 되면 가자 오래된 데이터를 삭제하고, false를 리턴하면 삭제하지 않는다. 디폴트로는 false를 리턴하도록 되어있어 항상 삭제하지 않는다.

 

아래는 Map의 크기가 5인 LinkedHashMap의 예제이다.

	public static void main(String[] args) {
		final int MAX_ENTRIES = 5;
		LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String, String>() {
			@Override
			public boolean removeEldestEntry(Map.Entry eldest) {
				return size() > MAX_ENTRIES;
			}
		};

		linkedHashMap.put("1", "a");
		linkedHashMap.put("2", "b");
		linkedHashMap.put("3", "c");
		linkedHashMap.put("4", "d");
		linkedHashMap.put("5", "e");
		linkedHashMap.put("6", "f"); /* {1=a} disappear and this item will be added. */

		for (Iterator<String> hashitr = linkedHashMap.values().iterator(); hashitr.hasNext();) {
			System.out.print(hashitr.next() + " ");
		}
	}

 

반응형
반응형

자바에서는 new를 사용하여 새로운 객체를 생성한다.

동일한 클래스를 여러 개의 객체를 만들어 사용하는 경우가 많겠지만, 클래스의 용도, 특징에 따라 굳이 객체를 여러 개 만들 필요가 없는 클래스들이 있다.

 

가장 먼저 우리가 자주 사용하는 String이다.

 

String : new vs literal

String 클래스는 두 가지 생성 방식이 있다.

 

1. new

String str = new String("camel");

2. literal

String str = "camel";

 

이 두 가지 방식으로 생성된 String 객체는 기능상으로 다른 점이 있을까? 당연하게도 없다.

하지만 생성된 String 객체가 존재하는 JVM의 위치는 차이가 있다.

 

new를 사용하여 객체를 만들게 되면 어떤 클래스라도 Heap 영역에 객체가 할당된다. 하지만 literal를 사용할 경우 Heap 영역 안에 다른 영역인 String Constant Pool 영역에 String 객체가 할당된다.

 

따라서 아래와 같은 결과가 나타난다.

		String newStr1 = new String("camel");
		String literalStr1 = "camel";

		System.out.println(newStr1 == literalStr1); // false
		System.out.println(newStr1.equals(literalStr1)); // true

기능상으로는 동일하기 때문에 equals는 true를 반환하지만, 객체의 주소 값을 비교하는 ==은 false를 반환한다.

 

그럼 아래의 결과는 어떻게 나올까?

		String newStr1 = new String("camel");
		String newStr2 = new String("camel");

		String literalStr1 = "camel";
		String literalStr2 = "camel";

		System.out.println(newStr1 == newStr2); // false
		System.out.println(literalStr1 == literalStr2); // true

new를 사용하면 항상 객체를 생성하므로 newStr1과 newStr2는 서로 다른 객체이다. 따라서 false가 반환된다.

 

하지만 literal로 선언한 두 값의 비교는 true를 반환하였다. 이유는 무엇일까?

String을 literal로 선언하면 내부적으로 intern() 메서드를 호출하게 된다. intern() 메서드는 주어진 문자열이 String Constant Pool 영역에 존재하는지 먼저 확인하고, 존재한다면 해당 값을 반환하고 없다면 새롭게 생성하기 때문이다.

 

이처럼 String은 특별한 경우가 아니라면 new를 사용하여 객체를 생성하지 말자.

 

정적 팩토리 메서드를 제공하는 변경 불가능 클래스(Immutable Class)

잘 짜인 정적 팩토리 메서드(규칙 1)를 제공하는 변경 불가능 클래스는 정적 팩토리 메서드를 호출하면 이미 만들어진 객체를 반환한다.

// Boolean.java

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

 

따라서 이런 클래스들은 문서를 잘 읽어보고, new를 사용하여 객체를 생성하지 않아도 되는지 확인하는 것이 좋다.

		Boolean newBool = new Boolean(true); // do not
		Boolean bool = Boolean.valueOf(true); // do it

 

변경 가능한 클래스라도 동일한 값을 가진 객체를 계속 사용할 경우

변경 불가능 클래스라면 동일한 값을 가진 객체를 생성할 필요 없이 재사용하면 된다. 하지만 변경 가능한 클래스라면 해당 객체를 다른 영역에서 변경할 수 있으므로, 필요할 때 객체를 자주 생성해서 사용한다.

 

하지만, 변경되지 않고 해당 영역에서만 사용하고 사라지는 변경 가능한 객체가 있을 수 있다.

아래 예를 보자.

 

	public boolean isBabyBoomer() {
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomStart = gmtCal.getTime();

		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomEnd = gmtCal.getTime();

		return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
	}

위 코드는 1946년부터 1964년 사이에 태어난 사람인지 확인하는 메서드이다.

 

위 메서드는 어떠한 파라미터도 받지 않고, 항상 동일한 작업을 한다. 하지만 Date 클래스인 boomStart와 boomEnd에 Date 객체는 메서드가 호출될 때마다 새롭게 생성된다.

 

또한 Calendar 클래스는 객체를 생성하는데 비용이 꽤 소요되는 클래스인데, 마찬가지로 항상 새롭게 객체를 생성한다.

위 코드는 아래와 같이 변경하면 약 250배 개선된다.

	private static final Date BOOM_START;
	private static final Date BOOM_END;

	/**
	 * The static initializer block is called only once.
	 */
	static {
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_START = gmtCal.getTime();

		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_END = gmtCal.getTime();
	}

	public boolean isBabyBoomerDoThis() {
		return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
	}

 

API가 객체를 새롭게 생성하는지, 생성하지 않는지 잘 확인하자

위에서 String의 literal 방식, Boolean의 정적 팩토리 메서드 방식, Calendar의 getInstance(), getTime() 등의 예를 들었다. 이렇게 각각의 API들이 객체를 재사용하는지, 아니면 새롭게 만드는지 분명하지 않을 수 있다. 그렇기 때문에 API의 객체를 생성할 때는 항상 문서를 잘 읽고 개발자가 원하는 방식에 맞게 잘 사용해야 한다.

 

아래 예를 들어보자.

	public static void main(String[] args) {
		Map<String, String> map = new HashMap<String, String>();

		map.put("key1", "val1");
		Set<String> keyset1 = map.keySet();

		map.put("key2", "val2");
		Set<String> keyset2 = map.keySet();

		System.out.println(keyset1 == keyset2); // true

	}

Map의 keySet 메서드는 key값의 목록을 Set 클래스로 반환한다. 위처럼 keyset1을 구하고, 새로운 값을 넣은 후 keyset2를 구했을 때, 일반적으로 생각하기에는 두 값이 서로 다를 것으로 생각한다. 그러나 두 keySet의 값은 동일하다. 

 

그러므로 keySet을 여러 번 호출하여 변수를 여러 개 만들어도 큰일 날 것은 없지만, 쓸데없는 짓이다.

 

다음으로는 많이 알려진 AutoBoxing(자동 객체화)의 문제(?)이다.

AutoBoxing은 기본 자료형(Premitive Type)과 그에 대응하는 객체 클래스와 섞어 사용할 수 있도록 해주는 기능이다.

 

아래 예를 들어보자

	public static void main(String[] args) {
		long start = System.currentTimeMillis();

		Long sum = 0L;
		for (long i = 0; i < Integer.MAX_VALUE; i++) {
			sum += i;
		}
	}

Long 클래스인 sum에 기본 자료형인 i의 값을 더하고 있다. 이때 기본 자료형 i는 AutoBoxing이 되어 자동 객체화된다. 즉 loop를 돌 때마다 객체가 계속 생성된다. 그래서 위의 수행 결과는 글쓴이는 9.988초 소요되었다.

 

위와 같이 짠 개발자가 의도한 게 있는지는 모르겠지만(없을 것이다.) 루프는 기본자료형을 사용하고, 최종적인 결과에서만 Long으로 반환하는 것이 맞을 것이다.

	public static void main(String[] args) {
		long start = System.currentTimeMillis();

		long sum = 0L;
		for (long i = 0; i < Integer.MAX_VALUE; i++) {
			sum += i;
		}
	}

 

반응형
반응형

개인적으로 규칙 3과 규칙 4의 순서가 바뀌는 것이 좋다고 생각함.

 

클래스가 private 생성자만을 가지고 있다면 자신만이 객체를 생성할 수 있기 때문에 규칙 3. 싱글턴 패턴에서 private 생성자를 사용하였다. 또한 싱글턴은 메모리에 올려야 하는 정보(필드)가 있기 때문에 객체를 만들어서 사용한다.

 

싱글턴과는 다르게 메모리에 올려서 사용할 정보들은 없는 Util 클래스들은 객체를 만들 목적의 클래스가 아니다. 따라서 Util 클래스를 작성할 때는 private 생성자를 사용하여 객체 생성을 막는 것이 좋다.

 

아래는 private 생성자를 사용한 Netty의 io.netty.util.internal.StringUtil 소스코드이다.

public final class StringUtil {

    public static final String EMPTY_STRING = "";
    public static final String NEWLINE = SystemPropertyUtil.get("line.separator", "\n");

    public static final char DOUBLE_QUOTE = '\"';
    public static final char COMMA = ',';
    public static final char LINE_FEED = '\n';
    public static final char CARRIAGE_RETURN = '\r';
    public static final char TAB = '\t';
    public static final char SPACE = 0x20;

    private static final String[] BYTE2HEX_PAD = new String[256];
    private static final String[] BYTE2HEX_NOPAD = new String[256];
    private static final byte[] HEX2B;

    ...
    
    // private 생성자를 사용하여 객체 생성을 막는다. 
    private StringUtil() {
        // Unused.
    }

 

private 생성자가 아닌 클래스를 abstract로 선언하여 객체를 생성하지 못하도록 막을 수도 있다. 하지만 해당 클래스는 상속받은 하위 클래스를 만들게 된다면 객체가 결국에 생성되는 것이니 올바른 방법이 아니다.

 

아래는 abstract 클래스로 선언하여 사용한 Spring의 org.springframework.util.StringUtils 소스코드이다.

// abstract로 선언하여 직접적으로 객체는 만들 수 없지만, 상속 받은 하위클래스는 객체 생성이 가능하다.
public abstract class StringUtils {

	private static final String[] EMPTY_STRING_ARRAY = {};

	private static final String FOLDER_SEPARATOR = "/";

	private static final String WINDOWS_FOLDER_SEPARATOR = "\\";

	private static final String TOP_PATH = "..";

	private static final String CURRENT_PATH = ".";

	private static final char EXTENSION_SEPARATOR = '.';
    
    ...

 

/**
 * Spring의 StringUtils는 클래스는 객체 생성을 막기 위해 abstract를 클래스로 만들었지만
 * 하위 클래스가 상속받는다면 객체 생성이 가능
 *
 * @author gwon
 * @history
 *          2021. 10. 24. initial creation
 */
public class ChildSpringStringUtils extends StringUtils {

	public static void main(String[] args) {
		StringUtils stringUtils = new ChildSpringStringUtils();

		stringUtils.isEmpty("abc");
	}
}

 

 

반응형
반응형

싱글턴 패턴은 안티 패턴이라고 주장하는 사람도 많지만 여전히 많은 곳에서 사용되고 있다.

 

싱글턴이란 애플리케이션이 실행 중에 객체를 하나만 가질 수 있는 클래스를 의미한다.

 

싱글턴을 구현하는 방법은 무수히 많으며 이펙티브 자바에서는 세 가지를 설명한다.

 

public static final 변수로 접근 가능한 싱글턴

public class PublicStaticFinalSingleton {
	public static final PublicStaticFinalSingleton INSTANCE = new PublicStaticFinalSingleton();

	private PublicStaticFinalSingleton() {

	}

	public void hello() {
		System.out.println("hello");
	}
}


public class Rule3 {

	public static void main(String[] args) {
		PublicStaticFinalSingleton.INSTANCE.hello();

		new PublicStaticFinalSingleton(); // private 생성자 뿐이므로 컴파일 에러
	}
}

 

getInstance()와 같은 정적 팩토리 메서드로 접근 가능한 싱글턴

public class GetInstanceSingleton {
	private static final GetInstanceSingleton INSTANCE = new GetInstanceSingleton();

	private GetInstanceSingleton() {

	}

	public static GetInstanceSingleton getInstance() {
		return INSTANCE;
	}

	public void hello() {
		System.out.println("hello");
	}
}

public class Rule3 {

	public static void main(String[] args) {
		GetInstanceSingleton.getInstance().hello();

		new GetInstanceSingleton(); // private 생성자 뿐이므로 컴파일 에러
	}
}

 

위 두 방법은 거의 비슷한 방법으로 Eager initialization을 사용한 싱글톤 방식이라고도 한다. 하지만 몇 가지 문제점이 있다.

 

1. 클래스가 로드하게 되면 바로 객체를 생성한다. 만약 해당 싱글톤 클래스가 객체를 생성하는데 많은 시간과 비용이 발생한다면, 애플리케이션이 로드되는데 오래 걸리는 문제가 있을 수 있다. 

 

2. 리플렉션을 사용한다면 생성자가 private여도 호출할 수 있다. 생성자가 두 번 호출되면 예외를 던지도록 처리하여 방어할 수 있다.

 

3. 싱글턴 클래스가 Serializeable이 가능하다면 역직렬화 때마다 새로운 객체가 생성된다. 나중에 포스팅할 내용을 미리 포스팅하여 설명하자면 아래와 같다.

 

/**
 * 싱글턴 클래스는 오직 하나의 객체만 생성되게 하는 클래스이다.
 * 만약 Serializable을 구현한다면 readObject에 의해서 새로운 객체가 만들어지므로 싱글턴 클래스가 아니게 된다.
 *
 * @author gwon
 * @history
 *          2019. 6. 8. initial creation
 */
public class SingletonElvis implements Serializable {
	public static final SingletonElvis INSTANCE = new SingletonElvis();

	private SingletonElvis() {

	}

}

public class Rule77 {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		// 싱글턴으로 미리 구현된 동일한 객체만 사용 가능
		SingletonElvis elvis = SingletonElvis.INSTANCE;

		// 직렬화 후, 역직렬화
		SingletonElvis newElvis = (SingletonElvis) deserialize(serialize(elvis));

		System.out.println(elvis == newElvis); // false
	}
}

위와 같이 싱글톤을 역직렬 화하면 새로운 객체가 생성되어 객체가 2개 이상이 된다.

 

이를 방어하기 위해서는 아래와 같이 역직렬화 후 호출되는 메서드인 readResolve() 메서드를 항상 동일한 객체를 리턴하도록 작성하면 된다.

/**
 * 이를 막기 위해 readResolve 메서드를 구현하면 싱글턴 속성을 만족하게 오직 하나의 객체만 반환하게 하면 된다.
 *
 * @author gwon
 * @history
 *          2019. 6. 8. initial creation
 */
public class SingletonElvisWithReadResolve implements Serializable {
	public static final SingletonElvisWithReadResolve INSTANCE = new SingletonElvisWithReadResolve();

	private SingletonElvisWithReadResolve() {

	}

	// 역직렬화가 끝난 후, 해당 메서드가 호출되므로 항상 동일한 객체(싱글턴)을 반환하도록 함.
	private Object readResolve() {
		return INSTANCE;
	}

}

 

Enum을 사용한 싱글턴

위에서 언급한 리플렉션, 직렬화를 위한 방어 로직을 작성하기 귀찮다면 Enum을 사용하는 것도 좋은 방법이다.

public enum EnumSingleton {
	INSTANCE;

	public void hello() {
		System.out.println("hello");
	}
}

public class Rule3 {

	public static void main(String[] args) {
		EnumSingleton.INSTANCE.hello();
	}
}

Enum은 직렬화가 자동으로 처리되며, 리플렉션을 통한 공격에도 안전하다.

 

위까지가 이펙티바 자바에서 설명하는 싱글톤 내용이다.

 

Lazy Initialization, Lazy Holder

Eager initialization 싱글톤이 있다면 Lazy initialization 싱글톤도 있다. 소스는 아래와 같다.

public class LazyInitializationSingleton {
	private static LazyInitializationSingleton INSTANCE;

	private LazyInitializationSingleton() {}

	public static LazyInitializationSingleton getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new LazyInitializationSingleton();
		}
		return INSTANCE;
	}

	public void hello() {
		System.out.println("hello");
	}
}

Lazy Initialization은 INSTANCE 필드에 바로 초기화하지 않고 getInstance()를 최초로 호출될 때 객체를 생성하는 방식이다. 따라서 호출되지 않는다면 객체 생성이 미루어지므로 애플리케이션이 로드되는데 부담이 없다.

 

하지만 여러 쓰레드가 동시에 getInstance()를 호출할 경우 객체가 두 번 이상 호출될 수 있는 위험이 존재한다. 이를 방지하기 위해 getInstance()에 synchronized 키워드를 사용하여 해결할 수 있지만, 해당 싱글톤이 자주 사용된다면 효율적이지 않다.

 

따라서 이를 해결하는 방법이 Lazy Holder이다.

 

Lazy Holder는 JVM 클래스 로더의 동작 방식을 이용하여 Lazy Initialization를 만족하면서, 동기화 문제도 해결한다.

https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom

public class LazyHolderSingleton {
	private LazyHolderSingleton() {};

	public static LazyHolderSingleton getInstance() {
		return LazyHolder.INSTANCE;
	}

	private static class LazyHolder {
		private static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton();
	}

	public void hello() {
		System.out.println("hello");
	}
}

Lazy Holder의 원리는 다음과 같다.

 

1. LazyHoladerSingleton은 static 필드가 없기 때문에, 즉 초기화할 것이 없으므로 아주 빠르게 클래스가 로드된다.

2. LazyHolder는 LazyHolder가 실행되기 전까지는 로드가 되지 않는다.

3. 어디선가 LazyHoladerSingleton.getInstance()를 호출하면 LazyHolder를 로드를 수행하고 초기화를 진행한다.

4. LazyHolder가 로드되면서 static 필드인 INSTANCE를 초기화할 때는 JVM 원리상 동시에 수행할 수 없다.

5. 따라서 LazyHolder는 멀티스레드에서도 안전하게 LazyHolderSingleton의 생성자를 호출하여 초기화를 완료한다.

반응형
반응형

규칙 1에서는 생성자 대신 정적 팩토리 메서드를 사용을 고려해보자는 규칙이었다.

 

하지만 생성자 뿐만아니라 정적 팩토리 메서드의 인자가 많을 때는 어떤 위치에 어떤 값이 들어가야 하는지 클라이언트가 하나씩 확인하면서 값을 채워줘야 하기 때문에 사용하기 불편한다.

 

따라서 이를 개선하고자 점진적 생성자 패턴(telescoping constructor pattern)이 만들어졌다.

 

점진적 생성자 패턴

점진적 생성자 패턴이란 필수 인자를 받는 생성자를 하나 정의하고, 선택적 인자를 받는 생성자를 여러개 점진적으로 만드는 방식이다.

 

아래는 Netty의 io.netty.handler.proxy.HttpProxyHandler 예시이다.

public final class HttpProxyHandler extends ProxyHandler {


    private final HttpClientCodecWrapper codecWrapper = new HttpClientCodecWrapper();
    private final String username;
    private final String password;
    private final CharSequence authorization;
    private final HttpHeaders outboundHeaders;
    private final boolean ignoreDefaultPortsInConnectHostHeader;
    private HttpResponseStatus status;
    private HttpHeaders inboundHeaders;

    public HttpProxyHandler(SocketAddress proxyAddress) {
        this(proxyAddress, null);
    }

    public HttpProxyHandler(SocketAddress proxyAddress, HttpHeaders headers) {
        this(proxyAddress, headers, false);
    }

    public HttpProxyHandler(SocketAddress proxyAddress,
                            HttpHeaders headers,
                            boolean ignoreDefaultPortsInConnectHostHeader) {
		...
    }

    public HttpProxyHandler(SocketAddress proxyAddress, String username, String password) {
        this(proxyAddress, username, password, null);
    }

    public HttpProxyHandler(SocketAddress proxyAddress, String username, String password,
                            HttpHeaders headers) {
        this(proxyAddress, username, password, headers, false);
    }

    public HttpProxyHandler(SocketAddress proxyAddress,
                            String username,
                            String password,
                            HttpHeaders headers,
                            boolean ignoreDefaultPortsInConnectHostHeader) {
  		...
    }

 

아래와 같이 생성자가 필요한 인자에 따라서 여러개가 존재하기 때문에 많은 인자가 들어있는 하나의 생성자보다는 사용하기가 편한다.

하지만 만약 해당 클래스의 필드가 추가되게 되면, 생성자를 추가로 생성해야 한다는 문제가 있다.

옵셔널 한 필드라면 생성자가 하나만 추가하게 코드를 작성할 수 있지만, 필수로 입력해야 하는 필드라면 여태 만들어 둔 생성자에 모두 추가해야 하므로 고쳐야 하는 부분이 더욱 많아진다.

 

필드 개수에 따라서 생성자를 추가하는 방법이 아닌 다른 방법은 없을까? 다음 대안인 자바빈 패턴(JavaBeans) 패턴을 보자.

 

자바 빈 패턴

자바 빈 패턴은 한국에서 제일 많이 사용하는 패턴이 아닐까 싶다.

 

자바 빈 패턴은 디폴트 생성자로 객체를 만들고 필드의 setter 메서드로 필요한 값을 하나씩 호출하여 채우는 방식이다.

public class User {
	private String name;
	private int age;
	private String addr;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public void setAddr(String addr) {
		this.addr = addr;
	}
}

public static void main(String[] args) {
	User user = new User();
	user.setName("홍길동");
	user.setAge(10);
	user.setAddr("서울");
}

 

점층적 생성자 패턴보다 복잡하지 않기 때문에 생성하기도 쉬우며, 코드 읽기에도 어려움이 없다.

 

하지만 자바 빈 패턴은 요즘(?) 대세인 클래스 생성 규칙을 위반한다.

 

다른 객체 생성 방식과는 다르게 한 번의 함수 호출로 객체 생성을 끝낼 수 없기 때문에 객체의 일관성이 깨질 수 있다.

하나의 함수로 객체를 생성하게 된다면, 해당 객체의 필드에 값들이 유효한지를 보장하도록 코드를 작성할 수 있지만 각각 필드에 대한 setter에서는 그 유효성을 보장하도록 코드를 작성하기 어렵고, 그렇게 한다고 하더라도 잘못 코드를 작성할 경우, 버그를 디버깅하기 어렵다.

 

또한 setter가 존재한다는 것은 값을 변경할 수 있기 때문에 Immutable 클래스를 만들 수가 없다.

 

인자가 많은 생성자, 점진적 생성자 패턴, 그리고 자바 빈 패턴을 단점을 커버할 객체 생성 방법은 없을까? 그 대안이 바로 빌더 패턴(Builder Pattern)이다.

 

빌더 패턴

빌더 패턴은 GOF 디자인 패턴 중에 하나이다. 하지만 여기서 설명하는 빌더 패턴은 GOF에서 설명하는 빌더 패턴을 객체 생성 관점에서 변형에서 사용한 예라고 할 수 있다.

 

빌더 패턴은 객체를 생성할 클래스안에 빌더 클래스를 작성하고, 이 빌더 클래스를 이용하여 객체를 생성한다.

일반적인 빌더 패턴 만드는 순서는 다음과 같다.

 

1. 필수로 입력받아야하는 필드만을 가진 생성자를 작성한다.

2. 옵셔널로 입력받아야하는 필드는 setter로 만들고 추가적으로 자신을 리턴하도록 한다.

3. build() 메서드를 만들고 실제 객체 생성자에 자신을 넘겨 해당 객체를 리턴한다.

NutritionFacts cocaCola = new NutriFacts.Builder(240, 8)
      .calories(100).sodium(35).carbohydrate(30).build();
public class NutritionFacts {
	private final int servingSize;
	private final int servings;
	private final int calories;
	private final int fat;
	private final int sodium;
	private final int carbohydrate;

	private NutritionFacts(Builder builder) {
		servingSize = builder.servingSize;
		servings = builder.servings;
		calories = builder.calories;
		fat = builder.fat;
		sodium = builder.sodium;
		carbohydrate = builder.carbohydrate;
	}

	public static class Builder {
		private final int servingSize;
		private final int servings;

		private int calories = 0;
		private int fat = 0;
		private int sodium = 0;
		private int carbohydrate = 0;

		public Builder(int servingSize, int servings) {
			this.servingSize = servingSize;
			this.servings = servings;
		}

		public Builder calories(int val) {
			calories = val;
			return this;
		}

		public Builder fat(int val) {
			fat = val;
			return this;
		}

		public Builder sodium(int val) {
			sodium = val;
			return this;
		}

		public Builder carbohydrate(int val) {
			carbohydrate = val;
			return this;
		}

		public NutritionFacts build() {
			return new NutritionFacts(this);
		}
	}
}

 

빌더 패턴은 위에 다른 패턴의 단점을 해결하면서 여러 장점이 존재한다.

 

1. 빌더 패턴은 build() 메서드에서 한 번에 객체를 생성하기 때문에 필드의 유효성을 판단할 수 있다.

 

2. build()로 만들어진 객체 자체에 setter을 두지 않도록 하여 Immutable 하게 객체를 생성할 수 있다.

 

3. 빌더 객체는 재사용할 수 있다.

NutriFacts.Builder builder = new NutriFacts.Builder(240, 8)
      .calories(100).sodium(35).carbohydrate(30);
      
NutriFacts n1 = builder.build();

builder.sodium(0);
NutriFacts n2 = builder.build();

 

4. 빌더에 부가적인 기능을 추가 할 수 있다.

public class NutritionFacts {
    ...

	public static class Builder {
    	private static int count = 0;
        
        ...

		public NutritionFacts build() {
			count++;
			return new NutritionFacts(this);
		}
        
	}
}

 

단점으로는 코드의 양이 증가할 수 있으며, 다른 패턴보다는 성능이 약간 떨어진다.

 

하지만 요즘 여러 라이브러리에서 어노테이션만으로 빌더 패턴을 적용할 수 있도록 제공하고 있기 때문에 첫 번째 단점은 쉽게 해결할 수 있다. (lombok 등)

@Builder
public class Member {...}
반응형
반응형

객체를 만드는 방법인 생성자(Constructor) 대신 객체를 리턴하도록 하는 public으로 선언된 메서드인 정적 팩터리 메서드를 사용하는 것이 좋은 점이 많다.

 

정적 팩터리 메서드는 다양한 클래스에서 볼 수 있는데, String 클래스의 valueOf 메서드가 그중 하나이다.

    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

    
    public static String valueOf(char data[]) {
        return new String(data);
    }

 

장점

1. 이름이 있다.

  • 생성자는 이름이 없어 오직 메소드 인자의 시그니처를 보고 파악해야 한다.
  • 정적 팩터리 메서드는 메서드 이름으로 어떤 의미의 객체를 리턴하는지 쉽게 파악할 수 있다.

 

2. 생성자처럼 반드시 새로운 객체를 만들도록 하지 않을 수 있다.

  • 내가 생각하는 가장 큰 장점 중에 하나로, 반드시 새로운 객체를 생성하지 않고, 이미 만들어져 있는 객체를 제공할 수도 있다.
  • 동일한 객체 생성을 요청하는 경우, 이미 만들어 놓은 객체 변수를 리턴할 수 도 있으며 또는 캐시를 사용하여 제공할 수 도 있다.
  • 객체 생성하는데 비용이 클 경우, Immutable 클래스인 경우 사용하기에 좋을 것이다.
    // Boolean code
        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. 생성자처럼 반드시 해당 클래스의 객체를 생성하지 않고, 하위 계층의 클래스를 생성할 수 있다.

  • 생성자는 반드시 자신의 객체만을 리턴한다.
  • 정적 팩터리 메서드는 반환형에 하위 계층 또는 구현체를 리턴할 수 있으므로 더욱 유연하다.
  • java.util.Collections는 다양한 정적 팩터리 메서드가 존재하는데 아래 예제에서는 리턴하는 객체가 Collections에서만 호출 가능한 inner class인 SynchronizedCollection를 생성하여 리턴한다. 이처럼 외부에서는 객체 생성 불가능한 클래스도 정적 메서드 클래스를 통해 리턴할 수 제공할 수 있다.
    package java.util;
    ...
    
    public class Collections {
        ...
    
        public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
            return new SynchronizedCollection<>(c);
        }
    
    
        static class SynchronizedCollection<E> implements Collection<E>, Serializable {
            private static final long serialVersionUID = 3053995032091335093L;
    
            final Collection<E> c;  // Backing Collection
            final Object mutex;     // Object on which to synchronize
    
            SynchronizedCollection(Collection<E> c) {
                this.c = Objects.requireNonNull(c);
                mutex = this;
            }
            
            ...
        }
        
        
        ...
        
    }

 

4. 제네릭을 사용한 객체를 만들 때 편리하다. (JDK  1.7부터는 생성자에서도 지원한다.)

 

단점

1. 정적 팩터리 메서드만 제공하는 클래스라면, 즉 생성자가 private로 되어 있어 생성자를 이용하여 객체를 생성할 수 없다면, 하위 클래스를 만들 수 없다.

 

2. 정적 팩터리 메서드는 일반 메서드와 다른 점이 없으므로 해당 메서드가 정적 팩터리 메서드인지 바로 알 수는 없다. 따라서 문서를 읽어야지 파악 가능하다.

 

 

2번 단점으로 인해, 일반적으로 정적 팩터리 메서드의 네이밍은 아래와 같이 작성한다.

  • valueOf : 주어진 값과 같은 값을 갖는 객체를 반환한다.
        // String.valueOf
        public static String valueOf(Object obj) {
            return (obj == null) ? "null" : obj.toString();
        }​
  • of : valueOf와 동일
        // Optional.of
        public static <T> Optional<T> of(T value) {
            return new Optional<>(value);
        }​
  • getInstance : 일반적으로 이미 존재하는 객체를 반환한다.
    // Singleton example
    public class ExampleClass {
        private static ExampleClass instance = new ExampleClass();
        private ExampleClass() {}
        public static ExampleClass getInstance() {
            return instance;
        }
    }​
  • newInstance : 일반적으로 항상 새로운 객체를 만들어 반환한다.
    // Class.newInstance
    clazz.newInstance()​
  • getType : 특정 클래스에서 Type의 객체를 생성할 경우 사용한다. 일반적으로 이미 존재하는 객체를 반환한다.
  • newType : 특정 클래스에서 Type의 객체를 생성할 경우 사용한다. 일반적으로 항상 새로운 객체를 만들어 반환한다.

 

반응형

+ Recent posts