티스토리 뷰
Mapstruct 를 활용하면 Entity ↔ DTO 간 필드들을 쉽게 맵핑하여 사용할 수 있습니다.
필드가 많아지면 일일이 손수 맵핑하는게 여간 귀찮은 일이 아니며,
생각없이 맵핑하다가 타이핑 실수로 맵핑이 안되는 경우가 발생 할 수 있기 때문에
Mapstruct 를 잘 활용하는 것이 좋습니다.
이번 포스트에서는 mapstruct 로 간단히 entity 와 DTO 간 필드를 맵핑하는 법과
@Mapping 어노테이션을 활용하여 필드 데이터를 조작 맵핑하는 방법을 다뤄보려고 합니다.
더 고급 기술들이 있지만 저도 아직 실 사용하지 않는터라 다 다루지는 않겠습니다.
Mapstruct 를 사용하기 위해서는 아래의 dependency 를 추가해줍니다.
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
mapstruct 인터페이스를 작성 후 빌드하면 build 폴더에 구현체가 생성됩니다.
@Mapper(componentModel = "spring")
public interface SampleToMapper {
SampleDTO toDTO(SampleEntity e);
SampleEntity toEntity(SampleDTO dto);
}
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-02-23T17:13:49+0900",
comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.6.jar, environment: Java 1.8.0_144 (Oracle Corporation)"
)
@Component
public class SampleToMapperImpl implements SampleToMapper {
@Override
public SampleDTO toDTO(SampleEntity e) {
if ( e == null ) {
return null;
}
SampleDTO sampleDTO = new SampleDTO();
sampleDTO.setGuid( e.getGuid() );
sampleDTO.setUserName( e.getUserName() );
sampleDTO.setUserEmail( e.getUserEmail() );
return sampleDTO;
}
@Override
public SampleEntity toEntity(SampleDTO d) {
if ( d == null ) {
return null;
}
SampleEntity.SampleEntityBuilder sampleEntity = SampleEntity.builder();
sampleEntity.userName( d.getUserName() );
sampleEntity.userEmail( d.getUserEmail() );
sampleEntity.guid( d.getGuid() );
return sampleEntity.build();
}
}
DTO에는 @Data 어노테이션 사용으로 setter로 맵핑해주고
Entity 에는 @setter 사용을 지양하기 때문에 @builder를 사용해서 필드를 맵핑해줍니다.
이렇게 해서 서비스 단에서 DTO ↔ Entity 간 필드를 손쉽게 맵핑할 수 있습니다.
private final SampleToMapper sampleToMapper;
SampleDTO dto = sampleToMapper.toDTO(sampleEntity);
...
SampleEntity entity = sampleToMapper.toEntity(sampleDTO);
...
위와 같이 선언한 필드명이 동일할 경우에는 mapstruct가 알아서 맵핑을 잘해주는데
DTO 와 Entity 간 필드명이 상이할 경우에는 맵핑이 되질 않습니다.
이럴 때 @Mapping 어노테이션을 사용하여 이 필드는 이 필드로 맵핑해라 라고 명시해줘야 합니다.
만약 아래와 같이 DTO와 Entity 가 있다면 @Mapping 어노테이션을 활용합니다.
@Data
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ScheduleDTO {
private String start;
private String end;
private String title;
private String id;
private Boolean allDay;
private String customHtml;
@Entity
@Getter
@NoArgsConstructor
@DynamicInsert
public class ScheduleEntity {
@Column(name = "START_DATE")
private String startDate;
@Column(name = "END_DATE")
private String endDate;
@Column(name = "TITLE")
private String title;
@Column(name = "REG_DATE", updatable = false)
private LocalDateTime regDate;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "UUID")
private String uuid;
날짜필드와 id 필드명이 다릅니다
이럴경우 필드명이 다르기 때문에 서로 맵핑이 되질 않습니다.
이럴때 아래와 같이 명시를 해주면 됩니다.
@Mapper(componentModel = "spring")
public interface ScheduleToMapper {
@Mapping(target = "id", source = "uuid")
@Mapping(target = "start", source = "startDate")
@Mapping(target = "end", source = "endDate")
@Mapping(target = "customHtml", expression = "java(ScheduleEntity.setCustomHtml(e))")
@Mapping(target = "allDay", constant = "true")
ScheduleDTO toDTO(ScheduleEntity e);
@Mapping(target = "uuid", source = "id")
@Mapping(target = "startDate", source = "start")
@Mapping(target = "endDate", source = "end")
ScheduleEntity toEntity(ScheduleDTO d);
}
리턴받는 쪽이 target 이고 인자쪽이 source 입니다.
이렇게 하면 필드명이 달라도 명시된 대로 맵핑이 올바르게 되는 것을 보실 수 있습니다.
@Override
public ScheduleDTO toDTO(ScheduleEntity e) {
if ( e == null ) {
return null;
}
ScheduleDTO.ScheduleDTOBuilder ScheduleDTO = ScheduleDTO.builder();
ScheduleDTO.id( e.getUuid() );
ScheduleDTO.start( e.getStartDate() );
ScheduleDTO.end( e.getEndDate() );
ScheduleDTO.title( e.getTitle() );
ScheduleDTO.customHtml( ScheduleEntity.setCustomHtml(e) );
ScheduleDTO.allDay( true );
return ScheduleDTO.build();
}
@Override
public ScheduleEntity toEntity(ScheduleDTO d) {
if ( d == null ) {
return null;
}
ScheduleEntity.ScheduleEntityBuilder ScheduleEntity = ScheduleEntity.builder();
ScheduleEntity.startDate( d.getStart() );
ScheduleEntity.endDate( d.getEnd() );
ScheduleEntity.title( d.getTitle() );
ScheduleEntity.uuid( d.getId() );
return ScheduleEntity.build();
}
빌드된 인터페이스 구현체를 보면 필드명이 다르지만 @Mapping 어노테이션으로 명시를 해줬기 때문에
잘 맵핑 해준 것을 볼 수 있습니다.
그 외에도 맵핑을 무시하는 ignore 옵션도 있고
값이 null 일때 디폴트값을 지정하는 defaultValue 옵션도 있고
아예 맵핑할때 값을 박아버리는 constant 옵션도 있습니다.
@Mapping(target = "allDay", constant = "true")
expression 으로 target 에 특정 로직을 적용할 때 사용합니다.
날짜 포맷팅을 한다던가 코드값에 따라 코드네임을 반환한다던가 그런 경우 사용합니다.
@Mapping(target = "customHtml", expression = "java(CampaignScheduleEntity.setCustomHtml(e))")
저는 조회한 데이터의 일부를 가지고 HTML 로 만들어줘야 하는 경우여서
Entity에 static method 를 하나 작성해서 사용했습니다.
mapstruct 가 맵핑하면서 일부 필드들에 데이터를 커스텀하여 넣을 수도 있어서
조회 후 필드들에 직접 커스텀 로직을 안태워도 되서 DTO ↔ Entity 간 맵핑의 번거로움을 많이 덜어낼 수 있었습니다.
'Java > Mapstruct' 카테고리의 다른 글
[Mapstruct] LocalDateTime To String Converter, 엔티티와 DTO간 타입이 다를때 맵핑하기 (feat.AttributeConverter) (0) | 2023.07.05 |
---|---|
[Mapstruct] 이클립스(eclipse) 로컬환경에서 구현체 적용/구동하는 설정법. (feat.STS) (10) | 2022.12.30 |