엔티티와 DTO를 매핑하는 과정을 간편하게 도와주는 라이브러리인 ModelMapper를 사용하는 과정에서 생긴 오류를 정리하는 게시글 입니다.
SourceObject(매핑할 객체)를 준비한다. -> SourceObject에는 Getter가 필요하다.DestinationObject(매핑될 객체)를 준비한다. -> DestinationObject에는 Setter가 필요하다.ModelMapper의 map메소드에 SourceObejct, DestinationObject를 넘겨서 DestinationObject를 얻는다.DTO를 Entity로 변경하거나 반대로 Entity를 DTO로 변경하는 과정에서 ModelMapper가 사용된다.
@Getter
public class SourceObject {
private String name;
private LocalDateTime dateTime;
public SourceObject(String name, LocalDateTime dateTime) {
this.name = name;
this.dateTime = dateTime;
}
}
@Setter
public class DestinationObject {
private String name;
private LocalDateTime dateTime;
@Override
public String toString() {
return "DestinationObject{" +
"name='" + name + '\'' +
", dateTime=" + dateTime +
'}';
}
}
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}
}
}
SourceObject가 DTO DestinationObject가 Entity객체라고 가정해보자.
위에서 DestinationObejct는 Setter가 필요한데 Entity객체에는 Setter가 있으면 어디에서든지 데이터베이스와 관련이 있는 Entity객체를 변경할 수 있어 객체의 불변성을 보장할 수 없고 캡슐화 원칙에 위배 된다.
DestinationObject에Setter메서드를 사용하지 않고ModelMapper를 이용해서 객체 간의 매핑을 할 수는 없을까?
ModelMapper는 내부적으로 Java Reflection API를 사용하여 private접근 제한자가 지정된 멤버 변수에도 접근할 수 있다. 즉, 접근 제한자에 상관없이 필드 값을 가져오거나 실행할 수 있다.
추가적으로 ModelMapper는 SourceObject와 DestinationObject의 필드 이름을 사용하여 매핑을 수행하고 Reflection을 통해 필드 값을 가져오거나 설정하여 매핑 작업을 완료한다.
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setFieldMatchingEnabled(true);
setFieldAccessLevel에서 AccessLevel.Private로 설정해주면 ModelMapper에서 private 필드에도 접근이 가능하도록 설정할 수 있다.
setFieldMatchingEnabled를 true로 설정하게 되면 필드 이름이 동일한 경우에만 매핑을 수행한다.
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를 제거한 이유는 뒤에서 설명하겠다.
public class DestinationObject {
private String name;
private int age;
@Override
public String toString() {
return "DestinationObject{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
기존의 DestinationObject에서 Setter를 제거하고 SourceObject와 마찬가지로 LocalDateTime필드를 int필드로 대체하였다.
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}
}
}
SourceObject와 DestinationObject에 Getter와 Setter를 제거했음에도 정상 동작한다!
이는 ModelMapper가 내부적으로 Java Reflection API를 사용하여 SourceObject와 DestinationObject의 private 접근 제한자의 멤버 변수에도 접근이 가능하기 때문임을 알 수 있다.
기본적으로 ModelMapper의 AccessLevel은 Protected이기 때문에 SourceObject, DestionationObejct의 private 필드에 접근할 수 없다. 하지만 ModelMapper는 매핑과정에서 Getter와 Setter를 이용하므로 Getter와 Setter 메서드가 구현이 되어 있다면 정상적으로 동작한다.
ModelMapper의 AccessLevel을 Private로 설정하게 된다면 Getter와 Setter가 없어도 동작하도록 할 수 있지만... SourceObject나 DestinationObejct에 Getter, 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는 공통된 필드를 가지는 SourceObject와 DestinationObject를 편하게 매핑해주는 라이브러리인데 그 과정에서 Getter와 Setter이 필요하다.
Getter와 Setter를 사용하지 않고 Java Reflection API를 이용해서 private필드에 직접 접근이 가능하지만 Java.time과 같이 닫힌 패키지 같은 경우에는 private필드에 접근할 수 없다.
매핑하는 객체가 가지는 필드에서 닫힌 패키지에 속한 필드가 있다면 ModelMapper 사용 대신에 다른 방법(매핑 메서드를 따로 작성하던가..)을 찾아야한다.
[Spring] Modelmapper에서의 매핑 관련 Troubleshooting
[Spring] ModelMapper Entity to DTO 변환 시 프로퍼티 null 해결