[Spring] DTO를 사용해야 하는 이유 생각해보기

Updated:

I. 개요

처음 스프링 부트와 AWS로 혼자 구현하는 웹 서비스의 예제를 따라할 때, Entity가 멀쩡히 있는데 왜 굳이 DTO라는 클래스를 새로 만들어서 프론트와 교환하는 비효율적인 방식을 선택할까 굉장히 고민했다.
특히, Request의 경우에는 프론트에서 보내주는 데이터의 묶음을 한 객체로 받아낼 수 있어서 그럴 듯 하다고 생각했으나 Response의 경우 그냥 Entity객체를 바로 쏴주고 골라 사용하면 되는데 대체 왜?? 라는 생각을 지울 수 없었다.

하지만 최근 프로젝트를 진행해감에 따라 DTO를 굳이 만들고 사용하는 이유를 조금은 알게된 것 같아서 생각한 내용들을 정리한다.

II. DTO를 사용하는 이유

1. 가공 및 수정의 편리성

가장 먼저 DTO의 필요성을 실감했던 부분이다.

GameController.java

model.addAttribute("game", game);
model.addAttribute("description", game.getDescription().replace("\n", "<br>"));

위 코드는 Model을 통해 View에게 정보를 넘겨주는데, 이 때 descriptionGame의 필드이다.
하지만 실제 프론트에서 사용하기 위해서는 기존의 Game안의 description과는 다른 형태로 별도의 가공이 필요하기 때문에 description이라는 이름으로 거의 같은 내용의 정보를 다시 한 번 넘겨주게 된다.
Entity를 그대로 넘겨주게 될 경우 별도의 가공이 어렵기 때문에, 이와 같이 정보의 중복이 일어났다.

이를 개선한 코드는 아래와 같다.

GameController.java

model.addAttribute("game", GameResponse.from(game));

GameResponse.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GameResponse {

    private Long id;
    private String title;
    private String description;
    private String pageUrl;
    private String imgUrl;
    private String releasedDate;
    private String price;
    private String language;
    private int reLike;
    private int reHate;

    @Builder
    public GameResponse(Long id, String title, String description, String pageUrl, String imgUrl, LocalDate releasedDate, String price, String language, int reLike, int reHate) {
        this.id = id;
        this.title = title;
        this.description = description.replace("\n", "<br>");
        this.pageUrl = pageUrl;
        this.imgUrl = imgUrl;
        this.releasedDate = releasedDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
        this.price = price;
        this.language = language;
        this.reLike = reLike;
        this.reHate = reHate;
    }

    public static GameResponse from(Game game) {
        return GameResponse.builder()
                .id(game.getId())
                .title(game.getTitle())
                .description(game.getDescription())
                .pageUrl(game.getPageUrl())
                .imgUrl(game.getImgUrl())
                .releasedDate(game.getReleasedDate())
                .price(game.getPrice())
                .language(game.getLanguage())
                .reLike(game.getReLike())
                .reHate(game.getReHate())
                .build();
    }
}

GameResponse라는 별도의 DTO를 만들었고, 정적 메서드로 하여금 Game객체를 받으면 이를 원하는 형태로 가공하여 반환하도록 했다.
이로써 기존의 discription의 중복이 사라지게 되었다.

추가적으로, 프론트의 요구사항에 따라 Entity의 설계를 수정하는 것은 결코 쉬운 일이 아니다.
이러한 부분을 DTO클래스의 수정을 통해서 쉽게 해결할 수 있을 것이다.

2. 데이터의 간소화

UserController.java

@GetMapping("/user/profile")
public String profile(@LoginUser SessionUser sessionUser, Model model) {
    model.addAttribute("user", userService.findById(sessionUser.getId()));
    return "user/profile";
}

User.java

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;

private String email;
private String password;
private String nickname;
private String picture;
private String introduction;

@Enumerated(EnumType.STRING)
private Role role;

@Enumerated(EnumType.STRING)
private RegistrationId registrationId;

private String authKey;

@OneToMany(mappedBy = "user")
private List<Post> posts;

@OneToMany(mappedBy = "user")
private List<OneLineComment> oneLineComments;

@OneToMany(mappedBy = "user")
private List<Comment> comments;

위 컨트롤러에서는 유저의 IduserService를 통해 DB를 조회하여 그에 맞는 User객체를 가져와 프론트로 보내게 된다.
profile.html가 필요로 하는 정보는 단 두 가지, 유저의 nicknameintroduction뿐이다.

하지만 실제로 User클래스는 어떻게 구성되어있을까? 수 많은 컬럼 뿐 아니라, Post, OneLineComment, Comment 세 가지의 Entity와도 연관관계 매핑되어 있다.
즉, user를 프론트로 전송하게 되면 그 안에 있는 다른 엔티티들까지 모두 딸려나가게 되는 것이다.

한 눈에 보기에도 너무나 비효율적이다.

이는 새로운 DTO를 만들어 nicknameintroduction 두 필드만 구성하여 내보낸다면 데이터를 주고 받는데에 있어서 훨씬 많은 이득을 얻을 수 있을 것이다.

4. 보안

아직은 직접 경험해보지 못했지만, 그래도 자료를 통해 어느정도 감은 잡을 수 있었다.
실제 필요로 하지 않는 불필요한 데이터까지 모두 전송되기 때문에, 절대 프론트까지 나가서는 안되는 민감한 정보 또는 비지니스 로직등이 모두 노출 될 수 있다.

Leave a comment