[Java] consumer + static factory method를 활용한 객체 생성 패턴

mhyun, Park·2022년 5월 21일
1

최근에 한 객체에 관해 Refactoring을 구상하다가 괜찮은 조합을 발견한 것 같아 정리할 겸 포스팅 하려 한다.

문제 상황! - 생성자 조합이 너무 많다

예를 들어 아래와 같이 Image라는 객체가 있다고 가정해보자.
Image는 여러개에 property를 가지고 있고 생성자 오버로딩을 이용하여 객체를 생성하고 있었으며 객체가 생성된 후에도 다양한 usecase에 맞춰 property가 set되는 DTO 객체로 이용되고 있다.

public class Image {
	private Size size;
	private ImageFormat format;
	private ImageMetaData metaData;
	private StrideInfo strideInfo;
	private DebugInfo debugInfo;

	// 1. 생성자
	public Image() {
	}

	// 2. 생성자
	public Image(Size size, ImageFormat format) {
		this(size, format, null, null, null);
	}

	// 3. 생성자
	public Image(Size size, 
		         ImageFormat format,
		         ImageMetaData metaData) {
		this(size, format, metaData, null, null);
	}

	// 4. 생성자
	public Image(Size size, 
		         ImageFormat format,
                 StrideInfo strideInfo) {
		this(size, format, null, strideInfo, null);
	}

	// 5. 생성자
	public Image(Size size, 
		         ImageFormat format,
		         ImageMetaData metaData,
		         StrideInfo strideInfo,
		         DebugInfo debugInfo) {
		this.size = size;
		this.format = format;
		this.metaData = metaData;
		this.strideInfo = strideInfo;
		this.debugInfo = debugInfo;
	}
    
    // 6. deep copy 함수
    public Image copyFrom(Image image) {
    	// deep copy logic or clone()
    }
    
	//... getter + setter
}

어느 곳에서나 그렇듯 처음엔 compact 했지만, 점점 과제가 진행됨에 따라Imageclass는 다양한 property를 가지게 되었을 것이고 이에 따라, property가 추가될 때마다 생성자 조합또한 거듭제곱 꼴로 늘어나버리게 되는 구조를 갖게 되었다. 그래서 갑작스럽게 이런 쓰임새를 갖고 있는 한 객체를 어떻게 Refactoring 하면 좋을까 고민을 하게 되었다.

Idea 1) Java 8 Consumer 사용

가장 먼저 builder pattern를 생각하긴 했지만, builder pattern은 builder를 위한 boilerplate code 코드들로 인해 code line이 길어지기도 하고 불변 객체를 유연하게 생성하는 환경에서 사용하는 것이 더욱 적합하다고 생각해 DTO 객체인 이 문제의 해결책으론 다소 적합하지 않다고 생각했다.

그 다음으로 내 머릿속을 스쳐간 것은 Kotlin의 apply scope function이었다.

kotlin apply()

  • 함수를 호출하는 객체 T를 이어지는 block으로 전달하고 객체 자체인 this를 반환한다.
public inline fun <T> T.apply(block: T.() -> Unit): T { 
	block(); 
    return this 
}

호출하는 객체 자신을 전달하여 block 안에서 별도 초기화 작업을 수행한 후 다시 자신을 return 함으로써 별도의 생성자 조합 필요없이 자유롭게 객체를 생성할 수 있는 것이다. 하지만, Java엔 apply와 같은 scope function을 지원하지 않기에 어떤 방식으로 이런 성격을 코드로 구현할 수 있을까 고민을 했고 Java 8 Functional Interface인 Consumer를 활용하면 좋겠다는 생각을 했다.

Java Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Consumer는 1개의 Type T 인자를 받고 return 값이 없는 Fuctional Interface로 accept()를 이용하여 Consumer를 구현한 객체 자신을 이용할 수 있는 환경을 제공한다.

public static void main(String[] args) {
    Consumer<String> consumer = str -> System.out.println(str.toUpperCase());

    consumer.accept("hello world");
}
> HELLO WORLD

그리고 이 Consumer를 방금 전 Image 예제에 적용을 하게 되면, 다음과 같이 단 한개의 생성자로 property의 모든 생성자 조합을 표현할 수 있게 된다.

public class Image {
	private Size size;
	private ImageFormat format;
	private ImageMetaData metaData;
	private StrideInfo strideInfo;
	private DebugInfo debugInfo;

