Springboot - 시간을 아끼는 DTO, Entity 변환 방법

박여명·2024년 6월 18일

개발

목록 보기
2/4

Info

배경

사이드 프로젝트를 진행할 때 JPA를 사용하며 Entity라는 개념을 알게 되었다 기존 DTO만 사용하던 형태에서 DTO, Entity로 늘어났다.
처음엔 몰랐지만 Entity, DTO의 수가 점차 많아지고 그 안에 필드 수가 많아지자 두 객체 간 변환을 하는 코드가 상당히 막노동 작업이 되어버렸다.
맘 같아서는 Entity를 그냥 DTO + Entity로 한 번에 쓰고 싶지만 알맞은 용도로 써야 한다는 책의 내용을 보고 수긍했다.

하지만 단순 변환코드 작성이 갈수록 오래 걸렸고 내 코딩 시간을 잡아먹는 게 아까워 고민하고 검색해 본 결과 3가지의 방법을 찾았으며 그중 가장 괜찮은 방법을 소개해 볼까 한다.

기존 방법

일반적으로 DTO, Entity를 변환하는 작업은
따로 getter, setter 를 통해 변환하거나 DTO, Entity Class 내부에 반대 객체를 반환하는 함수를 작성하는 등 명시적으로 코드 작성으로 처리할 것같다.

나는 이런 시간만 잡아먹는 단순 작업을 아래와같이 바꿔보았다.

시간을 아끼는 DTO, Entity 변환 방법

1. 리플랙션

맨 처음 사용한 방식인데 생각보다 코드가 길며 가독성이 어려운 점이있다.
그리고 무엇보다 너무나도 오류가 생기기 쉬운 구조이다.

	// Entity를 DTO로 변환하는 함수
    public static <D, E> D entityToDto(E entity, Class<D> dtoClass) {
        if (entity == null) {
            return null;
        }

        try {
            D dto = dtoClass.newInstance();
            for (Field entityField : entity.getClass().getDeclaredFields()) {
                entityField.setAccessible(true);
                Object value = entityField.get(entity);
                
                try {
                    Field dtoField = dtoClass.getDeclaredField(entityField.getName());
                    dtoField.setAccessible(true);
                    dtoField.set(dto, value);
                } catch (NoSuchFieldException ignored) {
                }
            }
            return dto;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Conversion error", e);
        }
    }

    // DTO를 Entity로 변환하는 함수
    public static <D, E> E dtoToEntity(D dto, Class<E> entityClass) {
        if (dto == null) {
            return null;
        }

        try {
            E entity = entityClass.newInstance();
            for (Field dtoField : dto.getClass().getDeclaredFields()) {
                dtoField.setAccessible(true);
                Object value = dtoField.get(dto);
                
                try {
                    Field entityField = entityClass.getDeclaredField(dtoField.getName());
                    entityField.setAccessible(true);
                    entityField.set(entity, value);
                } catch (NoSuchFieldException ignored) {
                }
            }
            return entity;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Conversion error", e);
        }
    }

2. ModelMapper

예시 코드 만드는중

3. MapStruct

  1. 의존성 추가 - pom.xml(주의 : 롬복뒤에 추가해야함!!)
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.3.Final</version>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.5.3.Final</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
  1. 인터페이스 정의
    인터페이스를 정의하는 이유는 구현코드를 MapStruct가 만들어줌
    -> MapStruct는 컴파일 시점에 매핑 코드를 생성하는데 매핑 과정에서 발생할 수 있는 런타임 오류를 줄이고 타입 안정성을 보장함 따라서 인터페이스를 통해 정적으로 타입 검사가 가능하다고 함!
    +커스커마이즈 가능
@Mapper
public interface MyMapper {
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

    MyDto entityToDTO(MyEntity e);
    MyEntity dtoToEntity(MyDto d);
}
  1. 사용 코드예시
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    private final MyMapper myMapper = MyMapper.INSTANCE;

    public MyDto getMyEntityById(Long id) {
        MyEntity myEntity = MyRepository.findById(id).orElse(null);
        return myMapper.entityToDTO(myEntity);
    }

    public UserDto saveUser(MyDto myDto) {
        MyEntity myEntity = MyMapper.dtoToEntity(myDto);
        MyEntity savedMyEntity = MyRepository.save(myEntity);
        return MyMapper.entityToDTO(savedMyEntity);
    }
}

각 방법의 특징과 장단점

1. 리플렉션 사용

장점

  • 사용이편함

단점

  • 성능 문제: 리플렉션은 런타임 시점에 메타데이터를 검사하고 접근하기 때문에, 일반적인 메서드 호출보다 훨씬 느림
  • 타입 안전성 부족: 컴파일 시점에 타입 체크가 이루어지지 않기 때문에, 런타임 시점에 발생할 수 있는 오류를 사전에 방지하기 어려움
    -> 이는 유지보수성과 디버깅을 어렵게 한다고 함
  • 코드 가독성: 리플렉션을 사용한 코드는 일반적으로 더 복잡하고 이해하기 어려움
    -> 실제로 내가 리플렉션을 사용한 코드를 처음본다 생각하면 굉장히 난해할 것 같음

2. ModelMapper

장점

  • 간편함: 설정과 사용이 간단하고 빠르게 매핑 작업가능
  • 유연성: 커스터마이즈된 매핑을 쉽게 설정 가능

단점

  • 런타임 에러: 매핑 오류가 런타임에 발생할 수 있어 타입 안전성을 보장하지 못합니다.
  • 성능: 리플렉션을 사용하므로 컴파일 시점에 생성된 매핑 코드보다 성능이 떨어질 수 있음

3. MapStruct

장점

  • 성능: 컴파일 시점에 매핑 코드를 생성하므로 성능이 뛰어남
  • 타입 안전성: 컴파일 시점에 타입 검사를 수행하므로 타입 안전성을 보장
  • 명확한 매핑 정의: 매핑이 명확하게 정의되어 있어 가독성과 유지보수성 높음

단점

  • 설정 필요: 처음 설정 시 약간의 학습과 설정 작업이 필요
  • 복잡한 매핑: 복잡한 매핑 로직을 구현할 때는 추가적인 설정이 필요

결론

엔터프라이즈급 서비스를 운영하는 기업이 이 MapStruct를 쓰는 경우가 많다하며 여러가지 측면에 우수 하므로 MapStruct를 쓰자!!

0개의 댓글