좌충우돌 프로젝트 이야기

함수형 프로그래밍을 재고 처리 로직에 적용해보기

호춘쿠키 2023. 5. 2. 03:32

함수형 프로그래밍을 떠올린 계기

함수형 프로그래밍을 적용해야겠다는 생각은 하나의 고민으로부터 시작합니다.

[
    {날짜: 2023-05-02, 재고: 10개},
    {날짜: 2023-05-03, 재고: 9개},
    {날짜: 2023-05-04, 재고: 15개,
    {날짜: 2023-05-05, 재고: 0개},
    {날짜: 2023-05-06, 재고: 2개}}
]

위와 같이 객실의 재고를 관리하는 데이터가 존재합니다. 어떤 사용자는 2023-05-02 ~ 2023-05-06 기간 동안 4박 5일로 객실을 예약하려고 합니다. 하지만 2023-05-05 날짜에 해당하는 재고는 0개이므로 이와 같은 예약요청은 실패해야 하는 상황입니다. 예약에 실패하는 경우 모든 객실 데이터의 재고는 반드시 보존되어야 합니다. 이런 요구사항을 어떻게 지킬 수 있을까요?

 

어렴풋이 배운 객체지향프로그래밍과 함수형 프로그래밍의 차이점을 활용해보기로 했습니다. 객체지향프로그래밍은 상태를 변경합니다. 반면에 함수형 프로그래밍은 인풋에 의존하지도, 인풋을 변경하지도 않습니다. 이점을 활용하면 재고 처리 실패시 데이터 복원을 걱정할 필요가 없을 것 같다는 생각이 들었습니다.

 

함수형 프로그래밍이란

  • 순수 함수는 동일한 입력값에 대해서 항상 같은 값을 반환합니다.
  • 전역 변수를 사용하거나 변경해서 예상하지 못한 부수 효과가 발생하지 않습니다.
  • Java에서는 순수 함수를 구현할 때 for, while문과 같은 반복문을 사용하지 않습니다. 반복문은 안에는 값의 처리와 변경과 관련된 코드가 섞여 있기 때문입니다. 반복문 대신 map, filter 같은 함수형 인터페이스를 매개변수로 받는 메서드를 이용합니다.

재고 처리 로직에 함수형 프로그래밍 적용하기

Stock: 재고 클래스

public abstract class Stock {  
    LocalDate date;  
    int quantity;  

    public Stock(LocalDate date, int quantity) {  
        this.date = date;  
        this.quantity = quantity;  
    }  

    public void validateQuantity() {  
        if (quantity <= 0) {  
            throw new IllegalStateException();  
        }  
    }  

    public boolean isBetween(LocalDate checkin, LocalDate checkout) {  
        return date.isEqual(checkin) || (date.isAfter(checkin) && date.isBefore(checkout));  
    }  

    @Override  
    public String toString() {  
        return "[date = " + date + ", stock:" + quantity + "]";  
    }  
}

StockOOP: OOP 패러다임의 재고 클래스

public class StockOOP extends Stock {
    public StockOOP(LocalDate date, int quantity) {
		super(date, quantity);
	}
	
    // 객체지향 프로그래밍 패러다임을 적용한 재고 클래스
    // this.quantity-- 구문을 포함 => 객체의 상태를 변경
	public void reduceQuantity(LocalDate checkin, LocalDate checkout) {  
    	if (isBetween(checkin, checkout)) {  
        	validateQuantity();  
        	this.quantity--;  
    	}
    }
}

StockFP: FP 패러다임의 재고 클래스

public class StockFP extends Stock {  
    public StockFP(LocalDate date, int quantity) {  
        super(date, quantity);  
    }  
	
    // 함수형 프로그래밍 패러다임을 적용한 재고 처리 로직
    // 객체의 상태를 변경하지 않고 값을 return 함
    public int reduceQuantity(LocalDate checkin, LocalDate checkout) {  
        if (isBetween(checkin, checkout)) {  
            validateQuantity();  
            return quantity - 1;  
        }  
        return quantity;  
    }  
}

재고 처리 로직 테스트하기

StockFP 클래스, StockOOP 클래스를 테스트하는 과정을 통해 각자 어떤 차이점이 존재하는지 소개하겠습니다.

