DTO, ModelMapper 그리고 Setter

Sehyeon·2024년 1월 19일

삽질 기록

목록 보기
2/4
post-thumbnail

이전에 점프 투 스프링부트를 참고해서 간단한 게시판을 구현해보았다. 게시판 기능 구현에 초점을 두어서 따로 DTO를 생성하지 않고 ViewEntity객체를 바로 넘기는 식으로 구현하였다. 또한 Entity객체에 Setter메서드를 추가해서 Entity값을 세팅하였는데 이러한 방식은 좋지 않다고 한다.
게시판을 다시 처음부터 만들어보면서DTO를 적용하고 Entity객체에 Setter를 제거해보자.


DTO란?

DTOData Transfer Object의 약자이다. DTO는 말 그대로 데이터 전송을 위한 객체이다.
스프링에서는 각 계층 간의 데이터 전송에 사용된다.

DTO가 필요한 이유

  1. Entity객체는 민감한 데이터(비밀번호 등...)가 포함될 수 있는데, View에서 Entity객체를 직접 사용하면 민감한 데이터가 노출될 위험이 있다.

  2. DTO를 통해 필요한 데이터만을 담아 전송하면, 불필요한 정보를 줄여서 효율적으로 데이터를 전송할 수 있다.

  3. 화면을 렌더링하는 View에는DTO를 사용하고 비즈니스 로직을 처리하는 부분에는 Entity객체를 사용함으로써 코드의 의도가 명확해진다.

  4. Entity의 변경이 비즈니스 로직에만 영향을 미치고, DTO의 변경이 클라이언트에만 영향을 미치게 되므로 코드를 더욱 쉽게 변경할 수 있다.

DTO 변환은 어디에서?

MVC 패턴에서 ControllerViewService의 중간다리 같은 역할을 한다.
View로 부터 받은 정보를 Service에 넘겨주고 비즈니스로직 처리 결과를 가져오는 것이 Controller의 역할인데 Controller에서 Entity를 생성하고 DTO로 변경하는 과정이 있는것은 좋지 않은 설계라고 생각한다.
그래서 Service계층에서 비즈니스 로직을 처리한후 결과 값을 DTO로 변경해서 Controller에 반환하는 식으로 구현하기로 하였다.


Setter

Entity에서의 Setter

일반적으로 Entity를 만들 때에는 Setter 메서드를 사용하지 않는 것이 좋다.
왜냐하면 Entity객체는 데이터베이스와 바로 연결되므로 데이터를 자유롭게 변환할 수 있는 Setter 메서드를 허용한다면 데이터 베이스의 값이 개발자가 의도하지 않은 방법으로 수정될 위험이 있기때문이다.

그러면 Entity 객체의 값이 수정이 필요하다면?

Setter를 만들지 않고 Entity객체를 구현했는데 만약에 Entity객체의 값이 수정이 필요하다면 어떻게 해야할까? 새로운 Entity객체를 만들어야하나?
아니다, 데이터를 변경해야 할 경우에는 값을 변경하는 메서드를 추가로 작성하면 된다. 하지만 setXXX와 같은 모호한 이름이 아니라 메서드의 이름만 봐도 의도가 확실하게 알 수 있도록 확실한 메서드명을 지정하는 것이 좋다.

DTO에 Setter가 필요한가?

일반적으로 객체에 Setter메서드를 구현하는 것은 좋지 않다고 생각한다. 왜냐하면 외부에서 객체의 내부 상태를 변경하기보다는 변경이 필요한 객체에 메시지를 보내서 해당 객체 스스로 상태를 변경하는 것이 객체지향적인 설계이기 때문이다.
하지만, DTO같은 경우에는 데이터 전송만을 목적으로 하기때문에 편의를 위해서 Setter가 있어도 괜찮다고 생각한다.


DTO 적용

@Entity
@Getter
public class Question {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 200)
    private String subject;

    @Column(columnDefinition = "TEXT")
    private String content;

    private LocalDateTime createDate;

    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    private List<Answer> answerList;

    protected Question() {
    }
}

게시판의 질문 Entity객체

