개발 낙서장

[Spring] DTO와 Entity 본문

Java

[Spring] DTO와 Entity

권승준 2024. 2. 15. 19:56

DTO와 Entity 분리

Entity는 실제 DB에 매칭되는 클래스이다. 그래서 서버에서 Entity의 값을 수정함에 따라 실제 DB의 값도 변경된다.
이러한 Entity를 서버 이외의 곳에서도 다루게 되면 데이터 변조의 위험이 생긴다.
그래서 DTO라는 클래스에 필요한 정보를 담아 요청, 응답하는 방법을 사용한다.

DTO란 Entity에 있는 정보들 중 각 요청, 응답에 맞게 필요한 정보들을 골라 담는 클래스이다.
예를 들어 User 클래스가 있다고 가정하면, 로그인을 할 때 User 전체 정보를 요청받을 필요가 없이 로그인에 필요한 ID, 비밀번호만 받으면 되는데 DTO가 없다면 User 전체 정보를 받고 비교해야 한다.

하지만 ID, 비밀번호 정보만 갖고 있는 LoginDto라는 클래스를 만들어 해당 객체에 값을 담아 보내면 로그인에 필요한 정보들만 담을 수 있어 요청에 낭비가 없고 User Entity가 변조되지 않는다.

또한 Entity가 다른 Entity와 양방향 관계인 경우 순환 참조 문제가 발생할 수도 있다.

그리고 한 가지 더 중요한 것은 Controller에게 일을 시키면 안 된다. 모든 데이터 처리와 작업들은 서버 단에서 이루어져야 한다. 만약 Entity가 넘어온다면 Controller에서 따로 데이터를 가공해 응답을 해주어야 하는데 이는 올바른 API가 아니다.
DTO에 필요한 데이터만 담아서 보내면 Controller는 추가적인 일을 할 필요 없이 그대로 요청에 맞는 응답을 반환해주면 된다.

어떻게?

방법은 간단하다. 각 API의 요청, 응답에 맞게 Dto 클래스를 만들어 필요한 필드 값만 선언해 사용하면 된다.

@Entity
@Getter
@Setter
@Table(name = "TB_USER")
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @Column(nullable = false, unique = true)
    private String nickname;

    @Column(nullable = false, unique = true)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Column
    private String introduce;

    public User(String nickname, String password, String email) {
        this.nickname = nickname;
        this.password = password;
        this.email = email;
    }
}

다음과 같은 User Entity가 있다고 하자. User에는 PK, 닉네임, 비밀번호, 이메일, 한 줄 소개 필드가 존재한다.
회원 가입을 할 때에는 닉네임, 비밀번호, 이메일만 받아 회원가입 기능을 구현하고 싶다면?

@Getter
@Setter
public class SignupRequestDto {

    private String nickname;

    private String password;

    private String email;
}

SignupRequestDto라는 클래스를 만들어 Signup 요청을 받을 때 Body 부분에 닉네임, 비밀번호, 이메일만 받아오면 된다.

    @PostMapping("/users/signup")
    public String signup(@RequestBody SignupRequestDto requestDto) {
        userService.signup(requestDto);

        return "login";
    }

이렇게 받아온 값을 토대로 서버에서 User를 생성해 저장해주면 되므로 다른 사용자에게 직접적인 데이터가 보여질 위험도 줄어들고 안전한 데이터 처리가 가능해진다.

DTO 안에 Entity는 어떻게?

DTO 안에 Entity가 존재할 가능성도 있다. 예를 들어 Document라는 테이블이 존재한다면 해당 글을 어느 유저가 작성했는지가 있어야 하므로 유저를 참조해야 한다. 또한 글을 조회할 때에도 작성자가 나오게 하려면 Dto에 User의 정보를 담아야 한다.

이럴 땐 Dto에 Entity가 담길 수도 있는데 위험한 것 아닌가?
그래서 Dto 안에 Entity가 담기면 안 된다. 요청이나 응답에는 Entity 자체를 다루는 일은 절대 없어야 한다.

@Entity
@Getter
@Setter
@Table(name = "TB_DOCUMENT")
@NoArgsConstructor
public class Document {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long documentId;
    
    @Column(nullable = false)
    private String title;
    
    @Column(nullable = false)
    private String content;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "userId", nullable = false)
    private User user;

    public Document(String title, String content, User user) {
        this.title = title;
        this.content = content;
        this.user = user;
    }
}

 

N:1 관계로 User 테이블을 참조하고 있는 Document Entity이다. 해당 글을 조회하는 API가 있고 이걸 DocumentResponseDto라는 Dto에 담아 반환하려면?

@Getter
@NoArgsConstructor
public class DocumentResponseDto {

    private User user; // <- ?
    private Long documentId;
    private String title;
    private String content;

    public DocumentResponseDto(Document doc, User user) {
        this.user = user;
        documentId = duc.getDocumentId();
        title = doc.getTitle();
        content = doc.getContent();
    }
}

아마 이렇게 하면 유저에 대한 정보가 노출되므로 매우매우매우 위험할 것이다. 또한 어떤 경우에서 서로 양방향 관계일 경우 순환 참조가 발생해 서버가 혹은 페이지가 뻗어버릴 수도 있다.

@Getter
@NoArgsConstructor
public class DocumentResponseDto {

    private UserResponseDto userResponseDto;
    private Long documentId;
    private String title;
    private String content;

    public DocumentResponseDto(Document doc, UserResponseDto userResponseDto) {
        this.userResponseDto = userResponseDto;
        documentId = duc.getDocumentId();
        title = doc.getTitle();
        content = doc.getContent();
    }
}

유저의 Id(PK)와 닉네임만 담긴 UserResponseDto를 만들어 담아주면 불필요한 정보가 노출될 일도 없고 순환 참조 등의 부가적인 에러도 발생하지 않을 것이다.
혹은 id와 nickname 필드를 만들어 가져온 User 클래스에서 해당 값만 넣어주는 방법도 있다.
어쨌든 중요한 것은 Dto에서 Entity 객체를 직접 갖고 있는 것은 좋지 않다는 것이다.

3줄 요약

  1. 절대 Entity를 Controller 단에서 다루지 않는다.
  2. 절대 Dto에서 Entity를 참조하지 않는다.
  3. 절대 Controller에게 추가적인 일을 시키지 않는다.

'Java' 카테고리의 다른 글

[Spring] @DataJpaTest 사용 시 UnsatisfiedDependencyException 발생  (0) 2024.02.20
단위 테스트  (1) 2024.02.16
데이터베이스(DB, DataBase)  (0) 2024.02.13
[Spring] Filter 예외 처리  (0) 2024.02.05
[Spring] 쿠키와 세션  (0) 2024.01.25
Comments