프로필사진

Go, Vantage point

가까운 곳을 걷지 않고 서는 먼 곳을 갈 수 없다.


Github | https://github.com/overnew/

Blog | https://everenew.tistory.com/





티스토리 뷰

반응형

 

 

 

 

 

이전 게시글에 이어서 Entity와 DTO 간의 변환 방법에 대해 소개하겠다.

[Spring] DAO, DTO, Entity, 기본 계층 설계

 

 

 

스프링 계층

 

계층 구조를 유지하기 위해서는 데이터베이스의 Entity와 DTO 간의 변환이 빈번히 일어나게 된다.

개발 초기 단계에서는 자주 클래스의 field들이 바뀌거나 추가된다.

이때 Entity와 DTO 간의 변환의 역활을 하는 기능은 field의 변동이 있을 때마다 계속 수정을 해주어야 한다.

Entity의 종류가 한두개면 모르겠지만, 수십 개가 있다면 이러한 작업은 반복적일 뿐만 아니라 유지보수도 힘들다.

 

 

 

 

 

라이브러리 추가

 

이를 위해 자바 진영에서 사용하는 라이브러리가 MapStruct이다.

https://mapstruct.org/

 

MapStruct – Java bean mappings, the easy way!

Java bean mappings, the easy way! Get started Download

mapstruct.org

 

 

 

Spring에서 MapStruct를 사용하고 싶다면 다음과 같이 build.gradle에 dependency를 추가해주자.

 

annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'

testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'

 

 

 

 

 

@Mapper

 

 

일단 다음과 같은 Member Entity와 DTO가 있다고 가정하자.

 

 

Member Entity

@Document(collection = "member")
@AllArgsConstructor
@Builder
@Getter
@ToString
public class Member {
    @Id
    private String id;
    private String password;

    private String name;

    @DocumentReference
    private List<Recipe> recipes;
}

 

 

 

MemberDTO

@Getter
@Builder
@AllArgsConstructor
public class MemberDTO {
    private String id;
    private String password;

    private String name;
    
    private List<RecipeDTO> recipeDTOs;
}

 

 

MapStruct를 적용하기 위해서는 변환되는 class는 빌더 패턴이 필수로 적용되어야 한다.

빌더 패턴의 유용성은 나중에 작성해 보도록 하겠다.

일단, Lombok을 사용한다면 @Builder를 적용하는 것으로 해결이 된다.

 

 

 

 

MapStruct 라이브러리를 추가했다면, @Mapper 어노테이션을 사용할 수 있다.

 

@Mapper는 특이하게도 인터페이스에 적용해주면, 메서드의 선언 부만으로 구현체를 만들어 준다.

 

MemberDTO memberToDto(Member member);

 

위의 선언부만 있더라도 매개변수 타입인 Member와 반환 타입인 MemberDTO를 분석해서 서로 간의 field를 매칭해, Member 인스턴스를 DTO인스턴스로 변환해서 반환해준다.

 

 

 

 

단, 이때 field명과 타입을 통해 매칭이 진행되기 때문에 둘이 다르다면 Mapper에게 이를 명시해 주어야 한다.

(AI는 아니므로 이런 작업은 필요하다...)

 

Member와 MemberDTO는 서로 Recipe 필드를 다른 이름과 다른 타입으로 저장한다.

따라서 MemberMapper은 다음과 같이 

Mapping(source ="매개변수 타입의 필드명", target ="반환 타입의 필드명")으로 명시해 두었다.

 

@Mapper( uses = {RecipeMapper.class})	//uses에 대해서는 아래에서 설명하겠다.
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
    

    @Mapping(source = "recipes", target = "recipeDTOs")
    MemberDTO memberToDto(Member member);

    @Mapping(source = "recipeDTOs", target = "recipes")
    Member DtoToMember(MemberDTO memberDto);
}

 

 

 

이러한 인터페이스를 작성하면 컴파일 시점에 구현체인 MemberMapperImpl이 자동 생성된다.

 

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-09-04T21:44:57+0900",
    comments = "version: 1.5.2.Final, compiler: javac, environment: Java 11.0.13 (Oracle Corporation)"
)
public class MemberMapperImpl implements MemberMapper {

    private final RecipeMapper recipeMapper = RecipeMapper.INSTANCE;

