본문 바로가기

Dev.../플밍 관련 자료

[펌] 어떻게 객체를 생성할 것인가?

어떻게 객체를 생성할 것인가?

객체지향 세계에서 가장 중요한 객체는 어떻게 생성하는 것이 좋을까? 상속보다는 컴포지션이 권장되면서 객체를 생성하는 방법은 더욱 더 중요해지고 있다.

객체를 생성하는 가장 흔한 방법은 public 생성자를 쓰는 것이다. 이 방법을 쓰면 반드시 생성하려는 객체의 클래스 이름이 명시적으로 나오게 되므로 객체 생성이 특정 구현에 완전히 얽매이게 된다. 따라서 이렇게 객체를 생성하면 나중에 다른 구현체로 바꾸려 할 때 문제가 생긴다. 또, 싱글톤(singleton)처럼 객체 수를 제한할 필요가 있다면 public 생성자로 객체를 생성하지 못하게 막아야 한다.

객체 생성은 어떻게 제어할까? 우선 팩토리 메소드라는 방법을 살펴보자. 팩토리 메소드는 단순하게 객체 생성을 책임지는 메소드이다. 이 메소드는 생성하는 객체를 정의한 클래스의 스태틱 메소드일 수도 있고 객체 생성 전담 클래스에 정의된 메소드일 수도 있다. 팩토리 메소드를 쓰면 public 생성자보다 다음과 같은 몇 가지 좋은 점이 있다.

생성자는 클래스와 같은 이름만 가질 수 있지만 팩토리 메소드는 의미있는 이름을 가질 수 있어서 코드를 이해하기 쉬워진다. 예를 들어 소수(素數)일 가능성이 큰 BigInteger 객체를 생성할 때, 생성자인 BigInteger(int, int, Random)보다 팩토리 메소드인 BigInteger.probablePrime를 쓰는 쪽이 훨씬 더 이해하기 쉽다. 또, 생성자를 중복 정의할 때 어쩔 수 없이 같은 시그니처를 가질 수밖에 없어 곤란한 경우가 있지만, 팩토리 메소드를 쓰면 이름만 바꾸면 된다.

팩토리 메소드를 쓰면 특정 시점에 존재하는 객체의 수를 엄격하게 관리할 수 있다. 싱글톤처럼 객체 수가 제한되거나 내용이 동등한 불변 클래스의 객체가 단 하나만 존재하게 하려면, 팩토리 메소드로 객체 생성을 제어해야 한다. 예를 들어, Boolean에는 기본타입 boolean 값을 받아 이에 해당하는 Boolean 객체를 만들어 내는 valueOf라는 팩토리 메소드가 있다. Boolean은 true아니면 false에 해당하는 객체 두 개만 있으면 된다. 따라서, public 생성자로 새로운 객체를 매번 생성할 필요가 없이 객체 두 개를 미리 만들어 놓고 필요할 때마다 이 객체를 제공하면 된다.

	public static Boolean valueOf(boolean b) {		return (b ? Boolean.TRUE : Boolean.FALSE);	}
생성자는 반드시 자신을 정의한 클래스의 객체를 생성해야 하지만, 팩토리 메소드는 리턴타입과 그 하위타입에 해당하는 어떤 객체라도 생성할 수 있다. 다시 말하면, 타입만 맞으면 언제라도 구현체를 마음대로 바꿀 수 있다. 이것은 아주 중요한 특성으로, 팩토리 메소드를 쓰는 가장 큰 이유라고 할 수 있다. 예를 들어, 컬렉션 프레임워크에는 수정할 수 없는 컬렉션(unmodifiable collection), 동기화 컬렉션(synchronized collection)과 같은 20여 개의 편리한 구현 클래스들이 있다. 이 클래스들의 객체는 모두 java.util.Collections에 있는 팩토리 메소드로만 얻을 수 있다. 만약 이 20개 클래스가 모두 외부에 드러났다면 컬렉션 프레임워크는 지금보다 훨씬 복잡했을 것이다. 다행히 팩토리 메소드가 이런 복잡성을 감췄기 때문에 오로지 인터페이스만 알면 모든 구현 클래스를 마음대로 쓸 수 있다. 또, 팩토리 메소드를 쓰면 클라이언트가 리턴받은 객체를 실제 구현 클래스 타입이 아닌 인터페이스 타입으로만 참조하도록 강제할 수 있다는 장점도 있다.

