Entity와 DTO 분리, 프로젝트 구조 변경

뚜우웅이·2023년 11월 21일

Entity와 DTO 분리

  • Entity는 데이터베이스와 연관이 있으며 데이터베이스의 영속성을 관리하기 위한 역할을 합니다.
  • DTO는 클라이언트와 서버 간의 데이터 전송을 관리하고, 필요한 데이터를 포함하는 구조로 클라이언트 요청 및 응답을 처리하는 데 사용됩니다.

Entity를 직접 사용하는 방식보다 DTO를 정의하여 요청과 응답을 하기 위한 객체로 사용하는 것이 더 권장됩니다.

만약 User 객체에 password 정보가 있을 때 User 자체를 return하게 되면 다른 사용자에게 보여주면 안 되는 password 정보가 같이 넘어가는 문제가 있습니다. 필요한 정보만 넘기기 위해서는 DTO를 사용해야 합니다.

분리를 하게 되면 얻는 이점은 아래와 같습니다.

  1. Entity 내부 구현을 캡슐화 할 수 있음
  2. 화면에 필요한 데이터 선별 가능
  3. 순환참조 예방
  4. Validation 코드 분리 가능

MapStruct

MapStruct는 객체 매핑 코드를 자동으로 생성해주는 Java 기반의 매핑 프레임워크입니다. 주로 Java 빈 (POJO) 객체 간의 데이터 전달 및 매핑을 더 간단하게 만드는 데 사용됩니다. MapStruct를 사용하면 반복적이고 지루한 매핑 코드를 직접 작성하지 않고도 자동으로 생성할 수 있습니다.

MapStruct는 Entity와 DTO 간의 매핑을 자동화해줍니다.

의존성 추가

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'

Mapper 생성

mapper 패키지를 생성한 후 BoardMapper 인터페이스를 생성해줍니다.

package toyproject.springmvcboard.mapper;

import org.mapstruct.Mapper;
import toyproject.springmvcboard.domain.Board;
import toyproject.springmvcboard.dto.BoardDTO;

@Mapper(componentModel = "spring")
public interface BoardMapper {
    BoardMapper INSTANCE = Mappers.getMapper(BoardMapper.class);
    BoardDTO boardToBoardDTO(Board board);

    Board boardDTOToBoard(BoardDTO boardDTO);
}

@Mapper(componentModel = "spring") 어노테이션을 사용하면 MapStruct가 해당 매핑 인터페이스를 스프링 빈으로 등록하도록 지시합니다. 이렇게 하면 스프링 애플리케이션에서 해당 매핑 클래스를 주입하거나 사용할 수 있게 됩니다.

BoardMapper INSTANCE = Mappers.getMapper(BoardMapper.class)는 MapStruct에서 제공하는 유틸리티인 Mappers를 사용하여 매퍼 인스턴스를 얻는 코드입니다.

MapStruct는 인터페이스로 선언된 매퍼에 대해 자동으로 구현체를 생성합니다. 그리고 이 생성된 구현체를 사용하여 매핑 작업을 수행할 수 있습니다.

자동 생성된 구현체

package toyproject.springmvcboard.domain.board;

import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-11-13T17:43:16+0900",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.5 (Azul Systems, Inc.)"
)
@Component
public class BoardMapperImpl implements BoardMapper {

    @Override
    public BoardDTO boardToBoardDTO(Board board) {
        if ( board == null ) {
            return null;
        }

        BoardDTO.BoardDTOBuilder boardDTO = BoardDTO.builder();

        boardDTO.id( board.getId() );
        boardDTO.title( board.getTitle() );
        boardDTO.content( board.getContent() );

        return boardDTO.build();
    }

    @Override
    public Board boardDTOToBoard(BoardDTO boardDTO) {
        if ( boardDTO == null ) {
            return null;
        }

        Board.BoardBuilder board = Board.builder();

        board.id( boardDTO.getId() );
        board.title( boardDTO.getTitle() );
        board.content( boardDTO.getContent() );

        return board.build();
    }
}

BoardService 생성

package toyproject.springmvcboard.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import toyproject.springmvcboard.domain.Board;
import toyproject.springmvcboard.dto.BoardDTO;
import toyproject.springmvcboard.mapper.BoardMapper;
import toyproject.springmvcboard.repository.BoardRepository;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class BoardService {
    private final BoardRepository boardRepository;
    private final BoardMapper boardMapper;

    @Autowired
    public BoardService(BoardRepository boardRepository, BoardMapper boardMapper) {
        this.boardRepository = boardRepository;
        this.boardMapper = boardMapper;
    }

    public List<BoardDTO> findAll() {
        List<Board> boards = boardRepository.findAll();
        return boards.stream()
                .map(board -> boardMapper.boardToBoardDTO(board))
                .collect(Collectors.toList());
    }


}

stream을 사용하여 List형태로 받아온 Board 정보를 BoardDTO로 매핑해준 뒤 List 형태로 반환을 해줍니다.

BoardController 수정

package toyproject.springmvcboard.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import toyproject.springmvcboard.dto.BoardDTO;
import toyproject.springmvcboard.service.BoardService;

import java.util.List;

@Controller
@RequestMapping("/board")
public class BoardController {

    private final BoardService boardService;

    @Autowired
    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    @GetMapping("/list")
    public String list(Model model) {
        List<BoardDTO> boardDTOs = boardService.findAll();
        model.addAttribute("boards", boardDTOs);
        return "board/list";
    }
}

기존의 boardRepository에 있는 findAll() 메소드를 사용한 것과 다르게 Service에서 구현한 findAll() 메소드를 사용해줍니다.

entity 대신에 view에 DTO를 전달하여 그린 화면입니다.

model의 패키지 명을 domain으로 변경하여 import에 있는 model이 domain으로 변경 되었습니다.

도메인형 구조로 변환

도메인 형 구조(Domain-Driven Design, DDD)는 소프트웨어 개발에서 비즈니스 도메인을 중심으로 모델을 구축하는 방법론입니다. 이 방법론은 실제 비즈니스 도메인의 복잡성을 다루기 위해 도입되었습니다.

도메인 형 구조는 비즈니스 기능을 기준으로 모듈을 나누어 개발함으로써 모듈 간의 강력한 응집성을 유지하고 결합성을 최소화합니다. 이는 코드를 더 쉽게 이해하고 유지보수하며, 유연한 시스템을 만드는데 도움이 됩니다.

위 사진과 같이 domain, global 패키지 안에 패키지와 클래스를 넣어 한 눈에 보기 쉽게 해줍니다.

domain 패키지 안에는 Controller, Servcie, dto, domain, exception 클래스 등으로 구성됩니다.
global 패키지 안에는 auth, common, config, error, infra, util등으로 구성됩니다.

디렉터리 구조 변경 시 bean 관련 문제가 발생할 수 있습니다. 이 때는 out 폴더를 삭제해준 뒤 gradle -> builde -> clean을 실행해줍니다.

profile
공부하는 초보 개발자

0개의 댓글