You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
해당 요구사항의 절차지향적으로 구현해보고, Collection Wrapper 클래스를 두어 객체지향적인 코드로 리팩토링 해본다.
Spring Data JPA 와 연동하는 방법도 알아본다.
2. 모델링
@Getter
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer price;
}
public interface ProductJpaRepository extends JpaRepository<Product, Long> {
}
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductJpaRepository productJpaRepository;
public Integer getTotalPrice(){
// 여기에 우리의 핵심 비지니스 로직인 상품 가격의 합계 계산 로직을 구현한다.
}
}
2.1 절차지향적인 구현
public Integer getTotalPrice(){
final List<Product> products = productJpaRepository.findAll();
Integer totalPrice = 0;
for (Product product : Products){
totalPrice += product.getPrice();
}
return price;
}
DB로부터 상품을 조회해온 뒤, 컬렉션 원소를 돌면서 totalPrice 라는 로컬 변수에 상품 가격의 합계금액을 누적한다.
전형적인 절차지향적인 코드의 예다.
2.2 Java Stream API를 이용한 함수형 프로그래밍 구현
public Integer getTotalPrice(){
final List<Product> products = productJpaRepository.findAll();
return products.stream() // (1)
.map(Product::getPrice) // (2)
.reduce(0, Intger::sum); // (3)
}
함수형 프로그래밍 스타일로 구현하였지만, 여전히 코드에서 합계금액을 계산하기 위한 절차를 기술하고 있다.
(1) List products 를 Stream 으로 반환한다. (레코드를 건건히 처리할 것이다)
(2) Product 타입을 getPrice() 로 Integer 타입으로 변환한다.
(3) Integer로 변환된 값을 0부터 누적시켜 합을 구한다.
2.3 객체지향적인 구현
public Integer getTotalPrice(){
final Products products = productJpaRepository.findAll();
return products.getTotalPrice();
}
Products 라는 List 의 Wrapper 클래스를 만들었다.
서비스 계층에서는 Products 라는 Collection Wrapper 클래스에게 상품의 합계금액을 계산하라는 메시지를 보낸다.
가장 객체지향적인 접근이다.
3. Collection Wrapper 클래스 구현
@RequiredArgsConstructor
public class Products implements Iterable<Product> {
private final Stream<Product> stream;
@Override public Iterator<Product> iterator() {
return stream.iterator();
}
public Integer getTotalPrice() {
return stream
.map(Product::getPrice)
.reduce(0, Integer::sum);
}
}
Iterable<Product> 인터페이스를 구현한 Products 클래스를 정의한다.
Iterable 은 컬렉션의 구현 방법을 노출시키지 않고, 컬렉션 요소들을 접근할 수 있게 하는 패턴,인터페이스다.
Iterable 인터페이스를 구현하면 for-each 문 사용이 가능해지고, Iterable 를 통해 Products 래퍼클래스의 요소들을 순회할 수 있게 하였다.
4. Spring Data JPA 와의 연동
Spring Data JPA 에서 위와 같은 패턴을 구현하려면 Spring Data JPA에 메서드 이름의 키워드를 보고 자동으로 쿼리 메서드를 만들어주는 Query Method 기능과 연동되어야 한다.
다행히 Spring Data JPA 에는 Query Method 에서는 여러 반환값을 지원하고 있으며, Streamable 인터페이스를 구현한 클래스를 반환값으로 받을 수 있다.
여기서는 ProductsSCO(Spring Collection Object) 라고 네이밍하였다.
Streamable 인터페이스가 Spring Data 모듈의 의존성을 가지기 때문에 때문에 도메인 Wrapper 클래스와 분리하기 목적으로 분리하였다.
import org.springframework.data.util.Streamable;
class ProductsSCO implements Streamable<Product> {
private final Streamable<Product> streamable;
@Override
public Iterator<Product> iterator() {
return streamable.iterator();
}
public Products toProducts(){
return new Products(streamable.stream());
}
}
interface ProductJpaRepository implements JpaRepository<Product, Long> {
ProductsSCO findByPriceGreaterThan(Integer price);
}
ProductJpaRepository 에 반환값이 Streamable 인터페이스를 구현한 ProductsSCO 래퍼 클래스 인 것을 볼 수 있다.
다만, 한계가 있는데 List<T> findAll() 과 같은 메서드들은 이미 JpaRepository에서 정의되기 때문에 Products findAll() 과 같은 형태로 재정의 할 수 없다.
5. Collection Wrapper 클래스를 적용한 전체 코드
먼저 도메인 모듈의 Spring에 대한 의존성을 분리하기 위해, domain 패키지와 springboot 패키지를 두었다.
domain 패키지
@Getter
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer price;
}
public interface ProductRepository {
Products findByPriceGreaterThan(Integer price);
}
@RequiredArgsConstructor
public class Products implements Iterable<Product> {
private final Stream<Product> stream;
@Override public Iterator<Product> iterator() {
return stream.iterator();
}
public Integer getTotalPrice() {
return stream
.map(Product::getPrice)
.reduce(0, Integer::sum);
}
}
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public Integer getTotalPrice(){
final var products = productRepository.findByPriceGreaterThan(0);
return products.getTotalPrice();
}
}
@RequiredArgsConstructor
@Repository
public class ProductRepositoryImpl implements ProductRepository {
private final ProductJpaRepository productJpaRepository;
@Override public Products findByPriceGreaterThan(final Integer price) {
final var productsSCO = productJpaRepository.findByPriceGreaterThan(price);
return productsSCO.toProducts();
}
}
@RequiredArgsConstructor
public class ProductsSCO implements Streamable<Product> {
private final Streamable<Product> streamable;
@Override public Iterator<Product> iterator() {
return streamable.iterator();
}
public Products toProducts(){
return new Products(streamable.stream());
}
}