1. DTO와 객체 복사
1️⃣ SSR과 API 통신의 차이
1) SSR (Server Side Rendering)
- 서버가 HTML을 직접 만들어서 클라이언트(브라우저)에 전달
- Mustache 같은 템플릿 엔진에서
- 객체 전체를 전달하더라도
- 화면에 필요한 필드만 선택적으로 렌더링 가능
- 즉, 객체에
password가 있어도 템플릿에서 쓰지 않으면 노출되지 않음
➡️ 렌더링 제어가 서버 템플릿 레벨에서 가능
2) 모바일 앱 / SPA (JSON 기반 통신)
- 브라우저가 아니라 앱이 서버와 데이터(JSON) 로 통신
- 서버에서 객체를 그대로 반환하면
- 객체의 모든 필드가 JSON으로 직렬화되어 전송될 수 있음
- 불필요하거나 민감한 정보(password 등)가 네트워크를 통해 전달됨
➡️ 문제는 “화면에 보이느냐”가 아니라 “전송되느냐”임
2️⃣ DTO(Data Transfer Object)가 필요한 이유
1) 핵심 이유
- 클라이언트에 필요한 데이터만 보내기 위해
- 보내면 안 되는 데이터를 구조적으로 차단하기 위해
2) 엔티티(Entity) vs DTO
- 엔티티
- 도메인/DB 중심
- 내부 로직, 연관관계, 민감 정보 포함 가능
- DTO
- 화면/API 응답 중심
- 필요한 데이터만 포함
- 구조가 엔티티와 달라도 됨
➡️ 엔티티 ≠ 응답 모델
3️⃣ 객체 복사(매핑)
1) 개념
- DTO는 보통 기존 객체(엔티티)를 그대로 쓰지 않고
- 필요한 정보만 새 객체로 옮겨 담는 과정을 거침
- 이 과정을 흔히 아래와 같이 부름
- 객체 복사
- 매핑(mapping)
- 변환(transform)
2) 중요한 점
- 단순 복사만 하는 게 아니라 아래 기능 모두 가능
- 필드 선택
- 구조 변경
- 값 가공
- 조건 필터링
4️⃣ DTO 매핑 책임은 어디에 두는가?
1) 좋은 방향
- “이 DTO는 이 엔티티로부터 만들어진다”는 책임을
- DTO 내부에 두는 것이 좋음
2) 이유
- 매핑 로직이 여기저기 흩어지지 않음
- 수정 시 영향 범위가 줄어듦
- 의도가 명확해짐
5️⃣ 연관 관계 데이터와 DTO
1) 엔티티 구조
- 보통 DB 중심:
- 1:N, N:1, N:M 관계
- 객체 그래프가 깊고 복잡함
2) DTO 구조
- 화면/API 중심
- 단순화된 구조
- 필요한 데이터만 추출
- 연관 객체 전체를 보내지 않아도 됨
- DTO에서는 연관 객체를
- 일부 필드만 뽑거나
- 다른 타입으로 변환하거나
- 개수를 제한하거나
- 조건에 따라 필터링 가능
6️⃣ DTO는 “보안 + 정책”의 역할도 함
1) 보안 + 정책의 역할
- DTO는 단순한 데이터 운반 객체가 아니라
- 외부에 공개할 정보의 경계선
- API 응답 정책의 구현체
2) 예시
- 비밀번호, 내부 플래그 제거
- 관리자 전용 정보 제외
- 특정 조건에 맞는 데이터만 응답
➡️ “DTO를 잘 설계하면 실수로 정보가 새는 구조가 안 나옴”
7️⃣ 이름 있는 생성자 (Static Factory Method)의 의미
1) 문제
- 기본 생성자를 열어두면
- 잘못된 조합의 필드
- 의미 없는 상태의 객체가 쉽게 만들어짐
2) 해결
- 기본 생성자를
private로 막고
- 목적에 맞는 이름 있는 생성자만 제공
3) 효과
- 객체 생성 의도가 명확해짐
- 유효하지 않은 상태의 객체 생성을 원천 차단
- 도메인 규칙을 코드 레벨에서 강제
➡️
new는 자유롭지만 위험➡️ 이름 있는 생성자는 제한적이지만 안전
8️⃣ 전체 핵심 요약
- SSR에서는 템플릿이 출력 제어를 하지만
API 통신에서는 “전송 데이터 자체”를 제어해야 함
- 엔티티를 그대로 반환하지 말고 DTO로 변환해서 응답
- DTO는
- 필요한 데이터만 담고
- 구조를 단순화하고
- 보안과 정책을 담당
- 객체 생성/복사 방식은
- 코드 품질, 유지보수성, 안정성에 직결됨
9️⃣ 참고 코드
DTO를 사용하는 이유
🔟 실습
0) User 클래스 (기본)
C:\workspace\spring_lab\skillapp\src\main\java\ex01\User.java
package ex01;
import lombok.Data;
@Data // Getter, Setter, toString
public class User {
private int id;
private String username;
private String email;
}1) Setter 사용
C:\workspace\spring_lab\skillapp\src\main\java\ex01\CopyEx01.java
package ex01;
import lombok.Data;
@Data
class UserDTO {
private int id;
private String username;
private String email;
}
public class CopyEx01 {
public static void main(String[] args) {
// 1. Setter (가장 안 좋은 방법, 그래도 알고 있어야 함)
User user = new User();
user.setId(1);
user.setUsername("ssar");
user.setEmail("ssar@nate.com");
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
System.out.println(dto);
}
}
2) 생성자 사용
C:\workspace\spring_lab\skillapp\src\main\java\ex01\CopyEx02.java
package ex01;
import lombok.Data;
@Data
class UserDTO2 {
private int id;
private String username;
private String email;
public UserDTO2(int id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
}
public class CopyEx02 {
public static void main(String[] args) {
// 2. 생성자
User user = new User();
user.setId(1);
user.setUsername("ssar");
user.setEmail("ssar@nate.com");
UserDTO2 dto2 = new UserDTO2(user.getId(), user.getUsername(), user.getEmail());
System.out.println(dto2);
}
}

3) 생성자 매개변수(User 클래스 넘기기)
C:\workspace\spring_lab\skillapp\src\main\java\ex01\CopyEx03.java
package ex01;
import lombok.Data;
@Data
class UserDTO3 {
private int id;
private String username;
private String email;
public UserDTO3(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
}
}
public class CopyEx03 {
public static void main(String[] args) {
// 3. 생성자의 매개변수에 User 클래스 넘기기
User user = new User();
user.setId(1);
user.setUsername("ssar");
user.setEmail("ssar@nate.com");
UserDTO3 dto3 = new UserDTO3(user);
System.out.println(dto3);
}
}

