본 시리즈는 메타 코딩님의 Junit 강의를 학습한 내용을 바탕으로 정리하였습니다.
지난 포스팅을 끝으로 BookRepository의 Test 코드 개발을 모두 끝냈다. 이제 실제로 Service layer를 통해 비즈니스 로직에 관해 설계해보자.
💡 서비스 레이어에 대해 읽어보면 좋은 글
https://wckhg89.tistory.com/13
우리가 프로젝트의 뼈대를 구축할 때 만들었던 service
패키지와 BookService
자바 파일을 건드려보자.
BookService.java
package site.metacoding.junitproject.service;
@Service
public class BookService {
// 1. 책 등록
// 2. 책 목록보기
// 3. 책 한건보기
// 4. 책 삭제
// 5. 책 수정
}
BookService
도 BookRepositoryTest
와 마찬가지로 5개의 기능을 가지고 개발할 계획이다. 이 중에서 책 등록에 관한 부분을 먼저 시작할 것이다.
일단 책을 등록하기 위해서는 Dto가 있어야한다. Book Entity의 필드 값을 DTO 즉, 데이터를 오브젝트로 변환해서 책 등록하기
메서드에 전달할 것이기 때문이다.
이 역시 프로젝트를 구축할 때, 파일을 만들어두었다. (Web
> Dto
하위에 BookSaveReqDto
) 이제 이것을 구현하자.
BookSaveReqDto
package site.metacoding.junitproject.web.dto;
import lombok.Getter;
import lombok.Setter;
import site.metacoding.junitproject.domain.Book;
@Getter
@Setter // Controller에서 Setter가 호출되면서 Dto에 값이 채워짐.
public class BookSaveReqDto {
private String title;
private String author;
public Book toEntity() {
return Book.builder()
.title(title)
.author(author)
.build();
}
}
이제 DTO도 완성되었으니 Service layer로 넘어가자.
BookService.java
package site.metacoding.junitproject.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import site.metacoding.junitproject.domain.Book;
import site.metacoding.junitproject.domain.BookRepository;
import site.metacoding.junitproject.web.dto.BookRespDto;
import site.metacoding.junitproject.web.dto.BookSaveReqDto;
@RequiredArgsConstructor // 1.
@Service
public class BookService {
private final BookRepository bookRepository;
public Book 책등록하기(BookSaveReqDto dto) { // 2.
Book bookPS = bookRepository.save(dto.toEntity());
return bookPS;
}
}
책 등록을 위해선 bookRepository
가 필요하다. 그러나 이를 final
로 선언하여 생성자를 생성하게 되면 오류가 발생하는데 이는 우리가 bookRepository
을 아직 구현하지 않은 상태이기 때문이다.
이 때 final
이 붙은 필드의 생성자를 자동으로 생성해주는 롬복 어노테이션이 바로 @RequiredArgsConstructor
이다.
BookSaveReqDto
dto를 책 등록하기 메서드에 주입시킨다.
이렇게해서 책 등록 구현이 끝나면 좋겠지만 아쉽게도 고려해야할 부분들이 있다. 위의 책 등록하기에서의 bookPS
는 Persistence Context에 저장된 영속화된 객체를 의미한다. 즉, bookRepository.save
를 통해 DB에 저장되고 난 후의 Book
이 되는 것이다.
이 영속화된 Entity인 bookPS
를 클라이언트가 요청을 하게 될 때, DB에서 그대로 가져오게 되면 어떻게 될까?
우선 이 해답을 얻기 위해서 스프링이 어떻게 요청과 응답을 처리하는지 흐름을 알 필요가 있다.
다음을 살펴보자.
스프링은 보통 위와 같은 플로우를 가지고 동작한다.
스프링의 동작 흐름을 이해하기 쉽게 넘버링을 했다.
클라이언트
의 요청에 따라 book
Entity가 생성된다. MIME 타입은 보통 JSON일 것이다.
Dispatcher Servlet
에서는 이를 토대로 주소를 분석한다.
(ex. url /book에 POST 방식으로 저장)
Controller
에서는 validation(유효성)을 체크하거나 값을 파싱하는 등의 작업을 수행하고, book
Entity를 bookSaveReqDto
와 같은 DTO로 변환한다.
Service
layer에서는 DTO를 Repository에 저장할 수 있게끔 다시 Entity로 변환한다.
변환된 book
Entity를 저장한다.
Persistence Context
에 의해 book
Entity가 있는지 없는지 체크된다.
book
Entity를 DB에 저장한다.
ex. {ID:1, title:Junit, author:메타코딩}
1. DB
에 저장된 book
Entity는 영구적으로 저장 (영속화) 된다.
2.Persistence Context
에 의해 bookPS
로 변환된다.
3~4. Repository
는 Service
로 bookPS
를 넘겨준다.
마찬가지로 Service
에서 Controller
로 영속화된 객체를 넘겨준다.
5.Service
단에서 트랜잭션이 시작되고 비즈니스와 관련된 모든 로직이 끝나면 트랜잭션이 종료된다. 여기서부터는 DB
에 쓰기(write)가 불가능해진다. 또한, DB
Session이 닫히기 때문에 역시 쓰기는 불가능하고 select만 가능해진다. 마지막으로 Controller
에선 Dispatcher Servlet
으로 bookPS
를 넘겨준다.
6.Dispatcher Servlet
에서 클라이언트
로 응답 데이터를 전달한다.
7.클라이언트
가 데이터를 받아보게 된다.
여기까지가 요청과 응답에 대한 흐름이다. 그러나 우리는 마지막 8번에 주목해보자.
만약 client가
bookPS
와 관련된 사항을 요청했다고 가정하자. 예를 들어,bookPS.getUser()
와 같은 내용이다. get 요청이기 때문에 DB에서부터bookPS
의 내용을 select하는 작업이 진행될 것이다.
bookPS
는 하나의 Entity이다. Service
는 bookPS
엔티티를 받아 Controller
에 전달할 것이다.
이렇게 되면 client 입장에서는 book
의 User 정보에 대한 것만을 요청했는데 영속화된 객체인 bookPS
의 getter를 통해 필요없는 다른 속성들까지 모두 넘어오게 된다. 따라서 필요이상으로 메모리를 잡아먹고 속도가 느려지게 된다.
이 외에도 Entity를 넘기기 되면 @NotEmpty
와 같은 검증 로직이 필요, 같은 엔티티에 대해 여러 로직이 필요 등 여러 문제 상황이 발생하게 된다.
그렇다면 어떻게 해야할까?
💡
bookPS
엔티티를Controller
로 바로 넘기는 것이 아니라Dto
로 변환시켜서 넘겨야한다.
따라서 BookSaveDto
처럼 BookRespDto
도 생성해서 Controller
는 오직 Dto
만 다룰 수 있게끔 해주어야 한다.
요청과 응답으로 Entity
대신 Dto
를 사용했을 때의 장점들을 알고 싶다면 이 블로그를 참고하면 더 자세히 나와있다.
(https://tecoble.techcourse.co.kr/post/2020-08-31-dto-vs-entity/)
BookRespDto.java
package site.metacoding.junitproject.web.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import site.metacoding.junitproject.domain.Book;
@NoArgsConstructor
@Getter
public class BookRespDto {
private Long id;
private String title;
private String author;
public BookRespDto toDto(Book bookPS) { // Dto로 변환하는 메서드
this.id = bookPS.getId();
this.title = bookPS.getTitle();
this.author = bookPS.getAuthor();
return this;
}
}
위의 Dto
들을 토대로 책 등록 코드를 완성시켜보자.
BookService.java
package site.metacoding.junitproject.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import javax.transaction.Transactional;
import site.metacoding.junitproject.domain.Book;
import site.metacoding.junitproject.domain.BookRepository;
import site.metacoding.junitproject.web.dto.BookRespDto;
import site.metacoding.junitproject.web.dto.BookSaveReqDto;
@RequiredArgsConstructor
@Service
public class BookService {
private final BookRepository bookRepository;
@Transactional(rollbackOn = RuntimeException.class) // 1.
public Book 책등록하기(BookSaveReqDto dto) {
Book bookPS = bookRepository.save(dto.toEntity());
return new BookRespDto().toDto(bookPS);
}
}