토이 프로젝트 배우게 된 것들 & 오류 해결🐰

ObjectMapper & ModelMapper

j_estory 2023. 2. 25. 15:24

토이프로젝트를 진행하면서, entity 객체에 대하여 dto로 변환하는 로직이 코드량의 많은 부분을 차지하고 있었다. 

특히 계층 구조가 깊은 객체일수록 dto로 변환하는 로직은 더 복잡해지고 방대해졌다.

 

해당 문제를 해결하기 위해 라이브러리를 사용하기로 하였고, 라이브러리를 찾아보던 중, 두가지 mapper가 존재하였다.

 

✅ ObjectMapper

ObjectMapper에 대해 알아보기 전, 직렬화 및 역직렬화에 대한 개념을 알고 있어야 한다.

 

✔️ 직렬화

 

객체 형식의 Object를 Json 형태로 변환해주는 것을 의미

직렬화를 위해서는 각 인스턴스 변수에 Getter 메소드가 포함되어 있어야 한다

OR

@JsonProperty가 명시되어 있어야 한다.

 

✔️ 역직렬화

 

Json 형식의 포맷을 Object 형식으로 변환해주는 것을 의미

역직렬화를 위해서는 기본생성자가 반드시 필요하다.

각 인스턴스 변수에 대한 Setter 메소드는 필수적으로 필요하지는 않지만 존재한다면 이를 활용하게 된다.

 

즉, ObjectMapper는 JSON 형식을 사용할 때, 응답들을 직렬화하고 요청들을 역직렬화 할 때 사용하는 기술이다.

 

이를 활용하여 Entity > Dto 변환 시 objectMapper의 convertValue()를 통해 변환이 가능하다.

AccommodationDto dto = objectMapper.convertValue(entity, AccommodationDto.class);

단! Entity간 양방향 맵핑 시, 순환참조가 발생할 수 있으니 조심해야 한다.

저 또한, 순환참조 오류가 발생하여 해당 오류에 대해서는 별도 포스팅으로 정리해보려고 한다.

 

위와 같이 변환이 가능하며, objectMapper 클래스의 직렬화 OR 역질렬화에 대한 옵션들을 적용할 수 있다

 

✔️ 직렬화 / 역직렬화에 대한 옵션 적용

objectMapper.setSerializationInclusion(Include.NON_EMPTY);

객체 직렬화 시, null이나 빈 값은 항목에 포함하지 않는다는 옵션이다.
default는 null이거나 빈 값이 있어도 무조건 Json 변환에 포함되게 된다.
하지만 실무에서는 null이라도 포맷을 그대로 받아서 확인하길 원하기 때문에 잘 사용하지 않는다.

 

mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Json 문자열을 역직렬화 할 경우 클래스 변수에 맵핑되지 않는 키 값이 존재하여도 에러 없이 무시한다는 옵션이다.
default는 이 경우 Exception을 발생시킨다.
Json 포맷을 엄격하게 지켜야 하는 경우 해당 설정은 피해야 하겠지만, 어차피 사용하지 않는 값이라면
로직은 정상적으로 수행되도록 설정하는 것이 좋을 것 같다.

위의 설정들은 ObjectMapper에서도 할 수 있고 각 클래스 변수에서도 할 수 있지만 기본적으로 양쪽에 모두 해주는 것이 좋을 것 같다.

Spring 같은 프레임워크를 사용하는 경우라면 해당 클래스가 위에서 정의한 ObjectMapper를 통해서만 변환된다는 보장이 없다.

 

예를 들어, Feign 의 경우도 body를 직렬화, 역직렬화 할때 기본적으로 spring에 포함되어 있는 ObjectMapper를 사용하기 때문이다.

또한, @RestController 사용시에도 기본적으로 직렬화, 역직렬화가 들어가게 된다.

 

여러곳에서 ObjectMapper를 사용하므로 해당 클래스를 아래와 같이 bean으로 등록해두고 공통적인 설정들도 함께 등록 해주면

ObjectMapper를 사용하는 곳에서 공통적으로 반영이 되어진다.

@Bean
public ObjectMapper customObjectMapper() {
	return new ObjectMapper()
    	.registerModule(new JavaTimeModule())
        .setSeriallizationInclusion(Include.NON_EMPTY)
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .enable(SerializationFeature.INDENT_OUTPUT);
}

 

✅ ModelMapper

해당 라이브러리는 Dto > Entity / Entity > Dto 간의 객체 변화를 해주는 라이브러리다.

 

결론적으로 저의 프로젝트에서는 ModelMapper을 사용하였다.

그 이유는, ObejctMapper 사용 시, 순환 참조에 대한 오류를 잡기 위해 양방향 관계의 필드에 모두 해당 참조를 방지해주는
어노테이션을 붙여야했고,
단순히 비즈니스 로직에서 객체 > 객체 변환인데불필요하게 직렬화, 역직렬화를 사용할 필요가 없다고 생각하였다.

 

따라서 ModelMapper 사용 방법을 정리해 보았다.

 

✔️ CASE 1. Dto와 Entity가 동일한 필드명일 때

ModelMapper modelMapper = new ModelMapper();
modelMapper.map(entity, AccommodationDto.class);

ModelMapper 객체를 생성하여 map() 메소드를 통해 변환이 가능하다.

map 메소드의 인자는 - map(source, target) 이다.

 

✔️ CASE 1. Dto와 Entity가 다른 필드명일 때

ModelMapper modelMapper = new ModelMapper();
List<AccommoFacilityInfoDto.Res> facilityList = new ArrayList<>();
for (AccommoFacilityInfo info :entity.getAccommoFacilityInfos()) {
    facilityList.add(AccommoFacilityInfoDto.Res.builder()
            .facilityName(info.getPopularFacility().getName())
            .logoUrl(info.getPopularFacility().getLogoUrl())
            .sort(info.getSort()).build());
}
PropertyMap<Accommodations, AccommodationDto> map = new PropertyMap<Accommodations, AccommodationDto>() {
    protected void configure() {
        map().setFacilityInfoRes(facilityList);
    }
};
modelMapper.addMappings(map);
return modelMapper.map(entity, AccommodationDto.class);

위와 같이 ModelMapper에서 제공해주는 PropertyMap을 통해 맵핑 룰을 직접 설정하여 

ModelMapper에 넣어주면 필드 명이 달라도 값이 잘 들어가게 된다.