DTO(Data Transfer Object)는 계층 간 데이터 교환을 하기 위해 사용하는 객체로, 로직을 가지지 않는 순수한 데이터 객체(Java Beans)이다. getter/setter 메서드만을 가지며, DB에서 데이터를 얻어서 Service나 Controller 등으로 보낼 때 사용한다. (=> 엔티티를 DTO 형태로 변환한 후 사용한다.)
❓DAO(Data Access Object)란❓
DB의 데이터에 접근하기 위한 객체로, 실제로 DB에 접근해서 데이터의 삽입, 삭제, 조회, 수정 등 CRUD 기능을 수행한다. Service와 DB를 연결하는 고리 역할을 하며,Repository
가 바로 DAO이다.
DTO를 사용해서 Review
엔티티를 생성하는 과정을 살펴보면 다음과 같다.
ReviewDTO
라는 DTO 객체를Controller
에 던져준다.Controller
는 이 DTO를Service
로 넘겨주고,Service
에서JPA
를 사용하여Review
객체를 저장한다.
이 과정을 코드로 보면 (필요한 부분만 뗌)
/* Review => Service에서 찐으로 객체 만들때 사용됨 */
public class Review {
private Long id; // 자동생성
private String body;
private Boolean isCompleted;
private Tag tag;
}
/* ReviewDTO => Controller와 Service의 파라미터로 사용됨 */
public class ReviewDTO {
private Long tutoringId;
private String body;
private Long tagId;
}
/* Controller => Service로 DTO를 넘겨줌 */
public void createReview (@RequestBody ReviewDTO createReview){
reviewService.createReview(createReview);
}
/* Service => DTO를 받아서 찐 엔티티를 만듦 */
public void createReview(ReviewDTO reviewDTO) {
Optional<Tag> tag = tagRepository.findById(reviewDTO.getTagId());
Review review = Review.builder()
.body(reviewDTO.getBody())
.isCompleted(false)
.tag(tag.get())
.build();
reviewRepository.save(review);
}
Review 엔티티와 DTO가 비슷한듯 다르게 생겼다. 미리 작성된 API 문서에 따르면 새로운 Review 객체를 만들기 위해서는 tutoringId
, body
, tagId
가 필요했는데, JPA로 Review 엔티티를 만들기 위해 필요한 필드(body
, isCompleted
, tag
)와는 완전히 동일하지 않았다.
이럴때 사용할 수 있는게 바로 DTO
이다.
Controller에 파라미터로 Review
를 썼다면 Review
에 존재하지 않는 필드인 tutoringId
을 받지 못할 것은 물론이고 1:N으로 매핑되어있는 Tag
도 아이디값으로만 왔다갔다할게 아니라 Tag
자체를 다뤘어야 했을 것이다. (어휴 귀찮아라!)
그런데 ReviewDTO
를 써서 실제로 받아야하는 필드만 따로 빼두니 간단하게 데이터가 전달될 수 있었던 것이다. 즉 DTO는, 쉽게 말해 내가 원하는 포맷으로 데이터를 받겠다! 라고도 할 수 있겠다.
반환값에 HTTP 상태코드를 같이 보낼 때 사용한다. HttpStatus
, HttpHeaders
, HttpBody
이 세가지 값이 필요하지만 다 넣을 필요없이 필요한 값만 넣어도 된다.
ResponseEntity<객체>
는 HTTP 상태코드와 함께 이 객체에 해당하는 데이터가 같이 넘어간다는 것을 의미한다.
💡
ResponseEntity<?>
이라고 쓰면 타입을 명시하지 않고 되는대로 다 받을 수 있다.
이번 프로젝트에는 기존에 ResponseCode
라는, 반환값으로 사용할 객체가 이미 만들어져있었기 때문에 얘를 같이 넘겨줄것이다.
// Entity
public class ResponseCode {
private String code;
}
// Controller
public ResponseEntity<ResponseCode> successCode (){
ResponseCode responseCode = ResponseCode.builder().code("SUCCESS").build();
return new ResponseEntity<>(responseCode, HttpStatus.OK);
}
public ResponseEntity<ResponseCode> failCode (){
ResponseCode responseCode = ResponseCode.builder().code("FAIL").build();
return new ResponseEntity<>(responseCode, HttpStatus.BAD_REQUEST);
}
이런식으로 작성하면 ResponseCode와 함께 HTTP 상태코드가 같이 반환된다.
이미 구현되어 있는 팩토리 메소드를 사용해서 아래처럼도 보낼 수 있다.
return ResponseEntity.ok().build(); // 상태코드만 반환
return ResponseEntity.ok(response); // body까지 반환
➕ 2023.06.15 추가
public ResponseEntity<?> example() {
if (...) {
...
return ResponseEntity.ok(objectA);
} else {
...
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(objectB);
}
}
위와 같이 함수의 반환값은 <?>
으로 명시하지 않고 받고, 경우에 따라 body에 담기는 객체의 클래스가 다른 경우, 해당 객체의 값을 제대로 받아 getter
, setter
등의 메소드를 사용하려면 다음과 같이 값을 받아서 앞에 해당 객체클래스를 명시해서 타입캐스팅을 해줘야한다.
ResponseEntity exampleResponse = example();
ObjectA objectA = (ObjectA) exampleResponse.getBody();
// => objectA는 A 클래스의 getter/setter 사용 가능
ObjectB objectB = (ObjectB) exampleResponse.getBody();
// => objectB는 B 클래스의 getter/setter 사용 가능
그냥 쓸 때
타입캐스팅을 하고 쓸 때
[스프링/Spring] DTO는 왜 써야 하나?
[Spring] DAO와 DTO의 차이 그리고 VO
ResponseEntity란 - 개념, 구조, 사용법, 사용하는 이유