MapStruct Ambiguous Constructors 에러와 MapStruct 가 생성자를 선택하는 방법

WIZ·2023년 11월 28일

TroubleShooting

목록 보기
5/7

사이드 프로젝트에서 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 라는 이름의 어노테이션을 생성자에 붙이라고 이에 대한 해결방법까지 제시를 해준다. 이렇게 친절할수가...😮


우선 해결해보자


AS-IS

기존 코드를 먼저 살펴보자.

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 키워드의 속성에 임의의 값을 저장할 수 밖에 없고 이는 잘못된 값이 저장된 채로 객체가 동작하면서 찾기 어려운 오류를 만들어낼거라 생각했기 때문이다.

TO-BE

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 가 매퍼를 구현하는 전체 과정에 대해서 깊이있게 다루진 않을거고, 위에서 소개한 이슈와 관련된 생성자를 선택하는 과정에 대해서만 소개할 예정이다.


mapstruct 가 매퍼를 구현할 때 객체 생성에 사용되는 생성자를 어떤 우선순위에 따라 선택하는지 살펴보자.

  1. 가장먼저 Builder 가 있는지 확인하고 있다면 Builder 를 선택한다.
  2. @Default 가 붙은 생성자를 선택한다.
  3. public 접근 제어자를 가진 생성자가 유일하다면 해당 생성자를 선택한다.
  4. 기본 생성자를 선택한다.

위 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.

  1. 위 에러는 기본 생성자가 없는 상태에서 여러 개의 생성자가 정의되어 있는 경우 발생하는 에러다.
  2. 기본 생성자를 만들거나, mapstruct 에서 사용하고자하는 생성자에 @Default 어노테이션을 붙여주면 해결된다. 참고로 @Default 어노테이션은 제공되는게 아니라 직접 정의해서 사용하면 된다. (이름은 무조건 동일해야함)

mapstruct 는 굉장히 사용하기 쉬운 라이브러리다.
그러다보니 어떻게 동작하는지 정확히 이해하지 않고 사용해왔던 것 같다.

이번 트러블 슈팅을 통해 라이브러리를 사용할 때 기본적인 동작방식에 대해서는 이해를 가지고 사용하는게 좋겠다고 생각했다. 알고 사용했더라면 겪지 않았을 이슈였고 여기에 꽤 많은 시간을 쓰면서 반성도 많이 했다...😢😢 나와 같은 실수를 하는 사람이 없길 바라며 포스팅을 마무리 지어본다!

0개의 댓글