Backend/spring boot

spring boot : entity <-> dto 변환 방법에 대한 고민( MapStruct 에 대해)

attlet 2024. 10. 9. 22:17

 

 

개요

프로젝트를 진행하며 여러 layer사이에 데이터를 전달할 때, dto, entity 간 변경이 자주 일어나는 상황에 변환하는 코드를 일일이 적어넣는 것이 불편했습니다. 

 

 

예시로 보면, member  한 명을 생성하는 post api는 다음처럼 작성되어 있었습니다.

 

 

@Controller
@RequiredArgsConstructor
class MemberController {
	
    private final MemberService memberService;
    
    @PostMapping()
    public Member createMember(@RequestBody MemberDto memberDto){   //dto 사용
    	Member member = memberService.createMember(memberDto);
        return member;
    }
}

 

 

위 컨트롤러에서 호출하는 service메서드 에서는 dto로 가져온 값을 entity로 변환하는 과정이 필요합니다. jpa 에서 제공하는 save 메서드의 경우, 엔티티를 매개변수로 받아야하기 때문입니다. 

 

 

@Override
public ResponseUserDto createUser(RequestUserDto userdto) {

    User user = userdto.toEntity();    //dto 를 entity로 변환
 
    User savedUser = userRepository.save(user);
    ResponseUserDto responseUserDto = new ResponseUserDto(user);

    return responseUserDto;
}



//RequestUserDto 내부

 public User toEntity(){
        return User.builder()
                .username(username)
                .password(password)
                .mail(mail)
                .build();
    }

 

dto를 entity로 변환하는 toEntity 함수를 사용하는 모습입니다. 이 toEntity는 dto안에 제가 작성한 함수입니다. 빌더 패턴을 이용해 쉽게 엔티티를 만들어 반환하는 모습입니다.

 

 

이런식으로 처음에는 적당히 함수로 만들어 귀찮은 변환과정을 해결하려 했지만... 이 방법은 dto가 늘면 늘수록 귀찮아질 것이 뻔했고, 저는 다른 방법을 찾아보기 시작했습니다. 

 

 

 

그러던 중 발견한 것은 MapStruct 입니다. 

 

 

MapStruct

 


 

 

 

Mapstruct는 객체 간 매핑을 위해 개발된 인터페이스로, 어노테이션 기반으로 사용이 가능합니다.

 

물론 MapStruct 말고도 다양한 mapper들이 존재하지만, 코드의 가독성과 성능상 이점이 매력적이라 느껴져 MapStruct를 선택해서 코드를 개선했습니다.

 

MapStruct의 특징은 다음과 같았습니다.

 

 

1. 컴파일 시점에 의도한 코드를 생성해, 성능이 다른 mapper에 비해 좋습니다. (다른 mapper의 경우 런타임 때 동적으로 생성하는 경우가 있음)

 

2. 어노테이션을 통해 간단하게 객체 간에 어떻게 변환할 지 지정할 수 있습니다.

 

 

쉽고, 성능이 좋다!  가 핵심인 것 같습니다.

 

 

물론 장점만 있지는 않습니다. 성능이 좋지만 그만큼 복잡한 매핑은 작성하기 어렵고, 런타임 시 동적으로 변환 코드를 생성하지 않는다는 한계가 있습니다. 하지만 이 단점은 제 엔티티랑 dto 사이 매핑이 복잡하지 않다는 점 때문에 고려할 필요가 없다고 판단했습니다.

 

 

 

 

MapStruct 사용


 

프로젝트 환경은 java 17, 빌드 도구는 gradle 기준입니다.

 

 

build.gradle 파일에 다음 의존성을 추가합니다.

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

 

이후 원하는 위치에 mapper 인터페이스를 만듭니다. 저 같은 경우, User 관리하는 service 를 개선하려하니 user 패키지 안에 만들었습니다.

 

 

 

이렇게 UserMapper 인터페이스를 생성한 뒤 Mapper 어노테이션을 붙인 뒤, componentModel을 통해 spring 의존성 주입이 되도록 설정합니다.  

 

간단히 entity -> dto와 dto -> entity 변환을 위한 메서드 두 개를  작성한 모습입니다. 매개변수 부분에 변환하고 싶은 타입의 객체를, 반환 타입에 얻고자 하는 타입의 객체명을 적어넣습니다. 

 

 

붉게 네모칠한 부분이 User를 생성하기 위한 RequestUserDto를 User 엔티티로 변환하기위한 메서드입니다.

 

 

 

이제 이를 이용해 UserService 코드를 변경해보았습니다.

 

 

 

다음은 UserService의 한 메서드입니다.

 

 

 

 

 

기존 코드는 위와 같습니다. 붉은 네모칠한 부분을 보면, user 엔티티를 Dto객체로 변환하는 작업을 하는 부분입니다.

 

생성자로 받아서 Dto 내부에서 객체를 생성해 반환하는 방식입니다. 

 

 

 

이렇게..

 

 

 

이제 작성한 userMapper를 이용해보도록 하겠습니다.

 

 

 

UserMapper의 toDto 메서드를 이용해 파라미터로 받은 entity를 Dto로 변환했습니다. 이렇게 변환 로직을 엔티티나 dto 의 생성자 등으로 분리돼서 관리하지 않고, mapper 하나로 관리가 가능해졌습니다.

 

 

 

 

동작 확인


 

User 테이블에 다음 두 데이터가 들어가 있습니다. 

 

 

 

1번 유저를 한 번 가져와 보도록 하겠습니다.

 

 

 

미리 만들어둔 swagger 통해서 admin 조회 선택 후 execute해서 get api 호출

 

 

 

이전과 같이 ResponseDto로 잘 변환되어서 반환되는 모습입니다. 

 

 

 

 

 

마무리


 

객체 간 변환도 한 엔티티 당 하나씩 로직을 모아둘 수 있는 방법이 있다는 건 흥미로웠습니다. 제 사이드 프로젝트에도 적용해보고, 실무에서도 이 경험이 좋은 방향으로 저를 이끌어 줄 것이라 기대합니다. ㅎㅎ