'ModelMapper'에 해당되는 글 2건

  1. 2020.04.29 :: Java - Model(Object) mapping을 위한 Mapstruct (맵스트럭트)!
  2. 2019.04.10 :: Spring - ModelMapper란? 2

오늘 다루어볼 내용은 Model mapping을 아주 쉽게 해주는 Mapstruct라는 라이브러리를 다루어볼 것이다. 그 전에 Model mapping에 많이 사용되는 ModelMapper와의 간단한 차이를 이야기해보면, Model Mapper는 Runtime 시점에 reflection으로 모델 매핑을 하게 되는데, 이렇게 런타임시에 리플렉션이 자주 이루어진다면 앱성능에 아주 좋지 않다. 하지만 Mapstruct는 컴파일 타임에 매핑 클래스를 생성해줘 그 구현체를 런타임에 사용하는 것이기 때문에 앱 사이즈는 조금 커질수 있지만 성능상 크게 이슈가 없어서 ModelMapper보다 더 부담없이 사용하기 좋다. 그리고 아주 다양한 기능을 제공하기 때문에 조금더 세밀한 매핑이 가능해진다.

 

바로 간단하게 Mapstruct를 맛보자.

 

build.gradle

build.gradle를 채워넣어보자 !

buildscript {
	ext {
		mapstructVersion = '1.3.1.Final'
	}
}

...

// Mapstruct
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

...

compileJava {
	options.compilerArgs = [
			'-Amapstruct.suppressGeneratorTimestamp=true',
			'-Amapstruct.suppressGeneratorVersionInfoComment=true'
	]
}

 

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
}
 
@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    Car to(CarDto carDto);
}
cs

 

사실 간단한 것이라, 어노테이션만 보아도 어떠한 기능을 제공하는지 확실하다. 하지만 각 클래스에 getter/setter 및 기본생성자는 꼭 생성해주자.(필드명이 동일할 일은 많이 없겠지만, 각 객체의 필드명이 동일하면 @Mapping 어노테이션은 생략가능하다.) 해당 코드로 테스트코드를 간단하게 작성하자.

 

1
2
3
4
5
6
7
8
9
10
11
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of("bmw x4""black");
        Car car = CarMapper.INSTANCE.to(carDto);
 
        assertEquals(carDto.getName(), car.getModelName());
        assertEquals(carDto.getColor(), car.getModelColor());
    }
}
cs

 

테스트가 성공하는 것을 볼 수 있다. 아주 간단하면서도 파워풀한 라이브러리인 것 같다. 그러면 실제로 생성된 코드를 한번 볼까?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class CarMapperImpl implements CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        car.setModelName( carDto.getName() );
        car.setModelColor( carDto.getColor() );
 
        return car;
    }
}
cs

 

우리가 작성하지 않았지만, 코드가 생성되었다. 리플렉션과 같이 비용이 큰 기술이 사용되지 않았다. 간단한 validation 코드와 생성자, getter, setter 코드 뿐이다. 이제 더 많은 Mapstruct 기능들을 살펴보자.

 

하나의 객체로 합치기

여러 객체의 필드값을 하나의 객체로 합치기가 가능하다. 특별히 다른 옵션을 넣는 것은 아니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserDto {
    private String name;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class AddressDto {
    private String si;
    private String dong;
}
 
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    UserInfo to(UserDto user, AddressDto address);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public UserInfo to(UserDto user, AddressDto address) {
        if ( user == null && address == null ) {
            return null;
        }
 
        UserInfo userInfo = new UserInfo();
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
 
        return userInfo;
    }
}
cs

 

이미 생성된 객체에 매핑

새로운 인스턴스를 생성하는 것이 아니라, 기존에 이미 생성되어 있는 객체에 매핑이 필요한 경우이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Mapper
public interface UserInfoMapper {
 
    UserInfoMapper INSTANCE = Mappers.getMapper(UserInfoMapper.class);
 
    @Mapping(source = "user.name", target = "userName")
    @Mapping(source = "address.si", target = "si")
    @Mapping(source = "address.dong", target = "dong")
    void write(UserDto user, AddressDto address, @MappingTarget UserInfo userInfo);
}
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
public class UserInfoMapperImpl implements UserInfoMapper {
 