4) 객체 2개 → DTO 옮기기 (생성자 매개변수로 객체 넘기기)
C:\workspace\spring_lab\skillapp\src\main\java\ex01\CopyEx04.java
package ex01;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
class Board { // 1:N의 관계에서 Board가 1
private int id;
private String title;
private String content;
private List<Reply> replies = new ArrayList<>();
}
@Data
class Reply { // 1:N의 관계에서 Reply는 N
private int id;
private String comment;
}
@Data
class DetailDTO {
private int id;
private String title;
private String content;
private List<String> comments = new ArrayList<>();
public DetailDTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getTitle();
// for (int i = 0; i < board.getReplies().size(); i++) {
// this.comments.add(board.getReplies().get(i).getComment());
// }
// for문은 복잡! stream이 보기 편함! -> 그래서 stream 배울 것
for (Reply reply : board.getReplies()) {
this.comments.add(reply.getComment());
}
}
}
public class CopyEx04 {
public static void main(String[] args) {
// 3. 클래스로 넘기기
Reply r1 = new Reply();
r1.setId(1);
r1.setComment("댓글1");
Reply r2 = new Reply();
r2.setId(2);
r2.setComment("댓글2");
Reply r3 = new Reply();
r3.setId(3);
r3.setComment("댓글3");
Board board = new Board();
board.setId(1);
board.setTitle("제목1");
board.setContent("내용1");
board.setReplies(List.of(r1, r2, r3));
DetailDTO detailDTO = new DetailDTO(board);
System.out.println(detailDTO);
}
}

package ex01;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
class Board { // 1:N의 관계에서 Board가 1
private int id;
private String title;
private String content;
private List<Reply> replies = new ArrayList<>();
}
@Data
class Reply { // 1:N의 관계에서 Reply는 N
private int id;
private String comment;
}
@Data
class DetailDTO {
private int id;
private String title;
private String content;
private List<String> comments = new ArrayList<>();
public DetailDTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getTitle();
// for (int i = 0; i < board.getReplies().size(); i++) {
// this.comments.add(board.getReplies().get(i).getComment());
// }
// for문은 복잡! stream이 보기 편함! -> 그래서 stream 배울 것
for (Reply reply : board.getReplies()) {
// if (id < 3)
if (reply.getId() < 3) {
this.comments.add(reply.getComment());
}
}
}
}
public class CopyEx04 {
public static void main(String[] args) {
// 3. 클래스로 넘기기
Reply r1 = new Reply();
r1.setId(1);
r1.setComment("댓글1");
Reply r2 = new Reply();
r2.setId(2);
r2.setComment("댓글2");
Reply r3 = new Reply();
r3.setId(3);
r3.setComment("댓글3");
Board board = new Board();
board.setId(1);
board.setTitle("제목1");
board.setContent("내용1");
board.setReplies(List.of(r1, r2, r3));
DetailDTO detailDTO = new DetailDTO(board);
System.out.println(detailDTO);
}
}

5) 이름이 있는 생성자
C:\workspace\spring_lab\skillapp\src\main\java\ex02\NamedEx01.java
객체 복사 X 객체 만드는 것 O
package ex02;
import lombok.Data;
@Data
class User {
// 공통
private int id;
private String username;
private String password;
private String type; // 학생, 선생 구분
// 학생
private String classRoom; // 교실
private String classYear; // 학년
// 선생
private String subject; // 담당 과목
private String teacherName; // 이름
private User() {} // 외부에서 절대 new 못함! 이름 있는 생성자에서 new 해줘야 함!
// 이름 있는 생성자 (목적에 맞게 new 가능 / 대신 기본생성자 접근 못하게 private로 막아야 함)
public static User createTeacher(int id, String username, String password, String subject, String teacherName) {
User user = new User();
user.id = id;
user.username = username;
user.password = password;
user.type = "선생";
user.subject = subject;
user.teacherName = teacherName;
return user;
}
public static User createStudent(int id, String username, String password, String classRoom, String classYear) {
User user = new User();
user.id = id;
user.username = username;
user.password = password;
user.type = "학생";
user.classRoom = classRoom;
user.classYear = classYear;
return user;
}
}
public class NamedEx01 {
public static void main(String[] args) {
User student = User.createStudent(1,"ssar","1234","101", "3");
User teacher = User.createTeacher(2,"cos","1234","수학", "cos");
System.out.println(student);
System.out.println(teacher);
}
}
Share article