안녕하세요. J4J입니다.
이번 포스팅은 엔티티와 DTO 간의 변환을 도와주는 ModelMapper에 대해 적어보는 시간을 가져보려고 합니다.
JPA로 이것저것 해보다가 궁금한 사항이 생겼습니다.
데이터베이스 테이블과 매핑하여 사용되는 엔티티를 view layer와 주고받아도 될까???
MyBatis만 사용하던 상황을 생각해보면 이런저런 상황들 때문에 dto에는 데이터베이스에 들어있는 값만 변수로 지정돼있지 않습니다.
이와 같은 상황을 동일하게 엔티티에 적용한다고 생각해봤을때 테이블과 자동으로 매핑되는 엔티티에는 치명적일 것이라고 생각이 들었습니다.
관련된 내용을 찾아보면서 다른 사람들도 저와 유사한 고민을 하고 있다는 것을 알게되었습니다.
그리고 궁금증에 대한 해답으로 DB layer와 view layer에 각각 엔티티와 DTO를 사용하는 것을 알게 되었습니다.
구체적으로는 Service단을 중심으로 DB ↔ Service는 엔티티, Service → Controller는 DTO를 사용하는 것입니다.
해당 방법을 알고 난 뒤로도 또 다른 궁금증이 생겼습니다.
위의 방법을 적용하기 위해서는 엔티티와 DTO가 서로 변환이 돼야 하는 작업이 필요한데 설마 변환할 때 변수 하나하나를 일일이 작성하는 것은 아니겠지...??
찾아본 결과 특정한 상황에서는 일일이 작성할 수도 있지만 일반적으로는 자동으로 매핑시켜주는 ModelMapper와 MapStruct를 사용하는 것을 알게 되었습니다.
이들 중 이번 포스팅에서는 ModelMapper의 사용방법에 대해 다뤄볼 것이고 MapStruct는 다음 포스팅에서 다뤄보도록 하겠습니다.
ModelMapper란?
ModelMapper는 위에서 언급한 대로 엔티티와 DTO 간에 변환할 때 자동으로 매핑시켜주는 라이브러리입니다.
매핑해줄 클래스에는 setter가 있어야 하고 매핑이 되는 클래스에서 getter가 있어야 사용 가능합니다.
기본적으로 ModelMapper에서 제공해주는 map메서드를 이용하여 변환할 수 있고 클래스 내부에 있는 변수들의 이름을 분석하여 자동 매핑시켜주는 방식입니다.
저는 map메서드를 간단히 사용해보는 것만 해볼 것이기 때문에 더 자세하게 알고 싶으신 분들은 ModelMapper 문서를 참고해주시면 됩니다.
테이블 / 엔티티 / DTO
이번 주제는 팀과 사람의 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.modelMapper.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 Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int value;
}
package com.spring.modelMapper.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 Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
- DTO 클래스
package com.spring.modelMapper.dto;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Data;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TeamDto {
private int id;
private String name;
private int value;
}
package com.spring.modelMapper.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;
}
공통 설정
[ 1. pom.xml에 maven 설정 ]
<!-- Model Mapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.4.0</version>
</dependency>
[ 2. ModelMapper 호출 방법 ]
ModelMapper를 호출하는 방법은 두 가지가 있습니다.
첫 번째는 사용되는 클래스마다 다음과 같이 ModelMapper인스턴스를 생성해주는 것입니다.
public class AServiceImpl implements AService {
public int methodA() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.map(source, destination);
}
...
}
두 번째는 빈으로 등록하여 클래스마다 등록된 빈을 호출하는 방법입니다.
예를 들어 DB설정 파일인 RootContext에 다음과 같이 ModelMapper을 빈으로 등록해줍니다.
public class RootContext {
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
...
}
그리고 사용되는 클래스에 다음과 같이 호출하며 사용해줄 수 있습니다.
public class AServiceImpl implements AService {
@Autowired
ModelMapper modelMapper;
public int methodA() {
modelMapper.map(source, destination);
}
...
}
저는 두 번째 방법으로 코드를 작성해보겠습니다.
방법 1: 연관관계가 없는 경우
위에서 생성한 Team과 Person 중 연관관계가 없는 것은 Team에 해당되겠습니다.
연관관계가 없는 경우는 클래스 내부에 자료형 변수밖에 없기 때문에 변수 이름을 동일하게만 해주면 됩니다.
현재 Team과 TeamDto는 변수명이 모두 동일하기 때문에 다음과 같이 ModelMapper를 이용하여 매핑을 해줄 수 있습니다.
package com.spring.modelMapper;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.modelMapper.config.RootContext;
import com.spring.modelMapper.dto.TeamDto;
import com.spring.modelMapper.entity.Team;
import com.spring.modelMapper.repository.PersonJpaRepository;
import com.spring.modelMapper.repository.TeamJpaRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class MapperTest {
@Autowired
TeamJpaRepository teamJpaRepository;
@Autowired
PersonJpaRepository personJpaRepository;
@Autowired
ModelMapper modelMapper;
@Test
public void modelMapper1() { // 객체 하나
Team team = teamJpaRepository.findById(1).get();
TeamDto teamDto = modelMapper.map(team, TeamDto.class);
log.info(teamDto.toString());
Team nextTeam = modelMapper.map(teamDto, Team.class);
log.info(nextTeam.toString());
}
@Test
public void modelMapper2() { // 리스트
List<Team> teamList = teamJpaRepository.findAll();
List<TeamDto> teamDtoList = teamList.stream().map(team -> modelMapper.map(team, TeamDto.class)).collect(Collectors.toList());
log.info(teamDtoList.toString());
List<Team> nextTeamList = teamDtoList.stream().map(teamDto -> modelMapper.map(teamDto, Team.class)).collect(Collectors.toList());
log.info(nextTeamList.toString());
}
}
실행하면 다음과 같은 로그가 출력됩니다.
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_,
team0_.value as value3_1_0_
from
team team0_
where
team0_.id=?
INFO : com.spring.modelMapper.MapperTest - TeamDto(id=1, name=A팀, value=220000000)
INFO : com.spring.modelMapper.MapperTest - Team(id=1, name=A팀, value=220000000)
Hibernate:
/* select
generatedAlias0
from
Team as generatedAlias0 */ select
team0_.id as id1_1_,
team0_.name as name2_1_,
team0_.value as value3_1_
from
team team0_
INFO : com.spring.modelMapper.MapperTest - [TeamDto(id=1, name=A팀, value=220000000), TeamDto(id=2, name=B팀, value=253000000), TeamDto(id=3, name=C팀, value=192300000)]
INFO : com.spring.modelMapper.MapperTest - [Team(id=1, name=A팀, value=220000000), Team(id=2, name=B팀, value=253000000), Team(id=3, name=C팀, value=192300000)]
양쪽으로 변환이 되어도 동일하게 값이 저장되는 것이 확인되고 있습니다.
방법 2: 연관관계가 있는 경우
연관관계가 있는 경우는 Team과 Person 중 Person에 해당됩니다.
Person클래스는 team_id라는 외래 키를 가지고 연관관계 매핑이 되어 있습니다.
이런 경우에는 PersonDto클래스의 teamId값이 Person클래스의 team이라는 이름의 객체에 id라는 이름의 변수 값으로 매핑이 됩니다.
반대로 Person클래스의 team객체에 들어있는 id라는 이름의 변수가 PersonDto클래스의 teamId라는 이름의 변수값으로 매핑이 됩니다.
결론적으로 코드 자체는 변수 이름만 잘 매핑이 되어있다면 방법 1과 동일하게 됩니다.
package com.spring.modelMapper;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import com.spring.modelMapper.config.RootContext;
import com.spring.modelMapper.dto.PersonDto;
import com.spring.modelMapper.entity.Person;
import com.spring.modelMapper.repository.PersonJpaRepository;
import com.spring.modelMapper.repository.TeamJpaRepository;
import lombok.extern.slf4j.Slf4j;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class MapperTest {
@Autowired
TeamJpaRepository teamJpaRepository;
@Autowired
PersonJpaRepository personJpaRepository;
@Autowired
ModelMapper modelMapper;
@Test
public void modelMapper3() { // 객체 하나
Person person = personJpaRepository.findById(1).get();
PersonDto personDto = modelMapper.map(person, PersonDto.class);
log.info(personDto.toString());
Person nextPerson = modelMapper.map(personDto, Person.class);
log.info(nextPerson.toString());
}
@Test
public void modelMapper4() { // 리스트
List<Person> personList = personJpaRepository.findAll();
List<PersonDto> personDtoList = personList.stream().map(person -> modelMapper.map(person, PersonDto.class)).collect(Collectors.toList());
log.info(personDtoList.toString());
List<Person> nextPersonList = personDtoList.stream().map(personDto -> modelMapper.map(personDto, Person.class)).collect(Collectors.toList());
log.info(nextPersonList.toString());
}
}
로그도 다음과 같이 출력됩니다.
Hibernate:
select
person0_.id as id1_0_0_,
person0_.name as name2_0_0_,
person0_.team_id as team_id3_0_0_,
team1_.id as id1_1_1_,
team1_.name as name2_1_1_,
team1_.value as value3_1_1_
from
person person0_
left outer join
team team1_
on person0_.team_id=team1_.id
where
person0_.id=?
INFO : com.spring.modelMapper.MapperTest - PersonDto(id=1, name=김장어, teamId=1)
INFO : com.spring.modelMapper.MapperTest - Person(id=1, name=김장어, team=Team(id=1, name=null, value=0))
Hibernate:
/* select
generatedAlias0
from
Person as generatedAlias0 */ select
person0_.id as id1_0_,
person0_.name as name2_0_,
person0_.team_id as team_id3_0_
from
person person0_
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_,
team0_.value as value3_1_0_
from
team team0_
where
team0_.id=?
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_,
team0_.value as value3_1_0_
from
team team0_
where
team0_.id=?
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_,
team0_.value as value3_1_0_
from
team team0_
where
team0_.id=?
INFO : com.spring.modelMapper.MapperTest - [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.modelMapper.MapperTest - [Person(id=1, name=김장어, team=Team(id=1, name=null, value=0)), Person(id=2, name=장연어, team=Team(id=1, name=null, value=0)), Person(id=3, name=정문어, team=Team(id=2, name=null, value=0)), Person(id=4, name=오징어, team=Team(id=2, name=null, value=0)), Person(id=5, name=쭈꾸미, team=Team(id=2, name=null, value=0)), Person(id=6, name=돌고래, team=Team(id=3, name=null, value=0)), Person(id=7, name=백상어, team=Team(id=3, name=null, value=0))]
참조
Entity to DTO, DTO to Entity 그리고 ModelMapper
이상으로 엔티티와 DTO 간의 변환을 도와주는 ModelMapper에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'Spring > JPA' 카테고리의 다른 글
[JPA] could not initialize proxy - no Session 에러 (0) | 2021.04.06 |
---|---|
[JPA] MapStruct, Entity ↔ DTO 변환 (1) | 2021.04.03 |
[JPA] MyBatis와 동시 사용 (DTO/엔티티 통합, 연관관계 매핑) (0) | 2021.03.31 |
[JPA] MyBatis와 동시 사용 (DTO/엔티티 분리) (7) | 2021.03.30 |
[JPA] @Query, 직접 쿼리 작성 (1) | 2021.03.29 |
댓글