왜??
Spring에 대한 공부와 여러가지 실습을 진행하면서 객체간 Mapping을 할 일이 많아졌고 각 객체 내부에 계속해서 다른 객체와 Mapping 해주는 로직을 짜게 되었습니다.
그러나 이제 객체가 늘어날수록 계속해서 Mapping 로직과 반환하는 DTO들이 많아졌고 관리의 용이성을 생각했을때 더 좋은 방법이 있다고 생각해서 찾아보게 되었습니다.
일반적으로 제공하는 Mapper가 있지만 MapStruct라는 좋은 것이 있어서 코드설명과 함께 제공을 해드리려고 합니다. 네이버 클라우드의 블로그를 참고했습니다.
편리한 객체 간 매핑을 위한 MapStruct 적용기 (feat. SENS)
Ncloud 문자/알림 발송 서비스 SENS 개발 과정에서 MapStruct를 활용해 보았습니다.
medium.com
일단 MapStruct는 뭘까요?
Java Bean의 유형간 매핑을 단순화하는 코드 생성기입니다.
MapStruct는 다음과 같은 장점들을 가지고 있습니다.
장점 :
- 컴파일 시점에 코드 생성 (빌드될때 코드 생성함)
- 다른 매핑 라이브러리보다 속도가 빠릅니다.
- 반복되는 객체 매핑에서 발생할수있는 중복문제를 해결할수 있고, 구현되는 코드가 컴파일 시점에 자동으로 생성되기 때문에 사용하기 편리합니다.
주의점 :
- Lombok 라이브러리의 Getter ,Setter같은 어노테이션을 통해서 객체간 mapping을 자동으로 해주기 때문에 Lombok 라이브러리가 dependency에 먼저 설정이 되어있어야 합니다.
사용방법은?
1. Dependency 설정
위 MapStruct 에서 최신 버젼에 따라 의존성을 추가해주시면 됩니다.
저는 다음과같은 의존성을 추가해주었습니다.
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.0.Beta1'
}
MapStruct – Java bean mappings, the easy way!
Java bean mappings, the easy way! Get started Download
mapstruct.org
저는 당근마켓 클론코딩 과제에 대한 간단한 예시로써 Entity->DTO간의 Mapping을 해주었습니다.
다음은 Customer에 대한 간단한 entity 예시가 있고
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
@Column(nullable = false)
private String name;
@Setter
@Column(nullable = false)
private int age;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
Mapping 하고싶은 CustomerDTO가 있다고 가정해보겠습니다.
public record CustomerFindDto (Long id,String name, int age){
}
이제 저희는 Repository => Service => Controller 단으로 각 전송되는 객체에 대해서 Mapping을 시도하려고 합니다. 위에 Customer를 CustomerDto로 mapping하려고 합니다. 아래 방법처럼 원하는 layer에서 각 변환을 시도할수 있습니다.
- Repositroy에서 entity를 가져온다.
- Service 레이어나 Controller 레이어, 원하는 레이어에서 Entity-> DTO 변환을 시도한다.
2. Mapper 생성하기
아래와 같이 @Mapper 어노테이션을 통해서 Spring에서 우리가 이러한 Mapper를 사용할거라고 지정을 해줍시다.
Customer를 CustomerDTO로 반환하는 인터페이스를 구현했습니다.
@Mapper(componentModel = "spring")
public interface CustomerMapper {
CustomerFindDto toCustomerDto(Customer customer);
}
그러면 이제 끝난건가요???
네 끝입니다!
실제로 빌드과정에서 만들어지는 구현체를 한번 확인해볼까요?

