정적 팩토리 메소드
정적 팩토리 메서드란 객체 생성의 역할을 하는 클래스 메서드 이다.
정적(static) 이란?
고정적이기에 인스턴스를 생성하지 않아도 사용할 수 있다.
정적 메소드: 인스턴스를 생성하지 않아도, 호출 메소드를 만들 수 있다.
final과 헷갈릴 수 있는데, final은 "최종적인", 즉 한번 값이 할당되면 "불변" 이라는 특성을 가지게 해준다.
추가적으로, 전역 변수와 정적 변수의 차이점이 헷갈리는 경우가 많아, 둘의 차이점을 정리해보겠다.
Global VS Static
1) 전역(global) 변수는 다른 파일에서도 가져다 쓸수 있지만 정적(static) 변수는 해당 파일의 scope안에서만 접근이 가능하다.
2) 초기화 하지 않은 정적(static) 변수의 경우 본문에서 사용하지 않으면 아예 메모리 상에 올라오지 않는다.
3) 정적(static) 객체의 경우 처음 구문이 수행되는 시점에 처음 생성자를 호출하도록 할 수 있다.. 이를 함수화하여 호출을하면 생성자의 호출 시점을 조정하는게 가능해진다.
장점은 아래와 같다.
시작 하기에 앞서, 정적 팩토리 메소드를 사용할시의 장,단점을 쭉 살펴보자.
장점
• 이름을 가질 수 있다. (동일한 시그니처(메소드명 & 파라미터 타입)의 생성자를 두개 가질 수 없다.)
• 호출될 때마다 인스턴트를 새로 생성하지 않아도 된다. (ex: Boolean.valueOf)
• 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. (인터페이스 기반 프레임워크, 인터페이스에 정적 메소드)
• 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. (ex: EnumSet)
• 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. (서비스 제공자 프레임워크)
단점
• 상속을 하려면 public이나 protected 생성하기 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없 다.
• 정적 팩터리 메서드는 프로그래머가 찾기 어렵다
1. 이름을 가질 수 있다.
만약 생성자를 사용 시, 시그니처가 같은 메소드를 생성할 수 없기에, 제한사항이 존재하게 된다.
시그니처(Signature)란?
메서드 시그니처는 자바 프로그래밍 언어에서 메서드 명과 매개변수의 순서, 타입, 개수를 나타낸다.
반환 타입과 exception은 시그니처에 포함되지 않는다.
오버로딩이 컴파일러가 시그니처를 통해 메서드 간의 차이를 식별하기 때문에 상당히 중요하다.
객체는 생성 목적과 과정에 따라 생성자를 구별해서 사용할 필요가 있다. new라는 키워드를 통해 객체를 생성하는 생성자는 내부 구조를 잘 알고 있어야 목적에 맞게 객체를 생성할 수 있다. 하지만 정적 팩토리 메서드를 사용하면 메서드 이름에 객체의 생성 목적을 담아 낼 수 있다.
public static Order primeOrder(Product product) {
Order order = new Order();
order.prime = true;
order.product = product;
return order;
}
public static Order urgentOrder(Product product) {
Order order = new Order();
order.urgent = true;
order.product = product;
return order;
}
정적 팩토리 메서드를 사용하면 해당 생성의 목적을 이름에 표현할 수 있어 가독성이 좋아지는 효과가 있다.
2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.
public class Settings {
.......
private Settings() {}
private static final Settings SETTINGS = new Settings();//private으로 외부에서 생성하지 못하게
public static Settings getInstance() { //외부에서 해당 객체를 사용하려면, static을 통해 정적으로 선언된 인스턴스를 가져온다.
return SETTINGS;
}
}
위의 코드에서 private static final로 생성자에 제한자를 걸어주는 것으로 private으로 접근을 제한하고, final로 1회만 수행되는 것을 보장한다. 즉, Settings는 이제 1개의 인스턴스만 가지는 것이다. 이후 외부에서 Settings 인스턴스를 사용하려면 public static으로 선언된 getInstance()를 통해 생성자에서 만들어둔 인스턴스를 가져오는 것만 가능하다.
실제로 자바에서 정적 팩토리 메소드 패턴을 어떻게 사용했는데 Boolean의 ValueOf()를 통해 확인해보자.
valueOf
@IntrinsicCandidate
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
TRUE, FALSE
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
우선, Boolean의 경우 TRUE, FALSE 라는 2개의 고정적인 객체를 가지고 있기에, 매번 TRUE, FALSE를 사용할 때마다 객체를 생성하는 건 비효율적이다. ValueOf() 메소드는 파라미터로 들어온 boolean 이 true냐 false냐 에 따라 '정적 팩토리 메소드'를 통해 '미리 만들어둔' TRUE, FALSE 인스턴스를 가져와서 사용한다.
이와 같이, 호출시마다 인스턴스를 생성하지 않아도 되는 정적 팩토리 메소드의 특성은 플라이 웨이트 패턴의 근간이 되기도 한다.
+@ 플라이웨이트 패턴과 상용 가능하다.
플라이웨이트(FlayWeight) 패턴
- 객체를 가볍게 만들어 메모리 사용을 줄이는 패턴.
- 자주 변하는 속성(또는 외적인 속성, extrinsit)과 변하지 않는 속성(또는 내적인 속 성, intrinsit)을 분리하고 재사용하여 메모리 사용을 줄일 수 있다
같은 객체가 반복적으로 사용될 때 효과적인 디자인 패턴이다.
같은 객체가 여러번 사용되면, 해당 객체를 매번 새로 만들기 보다, 같은 객체를 재사용하거나, 캐싱해서 굳이 매번 생성&제거의 코스트를 들일 필요가 없어진다.
플라이웨이트 패턴은 자주 사용되는( 잘 변경되지 않는 객체)를 플라이웨이트 팩토리에 모아 놓고, 꺼내서 쓰는 방법이다.
플라이웨이트 패턴 예시
public Class Charactor{
private int familySize;
private int fontSize;//자주 바뀌지 않음
private char value;//자주 바뀐다.
}
위에서, 각 문자는 매 입력마다 바뀔 가능성이 높지만, 폰트의 크기는 보통 자주 바뀌지 않는다.
따라서, 폰트의 크기를 나타내는 familySize, fontSize를 하나의 객체로 묶은 뒤, 캐싱하는 것으로 변경 가능성이 낮은 객체를 굳이 매번 생성, 삭제하지 않는 것으로 비용을 감소시키는 것이다.
public Class Charactor{
private Font font;
private char value;//자주 바뀐다.
}
이제 아래와 같이 font에 대한 객체를 캐싱해주면된다.
public Class Font{
private int familySize;
private int fontSize;//자주 바뀌지 않음
}
이제 아래와 같이 fontFactory라는 저장소에서, key값을 가지고 이미 생성되 있는 객체를 가져오는 것으로, 같은 객체를 새롭게 생성할 필요가 없다.
Charactor c1= new Charactor('val1', 'white',fontFactory.getFont("nanum:12"));
Charactor c1= new Charactor('val2', 'white',fontFactory.getFont("nanum:12"));
Charactor c1= new Charactor('val3', 'black',fontFactory.getFont("nanum:12"));
enum과 같이 자주 사용되는 요소의 개수가 정해져있다면 정적 팩토리 메소드를 통해 해당 개수만큼 미리 생성해놓고 조회(캐싱)할 수 있는 구조로 만드는 것이 좋다.
왜 정적 팩토리 메소드와 상용될까?
- 정적 팩터리 메서드와 캐싱구조를 함께 사용하면 매번 새로운 객체를 생성할 필요가 없어진다.
- 생성자의 접근 제한자를 private으로 설정함으로써 객체 생성을 정적 팩토리 메서드로만 가능하도록 제한할 수 있다는 것이다. 이를 통해 정해진 범위를 벗어나는 생성(예를 들어 Setting은 1개 인스턴스만 유지,로또 번호는 45개 이상 불가라는 제약조건)을 막을 수 있다는 장점을 확보할 수 있다.
public class LottoNumber {
private static final int MIN_LOTTO_NUMBER = 1;
private static final int MAX_LOTTO_NUMBER = 45;
private static Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();
static {
IntStream.range(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
.forEach(i -> lottoNumberCache.put(i, new LottoNumber(i)));
}
private int number;
private LottoNumber(int number) {
this.number = number;
}
public LottoNumber of(int number) { // LottoNumber를 반환하는 정적 팩토리 메서드
return lottoNumberCache.get(number);
}
...
}
ex) 로또 번호는 45개로 고정이다. 로또 1~45 까지 45개의 인스턴스를 정적으로 만들어놓고, 캐싱에 이용한다면 로또 번호를 사용할 때마다 새롭게 만들지 않고, 만들어진 45개를 계속 사용 가능하다. (쓰레드 풀, 커넥션 풀에서 미리 생성해두는 것과 유사한 이점)
3. 리턴타입의 하위 타입(==호환가능한) 리턴 가능
이를 통해 아래와 같은 응용이 가능해진다.
- 인터페이스를 리턴 타입으로 두고 그 구현 클래스를 리턴할 수 있다. -> 인터페이스 기반 프레임워크 사용 가능
- A클래스를 리턴 타입으로 두고, A 클래스의 하위 클래스를 리턴할 수 있다.
예시: 인터페이스를 리턴타입으로 두고 구현체 리턴
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관이 없다는 것이다.
- 예를 들어, EnumSet 클래스는 public 생성자 없이 오직 정적 팩토리 메소드만 제공하는데, 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다.
간단 예시
public class languageServiceFactory{
.......
public static languageService of(String lang){ //languageService는 인터페이스다.
if(lang.equals("ko")){
return new KoreanLangService();//languageService의 구현체(한국어 버전)
}
else{
return new EnglishLangService();//languageService의 구현체(영어버전)
}
}
EnumSet 예시
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);
}
+@
- java 8 부터는 static메소드를 인터페이스에서 선언 가능하다. -> 인터페이스에서 정적 팩토리 메소드를 바로 사용 가능해진다. ( 굳이 정적 팩토리 메소드가 필요하다고 languageServiceFactory와 같은 클래스를 거칠 필요가 없다.)
- 인터페이스에 아무런 제한자가 없으면, 기본 public.
- 클래스에선 제한자 없으면 클래스 레벨의 private으로.
또한 ServiceLoader를 통해, 인터페이스의 구현체를 찾아오는 것으로, 인터페이스(추상화)에만 의존하여 DIP를 지킬 수 있다.
ServiceLoader 예시
package me.whiteship.chapter01.item01;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.ServiceLoader;
public class HelloServiceFactory {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//Loader는 등록 되어있는 구현체중에, helloservice와 같은 타입인걸 가져온다.
//0개 혹은 다수 존재 가능하기에 Iterable타입을 반환한다.
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
//해당 타입의 등록된 구현체가 0개일 수 있다 -> Optional
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h -> {
System.out.println(h.hello());
});
//아래와 같이, 그냥 구현체 자체를 가져와도 문제는 없다. 하지만, ChineseHelloService에 의존적이다!
// HelloService helloService = new ChineseHelloService();
// System.out.println(helloService.hello());
}
여기서 HelloServiceFactory는 HelloService에 대한 어떠한 구현체도 가져오지 않았다. 하지만 ServiceLoarder에 인터페이스를 전달해 해당 타입의 구현체를 가져올 수 있다.
이처럼, 정적 팩토리 메소드를 사용하면 구현체 없이, 인터페이스만 있어도 된다.
인터페이스만 사용하면 어떤 이점이 있을까?
"추상화"
- 특정 구현 클래스(ChineseHelloService() )에 의존적이지 않아도 된다.
HelloServiceFactory 클래스는 구현 클래스를 몰라도 된다. "추상화 된 인터페이스"만 의존해서 원하는 구현체를 가져오는 것.
ex) spring 의 ioc(di) 프레임워크에서 인터페이스를 통해 구현 클래스를 가져올 수 있다.
하지만, 실제 개발에선 보통 ServiceLoader를 사용하기 보단 Spring을 사용하기에, 개념만 알아두자.
인터페이스에 정적 메소드
자바 8부터 인터페이스에 default, static 메소드를 가질 수 있게 되었다.
인터페이스에 default 메소드
• 인터페이스에서 메소드 선언 뿐 아니라, 기본적인 구현체까지 제공할 수 있다.
• 기존의 인터페이스를 구현하는 클래스에 새로운 기능을 추가할 수 있다.
인터페이스에 static메소드
• 자바 9부터 private static 메소드도 가질 수 있다(보통 내부 로직을 간추리를 용도로 사용).
• 단, private 필드는 아직도 선언할 수 없다.
자바8 전에는 인터페이스에 정적 메서드를 선언할 수 없었다. 그렇기 때문에 이름이 “Type”인 인터페이스를 반환하는 정적 메서드가 필요하면, “Types”라는 (인스턴스화 불가인) 동반 클래스를 만들어 그 안에 정의하는 것이 관례였다.
하지만, 자바 8부터는 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 별로 없다. 동반 클래스에 두었던 public 정적 멤버들 상당수를 그냥 인터페이스 자체에 두면 되는 것이다.
하지만 아직 정적 메서드로는 public만 허용하기 때문에 여전히 다른 코드들을 package-private으로 두어야 할 수 있다.
5. 정적 팩토리 메소드를 작성하는 시점에는, 반환할 객체의 클래스가 존재하지 않아도 된다.
이를 통해 A()라는 메소드가 B()라는 메소드를 사용하는데, A()를 작성 중일 때 B()가 존재하지 않는다면, 많은 문제점이 생겨날 것이다. 정적 팩토리 메소드는 "실행 시점" 에만 반환할 객체의 클래스가 존재하면 되기에, 유연하다.
ex) A() 생성 -> B 가없어서 불가 -> B도 생성 안됨...
이런 유연함은 서비스 제공자 프레임워크의 근간이 된다.
서비스 제공자 프레임워크
애플리케이션 코드를 그대로 유지하고, 외적인 요인으로 확장 가능하도록 만드는 방법이다.
대표적인 서비스 제공자 프레임워크로는 Spring, jdbc 등 이 있다.
서비스 제공자 프레임워크는 크게 3가지 핵심 컴포넌트로 구성되어있다.
1) 서비스 제공자 인터페이스 (구현체의 동작을 정의한다.)
2) 서비스 제공자 등록 API (서비스 인터페이스의 구현체를 등록하는 방법)
ex) 스프링에선 @Configuration을 붙인 설정 클래스에서 @bean을 붙인 메소드로 서비스 구현체를 등록한다.
3) 서비스 접근 API (서비스의 클라이언트가 서비스 인터페이스의 인스턴스를 가져올 때 사용하는 API)
ex) 스프링에선 ApplicationContext에서 getBean으로 원하는 클래스의 빈을 가져온다.
리플랙션이란?
public static void main(String[] args){
..........
Class<?> aClass = Class.forName("me.whiteship.hello.ChineseHelloService");
Constructor<?> constructor = aClass.getConstructor();
HelloService helloService = (HelloService) constructor.newInstance();
System.out.println(helloService.hello());
}
클래스로더를 통해 읽어온 클래스 정보(거울에 반사”된 정보)를 사용하는 기술
- 리플렉션을 사용해 클래스를 읽어오거나, 인스턴스를 만들거나, 메소드를 실행하거나, 필드의 값을 가져오거나 변경하는 것이 가능하다.
언제 사용할까?
- 특정 애노테이션이 붙어있는 필드 또는 메소드 읽어오기 (JUnit, Spring)
- 특정 이름 패턴에 해당하는 메소드 목록 가져와 호출하기 (getter, setter)
+@ 객체 생성을 캡슐화할 수 있다.
정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법이기도 하다.
캡슐화란?
데이터의 은닉을 말한다. 여기서는 생성자를 클래스의 메서드 안으로 숨기면서 내부 상태를 외부에 드러낼 필요없이 객체 생성 인터페이스 단순화 시킬 수 있다.
아래 코드를 보자. 웹 어플리케이션을 개발하다보면 계층 간에 데이터를 전송하기 위한 객체로 DTO(Data transfer object)를 정의해서 사용한다.
DTO와 Entity간에는 자유롭게 typeCast가 가능해야 하는데, 정적 팩터리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있다.
public class CarDto {
private String name;
private int position;
pulbic static CarDto from(Car car) {
return new CarDto(car.getName(), car.getPosition());
}
}
// Car -> CatDto 로 변환
CarDto carDto = CarDto.from(car);
위와 같이, static을 통해 정적 팩토리 메소드로 EntityToDTO 로직을 구현하면, 이제
CarDto를 모르는 클래스에서도, CarDto.from()을 통해 엔티티->CarDto 변환 로직을 사용할 수 있다.
만약 정적 팩토리 메서드를 쓰지 않고 DTO로 변환한다면 외부에서 생성자의 내부 구현을 모두 드러낸 채 해야할 것이다.
Car carDto = CarDto.from(car); // 정적 팩토리 메서드를 쓴 경우
CarDto carDto = new CarDto(car.getName(), car.getPosition); // 생성자를 쓴 경우
이처럼 정적 팩토리 메서드는 단순히 생성자의 역할을 대신하는 것 뿐만 아니라, 우리가 좀 더 가독성 좋은 코드를 작성하고 객체지향적으로 프로그래밍할 수 있도록 도와 준다
자, 이제 단점을 알아보자.
단점 1: 정적 팩토리 메소드로 생성자 대체시 private으로 막는 경우 상속이 불가해진다.
public class Settings {
private boolean useAutoSteering;
private boolean useABS;
private Difficulty difficulty;
private Settings() {}
private static final Settings SETTINGS = new Settings();
public static Settings getInstance() {
return SETTINGS;
}
}
위와 같이, 정적 팩토리 메소드로 생성자를 대체할 경우, 보통 private을 통해 외부 접근을 금지 시킨다. ( 정적인 메소드를 굳이 외부에서 접근해서 인스턴스를 변화시킬 이유가 없기에) 근데 이러면, "상속" 을 사용할 수 없게 된다.
만약 '정적 팩토리 메소드'만 사용해서 생성자를 구성한다면, (생성자 1회 사용됨) 상속이 불가능해진다.
해결법: 상속 없이 사용할 메소드를 보유한 클래스를 필드로 가져오기
아래와 같이 상속을 사용하지 않고, 사용하고 싶은 클래스를 필드로 가져와서 해당 기능을 사용하는 것으로 상속을 대체하여 해결할 수 있다.
Child {
Parent parent;
.....
}
단점2: 정적 팩토리 메소드가 인스턴스 생성을 수행한다는 것을 알아보기 힘들다.
생성자가 아니기에, 따로 프로그램 적으로 정적 팩토리 메소드가 "나는 생성자와 같이 인스턴스를 만드는 용도야"와 같이 표기되지 않는다.
하나의 예시로, javadoc을 통한 문서 자동화를 통해 클래스의 Constructor 정보를 보게되면, 생성자만 나오고, 정적 팩토리 메소드는 당연히 나오지 않는다.
이러한 가독성 측면의 문제는 네이밍 컨벤션을 준수하여 해결할 수 있다.
해결법1: 정적 팩토리 메서드 네이밍 컨벤션
- from : 하나의 매개 변수를 받아서 객체를 생성 (ex: 엔티티 -> DTO 변환시 엔티티 1개만 매개변수로)
- of : 여러개의 매개 변수를 받아서 객체를 생성
- getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음. (ex: 싱글톤으로 관리되는 클래스의 인스턴스 반환)
- newInstance | create : 매번 새로운 인스턴스를 생성
- get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
- new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.
사용 예시
아래는 java 기반 스프링 프로젝트에서 entity -> DTO 변환을 수행하는 정적 팩토리 메소드이다.
public static ClubMemberDTO of(ClubMember clubMember, UrlUtil urlUtil) {
try {
if (clubMember == null) return null;
ClubMemberDTO memberDTO = ClubMemberDTO.builder()
.email(clubMember.getEmail())
.name(clubMember.getName())
........
}
해결법 2: javadoc 고려하여 주석 표기하기 [@see, #메소드]
아래는 정적 팩토리 메소드를 통해 정적으로 만들어둔 인스턴스를 반환하는 클래스의 예시이다.
/**
* 이 클래스의 인스턴스는 #getInstance()를 통해 사용한다.
* @see #getInstance()
*/
public class Settings {
........
private Settings() {}
public static Settings getInstance() {
return SETTINGS;
}
....
}
@see를 통해 보라는 표시를 하고, #getInstance() 와 같이, #을 사용해 클래스 자신의 내부에 있는 메소드를 참조할 수 있다. 이와 같이 표기해두면, 주석을 기반으로 문서를 자동으로 작성해주는 javadoc에서도 생성자가 아니어도 명확하게 @see에 표기된 내용을 보여주고, 명시한 getInstacne() 메소드를 보일 수 있다.
'java' 카테고리의 다른 글
java의 동작 원리 및 구조 [JVM의 구조] (0) | 2023.08.02 |
---|---|
[effective java] 생성자에 매개변수가 많다면 빌더를 고려하자 (0) | 2023.07.18 |
[java] 체크, 언체크 예외에 대한 분석과 잘못된 오해 (0) | 2023.07.18 |
[이펙티브 자바] ENUM (0) | 2023.07.15 |