[Mapstruct] uses = 옵션 사용 ( 다른 엔티티 DTO들 간의 Mapper 메서드 공유하기 )
문제
Question Entity
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Question extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long questionId;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Answer> answers = new ArrayList<>();
public void setAnswer(Answer answer){
answers.add(answer);
if(answer.getQuestion() != this){
answer.setQuestion(this);
}
}
}
Answer Entity
@Entity
@NoArgsConstructor
@Setter
@Getter
public class Answer extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long answerId;
//Todo: 사용자 객체
// @Column(nullable = false)
// private Owner owner;
@ManyToOne
@JoinColumn(name = "question_id")
private Question question;
private Long score;
private Boolean isAccepted;
@Lob // Large Object
@Column(nullable = false)
private String content;
public void setQuestion(Question question){
this.question = question;
if(!this.question.getAnswers().contains(this)){
this.question.setAnswer(this);
}
}
}
애노테이션을 보면 알겠지만 Answer와 Question는 다대일 관계를 가지고 있다.
{
"data": {
"questionId": 1,
"title": "제목입니다",
"content": "내용입니다.",
"answers": [
{
"answerId": 1,
"questionId": null, ## null 이 아닌 1이 나와야함
"score": 0,
"isAccepted": false,
"content": "해답은 다음과 같습니다. 어쩌구 저쩌구@@@@",
"createdAt": "2023-02-22T18:24:33.039779",
"modifiedAt": "2023-02-22T18:24:33.039779"
}
]
}
}
위는 Mapstruct를 통해 최종적으로 만들어지는 Response Json 이다. Question 안에 Answer 목록들이 있는 형태로 되어있는데 questionId에 값이 들어오지 않고 있는 모습을 볼 수 있었다.
문제 해결
원인은 MapStruct를 통해 만든 Mapper들이 서로 연동되지 않기 때문이었다.
AnswerMapper
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface AnswerMapper {
Answer answerPostDtoToAnswer(AnswerDto.Post requestBody);
Answer answerPatchDtoToAnswer(AnswerDto.Patch requestBody);
@Mapping(source = "question.questionId", target = "questionId")
AnswerDto.Response answerToAnswerResponseDto(Answer answer);
List<AnswerDto.Response> answersToAnswerResponseDtos(List<Answer> answers);
}
Answer Mapper는 위의 @Mapping
을 통해 객체필드값(source)에서 알맞은 부분을 가져와 ResponseDto의 필드에(target) 넣어주도록 설정해주었다.
QuestionMapper
@Mapper(componentModel = "spring")
public interface QuestionMapper {
Question questionPostDtoToQuestion(QuestionDto.QuestionPostDto questionPostDto);
Question questionPatchDtoToQuestion(QuestionDto.QuestionPatchDto questionPatchDto);
QuestionDto.QuestionResponseDto questionToQuestionResponseDto(Question question);
List<QuestionDto.QuestionResponseDto> questionsToQuestionResponseDtos(List<Question> questions);
}
@Getter
@Builder
public static class QuestionResponseDto {
private Long questionId;
private String title;
private String content;
private List<AnswerDto.Response> answers;
}
QuestionMapper에서는 Question ResponseDto를 만들기 위해 AnswerDto.Response
(DTO 라고 생각하시면 됩니다.)를 사용한다.
가장 쉽게 생각해볼 것은 “@Mapping
을 써볼수 있나?” 이지만 QuestionResponseDto
에 객체 필드가 가 아닌 List형식으로 answers 필드가 생성되어 있으므로 @Mapping
지정해주는 것은 불가능하다.
다시 생각해보자. 현재 가장 핵심인 문제는 Answer 엔티티를 AnswerDto.Response
로 변환해주는 메서드가 없다는 것이다.
그러므로 Answer 엔티티를 AnswerDto.Response
로 변환해주는 메서드를 가진 AnswerMapper를 QuestionMapper 에서 사용할 수도록 하는 것이다. 이는 아래처럼 use 옵션을 붙여주면 된다.
@Mapper(componentModel = "spring",uses = AnswerMapper.class)
public interface QuestionMapper {
Question questionPostDtoToQuestion(QuestionDto.QuestionPostDto questionPostDto);
Question questionPatchDtoToQuestion(QuestionDto.QuestionPatchDto questionPatchDto);
QuestionDto.QuestionResponseDto questionToQuestionResponseDto(Question question);
List<QuestionDto.QuestionResponseDto> questionsToQuestionResponseDtos(List<Question> questions);
}
이제 Mapstruct가 적절히 매핑코드를 생성해주어, Answer 엔티티의 객체필드로 된 Question에서 ID만 뽑아와 DTO에 넣어주는 작업을 해주게 된다. 그럼 올바른 ID 값을 얻을 수 있다.
{
"data": {
"questionId": 1,
"title": "제목입니다",
"content": "내용입니다.",
"answers": [
{
"answerId": 1,
"questionId": 1
"score": 0,
"isAccepted": false,
"content": "해답은 다음과 같습니다. 어쩌구 저쩌구@@@@",
"createdAt": "2023-02-22T18:24:33.039779",
"modifiedAt": "2023-02-22T18:24:33.039779"
}
]
}
}
좀 더 디테일하게 확인해보고 싶으면?
build - generated - annotationProcessor - 하위 main 에서 Mapstruct가 동작한 구현체를 확인해보자. use의 유무에 따라 어떤 구현체가 만들어지는지 확인해보면 더 깊은 이해를 할 수 있다.
AnswerMapperImpl
같은 이름으로 생성된다.