도슐랭스타

DTO 클래스 생성 Request, Response 본문

Spring Boot project

DTO 클래스 생성 Request, Response

도도.__. 2025. 4. 13. 13:27

DTO = Data Transfer Object
데이터를 전송하기 위한 객체
요청이나 응답에 사용할 데이터만 따로 담아두는 전용 박스

아니 처음에 DTO가 전혀 이해가 안 가는 겁니다.
이거 왜 쓰는데?! 뭐가 다른데?! 으악

코드 먼저 보겠습니다.

ReadingRecordRequest.java

package me.dodo.readingnotes.dto;

import java.time.LocalDate;

public class ReadingRecordRequest {
    private String title;
    private String author;
    private LocalDate date;
    private String content;

    // 기본 생성자
    // 없으면 JPA와 동일하게 Jackson이 리플렉션을 못해서 자동으로 body에 있는 값을 객체 형태로 넣을 수 없게 됨.
    // 스프링은 내부적으로 Jackson이라는 JSON 변환기를 씀.
    public ReadingRecordRequest(){}


    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }

    public LocalDate getDate() {
        return date;
    }
    public void setDate(LocalDate date) {
        this.date = date;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

ReadingRecordResponse.java

package me.dodo.readingnotes.dto;

import java.time.LocalDate;

public class ReadingRecordResponse {
    private Long id;
    private String title;
    private String author;
    private LocalDate date;
    private String content;

    public ReadingRecordResponse(Long id, String title, String author, LocalDate date, String content) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.date = date;
        this.content = content;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public LocalDate getDate() {
        return date;
    }

    public String getContent() {
        return content;
}

}

ReadingRecordController.java (기존 코드에서 수정)

package me.dodo.readingnotes.controller;

import me.dodo.readingnotes.domain.ReadingRecord;
import me.dodo.readingnotes.dto.ReadingRecordRequest;
import me.dodo.readingnotes.dto.ReadingRecordResponse;
import me.dodo.readingnotes.service.ReadingRecordService;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/records")
public class ReadingRecordController {

    private final ReadingRecordService service;

    public ReadingRecordController(ReadingRecordService service){
        this.service = service;
    }

    @PostMapping
    public ReadingRecordResponse saveRecord(@RequestBody ReadingRecordRequest request) {
        ReadingRecord record = new ReadingRecord(
                request.getTitle(),
                request.getAuthor(),
                request.getDate(),
                request.getContent()
        );

        ReadingRecord saved = service.saveRecord(record);
        return new ReadingRecordResponse(
                saved.getId(), // 엔티티.get~~ 여기서 엔티티가 saved라는 이름의 엔티티일뿐.
                saved.getTitle(),
                saved.getAuthor(),
                saved.getDate(),
                saved.getContent()
        );
    }

    @GetMapping
    public List<ReadingRecordResponse> getAllRecords() {
        return service.getAllRecords().stream()
                .map(r->new ReadingRecordResponse( r.getId(), r.getTitle(), r.getAuthor(), r.getDate(), r.getContent()))
                .collect(Collectors.toList());
    }

    @GetMapping("/{id}")
    public ReadingRecordResponse getRecordById(@PathVariable Long id) {
        ReadingRecord r = service.getRecord(id);
        return new ReadingRecordResponse(
                r.getId(), r.getTitle(),
                r.getAuthor(), r.getDate(), r.getContent()
        );
    }
}

controller를 위에서부터 보면

1. @Autowired 어노테이션 안 써도 돼?

스프링에서는 생성자가 1개뿐이라면,
그 생성자에 자동으로 @Autowired를 추가한 것처럼 동작함! → 안 써줘도 됨.

2. @RequestBody ReadingRecordRequest request 이게 뭐임

body에 담긴 JSON을 @RequestBody가 읽고 JSON을 "request"라는 이름의 "ReadingRecordRequest" 자바 객체(DTO)로 바꿈.
@RequestBody ReadingRecordRequest request는 사실 스프링이 new ReadingRecordRequest()를 먼저 실행하고,
그 다음에 요청 JSON의 값을 자동으로 매개변수 없이 객체에 채워 넣는 과정임

2 - 1) 매개변수 생성자를 쓰는 방식이 아니라고?!

요청 JSON 데이터를 자동으로 채우는 방식은 → new ReadingRecordRequest(...)처럼 매개변수를 넣어서 생성자 호출하는 게 아님.

방식 설명
❌ new ReadingRecordRequest("해리포터", "J.K. 롤링", LocalDate.of(...)) ❌ 매개변수 생성자는 Jackson이 사용 못함
✅ new ReadingRecordRequest() → setter() 호출 ✅ 기본 생성자 + setter로 필드 채움

//이게
@RequestBody ReadingRecordRequest request

//이것임 (매개변수 없이 객체 만듦)
ReadingRecordRequest request = new ReadingRecordRequest();
// 기본 생성자 함수 호출 후에 setter()를 각각 호출하는 것임.
// ex. setTitle(), setAuthor() ...

