사이드 프로젝트에서 Domain Entity 와 JPA Entity 를 분리해 개발하고 있고, 두 객체를 쉽게 매핑하기 위해 mapstruct 를 사용하고 있다.
지금까지는 단순히 mapstruct 가 만들어주는 매핑 메소드를 사용만 해왔는데, 최근 마주한 이슈를 토대로 mapstruct 가 매핑 메소드를 어떻게 만들어나가는지에 대해 소개해보려한다.
mapstruct 를 사용하던 중 아래와 같은 에러를 만났다.
error: Ambiguous constructors found for creating com.example.orderservice.domain.entity.Order: Order(java.lang.String, java.lang.String, int, int, int, java.lang.String, java.time.LocalDateTime), Order(java.lang.String, int, int, java.lang.String). Either declare parameterless constructor or annotate the default constructor with an annotation named @Default.
위 에러의 핵심 구문은 Ambiguous constructors found 이다.
생성자를 찾는 과정에서 뭔가 모호함이 있어서 에러가 발생했다는 것이다.
Either declare parameterless constructor or annotate the default constructor with an annotation named @Default.
마지막에 기본 생성자를 만들거나 @Default 라는 이름의 어노테이션을 생성자에 붙이라고 이에 대한 해결방법까지 제시를 해준다. 이렇게 친절할수가...😮
기존 코드를 먼저 살펴보자.
class Order(
val id: String,
val productId: String,
val qty: Int,
val unitPrice: Int,
val totalPrice: Int,
val userId: String,
val createdAt: LocalDateTime
) {
constructor(productId: String, qty: Int, unitPrice: Int, userId: String): this(
id = UUID.randomUUID().toString(),
productId = productId,
qty = qty,
unitPrice = unitPrice,
totalPrice = qty * unitPrice,
userId = userId,
createdAt = LocalDateTime.now()
)
}
보다시피 생성자가 2개 존재한다. 이로 인해서 mapstruct 가 만들어주는 매퍼에서 객체를 생성 할 때 어떤 생성자를 선택해야하는지 모호해진 것이다.
결론부터 말하면 에러 메시지에서 제시한 방법 중 나는 두 번째 방법을 이용해 문제를 해결했다. Kotlin 의 경우 기본생성자를 억지로 만들게되면 Setter 가 없는 val 키워드의 속성에 임의의 값을 저장할 수 밖에 없고 이는 잘못된 값이 저장된 채로 객체가 동작하면서 찾기 어려운 오류를 만들어낼거라 생각했기 때문이다.
class Order @Default constructor(
val id: String,
val productId: String,
val qty: Int,
val unitPrice: Int,
val totalPrice: Int,
val userId: String,
val createdAt: LocalDateTime
) {
constructor(productId: String, qty: Int, unitPrice: Int, userId: String): this(
id = UUID.randomUUID().toString(),
productId = productId,
qty = qty,
unitPrice = unitPrice,
totalPrice = qty * unitPrice,
userId = userId,
createdAt = LocalDateTime.now()
)
}
mapstruct 에게 선택되길 원하는 생성자 위에 단순히 @Default 어노테이션을 붙여주기만 하면 된다. 근데 @Default 라는 어노테이션은 정의되어 있지가 않다 -.-
@Default 어노테이션까지도 우리가 새롭게 정의를 해줘야한다.
주의해야 할 점은 이름이 반드시 Default 여야 한다는 것이다!!
annotation class Default
그 결과로 생성된 매퍼 코드를 잠깐 살펴보자.
@Override
public OrderJpaEntity mapToJpaEntity(Order order) {
if ( order == null ) {
return null;
}
String id = null;
String productId = null;
int qty = 0;
int unitPrice = 0;
int totalPrice = 0;
String userId = null;
LocalDateTime createdAt = null;
id = order.getId();
productId = order.getProductId();
qty = order.getQty();
unitPrice = order.getUnitPrice();
totalPrice = order.getTotalPrice();
userId = order.getUserId();
createdAt = order.getCreatedAt();
OrderJpaEntity orderJpaEntity = new OrderJpaEntity( id, productId, qty, unitPrice, totalPrice, userId, createdAt );
return orderJpaEntity;
}
@Default 를 붙인 생성자가 정상적으로 선택되어 객체를 생성하고 있는 것을 확인할 수 있다.
굳이 매퍼 코드를 소개한 이유가 따로 있다.
mapstruct를 사용하면서 느낀 가장 중요한 것은 자동으로 매퍼를 생성해준다고 그냥 사용만 하는 것이 아니라 반드시 생성된 매퍼 코드를 살펴봐야 한다는 것이다.자동으로 생성되기 때문에 내가 의도한 것과 다른 방향으로 매퍼가 생성될 수 있다. 객체를 매핑하는 과정이 내가 의도한 것과 동일한지 반드시 검증해야한다. 나중에 문제가 생기고 디버깅을 하게되면 내가 작성한 코드가 아니다보니 더욱 발견하기 어려울 수 있기 때문에 반드시 검증하고 넘어가는 습관을 가지도록 하자!!
이렇게 행복하게 넘어갈 수 있었겠지만.. 계속 기본적인 구조만 사용하게 될 것 같아서 mapstruct 가 매퍼를 만드는 과정을 조금이나마 정리하고 넘어가보자.
해당 포스팅을 통해 mapstruct 가 매퍼를 구현하는 전체 과정에 대해서 깊이있게 다루진 않을거고, 위에서 소개한 이슈와 관련된 생성자를 선택하는 과정에 대해서만 소개할 예정이다.
mapstruct 가 매퍼를 구현할 때 객체 생성에 사용되는 생성자를 어떤 우선순위에 따라 선택하는지 살펴보자.
Builder 가 있는지 확인하고 있다면 Builder 를 선택한다.@Default 가 붙은 생성자를 선택한다.public 접근 제어자를 가진 생성자가 유일하다면 해당 생성자를 선택한다.위 4가지 단계를 모두 거쳤는데 생성자를 선택하지 못했다면 컴파일 에러가 발생한다. 이런 케이스는 위 포스팅에서 소개한 기본 생성자 없이 여러 개의 생성자가 존재하는 케이스 (Ambiguous Constructors) 가 있다.
트러블 슈팅 과정을 간략하게 정리하며 포스팅을 마치려고 한다.
error: Ambiguous constructors found for creating com.example.orderservice.domain.entity.Order: Order(java.lang.String, java.lang.String, int, int, int, java.lang.String, java.time.LocalDateTime), Order(java.lang.String, int, int, java.lang.String). Either declare parameterless constructor or annotate the default constructor with an annotation named @Default.
mapstruct 에서 사용하고자하는 생성자에 @Default 어노테이션을 붙여주면 해결된다. 참고로 @Default 어노테이션은 제공되는게 아니라 직접 정의해서 사용하면 된다. (이름은 무조건 동일해야함)mapstruct 는 굉장히 사용하기 쉬운 라이브러리다.
그러다보니 어떻게 동작하는지 정확히 이해하지 않고 사용해왔던 것 같다.
이번 트러블 슈팅을 통해 라이브러리를 사용할 때 기본적인 동작방식에 대해서는 이해를 가지고 사용하는게 좋겠다고 생각했다. 알고 사용했더라면 겪지 않았을 이슈였고 여기에 꽤 많은 시간을 쓰면서 반성도 많이 했다...😢😢 나와 같은 실수를 하는 사람이 없길 바라며 포스팅을 마무리 지어본다!