본문 바로가기
SPRING

정적 팩토리 메서드(Static Factory Method) 맛보기

by 킹명주 2024. 2. 25.

이번에 프로젝트를 진행하면서 정적 팩토리 메소드를 도입해보았는데, 동료들에게 꽤 좋은 평가를 받아 이번 주제로 글을 작성해보려고 한다..!!

이번 글을 한줄 요약하자면 정적 팩토리 메소드란 무엇이고 도입 배경을 소개하는 것이다.


정적 팩토리 메소드

 

정적 팩토리 메소드는 23개의 디자인 패턴을 수록한 'GoF' Design Patterns에서 소개하고 있는 팩토리 메서드를 응용한 디자인 패턴이라고 생각하면 된다.

 

오호라.. 그렇다면 팩토리 메소드를 이해하지 않고는 정적 팩토리 메소드를 이해하기란 어렵다는 말씀?!

꼭 그렇지는 않지만 완전한 이해를 위해 GoF 디자인 패턴 책에서 소개하고 있는 "팩토리 메서드"를 먼저 알아보고 "정적 팩토리 메서드"에 대해 설명하겠다.

 

팩토리 메서드(Factory Method)

팩토리메서드는 객체 생성을 Sub Class로 분리하여 캡슐화를 보장하는 패턴이다. 이렇게 말하면 좀 어려우니 예시와 같이보자!

interface Product {
    void use();
}
class PaintProduct implements Product {
    @Override
    public void use() {
        System.out.println("페인트를 사용합니다.");
    }
}

class ConcreteProduct implements Product {
    @Override
    public void use() {
        System.out.println("콘크리트를 사용합니다.");
    }
}
abstract class Creator {
    abstract Product createProduct();

    void operate() {
        Product product = createProduct();
        product.use();
    }
}

class PaintCreator extends Creator {
    @Override
    Product createProduct() {
        return new PaintProduct();
    }
}

class ConcreteCreator extends Creator {
    @Override
    Product createProduct() {
        return new ConcreteProduct();
    }
}

public class Main {
    public static void main(String[] args) {
        Creator paintCreator = new PaintCreator();
        paintCreator.operate();

        Creator concreteCreator = new ConcreteCreator();
        concreteCreator.operate();
    }
}

위 예시를 보면,  Product 객체 생성을 해당하는 클래스에서 수행하는게 아니라 Creator에게 넘기고 있다. 또한, 하위 클래스에서 구체적인 객체 생성을 나타내고 있다.

즉, 팩토리 메서드 패턴은 인스턴스 생성 방법은 상위 클래스에서 결정하되 생성될 인스턴스가 구체적으로 어떤 클래스의 인스턴스인지는 하위 클래스에서 결정하도록 하는 디자인 패턴을 말한다.

 

이 패턴을 도입했을 때 얻을 수 있는 명확한 장점은 무엇일까? 바로 객체 생성을 캡슐화한다는 것이다. 또한, new 객체와 다르게 자기 자신만의 이름을 가질 수 있으며 확장성(유연성)도 뛰어나다.

 

정적 팩토리 메서드(Static Factory Method)

팩토리메서드를 이해했다면 정적 팩토리메서드는 누워서 껌먹기이다!! 물론, 팩토리메서드의 캡슐화, 유연성 장점은 유지하지만 사용법에 차이가 있다. 팩토리 메서드는 서브 클래스에서 객체 생성을 구체화하지만, 정적 팩토리 메서드는 보통 클래스 내부에서 정의하여 사용한다. 즉, 정적 팩토리 메서드란 객체 생성의 역할만 하는 클래스 내의 Method라고 생각하면 된다.

 

API를 개발해보신 분들이라면, ResponseEntity를 활용해보신 경험이 있을 것이다. 필자는 주로, 아래와 같이 사용하는 편인데

    @GetMapping
    public ResponseEntity<TestMessageResponse> getTestMessage() {
        return ResponseEntity.status(HttpStatus.OK).body(testService.getTestMessage());
    }

이처럼 ResponseEntity의 정적 팩토리 메서드를 활용하고 있다. (* 아래의 ResponseEneity status method 참고)

	// Static builder methods

	/**
	 * Create a builder with the given status.
	 * @param status the response status
	 * @return the created builder
	 * @since 4.1
	 */
	public static BodyBuilder status(HttpStatusCode status) {
		Assert.notNull(status, "HttpStatusCode must not be null");
		return new DefaultBuilder(status);
	}

 

물론, 정적 팩토리 메서드를 꼭 사용하지 않고 new 객체를 사용할 수도 있다.

@GetMapping
public ResponseEntity<TestMessageResponse> getTestMessage() {
    return new ResponseEntity<>(testService.getTestMessage(), HttpStatus.OK);
}

뭔가 큰 차이가 없어보이는데, 필자는 왜 ResponseEntity의 정적 팩토리 메서드를 사용했을까? 

 

바로, 명확한 함수 이름 때문이다. new 객체로 생성하는 것보다 status와 body를 통해, 어떤 데이터를 활용할 것인지 명확하게 구별할 수 있다.


