도슐랭스타
DTO 클래스 생성 Request, Response 본문
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 |