다음과 같은 폴더에 구현체가 들어가 있는 것을 볼수 있습니다.
실제로 구현체를 들어가보면 다음과같이 구현이 되어있는것을 볼수 있고 각 mapping 을 getter등을 통해서 CustomerDTO로 Mapping 해주는 것을 볼수 있습니다.
@Component
public class CustomerMapperImpl implements CustomerMapper {
@Override
public CustomerFindDto toCustomerDto(Customer customer) {
if ( customer == null ) {
return null;
}
Long id = null;
String name = null;
int age = 0;
id = customer.getId();
name = customer.getName();
age = customer.getAge();
CustomerFindDto customerFindDto = new CustomerFindDto( id, name, age );
return customerFindDto;
}
}
Mapper Interface를 구현하게 되면 매핑이 필요한 객체에 대해 자동으로 구현체를 만들어주는데 현재 Customer와 CustomerDTO는 필드값이 동일해 mapping의 과정이 너무 간단했습니다
3. 그렇다면 객체간의 필드값이 다른 경우는..?
객체간의 필드값이 다른경우는 여러객체를 하나의 객체에 매핑하거나 각 Dto간의 타입은 같지만 네이밍이 다를수도 있겠죠?
예시를 한번 들어보겠습니다.
public class Product {
@Getter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
@Column(nullable = false)
private String itemName;
@Setter
@Column(nullable = false)
private String itemDescription;
@Setter
@Column(nullable = false)
private Integer price;
@Getter
@JoinColumn(nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Place place;
@ColumnDefault("0")
@Builder.Default
@Setter
private Integer good=0;
@Setter
@Column(nullable = false)
@Builder.Default
@Enumerated(EnumType.STRING)
// @ColumnDefault("NOT_SOLD")
private CellingStatus cellingStatus = CellingStatus.NOT_SOLD;
}
다음과같은 Product 객체가 있고 각기 다른 네이밍으로 Dto로 변환을 시도하려고 합니다. 기본적으로 네이밍이름을 같게 해주는것이 좋지만 네이밍이 혹시 다른 경우를 들어보겠습니다.
아래는 ProductDto에 관한 내용으로 price2 와 itemDescription2로 필드의 네임이 다른것을 확인할수 있습니다.
public record ProductFindDto(
Long productId,
String itemName,
int price2 ,
String itemDescription2 ,
CellingStatus cellingStatus,
Place place, int good,
Long customerId
)
그러면 Product에 대한 Mapper 인터페이스를 구현하면 아래와 같은 코드로 구현을 할수 있습니다.
@Mapper(componentModel = "spring")
public interface ProductMapper {
// ProductMapper INSTNACE = Mappers.getMapper(ProductMapper.class)
@Mapping(target = "productId", source = "id")
@Mapping(target = "customerId", source = "product.customer.id")
@Mapping(target = "price2", source = "product.price")
@Mapping(target = "itemDescription2", source = "product.itemDescription")
ProductFindDto toProductFindDto(Product product);
}
@Mapping 어노테이션을 통해서 product.itemDescription 처럼 가져오려는 source에서 target은 ProductFindDTO의 itemDescription2에 매핑을 하고 싶으니 각각 source와 target을 명시해서 적어줄수 있습니다.
4. Mapper객체 사용하기
사실 저는 이게 JPA Data Respository를 상속받는 과정과 꽤 유사하다고 느꼈었는데 제 착각인가요..?그냥 말해봤습니다
어쨋든 이러한 Mapper를 각 원하는 레이어에 삽입 해서 사용할수 있습니다.
예를들어 저는 Controller단에서 넣어주는걸 선호했는데 예상외로 Entity의 반환값들을 사용할일이 많아서 Controller에 Mapper를 삽입해 주었습니다.
public class CustomerController {
private final CustomerService customerService;
private final CustomerMapper customerMapper;
@GetMapping("/{customerId}")
public CustomerFindDto getCustomerBasicInfo(
@PathVariable Long customerId
) {
return customerMapper.toCustomerDto(customerService.getCustomerById(customerId));
}
}
위와 같이 Mapper를 삽입해주고 service에서 올라오는 entity를 Mapper로 묶어주어 DTO로 반환을 해주는 모습입니다.
물론 Controller 단보단 Service에서 Dto로 변환해서 사용하는게 조금 더 안정적이라고 생각은 합니다만.. 생각보다 불편한점이 많아 간단하게 보여주기 위해서 이런식으로 코드를 짜게 되었습니다.
이제 다음과 같은 방식을 사용해서 Dto를 마구잡이로 만들어서 마구마구 매핑해봅시다.
다음에는 GCP혹은 AWS와 Docker Repostiroy를 클라우드에 등록하고 github Action과 연동해서 최신 Docker image로 빌드하는 CI/CD 과정을 담은 내용으로 돌아올까 합니다.
그럼 즐코하세용~!
'Spring' 카테고리의 다른 글
Swagger로 사랑받는 개발자 되기 [ Spring ] (0) | 2024.08.01 |
---|---|
Spring 검색조회 필터링 구현 방법 [JPA Specification] (0) | 2024.07.31 |
JPA - 단방향? 양방향? OneToMany? ManyToOne? (3) | 2024.04.29 |
@ManytoOne, @ManytoMany, @OnetoOne, @OnetoMany? (2) | 2024.04.28 |
SpringBoot와 Docker Container :Postgresql 연결하기! (2) | 2024.04.22 |