[개발] mapstruct 사용법

도현김·2023년 5월 7일
post-thumbnail

1. mapStruct

MapStruct란 객체간의 매핑 작업에 사용되는 매핑 라이브러리이다. 아마 계층형이나 헥사고날과 같은 아키텍처를 토대로 개발하게 되면 dtodomain 객체로 바꾸던지 domainentity로 바꾸던지 특정 계층에서 특정 계층으로 값을 운반할 때 객체 간의 매핑 작업은 불가피 할 것이다. 그럴 때 유용하게 사용할 수 있는 것이 바로 mapStruct이다.

1.1 어노테이션 & 구현체 자동 생성

mapStruct는 인터페이스를 만들 때 @Mapper 어노테이션을 함께 적어주면 인터페이스 내의 메소드 선언문을 보고 자동으로 구현체를 만들어 준다. 그리고 그 구현체는 컴파일 시점에 코드를 생성하여 런타임에서 안정성을 보장한다. 이를 통해 반복되는 객체 매핑에서 발생할 수 있는 오류와 피로감을 줄일 수 있다.

// Member.java

@Entity
@Table(name = "member")
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password", nullable = false)
    private String password;
}


// MemberDto.java

@Getter
@Setter
public class MemberDto {

    private Long id;
    private String email;
    private String password;
}

예를 들어서 위와 같은 회원 객체가 있고 그 회원 객체와 똑같은 멤버를 가지는 회원 dto가 있다고 가정하자. mapStruct를 사용하지 않았을 경우에는 회원 dto 코드에 아래와 같이 코드를 작성해야한다.

public class MemberMapper {

    public MemberDTO toDto(Member member) {
        MemberDTO dto = new MemberDTO();
        dto.setId(member.getId());
        dto.setName(member.getName());
        dto.setEmail(member.getEmail());
        return dto;
    }
}

그러나 mapstruct를 사용하면 구현체를 알아서 만들어 주기 때문에 아래와 같이 메소드의 선언문만 작성해줘도 컴파일시 알아서 구현체를 만들어준다. 이는 매우 직관적이며 코드를 작성할 수고를 덜어준다. 현재는 회원 객체의 멤버가 3개뿐이고 dto가 하나이기 때문에 유용함을 별로 느끼지 못할 수 있다. 하지만 멤버 수가 늘어나고 dto의 수가 많아지면 그 장점은 더욱 배가 될 것이다.

@Mapper(componentModel = "spring")
public class MemberMapper {

	MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    MemberDTO toDto(Member member);
}

1.2 사용 라이브러리 추가

우선 해당 의존성을 추가해주어야 한다.

implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'

또한, Lombok 라이브러리에 먼저 dependency (의존성) 추가가 되어있어야 한다. MapStruct는 Lombok의 getter, setter, builder를 이용하여 생성되므로 Lombok 보다 먼저 의존성이 선언된 경우 실행할 수 없다.

1.3 mapping 방법

일반적으로 값을 매핑하고자 하는 대상은 메소드의 인자에, 값이 매핑될 대상은 메소드의 리턴 값에 위치되어 매핑이 진행된다. 여기서 매핑될 대상의 멤버와 매핑하는 대상의 멤버의 변수명과 타입이 동일하다면 순조롭게 매핑이 진행된다. 그러나 만일 매핑될 대상과 매핑하는 대상의 멤버가 다르다면 아래와 같이 값이 대입될 수 있다.

{
	MemberDTO toDto(Member member);
   
	// 같은 경우 => 순조롭게 대입 됨
	// memberDTO : id, password, name
    // member : id, password, name
    
    // 받는 대상이 더 큰 경우 => ( name == null )
    // memberDTO : id, password, name
    // member : id, password
    
    // 주는 대상이 더 큰 경우 => (name 값 소멸)
    // memberDTO : id, password
    // member : id, password, name
}

그런데 만약 받는 대상이 더 큰 경우, 특정 값이 채워지지 않아서 null이 되는 경우가 있다 이럴 때는 아래와 같이 null 값이 채워질 수 있게 인자를 추가해주면 된다.

{
	// ...
    
	MemberDTO toDto(Member member, String name);
    
}

그런데 만약 매핑하는 객체와 매핑되는 객체의 멤버의 변수명이 다르면 어떻게 될까? 아마 값이 무시되고 매핑이 성사되지 않을 것이다. 이럴 때 사용하는 어노테이션이 @mapping 어노테이션이다.

예를 들어 memeberDto에는 nickname 이라는 멤버가 추가되었고 member에는 nick이라는 멤버가 추가되었는데 nick이 nickname으로 매핑되 길 원한다. 그러나 일반적으로는 매핑이 되지 않을 것이다. 따라서 그런 경우에는 아래처럼 매핑을 진행해주면 mapstruct가 그 정보를 보고 알아서 매핑을 진행해 줄 것이다.

{
	// ...
  	// source가 인자에 해당, target이 반환 값 객체에 해당.
  
	@Mapping(source="nick", target="nickname")
	MemberDTO toDto(Member member);
}

만약 아래처럼 매핑하는 대상, 즉 인자가 여러 개가 있다면 어떨까? 그러면 아래처럼 특정 객체에서 .을 사용하여 해당 객체 내부의 어떤 값이라고 지정해주면 된다.

{	
	//...
    // 인자 : member과 order
    
	@Mapping(source="member.nick", target="nickname")
	MemberDTO toDto(Member member, Order order);
}

그럼 또한 수동으로 매핑해야할 멤버가 여러 개이면 어떻게 해야할까? 그럴 땐 아래처럼 @Mappings 어노테이션으로 @Mapping 어노테이션들을 묶어주면 된다.

{
    @Mappings({
            @Mapping(target = "...", source = "..."),
            @Mapping(target = "...", source = "...")
    })
    MemberDTO toDto(Member member, Order order);
}

1.4 마치며

지금 필자가 소개한 기능은 매우 극히 일부이며 내가 설명한 것들은 이번 프로젝트에서 사용했었던 기능들만 소개한 것이다. 더 자세한 기능을 알아보기 원한다면 이 포스팅 참고하는 것이 좋을 것 같다.

2. 참고 자료

profile
안녕하세요! 신입 개발자 김도현입니다.

0개의 댓글