안녕하세요 오늘은 Spring Boot의 Response들을 Builder Pattern을 이용하여 유연하게 작성하는 법에 대해서 포스팅하도록 하겠습니다.
API 서버를 구축하다보면 API별 Response가 제각각이기 때문에 Spring Boot에서 이런 모든 API들을 정의할 때 API별로 Response 객체를 구축해서 진행하신 경험이 있으실 겁니다. 이를 빌더 패턴을 이용하면 간단하게 정리할 수 있습니다.
우선 빌더 패턴이란 무엇일까요? 빌더 패턴이란 복잡한 객체를 생성하는 과정을 단순화하고 객체 생성 코드를 가독성 있게 만들어주는 디자인 패턴입니다. 보통 객체를 생성할 경우 생성자를 이용하거나 저번 시간에 말씀드린 정적 팩토리 메서드를 호출합니다. 하지만 매개변수가 많은 객체의 경우 코드의 길이가 지나치게 길어져 가독성의 문제가 발생합니다. 따라서 이를 해결하기 위해 빌더 패턴이 도입됩니다.
빌더 패턴은 크게 다음의 4가지로 이루어져 있습니다.
아래의 예시를 통해 확인해보겠습니다. 아래의 코드는 Response( = Product)를 구현하는 추상 클래스입니다. 저희 서비스의 API의 경우 JSON 형식으로 데이터를 주고받기 때문에 HashMap을 이용하여 Response를 구현하였습니다. 객체 내부에 Builder( = Builder) 객체를 생성하여 상속받는 자식 클래스가 해당 부분을 구현하게 됩니다.( = Concrete Builder)
여기서 <T extends Builder>의 경우 제너릭 타입으로 T와 Builder를 상속받는 객체까지만 허용하겠다는 의미입니다. 따라서 Response 객체를 상속받지 않은 클래스의 경우 Builder를 사용할 수 없으며 자연스럽게 Response 객체와 그 자식 클래스들만이 해당 Builder 클래스에 접근할 수 있습니다.
또한 주의 깊게 보셔야 할 부분은 self() 메소드입니다. 자식 클래스에서 add 메서드를 호출하면 부모 클래스의 response에 추가되고 자식 클래스에서 접근이 어렵습니다. 따라서 자식 클래스에서 self() 메소드를 return this로 정의한다면 add 메소드가 실행된 이후 자식 클래스가 반환되어 같은 HashMap을 공유할 수 있습니다.
public abstract class Response {
final HashMap<String,Object> response;
abstract static class Builder<T extends Builder<T>>{
HashMap<String,Object> response = new HashMap<>();
public T add(String key, Object val){
response.put(key,val);
return self();
}
abstract Response build();
protected abstract T self();
}
Response(Builder<?> builder){
response = new HashMap<>(builder.response);
}
}
아래는 위의 Response 추상 클래스를 상속받은 SuccessResponse 객체입니다. 부모 클래스에서 구현된 Builder 클래스를 상속받아 필요한 내용을 재구축합니다.
public class SuccessResponse extends Response {
public HashMap<String,Object> info;
private SuccessResponse(Builder builder){
super(builder);
info = builder.response;
}
public static class Builder extends Response.Builder<Builder>{
public Builder(int code, String message){
this.response.put("isSuccess",true);
this.response.put("code",code);
this.response.put("message",message);
}
@Override
public SuccessResponse build() {
return new SuccessResponse(this);
}
@Override
protected Builder self() {
return this;
}
}
}
이를 클라이언트에서 호출하면 다음과 같습니다.
SuccessResponse response = new SuccessResponse.Builder(100,"성공적으로 로그인되었습니다.")
.add("result",jwt)
.build();
이런 방식으로 Response를 변경한다면 API별 각각의 Response 객체를 따로 만들지 않고 .add()를 이용하여 필요한 요소들을 추가한 후 최종적으로 HashMap<String,Object>를 반환하면 성공적으로 리턴할 수 있습니다.
저는 여기서 한 발 더 나아가 빌더 패턴과 싱글톤 패턴의 공존화에 대해서도 고민해보았습니다. 결론부터 말씀드리자면 현재 상황에서 두 디자인 패턴의 공존은 무의미하다는 결론을 내렸습니다. 빌더 패턴은 복잡한 객체를 생성하는 과정을 단순화하고 객체 생성 코드를 가독성 있게 만들어주는 것에 포커싱을 맞춘 반면 싱글톤 패턴은 객체의 유일성을 보장하는 것에 포커싱을 맞추었습니다. 따라서 빌더 패턴으로 만들 객체가 유일해야 한다고 하면 공존을 고려해야겠지만 현재의 경우 API별로 Response는 모두 다르기 때문에 싱글톤 패턴으로 객체의 유일성을 보장하는 것이 무의미하다고 생각했습니다. 따라서 추후 유일성이 보장되야 하는 객체에 매개변수가 많을 경우 다시 검토해보려고 합니다.
그럼 지금까지 긴 글 읽어주셔서 감사합니다!