서버와 클라이언트가 데이터를 주고받기 위해 사용하는 객체를DTO (Data Transfer Object) 라고 부릅니다.
말 그대로, 필요한 데이터를 담아서 전달하기 위한 박스라고 할 수 있습니다.
예를 들어, 클라이언트가 게시글 제목만 전송하는 경우 이렇게 간단하게 만들 수 있어요:
public record PostRequest(String title) {}
근데 우리에겐 Entity가 있잖아요..? DTO랑 Entity의 차이가 뭘까요??
Entity는 데이터베이스 테이블과 1:1 매핑되는 진짜 데이터 모델입니다.
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private int likeNum;
@OneToMany(mappedBy = "post")
private List<Comment> comments;
}
근데 해당 화면에 이 모든 데이터가 필요하지 않을 수 있잖아요..?
만약 좋아요 수와 제목만 필요한 상황이라면! 따로 DTO를 만들어서..
public record PostSummaryResponse(Long id, String title, int likeNum) {}
이렇게 필요한 정보만 담아서 줄 수 있겠죠!
→ Entity도 데이터를 담을 수 있는데 굳이 왜 DTO라는 걸 또 만들어야 할까?
1️⃣ 보안의 이유
Entity(DB객체)는 보통 데이터베이스 구조를 그대로 가지고 있기 때문에 이를 클라이언트에 그대로 보내면 민감한 정보를 노출하게 됩니다.
2️⃣ 역할 분리
Entity는 단순한 데이터 저장용이 아니라, 비즈니스 로직을 포함할 수도 있는 객체입니다. 반면, DTO는 오직 데이터 전달만을 위한 객체입니다.
3️⃣ 유연성
API 스펙이 바뀌더라도 Entity는 건드리지 않고 DTO만 바꿀 수 있습니다.
만약, API 스펙이 바뀔 때마다 Entity를 고친다면, DB와 연관된 로직들이 영향을 받을 수 있게 되겠죠..
API 스펙은 자주 바뀔까? 그렇다면 DTO는 어떤 형태를 띄고 있어야 할까?처음에는 게시글의 title만 화면에 보여줬다고 가정해볼게요.
하지만 어느 날, 기획자가
"이제 게시글 목록에 작성일자(createdAt) 도 같이 보여주세요!"
라고 요구할 수도 있겠죠.
이처럼 프론트엔드의 화면 요구사항이 바뀌면,
그에 따라 API가 전달해야 할 데이터 구조도 바뀌게 됩니다.
예를 들어, 아래와 같이 PostResponse DTO가 확장될 수 있어요:
// 원래의 DTO
public record PostResponse(String title) { }
// 수정 후의 DTO
public record PostResponse(String title, LocalDateTime createdAt) { }
DTO를 따로 분리해두었기 때문에, DB 구조나 Entity, 비즈니스 로직에 영향을 주지 않고도새로운 요구사항을 쉽게 반영할 수 있게 되었습니다.
따라서 DTO는...
와 같은 특징을 가져야 합니다.
Java 16부터는 record라는 문법이 추가돼서, DTO 작성이 매우 간결해졌습니다.
public class PostRequest {
private String title;
public PostRequest() { }
public PostRequest(String title) {
this.title = title;
}
public String getTitle() {
return title;
} -> 나중에 컨트롤러나 서비스 계층에서 해당 title을 꺼내서 써야되니까.. 여기에 getter가 필요한거임.
}
PostRequest postRequest = new PostRequest("post title");
String title = postRequest.getTitle(); //getter를 통해 title 값을 가져옴
이때, getTitle()이 없으면 postRequest.title로 직접 접근할 수가 없게 됩니다.
왜냐하면 title은 private으로 선언되어 있어서 외부에서는 직접 접근이 불가능하기 때문이죠!
그럼 왜 private으로 필드를 막아두고 getter로 열어두는걸까?→ 캡슐화 원칙을 따르기 위해서 입니다.
캡슐화란?
private)를 외부에서 직접 접근 못 하게 막고 getter, setter 같은 메서드를 통해 제어된 방식으로 접근하게 만드는 것을 의미합니다. 이렇게 하면…
→ 실수로 객체 내부 상태를 잘못 바꾸는 걸 막을 수 있습니다
→ 디버깅/테스트/유지보수가 쉬워집니다.
public record PostRequest(String title) { }
private final String title 필드가 생성됨 → 불변성 필드가 final 로 고정되어, 값이 변경되지 않음title()), equals(), hashCode(), toString() 메서드 생성그러면 편하고 가독성도 좋게 record를 계속 사용하면 좋지 않을까?
라는 생각이 들었는데요..
당연히 아닙니다!!!
1️⃣ 값 변경 불가
record의 모든 필드는 자동으로 final로 선언되기 때문에, 생성 이후 값을 수정할 수 없습니다.
유연한 상태 변경이 필요한 경우에는 적합하지 않습니다.
2️⃣ 상속 불가능
record는 암묵적으로 final 클래스로 생성되기 때문에 다른 클래스가 이를 상속하거나 record 자체가 어떤 클래스를 상속받는 것도 불가능하게 됩니다. 복잡한 계층 구조가 필요한 설계에서는 제약이 될 수 있겠죠
⇒ 이러한 특성 때문에 record는 상태를 유지하거나 로직을 수행하는 클래스보다는, 단순히 데이터를 전달하는 용도에 특화된 구조라고 생각이 듭니다. 내부에 복잡한 로직이나 상태 관리가 필요한 경우에는 일반 클래스를 사용하는 것이 더 좋은 듯 합니다!