	// 1. 생성자
	public Image(Consumer<Image> imageConsumer) {
		imageConsumer.accept(this);
	}

    // 2. deep copy 함수
    public Image copyFrom(Image image) {
    	// deep copy logic or clone()
    }

	//... getter + setter
}
Image image = new Image($ -> {
    $.setSize(size);
    $.setFormat(format);
});

Image image = new Image($ -> {
    $.setSize(size);
    $.setFormat(format);
    $.setMetaData(metaData);
});

Image image = new Image($ -> {
    $.setSize(size);
    $.setFormat(format);
    $.setStrideInfo(strideInfo);
});

이렇게 Consumer를 활용함으로써 kotlin apply()와 같은 역할을 제공할 수 있게 되었다.
하지만.... 이 문제엔 추가적으로 아래와 같이 기존 image를 deep copy한 후 새로이 property를 덮는 경우도 있다고 가정해보자.

Image image = new Image();

...

Image newImage = new Image();
newImage.copyFrom(image);
newImage.setStrideInfo(new StrideInfo(size));

이러한 case도 물론 consumer를 통해 만든 생성자를 이용하여 아래와 같이 strideInfo만 따로 set할 순 있지만,
deep copy를 해야하는 상황으로 인해 코드가 지저분해지게 되기에 이러한 case도 포용할 수 있는 방법을 생각해야만 했다.

Image image2 = new Image($ -> {
    $.setSize(new Size(image.getSize().getWidth(), image.getSize().getHeight()); // deep copy
    $.setFormat(image.getFormat());			// deep copy 생략
    $.setMetaData(image.getMetadata());		// deep copy 생략
    $.setDebugInfo(image.getDebugInfo());	// deep copy 생략
    $.setStrideInfo(new StrideInfo(size));
});

Idea 2) static factory method 적용

그래서 난, 이전에 포스팅 했기도 하지만 (생성자 대신 정적 팩토리 메소드를 고려하라)
지금과 같이 다양한 객체 생성 case를 가지는 상황에 정적 팩토리 메소드를 활용하면 좋을 것 같아 한 번 적용 해봤다.

public class Image {
	private Size size;
	private ImageFormat format;
	private ImageMetaData metaData;
	private StrideInfo strideInfo;
	private DebugInfo debugInfo;

	private Image() {}

	public static Image create(Consumer<Image> imageConsumer) {
		Image image = new Image();

		imageConsumer.accept(image);

		return image; 
	}


	public static Image createAfterCopy(Image image, Consumer<Image> imageConsumer) {
		Image image = new Image();

		image.copyFrom(image);
		imageConsumer.accept(image);

		return image; 
	}

    public Image copyFrom(Image image) {
    	// deep copy logic or clone()
    }

	//... getter + setter
}

private 생성자를 통해 규약된 method를 통해서만 생성할 수 있도록 강제했고
static factory method의 가장 큰 장점인 객체 생성에 이름을 부여함으로써 createcreateAfterCopy 를 각각의 이름에 맞는 동작을 통해 객체를 생성했다.
따라서, static facotry method 를 통해 다음과 같이 규격화된 format으로 Image 객체를 생성할 수 있게 되었다.

Image image = Image.create($ -> {
    $.setSize(size);
    $.setFormat(format);
});

Image image = Image.create($ -> {
    $.setSize(size);
    $.setFormat(format);
    $.setMetaData(metaData);
});

Image image = Image.create($ -> {
    $.setSize(size);
    $.setFormat(format);
    $.setStrideInfo(strideInfo);
});

Image newImage = Image.createAfterCopy(image, $ -> {
    $.setStrideInfo(strideInfo);
    $.setDebugInfo(debugInfo);
});

기대 효과

이번 포스팅에선 consumerstatic factory method 조합을 통해 객체 생성 패턴을 만들어 봤는데
consumer를 통해서 객체 생성에 대한 유연성확장성을 가질 수 있었고
static factory method를 통해서 객체 생성에 대한 가독성을 높일 수 있었다.

기존처럼 생성자 오버로딩 이용했을 땐 property가 변경될 때마다 모든 생성자를 신경 쓰고 건드려야 했지만
이 두개의 조합을 통해, property가 무수히 늘어나고 객체 생성 case가 다양해지는 상황에도 해당 case에 맞는 static factory method만 관리하면 되기 때문에 유지보수성을 충분히 높힐 수 있을 것이라 생각한다.

profile
Android Framework Developer

0개의 댓글