JAVA

객체 생성

객체 생성 시 가장 많이 사용하는 방법은 아무래도 new 키워드와 생성자를 사용하는 것입니다.

Apple apple = new Apple(...);
Banana banana = new Banana(...);

그런데 인터넷 강의에서도 그렇고 Effective_Java 도서에서도 생성자를 통한 객체 생성은 피하라고 합니다. 

그럼 대안이 무엇이 있을까요?

 

1. 정적 팩토리 메서드 패턴

생성자를 접근제어자를 사용해 감춘 다음 정적 팩토리 메서드를 통해 생성하는 방식입니다.

class Apple{
    private int weight;
    
    private Apple(int weight){
        this.weight = weight;
    }
    
    public static Apple makeApple(int weight){
        return new Apple(weight);
    }
}

private 접근제어자를 통해 생성자를 숨긴 다음, makeApple( ) 메서드가 Apple을 생성하고 있습니다.

 

 

이점

이를 통해서 얻을 수 있는 이점은 무엇이 있을까요?

1) 가독성이 좋아진다.

일단 메서드 형태를 사용하면서 얻을 수 있는 첫 번째 이점은 아무 의미가 없어 보이는 new 대신에 직관적인 이름을 사용할 수 있다는 것입니다!

Apple apple1 = new Apple (150, GREEN);
Apple apple2 = makeGreenApple(150);

// 만약 색상을 숫자로 표기하고 있다면..?
Apple apple1 = new Apple (150, 2);
Apple apple2 = makeGreenApple(150);

색상값을 표시하는 GREEN이 있어 반환되는 객체를 파악하는데 문제가 없다지만 과연 색상을 숫자로 표현한다 해도 이를 바로 맞출 수 있을까요?

 

이렇게 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 묘사하지 못합니다.

이런 경우 팩토리 메서드의 이름만 잘 지어도, 객체의 특성을 한눈에 파악할 수 있습니다!

 

예시 : BigInteger.probablePrime( )

 

 

2) 시그니처 제약을 벗어날 수 있다.

메서드 명, 파라미터 순서, 타입, 개수들을 묶어 시그니처라고 부릅니다.

모든 메서드들은 이 시그니처 제약을 받지만 특히 생성자는 이미 메서드 명이 같은것과 다름없기 때문에 시그니처 제약에 조금 더 불리하게 작용을 합니다. (파라미터 요건만 일치하게 되면 시그니처 제약이 걸리기 때문)

그렇기 때문에 정적 팩토리 메서드를 사용하면 조금 더 제약에 자유로울 수 있습니다.

 

 

3) 메모리 최적화

생성하는데 엄청나게 큰 비용이 드는 객체가 있다고 합시다. 더군다나 이 객체는 불변이어서 한번 생성한 쭉 재활용이 가능합니다. 이런 경우 정적 팩토리 메서드를 사용해 불변인 객체를 캐싱하는 것이 좋습니다!

private static final Object REALLY_HEAVY_OBJECT = new Object();

public static Object getObject(){
	return REALLY_HEAVY_OBJECT;
}

이러한 기법을 이른 선언(Eager Initialization)이라고 하는데 주의할 점이 있습니다.

  • 미리 선언을 하는 만큼 자주 재사용이 되는가?
  • 미리 선언을 하는 만큼 무거운가?

이게 미리 선언을 하는 만큼, 애플리케이션을 실행할 때의 성능 감소를 불러일으키는데 이를 감수할 만큼의 이득이 있는지 잘 생각해보셔야 합니다.

 

어찌 되었든 이렇게 반복되는 요청에 같은 객체를 반환할 수 있어, 언제 어느 인스턴스를 살아있게 할지 통제할 수 있습니다. (이를 인스턴스 통제 클래스라 합니다.)

 

예시 : Boolean.valueOf(boolean)

 

4) 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

생성자의 경우에는 자신밖에 반환하지 못하지만, 메서드를 사용하는 경우 다형성으로 인해 자신의 하위 타입 객체를 반환할 수 있습니다!

많은 자료구조의 상위 타입인 Collection을 예로 들어 봅시다.

public static Collection<Integer> makeCollection(int type){
    return switch (type){
        case 1 -> new ArrayList<Integer>();
        case 2 -> new HashSet<Integer>();
        case 3 -> new LinkedList<>();
        default -> throw new IllegalStateException("Unexpected value: " + type);
    };
}

 

이렇게 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 제공합니다.