@Getter
@Setter
@Builder
public class QuestionDto {

    private Integer id;
    private String subject;
    private String content;
    private LocalDateTime createDate;
}

질문에 대응되는 DTO객체

@Service
@RequiredArgsConstructor
public class QuestionService {
    private final QuestionRepository questionRepository;

    public List<QuestionDto> getList() {
        return this.questionRepository.findAll().stream()
                .map(question -> QuestionDto.builder()
                        .id(question.getId())
                        .subject(question.getSubject())
                        .content(question.getContent())
                        .createDate(question.getCreateDate())
                        .build())
                .toList();
    }
}

Builder패턴을 사용해서 Service계층에서 조회한 질문 목록을 QuestionDto로 변경해서 반환해준다.

@RequiredArgsConstructor
@Controller
@RequestMapping("/question")
public class QuestionController {

    @GetMapping("/list")
    public String questionList(Model model) {
        List<QuestionDto> questionList = questionService.getList();
        model.addAttribute("questionList", questionList);
        return "question_list";
    }
}

Controller에서 Service로 부터 받은 Dto리스트를 View에 넘겨준다.
정상적으로 실행된다.

아쉬운점

데이터베이스에서 조회한 `Question 리스트를 Service 계층에서 QuestionDto로 변경한다음에 컨트롤러에 반환하고 컨트롤러에서 ViewquestionList라는 이름으로 조회 정보를 넘겨서 화면에 렌더링 해보았다.

@Service
@RequiredArgsConstructor
public class QuestionService {
    private final QuestionRepository questionRepository;

    public List<QuestionDto> getList() {
        return this.questionRepository.findAll().stream()
                .map(question -> QuestionDto.builder()
                        .id(question.getId())
                        .subject(question.getSubject())
                        .content(question.getContent())
                        .createDate(question.getCreateDate())
                        .build())
                .toList();
    }
}

하지만 위의 코드를 보면 조회한 Question 엔티티를 QuestionDto로 변환하는 부분이 너무 지저분하다. 만약에 QuestionDto의 필드가 많아진다면 Service계층에서의 Dto변환 코드는 점점 더 길어 질 것이다...


ModelMapper

앞서 Service계층에서 EntityDTO로 변경하는 과정에서 지저분한 코드가 있는 것을 확인했다. 어떻게 하면 이부분을 개선할 수 있을까 찾아보던 도중 ModelMapper라는 것을 찾게 되었다.

ModelMapper란?

"서로 다른 클래스의 값을 한 번에 복사하게 도와주는 라이브러리"
어떤 Object(Source Object)에 있는 필드 값들을 자동으로 원하는 Object(Destination Object)에 Mapping 시켜주는 라이브러리이다.

간단한 사용 방법

  1. build.gradle에 의존성 추가
implementation 'org.modelmapper:modelmapper:2.4.2'
  1. 스프링 빈에 수동 등록
@Configuration
public class AppConfig {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}

안해도 되지만 스프링의 의존성 주입을 이용하기 위해서 빈으로 등록해주었음

  1. ModelMappermap 메서드를 이용해서 Mapping을 진행한다.
@Service
@RequiredArgsConstructor
public class QuestionService {
    private final QuestionRepository questionRepository;
    private final ModelMapper modelMapper;

    public List<QuestionDto> getList() {
        return this.questionRepository.findAll().stream()
                .map(question -> modelMapper.map(question, QuestionDto.class))
                .toList();
    }
}

map메서드의 첫번째 인자값이 Source Obejct 두번째 인자값이 Destination Object가 된다.
위의 코드에서는 Question객체의 필드가 QuestionDto의 필드로 매핑된다.

  1. 매핑된 값 확인
    디버그 모드로 실행했을때 Controller에서 QuestionQuestionDto로 매핑이 잘되어 있는 것 을 확인할 수 있다.

참고 자료

https://modelmapper.org/
https://devwithpug.github.io/java/java-modelmapper/
https://squirmm.tistory.com/entry/Spring-modelMapper
https://hudi.blog/data-transfer-object/

0개의 댓글