# 쥬스 메이커
## 📖 목차
1. [소개](#🌱-소개)
2. [타임라인](#📆-타임라인)
3. [시각화된 프로젝트 구조](#👀-시각화된-프로젝트-구조)
4. [트리 다이어그램](#🌳-트리-다이어그램)
5. [실행 화면](#💻-실행-화면)
6. [트러블 슈팅](#❓-트러블-슈팅)
7. [참고 링크](#🔗-참고-링크)
## 🌱 소개
`Gundy`와 `준호`의 iOS 쥬스 메이커 프로젝트입니다.
## 📆 타임라인
<details>
<summary>STEP 1</summary>
220830
- 열거형 `Fruit`
- 과일을 나타내는 열거형 `Fruit` 추가
- 클래스 `FruitStore`
- 과일을 저장하는 `inventory` 프로퍼티 추가
- 수량을 받아 모든 과일을 동일한 수량으로 초기화하는 이니셜라이저 추가
- `inventory` 프로퍼티에 과일 수량을 추가하는 `add` 메서드 추가
- `inventory` 프로퍼티에 과일 수량을 감소시키는 `subtract` 메서드 추가
- 쥬스를 만들 과일 수량이 충분한지 확인하는 `canSubtract` 메서드 추가
- `Dictionary`를 받아 원하는 과일만 선택적으로 초기화하는 이니셜라이저 추가
- `add`, `subtract` 메서드에 오류 처리 구문 추가
- `canSubtract` 메서드 삭제
- `[Juice.Material]` 배열을 받아 각 과일별 재고가 충분한지 확인하는 `checkStockOfFruits` 메서드 추가
- `inventory` 프로퍼티의 값을 변경하는 `changeStock` 메서드 추가
- `add`, `subtract` 메서드에서 `changeStock` 호출해 값을 변경하는 것으로 수정
- 열거형 `Juice`
- 쥬스를 나타내는 열거형 `Juice` 추가
- 쥬스의 재료가 될 과일과 수량을 담을 중첩 구조체 `Material` 추가
- 쥬스 case별로 `Material` 값으로 반환하는 `recipe` 연산 프로퍼티 추가
- 구조체 `JuiceMaker`
- 과일을 받을 `fruitStore` 프로퍼티 추가
- `fruitStore` 프로퍼티의 `canSubtract` 메서드와 `subtract` 메서드를 호출해 쥬스를 만드는 `makeJuice` 메서드 추가
- `FruitStore`의 인스턴스를 받아 `fruitStore` 프로퍼티를 초기화하는 이니셜라이저 생성
- `makeJuice` 메서드에 오류처리 구문 추가
- `makeJuice` 메서드에서 호출할 재고 확인 메서드를 `checkStockOfFruits` 메서드로 변경
- `makeJuice` 메서드에서 호출할 과일 수량 변경 메서드를 `changeStock` 메서드로 변경
- 에러타입 `FruitStoreError`
- 에러 프로토콜을 준수하는 열거형 `FruitStoreError` 추가
- 에러가 던져진 이유를 설명하는 `failureReason` 연산 프로퍼티 추가
- 재고가 부족한 경우인 `outOfStock` 케이스 추가
</details>
<details>
<summary>STEP 1 1차 Feedback 반영</summary>
220901
- 네이밍
- `add` 메서드 `increaseStock`으로 네이밍 변경
- `subtract` 메서드 `decreaseStock`으로 네이밍 변경
- `changeStock(of fruit: Fruit, amount: Int)` 메서드 `changeStock(of fruit: Fruit, by amount: Int)`로 아규먼트 레이블 변경
- `checkStockOfFruits` 메서드 `checkStockOfIngredients`로 네이밍 변경
- `makeJuice` 메서드 `make(_ juice: Juice)`로 네이밍 변경
- 에러타입 `FruitStoreError`의 `wrongAmount` 케이스 `invalidAmount`로 네이밍 변경
- 열거형 `Juice`의 모든 케이스 뒤에 Juice 붙이는 것으로 네이밍 변경
- 열거형 `Juice` 내부의 중첩타입 `Material` 구조체 `Ingredient`로 네이밍 변경
- 메서드
- 현재 재고를 가져오는 `getCurrentStock` 메서드 추가
- 재고가 충분한 지 확인하는 `checkInventoryHasStock` 메서드 추가
- `checkStockOfIngredients` 메서드 내부의 재고 확인 기능을 `checkInventoryHasStock` 메서드 호출로 변경
- 은닉화
- `inventory` 프로퍼티를 `private(set)` 에서 `private`로 변경
</details>
<details>
<summary>STEP 1 2차 Feedback 반영</summary>
220902
- 창고 내부 과일 목록에 있는지 확인해 Bool값을 반환하는 `isInventoryFruitsListHas` 메서드 추가
- `increase` 메서드 내부 재고 확인 기능 `isInventoryFruitsListHas` 메서드 호출로 변경
- 에러타입 `FruitStoreError`의 `notInFruitList` 케이스 `notInInventoryFruitList`로 네이밍 변경
- 에러타입 `FruitStoreError`의 `failureReason` 프로퍼티의 switch 구문 내부 `notInInventoryFruitList` 케이스 반환 문자열 변경
- `checkInventoryHasStock` 메서드 은닉화
- `isInventoryFruitsListHas` 메서드 은닉화
</details>
<details>
<summary>STEP 2</summary>
220905
- 뷰 컨트롤러 `StockEditViewController`
- 재고를 수정하는 `StockEditViewController` 추가
- 버튼이 눌려졌을 때 화면을 dismiss하는 `touchUpDismissButton` 메서드 추가
- 클래스 `FruitStore`
- 기존 `getCurrentStock` 메서드 `currentStock`으로 네이밍 변경
- 뷰 컨트롤러 `ViewController`
- 쥬스 완성시 얼럿 메시지를 표시하는 `showOkayAlert` 메서드 추가
- 재고 부족시 얼럿 메시지를 표시하는 `showStockEditAlert` 메서드 추가
- 각 주문 버튼 터치시 쥬스를 만드는 `touchUpJuiceOrderButton` 메서드 추가
- 재고 추가 화면 관련 전환하는 `presentStockEditView` 메서드 추가
- 에러타입 `FruitStoreError`
- `unexpectedError` 케이스 추가
- `failureReason` 프로퍼티에 `unexpectedError` 케이스 추가
- 열거형 `Juice`
- 쥬스별 이름을 반환하는 `name` 프로퍼티 추가
- 구조체 `JuiceMaker`
- `make` 메서드에 `Result`타입 반환으로 수정
220906
- 뷰 컨트롤러 `ViewController`
- 화면의 재고 레이블 데이터를 변경하는 updateFruitStockLabel` 메서드 추가
- `FruitStore` 타입의 `fruitStore` 프로퍼티 추가
- `JuiceMaker` 타입의 `juiceMaker` 프로퍼티 추가
</details>
<details>
<summary>STEP 2 Feedback 반영</summary>
220907
- 뷰 컨트롤러 `ViewController`
- 버튼에서 주문하는 주스가 무엇인지 반환하는 `juice` 메서드 추가
- `touchUpJuiceOrderButton` 메서드에서 `juice` 메서드 호출하는 것으로 변경
- 기존 매직 리터럴을 `AlertText`의 프로퍼티로 대체
- 메시지와 가변 매개변수로 얼럿을 띄우는 `showAlert` 메서드 추가
- `showOkayAlert` 메서드와 `showStockEditAlert` 메서드에서 `showAlert` 메서드 호출하는 것으로 변경
- `juiceMaker` 프로퍼티의 `make` 메서드 호출 시 반환이 failure일 때, 오류에 맞는 얼럿 메서드 호출하는 것으로 수정
- 에러타입 `FruitStoreError`
- `failureReason` 프로퍼티 `localizedDescription`으로 네이밍 변경
- `notInInventoryFruitList` 케이스일 때 재고의 nil을 표현하기 위한 `notExist` 프로퍼티 추가
- 열거형 `AlertText`
- 얼럿 관련 메서드에서 사용하던 기존 매직 리터럴을 담을 `AlertText` 열거형 추가
- 구조체 `JuiceMaker`
- `make` 메서드에서 catch하지 않는 `invalidAmount` 에러 캐치구문 삭제
</details>
<details>
<summary>STEP 3</summary>
220912
- 뷰 컨트롤러 `StockEditViewController`
- 네비게이션 바 추가하여 타이틀 `재고 수정`으로 변경
- 과일의 현재 재고로 레이블을 업데이트 하는 `updateFruitStockLabel` 메서드 추가
- `FruitStoreDelegate?` 타입의 `delegate` 프로퍼티 추가
- 스텝퍼를 이용해 재고 레이블을 수정하는 `touchUpStepper` 메서드 추가
- 스텝퍼를 통해 입력되는 값을 이용해 재고 수량을 변경하는 `changeStock` 메서드 추가
- `updateFruitStockLabel` 메서드에 재고가 nil일시 연관된 스텝퍼를 비활성화하는 기능 추가
- 뷰 컨트롤러 `ViewController`
- `FruitStoreDelegate` 프로토콜 채택
- `presentStockEditView` 메서드에서 present하는 컨트롤러를 `stockEditViewController`의 네비게션 컨트롤러로 변경
- `presentStockEditView` 메서드에서 `stockEditViewController`의 `delegate` 프로퍼티를 self로 설정
- 재고가 nil인 과일의 쥬스 주문 버튼을 눌렀을 때 재고 목록에 과일을 추가할 수 있는 `showAddFruitsAlert` 메서드 추가
- 작은 화면일 때 쥬스 주문 버튼의 타이틀 레이블 폰트 사이즈를 딱 맞게 줄여주는 `fitText` 메서드 추가
- 프로토콜 `FruitStoreDelegate`
- `FruitStore` 타입의 `fruitStore` 프로퍼티를 갖는 `FruitStoreDelegate` 프로토콜 추가
- 클래스 `FruitStore`
- 쥬스의 재료중 재고가 nil인 과일을 추가하는 `addFruit` 메서드 추가
</details>
<details>
<summary>STEP 3 Feedback 1차 반영</summary>
220914
- 열거형 `Fruit`
- `Int` 타입의 원시값 추가
- 뷰 컨트롤러 `StockEditViewController`
- 과일 재고 레이블의 태그를 각 과일의 원시값으로 설정
- 과일 재고 스텝퍼의 태그를 각 과일의 원시값을 음수로 변경하여 설정
- 메서드의 레이블을 변경하는 `updateFruitStockLabel` 메서드 추가
- 스텝퍼의 상태 변경을 하는 `updateFruitStockStepper` 메서드 추가
- `updateFruitStock` 메서드에서 `updateFruitStockLabel` 메서드와 `updateFruitStockStepper` 메서드를 호출하는 것으로 변경
- 뷰 컨트롤러 `ViewController`
- `fitText` 메서드 `adjustFontSizeOfButtonsToFitWidth`로 네이밍 변경
- 열거형 `Fruit`
- `addFruit` 메서드 `addNewFruitsOf(_ fruitList: [Fruit])`로 네이밍 변경
</details>
<details>
<summary>STEP 3 Feedback 2차 반영</summary>
220916
- 열거형 `Juice`
- `Int` 타입 원시값 추가
- 뷰 컨트롤러 `ViewController`
- 쥬스 주문 버튼을 담을 배열 `juiceOrderButtons` 프로퍼티 추가
- 과일 재고 레이블을 담을 배열 `fruitStockLabels` 프로퍼티 추가
- `juice` 메서드 switch 구문을 `juiceOrderButtons`의 인덱스와 과일의 원시값을 활용하는 방법으로 변경
- `updateFruitStockLabel` 메서드의 반복되는 if-else 구문을 `fruitStockLabels`와 쥬스의 원시값을 통한 for-in 구문으로 변경
- `presentStockEditView` 메서드에서 `stockEditViewController.delegate`에 할당하는 대상이 `self.fruitStore`로 변경
- 뷰 컨트롤러 `StockEditViewController`
- 과일 재고 레이블을 담을 배열 `fruitStockLabels` 프로퍼티 추가
- 과일 재고 스텝퍼를 담을 배열 `fruitStockStepper` 프로퍼티 추가
- 스텝퍼에서 수정하는 과일이 무엇인지 반환하는 `fruit(of stepper: UIStepper) 메서드 추가
- `touchUpStepper` 메서드의 switch case 기능을 `fruit` 메서드 호출로 변경
- `viewWithTag`로 레이블과 스테퍼를 특정하던 기능을 `fruitStockLabels`, `fruitStockSteppers` 배열 및 인덱스를 활용한 방법으로 변경
- 프로토콜 `FruitStoreDelegate`
- `currentStock(of fruit: Fruit) throws -> Int` 메서드 추가
- `changeStock(of fruit: Fruit, by amount: Int)` 메서드 추가
- `fruitStore` 프로퍼티 삭제
- 클래스 `FruitStore`
- `FruitStoreDelegate` 프로토콜 채택
</details>
## 👀 시각화된 프로젝트 구조

## 🌳 트리 다이어그램
```
JuiceMaker.xcodeproj
└── JuiceMaker
├── Controller
| └── AppDelegate.swift
│ └── SceneDelegate.swift
│ └── ViewController.swift
│ └── StockEditViewController.swift
├── Model
│ └── AlertText.swift
│ └── Fruit.swift
│ └── FruitStore.swift
│ └── FruitStoreError.swift
│ └── FruitStoreDelegate.swift
│ └── Juice.swift
│ └── JuiceMaker.swift
└── View
└── Main.storyboard
└── Assets.xcassets
└── LaunchScreen.storyboard
```
## 💻 실행 화면

## ❓ 트러블 슈팅
<details>
<summary>딕셔너리의 값</summary>
클래스 FruitStore 내부의 프로퍼티 inventory를 딕셔너리로 결정하였기 때문에, 과일의 현재 수량에 접근하려고 하면 항상 옵셔널인 것이 문제였습니다. increasStock, decreaseStock 등의 메서드에서 이 값에 접근할 때 닐 병합 연산자를 사용할지 고민을 하였는데, 닐 병합연산자를 사용한다면 nil일 때 해당 과일의 잔여 수량을 0으로 설정해야해서 이 부분이 자연스럽지 않았습니다. 옵셔널 바인딩을 통해 인벤토리에 해당 과일 키가 있는지 구분하여 추가하는 방식으로 고민해본 결과 0 등의 매직 넘버를 사용하지 않고 해결할 수 있었습니다.
</details>
<details>
<summary>조건 확인</summary>
두 과일을 사용하는 딸기바나나, 망고키위 주스의 경우 한 과일이 재고가 충분하더라도 다른 과일이 불충분하면 만들 수 없어야 했습니다. 한 과일의 재료가 충분한지 확인하고 수량을 변경시켰었는데, 이 때 다른 과일이 부족한 경우가 발생했습니다. 이 문제를 해결하기 위해 클래스 FruitStore 내부에 checkStockOfIngredients 메서드를 생성해 실행한 결과 재료가 되는 과일 배열이 모두 충분할 때 쥬스를 만들게 했습니다.
</details>
<details>
<summary>이니셜라이저</summary>
클래스 FruitStore는 각 과일의 초기 재고가 10개로 요구되어 있어 모든 과일을 같은 수량으로 초기화하는 이니셜라이저를 만들었습니다. 하지만 꼭 모든 과일을 원하지 않을 수도 있고, 과일 별로 수량을 맞추지 않을 수도 있다고 생각했습니다. 과일 종류와 수량을 원하는 값으로도 초기화 할 수 있도록 이니셜라이저를 하나 더 추가하였습니다.
</details>
<details>
<summary>오류 처리</summary>
쥬스메이커 인스턴스가 쥬스를 만들 때 canSubtract 메서드로 확인해서 오류를 미연에 방지하기 때문에 보통의 경우 오류가 없을 것이라고 생각했지만, 쥬스메이커를 거치지않고 인스턴스 fruitStore의 add, subtract 메서드에 직접 접근하는 경우가 있을 수 있다고 생각해 add, subtract 메서드에만 오류 처리를 하였습니다. 하지만 쥬스를 만드는 make 메서드도 오류를 캐치해야한다고 생각했습니다. 지금의 checkStockOfIngredients 메서드를 만들고, make 메서드 내부에서 try 키워드가 두번 사용되지 않도록 과일 재고 수량을 변경하는 기능을 changStock 메서드로 분리해 increasStock(기존 add), decreaseStock(기존 subtract), make 메서드에서 호출하게 했습니다.
</details>
<details>
<summary>기능 분리</summary>
getCurrentStock 메서드는 과일 재고량을 가져오면서 창고 과일 목록에 해당 과일이 없으면 오류를 던집니다. increaseStock 메서드에서는 목록에 과일이 있는지 확인하는 기능만 필요해서 getCurrentStock 메서드를 사용하는 건 적절하지 않았습니다. 목록에 과일이 있는지 판단해서 Bool 타입으로 반환해주는 isInventoryFruitsListHas 메서드를 추가 한 뒤, increaseStock 메서드에서 호출하도록 수정했습니다.
</details>
<details>
<summary>Delegate</summary>
StockEditViewController에서 ViewController에 있는 fruitStore의 재고 확인(currentStock), 수정(changeStock) 메서드를 호출해야 했습니다. 알림을 보내주는 NotificationCenter보다 Delegate 패턴이 이 경우에 더 적절하다고 생각했습니다. currentStock, changeStock 메서드를 요구하는 FruitStoreDelegate 프로토콜을 추가했습니다. FruitStore에서 FruitStoreDelegate를 채택했습니다. StockEditViewController에 FruitStoreDelegate 타입의 delegate 프로퍼티를 추가했습니다.
</details>
<details>
<summary>UIView 접근방법</summary>
ViewController의 updateFruitStockLabel 메서드와 StockEditViewController의 updateFruitStockLabel, updateFruitStockStepper 메서드는 Label의 text / Stepper의 value를 재고에 맞게 업데이트합니다. 처음에는 모든 Label과 Stepper를 @IBOutlet 변수를 사용해서 접근했습니다. 코드 중복을 없애려고 tag를 사용해 접근하는 방법을 고민해보았습니다. 최종적으로는 UILabel/UIStepper을 담은 배열을 @IBOutlet 변수로 선언해서 열거형 Fruit의 rawValue로 접근하도록 했습니다.
</details>
## 🔗 참고 링크
- Swift API Design Guidelines
- [naming](https://www.swift.org/documentation/api-design-guidelines/#naming)
- Swift Language Guide
- [Initialization](https://docs.swift.org/swift-book/LanguageGuide/Initialization.html)
- [Access Control](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html)
- [Nested Types](https://docs.swift.org/swift-book/LanguageGuide/NestedTypes.html)
- [Type Casting](https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html)
- [Error Handling](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html)
- [Protocols](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html)
---
[🔝 맨 위로 이동하기](#쥬스-메이커)
---
프로젝트 STEP 시작 전 나눈 이야기
[STEP 1](#STEP-1-시작전)
[STEP 2](#STEP-2-시작전)
#### STEP 1 시작전
- FruitStore 타입: 과일의 재고를 관리
- 과일의 종류: 딸기, 바나나, 파인애플, 키위, 망고
- enum Fruit
- 과일의 초기 재고: 10개
- initialStock(초기 재고)
- inventory 과일, 수량
- 과일의 수량을 변경하는 기능
- manageInventory()
- JuiceMaker 타입: FruitStore를 소유하고, 쥬스를 제조
- FruitStore의 과일을 사용해 쥬스 제조
- makeJuice()
- enum Recipe
- 딸기: 16개
- 바나나: 2개
- 키위: 3개
- 파인애플: 2개
- 딸바: 딸기 10개, 바나나 1개
- 망고: 3개
- 망키: 망고 2개, 키위 1개
- FruitStore 소유
- fruitStore
- (선택사항) 필요한 경우 오류처리 구현
- 에러 - 프로그래머가 예상치 못한 것?
- 재고가 부족한 쥬스 주문 시, 에러인가?
- 커밋 -> 함수 하나 만들 때마다 커밋하고 교대
#### Feedback
- 재고 체크 메서드 분리
- 네이밍
- inventory 타입으로 분리할지 고민
#### Feedback2
- `notInFruitList` 네이밍 수정
- `getCurrentStock` 수정
- 은닉화 살펴보기
#### STEP 2 시작전
### Pull Request
코드를 작성할 때 고민되었던 점, 해결이 되지 않은 점, 조언을 얻고 싶은 부분 등 상세하게 작성
[STEP 1](#STEP-1-PR)
[STEP 2](#STEP-2-PR)
[STEP 3](#STEP-3-PR)
#### STEP 1 PR
쥬스메이커 [STEP 1] Gundy, 준호
@junbangg
안녕하세요 알라딘!🏜
건디와 준호의 첫 PR입니다.🥹
여러모로 고민이 많이 되는 STEP 1이었습니다. 그렇다보니 이 방법이 좋은지 저 방법이 좋은지 고민하게 되고, 선택한 방법에는 이유가 모두 있다고 생각합니다. 이유가 타당하지 않을지도 모르니까 이상한 것 같은 부분은 최대한 알려주시면 좋겠습니다! 🙌
#### 함수별 기능
- `makeJuice`: 열거형 Juice의 case를 전달인자로 받아 주스를 만드는 메서드
- `add`: 클래스 FruitStore의 프로퍼티 inventory 내부 값에 전달인자를 더해주는 메서드
- `subtract`: 클래스 FruitStore의 프로퍼티 inventory 내부 값에서 전달인자를 빼주는 메서드
- `changeStock`: add, subtract, makeJuice 등에서 호출돼 클래스 FruitStore의 프로퍼티 inventory 내부 값을 변경시켜주는 메서드
- `checkStockOfFruits`: 레시피에 포함된 과일의 재고가 적절한지 확인하는 메서드
### 코드를 작성할 때 고민되었던 점
- 무엇보다 네이밍이 정말 고민이었습니다. 적절한 이름 짓는 게 진짜 너무 어려워요😅 최대한 상의하고 고민해가며 네이밍을 결정하였는데, 이상한 점은 말해주시면 감사하겠습니다.
- 클래스 FruitStore 내부의 프로퍼티 inventory를 딕셔너리로 결정하였기 때문에, 과일의 현재 수량에 접근하려고 하면 항상 옵셔널인 것이 문제였습니다. add, subtract 등의 메서드에서 이 값에 접근할 때 닐 병합 연산자를 사용할지 고민을 하였는데, 닐 병합연산자를 사용한다면 nil일 때 해당 과일의 잔여 수량을 0으로 설정해야해서 이 부분이 자연스럽지 않았습니다. 옵셔널 바인딩을 통해 인벤토리에 해당 과일 키가 있는지 구분하여 추가하는 방식으로 고민해본 결과 0 등의 매직 넘버를 사용하지 않고 해결할 수 있었습니다.
- 두 과일을 사용하는 딸기바나나, 망고키위 주스의 경우 한 과일이 재고가 충분하더라도 다른 과일이 불충분하면 만들 수 없어야 했습니다. 이 방법을 구현하기 위해 클래스 FruitStore 내부에 checkStockOfFruits 메서드를 생성해 실행한 결과 재료가 되는 과일 배열이 모두 충분할 때 쥬스를 만들게 했습니다.
- 클래스 FruitStore는 각 과일의 초기 재고가 10개로 요구되어 있으나, 꼭 모든 과일을 넣지 않을 수도 있고, 과일 별로 수량을 맞추지 않을 수도 있다고 생각해 원하는 값으로도 초기화 할 수 있도록 두 가지 경우로 이니셜라이저를 구분하였습니다.
- 쥬스메이커 인스턴스가 쥬스를 만들 때 canSubtract 메서드로 확인해서 오류를 미연에 방지하기 때문에 보통의 경우 오류가 없을 것이라고 생각했지만, 쥬스메이커를 거치지않고 인스턴스 fruitStore의 add, subtract에 직접 접근하는 경우가 있을 수 있다고 생각해 해당 메서드에 오류 처리를 하였습니다. 하지만 결국 makeJuice 메서드도 오류를 캐치해야한다고 생각해 지금의 checkSctockOfFruits 메서드를 만들고, makeJuice 메서드 내부에서 try 키워드가 두번 사용되지 않도록 과일 재고 수량을 변경하는 기능을 changStock 메서드로 분리해 add, subtract, makeJuice에서 호출하게 했습니다.
~~~swift
// 변경 전
func makeJuice(of juice: Juice) {
for material in juice.recipe {
guard fruitStore.canSubtract(amount: material.amount, of: material.fruit) else {
return
}
}
for material in juice.recipe {
do {
try fruitStore.subtract(amount: material.amount, of: material.fruit)
} catch FruitStoreError.wrongAmount {
print(FruitStoreError.wrongAmount.failureReason)
} catch FruitStoreError.notInFruitList {
print(FruitStoreError.notInFruitList.failureReason)
} catch {
print(error)
}
}
}
// 1차 변경
func makeJuice(of juice: Juice) {
do {
try fruitStore.checkStockOfFruits(in: juice.recipe)
for material in juice.recipe {
try? fruitStore.subtract(amount: material.amount, of: material.fruit)
}
} catch FruitStoreError.wrongAmount {
print(FruitStoreError.wrongAmount.failureReason)
} catch FruitStoreError.notInFruitList {
print(FruitStoreError.notInFruitList.failureReason)
} catch FruitStoreError.outOfStock {
print(FruitStoreError.outOfStock.failureReason)
} catch {
print(error)
}
}
// 최종
func makeJuice(of juice: Juice) {
do {
try fruitStore.checkStockOfFruits(in: juice.recipe)
for material in juice.recipe {
fruitStore.changeStock(of: material.fruit, amount: -material.amount)
}
} catch FruitStoreError.wrongAmount {
print(FruitStoreError.wrongAmount.failureReason)
} catch FruitStoreError.notInFruitList {
print(FruitStoreError.notInFruitList.failureReason)
} catch FruitStoreError.outOfStock {
print(FruitStoreError.outOfStock.failureReason)
} catch {
print(error)
}
}
~~~
### 조언을 얻고 싶은 부분
- 쥬스를 만드는 데 필요한 과일의 수량만큼 재고를 감소하기 전에 레시피에 포함된 과일의 재고가 적절한지 checkStockOfFruits 메서드로 확인을 해주었습니다.
- checkStockOfFruit 메서드로 한 번 확인을 해주었기 때문에 subtract 메서드 대신 changeStock 메서드를 사용했습니다.
- subtract 메서드는 직접 호출할 때 적절한 인자인지 등을 체크해준 뒤 changeStock 메서드로 재고를 감소해주는 메서드입니다.
- changeStock 메서드는 별도의 확인 없이 재고를 조정해주는 메서드입니다.
- 이런 식으로 메서드를 분리해서 사용한 것에 대해 조언을 얻고 싶습니다. 더 좋은 방법이 있는지 궁금합니다.
~~~swift
func subtract(amount: Int, of fruit: Fruit) throws {
guard amount > 0 else {
throw FruitStoreError.wrongAmount
}
guard let currentStock = inventory[fruit] else {
throw FruitStoreError.notInFruitList
}
guard currentStock >= amount else {
throw FruitStoreError.outOfStock
}
changeStock(of: fruit, amount: -amount)
}
func changeStock(of fruit: Fruit, amount: Int) {
if let currentStock = inventory[fruit] {
inventory[fruit] = currentStock + amount
}
}
~~~
- 54bdc31 커밋까지 진행했을 시, 오류 처리를 구현하지 않아도 모든 기능이 정상 작동을 하는데, 과연 재고 부족 등이 에러인건지, 꼭 오류 처리를 해줘야하는지 조언을 구하고 싶습니다. 예상되는 조건을 모두 처리했는데 그것이 에러라고 할 수 있는 것인지 궁금합니다.
#### STEP 2 PR
쥬스메이커 [STEP 2] Gundy, 준호
@junbangg
안녕하세요 알라딘!🏜
두 번째 PR도 잘 부탁드립니다.
약간 두서없이 적은 내용도 있지만...우리의 프린스 알리는 지니처럼 찰떡같이 알아들을 것이라고 믿습니다! 감사해요!!🙌
#### 함수별 기능
- `showOkayAlert`: 쥬스가 완성되었을 때 호출할 얼럿을 띄우는 메서드
- `showStockEditAlert`: 주문받은 쥬스를 만들 과일의 재고가 부족할 때, 재고 수정 페이지로 넘어갈 것인지 선택을 받는 얼럿을 띄우는 메서드
- `touchUpJuiceOrderButton`: 각 쥬스별로 주문 버튼을 터치했을 때 쥬스를 만들게 하는 메서드
- `presentStockEditView`: 재고 수정 뷰로 화면전환을 하는 메서드
- `touchUpDismissButton`: 닫기 버튼을 누르면 재고수정 뷰에서 빠져나오게 하는 메서드
- `updateFruitStockLabel`: 과일의 재고 데이터를 뷰의 레이블에 업데이트하는 메서드
### 코드를 작성할 때 고민되었던 점
- FruitStoreError를 캐치해 print하던 `make` 메서드를 이용해 쥬스를 만드려다보니, 반환 값이 필요해졌습니다. 그 반환에 따라 어느 얼럿을 띄울 것인지를 선택해야 했기 때문입니다. 기존에 `make` 메서드를 작성할 때도 오류처리를 한 것은 나름의 이유가 있었기 때문에 이 상황을 쉽게 해결하려고 오류처리를 없앨 수는 없었습니다. 그 결과 반환 타입을 `Result` 타입으로 선택해 적절한 얼럿을 호출하게 하였습니다.
- 이번 스텝의 구현 사항에는 재고수정 뷰로 화면전환까지만 구현할 뿐, 그 화면에서 빠져나오는 기능을 요구하지 않습니다. 하지만 테스트 과정이나 리뷰의 편의를 생각해 닫기 버튼을 구현하는 게 옳다고 생각하였습니다. 다음 스텝에서 요구하는 버튼과는 달라졌지만, 수정을 할 생각으로 만들었습니다.
### 조언을 얻고 싶은 부분
- 얼럿에 들어갈 메시지를 문자열로 전달할 때나 얼럿 액션의 title 같은 경우는 현재 매직리터럴로 보입니다. 모든 경우에서 매직리터럴은 배열 등으로 따로 타입을 정의해서 처리해 주는 것이 좋을까요?
- `updateFruitStockLabel` 메서드를 작성하면서, 새로 오류 처리 없이 과일의 재고를 반환하는 메서드를 생성하기보다는 기존에 구현했던 메서드인 `currentStock`을 호출해 사용하기로 결정하였습니다. 다만 `currentStock` 메서드는 원래 `checkInventoryHasStock` 메서드에서 현재 창고 품목에 있는지 확인하고 수량을 가져오는 용도로 호출해 사용하고 있었는데, `checkInventoryHasStock` 메서드와 `currentStock`메서드를 분리하는 것이 더 좋았을까요? `updateFruitStockLabel` 메서드에서는 던져진 오류에 적절한 처리를 해주지 않기 때문에 이런 고민이 들었습니다.
- if let 문에 else 블록을 추가해서 에러가 발생한 경우를 추가하는 게 좋을까요?
~~~swift
func updateFruitStockLabel() {
if let strawberryStock = try? juiceMaker.fruitStore.currentStock(of: .strawberry) {
strawberryStockLabel.text = "\(strawberryStock)"
}
if let bananaStock = try? juiceMaker.fruitStore.currentStock(of: .banana) {
bananaStockLabel.text = "\(bananaStock)"
}
if let kiwiStock = try? juiceMaker.fruitStore.currentStock(of: .kiwi) {
kiwiStockLabel.text = "\(kiwiStock)"
}
if let pineappleStock = try? juiceMaker.fruitStore.currentStock(of: .pineapple) {
pineappleStockLabel.text = "\(pineappleStock)"
}
if let mangoStock = try? juiceMaker.fruitStore.currentStock(of: .mango) {
mangoStockLabel.text = "\(mangoStock)"
}
}
~~~
- fruitStore 상수를 만든 뒤에 JuiceMaker 이니셜라이저에 만든 fruitStore를 전달해서 저장 프로퍼티에 할당하려고 했는 데 아래와 같은 에러가 발생했습니다.
- 아래 사용한 방법처럼 이니셜라이저에서 바로 FruitStore의 인스턴스를 만들어서 전달하니 에러는 발생하지 않았습니다.
- 왜 두 가지 방법의 결과가 다른 지 궁금합니다.
- 알아보니 ViewController의 인스턴스가 만들어 지기 전에는 self의 프로퍼티에 접근할 수 없어, fruitStore 프로퍼티를 사용할 수 없기 때문에 juiceMaker의 기본값을 초기화 할 때 fruitStore 프로퍼티를 사용할 수 없었습니다. lazy를 붙여 지연 저장 프로퍼티로 하면 사용할 때 초기화가 되기 때문에 fruitStore 프로퍼티를 사용할 수 있습니다. 메서드는 프로퍼티와 달리 인스턴스 생성 후에 사용되는 것이므로 self의 프로퍼티에 대한 접근이 가능했습니다. 뷰 컨트롤러의 인스턴스를 이니셜라이저를 통해 만들어주면 Lazy 키워드 없이도 지금처럼 juiceMaker와 fruitStore를 사용가능한지 궁금합니다.
- ViewController의 인스턴스화는 어디서 어떻게 되는지도 궁금합니다.
~~~swift
// 시도했던 방법 - 실패
let fruitStore: FruitStore = FruitStore(initialStock: 10)
var juiceMaker: JuiceMaker = .init(fruitStore: fruitStore)
// Cannot use instance member 'fruitStore' within property initializer; property initializers run before 'self' is available
/*
struct JuiceMaker {
let fruitStore: FruitStore
init(fruitStore: FruitStore) {
self.fruitStore = fruitStore
}
*/
// 사용한 방법
var juiceMaker: JuiceMaker = .init(fruitStore: .init(initialStock: 10))
// 수정한 방법
var fruitStore: FruitStore = .init(initialStock: 10)
lazy var juiceMaker: JuiceMaker = .init(fruitStore: fruitStore)
~~~
- 쥬스 제조 후 완성 메시지를 출력하는 showOkayAlert 메서드와 재고가 부족하다는 메시지를 출력하는 showStockEditAlert 메서드를 만들었습니다.
- showOkayAlert 메서드는 출력 메시지를 다르게하면 재사용이 가능해서 showOkayAlert로 네이밍을 했습니다.
- showStockEditAlert 메서드는 재고가 부족하다는 메시지를 출력한 뒤 사용자가 예/아니오 중 무엇을 선택하는 지에 따라 재고수정화면으로 이동하거나 얼럿이 닫힙니다. 특정 행위(재고수정화면으로 이동)가 바로 이어서 발생할 수 있기 때문에 ShowStockEditAlert로 네이밍을 했습니다.
- 네이밍이 적절한 지 궁금합니다.
~~~swift
func showOkayAlert(_ message: String) {
let alert = UIAlertController(title: nil,
message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK",
style: .default,
handler: nil)
alert.addAction(okAction)
present(alert,
animated: true,
completion: nil)
}
func showStockEditAlert(_ message: String) {
let alert = UIAlertController(title: nil,
message: message,
preferredStyle: .alert)
let editAction = UIAlertAction(title: "예",
style: .default) { (action) in
self.prsentStockEditView()
}
let cancelAction = UIAlertAction(title: "아니오",
style: .default)
alert.addAction(editAction)
alert.addAction(cancelAction)
present(alert,
animated: true,
completion: nil)
}
~~~
- viewController의 메서드는 은닉화를 어떻게 하면 좋을 지 궁금합니다.과연 뷰 컨트롤러에서는 은닉화가 의미가 있는가? 이니셜라이저가 없는데 뷰 인스턴스는 생성이 되는가? 하는 생각이 들었습니다. 이에 조언을 구하고 싶습니다.
오류처리를 해야하는 이유: 프로그램이 개발자가 의도한대로 동작할 것이란 보장은 없기 때문에 의도하지 않은 동작이 발생하면 정상화 작업을 해주기 위해서 오류처리가 필요한 것 같습니다. 더불어 내부 코드를 알지 못하는 사용자에게 오류에 대한 내용을 전달하는 역할도 할 것 같습니다.
#### STEP 3 PR
쥬스메이커 [STEP 3] Gundy, 준호
@junbangg
안녕하세요 알라딘! 🏜
추석은 잘 마무리하고 계신가요? 🌕
즐거운 한가위였기를 바랍니다. 😄
세 번째 PR도 잘 부탁드립니다. 🙌
이번 피드백도 감사합니다!
#### 함수별 기능
- `updateFruitStock`: 각 과일의 레이블을 수량에 맞게 바꾸고 Stepper와 동기화 시키는 메서드, 과일이 목록에 없다면 레이블의 문구를 `FruitStoreError.notExist`로 바꾸고 Stepper를 비활성화 시킨다.
- `touchUpStepper`: 각 스텝퍼별로 `changeStock` 메서드를 호출해 재고의 변화를 과일 레이블에 적용시키는 메서드.
- `changeStock`: 스텝퍼의 값과 재고의 차를 변화량으로 반환하는 메서드.
- `showAddFruitsAlert`: 재고 목록에 없는 과일을 목록에 추가할 것인지 얼럿을 띄우는 메서드.
- `addFruit`: Juice의 recipe의 과일 중 재고목록에 없는 과일을 추가하는 메서드.
### 코드를 작성할 때 고민되었던 점
- 화면 사이의 데이터 공유를 위해 Delegate 패턴을 활용했습니다.
- FruitStore 타입의 fruitStore 프로퍼티를 요구하는 FruitStoreDelegate 프로토콜을 추가한 뒤, ViewController에서 채택했습니다.
- StockEditViewController에 FruitStoreDelegate 타입의 옵셔널 변수 delegate를 약한참조로 추가했습니다.
- StockEditViewController를 present 하기 전에 delegate를 self(ViewController)로 초기화했습니다.
- delegate를 사용해서 ViewController 프로퍼티 fruitStore의 currentStock, changeStock 메서드를 호출했습니다.
~~~swift
// FruitStoreDelegate 프로토콜 추가
protocol FruitStoreDelegate: AnyObject {
var fruitStore: FruitStore { get set }
}
// ViewController에서 FruitStoreDelegate 프로토콜 채택
class ViewController: UIViewController, FruitStoreDelegate {
// StockEditViewController의 delegate를 ViewController로 초기화
func presentStockEditView() {
guard let stockEditViewController = self.storyboard?.instantiateViewController(withIdentifier: "StockEditViewController") as? StockEditViewController else {
return
}
stockEditViewController.delegate =
// FruitStoreDelegate 프로토콜 타입의 delegate 추가
class StockEditViewController: UIViewController {
weak var delegate: FruitStoreDelegate?
// delegate를 사용해서 currentStock, changeStock 메서드 호출
func changeStock(of fruit: Fruit, by stockStepper: UIStepper) -> Int? {
guard let currentStock = try? delegate?.fruitStore.currentStock(of: fruit) else {
return nil
}
let stockAmountToChange: Int = Int(stockStepper.value) - currentStock
delegate?.fruitStore.changeStock(of: fruit, by: stockAmountToChange)
return currentStock + stockAmountToChange
}
~~~
- 싱글턴을 사용하지 않은 이유는 싱글턴은 결국 `share`에 그 목적이 있다고 생각하기 때문입니다. "JuiceMaker라는 클래스는 FruitStore를 소유하고 있다"는 STEP 1의 요구조건과는 거리가 있다고 느껴져 싱글턴을 사용하지 않았습니다. FruitStore를 직접 소유한다는 점에서 델리게이트 패턴을 적용하는 것이 더욱 적절하다고 생각했습니다.
- 첫 화면과 겹치는 과일 이미지 및 재고 레이블은 첫 화면의 오토레이아웃 제약사항을 따랐습니다. 그 편이 사용자 입장에서 볼 때 더 자연스럽게 느껴질 것이라고 생각했습니다. Stepper의 경우는 기존 오토레이아웃 조건에서의 규칙을 그대로 적용하되, 사이즈 조절이 되지 않는 특성상 가운데 정렬을 우선적으로 따르게 설정하였습니다.
- 기존에는 재고수정 페이지로 화면전환을 할 때, 재고 수정 뷰 컨트롤러를 프레젠트 하였습니다. 하지만 네비게이션 바를 구현해야했기 때문에 `let navigationController = UINavigationController(rootViewController: stockEditViewController)` 선언부를 추가하고 네비게이션 컨트롤러를 프레젠트 하는 것으로 변경하였습니다.
- 재고를 수정하는 것이 어느 순간에 이루어지는 것이 적절한지에 대한 고민을 했습니다. 스텝퍼를 누를때마다 재고가 실시간으로 변경되어야 하는 것인지, 장바구니에 물건을 담기전 수량만 변경하는 것처럼 생각해 최종적으로 무언가 액션이 추가됐을 때 재고가 변경되어야 하는 것인지 고민을 했습니다. 결국 상의한 결과 레이아웃에 컴포넌트를 추가하지 않고 기능을 만족시키기 위해 즉각적으로 재고 수량이 변경되는 것으로 선택하였습니다.
### 조언을 얻고 싶은 부분
- 현재 저희는 스토리보드의 인스펙터 탭을 통한 기능과 코드를 통한 기능을 함께 사용하고 있습니다. 때문에 해당 사항은 코드만 봐서는 구현되지않았는지 생각이 들 수도 있을 것 같습니다. 가령 저희는 스텝퍼의 미니멈밸류를 0으로 하고, 스텝퍼와 재고를 동기화시켰기때문에 스텝퍼를 통한 조작에서 재고는 0보다 작아질 수 없습니다. 하지만 이는 코드에서는 알 수 없는 부분이고, 혹시 누군가가 스토리보드를 통해 미니멈밸류를 수정한다면 발견하지 못하고 사용될 가능성도 있다고 생각합니다. 이같은 경우 코드상에서 더욱 분명히 하기 위해 `viewDidLoad` 메서드 등에서 `stepper.minimumValue = 0` 등의 코드를 작성하는 것이 좋을까요? 스토리보드를 건들지않는한 `stepper.minimumValue = 0`는 쓰이지 않는 코드가 됩니다. 이럴 때 어떤 선택이 더 좋을지 조언을 구하고 싶습니다.
- 오류처리를 위해 try를 사용하는 구간이 생기면 적절한 처리를 위해 고민을 했습니다. 하지만 `StockEditViewController`의 `changeStock` 메서드 같은 경우는 그저 nil을 반환할 뿐입니다. 이는 저희가 `updateFruitStock` 메서드를 통해 해당 오류가 발생할 원천인 스텝퍼를 비활성화했기 때문에 nil을 받는 경우가 절대 없으리라는 생각이 듭니다. 이런 경우 '아마도 절대 사용되지 않을 오류처리 코드'가 될지라도 '만에하나 나의 의도대로 스텝퍼가 비활성화되지않을 수 있으니까' nil을 반환하는 것보다도 더 적극적인 오류처리를 하는 것이 좋을지 궁금합니다.
- updateFruitStock 메서드는 Label의 text를 수정하고 Stepper의 value와 isEnabled를 수정하는 역할을 합니다. 한 메서드에서 한번에 많은 기능을 처리하는 데 어떤 방향으로 기능 분리를 하면 좋을까요? Label과 Stepper를 기준으로 분리를 해야할 지, if 블록 단위로 분리를 해야할 지... 조언을 얻고 싶습니다.
~~~swift
func updateFruitStock() {
if let strawberryStock = try? delegate?.fruitStore.currentStock(of: .strawberry) {
strawberryStockLabel.text = "\(strawberryStock)"
straberryStockStepper.value = Double(strawberryStock)
} else {
strawberryStockLabel.text = FruitStoreError.notExist
straberryStockStepper.isEnabled = false
}
...
}
~~~
- `import Foundation`의 삭제와 같은 작업만으로 커밋을 진행할 때 메시지 서브젝트를 무엇으로 해야할 지 고민입니다. 우선 chore가 가장 적합한 것 같아서 그렇게 적용했는데, 어떤 카르마 스타일을 따라야 할까요?
한 객체와 관련된 컴포넌트가 여러종류일 때, 2개까지는 `tag`를 사용할 수 있을 것 같습니다. 양수와 음수로 구분하면 되니까요. 하지만 3개 이상부터는 해당 객체와 컴포넌트들의 튜플로 구성된 딕셔너리를 정의해서 사용하는 것 밖에는 그다지 방법이 떠오르지 않습니다. 혹시 여러 컴포넌트가 한 객체와 관련될 때 어떤 방법을 사용해볼 수 있을까요?
- 과일 타입(아마도 `FruitStore`)은 왜 클래스로 구현하라고 요구했을까요? 두 가지 이상의 이유를 제시해주세요.
- 일단 과일 타입은 어떻게 구현하라고 요구된 사항이 없고, `FruitStore`라면 클래스로 구현하게끔 되어있었다. 내가 생각하는 이유는 `FruitStore`는 각 과일을 관리하기 때문에 구조체로 구현했다면 인스턴스의 값을 어딘가에 할당하거나 할 때 마다 값이 복사되어서 별개의 인스턴스를 대상으로 `JuiceMaker` 인스턴스의 메서드가 사용됐을 것이다.
- 뷰컨트롤러간의 데이터를 주고받을 때 `FruitStore`가 값타입이라면 원본 인스턴스의 값이 변경되지 않았을 것이다. 물론 복사하면서 계속 새 값을 할당해줄 수도 있겠지만 그건 이 상황에 맞지 않는 해결법인 것 같다.
- ~~싱글톤, 델리게이트 등을 연습하기 좋은 예제라서 클래스로 구현하라고 요구했다.~~
- FruitStore 인스턴스를 앱 내에서 공유할 때 참조하는 쪽에서 값의 변경을 확인할 수 있음.
- 과일가게 타입(아마도 `JuiceMaker`)은 왜 구조체로 구현하라고 요구했을까요?
- 서로 다른 인스턴스에서 공유할 데이터(예: `FruitStore`의 과일재고)를 직접 보유하지 않기때문이라고 생각한다.
- 상속, 타입캐스팅, 디이니셜라이저 등 구조체에는 없고 클래스에는 있는 기능이 필요하지 않기 때문.