이를 통해서 파라미터에 따라 매번 다른 클래스의 객체 역시 반환할 수 있습니다.

이 방법 외에도 파라미터의 개수, 타입 등 오버로드를 통해서도 다른 클래스의 객체 반환이 가능합니다.

 

 

5) 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

반환할 때만 그 객체가 있으면 아무 문제없다는 말입니다!

public static Apple getApple(int weight){
    Apple apple = new Apple();
    
    // 기타 작업을 수행한 newApple 생성
    apple = newApple
    
    return apple;	// 처음 생성한 apple과 상관없이 기타 작업에 따라 결과가 달라진다.
}

서비스 구현체를 클라이언트에 제공하는 역할을 프레임워크가 통제해 클라이언트를 구현체로부터 분리해줍니다.

 

 

TMI

참고로 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트와 하나의 선택 컴포넌트로 나뉘게 됩니다.

핵심

  • 서비스 인터페이스 : 구현체의 동작을 정의
  • 제공자 등록 API : 제공자가 구현체를 등록할 때 사용
  • 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용

선택

  • 서비스 제공자 인터페이스 : 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명
    (DI, 서비스 제공자 프레임워크 패턴, Bridge 패턴 등이 있다.)
    • 이 방식은 ServiceLoader가 등장하면서 직접 만들 필요가 없어졌다.

 

대표적인 예시로는 JDBC가 있습니다.

커넥션을 미리 생성했더라도, 드라이버 주소(String)에 따라 불러오는 커넥션이 다릅니다! (H2, MySQL, Oracle,...)

  • Connection : 서비스 인터페이스
  • DriverManager.registerDriver : 제공자 등록 API
  • DriverMAnager.getConnection : 서비스 접근 API
  • Driver :  서비스 제공자 인터페이스

 

단점

단점이 존재하나 정말 사소합니다.

public, protected 생성자 없이 정적 팩토리 메서드만 제공하는 클래스는 상속할 수 없다.

생성자가 없으면 상속이 불가능한 것은 당연합니다. 그래서 편의성 구현체 같은 경우에는 상속이 불가능합니다.

하지만 불변 타입인 경우나 보통 상속 대신 컴포지션을 권장하기 때문에 단점이라 하기에는 조금 애매합니다.

 

 

사용자가 정적 팩토리 메서드가 무엇인지 찾아내는 게 어렵다.

생성자처럼 명확히 드러나있지 않아, 사용자가 직접 정적 팩토리 메서드 방식 클래스를 인스턴스 화할 방법을 알아내야 합니다.(문서화가 중요합니다!)

 

이를 방지하기 위해 명명규칙들이 있는데 참고하시면 됩니다.

명명 규칙 설명
from 매개변수를 하나 받아 해당 타입의 인스턴스를 반환하는 형변환 메서드 / ex: Date d = Date.from(instant)
of 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드 / ex: Set> faceCards = EnumSet.of(JACK, QUEEN, KING)
valueOf From, of의 더 자세한 버전 / ex: BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)
instance, getInstance 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지는 않는다. / ex: StackWalker luke - StackWalker.getInstace(options)
create, newInstance instance, getInstance와 같지만, [매번 새로운 인스턴스를 생성해 반환함을 보장.] / Object newArray = Array.newInstance(classObject, arrayLen)
getType getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때마다 사용. / FileStore fs = Files.getFileStore(path)
newType newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할때 사용
type getType과 newType의 간결한 버전

 

 

 

2. 빌더 패턴

생성자나 정적 팩토리 메서드를 사용하는데 파라미터가 많은 경우 주의를 기울여야 합니다.

  • 같은 타입이나 전혀 엉뚱한 파라미터 값을 입력할 수 있다.

다음 코드를 보면 paramter4와 paramter5의 값이 뒤바뀌어 있습니다. 하지만 안타깝게도 타입은 둘 다 String이기 때문에 컴파일러가 잡아내지 못합니다. 결국 개발자는 어디서 이러한 오류가 생겼는지 전전긍긍할 수밖에 없습니다.

// 과연 몇번째 파라미터가 무슨 값인지 한눈에 알 수 있나???
ToManyParameter createByConst = new ToManyParameter("parameter1", "parameter2",
                "parameter3", "parameter5", "parameter4", "parameter6",
                "parameter7", "parameter8", "parameter9");

 

  • 가독성이 떨어지는 결과를 낳게 된다.

