안녕하세요. J4J입니다.
이번 포스팅은 엔티티와 DTO 간 변환을 도와주는 MapStruct에 대해 적어보는 시간을 가져보려고 합니다.
들어가기에 앞서 이전 포스팅에서 MapStruct와 유사한 역할을 해주는 ModelMapper의 사용법에 대해 작성을 했었습니다.
ModelMapper에 대해 알고 싶으신 분들은 이곳으로 방문해주시면 됩니다.
MapStruct란?
MapStruct는 엔티티와 DTO 간에 변환할 때 자동으로 매핑시켜 변환되도록 도와주는 라이브러리입니다.
매핑해줄 클래스에는 setter가 있어야 하고 매핑이 되는 클래스에는 getter가 있어야 사용 가능합니다.
또한 추가적인 인터페이스를 작성해야 되고 maven install을 통해 작성된 인터페이스에 맞는 구현 클래스도 만들어져 있어야 합니다.
MapStruct는 최근들어서 각광받고 있는 것으로 보이고 이전에는 ModelMapper를 주로 사용했던 것으로 보입니다.
MapStruct가 ModelMapper보다 장점이라고 생각되는 것들은 다음과 같은 것들이 있습니다.
- 매핑 속도가 빠름
- 명시적임 (변수들이 어떻게 매핑되는지 확인 가능)
- 컴파일 단계에서 에러 확인 가능
- 변수 명의 제약이 덜함
그에 비해 단점이라고 생각되는 것은 파일 개수가 많아지는 것밖에 없습니다.
MapStruct 문서를 보시면 maven, gradle 등 설정 방법들이 나오는데 저는 maven을 이용하여 어떻게 사용되는지를 보여드리도록 하겠습니다.
테이블 / 엔티티 / DTO
이번 주제는 ModelMapper에서 사용했던 팀과 사람의 1:n 매핑을 그대로 해보겠습니다.
이번에도 JPA의 구체적인 설정은 언급하지 않을 것이니 관련된 것이 필요하신 분들은 다음 글들을 참고하시길 바랍니다.
2021.03.21 - [Spring/JPA] - [JPA] JPA 환경설정
2021.03.22 - [Spring/JPA] - [JPA] JPA Repository설정 및 CRUD
- DB 테이블 및 데이터
create table team (
id int auto_increment,
name varchar(50),
value int,
primary key(id)
);
insert into team(name, value) values ('A팀', 220000000);
insert into team(name, value) values ('B팀', 253000000);
insert into team(name, value) values ('C팀', 192300000);
create table person (
id int auto_increment,
name varchar(50),
team_id int,
primary key(id)
);
insert into person(name, team_id) values ('김장어', 1);
insert into person(name, team_id) values ('장연어', 1);
insert into person(name, team_id) values ('정문어', 2);
insert into person(name, team_id) values ('오징어', 2);
insert into person(name, team_id) values ('쭈꾸미', 2);
insert into person(name, team_id) values ('돌고래', 3);
insert into person(name, team_id) values ('백상어', 3);
- 엔티티 클래스
package com.spring.mapStruct.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "team")
public class TeamEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int value;
}
package com.spring.mapStruct.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "person")
public class PersonEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private TeamEntity team;
}
- DTO 클래스
package com.spring.mapStruct.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TeamDto {
private int id;
private String name;
private int value;
}
package com.spring.mapStruct.dto;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Data;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonDto {
private int id;
private String name;
private int teamId;
}
공통 설정
제가 설정하는 방법은 Lombok을 필수로 사용해야 됩니다.
Lombok사용환경이 세팅되어 있지 않다면 여기를 참고해주세요.
[ 1. pom.xml에 maven 설정 ]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
...
<dependencies>
<!-- Map Struct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
...
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
...
</plugins>
</build>
</project>
혹시나 똑같이 설정을 하셨는데도 정상적으로 동작되지 않으면 version이 올바르게 설정되었는지도 확인하시길 바랍니다.
저 같은 경우는 maven-compiler-plugin이 2.5.1버전으로 되어있어서 안되가지고 엄청 헤맸었습니다 ㅠㅠ.
[ 2. Default Mapper 생성 (필수 X) ]
package com.spring.mapStruct.mapper;
import java.util.List;
public interface StructMapper<D, E> {
D toDto(E entity);
E toEntity(D dto);
List<D> toDtoList(List<E> entityList);
List<E> toEntityList(List<D> dtoList);
}
방법 1: 연관관계가 없는 경우
Team과 Person중 연관관계가 없는 것은 Team에 해당됩니다.
연관관계가 없고 변수명이 엔티티와 DTO가 모두 동일할 경우에는 다음과 같이 사용하시면 됩니다.
[ 1. TeamMapper 생성 ]
package com.spring.mapStruct.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import com.spring.mapStruct.dto.TeamDto;
import com.spring.mapStruct.entity.TeamEntity;
@Mapper
public interface TeamMapper extends StructMapper<TeamDto, TeamEntity> {
TeamMapper INSTANCE = Mappers.getMapper(TeamMapper.class);
// extends를 안할 경우 다음 코드 추가
/*
TeamDto toDto(TeamEntity entity);
TeamEntity toEntity(TeamDto dto);
List<TeamDto> toDtoList(List<TeamEntity> entityList);
List<TeamEntity> toEntityList(List<TeamDto> dtoList);
*/
}
[ 2. 프로젝트 우 클릭 → Run As → Maven clean ]
Maven clean시 출력되는 로그
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< com.spring:mapStruct >------------------------
[INFO] Building jpa_mapStruct 1.0.0-BUILD-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ mapStruct ---
[INFO] Deleting C:\Users\User\Desktop\tistory_spring\jpa_mapStruct\target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.444 s
[INFO] Finished at: 2021-04-03T14:44:36+09:00
[INFO] ------------------------------------------------------------------------
[ 3. 프로젝트 우 클릭 → Run As → Maven install ]
Maven install시 출력되는 로그
...
[INFO] --- maven-install-plugin:2.4:install (default-install) @ mapStruct ---
[INFO] Installing C:\Users\User\Desktop\tistory_spring\jpa_mapStruct\target\mapStruct-1.0.0-BUILD-SNAPSHOT.war to C:\Users\User\.m2\repository\com\spring\mapStruct\1.0.0-BUILD-SNAPSHOT\mapStruct-1.0.0-BUILD-SNAPSHOT.war
[INFO] Installing C:\Users\User\Desktop\tistory_spring\jpa_mapStruct\pom.xml to C:\Users\User\.m2\repository\com\spring\mapStruct\1.0.0-BUILD-SNAPSHOT\mapStruct-1.0.0-BUILD-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 15.726 s
[INFO] Finished at: 2021-04-03T14:47:43+09:00
[INFO] ------------------------------------------------------------------------
[ 4. TeamMapperImpl 클래스 자동 생성 확인 ]
package com.spring.mapStruct.mapper;
import com.spring.mapStruct.dto.TeamDto;
import com.spring.mapStruct.entity.TeamEntity;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-04-03T14:49:30+0900",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_111 (Oracle Corporation)"
)
public class TeamMapperImpl implements TeamMapper {
@Override
public TeamDto toDto(TeamEntity entity) {
if ( entity == null ) {
return null;
}
TeamDto teamDto = new TeamDto();
teamDto.setId( entity.getId() );
teamDto.setName( entity.getName() );
teamDto.setValue( entity.getValue() );
return teamDto;
}
@Override
public TeamEntity toEntity(TeamDto dto) {
if ( dto == null ) {
return null;
}
TeamEntity teamEntity = new TeamEntity();
teamEntity.setId( dto.getId() );
teamEntity.setName( dto.getName() );
teamEntity.setValue( dto.getValue() );
return teamEntity;
}
@Override
public List<TeamDto> toDtoList(List<TeamEntity> entityList) {
if ( entityList == null ) {
return null;
}
List<TeamDto> list = new ArrayList<TeamDto>( entityList.size() );
for ( TeamEntity teamEntity : entityList ) {
list.add( toDto( teamEntity ) );
}
return list;
}
@Override
public List<TeamEntity> toEntityList(List<TeamDto> dtoList) {
if ( dtoList == null ) {
return null;
}
List<TeamEntity> list = new ArrayList<TeamEntity>( dtoList.size() );
for ( TeamDto teamDto : dtoList ) {
list.add( toEntity( teamDto ) );
}
return list;
}
}
[ 5. 단위 테스트 ]
package com.spring.mapStruct;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.mapStruct.config.RootContext;
import com.spring.mapStruct.dto.TeamDto;
import com.spring.mapStruct.entity.TeamEntity;
import com.spring.mapStruct.mapper.TeamMapper;
import com.spring.mapStruct.repository.PersonJpaRepository;
import com.spring.mapStruct.repository.TeamJpaRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class StructTest {
@Autowired
TeamJpaRepository teamJpaRepository;
@Autowired
PersonJpaRepository personJpaRepository;
@Test
public void structTest1() {
TeamEntity teamEntity = teamJpaRepository.findById(1).get();
TeamDto teamDto = TeamMapper.INSTANCE.toDto(teamEntity);
log.info(teamDto.toString());
TeamEntity nextTeamEntity = TeamMapper.INSTANCE.toEntity(teamDto);
log.info(nextTeamEntity.toString());
}
@Test
public void structTest2() {
List<TeamEntity> teamEntityList = teamJpaRepository.findAll();
List<TeamDto> teamDtoList = TeamMapper.INSTANCE.toDtoList(teamEntityList);
log.info(teamDtoList.toString());
List<TeamEntity> nextTeamEntityList = TeamMapper.INSTANCE.toEntityList(teamDtoList);
log.info(nextTeamEntityList.toString());
}
}
테스트코드를 실행하면 다음과 같이 서로 매핑되며 올바르게 저장되는 것을 확인할 수 있습니다.
Hibernate:
select
teamentity0_.id as id1_1_0_,
teamentity0_.name as name2_1_0_,
teamentity0_.value as value3_1_0_
from
team teamentity0_
where
teamentity0_.id=?
INFO : com.spring.mapStruct.StructTest - TeamDto(id=1, name=A팀, value=220000000)
INFO : com.spring.mapStruct.StructTest - TeamEntity(id=1, name=A팀, value=220000000)
Hibernate:
/* select
generatedAlias0
from
TeamEntity as generatedAlias0 */ select
teamentity0_.id as id1_1_,
teamentity0_.name as name2_1_,
teamentity0_.value as value3_1_
from
team teamentity0_
INFO : com.spring.mapStruct.StructTest - [TeamDto(id=1, name=A팀, value=220000000), TeamDto(id=2, name=B팀, value=253000000), TeamDto(id=3, name=C팀, value=192300000)]
INFO : com.spring.mapStruct.StructTest - [TeamEntity(id=1, name=A팀, value=220000000), TeamEntity(id=2, name=B팀, value=253000000), TeamEntity(id=3, name=C팀, value=192300000)]
방법 2: 연관관계가 있는 경우
Team과 Person 중 연관관계가 있는 경우는 Person에 해당됩니다.
[ 1. PersonMapper 생성 ]
package com.spring.mapStruct.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import com.spring.mapStruct.dto.PersonDto;
import com.spring.mapStruct.entity.PersonEntity;
import com.spring.mapStruct.entity.TeamEntity;
@Mapper
public interface PersonMapper extends StructMapper<PersonDto, PersonEntity> {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Override
@Mapping(source = "team.id", target = "teamId") // 변수명이 다를 경우 / source = Entity, target = DTO
// @Mapping(target = "teamId", constant = "20") // 임의로 집어넣고 싶을 경우
PersonDto toDto(PersonEntity entity);
@Override
default PersonEntity toEntity(PersonDto dto) { // 직접 설정, 직접 설정할 경우는 Impl클래스에 나타나지 않음
PersonEntity personEntity = new PersonEntity();
personEntity.setId(dto.getId());
personEntity.setName(dto.getName());
TeamEntity teamEntity = new TeamEntity();
teamEntity.setId(dto.getTeamId());
personEntity.setTeam(teamEntity);
return personEntity;
}
// toDto, toEntity와 동일한 방법으로 리스트 매핑
/*
@Override
List<PersonDto> toDtoList(List<PersonEntity> entityList);
@Override
List<PersonEntity> toEntityList(List<PersonDto> dtoList);
*/
}
[ 2 ~ 4는 방법 1과 동일 ]
[ 5. 단위 테스트 ]
package com.spring.mapStruct;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.mapStruct.config.RootContext;
import com.spring.mapStruct.dto.PersonDto;
import com.spring.mapStruct.entity.PersonEntity;
import com.spring.mapStruct.mapper.PersonMapper;
import com.spring.mapStruct.repository.PersonJpaRepository;
import com.spring.mapStruct.repository.TeamJpaRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class StructTest {
@Autowired
TeamJpaRepository teamJpaRepository;
@Autowired
PersonJpaRepository personJpaRepository;
@Test
public void structTest3() {
PersonEntity personEntity = personJpaRepository.findById(1).get();
PersonDto personDto = PersonMapper.INSTANCE.toDto(personEntity);
log.info(personDto.toString());
PersonEntity nextPersonEntity = PersonMapper.INSTANCE.toEntity(personDto);
log.info(nextPersonEntity.toString());
}
@Test
public void structTest4() {
List<PersonEntity> personEntityList = personJpaRepository.findAll();
List<PersonDto> personDtoList = PersonMapper.INSTANCE.toDtoList(personEntityList);
log.info(personDtoList.toString());
List<PersonEntity> nextPersonEntityList = PersonMapper.INSTANCE.toEntityList(personDtoList);
log.info(nextPersonEntityList.toString());
}
}
코드를 실행하면 다음과 같이 올바르게 매핑되는 것을 확인할 수 있습니다.
Hibernate:
select
personenti0_.id as id1_0_0_,
personenti0_.name as name2_0_0_,
personenti0_.team_id as team_id3_0_0_,
teamentity1_.id as id1_1_1_,
teamentity1_.name as name2_1_1_,
teamentity1_.value as value3_1_1_
from
person personenti0_
left outer join
team teamentity1_
on personenti0_.team_id=teamentity1_.id
where
personenti0_.id=?
INFO : com.spring.mapStruct.StructTest - PersonDto(id=1, name=김장어, teamId=1)
INFO : com.spring.mapStruct.StructTest - PersonEntity(id=1, name=김장어, team=TeamEntity(id=1, name=null, value=0))
Hibernate:
/* select
generatedAlias0
from
PersonEntity as generatedAlias0 */ select
personenti0_.id as id1_0_,
personenti0_.name as name2_0_,
personenti0_.team_id as team_id3_0_
from
person personenti0_
Hibernate:
select
teamentity0_.id as id1_1_0_,
teamentity0_.name as name2_1_0_,
teamentity0_.value as value3_1_0_
from
team teamentity0_
where
teamentity0_.id=?
Hibernate:
select
teamentity0_.id as id1_1_0_,
teamentity0_.name as name2_1_0_,
teamentity0_.value as value3_1_0_
from
team teamentity0_
where
teamentity0_.id=?
Hibernate:
select
teamentity0_.id as id1_1_0_,
teamentity0_.name as name2_1_0_,
teamentity0_.value as value3_1_0_
from
team teamentity0_
where
teamentity0_.id=?
INFO : com.spring.mapStruct.StructTest - [PersonDto(id=1, name=김장어, teamId=1), PersonDto(id=2, name=장연어, teamId=1), PersonDto(id=3, name=정문어, teamId=2), PersonDto(id=4, name=오징어, teamId=2), PersonDto(id=5, name=쭈꾸미, teamId=2), PersonDto(id=6, name=돌고래, teamId=3), PersonDto(id=7, name=백상어, teamId=3)]
INFO : com.spring.mapStruct.StructTest - [PersonEntity(id=1, name=김장어, team=TeamEntity(id=1, name=null, value=0)), PersonEntity(id=2, name=장연어, team=TeamEntity(id=1, name=null, value=0)), PersonEntity(id=3, name=정문어, team=TeamEntity(id=2, name=null, value=0)), PersonEntity(id=4, name=오징어, team=TeamEntity(id=2, name=null, value=0)), PersonEntity(id=5, name=쭈꾸미, team=TeamEntity(id=2, name=null, value=0)), PersonEntity(id=6, name=돌고래, team=TeamEntity(id=3, name=null, value=0)), PersonEntity(id=7, name=백상어, team=TeamEntity(id=3, name=null, value=0))]
참조
Spring MapStruct Getting Started - 1(Transfer Object Pattern)
이상으로 엔티티와 DTO 간 변환을 도와주는 MapStruct에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'Spring > JPA' 카테고리의 다른 글
[JPA] 자동으로 Datetime 설정하기 (0) | 2022.04.07 |
---|---|
[JPA] could not initialize proxy - no Session 에러 (0) | 2021.04.06 |
[JPA] ModelMapper, Entity ↔ DTO 변환 (0) | 2021.04.01 |
[JPA] MyBatis와 동시 사용 (DTO/엔티티 통합, 연관관계 매핑) (0) | 2021.03.31 |
[JPA] MyBatis와 동시 사용 (DTO/엔티티 분리) (7) | 2021.03.30 |
댓글