    @Override
    public MemberDTO memberToDto(Member member) {
        if ( member == null ) {
            return null;
        }

        MemberDTO.MemberDTOBuilder memberDTO = MemberDTO.builder();

        memberDTO.recipeDTOs( recipeMapper.recipeListToDTOList( member.getRecipes() ) );
        memberDTO.id( member.getId() );
        memberDTO.name( member.getName() );
        memberDTO.password( member.getPassword() );

        return memberDTO.build();
    }

    @Override
    public Member DtoToMember(MemberDTO memberDto) {
        if ( memberDto == null ) {
            return null;
        }

        Member.MemberBuilder member = Member.builder();

        member.recipes( recipeDTOListToRecipeList( memberDto.getRecipeDTOs() ) );
        member.id( memberDto.getId() );
        member.password( memberDto.getPassword() );
        member.name( memberDto.getName() );

        return member.build();
    }
    
    protected List<Recipe> recipeDTOListToRecipeList(List<RecipeDTO> list) {
        if ( list == null ) {
            return null;
        }

        List<Recipe> list1 = new ArrayList<Recipe>( list.size() );
        for ( RecipeDTO recipeDTO : list ) {
            list1.add( recipeMapper.DTOtoRecipe( recipeDTO ) );
        }

        return list1;
    }
 }

 

 

 

구현체를 살펴보면 builder 기능을 사용해 반환 값에 필드를 채워주는 것을 확인할 수 있다.

만약 Entity와 DTO class 간의 필드 차이가 있더라도 매핑이 되지 않는 필드는 어떠한 값도 주입되지 않고 null로 반환된다.

따라서 Mapper를 이용했을 때 값이 null로 반환된다면 매핑이 되지 않았을 가능성이 높다.

 

 

 

 

사용법

 

구현한 Mapper의 INSTANCE를 이용해 원하는 메서드를 불러 사용하면 된다.

MemberMapper.INSTANCE.memberToDto(memberInstance)

 

 

 

 

@Mapper(uses = "ReferenceMapper.class")

 

이제 다른 문제를 생각해보자.

모든 매퍼들은 서로의 메서드를 참조해서 자신의 구현체를 만들지 않고, 독립적으로 매핑을 시킨다.

 

Member class는 Recipe라는 다른 객체를 필드 타입으로 이용하고 있다.

만약 RecipeMapper도 Recipe와 RecipeDTO 간의 서로 다른 타입과 매핑하기 위해 Mapping을 적용해 두었다면 어떻게 될까?

 

MemberMapper는 RecipeMapper가 존재하는지 조차 모르기 때문에, 

그냥 자신이 직접 Recipe와 RecipeDTO를 변환하는 메서드를 자동 생성해 사용한다.

따라서 Member의 Recipe 리스트와 MemberDTO의 RecipeDTO 리스트는 서로 변환 시 맵핑이 가능한 필드들만 주입된다.

 

 

맵핑이 잘 적용 됐겠지 라는 착각에 빠져 테스트를 돌리다 보면 NullPointer 지옥에 빠지게 된다.

(진짜 미쳐버릴 뻔했다....)

 

 

이런 문제는 MemberMapper가 Recipe와 RecipeDTO를 변환할 때는 RecipeMapper의 메서드를 사용하도록 하면 해결이 된다.

 

이때 사용하는 것이 @Mapper(uses = "ReferenceMapper.class") 이다. 

 

 

@Mapper( uses = {RecipeMapper.class})
public interface MemberMapper {
	...위와 동일
}

 

이처럼 다른 Mapper를 사용하도록 명시하면 RecipeMapper의 메서드를 이용해 생성이 된다.

 

 

 

 

public class MemberMapperImpl implements MemberMapper {

    private final RecipeMapper recipeMapper = RecipeMapper.INSTANCE;

    @Override
    public MemberDTO memberToDto(Member member) {
        if ( member == null ) {
            return null;
        }

        MemberDTO.MemberDTOBuilder memberDTO = MemberDTO.builder();

        memberDTO.recipeDTOs( recipeMapper.recipeListToDTOList( member.getRecipes() ) );
        ...
        //위와 동일
    }
}

MemberMapperImpl이 RecipeMapper를 생성해서 이용하는 것을 확인할 수 있다.

 

 

 

반응형
댓글
반응형
인기글
Total
Today
Yesterday
«   2024/05   »
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
글 보관함