ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Entity, DTO 그리고 @Service
    Spring Framework 2021. 7. 13. 22:24
    반응형

    서론

    Spring Data JPA를 사용하면서 발생하는 여러 문제들 때문에 Entity와 DTO를 분리하는 과정을 담았습니다. 그리고 이 DTO를 어떻게 스프링 컨테이너가 관리하는 Bean으로 등록할 것인지에 대한 고민을 담았습니다.


    Entity 사용의 문제점

     

    • Post(게시글)이 Board(게시판)의 boardNo(게시판 번호) 참조
    • 1개의 게시판에 여러 개의 게시물 --> 1:N (일대다) 매핑
    // 애노테이션 생략
    public class Board implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long boardNo;
    
        @Column(nullable = false, length = 100)
        private String name;
    
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "board")
        private Set<Post> posts; // 게시판에서 게시물(post)을 참조 중
    }
    // 애노테이션 생략
    public class Post implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long postNo;
    
        @Column
        private String title;
    
        @Column
        private String author;
    
        @Column
        private String content;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "boardNo")
        private Board board; // 또 게시판(Board) entity 조회
        
        // 기타 유저 필드 생략
    
    }

     

    게시물 목록을 조회하기 위해 게시판 (board)를 먼저 조회 합니다. 

     

    하지만 다시 Post --> Board 조회합니다. 왜냐하면 Post 객체는 Board 객체를 참조하고 있기 때문인데요. 이때의 문제점은 무한 순환 참조가 발생합니다. 즉 Post <--> Board 참조가 반복해서 발생합니다. 


    무한 순환 참조 해결책

    1. @JsonIgnore

    스프링부트는 Object를 JSON으로 변환할 때 HttpMessageConverters 클래스에서 Jackson 라이브러리를 사용합니다. 즉 Object를 JSON 형태로 직렬화 합니다.

     

    이때 Post 객체 직렬화 과정에서 Board 포함되어 있어, Board 객체를 같이 직렬화 합니다. 하지만 이 과정에서, Post 객체가 또 포함되어 있습니다.

     

    한 마디로 직렬화를 하는 과정에서 Entity가 서로 연관 관계에 있기 때문에 계속 반복해서 직렬화를 합니다. 

     

    @JsonIgnore은 간단합니다. 직렬화 과정에서 해당 애노테이션이 붙은 객체는 JSON으로 직렬화 하지 않습니다. 즉 아무리 연관 관계에 있더라도, 애초에 대상에서 제외하는 겁니다. 

     

    하지만 세상에 공짜는 없습니다. 간단한만큼 문제도 있습니다. 

     

    애플리케이션이 아주 간단하다면 @JsonIgnore을 사용하면 됩니다. "하지만 해당 필드 직렬화를 다른 곳에서 사용이 되어야 한다면, 어떻게 할까요?" 꼭 2개의 entity만이 연관 관계에 있는 것이 아니라, 3개가 연관 관계 일수도 있고 API의 목적에 따라도 다를 것입니다.

     

    그래서 저는 아래의 방법을 추천합니다.

     

    2. API에 맞는 DTO 구분

    // 애노테이션 상황
    public class BoardDTO {
    
        private long boardNo;
        private String name;
        private Set<Post> posts;
    }
    // 애노테이션 생략
    public class OnlyBoardDTO {
    
        private long boardNo;
        private String name;
        // Post 객체 연관관계 X
    
        // Board entity to OnlyBoardDTO
        public OnlyBoardDTO toOnlyBoardDTO(Board board) {
            return OnlyBoardDTO.builder()
                    .boardNo(board.getBoardNo())
                    .name(board.getName())
                    .build();
        }
        
        // OnlyBoardDTO to Board Entity
        public Board toBoardEntity(OnlyBoardDTO boardDTO) {
            return Board.builder()
                    .boardNo(boardDTO.getBoardNo())
                    .name(boardDTO.getName())
                    .build();
        }
    }

     

    • BoardDTO - Post entity를 참조해서 게시글을 가지고 옵니다
    • OnlyBoardDTO - Post entity를 참조하지 않고, 오직 게시판의 정보만 가지고 옵니다

     

    필요 API에 따라 DTO를 구분했습니다. 

     

    그리고 중요한 것은 Entity <--> DTO 사이의 변환을 위해서 메서드를 정의했습니다. 왜냐하면 DTO를 DB ~ Presentation layer까지 다 사용할 수는 없습니다. 어느 범위까지 사용할 것인지 정의를 하고, 이를 변환해야 합니다. 

     

    "그렇다면 DTO로 반환을 하게 되면 왜 무한 순환 참조가 일어나지 않을까요? "

     

    위에서 언급했듯이 무한 순환 참조는 객체를 JSON 형태로 변환하는 직렬화 과정에서 발생합니다. 즉 JSON으로 직렬화 할 객체에 연관 관계의 필드를 넣지 않으면 됩니다. @JsonIgnore에서는 애노테이션으로 제외를 시켰다면, DTO에서는 그냥 필드에 정의를 하지 않습니다.

     

    이때 애노테이션 방식과 다른 것은 Entity는 모든 필드를 정의했습니다. 그리고 상황에 맞는 DTO를 따로 구분을 했습니다. 그래서 좀 더 유연하게 대응할 수 있습니다. 

     

    DTO 방식은 무한 순환 참조를 방지할 수 있다는 아주 큰 장점이 있지만, 상황에 맞게 DTO를 많이 만들어야 하는 단점도 있습니다. 그래서 DTO 방식을 선택할 때는 객체명, 주석을 아주 꼼꼼하고 명확하게 작성해야 할 것 같습니다. 

     

    3. 아예 참조하지 않는 방법

    하지만 애플리케이션이 간단해서 Board에서 굳이 Post를 참조하지 않아도 된다면 연관관계를 없애도 될 것 같습니다.

     

    게시글 목록 조회 시, 게시판 이름을 먼저 조회 후 해당 게시판에 포함된 게시글들만 추가로 조회하면 됩니다. 이렇게 되면 양방향이 아닌, 단방향 매핑이 됩니다. 그래서 저는 과감히 제거 후 단방향으로 선택을 했습니다.


    DTO, Entity 정의 범위?

    이것은 정의하는 사람, 상황에 따라 다르다고 생각합니다. 다만 보통은 아래와 같이 정의하는 것 같습니다.

     

    DB --> (Entity) --> Service -->  (DTO) --> Controller/Presentation layer


    DTO의 빈 등록?

    @Service
    public class ReplyDTO implements Serializable {
    }
    @Service
    @AllArgsConstructor
    public class BoardService {
        private final ReplyDTO replyDTO;
    }

     

    처음에는 서비스단에서 위와 같이 DTO를 빈으로서 주입하고, 메서드들을 사용하려고 했습니다. 

     

    하지만 고민이 됐습니다. "DTO를 서비스로서 빈 등록을 해도 되는 것인가?"

     

    열심히 고민하고 구글, 페이스북을 검색하면서 내린 결론입니다. 

     

    스프링 컨테이너가 관리하는 빈들은 Singleton 입니다. 이는 한번 컨테이너에 등록되고 바뀌지 않는 것입니다.

     

    하지만 DTO에는 수많은 데이터가 담기고, 옮겨지고 반복됩니다. 이러한 것을 Singleton으로 관리하는 것은 원래의 목적에도 맞지 않고, 데이터가 변경될 우려가 있다고 생각했습니다. 

     

    그래서 아래와 같이 메서드 안에서 지역변수로서 선언 후 사용했습니다. 메서드의 사용 종료와 함께 해당 객체도 해제가 될 것이기 때문입니다.

        @Transactional
        public Reply updateReply(생략) {
            ReplyDTO replyDTO = getReply(replyNo); // 객체 생성
    
            // 코드 생략
        }

    DTO 생성의 추가 이점

    Entity는 DB layer로서 DB와 통신하는 것을 담당합니다. 하지만 Validation 역할까지 담당하면 과도한 역할을 담당하게 됩니다. 또한 API에 맞는 유효성 검증을 할 수 없습니다. 해당 Entity를 참조하는 서비스 로직은 모두 같은 유효성 검증을 사용해야 합니다.

     

    이때 DTO를 생성 후 API에 맞는 유효성 검증 후 DB layer로 넘겨준다면, 좀 더 유연한 검증을 할 수 있다고 생각합니다.


    참고 문헌

    https://docs.spring.io/spring-boot/docs/2.4.4/api/org/springframework/boot/autoconfigure/http/HttpMessageConverters.html

    https://k3068.tistory.com/m/32?category=919284 

    https://kkambi.tistory.com/194

    반응형

    댓글

Designed by Tistory.