정적 팩토리 메서드를 도입하게 된 이유

 

그래서 나는 왜 실무에서 정적 팩토리 메서드를 사용했을까?! 인터넷에 찾아보면, 장점이 엄청 많을텐데, 예를 들어 캡슐화, 상속 등등..

이런 장점들이 있어서 도입하게 된 것은 아니다. 도입하게 된 이유에 대해 차근차근 설명하겠다..!

 

1. 코드 중복 제거

    public ExampleResponse example1() {
        return ExampleResponse.builder()
                              .title("킹명주1")
                              .contents("이 사람의 블로그는 1997년... (생략)")
                              .writer("킹명주")
                              .date(LocalDateTime.now())
                              .build();
    }

    public ExampleResponse example2() {
        return ExampleResponse.builder()
                              .title("킹명주2")
                              .contents("이 사람의 블로그는 꿀잼... (생략)")
                              .writer("킹명주 아닙니다.")
                              .date(LocalDateTime.now())
                              .build();
    }

실무에서 하나의 Response를 3개의 함수에서 사용한 경험이 있다. 그러다보니 위의 예제 처럼 Builder로 인해 코드 볼륨이 증가했고 스크롤이 생길 정도로 가독성이 떨어졌다. 그래서 이러한 객체 생성에서 코드 볼륨을 줄일 수 있는 방법을 찾아 보았다.

 

해당 문제점을 정적 팩토리 메서드로 극복할 수 있는데,

package com.practice.toss_java.test.dto;

import lombok.Getter;
import java.time.LocalDateTime;

@Getter
public class ExampleResponse {

    private String title;
    private String contents;
    private String writer;
    private LocalDateTime date;

    private ExampleResponse(String title, String contents, String writer, LocalDateTime date){
        this.title = title;
        this.contents = contents;
        this.writer = writer;
        this.date = date;
    }

    public static ExampleResponse of(String title, String contents, String writer, LocalDateTime date){
        return new ExampleResponse(title, contents, writer, date);
    }
}

 

    public ExampleResponse example1() {
        return ExampleResponse.of("킹명주1", "이 사람의 블로그는 1997년... (생략)", "킹명주", LocalDateTime.now());
    }

    public ExampleResponse example2() {
        return ExampleResponse.of("킹명주2", "이 사람의 블로그는 꿀잼... (생략)", "킹명주 아닙니다.", LocalDateTime.now());
    }

이런식으로 코드 중복을 줄여 가독성을 높였다. 여기서 ExampleResponse의 생성자는 private로 지정하여 다른 곳에서 객체 생성을 하지 못하게 막았다. (객체 생성을 하려면 정적 팩토리 메서드를 이용해야 함)

 

2. 명확한 함수 이름 및 유연성

실무에서 MyBatis를 사용하면서 Optional을 사용한 경험이 있는데, 실패했을 때에도 결과를 클라이언트에게 전송해야 했다. 아래 정적 팩토리 메서드를 도입하기 전 코드를 살펴보자.

    public ExampleResponse example() {
        ExampleResponse exampleResponse = selectTestById("kingmj").orElse(ExampleResponse.builder()
                                                                                         .isSuccess(false)
                                                                                         .message("실패다람쥐")
                                                                                         .data("")
                                                                                         .build());
        return exampleResponse;
    }

스읍.. 뭔가 실패했을 때 결과를 전송하는 건 알겠는데 뭔가 명확하지가 않았다. "빡" 하고 눈에 띄지 않는 느낌이랄까.. 그래서 정적 팩토리 메서드 패턴을 도입했는데,

@Getter
public class ExampleResponse {

    private boolean isSuccess;
    private String message;
    private String data;
    
    private ExampleResponse(boolean isSuccess, String message, String data){
        this.isSuccess = isSuccess;
        this.message = message;
        this.data =data;
    }
    
    public static ExampleResponse fail(){
        return new ExampleResponse(false, "실패했다람쥐", "Not Found");
    }
 }

 

    public ExampleResponse example() {
        ExampleResponse exampleResponse = selectTestById("kingmj").orElse(ExampleResponse.fail());
        return exampleResponse;
    }

위랑 비교하면 어떤가??? 뭔가 아 이친구는 실패했을 때 response구나 하면서 뭔가 눈에 빡 들어오는 느낌이다. 아마 글을 읽으시는 분들도 느꼈을 것이라고 생각한다. (강요아님 ㅎ

 

 

물론, 정적 팩토리 메서드를 도입하자고 했을 때 다른 의견도 많았다. dto 안에 builder(builderClassName, builderMethodNamed)를 여러개 선언 후, 상황에 맞게 사용하는 것은 어떨까 등....

 

그러므로, 이 글을 읽으시는 개발자 분들도 항상 정적 팩토리 메서드가 정답은 아니다 라는 것을 인지하고 목적에 맞게 사용하시면 될 것이다.

GO럼 이만~~ (2024년도 화이팅)