먼저 아래와 같은 예시를 참고하여 테스트 목적을 설명하겠습니다. 어떤 객실의 재고 데이터는 (날짜, 재고) 쌍으로 이루어져있습니다.

사용자는 2023-05-02 ~ 2023-05-04 기간 동안 2박 3일로 객실을 예약하려고 합니다. 하지만 2023-05-03 날짜에 해당하는 재고는 0개이므로 예약에 실패해야 하고, 모든 객실 데이터의 재고는 반드시 보존되어야 합니다.

 

Index 날짜 재고
0 2023-05-02 10개
1 2023-05-03 0개

 

테스트1. reserveOOP()

OOP 패러다임을 이용한 재고처리 메서드

public class StockTest {

    @Test
    void stockOOPTest() {
        // given
        List<StockOOP> stocksOOP = List.of(
            new StockOOP(LocalDate.of(2023, 5, 2), 10),  
            new StockOOP(LocalDate.of(2023, 5, 3), 0));

        LocalDate checkin = LocalDate.of(2023, 5, 2);  
        LocalDate checkout = LocalDate.of(2023, 5, 4);

        // test: oop 이용한 재고감소
        try {  
            reserveOOP(stocksOOP, checkin, checkout);  
        } catch (RuntimeException e) {  
            System.out.println("수량부족");  
        } finally {  
            System.out.println(stocksOOP);  
        }
    }
}
// stockOOP: [
// {date=2023-05-02, stock: 10}
// {date=2023-05-03, stock: 0}]
public void reserveOOP(List<StockOOP> stocks, LocalDate checkin, LocalDate checkout) {  
    for (StockOOP stock : stocks) {  
        stock.reduceQuantity(checkin, checkout);  
    }  
}

1. stockOOP(0) 데이터의 stcok 1 감소

2. stockOOP(1) 데이터의 stcok이 0이므로 예외가 발생

 

stockOOP(1) 재고를 감소시키는 과정에서 예외가 발생했습니다. 하지만 그 이전에 StockOOP(0) 재고를 처리하는 과정에서 데이터의 변경이 일어나며 돌이킬 수 없습니다.

 

 

테스트2. reserveFP()

FP 패러다임을 이용한 재고처리 메서드

public class StockTest { 

    @Test
    void stockFPTest() {
        // given
        List<StockFP> stocksFP = List.of(
            new StockFP(LocalDate.of(2023, 5, 2), 10),  
            new StockFP(LocalDate.of(2023, 5, 3), 0));

        LocalDate checkin = LocalDate.of(2023, 5, 2);  
        LocalDate checkout = LocalDate.of(2023, 5, 4);

        // test: fp 이용한 재고감소
        try {  
            stocksFP = reserveFP(stocksFP, checkin, checkout);  
        } catch (RuntimeException e) {  
            System.out.println("수량부족");  
        } finally {  
            System.out.println(stocksFP);  
        }
    }
}
// stockOOP: [
// {date=2023-05-02, stock: 10}
// {date=2023-05-03, stock: 0}]
public static List<StockFP> reserveFP(List<StockFP> stocks, LocalDate checkin, LocalDate checkout) {
	return stocks.stream()  
    		.map(stockFP -> new StockFP(stockFP.date, stockFP.reduceQuantity(checkin, checkout)))  
        	.collect(Collectors.toList());
}

1. stockOOP(0) 데이터의 stock - 1 값을 반환한 값을 이용해 새로운 StockFP 객체를 생성한다.

2. stockOOP(1) 데이터의 stcok - 1 값을 반환하려 했으나, stock 값이 0이므로 예외가 발생한다.

 

함수형 프로그래밍 방식을 적용한 reserveFP() 메서드는 데이터를 수정하지 않고, stock - 1 값을 반환합니다. stockOOP(1) 데이터를 처리하는 과정에서 예외를 뱉어내며 메서드가 종료됩니다. 결과적으로 try문 내의 reserveFP() 메서드가 실패하고 기존 데이터가 유지됩니다. 

 

실행 결과

  • OOP 패러다임의 동작방식: 예약 실패에도 불구하고, 2023-05-02 날짜의 상품 재고가 감소한다.
  • FP 패러다임의 동작방식: 재고가 유지된다.