우선 들어가기에 앞서 정적 팩토리 메서드는 디자인 패턴은 아니지만, 비슷한 결을 가지고 있다. 이러한 디자인 패턴들을 공부할때는 개인적으로 해당 디자인패턴의 장점과 단점을 파악하는 공부 방법이 가장 좋은 것 같다. 이해하기가 더 쉬운듯
(1) 정적 팩토리 메서드란?
보통 객체를 생성하는 방법은 "생성자"를 통해 객체를 생성 하는 방법이 있다. 하지만, 이번에 알아 볼 방법은 메서드를 활용해 객체를 생성하는 방법이다. 상황에 따라서 잘 사용하면 유지보수에 매우 좋다. 참고로 정적 팩토리 메서드는 디자인 패턴이 아닌 객체를 생성하는 방법 중 하나이다.
1. 정적
- 자바에서 static 키워드를 의미하며, static 키워드는 고정된 메모리 공간을 갖는다.
- 클래스가 메모리에 로드될 때 한 번만 생성이 되며, 인스턴스화 된 모든 객체에서 공유가 된다.
- 메모리에 올라갈 땐 static 필드 및 메서드가 클래스 레벨로 올라가게 된다.
- 인스턴스를 생성하지 않고도 클래스 이름으로 접근할 수 있다.
2. 팩토리
- 객체를 생성하는 생산자(공장)의 역할을 의미한다.
- 더나아가 객체를 직접 생성자를 호출해 생성하지 않고 중간에 생성하는 메서드나 클래스로 생성하는 것을 의미한다.
- 객체 생성의 책임을 분리하여 맡기는 것을 의미한다.
3. 메서드
- 메서드는 프로그램의 특정 동작을 정의하는 코드 블록이다.
- 메서드는 함수의 성질을 가지고 있고, 입력값을 받을 수 있고, 결과값을 반환할 수 있다.
(2) 정적 팩토리 메서드 장점
1. 이름을 가질 수 있다.
- 일반 생성자는 이름을 가질 수 없어 생성자 자체 만으로는 반환 될 객체의 특성을 제대로 설명하지 못한다.
- 반면 정적 팩토리 메서드는 이름을 가지는 메서드를 통해 객체를 생성 후 반환하기 때문에 한 눈에 알아보기 쉽다.
- 결과적으로는, 메서드를 활용해 객체 생성의 명확한 의미를 부여함으로써 유지보수에 장점이 있어 진다.
public class User {
private String name;
private User(String name) {
this.name = name;
}
// 정적 팩토리 메서드 - createUser와 같은 명확한 이름을 가질 수 있음.
public static User createUser(String name) {
return new User(name);
}
}
2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
정적 팩토리 메서드를 활용하면, 생성자를 호출 하기 전 메서드 블록 내부에서 추가적인 로직을 구현할 수 있게 된다. 이 점을 활용해, 인스턴스가 이미 생성이 되어 있는 경우, 기존의 인스턴스를 반환해 코드의 재활용성을 높이며 객체 생성 비용을 줄일 수 있게 된다.
* 정적 팩토리 메서드는 호출될 때마다 새로운 인스턴스를 반드시 생성하지 않아도 된다는 의미이다.
예시 코드 (1) - 정적 팩토리 메서드 활용
public class Color {
private final int red;
private final int green;
private final int blue;
private static final Map<String, Color> cache = new HashMap<>();
private Color(int red, int green, int blue) {
this.red = red;
this.green = green;
this.blue = blue;
}
public static Color of(int red, int green, int blue) {
String key = red + "," + green + "," + blue;
// containsKey() -> key값과 같은 값의 존재 여부 파악 / return true & false
if (!cache.containsKey(key)) { // false -> 객체가 없는 경우
cache.put(key, new Color(red, green, blue)); // 객체 생성
}
return cache.get(key); // key에 맞는 객체 반환
}
}
해당 Color 클래스는 of() 메서드를 통해서만 인스턴스를 생성 할 수 있으며, of 메서드를 호출 할 때 key 값이 Map에 없는 경우에는 새로운 객체를 생성하고, 있는 경우에는 객체를 생성하지 않고 기존 key 값의 해당하는 객체를 반환하는 코드이다.
* 이렇게 정적 팩토리 메서드를 활용하면 메서드 내에서의 조건을 통해 불필요한 객체 생성을 없앨 수 있게 된다.
예시 코드 (2) - Boolean 래퍼 클래스
- 필요에 따라 같은 인스턴스를 호출하기 때문에 메모리를 아낄 수 있음.
- Boolean 래퍼 클래스는 정적 팩토리 메서드와 정적 상수를 통해서만 객체 생성이 가능하다.
public class Main {
public static void main(String[] args) {
Boolean a = Boolean.valueOf(true); // valueOf 정적 팩토리 메서드
Boolean b = Boolean.valueOf(true); // valueOf 정적 팩토리 메서드
// a == b 주소값이 같다.
// Boolean a = Boolean.TRUE; 상수로도 생성이 가능하다.
}
}
// Boolean.java
@IntrinsicCandidate
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
// Boolean.java
public static final Boolean TRUE = new Boolean(true);
// Boolean.java
@Deprecated(since="9", forRemoval = true)
public Boolean(boolean value) {
this.value = value;
}
- 코드를 보면 main 메서드에서 valueOf() 정적 팩토리 메서드를 호출을 하고 있다.
- 해당 부분의 객체는 생성을 하고 있음에도 참조하는 주소값이 같다.
( 즉, 객체를 새로 만드는 것이 아닌 기존의 객체를 참조하고 있다는 의미이다. )
- valueOf 메서드에서 삼항 연산자를 통해 정적 상수 필드를 호출하고 있다.
- 정적 상수 필드값에는 Boolean 객체를 생성해서 값을 초기화를 해주고 있는 상태이다.
- 이러한 방식으로 이용을 하게 되면, 정적 상수 필드를 통해 올라간 정적 상수는 모든 클래스에서 공유가 되는 공유 변수이기 때문에 객체의 주소값이 같게 나오는 것이다.
그리고 마지막 코드를 보면 Boolean의 생성자가 있는 것이 보일 것 이다. 소스코드를 보면 "생성자가 존재하는데 저걸로 생성하면 되는 것이 아닌가" 라는 생각을 할 수 있을 것이다. 해당 코드를 자세히 보면 위에 @Deprecated 애너테이션이 붙어있는 것을 확인할 수 있다. 해당 애너테이션은 더 이상 자바에서 지원을 하지 않기에, 향후 버전 업데이트에 따라 지워질 수 있는 코드라는 의미이다. 거기에 덫 붙혀 forRemoval 옵션을 통해 컴파일 시점에 다른 애너테이션이 해당 객체를 생성할 시 컴파일 오류를 내보내게 된다.
여기서 한 가지 더 궁금할 수 있다. "그럼 위에서 정적 상수 필드값에는 객체를 생성하고 있는데 애너테이션이 붙어있으면 생성자를 통해 객체를 생성하는 부분에 오류가 나야 하는 것이 아닌가?" 라는 생각을 할 수 있다.
클래스 로딩 시점에 변수 초기화를 먼저 하고 애너테이션이 동작되기 때문에 객체 생성이 아닌 해당 클래스에서 변수에 미리 객체를 초기화를 해둘 수 있게 된다. 그러면 Deprecated 애너테이션을 벗어날 수 있게 된다.
이러한 과정 때문에 Boolean 객체의 생성자를 통해 생성하는 것은 불가능하다.
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
정적 팩터리 메서드는 메서드를 통해서 객체를 생성해 반환하기 때문에 반환 타입을 정해줄 수 있게 된다. 이때, 이 반환 타입은 다형성을 적용해 인터페이스 타입으로 반환을 받을 수 있게 된다. 이러면 유연성과 유지보수성이 매우 높아진다는 장점이 존재한다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
정적 팩터리 메서드에 인자값으로 구분할 수 있는 객체의 정보를 입력하여, 조건식을 통해 해당 매개변수 정보와 일치할 시 그에 맞는 객체를 반환하는 것을 의미한다. 이러한 방식을 통해 매번 다른 클래스의 객체를 반환할 수 있게 된다.
5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
정적 팩터리 메서드 호출 시점에 객체를 동적으로 생성해서 반환할 수 있다는 의미이며, 해당 방식은 유연성과 메모리 효율성을 높일 수 있게 된다. 쉽게 말해, 객체가 필요한 시점인 정적 팩터리 메서드를 호출하는 시점에 객체를 만들어서 제공하기 때문에 불필요한 객체를 미리 생성하지 않아도 되므로 메모리 낭비를 줄일 수 있다는 장점이 있다.
(3) 정적 팩토리 메서드 단점
1. 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없게 된다.
일단 기본적으로 자바에서 상속을 이용하면 하위 클래스는 생성자에서 super() 키워드를 통해 상위 클래스의 생성자를 내부에서 생성을 하고 있게 된다. 이제 이 부분을 기억하고 정적 팩터리 메서드만을 제공하는 클래스가 있다고 가정하자, 생성자는 private일 수 밖에 없을 것이다. 그러면 private 생성자는 상속구조에서 외부에서 생성자를 호출하는 것이기 때문에 오류가 날 것이다. 이러한 문제로 인해 상속이 불가능해진다.
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
생성자을 통해 객체를 생성하는 방식은 개발자가 클래스를 인스턴스화 시키는거구나 라고 명확하게 알 수 있는데 반해 정적 팩터리 메서드를 활용해서 만들면, 메서드이다 보니까 이름이 모호해지면 해당 부분이 객체를 생성하는 용도 인지 다른 작업을 하는 메서드인지 구분이 어려워질 수 있다.
( 이러한 부분을 보완하기 위해 메서드 이름에 의미를 부여해 객체 생성 목적임을 명확히 전달하는 것이 중요하다. )
(4) 결론
- 정적 팩토리 메서드는 static 메서드를 의미한다.
- 객체를 static 메서드에서 생성을 하는 것이다.
- 주로 생성하고자 하는 클래스 내부에 static 메서드가 정의 되어 있음.
- 결론은 뭐가 되었든 간에 객체 생성은 new 키워드로 한다.