    @Override
    public void write(UserDto user, AddressDto address, UserInfo userInfo) {
        if ( user == null && address == null ) {
            return;
        }
 
        if ( user != null ) {
            userInfo.setUserName( user.getName() );
        }
        if ( address != null ) {
            userInfo.setDong( address.getDong() );
            userInfo.setSi( address.getSi() );
        }
    }
}
 
cs

 

타입 변환

대부분의 암시적인 자동 형변환이 가능하다. (Integer -> String ...) 다음은 조금 유용한 기능이다.

1
2
3
4
5
6
7
8
9
@Mapper
public interface CarMapper {
 
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
 
    @IterableMapping(numberFormat = "$#.00")
    List<String> prices(List<Integer> prices);
}
cs

 

만약 클래스에 있는 List<Integer> 필드값을 List<String>의 다른 포맷으로 변경하고 싶다면 @IterableMapping 어노테이션을 이용하자. 위와 비슷하게 날짜 데이터를 문자열로 변환하는 dateFormat이라는 옵션도 존재한다.

 

Source, Target mapping policy

매핑될 필드가 존재하지 않을 때, 엄격한 정책을 가져가기 위한 기능을 제공한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ja@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class Car {
    private String modelName;
    private String modelColor;
    private String modelPrice;
    private String description;
}
 
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class CarDto {
    private String name;
    private String color;
    private Integer price;
}
 
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 타겟이 되는 오브젝트 필드에 대한 정책을 가져간다. Car 클래스에는 description 필드가 있는데, CarDto 클래스에는 해당 필드가 존재하지 않기때문에 컴파일시 컴파일에러가 발생한다.(ERROR, IGNORE, WARN 정책존재) 만약 특정 필드는 해당 정책을 피하고 싶다면 아래와 같이 어노테이션하나를 달아준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

null 정책

Source가 null이거나 혹은 Source의 특정 필드가 null일때 적용가능한 정책이 존재한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 Source 오브젝트가 null일때, 기본생성자로 필드가 비어있는 Target 오브젝트를 반환해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(
            source = "description"
            target = "description"
            ignore = true,
            nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT
    )
    Car to(CarDto carDto);
 
}
cs

 

위 코드는 각 필드에 대해 null 정책을 부여한다. 만약 SET_TO_DEFAULT로 설정하면, List 일때는 빈 ArrayList를 생성해주고, String은 빈문자열, 특정 오브젝트라면 해당 오브젝트의 기본 생성자 등으로 기본값을 생성해준다.

 

특정 필드 매핑 무시

특정 필드는 매핑되지 않길 원한다면 @Mapping 어노테이션에 ignore = true 속성을 넣어준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL
)
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(source = "color", target = "modelColor")
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    @Mapping(target = "description", ignore = true)
    Car to(CarDto carDto);
 
}
 
public class MapstructTest {
 
    @Test
    public void test() {
        CarDto carDto = CarDto.of(
                "bmw x4",
                "black",
                10000,
                "description");
        Car car = CarMapper.INSTANCE.to(carDto);
        System.out.println(car.toString());
    }
}
 
result =>
 
Car(modelName=bmw x4, modelColor=black, modelPrice=$10000.00, description=null)
cs

 

매핑 전처리, 후처리

매핑하기 이전과 매핑 이후 특정 로직을 주입시킬 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Mapper(
        unmappedTargetPolicy = ReportingPolicy.ERROR,
        nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL,
        componentModel = "spring"
)
public abstract class CarMapper {
 
    @BeforeMapping
    protected void setColor(CarDto carDto, @MappingTarget Car car) {
        if (carDto.getName().equals("bmw x4")) {
            car.setModelColor("red");
        } else {
            car.setModelColor("black");
        }
 
    }
 
    @Mapping(source = "name", target = "modelName")
    @Mapping(target = "modelColor", ignore = true)
    @Mapping(source = "price", target = "modelPrice", numberFormat = "$#.00")
    public abstract Car to(CarDto carDto);
 
    @AfterMapping
    protected void setDescription(@MappingTarget Car car) {
        car.setDescription(car.getModelName() + " " + car.getModelColor());
    }
}
 
<Generate Code>
 
