현재 진행중인 프로젝트에서, 유저의 거래 기능을 담당하는 도메인 객체인 Trade 객체를 생성시에,
파라미터로 5개의 변수를 받아야 하는 상황이 생겼습니다. 그에 따라 생성자 코드가 다음과 같이
public Trade(Long buyerId, Long sellerId, Long productId, LocalDateTime tradeDate,
int tradeProductQuantity) {
this.buyerId = buyerId;
this.sellerId = sellerId;
this.productId = productId;
this.tradeDate = tradeDate;
this.tradeStatus = TradeStatus.BEFORE_TRADE;
this.tradeProductQuantity = tradeProductQuantity;
}
길어지게 되었습니다. 이렇게 많은 변수를 받는 생성자를 사용하게 되면,
private Trade createTrade() {
Trade trade = new Trade(1L,2L,1L,LocalDateTime.now(),10);
return trade;
}
위 코드와 같이 제가 작성한 코드를 보는 다른 사람이, 각 인자가 어떤 의미 인지 알기 어려울 것 같다는 생각이 들었습니다. 그에 따라 작성한 코드를 보완할 수 있는 방법을 찾게 되었습니다.
점층적 생성자 패턴
먼저 객체의 생성을 깔끔하게 도와줄 수 있는 점층적 생성자 패턴(telescoping constructor pattern)을 생각해보았습니다.
해당 패턴을 적용한 코드는 다음과 같습니다.
public class Trade {
private final Long id; //선택적 인자
private final Long userId; //필수 인자
private final Long productId; //필수 인자
private final int tradeProductQuantity; // 필수 인자
//모든 인자를 다 받는 생성자
public Trade(Long id, Long userId, Long productId, int tradeProductQuantity){
this.id = id;
this.userId = userId;
this.productId = productId;
this.tradeProductQuantity=tradeProductQuantity;
}
public Trade(Long userId, Long productId, int tradeProductQuantity){
this(null,1L,1L,10); // id값은 auto_increment이기에 null로 적용
}
}
위와 같은 방식으로 객체 생성을 하게 된다면, new Trade("1L","1L",10) 과 같은 호출을 통해, 굳이 설정해줄 필요 없는 id값을 미리 설정할 수 있다는 장점이 있습니다.
반면에 단점으로는 필요한 인자의 갯수가 증가함에 따라, 기존에 작성되어 있는 여러 생성자 코드가 수정되어야 하고, 또한 새롭게 추가된 인자를 포함한 생성자 코드를 추가해줘야 한다는 단점이 있습니다. 그리고 객체를 생성하는 코드만 봐서는 의미를 알기 어렵다는 문제점이 여전히 남아 있습니다.
따라서 점층적 생성자 패턴은 적합하다 생각하지 않아 다른 방법을 찾아봤습니다.
자바빈 패턴
Trade trade = new Trade();
trade.setId(1L);
trade.setUserId(1L);
trade.setProductId(1L);
trade.setTradeProductQuantity(10);
위 패턴은 자바 빈(https://ko.wikipedia.org/wiki/자바빈즈) 규약 중 setter 메서드를 통해 객체를 생성하는 코드를 읽기 좋게 만드는 방법입니다.
위 방식의 장점으로는 객체 생성시의 각인자의 의미를 파악하기 쉬워졌다는 장점과 점층적 생성자 패턴과 달리 복잡하게 여러개의 생성자를 만들지 않아도 된다는 장점이 있습니다.
하지만 위 방식으로 인해 객체의 일관성이 깨진다는 단점과, setter 메서드가 있으므로 불변 객체(immutable object)를 만들수가 없다는 단점이 있습니다.(불변 객체를 사용하지 못하기 때문에, 멀티 스레드 환경에서의 thread-safety를 확보하려면 점층적 생성자 패턴보다 더 복잡하고 많은 코드를 작성해줘야 한다는 단점이 있습니다.)
객체 생성시의 복잡성을 해결하기 위해, 객체의 일관성과, 불변성을 포기한다는 것은 맞지 않다고 생각해 또 다시 다른 방법을 찾아보게 되었습니다.
마지막으로 찾아본 방법은 빌더 패턴 입니다.
빌더 패턴(Builder Pattern)
public class Trade {
private Long id;
private Long userId;
private Long productId;
private LocalDateTime tradeDate;
private TradeStatus tradeStatus;
private int tradeProductQuantity;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public static class Builder {
private final Long id; //필수 인자
private final Long userId; //필수 인자
private final Long productId; //필수 인자
private final LocalDateTime tradeDate; //필수 인자
private final int tradeProductQuantity; //필수 인자
private TradeStatus tradeStatus; // 선택적 인자
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Builder(Long id, Long userId, Long productId, LocalDateTime tradeDate, int tradeProductQuantity) {
this.id = id;
this.userId = userId;
this.productId = productId;
this.tradeDate = tradeDate;
this.tradeProductQuantity = tradeProductQuantity;
}
public Builder tradeStatus(TradeStatus tradeStatus) {
this.tradeStatus = tradeStatus;
return this; // 위와 같이 설정 함으로써 .()과 같이 메서드 체인을 이어갈 수 있습니다.
}
public Builder createdAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
return this;
}
public Builder updatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
return this;
}
public Trade build() {
return new Trade(this);
}
}
private Trade(Builder builder) {
id = builder.id;
userId = builder.userId;
productId = builder.productId;
tradeDate = builder.tradeDate;
tradeProductQuantity = builder.tradeProductQuantity;
tradeStatus = builder.tradeStatus;
createdAt = builder.createdAt;
updatedAt = builder.updatedAt;
}
}
위와 같이 내부 클래스로 Builder 클래스를 정의하고 생성하게 되면 다음과 같이 객체를 생성 할 수 있게 됩니다.
Trade trade = new Trade.Builder(1L,1L,2L,LocalDateTime.now(),10)
.tradeStatus(TradeStatus.TRADABLE)
.createdAt(LocalDateTime.now())
.updatedAt(LOcalDateTime.now()) //예시에서는 단순화를 위해 now()로 사용했습니다
.build(); // 해당 메서드까지 호출해야 build() 가 객체를 생성해 돌려둡니다
현재 에서도 Builder 생성시에, 많은 인자를 받고 있어 적합하지 않다 생각이 들지만 기존 코드보다 더 직관적으로, Trade 객체 생성시에 어떤 인자를 받는지 알기 쉬워졌다 생각합니다. 또한 setter 메서드를 사용하지 않았기 때문에 불변 객체를 생성 할 수 있습니다. 또한 마지막에 .build() 메서드를 통해 객체 생성 함수에서 잘못된 값이 입력되었는지 검증하게 코드를 작성할 수도 있습니다.
위와 같이 직접 코드를 작성해, Builder 패턴을 구현할 수도 있지만 롬복에서 제공해주는 @Builder 애노테이션으로 쉽게 빌더 패턴을 구현할 수도 있습니다.
그에 관해서는 롬복공식문서 를 참고해 주세요
현재 프로젝트에서 Builder 패턴을 적용해, 객체 생성시에 어떤 인자를 받는지 더 직관적으로 알 수 있게 하기 위해, 점층적 생성자 패턴과 자바 빈 패턴, 그리고 빌더 패턴의 각각의 장단점을 비교해 보고 간단한 코드 예시를 통해, 각 패턴을 적용했을 때 어떤 차이점이 있는지 알아봤습니다. 제가 예시로 작성한 코드가 다소 미흡하지만 더 자세한 코드를 보기 원하신다면 굿즈포유 해당 링크를 참고해주세요
참고 자료
'프로그래밍 > 프로젝트' 카테고리의 다른 글
테스트 시, Redis Session 으로 인해 생긴 문제 해결 (0) | 2023.04.19 |
---|---|
SQL Injection (0) | 2023.04.05 |
테스트 커버리지를 70% 이상 유지하면서 느낀점 (0) | 2023.03.21 |
도커 컴포즈 사용 시 DB 초기화 문제 해결 과정 (0) | 2023.03.18 |
캐싱은 언제 적용하는게 좋을까? (2) | 2023.03.07 |