[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
에게 정보를 넘겨주는데, 이 때 description
은 Game
의 필드이다.
하지만 실제 프론트에서 사용하기 위해서는 기존의 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;
위 컨트롤러에서는 유저의 Id
로 userService
를 통해 DB를 조회하여 그에 맞는 User
객체를 가져와 프론트로 보내게 된다.
profile.html
가 필요로 하는 정보는 단 두 가지, 유저의 nickname
과 introduction
뿐이다.
하지만 실제로 User
클래스는 어떻게 구성되어있을까?
수 많은 컬럼 뿐 아니라, Post
, OneLineComment
, Comment
세 가지의 Entity
와도 연관관계 매핑되어 있다.
즉, user
를 프론트로 전송하게 되면 그 안에 있는 다른 엔티티들까지 모두 딸려나가게 되는 것이다.
한 눈에 보기에도 너무나 비효율적이다.
이는 새로운 DTO
를 만들어 nickname
과 introduction
두 필드만 구성하여 내보낸다면 데이터를 주고 받는데에 있어서 훨씬 많은 이득을 얻을 수 있을 것이다.
4. 보안
아직은 직접 경험해보지 못했지만, 그래도 자료를 통해 어느정도 감은 잡을 수 있었다.
실제 필요로 하지 않는 불필요한 데이터까지 모두 전송되기 때문에, 절대 프론트까지 나가서는 안되는 민감한 정보 또는 비지니스 로직등이 모두 노출 될 수 있다.
Leave a comment