@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class CarMapperImpl extends CarMapper {
 
    @Override
    public Car to(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }
 
        Car car = new Car();
 
        setColor( carDto, car );
 
        car.setModelName( carDto.getName() );
        if ( carDto.getPrice() != null ) {
            car.setModelPrice( new DecimalFormat( "$#.00" ).format( carDto.getPrice() ) );
        }
        car.setDescription( carDto.getDescription() );
 
        setDescription( car );
 
        return car;
    }
}
cs

 

전처리와 후처리를 위한 메서드는 private을 사용해서는 안된다. 그 이유는 generate된 코드에 전,후 처리 메서드가 들어가는 것이 아니라 추상 클래스에 있는 메서드를 그대로 사용하기 때문이다.

 

이외에도 지원하는 기능이 너무 많다.. 다 못쓰겠다. 아래 공식 레퍼런스를 살펴보자..

 

 

MapStruct 1.3.1.Final Reference Guide

The mapping of collection types (List, Set etc.) is done in the same way as mapping bean types, i.e. by defining mapping methods with the required source and target types in a mapper interface. MapStruct supports a wide range of iterable types from the Jav

mapstruct.org

 

posted by 여성게
:
Web/Spring 2019. 4. 10. 21:29

컨트롤러에서 요청을 엔티티객체로 받는 경우가 있다. 이럴경우 받은 엔티티 객체로 DB까지 로직들이 순차적으로 수행이 될것이다. 그런데 만약 엔티티를 조회하거나, 리스트를 조회하는 경우가 있다고 가정해보자. 그렇다면 요청을 받고 엔티티객체를 조회한 후에 컨트롤러에서 응답값으로 ResponseEntity body에 엔티티객체를 실어 보낼 수 있다. 하지만 여기에서 만약 엔티티객체에서 내가 보내기 싫은 데이터가 포함되어있다면? 그것이 만약 유저정보에 대한 것이고, 그 객체에 패스워드까지 존재한다면? 상상하기 싫은 상황일 것이다. 여기서 해결할 수 있는 방법은 몇가지 있다. 예를 들어 @JsonIgnore,@JsonProperty로 응답을 JSON으로 반환하기 할때 원하는 인스턴스 변수를 제외하고 보낼 수도 있고, 응답용 DTO를 만들어서 응답을 다른 객체로 convert한 후에 보낼 수도 있을 것이다. 어노테이션을 이용한 방법은 추후에 다루어볼것이고, 오늘 다루어 볼 것은 응답용 DTO를 만들어서 응답을 보내는 예제이다. 

 

만약 응답용 DTO를 만들어서 내가 응답으로 내보내고 싶은 정보만 인스턴스변수로 set해서 보내면 될것이다. 하지만 여기서 아주 노가다가 있을 것이다. 그것은 따로 Util 용 Method로 빼서 일일이 set,get하는 방법일 것이다. 만약 한두개의 인스턴스 변수라면 상관없지만 만약 응답으로 내보낼 인스턴수 변수가 아주 많다면 이만한 노가다성 작업도 없을 것이다. 오늘 여기서 소개해줄 해결사가 바로 ModelMapper라는 객체이다. 바로 예제 코드로 들어가겠다.

 

오늘 만들어볼 예제는 이전 포스팅에서 다루어봤던 미완성 GenericController의 응용이다. 자세한 설명은 밑의 링크에서 참고하면 될듯하다.

▶︎▶︎▶︎2019/03/22 - [Spring] - Spring - Springboot GenericController(제네릭컨트롤러), 컨트롤러 추상화

 

1
2
3
4
5
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.0</version>
        </dependency>
cs

ModelMapper를 사용하기 위해서 의존성을 추가해준다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class CommonBean {
    
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}
cs

사용할 ModelMapper클래스를 빈으로 등록해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface DomainMapper {
    
    public <D,E> D convertToDomain(E source,Class<extends D> classLiteral);
}
 
@Component
public class DomainMapperImpl implements DomainMapper{
    
    private ModelMapper modelMapper;
    
    public DomainMapperImpl(ModelMapper modelMapper) {
        this.modelMapper=modelMapper;
    }
    
    /*
     * 공통 매퍼
     */
    @Override
    public <D, E> D convertToDomain(E source, Class<extends D> classLiteral) {
        return modelMapper.map(source, classLiteral);
    }
 
}
cs

