OrderApp의 start() 메서드에서 주석 처리한 부분 중, 메뉴 출력을 주석 해제하고 삭제한 다음, 지금까지 작성한 printMenu()를 호출해보자.
주석처리 되어 있던 사용자 입력받기의 주석을 해제하고, 아래처럼 코드를 입력하자
장바구니 기능은 Cart 클래스 내에 작성한다. 장바구니와 관련하여 필요한 기능은 다음과 같다.
장바구니 담기 → addToCart()
옵션 고르게 하기 → chooseOption()
햄버거 세트 구성하기 → composeSet()
장바구니 출력하기 → printCart()
이 중, 옵션 고르게 하기와 햄버거 세트 구성하기는 장바구니를 담는 과정에 종속됩니다. 사용자가 상품을 선택했다는 것은 해당 상품을 장바구니에 담겠다는 것이고, 장바구니를 담을 때에 상품의 옵션이 지정되어야 하기 때문입니다.
장바구니 담기 → addToCart()
옵션 고르게 하기 → chooseOption()
햄버거 세트 구성하기 → composeSet()
장바구니 출력하기 → printCart()
즉, addToCart() 내에서 chooseOption()과 composeSet()을 호출해 주면 되겠군요.
burgerqueen 패키지에 Cart 클래스를 생성
Cart 객체는 장바구니 역할을 하는 객체로, 장바구니에 담긴 상품들을 저장하고 있어야 합니다. 따라서, 장바구니 상품을 의미하는 items 필드를 선언하고 아래와 같이 초기화 해줍시다. 그리고, 여러 메서드에서 사용자로부터 입력받을 예정이니 필드로 Scanner를 정의하자
items 배열의 길이를 0으로 설정한 이유는 장바구니에 상품을 추가할 때마다 길이를 1씩 늘려줄 것이기 때문이다.
printCart()는 장바구니의 상품들을 단순히 출력해 주는 기능만 하므로 반환값이 필요하지 않으며, Cart 객체의 인스턴스 변수 items를 매개변수로 받지 않아도 되므로 입력값 또한 필요하지 않다.
또한, printCart()는 OrderApp의 start() 메서드에서 호출될 수 있어야 하므로 접근 범위는 public이어야 한다.
큰틀을 작성해보니 장바구니 상품들을 옵션 정보와 함께 출력하는 부분, 금액 합계 부분을 채워야 한다.
장바구니 상품들을 옵션 정보와 함께 출력 → printCartItemDetails()
금액 합계 계산 → calculateTotalPrice()
printCartItemDetails()의 역할은 상품명, 가격, 옵션을 출력해주어야 한다.
그러므로
items 배열을 순회하면서,
items[n]이
BurgerSet 타입의 인스턴스라면 세트 정보를 출력,
Hamburger 타입의 인스턴스라면 (단품)을 출력,
Side 타입의 인스턴스라면 케첩 개수를 출력,
Drink 타입의 인스턴스라면 빨대 유무를 출력
이러한 동작을 수행하여야 한다.
printCartItemDetails()의 메서드 시그니처는 다음과 같다.
입력값 : Cart 클래스의 필드인 items를 출력할 것이므로, 입력값이 필요하지 않다.
반환값 : 단순 출력 메서드이므로, 반환값이 필요하지 않다.
접근 범위 : Cart 내부(printCart() 메서드)에서만 호출할 것이므로, private으로 지정하자.
(BurgerSet), (Side), (Drink)와 같은 것들이 product 앞에 붙어있는 것을 확인할 수 있다. 이는 참조변수를 다운캐스팅하기 위함인데, 다운캐스팅하는 이유는 다음과 같다.
items는 Product[] 타입의 배열이지만, items[n]은 본래 Hamburger, Side, Drink, BurgerSet 타입의 인스턴스이다.
즉, items[n]은 new Product()가 아니라, new Hamburger(), new Side(), new Drink(), new BurgerSet() 등을 통해 인스턴스화된 인스턴스이다.
-->업캐스팅이 되어 있는 상태라는 의미이다.
즉, Product 타입의 product를 통해서는 hasStraw(), getKetchup() 등의 하위클래스의 메서드를 호출할 수 없다.(다운 캐스팅이 필요하다.)
calculateTotalPrice()는 배열을 순회하면서 금액 합계를 구하는 메서드이다.
addToCart()는 사용자가 상품을 선택하고 나서 실행되는 메서드로, 옵션을 선택하고, 필요한 경우 세트 구성까지 해주어야 한다. 즉, 앞서 언급한 것처럼 앞으로 정의할 chooseOption()과 composeSet()을 addToCart() 내부에서 호출해주어야 한다.
입력값 : OrderApp에서 사용자가 메뉴 상품을 고른 경우 addToCart()가 호출되며, 이때, 사용자의 입력값은 해당 상품의 id와 동일합니다. 이 id를 입력값으로 받아오도록 한다.
반환값 : Cart 클래스의 items 필드에 상품을 추가해주기만 하면 되므로, 반환값이 필요하지 않다.
접근 범위 : OrderApp에서 호출해주어야 하므로 public으로 지정한다.
상품을 장바구니(items 필드)에 담으려면 먼저 입력값으로 받은 productId를 id 값으로 가지는 상품을 찾아야 한다. 상품 정보는 ProductRepository에 있으므로, ProductRepository의 인스턴스를 Cart 클래스의 필드로 정의할 필요가 있다. Cart클래스의 필드로 productRepository를 정의해 주고, 생성자를 통해 초기화될 수 있도록 작성한다.
이제 상품을 검색하는 코드를 작성해 보자
의도한 대로 기능은 작동하지만, 객체지향적인 코드는 아니다.
이유를 알기 위해서는 객체지향적인 설계를 알아야 한다. 프로그래밍에서 설계는 코드를 배치하는 것을 의미한다. 그러므로 객체지향적인 설계라 함은 객체지향 원리를 적용하여 코드를 배치하는 것으로 파악할수 있다.
어떤 코드가 ‘객체지향적이다’ 또는 ‘객체지향적으로 작성되었다’면, 해당 코드는 변화와 확장에 유연하다는 의미이다.
예를 들어 클래스 A, B, C, D, E로 구성되는 어떤 프로그램이 있다고 할때,
A 클래스를 수정함에 따라 부수적으로 수정해야 하는 코드들의 범위가 합리적이지 못한 경우, 객체 간 결합도가 높다고 표현을 하며,
A 클래스를 수정함에 따라 B, C, D, E 클래스를 수정해야 하는 부분이 합리적인 범위 혹은 수정하지 않아도 된다면, 이를 객체 간 결합도가 낮다고 표현한다.
즉, 결합도가 낮게 프로그램을 설계하면, 추후 어떤 변화가 발생하여도 기존의 코드를 최소한으로 수정해도 된다.
결론적으로, 변화와 확장에 유연하게 프로그램을 설계하려면 객체 간 결합도를 낮추어야 하며, 이것이 객체지향적인 설계의 핵심이다.
그렇다면, 객체 간 결합도를 어떻게 낮출 수 있을까? 바로, 객체의 자율성을 높이면 된다.
자율성을 가진 객체란, 능동적으로 자신의 역할을 수행하는 객체를 의미한다.
상품 정보를 저장하고, 해당 상품 정보에 접근하는 역할을 가진 productRepository가 수행해야 하는 역할이다.
즉, productRepository는 상품 정보를 저장하고, 해당 상품 정보에 접근하는 역할까지 겸비해야 하는 객체인데, productId를 통해 product를 검색하는 로직이 productRepository가 아닌 addToCart()에 작성되어 있다는 것이 핵심 문제입니다.
결론적으로 productRepository는 자율적으로 자신의 역할을 수행하고 있지 않다고 할 수 있다.
해결방법은 productRepository가 상품 검색 기능을 자율적으로 수행할 수 있도록 상품 검색 코드를 productRepository로 옮긴다.
Cart에서는 그저 productRepository의 findById()를 호출해주기만 하면 된다.
After : Cart 인스턴스는 productRepository의 findById()를 호출할 뿐, 어떻게 검색이 이루어지는지 전혀 알지 못합니다. 검색 로직은 productRepository가 findById()를 통해 직접 수행한다.
이처럼 객체 내부의 세부적인 동작을 캡슐화를 통해 감추면 객체 간 결합도를 낮출 수 있다.