ModelMapper 오류 정리

Sehyeon·2024년 3월 19일

삽질 기록

목록 보기
4/4
post-thumbnail

엔티티와 DTO를 매핑하는 과정을 간편하게 도와주는 라이브러리인 ModelMapper를 사용하는 과정에서 생긴 오류를 정리하는 게시글 입니다.

ModelMapper 사용법

  1. SourceObject(매핑할 객체)를 준비한다. -> SourceObject에는 Getter가 필요하다.
  2. DestinationObject(매핑될 객체)를 준비한다. -> DestinationObject에는 Setter가 필요하다.
  3. ModelMappermap메소드에 SourceObejct, DestinationObject를 넘겨서 DestinationObject를 얻는다.

사용 예시

DTOEntity로 변경하거나 반대로 EntityDTO로 변경하는 과정에서 ModelMapper가 사용된다.

소스코드

SourceObject(매핑할 객체)

@Getter
public class SourceObject {
    private String name;
    private LocalDateTime dateTime;

    public SourceObject(String name, LocalDateTime dateTime) {
        this.name = name;
        this.dateTime = dateTime;
    }
}

DestinationObject(매핑될 객체)

@Setter
public class DestinationObject {
    private String name;
    private LocalDateTime dateTime;

    @Override
    public String toString() {
        return "DestinationObject{" +
                "name='" + name + '\'' +
                ", dateTime=" + dateTime +
                '}';
    }
}

Main

public class Main {
    public static void main(String[] args) {
        // SourceObject 생성
        SourceObject sourceObject = new SourceObject("Example", LocalDateTime.now());

        // ModelMapper 생성
        ModelMapper modelMapper = new ModelMapper();

        // SourceObject를 DestinationObject로 매핑
        DestinationObject destinationObject = modelMapper.map(sourceObject, DestinationObject.class);

        // 매핑된 DestinationObject 출력
        System.out.println("destinationObject = " + destinationObject.toString()); // destinationObject = DestinationObject{name='Example', dateTime=2024-03-19T10:40:19.099631}
    }
}

문제점

SourceObjectDTO DestinationObjectEntity객체라고 가정해보자.
위에서 DestinationObejctSetter가 필요한데 Entity객체에는 Setter가 있으면 어디에서든지 데이터베이스와 관련이 있는 Entity객체를 변경할 수 있어 객체의 불변성을 보장할 수 없고 캡슐화 원칙에 위배 된다.

DestinationObjectSetter메서드를 사용하지 않고 ModelMapper를 이용해서 객체 간의 매핑을 할 수는 없을까?

ModelMapper의 간단한 작동원리

ModelMapper는 내부적으로 Java Reflection API를 사용하여 private접근 제한자가 지정된 멤버 변수에도 접근할 수 있다. 즉, 접근 제한자에 상관없이 필드 값을 가져오거나 실행할 수 있다.
추가적으로 ModelMapperSourceObjectDestinationObject필드 이름을 사용하여 매핑을 수행하고 Reflection을 통해 필드 값을 가져오거나 설정하여 매핑 작업을 완료한다.

ModelMapper 사용 대상 객체에서 Getter, Setter를 제거해보자

ModelMapper 설정

ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
        .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
        .setFieldMatchingEnabled(true);

setFieldAccessLevel에서 AccessLevel.Private로 설정해주면 ModelMapper에서 private 필드에도 접근이 가능하도록 설정할 수 있다.
setFieldMatchingEnabledtrue로 설정하게 되면 필드 이름이 동일한 경우에만 매핑을 수행한다.

SourceObject

public class SourceObject {
    private String name;
    private int age;

    public SourceObject(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

기존의 SourceObejct에서 Getter를 제거하고 LocalDateTime자료형의 dateTime대신에 int자료형인 age를 추가하였다.
LocalDateTime을 제거하고 int를 제거한 이유는 뒤에서 설명하겠다.

DestinationObject

public class DestinationObject {
    private String name;
    private int age;

    @Override
    public String toString() {
        return "DestinationObject{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

기존의 DestinationObject에서 Setter를 제거하고 SourceObject와 마찬가지로 LocalDateTime필드를 int필드로 대체하였다.

Main

public class Main {
    public static void main(String[] args) {
        // SourceObject 생성
        SourceObject sourceObject = new SourceObject("Example", 30);

        // ModelMapper 생성
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setFieldMatchingEnabled(true);

        // SourceObject를 DestinationObject로 매핑
        DestinationObject destinationObject = modelMapper.map(sourceObject, DestinationObject.class);

        // 매핑된 DestinationObject 출력
        System.out.println("destinationObject = " + destinationObject.toString()); //destinationObject = DestinationObject{name='Example', age=30}
    }
}

SourceObjectDestinationObjectGetterSetter를 제거했음에도 정상 동작한다!
이는 ModelMapper가 내부적으로 Java Reflection API를 사용하여 SourceObjectDestinationObjectprivate 접근 제한자의 멤버 변수에도 접근이 가능하기 때문임을 알 수 있다.

ModelMapper의 LocalDateTime 매핑 문제

기본적으로 ModelMapperAccessLevelProtected이기 때문에 SourceObject, DestionationObejctprivate 필드에 접근할 수 없다. 하지만 ModelMapper는 매핑과정에서 GetterSetter를 이용하므로 GetterSetter 메서드가 구현이 되어 있다면 정상적으로 동작한다.

ModelMapperAccessLevelPrivate로 설정하게 된다면 GetterSetter가 없어도 동작하도록 할 수 있지만... SourceObjectDestinationObejctGetter, Setter를 구현하지않고 private 접근제한자의 LocalDateTime 자료형으로 하는 멤버 변수가 있다면 매핑 오류가 발생한다!

발생한 오류

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.time.LocalDate java.time.LocalDateTime.date accessible: module java.base does not "opens java.time" to unnamed module @6a5fc7f7

오류 발생 이유

java.time 패키지가 닫혀있어서 Java Refletion API를 사용하여 java.time.LocalDateTime 클래스의 private필드인 date에 접근할 수 없다.
"패키지가 닫혀 있다"라는 의미는 외부에서 패키지에 대한 접근을 제어하는 메커니즘을 의미하는데 패키지가 열려 있다면 다른 모듈에서 해당 패키지에 대한 접근이 허용되고, 패키지가 닫혀 있다면 외부에서의 접근이 차단된다.

정리

ModelMapper는 공통된 필드를 가지는 SourceObjectDestinationObject를 편하게 매핑해주는 라이브러리인데 그 과정에서 GetterSetter이 필요하다.
GetterSetter를 사용하지 않고 Java Reflection API를 이용해서 private필드에 직접 접근이 가능하지만 Java.time과 같이 닫힌 패키지 같은 경우에는 private필드에 접근할 수 없다.
매핑하는 객체가 가지는 필드에서 닫힌 패키지에 속한 필드가 있다면 ModelMapper 사용 대신에 다른 방법(매핑 메서드를 따로 작성하던가..)을 찾아야한다.

참고

[Spring] Modelmapper에서의 매핑 관련 Troubleshooting
[Spring] ModelMapper Entity to DTO 변환 시 프로퍼티 null 해결

0개의 댓글