본 시리즈는 메타 코딩님의 Junit 강의를 학습한 내용을 바탕으로 정리하였습니다.
저번 시간에 수정하던 BookService
를 다시 한번 살펴보자.
BookService
// 2. 책 목록보기
public List<BookRespDto> 책목록보기() {
// 코드 수정
List<BookRespDto> dtos = bookRepository.findAll().stream()
.map((bookPS) -> new BookRespDto().toDto(bookPS))
.collect(Collectors.toList());
// (..생략)
return dtos;
}
위 코드를 수정하기 전의 코드는 다음과 같다.
BookService
// 2. 책 목록보기
public List<BookRespDto> 책목록보기() {
// 코드 수정
List<BookRespDto> dtos = bookRepository.findAll().stream()
// .map((bookPS) -> new BookRespDto().toDto(bookPS))
.map(new BookRespDto()::toDto())
.collect(Collectors.toList());
// (..생략)
}
이렇게하면 원래는 map에서 toDto
를 참조함으로써 Book
이 들어올 수 있게 된다. 그런데 .map(new BookRespDto()::toDto())
이 부분이 작동하지 않으면서 문제가 발생했던 것이다.
이쯤에서 map의 작동방식을 살펴보면 왜 이런 문제가 생겼는지 알 수 있을 것 같다.
보는 것과 같이 map
메서드는 List
로부터 타입 변환되어 Stream
되고 있는 데이터를 한 건씩 가져와서 처리를 하는 방식이다.
.map(new BookRespDto()::toDto())
이렇게 map에 참조문법 ( ::
) 이 들어가면 참조되는 메서드( toDto
) 가 map으로 넘어가게 된다.
위 그림과 같이 map
메서드가 실행이 되면 메서드 내부적으로 반복문이 돌면서 map
으로 넘어온 toDto
. 즉, 참조한 toDto
메서드를 실행하게 되고
toDto
메서드는 bookRepository.findAll().stream()
을 실행한 결과로 Stream
되어지는 Book
을 받아와서 실행한 후, 가공된 dto
를 다음 그림과 같이 리턴하게 되는 것이다.
Book
type그렇다면 Stream
에 아직 남아있는 두 번째 타입이 들어오게 되면 어떻게 될까?
위의 그림처럼 같은 heap을 공유하게 된다. 더군다나 BookRespDto
에는 다른 생성자가 있는 것도 아니기 때문에 결국 완전히 똑같은 두 개의 dto
만이 생성되는 것이다.
결국 정리하면 new
를 통해 새로운 객체가 생성되는 것도 아니고 한 번 new
로 생성한 객체에 toDto()
메서드만 두 번 실행되기 때문에 동일한 dto
만 두 번 리턴되는 것이다. 그도 그럴 것이 map
은 매개변수로 객체를 한 번만 전달 받는다. 같은 객체에서 같은 메소드가 동작하니 당연히 똑같은 값이 나올 수 밖에 없다.
그렇다면 어떻게 수정하면 좋을까?
Dto
부터 손봐야할 것 같다.
기존의 this
를 리턴하던 코드를 다음과 같이 생성자를 추가하여 @Builder
패턴으로 바꾸자.
BookRespDto.java
package site.metacoding.junitproject.web.dto;
import lombok.Builder;
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;
@Builder
public BookRespDto(Long id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
}
}
Book
도 다음과 같이 dto
의 builder
를 받는 방식으로 수정해야한다.
Book.java
package site.metacoding.junitproject.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import site.metacoding.junitproject.web.dto.BookRespDto;
@NoArgsConstructor
@Getter
@Entity
public class Book {
// ..(생략)
public BookRespDto toDto() {
return BookRespDto.builder()
.id(id)
.title(title)
.author(author)
.build();
}
}
지금까지 작업한 리팩토링을 토대로 우리는 책 목록보기 메서드를 다음과 같이 만들수 있다.
// 2. 책 목록보기
public List<BookRespDto> 책목록보기() {
List<BookRespDto> dtos = bookRepository.findAll().stream()
.map(Book::toDto)
.collect(Collectors.toList());
return dtos;
}
기존의 new
를 통해 BookRespDto()
를 생성하고 toDto
를 참조할 바에 어짜피 findAll()
을 하게되면 stream에 Book
이 담기게 될테고 map 입장에서는 Book
을 한 건씩 꺼내오면 되는 것이기 때문에 아예 Book
타입을 넣는 것이 심플하다.
물론 저번 포스팅에서 수정했던 것처럼
.map((bookPS) -> new BookRespDto().toDto(bookPS))
이렇게 처리해도 상관은 없다. 하지만 꺼내오는 것을 타입으로 받아서 메서드로 처리하는 것이 훨씬 깔끔하게 처리할 수 있다는 것이다.
테스트도 잘 동작하는 모습.