서론☁️
기존에 DDD의 4Laeyerd 형태를 띄고있는 구조였습니다.
명확히 4개의 레이어로 구분하여 수정시에 다른 레이터에게 영향을 미치기 않도록 하는것이 목표인 구조입니다.
하지만 아직 구조적으로 미흡한 부분 중 하나는 Application과 Presenetation이 하나의 dto를 공유한다는 것이었습니다.
presentation 영역의 DTO를 어플리케이션 레이어에 그대로 전달하여 사용하고 잇습니다.
즉, 어플리케이션 레이어에서 프레젠테이션 영역에 의존하고 있는 형태이고, 이렇게 되면 DIP 원칙에 어긋납니다.
그렇기 때문에 서비스는 서비스 영역의 dto로만 사용을 해야하며, presentation 에서는 presentation 영역에 맞는 dto로 변경해서 사용해야합니다.
이렇게 명확히 분리할 경우 단점부터 말하자면 매번 컨트롤러에서 서비스로 전달할때 DTO를 맵핑시켜주어야하기때문에 번거롭습니다.
그럼에도 이렇게 분리하는것이 좋은 이유는 다음과 같습니다.
1. DIP를 준수한다.
Presentation 계층의 dto가 변경되더라도 직접적으로 Application 계층에 영향을 주지 않습니다.
2. 각 계층의 역할에 맞는 DTO를 작성할 수 있다.
Application 계층에서만 필요한 DTO의 메서드, 변수를 다른 계층으로 전달하지 않을 수 있습니다.
3. 변경 범위가 명확해진다.
사실 이렇게 구현하더라도 Presentation 계층의 dto가 변경되면 Application 계층의 dto와 매핑시켜주는 적어도 클래스는 수정해야할 수 있습니다.때론, 구조가 완전히 바뀐다면 당연히 Application계층도 수정해주어야겠지만 기존에는 Application 계층의 작은 수정이라도 Presentation 계층에 영향을 미쳤다면 이제는 변경 범위를 제어할 수 있습니다.
그렇다면 예시를 들어보며 어떻게 리팩토링을 진행하였는지 말씀드리겠습니다.
진행
@PostMapping("/signup")
public ResponseEntity<ApiResponseData<String>> signup(@RequestBody JoinRequest joinRequest){
joinService.join(joinRequest);
return ResponseEntity.ok(ApiResponseData.success(null, "회원가입에 성공하였습니다."));
}
Application에서 하나의 컨트롤러를 살펴보면 클라이언트로 받은 JoinRequest Dto를 그대로 service에게 넘겨서 사용하고있는것을 볼수있습니다. 이를 다음과 같은 구조로 변경하여 진행하였습니다.
Controller → [JoinRequest] → Mapper → [JoinDto] → Service
여기서 핵심적으로 봐야할 것은 변환시켜주는 별도의 Mapper클래스입니다.
Mapper클래스에서 매핑함수를 구현하는 방법으로 3가지를 고려하였습니다.
1. ObjectMapper클래스 활용
2. 매핑 메서드를 직접 작성
3. MapStruct를 사용
저는 3번 MapStruct를 사용하기로 결정하였습니다.
MapStruct는 Java bean 유형 간의 매핑 구현을 단순화하는 코드 생성기입니다.
특징은 다음과 같습니다.
- 반복되는 객체 매핑에서 발생할 수 있는 오류를 줄일 수 있으며, 구현 코드를 자동으로 만들어주기 때문에 사용이 편맇마.
- 컴파일 시점에 코드를 생성하여 런타임에서 안정성을 보장. 빌드시 미리 검사.
// 롬복
compileOnly 'org.projectlombok:lombok'
//MapStruct 의존성
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
다음과 같은 의존성을 추가하여야합니다. 참고로 MapStruct는 Lombok의 getter, setter, builder를 이용하여 생성되므로 반드시 추가되어야합니다.
또한 롬복에서 MapStruct와 충돌을 없애기 위한 org.projectlombok:lombok-mapstruct-binding 애노테이션 프로세서를 제공합니다. 이것을 사용하지 않으면 롬복 Annotation Processor와 호출 순서 등에서 충돌이 있습니다.
Mapper클래스는 다음과 같이 작성합니다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface DtoMapper {
JoinDto toJoinDto(JoinRequest request);
}
이렇게만 사용하면 알아서 구현체를 생성해줍니다. (매우 편리)
commponentModel = "spring"은 매퍼를 빈으로 만들어야 하는 경우, 아래와 같이 설정하면 빈으로 등록할 수 있습니다.
저는 Contoller에서 빈을 주입받아 사용해야하기 때문에 등록해주었습니다.
unmappedTargetPolicy는 Target 필드(JoinDTO)에는 존재하는데 source(JoinRequest)의 필드가 없는 경우에 대한 정책입니다.
- ERROR : 매핑 대상이 없는 경우, 매핑 코드 생성 시 error 가 발생합니다.
- WARN : 매핑 대상이 없는 경우, 빌드 시 warn 이 발생합니다.
- IGNORE : 매핑 대상이 없는 경우 무시하고 매핑됩니다.
세가지 정책이있고 저는 여기서 ERROR를 선택하였습니다.
이밖에도 default값을 사용하거나 커스텀 메서드를 사용할 수 있게하는등 다양한 설정이 존재하였습니다. 관심있으신분들은 공식문서나 관련 자료를 검색하시면 좋을것 같습니다.
다시 컨트롤러로 돌아와서 해당 매핑메서드를 추가하였습니다.
@PostMapping("/signup")
public ResponseEntity<ApiResponseData<String>> signup(@RequestBody JoinRequest joinRequest){
joinService.join(dtoMapper.toJoinDto(joinRequest));
return ResponseEntity.ok(ApiResponseData.success(null, "회원가입에 성공하였습니다."));
}
이렇게 사용만하면 끝입니다.
+ 오류발생
@Getter
public class JoinDto {
private String username;
private String password;
private String slackId;
private Role role;
private String name;
private String group;
public void setPassword(String password) {
this.password = password;
}
}
매핑하는 과정에서 모든 값들이 매핑되지 않아 저장될때 null값이 들어가는 현상이 발생했습니다.
타겟클래스를 살펴보니 Getter 어노테이션과 비밀번호를 설정하는 set함수만이 존재하였습니다.
앞서 말씀드렸다시피 Mapper의 구현체는 기본적으로 Setter또는 생성자를 통해 만듭니다. 그렇기 때문에 기본생성자만 존재하고 set함수는 password만 존재하다보니 구현체를 생성할때 password만 올바르게 매핑되었습니다.
@AllArgsContructor를 통해 생성자를 만들어 이를 해결하였습니다.
@Getter
@AllArgsConstructor
public class JoinDto {
private String username;
private String password;
private String slackId;
private Role role;
private String name;
private String group;
public void setPassword(String password) {
this.password = password;
}
}
정리📗
이번 글에서는 왜 DDD에서 각 레이어별로 DTO를 분리해주어야하는지, 그리고 어떻게 매핑클래스를 작성하는지에 대해 살펴보았습니다.
MapStruct는 매번작성해야하는 매핑 메소드를 자동으로 만들어주기 때문에 매우 편리하지만 자동으로 만들어주는만큼 JPA처럼 실제로 어떻게 구현되는지, 원하는 대로 동작하는지는 확인이 어려웠습니다.
하지만 반복되는 매핑 코드를 자동으로 생성해주는 만큼 휴먼 에러, DTO의 변경에도 유연하게 대처할수 있다는 점이 큰 장점인것 같습니다.
즉, 편리하더라도 조심히 쓰자가 결론이었습니다.
그럼이만!
참고
편리한 객체 간 매핑을 위한 MapStruct 적용기 (feat. SENS)
Ncloud 문자/알림 발송 서비스 SENS 개발 과정에서 MapStruct를 활용해 보았습니다.
medium.com
'IT 서비스 > 주문 관리 플랫폼' 카테고리의 다른 글
Spring Cloud환경에서 Gateway에 Sagger 적용 (0) | 2025.03.24 |
---|---|
[ 리뷰 기능 ] Redis+Scheduler를 활용한 매장 평균 별점 기능 고도화하기 - 2 (2) | 2025.02.21 |
[ 리뷰 기능 ] 매장 평균 별점 기능 고도화하기 - 1 (0) | 2025.02.20 |
👨💻[ 자동 메뉴 설명 작성 기능 - 1 ]OpenAI vs Gemini vs Llama, 그리고 Gemini 사용방법 (0) | 2025.02.14 |
댓글