10-4. DTO와 객체 복사

박은서's avatar
Feb 06, 2026
10-4. DTO와 객체 복사

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); } }
notion image

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); } }
notion image

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); } }
notion image

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); } }
notion image
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); } }
조건 추가
notion image

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); } }
notion image
Share article