위의 경우와 비슷한 케이스입니다. 몇 번째 파라미터가 무슨 값인지 한눈에 알 수 없어 가독성이 떨어집니다.

이를 해결하기 위해서 두 가지 선택지를 고려할 수 있습니다.

 

1) 자바 빈즈 패턴

기본 생성자로 오브젝트만 생성한 뒤 setter 메서드를 통해 정보를 입력하는 방법입니다.

JavaBeansPattern createBySetter = new JavaBeansPattern();
createBySetter.setParameter1("parameter1");
createBySetter.setParameter2("parameter2");
createBySetter.setParameter3("parameter3");
createBySetter.setParameter4("parameter4");
createBySetter.setParameter5("parameter5");

이렇게 setter 메서드를 사용하게 되면, 실수할 일도 없어지게 되고, 가독성도 높아지게 됩니다.

하지만 여러 단점이 아직까지 해결되지 않습니다.

  • 완전한 정보를 가진 객체를 만들기 위해서 많은 메서드의 호출이 필요하다.
  • 객체가 완전히 생성되기 전까지 일관성이 무너진다. (디버깅이 어려워진다.)
  • 클래스를 불변으로 만들지 못한다. (setter 메서드를 사용해 빈 데이터를 계속해서 수정해야 하기 때문)

 

 

2) 빌더 패턴

필수 파라미터들만 입력하는 생성자, 팩토리 메서드를 호출해 빌더 객체를 만든 뒤 조립하는 형태입니다.

BuilderPattern createByBuilder = new BuilderPattern
        .Builder("parameter1", "parameter2")	// 필수 파라미터만으로 빌더 생성
        .parameter3("parameter3")		// 이후 선택 값들을 입력해 객체 커스텀
        .parameter4("parameter4")
        .parameter5("parameter5")
        .build();
  • 생성자, 팩토리 메서드를 호출해 필수 파라미터가 입력된 빌더 객체를 만든다.
  • 빌더 객체가 제공하는 setter 메서드들로 선택적인 파라미터를 입력한다.
  • 마지막으로 build() 메서드를 통해 객체를 생성한다.
public class BuilderPattern{
    private final String parameter1;
    private final String parameter2;
    private final String parameter3;
    private final String parameter4;
    private final String parameter5;
    
    private BuilderPattern(Builder builder) {
        parameter1 = builder.parameter1;
        parameter2 = builder.parameter2;
        parameter3 = builder.parameter3;
        parameter4 = builder.parameter4;
        parameter5 = builder.parameter5;
    }
    
    static class Builder{
    	// 필수 사항들은 입력
    	private final String parameter1;
    	private final String parameter2;
        
        // 선택 사항들은 기본값으로 초기화
    	private final String parameter3 = "-";
    	private final String parameter4 = "-";
    	private final String parameter5 = "-";
        
        public Builder(String parameter1, String parameter2) {
            this.parameter1 = parameter1;
            this.parameter2 = parameter2;
        }
        
        // setter을 통한 커스텀
        // 계속해서 자기 자신(Builder)을 반환하기 때문에 연쇄적으로 사용이 가능
        public Builder parameter3(String message){
        	parameter3 = message;
            return this;
        }

        public Builder parameter4(String message){
        	parameter4 = message;
            return this;
        }
        
        public Builder parameter5(String message){
        	parameter5 = message;
            return this;
        }
        
        // 마지막으로 BuilderPattern을 반환하는 build() 메서드
        public BuilderPattern build(){
        	return new BuilderPattern(this)
        }
}

parameter3와 같은 setter 메서드를 통해 자기 자신을 계속 반환하며 연쇄적으로 메서드를 사용할 수 있게 만듭니다.

이러한 방식을 플루언트 API 또는 연쇄 메서드라 합니다.

마지막으로 build( ) 메서드를 통해 원 객체(BuilderPattern)의 생성자를 반환하게 됩니다.

 

장단점

이러한 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋습니다. 또한 파라미터들을 동적으로 할당할 수 있기 때문에 (필요 없는 메서드는 안 쓰면 그만) 조금 더 자유롭습니다.

하지만 보셨다시피 빌더 패턴을 만드는 것 자체가 장황할 수 있습니다.

그렇기 때문에 롬복에서 제공하는 @Builder 어노테이션을 고려해보시는 것이 좋습니다!

'JAVA' 카테고리의 다른 글

Arrays.asList( )와 List.of( )  (0) 2021.10.25
박싱 & 언박싱  (0) 2021.08.12