백엔드 REST API를 구현하다보면, DTO를 통해서 값을 전달받은 후, Entity에 해당하는 value로 변환하여 필요한 서비스와 로직을 처리해준 후, 다시 DTO를 통해서 값을 반환해주는 순서로 많이 구현한 경험이 있다.
나는 이럴때마다 dto에 toDto, toEntity와 같이 dto와 entity간에 타입 변환을 위해서 메소드를 구현하여 사용하고 있었다.
그러던 중에, of와 from 과 같은, 내가 작성해줬던 메소드와 비슷한 기능을 하지만, 다른 많은 개발자 분들이 공통적인 메소드명을 가지고 사용하고 있는 것을 인지하게 되었다. of와 from을 따라 가면서, 정적 팩토리 메서드
와 정적 팩토리 메서드 네이밍
에 대해 알게 되었다. 이를 적용하여 전과 후를 비교하고, 장단점에 대해서 생각해볼까 한다.
GoF의 팩토리 패턴에서 유래된 이름으로, 말 그대로 객체를 생성하는 역할을 하는 메소드
이다.
대부분 dto -> entity로 변환을 할 때, 아래와 같이 사용한다.
public ProductResponseDto toDto(Product product) {
this.id = product.getId();
this.name = product.getName();
this.description = product.getDescription();
this.stock = product.getStock();
this.category = product.getCategory();
this.price = product.getPrice();
return this;
}
public CartReadResponseDto toDto(Cart cart) {
this.cartId = cart.getId();
this.userId = cart.getUser().getId();
this.cartItems = cart.getItems();
this.totalAmounts = cart.getTotalAmounts();
return this;
}
이와 같이 toDto 라는 이름을 통해서 적용해주고 있었다.
@Transactional
public ProductResponseDto update(Long id, ProductRequestDto productRequestDto) {
Product updateProduct = productRepository.findById(id)
.orElseThrow(NoSuchProductExist::new);
updateProduct.update(productRequestDto);
return new ProductResponseDto().toDto(updateProduct);
}
이후 이런 식으로 new 를 통해서 새로운 dto객체를 생성하여 만들어주는 방식을 사용하고 있었다.
우선 dto의 생성자를 만들어 주고, 이를 사용하는 static from 메소드를 생성해주었다.
@Transactional(readOnly = true)
public CartReadResponseDto get(Long userId) {
return CartReadResponseDto.from(cartRepository.findById(userId)
.orElseThrow(CartNotExistException::new));
}
이렇게 사용하면 어떤 이점을 얻을 수 있는지 생각해보자.
팩토리 메서드를 통해서 하나의 value 나 dto, 또는 entity를 매개변수로 정해서 객체 생성에 필요한 매개변수를 캡슐화를 해줄 수 있다는 장점이 있다.
본래 dto 객체를 반환해줄때에, 매개변수로 받은 cart 객체에서 일일이 getter 메소드를 통해서 매개변수에 집어 넣어주고, 새로운 dto객체를 생성해줘야 했지만, 정적 팩토리 메서드를 사용하면 dto 내부 구조를 모르더라도 dto 객체 생성이 쉽게 가능하다.
기존에는 new 생성자를 통해서 새로운 객체를 생성하여 사용하는 방식이었다. new를 통해서 만든 객체는 어떠한 목적으로 만들어진 것인지 new만 봐서는 쉽게 판단할 수 없다. 이를 static method를 통해서, 이름을 가지게 할 수 있다.
from 과 of 같은 정적 팩토리 메서드 네이밍 규칙을 따를수도 있고, from과 of 대신 적절한 이름을 갖는 메소드 명을 통해서 보다 객체 생성과 사용의 목적을 명확히 할 수 있다는 장점이 있다.
A라는 클래스를 B, C, D 가 상속 받는 경우일 때, A의 정적 팩토리 메서드를 통해서 B C D 의 객체를 생성하여 반환할 수 있다.
상속과 다형성의 특징 덕분에 가능하기도 하지만, new와 생성자와는 달리, 정적 팩토리 메서드가 객체라는 리턴 타입을 갖는다는 특징이 더해져서 하위 타입으로 리턴을 해줄 수 있다는 장점을 가진다.
만일 특정 객체에 대해 사용하는 수가 정해져 있다면, static의 팩토리 메서드를 통해서 새로운 객체를 생성할 필요가 없고 생성해준 객체만을 사용할 수 있다.
public class LottoNumber {
private static final int MIN_LOTTO_NUMBER = 1;
private static final int MAX_LOTTO_NUMBER = 45;
private static Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();
static {
IntStream.range(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
.forEach(i -> lottoNumberCache.put(i, new LottoNumber(i)));
}
private int number;
private LottoNumber(int number) {
this.number = number;
}
public LottoNumber of(int number) { // LottoNumber를 반환하는 정적 팩토리 메서드
return lottoNumberCache.get(number);
}
...
}
이와 같이 로또의 개수를 정해놓고 로또를 반환해주는 코드가 있다고 가정해보자.
생성자를 private로 설정하여 다른 외부의 접근을 막아두고, 이후 팩토리 메서드를 통해서 정해진 객체만을 사용하도록 구현하는게 가능해진다. 또한 이를 통해서 불필요한 다른 숫자의 로또 객체 생성을 막을 수 있다.
정적 팩토리 메서드를 사용할 때, 생성자를 private를 통해서 사용하는 경우 상속을 통한 확장이 불가능해진다. 상속을 통한 확장을 막는 것은 상황에 따라 장점이자 단점이 될 수 있을 것 같다.
생성자는 코드의 윗부분에 위치하는 경우가 많아 찾기가 용이하지만, 정적 팩토리 메서드의 경우에는 위치가 정해지지 않아 또 다른 개발자가 사용 시에 찾아야 하는 경우가 발생할 수 있다.