개발을 하다 보면 객체 간 변환이 필요할 때가 많다. 예를 들어, Entity ↔ DTO 변환을 해야 할 때, 일반적으로 Setter를 사용하거나, 생성자를 활용한 변환 로직을 직접 작성할 수도 있다. 하지만 유지보수성과 가독성을 위해, Static Method Factory, MapStruct, Builder 패턴 같은 변환 패턴을 고려할 수 있다.
이번 글에서는 MapStruct와 Builder 패턴을 비교하고, 각각의 장단점과 구현 방법을 살펴보려한다.
객체 매핑을 자동화하는 라이브러리로,
컴파일 시점에 코드를 자동 생성(@Mapper를 붙인 인터페이스의 구현체)하여 성능이 뛰어나며, 코드의 가독성을 높여준다.
➡️ 주요 목적 : 변환
build.gradle📜
implementation 'org.mapstruct:mapstruct:1.6.0'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.0'
@Getter
@SuperBuilder
public class BaseDto extends BaseTimeDto {
@Schema(description = "생성자 ID", example = "1")
private Long createdBy;
@Schema(description = "수정자 ID", example = "1")
private Long updatedBy;
}
@Getter
@SuperBuilder
public abstract class BaseTimeDto {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@Schema(example = "2025-01-01 00:00:00", description = "생성시간")
private LocalDateTime createdDateTime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@Schema(example = "2025-01-01 00:00:00", description = "수정시간")
private LocalDateTime updatedDateTime;
}
@Getter
@SuperBuilder
public class MemberResponse extends BaseDto {
@Schema(description = "사용자 ID", example = "1")
private Long id;
@Schema(description = "로그인 ID", example = "user1")
private String loginId;
@Schema(description = "사용자명", example = "홍길동")
private String name;
@Schema(description = "권한", example = "ROLE_USER")
private String role;
@Schema(description = "계정 잠금 여부", example = "false")
private Boolean isUse;
@Schema(description = "마지막 로그인 시간", example = "2025-01-01 00:00:00")
private LocalDateTime lastLoginDateTime;
}
MemberMapper📜
source - 매핑값을 가져올 대상target - 매핑할 대상@Mapper
public interface MemberMapper {
MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
@Mapping(target = "id", source = "id")
@Mapping(target = "loginId", source = "loginId")
@Mapping(target = "name", source = "name")
@Mapping(target = "role", source = "role")
@Mapping(target = "lastLoginDateTime", source = "lastLoginDateTime")
@Mapping(target = "isUse", source = "isUse")
@Mapping(target = "createdDateTime", source = "createdDateTime")
@Mapping(target = "updatedDateTime", source = "updatedDateTime")
@Mapping(target = "createdBy", source = "createdBy")
@Mapping(target = "updatedBy", source = "updatedBy")
MemberResponse toDto(Member member);
}
컴파일 시, 자동으로 MemberMapperImpl 클래스가 생성된다.
변환 조건이 필요할 경우, 해당 메서드를 직접 구현해야한다.
MemberMapperImpl📜
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-03-12T17:35:08+0900",
comments = "version: 1.6.0, compiler: javac, environment: Java 21.0.4 (Amazon.com Inc.)"
)
public class MemberMapperImpl implements MemberMapper {
@Override
public MemberResponse toDto(Member member) {
if ( member == null ) {
return null;
}
Long id = null;
String loginId = null;
String name = null;
String role = null;
LocalDateTime lastLoginDateTime = null;
boolean isUse = false;
id = member.getId();
loginId = member.getLoginId();
name = member.getName();
role = member.getRole();
lastLoginDateTime = member.getLastLoginDateTime();
if ( member.getIsUse() != null ) {
isUse = member.getIsUse();
}
MemberResponse memberResponse = new MemberResponse( id, loginId, name, role, isUse, lastLoginDateTime );
memberResponse.setCreatedDateTime( member.getCreatedDateTime() );
memberResponse.setUpdatedDateTime( member.getUpdatedDateTime() );
memberResponse.setCreatedBy( member.getCreatedBy() );
memberResponse.setUpdatedBy( member.getUpdatedBy() );
return memberResponse;
}
}
package com.project.beauty_care.domain.member;
import com.project.beauty_care.domain.mapper.MemberMapper;
import com.project.beauty_care.domain.member.dto.MemberCreateRequest;
import com.project.beauty_care.domain.member.dto.MemberResponse;
import com.project.beauty_care.global.enums.Errors;
import com.project.beauty_care.global.enums.Role;
import com.project.beauty_care.global.exception.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository repository;
public MemberResponse findMemberById(Long id) {
Member member = findById(id);
return MemberMapper.INSTANCE.toDto(member);
}
private Member findById(Long memberId) {
return repository.findById(memberId)
.orElseThrow(() -> new EntityNotFoundException(Errors.NOT_FOUND_MEMBER));
}
}
객체의 생성 과정과 표현 방법을 분리하여, 유연하고 가독성 좋은 객체 생성 방식을 제공하는 디자인 패턴
매개변수가 많을 때 사용한다.
➡️주요 목적 : 객체 생성
Member📜
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(uniqueConstraints = @UniqueConstraint(name = "UQ_MEMBER_LOGIN_ID", columnNames = {"login_id"}))
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String loginId;
@NotNull
private String password;
@NotNull
private String name;
private String role;
private Boolean isUse;
private LocalDateTime lastLoginDateTime;
@Builder
public Member(String loginId, String password, String name, Role role, LocalDateTime lastLoginDateTime) {
this.loginId = loginId;
this.password = password;
this.name = name;
this.role = role.getValue();
this.isUse = Boolean.TRUE;
this.lastLoginDateTime = lastLoginDateTime;
}
}
필드를 지정 안할 경우, 기본값을 할당한다.
private Member createMember(String loginId) {
return Member.builder()
.name("test")
.loginId(loginId)
.role(Role.USER)
.password("qwer1234")
.build();
}
| case | MapStruct | Builder |
|---|---|---|
| DTO <-> Entity 변환 | ✅ | ❌ |
| 객체 직접 생성 | ❌ | ✅ |
| 객체 불변성 | ❌ | ✅ |
| 가독성 | ✅ | ✅ |
| 성능 최적화 | ✅ | ❌(chaining) |
Entity 클래스 생성 -> Builder
단순 DTO 변환 -> MapStruct
목적에 맞는 Design Pattern 혼용 필요성