그리고 ModelMapper를 이용하여 추후에 많은 유틸메소드(도메인 클래스의 인스턴스를 조작하기 위함)를 만들 가능성이 있을 수도 있기 때문에 따로 유틸로 클래스를 만들어서 해당 클래스내에서 ModelMapper를 활용할 것이다. 여기서는 따로 유틸메소드는 없고 하나의 메소드만 있다. 이것은 엔티티클래스와 매핑할 도메인클래스의 클래스리터럴(Domain.class)를 매개변수로 받아서 ModelMapper.map(엔티티클래스,도메인클래스리터럴) 메소드에 전달한다. 메소드의 반환값으로는 도메인클래스를 반환한다. 여기서 도메인클래스 필드에 엔티티클래스의 필드를 매핑할때는 필드명으로 비교를 한다. 아래에서도 다시 설명하겠지만, 도메인 클래스를 만들때는 엔티티 클래스에서 Http Response로 보내고 싶은 데이터의 필드의 이름과 동일하게 도메인클래스의 필드명으로 만들어줘야하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * BaseEntity 추상클래스
 * 해당 추상클래스를 상속할때 @Tablegenerator는 상속하는 클래스에서 정의해야함
 * 또한 id field의 칼럼속성도 필요할때에 재정의해야함
 * @author yun-yeoseong
 *
 */
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Setter
@Getter
@EqualsAndHashCode(of="id")
public abstract class BaseEntity<extends BaseEntity<?>> implements Comparable<T>{
    
    @Id
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    private long id;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="CREATED_DATE",nullable=false,updatable=false)
    @CreatedDate
    private LocalDateTime createdDate;
    
    @JsonProperty(access=Access.READ_ONLY)
    @Column(name="UPDATED_DATE",nullable=false)
    @LastModifiedDate
    private LocalDateTime modifiedDate;
    
//    @Column(name="CREATED_BY",updatable=false/*,nullable=false*/)
//    @CreatedBy
    
//    @Column(name="UPDATED_BY",nullable=false)
//    @LastModifiedBy
 
    @Override
    public int compareTo(T o) {
        if(this == o) return 0;
        return Long.compare(this.getId(), o.getId());
    }
 
}
 
/**
 * Main Category Entity
 * @author yun-yeoseong
 *
 */
@Entity
@Table(name="MAIN_CATEGORY"
        ,indexes=@Index(columnList="MAIN_CATEGORY_NAME",unique=false))
@AttributeOverride(name = "id",column = @Column(name = "MAIN_CATEGORY_ID"))
@TableGenerator(name="SEQ_GENERATOR",table="TB_SEQUENCE",
                pkColumnName="SEQ_NAME",pkColumnValue="MAIN_CATEGORY_SEQ",allocationSize=1)
@Getter
@Setter
@ToString
public class MainCategoryEntity extends BaseEntity<MainCategoryEntity> implements Serializable{
    
    private static final long serialVersionUID = 5609501385523526749L;
    
    @NotNull
    @Column(name="MAIN_CATEGORY_NAME",nullable=false)
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 엔티티 클래스이다. 우선 공통 엔티티 필드들은 BaseEntity라는 추상클래스로 뺐다. 그리고 정의할 엔티티클래스에서 해당 BaseEntity를 상속하여 정의하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@Setter
@ToString
public class MainCategoryDTO implements Serializable{
 
    private static final long serialVersionUID = 3340033210827354955L;
    
    private Long id;
    
    private String mainCategoryName;
    
}
cs

이번 예제에서 사용할 도메인클래스이다. 엔티티의 생성일과 수정일은 굳이 반환할 필요가 없기때문에 도메인클래스 필드에서 제외하였다. 그러면 엔티티 클래스에서 생성일, 수정일을 제외하고 클라이언트에게 값이 반환될 것이다. 여기서 중요한 것은 앞에서도 한번 이야기 했지만, ModelMapper는 매핑에 사용할 필드들을 이름으로 비교하기 때문에 엔티티클래스에서 사용하는 필드이름과 도메인 클래스에서 매핑할 필드이름이 동일해야한다는 것이다. 이점은 꼭 기억해야 할 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * 
 * GenericController - 컨트롤러의 공통 기능 추상화(CRUD)
 * 재정의가 필요한 기능은 @Override해서 사용할것
 * 
 * @author yun-yeoseong
 *
 * @param <E> Entity Type
 * @param <D> Domain Type
 * @param <ID> Entity key type
 */
@Slf4j
public abstract class GenericController<E,D,ID> {
    
