Backend Development/SPRING

[MapStruct] 기본 사용법

foreverWon 2025. 7. 21. 14:25
목차

    1. MapStruct란?

    • DTO와 Entity 간 매핑을 자동화해주는 Java 어노테이션 기반 코드 생성기
      • 반복적인 setter, builder 코드 제거
      • @Mapping, @Mapper 등의 어노테이션 기반으로 설정

    2. Why DTO?

    Entity ↔ JSON 변환 시 생기는 문제점

    문제점 설명
    보안 이슈 비밀번호, 내부 ID 등 민감한 정보 노출 위험
    유연성 저하 Entity 변경 시 API 응답/요청도 영향받음
    캡슐화 위반 DB와 API 설계가 강하게 결합됨
    Validation 혼란 입력 값 검증 로직이 분리되기 어려움

    DTO ← (MapStruct) → Entity

    • Controller: DTO로 요청 받고 응답 줌
    • Service: Entity로 로직 수행
    • MapStruct: DTO ↔ Entity 변환 담당 (mapper 인터페이스)

    3. 매핑 전략

    • 필드 이름이 같으면 → 자동 매핑
    • 필드 이름이 다르면 → @Mapping 어노테이션 활용
      • 예: @Mapping(source = "username", target = "name")
      • 중첩된 객체도 지원: @Mapping(source = "stamp.count", target = "stampCount")

    4. 생성자 우선순위

    1. 빌더 패턴: @Builder
    2. 전체 생성자: @AllargsConstructor
    3. 기본 생성자 + Setter

    5. build.gradle 설정

    dependencies {
        implementation 'org.mapstruct:mapstruct:1.5.5.Final'
        annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    }

    6. 정리

    • DTO는 Entity와 API 간 중간 다리 역할
    • MapStruct는 그 사이의 변환을 자동화하는 도구
    • 깔끔한 계층 분리를 유지하면서도 개발 속도, 안정성을 높여줌
    • 실무에서는 단순 매핑에 MapStruct 자주 사용
    • 복잡하거나 조건 있는 매핑은 커스터마이징하거나 수동 매퍼 필요

    7. 코드로 보는 MapStruct

    // 이 매퍼를 Spring Bean으로 등록해서 DI를 받을 수 있게 해주는 어노테이션
    @Mapper(componentModel = "spring")
    public interface MemberMapper {
    
        // MemberPostDto → Member 매핑 (POST 요청용)
        // username 필드를 Member의 name으로 매핑
        // phoneNumber 필드를 Member의 phone으로 매핑
        @Mapping(source = "username", target = "name")
        @Mapping(source = "phoneNumber", target = "phone")
        Member memberPostDtoToMember(MemberPostDto memberPostDto);
    
        // Member 리스트 → MemberResponseDto 리스트 매핑 (조회 시 응답용)
        List<MemberResponseDto> membersToMemberResponseDtos(List<Member> members);
    
        // Member → MemberResponseDto 매핑 (단건 조회 시 응답용)
        // Member 내 Stamp 엔티티의 count 값을 ResponseDto의 stampCount로 매핑
        @Mapping(source = "stamp.count", target = "stampCount")
        MemberResponseDto memberToMemberResponseDto(Member member);
    }
    @Getter
    @AllArgsConstructor
    @Builder
    public class MemberPostDto {
        private String email;
        private String username;
        private String phoneNumber;
    }
    //@AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    //@Builder
    public class MemberResponseDto {
        private long memberId;
        private String email;
        private String name;
        private String phone;
        private LocalDateTime createdAt;
        private LocalDateTime modifiedAt;
        private List<OrderResponseDto> orders;
        private int stampCount;
    }
    @NoArgsConstructor
    @Getter
    @Setter
    @Entity
    public class Member {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long memberId;
    
        @Column(nullable = false, updatable = false, unique = true)
        private String email;
    
        // (1)
        @Column(length = 100, nullable = false)
        private String name;
    
        @Column(length = 13, nullable = false, unique = true)
        private String phone;
    
        @Column(nullable = false)
        private LocalDateTime createdAt = LocalDateTime.now();   // (2)
    
        // (3)
        @Column(nullable = false, name = "LAST_MODIFIED_AT")
        private LocalDateTime modifiedAt = LocalDateTime.now();
    
        @OneToMany(mappedBy = "member")
        private List<Order> orders = new ArrayList<>();
    
        @OneToOne(mappedBy = "member", cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
        private Stamp stamp;

     

    728x90