흔히 클래스의 인스턴스를 얻는 방법은 public
생성자가 일반적이었음
여기서 생성자와 별도로 정적 팩터리 메소드를 통해서 인스턴스를 얻을 수 있음
이는 클래스의 인스턴스를 반환하는 단순한 정적 메서드임, 예시를 보면 다음과 같음
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
여기서 위의 예시뿐 아니라 그럼 추가적으로 정적 팩터리 메서드 예시로 들 수 있는 것이 무엇이 있을까..?
정적 팩터리 메서드가 말로만 들으면 바로 와닿지 않아서 좀 더 자세하게 알아보고 싶어서 그 내부 코드를 좀 더 들어가봄
직접 내부적으로 들어간 코드로 본다면 valueOf 메서드를 통해서 우리가 흔히 사용하는 생성자를 활용한 방식이 아니라 이미 내부적으로 객체 생성에 대해서 정의가 되어 있기 때문에 단순히 메서드를 통해서 객체 생성을 할 수 있는 것임을 알 수 있었음
그렇다면 이와 유사한 예로 다른 케이스 역시 이렇게 구현될 것 같아 추가로 String에 대해서도 알아봄
int를 String으로 변환하는데 있어서 String객체로 바꾼 것인데 여기서 String에 대해서 생성자에 흔히 생각하는 방식이 아닌 valueOf 메서드를 통해서 위의 Boolean과 같이 변환이 된 것을 알 수 있음
결과적으로 생성자를 직접 생각하면서 한 방식보다 훨씬 간편해진 것인데 이에 대한 여러가지 장점과 단점을 확인해보자
생성자는 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못함, 즉 전통적인 방식의 생성자는 말 그대로 객체를 만드는 역할일 뿐 그 객체의 이름만으로는 특성을 파악하기가 쉽지 않음
이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있음
생성자 방식 = BigInteger(int, int, Random)
vs 정적 팩터리 메서드 방식 = BigInteger.probablePrime
이 중 무엇이 값이 소수인 BigInteger를 반환한다는 의미가 더 큰가?
하나의 시그니처로는 생성자를 하나만 만들 수 있음, 입력 매개변수들의 순서를 다르게 한 생성자를 새로 추가하는 식으로 이 제한을 피해볼 수 있지만 이렇게 하면 각 생성자가 어떤 역할을 하는지 정확하기 기억하기 어려워 엉뚱하게 호출 하거나 읽는 사람도 클래스 설명 문서를 찾아보지 않고는 의미를 알지 못할 것임
이름을 가질 수 있는 정적 팩터리 메서드에는 이런 제약이 없음, 앞서 설명한대로 생성자가 여러 개 필요할 것 같으면 이 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주면 해결이 됨
위에서 본 소수인 BigInteger 반환만 봐도 그렇지 않은가? 이런식으로 상황에 맞게 메서드를 직접 지어줌으로써 단순히 생성자만 증식시키는 방식으로 하는 것보다 훨씬 직관적으로 활용할 수 있음
이 덕분에 불변 클래스(아이템 17)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있음
앞서 본 Boolean.valueOf(boolean)
의 경우 객체를 아예 생성하지 않음, 그래서 같은 객체가 요청되는 상황이라면 성능을 상당히 끌어올려줌(플라이웨이트 패턴(아이템 95)도 이와 비슷한 기법)
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 어는 인스턴스를 살아 있게 할지를 철저히 통제할 수 있음, 이런 클래스를 인스턴스 통제 클래스라고 함
인스턴스를 통제하면 클래스를 싱글턴(아이템 3)으로 만들수도, 인스턴스화 불가(아이템 4)로 만들 수 있음
불변 값 클래스(아이템 17)에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있음(a==b
일 때만 a.equals(b)
가 성립), 열거 타입(아이템 34)은 인스턴스가 하나만 만들어짐을 보장함
인스턴스 캐싱과 플라이웨이트 패턴... 뒤에서 나올 부분이기도 하지만 이번 아이템에서 어떤 것을 마하는 것일까?
그리고 불변 클래스 관련해서도 인스턴스가 단 하나뿐임도 보장한다는 것이...?
캐시라는 개념은 컴퓨터 과학에서 흔히 말하는 부분이었음, 사전적인 정의로 본다면 데이터나 값을 미리 복사해 놓는 임시장소로 볼 수 있음
캐싱은 이러한 캐시라는 작업을 하는 것을 의미함
그럼 위에서 말한 인스턴스 캐싱은 무엇을 의미하는 것일까?
자바에서 참조 타입을 사용하는 Wrapper Class가 존재함, 우리가 흔히 일반적으로 Primitive Type으로 쓰는 int,byte,short,float등을 Wrapper Class로 만든 것임
왜냐하면 컬렉션 사용시 예를 들어서 List를 본다면 여기서 타입을 명시해주기 위한 Generic을 사용함, 하지만 여기서 List<int>
를 사용하면 우리가 생각하는 int형 배열처럼 생기지 않음 Generic안에 들어가는 값으로는 객체만 가능하기 때문에
그래서 여기서 Wrapper Class인 Integer를 사용해서 List<Integer>
를 써야함, 이 Wrapper Class는 어떻게 보면 다양한 라이브러리와 프레임워크의 활용을 위해서 제공하는 것으로 객체로써 사용을 하는 것으로 볼 수 있음
그럼 이 인스턴스 캐싱이라는 것을 다시 보면 인스턴스는 각각의 주소를 가지는데, 이 Wrapper Class로 생성한 값(인스턴스)들은 서로 다른 주소값을 가진다고 생각해서 equals
를 써야할 것 같지만 ==
를 쓸 수 있음, 이는 다시 말해 동일한 주소를 가지고 있다는 것인데 이것이 가능하게 바로 인스턴스 캐싱으로 인한 것임
IntegerCache의 내부 코드를 보면 위와 같이 Integer범위의 기본값(-128~127)이 Integer 인스턴스를 미리 생성하여 cache 배열에 저장하는 것을 볼 수 있음
Integer의 valueOf메서드는 위와 같은데 이를 보면 low와 high사이 값이면 IntegerCache의 cache에 저장된 값을 반환하고 그 외의 경우 새로 Integer 인스턴스를 생성하여 반환하는 것임
캐싱해둔 범위 내의 숫자일 경우 캐싱한 인스턴스를 반환하고 범위 밖의 수일 경우 새로 인스턴스를 생성하여 반환하는 방식임
그렇기 때문에 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있는 것임, 불변 클래스에서
뒤의 아이템에서 살펴볼 것이지만 플라이웨이트 패턴은 어떤 클래스의 인스턴스 한 개만 가지고 여러 개의 가상 인스턴스를 제공하고 싶을 때 사용하는 패턴이라 어떻게 보면 객체를 아예 새로 생성하는 것이 아니기 때문에 같다고 말한 것 같음
그러면 장점 중 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장한다고 하였는데 이는 위에 Wrapper Class에서 입증이 되었음
인스턴스는 각각의 주소를 가지지만 Wrapper Class에서 봤듯이 인스턴스 캐싱을 통해서 같은 주소를 참조하게 할 수 있음
근데 불변 값 클래스 역시 이러한 기법을 사용 할 수 있기 때문에 인스턴스가 단 하나뿐임을 보장을 무조건 할 수 있다고 봄
대표적인 불변 값 클래스도 String, Integer, Boolean등의 Wrapper Class임
반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 엄청난 유연성을 제공함
API를 만들 때 이를 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있음
인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 함
컬렉션 프레임워크의 경우도 정적 팩터리 메서드를 통해 얻음, 이 내부적인 클래스를 공개하지 않고 API외견을 훨씬 작게 유지하고 익혀야 하는 개념의 수와 난이도를 낮춤, 이말은 즉, 우리가 자바에서 List
,Set
,Queue
그리고 세부적으로 ArrayList
,HashSet
등이 들어가 있는 것인데 이를 사용할 때 직관적으로 list.add(i)
list.get(i)
즉 해당 메서드의 역할과 기능을 생각해서 직관적으로 씀, 이런 것이 바로 정적 팩터리 메서드를 활용한 장점으로 볼 수 있다는 것임
결국 정적 팩터리 메서드를 사용하는 클라이언트는 얻은 객체를 인터페이스만으로 다루게 됨
결국 이런 내용은 개발을 하는데 있어서 우리가 당연히 생각하는 부분이었는데 알고보면 이런 내면의 구현과 복잡함이 해결된 것임, 일례로 자바 기반으로 스프링을 통한 서버 개발 혹은 안드로이드 개발을 할 때 우리는 공식문서상에 기재되어 있는 해당 인터페이스와 요소들 메서드만을 가지고 내부적인 것은 알 필요없이 구현을 하고 만들 수 있음
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없음
다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 됨
이는 책에서는 EnumSet
을 들었지만 당장 친숙한 컬렉션 프레임워크만 봐도 그럼
List
하위의 ArrayList
, Vector
, LinkedList
등 어떤 객체를 반환해도 상관이 없음, 우리는 이를 직관적으로 해당 자료구조에 맞게 사용하지 건네주는 객체가 어느 클래스의 인스턴스인지 알고 무조건 처리해야하는 그런 사전작업이 필요가 없음
이러한 유연함을 통해 서비스 제공자 프레임워크를 만드는 근간이 됨
서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 구성됨
서비스 인터페이스 : 구현체의 동작 정의
제공자 등록 API : 제공자가 구현체를 등록할 때 사용함
서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용함
서비스 제공자 인터페이스 : 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명해줌(이 컴포넌트가 쓰이기도 함, 쓰이지 않으면 리플렉션(아이템65) 사용함)
클라이언트가 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있음, 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환함
서비스 접근 API가 바로 유연한 정적 팩터리의 실체임
JDBC에서는 Connection
이 서비스 인터페이스 역할을 하고 DriverManager.registerDriver
가 제공자 등록 API 역할을, DriverManager.getConnection
이 서비스 접근 API역할을, Driver
가 서비스 제공자 인터페이스 역할을 수행함
이를 통해서 DB에 대한 활용에 있어서 자바를 통해서 접근하고 처리를 할 수 있는 역할을 하는 것임
이 외에도 다양한 여러 변형 패턴들이 존재함
컬렉션 프레임워크에서 유틸리티 구현 클래스들은 상속할 수 없다는 이야기임, 이는 모든 컬렉션에 있어서 정렬, 셔플, 탐색과 같은 기능을 즉 컬렉션을 컨트롤하는 클래스에 대해서 상속을 할 수 없다는 이야기임(생성자가 막혀있음, 상속해서 호출하는게 불가능)
근데 이 컬렉션 프레임워크의 경우를 보면 어차피 유틸리티 구현 클래스 자체는 그 용도 자체가 컬렉션에 필요한 작업을 하는 것이라서 상속문제에 있어서는 크게 문제가 되지 않아보임, 하지만 어쨌든 상속해서 처리하는 부분이 막혀있다는 것은 어찌보면 작업이 그만큼 늘어날 수도 있기 때문에 단점으로 볼 수 있을 것 같음
이 부분은 하지만 추후 상속보다 컴포지션을 사용(아이템 18)하도록 유도하고 불변 타입(아이템 17)으로 만들려면 이 제약을 지킬 수 밖에 없음
뒤에서 본다지만 컴포지션이...?
기존 클래스를 확장하는 대신에 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방식인데 이는 단점에서 언급한대로 하위 클래스를 만들 수 없는 부분에서 생길 수 밖에 없는 요소고 어찌보면 아이템 18에서 이를 유도하도록 써 있기 때문에 이를 활용해서 잘 처리할 수 있을 것 같음
이는 API 설명에 명확히 드러나지 않아서 찾기가 어렵다는 것임, 공식 문서만 봐도 생성자의 경우 별도로 처리가 되어 있어서 보기 쉽지만 정적 팩터리 메서드의 경우 일반 메서드와 같이 작성되어 있기 때문에 이를 판단하기 쉽지가 않은 문제가 있음
그래서 정적 팩터리 메서드에 흔히 사용하는 명명 방식들이 정해져 있음, 이를 바탕으로API 문서를 잘 쓰고 널리 알려진 규약을 잘 써야함(이 부분은 책 참고)