    private JpaRepository<E, ID> repository;
    
    private DomainMapper domainMapper;
    
    private Class<D> dtoClass ;
    
    @SuppressWarnings("unchecked")
    public GenericController(JpaRepository<E, ID> repository,DomainMapper domainMapper) {
        
        this.repository = repository;
        this.domainMapper = domainMapper;
        
        /*
         * 타입추론로직
         */
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[1];
        
        if (type instanceof ParameterizedType) {
            this.dtoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.dtoClass = (Class<D>) type;
        }
 
    }
    
    /*
     * 엔티티아이디로 조회
     */
    @GetMapping("/{id}")
    public  ResponseEntity<D> select(@PathVariable ID id) {
        log.info("GenericController.select - {}",id);
        E e = repository.findById(id).get();
        D d = domainMapper.convertToDomain(e, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.OK);
    }
    
    /*
     * 리스트 조회
     */
    @GetMapping
    public ResponseEntity<List<D>> list(){
        log.info("GenericController.list");
        List<E> lists = repository.findAll();
        List<D> response = lists.stream().map(e->domainMapper.convertToDomain(e, dtoClass)).collect(Collectors.toList());
        return new ResponseEntity<List<D>>(response, HttpStatus.OK);
    }
    
    /*
     * 엔티티 생성
     */
    @Transactional
    @PostMapping
    public ResponseEntity<D> create(@RequestBody E e) {
        log.info("GenericController.create - {}",e.toString());
        log.info("dtoClass type = {}",dtoClass.getName());
        E created = repository.save(e);
        D d = domainMapper.convertToDomain(created, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 수정
     */
    @Transactional
    @PutMapping("/{id}")
    public ResponseEntity<D> update(@RequestBody E t) {
        log.info("GenericController.update - {}",t.toString());
        E updated = repository.save(t);
        D d = domainMapper.convertToDomain(updated, dtoClass);
        return new ResponseEntity<D>(d, HttpStatus.CREATED);
    }
    
    /*
     * 엔티티 삭제
     */
    @SuppressWarnings("rawtypes")
    @Transactional
    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable ID id) {
        log.info("GenericController.delete - {}",id);
        repository.deleteById(id);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}
 
cs

오늘 예제로 살펴볼 GenericController이다. 주석에 나와있는 데로 E,D,ID 제네릭타입은 순서대로 엔티티클래스타입,도메인클래스타입,엔티티클래스 ID타입이다. 그리고 위에서 만든 DomainMapper라는 클래스를 이용하여 사용자 요청에 대한 결과값을 조작하여 도메인클래스로 변경하여 리턴하고 있다.(엔티티에서 반환할 데이터만 DTO로 정의해 반환함) 그리고 하나 더 설명할 것은 이전 포스팅에서 다루었던 제네릭 컨트롤러와는 다르게 제네릭 타입으로 도메인클래스 타입을 받을 수 있게 하나 선언하였고, 해당 도메인 클래스의 타입을 추론하는 로직을 생성자에 한번 넣어주었다. 왜냐하면 도메인 클래스는 직접적으로 실체를 매개변수로 받고 있지 않기 때문에 타입을 추론하여 DomainMapper의 메소드의 매개변수로 들어가야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
@RequestMapping("/category/main")
public class MainCategoryController extends GenericController<MainCategoryEntity, MainCategoryDTO , Long>{
    
    private MainCategoryService mainCategoryService;
 
    public MainCategoryController(JpaRepository<MainCategoryEntity, Long> repository, DomainMapper domainMapper,
            MainCategoryService mainCategoryService) {
        super(repository, domainMapper);
        this.mainCategoryService = mainCategoryService;
    }
    
}
 
cs

마지막으로 제네릭 컨트롤러를 상속한 컨트롤러이다. 단순 CRUD 메소드가 추상화 되어있기 때문에 굉장히 깔끔한 코드가 되었다. 이 예제 코드를 실행시켜보면 반환 객체로 DTO가 나가는 것을 확인할 수 있다. 

 

여기까지 ModelMapper를 다루어봤다. 굉장히 노가다성 작업을 줄여주는 좋은 놈인것 같다. 글을 읽고 혹시 틀린점이 있다면 꼭 지적해주었음 좋겠다.

posted by 여성게
: