어떤 카테고리로 분류할지 고민이 된다.
[Spring/그 외] 카테고리에 해야할지,
[프로젝트] 카테고리에 해야할지,
[생각들] 카테고리에 해야할지.
일단 [생각들] 카테고리에 분류했다.
결국 [프로젝트] 카테고리로 옮겼다.
각설하고.
MySQL을 알게되니, MyBatis를 알게되고
MyBatis를 알게되니, JPA를 알게되고
JPA를 알게되니, TestCode를 알게되고
TestCode를 알게되니 DTO를 알게되었다.
파도파도 끝이 없는 코딩의 세계.
(DTO 정리만 벌써 3번째인 것 같음;;)
<궁금증 List>
- DTO가 정확히 뭘 의미하는 걸까?
- DTO를 도대체 "왜" 쓰는 걸까?
- 왜?왜?왜?왜?왜?왜에에에에에ㅔㅔㅔ에????????????????
DTO란?
DTO(Data Transfer Object)는 프로세스 간 데이터를 전달하는 객체이다.
즉, 계층 간 데이터 전송을 위해 도메인 모델 대신 사용되는 객체이다.
※ 이때 계층이란?
- Presentation(View, Controller), Buisness(Service), Persistence(DAO, Repository) 등을 의미한다.
- DTO는 순수하게 데이터를 저장하고, 데이터에 대한 getter, setter만을 가져야한다고 한다.
- DTO는 어떠한 비즈니스 로직을 가져서는 안 되며,
저장, 검색, 직렬화, 역직렬화 로직만을 가져야 한다고 한다.
(※ 직렬화 : DTO를 Byte, JSON, XML 등의 형태로 변환하는 것)
또한 로버트 C.마틴의 저서 [클린 코드]를 통해 다음을 알 수 있다.
- 외부와 통신하는 프로그램에 있어 호출은 큰 비용이며, 이를 줄이고 더욱 효율적으로 값을 전달할 필요가 있다.
- 이를 위해 데이터를 모아 한 번에 전달하는 방법이 고안되었다.
- 이때 이 클래스를 DTO라고 한다.
도메인 대신 DTO를 사용하면 좋은 이유
도메인 모델을 캡슐화 하여 보호할 수 있다.
DTO 대신 도메인 모델을 계층 간 전달에 사용하면,
UI 계층에서 도메인 모델의 메소드를 호출하거나 상태를 변경시킬 수 있다.
또한 UI 화면마다 사용하는 도메인 모델의 정보는 상이하다.
하지만 도메인 모델은 UI에 필요하지 않은 정보까지 가지고 있다.
이런 모든 도메인 모델 속성이 외부에 노출되면, 보안 문제가 발생할 수 있다.
DTO를 써야만 하는 이유
DTO를 쓰지 않으면, 통신의 횟수가 증가할 뿐 아니라 로직이 비효율적이 되기 때문이다.
- 데이터를 묶어서 하나의 요청으로 보내면, 검증과 로직 처리 역시 한 번에 할 수 있겠다.
- 데이터를 묶어서 하나의 요청으로 보내면, 하지 않아도 될 고민을,
따로 보낼 땐 다 해줘야 해서 불편하겠다.
- 안정성과 수행시간 모두 묶어 보내는 쪽이 유리하겠다.
물론 이런 단순한 이유만 있는 것은 아니다.
- 직렬화를 캡슐화함으로써, DTO는 이 로직을 코드에서 제외하고,
원하는 경우 직렬화를 변경할 수 있는 명확한 지점을 제공한다.
(전달된 값을 서버에 사용하는 값으로 바꾸기 위해 직렬화를 거쳐야 함.
컨트롤러(통신 담당) Layer에서 직렬화에 대한 코드를 가져야 한다.)
마지막으로 DTO를 써야 하는 이유는,
Entity가 아닌 DTO를 전달함으로써 각 레이어 간 역할을 분리할 수 있기 때문이다.
요청과 응답으로 엔티티(Entity) 대신 DTO를 사용하자
- 예시 1 : 컨트롤러에서 요청과 응답으로 엔티티를 직접 사용했을 시
@GetMapping("/lines/{id}")
public ResponseEntity<Line> read(@PathVariable("id") Long id) {
Line line = lineService.readLine(id);
return ResponseEntity.ok(line);
}
- 예시 2 : DTO를 정의하여 요청과 응답의 객체로 사용
@GetMapping("/lines/{id}")
public ResponseEntity<LineResponseDto> read(@PathVariable("id") Long id) {
LineResponseDto line = lineService.readLine(id);
return ResponseEntity.ok(line);
}
이유 1. 엔티티 내부 구현을 캡슐화 할 수 있다.
엔티티가 getter와 setter를 갖게 된다면,
controller와 같은 비즈니스 로직과 크게 상관없는 곳에서 자원의 속성이
실수로라도 변경될 수 있다.
또한 Entity를 UI계층에 노출하는 것은
테이블 설계를 화면에 공개하는 것이나 다름 없기 때문에 보안상으로도 바람직 하지 않다.
따라서 엔티티 내부 구현을 캡슐화하고, UI계층에 노출시키지 않아야하는 것은
충분히 데이터 전달 역할로 DTO를 사용해야 할 이유로 볼 수 있다.
이유 2. 화면에 필요한 데이터를 선별할 수 있다.
애플리케이션이 확장되면, 엔티티의 크기는 점차 커지게 된다.
엔티티의 크기만 커질까?
화면도 다양해지고, API 스펙도 더 많아질 것이다.
이때 요청과 응답으로 엔티티를 사용한다면,
요청하는 화면에 필요하지 않은 속성까지도 함께 보내지게 된다.
→ 속도가 느려짐
※ 물론, 엔티티에서도 @JsonIgnore 같은 어노테이션을 사용하면,
화면으로 보내지 않을 속성을 지정할 수 있는데, 이 역시 근본적인 해결책이 될 수는 없다.
이유 3. 순환 참조를 예방할 수 있다.
서로 참조하는 객체를 계속 호출하면서 결국 무한 루프에 빠지게 되는 문제를 낳게된다.
양방향 참조가 부득이한 상황이라면,
순환참조가 일어나지 않도록 응답의 return으로 DTO로 두는 것이 더 안전하다고 할 수 있다.
이유 4. validation 코드와 모델링 코드를 분리할 수 있다.
엔티티 클래스는 DB의 테이블과 매칭되는 필드가 속성으로 선언되어 있고,
복잡한 비즈니스 로직이 작성되어있는 곳이다.
그렇기 때문에, 속성에는 @Column, @JoinColumn, @ManyToOne, @OneToOne 등의
모델링을 위한 코드가 추가된다.
여기에 만약 @NotNull, @NotEmpty, @NotBlank 등과 같은 요청에 대한 값의
validation 코드가 들어간다면,
엔티티의 클래스는 더 복잡해지고 그만큼 가독성이 저하된다.
이때, 각각의 요청에 필요한 validation을 DTO에서 정의한다면,
엔티티 클래스를 좀 더 모델링과, 비즈니스 로직에만 집중되도록 만들 수 있다.
- 예시
[Member 엔티티]
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="member_id")
private Long id;
private String name;
@Column(unique = true)
private String email;
private String password;
private String address;
@Enumerated(EnumType.STRING)
private Role role;
public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder) {
Member member = new Member();
member.setName(memberFormDto.getName());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.ADMIN);
return member;
}
}
→ 갑자기 든 생각인데, Entity에 setter 써도 되나..?;;;
[MemberFormDto]
@Setter
@Getter
public class MemberFormDto {
@NotBlank(message = "이름은 필수 입력 값입니다.")
private String name;
@NotEmpty(message = "이메일은 필수 입력 값입니다.")
@Email(message = "이메일 형식으로 입력해주세요.")
private String email;
@NotEmpty(message = "비밀번호는 필수 입력 값입니다.")
@Length(min=8, max=16, message = "비밀번호는 8자 이상, 16자 이하로 입력해주세요")
private String password;
@NotEmpty(message = "주소는 필수 입력 값입니다.")
private String address;
}
DTO와 VO의 차이
VO(Value Object)도 DTO와 동일한 개념이다.
다만 DTO와의 차이는, DTO는 데이터를 계층간 교환하는데 의미가 있고,
VO는 읽기만 가능한 read-only 속성을 가진 객체로서 데이터 그 자체에 의미를 둔다.
결론
API 스펙과 엔티티 사이에 의존성이 생기는 문제도 있다.
우리는 UI와 도메인이 서로 의존성을 갖지 않고, 독립적으로 개발하는 것을 지향하기 때문에
이를 중간에서 연결시켜주는 DTO의 역할이 중요하다.
요청과 응답으로 DTO를 사용하면,
각각의 DTO 클래스가 데이터를 전송하는 클래스로서의 역할을 명확히 가질 수 있게 되고,
이는 하나의 클래스가 하나의 역할을 해야 한다는 객체지향의 정신과도 부합한다.
∴ 하나의 엔티티에 너무 많은 책임을 주지 말자.
내 생각...
추가적으로, Controller(Web Layer)에서 Service(Service Layer)로 값을 전달할 때,
요청으로 받은 DTO를 그대로 넘기는 것에 대한 고민들이 있는 듯 하다.
이를 위한 해결책으로 DTO를 하나 더 만들어
Controller-Service 통신 간에 사용하는 것(서비스 DTO)을 제시하는 방법이 있다고 한다.
어렵고도 깊은 코딩의 세계이다.
- DTO 고찰 일기 끝(사실 끝은 아님) -
참고
- https://kafcamus.tistory.com/13
- https://hudi.blog/data-transfer-object/
- https://velog.io/@ohzzi/Entity-DAO-DTO%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EB%A9%B0-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C
- https://tecoble.techcourse.co.kr/post/2020-08-31-dto-vs-entity/
- https://kafcamus.tistory.com/12
- https://techblog.woowahan.com/2711/
'프로젝트' 카테고리의 다른 글
CRUD를 분석해보자 (1) - 게시글 생성 API (0) | 2023.02.11 |
---|---|
Spring Security를 이용하여 로그인을 구현해보자 (0) | 2023.01.26 |
[예외 처리] 회원가입 Custom Exception을 해보자 (0) | 2023.01.19 |
프로젝트 진행 중 (2) - JPA 연결 (0) | 2022.12.19 |
프로젝트 진행 중 (1) - JPA 삽질 (2) | 2022.12.19 |