2 - 2) 근데 생성자 함수가 없다면?

ReadingRecordRequest request = new ReadingRecordRequest(); // 기본 생성자 호출
request.setTitle("해리포터");
request.setAuthor("J.K. 롤링");
request.setDate(LocalDate.of(2025, 4, 10));

사실 생성자 함수를 먼저 호출하는데 이게 없다?!

스프링은 Jackson이라는 JSON 변환기를 사용함.
JSON을 객체로 변화 시키는데 JPA처럼 리플렉션을 하려면 기본 생성자가 꼭 있어야함!!
안 그러면 에러남.

3. 그래서 왜 굳이 DTO 쓰는데!!

겉으로 보기엔 똑같아 보여도, 앞으로 확장되거나 유지보수할 때 엄청난 이점이 있음!

DTO 없이 바로 반환할 때

return saved; // Entity 자체를 그대로 반환

여러 문제가 생김.

1.  DB 관련 필드가 그대로 노출됨 → 데이터 노출
2. DB 구조가 바뀌면 API 응답도 바뀜 → 변경 어려움
3. Entity는 테스트나 가공이 불편 → 테스트 어려움
4. DB와 응답 형식이 너무 강하게 연결됨 → 캡슐화 깨짐.

DTO 사용해서 응답할 떄

return new ReadingRecordResponse(
    saved.getId(),
    saved.getTitle(),
    saved.getAuthor(),
    saved.getDate()
);

→ 장점이 많음.

1. 필요한 값(DB필드)만 선택 가능 → 보안 강화
2. 응답 커스터마이징 가능
3. DB 구조 바뀌어도 API 응답은 유지 가능 → 유지보수 유리

3 - 1)  DTO에서 숨기고 싶은 값 처리하고 싶다면?

방법 1: DTO에서 아예 필드를 제거

// 숨길 값이 DTO에 아예 없음
public class ReadingRecordResponse {
    private Long id;
    private String title;
    private String author;

    // date, content 등은 없음
}

→ 아무리 Entity에 값이 있어도, 응답 JSON에는 절대 안 나옴.

방법 2: @JsonIgnore 사용 (Jackson 기능)

public class ReadingRecordResponse {
    private Long id;
    private String title;
    private String author;

    @JsonIgnore
    private LocalDate date; // ← 응답에 나타나지 않음!
}

 

→ 필드는 가지고 있지만, 응답으로 보낼 때만 제외하고 싶을 때 유용함.
→ 필요할 땐 내부 로직에서는 date를 사용할 수 있고, 사용자에겐 안 보여줌.

방법 3: 커스텀 JSON 이름이나 포맷 지정

@JsonProperty("writtenDate")
@DateTimeFormat(pattern = "yyyy/MM/dd")
private LocalDate date;

→ date라는 필드를 JSON에서는 writtenDate로 보내고, 날짜 포맷도 "2025/04/10"처럼 바꿔줄 수 있음.

3 - 2) DB 구조가 바뀌어도 API 응답은 유지 가능하다니?

DTO 없이 엔티티 직접 반환할 때

@GetMapping("/records/{id}")
public ReadingRecord getRecord(@PathVariable Long id) {
    return repository.findById(id).orElse(null); // 엔티티 그대로 응답
}

만약 DB 구조가 바뀐다면?

@Entity
public class ReadingRecord {
    private String title;
    private String author;
    private String content; // ← 이게 새로 생겼다고 해보자
}

→ 그럼 응답 JSON도 바뀜.

{
  "title": "해리포터",
  "author": "J.K. 롤링",
  "content": "중요한 메모" ← 예상치 못한 필드!
}

기존 응답 형식을 쓰던 다른 앱, 서비스가 깨질 수도 있음!!

DTO를 쓰면?

@GetMapping("/records/{id}")
public ReadingRecordResponse getRecord(@PathVariable Long id) {
    ReadingRecord entity = repository.findById(id).orElse(null);
    return new ReadingRecordResponse(entity.getId(), entity.getTitle(), entity.getAuthor());
}

→ 응답은 오직 ReadingRecordResponse가 정한 값만 보여줌.
→ 엔티티에 뭘 추가하든, DTO를 수정하지 않으면 응답 형식은 절대 바뀌지 않음!!

→ DTO를 사용하면 DB가 바뀌어도 API 응답 유지 가능!


테스트

post/get 모두 잘 작동함!

반응형

'Spring Boot project' 카테고리의 다른 글

제목, 작가, 날짜로 조회  (0) 2025.04.17
H2 DB 설정  (1) 2025.04.14
ReadingRecordController 생성, get/post Test  (1) 2025.04.12
ReadingRecordService 생성  (0) 2025.04.11
ReadingRecordRepository 생성  (0) 2025.04.10
Comments