본 시리즈는 메타 코딩님의 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);
}
}