클래스를 만들 때 습관처럼 public 생성자를 만들지 말고 자유롭게 객체가 생성될 필요가 있는지 다시 한 번 생각해 보라. 객체 생성을 제어하고 싶다면 팩토리 메소드를 쓰는 것이 좋다.

모든 생성자를 private로 만들어 외부에서 해당 클래스의 객체를 만들지 못하게 막아야 하는 경우가 있다. 상태가 없는 유틸리티 클래스(java.util.Collections, java.util.Arrays 같은 것들)는 객체를 만들 필요가 없다. 따라서, 이런 클래스의 생성자는 모두 private이고 메소드는 모두 static이다.

싱글톤은 정확히 하나의 객체만 존재하는 클래스로 생성자를 private으로 정의하고 클라이언트가 이 클래스의 유일한 인스턴스에 접근할 수 있도록 public static 필드나 메소드를 제공하는 방식으로 구현한다. 우선 public static 필드를 쓰는 방식을 살펴보자.

	public class JNDIService implements IService	{		...		public static final JNDIService SINGLETON = new JNDIService();				private JNDIService() {			super();			caches = new HashMap();		}		...	}
JNDIService의 private 생성자는 이 클래스가 로딩되는 시점에 단 한 번 호출된다. 클라이언트는 접근할 수 있는 JNDIService 생성자가 없기 때문에 더 이상 이 클래스의 객체를 만들 수 없다. JNDIService 객체는 SINGLETON이 참조하는 객체 하나만 존재한다.

다음으로 public static 메소드를 쓰는 방식을 살펴보자.

	public class JNDIService implements IService	{		...		private static JNDIService singleton = null;				private JNDIService() {			super();			caches = new HashMap();		}			public synchronized static JNDIService getInstance() {			if (singleton == null) {				singleton = new JNDIService();			}				return singleton;		}		...	}
public static 필드를 쓰면 자동으로 JNDIService 객체가 하나 생성된다. 만약 싱글톤을 생성하는 비용이 크다면 이것은 낭비다. 이런 경우에는 public static 메소드 방식을 써서 늦은 초기화(lazily initialization)를 해야 한다. 하지만, 다중 스레드 환경에서 싱글톤을 보장하려면 앞의 코드처럼 반드시 동기화해야 한다. 늦은 초기화도 필요하고 동기화도 피해야 한다면 다음과 같이 보유자 클래스(Initiate-on-demand holder class) 구현 패턴을 써야 한다.

	public class JNDIService implements IService	{		...		// 보유자 클래스		private static class SingletonHolder {			static final JNDIService SINGLETON = new JNDIService();		}				private JNDIService() {			super();			caches = new HashMap();			...		}			// 동기화도 필요 없고 비교도 필요 없다.		public static JNDIService getInstance() {				return SingletonHolder.SINGLETON;		}		...	}
이 구현 패턴은 모든 클래스의 초기화는 이 클래스가 처음으로 쓰이는 순간 이루어진다는 사실에 기초한 것이다. getInstance 메소드를 호출하여 SingletonHolder 클래스의 SINGLETON 필드를 처음으로 읽는 순간 SingletonHolder 클래스가 초기화된다. 따라서, 동기화가 필요 없고 비교도 필요 없다. 아무런 추가비용 없이 늦은 초기화의 장점까지 제공할 수 있는 아주 유용한 구현 패턴이다.

객체를 생성할 때 상황에 따라 여러 가지 전략이 필요하다. 무심코 public 생성자를 만들고 있는 자신을 한 번 돌아보라. 객체 생성은 많은 비용이 들어가는 작업일 수도 있고 시스템의 유연성에 큰 영향을 미치는 작업일 수도 있기 때문에 신중해야 한다.