- 사용자는 1000원, 5000원 단위로 금액을 투입할 수 있다.
- 투입된 금액 및 잔액은 잔액창에서 확인할 수 있다.
- 구매 버튼을 누르면 선택한 제품이 하단 상품 출구창에 나타난다.
- 구매한 제품이 상품 출구창 너비를 넘어가는 경우 스크롤해서 전체 목록을 확인할 수 있다.
- 재고 및 잔액에 따라 상품 구매가 불가능할 경우 구매 버튼이 비활성화 된다.
- 재고를 추가하거나 금액을 더 투입하면 다시 구매 버튼이 활성화 된다.
- 객체지향 프로그래밍(OOP) 방식에 따라 음료 객체를 설계했다.
- 최상위 객체
Beverage와 이를 상속하는 하위 클래스Milk,SoftDrink,Coffee,EnergyDrink를 생성했다.
- 지난 스텝에서 구현한 음료 클래스를 3단계로 구분했다.
- 각 하위 클래스는 상위 클래스에는 없는 속성을 하나 이상 포함하도록 설계했다.
- (1단계)
Beverage - (2단계)
Milk,SoftDrink,Coffee,EnergyDrink - (3단계)
StrawberryMilk,ChocolateMilk/ZeroCalorieSoftDrink/CafeLatte
- 기본적인 동작을 하는 자판기 객체를 설계했다.
- 자판기 금액 추가, 재고 추가, 음료수 구매, etc.
- 자판기 화면을 구현하고 MVC 형태에 맞춰 작성한 소스 파일을 정리했다.
- 상품 목록이 뜨는 인벤토리창은
UICollectionView로 구현했다.- 인벤토리 내 각 상품 정보(재고 추가 버튼, 상품 이미지, 재고 레이블, 구매 버튼)는 재사용을 위해 xib로 구현했다.
VendingMachine객체를AppDelegate에 생성하고 앱 생명주기에 따라 객체 인스턴스 속성을 저장하고 불러올 수 있도록 했다.- 앱이 종료되는 시점에 호출되는
sceneDidEnterBackground:메소드 내에서UserDefaults로 저장했다.UserDefaults는 간단한 사용자 설정(e.g. 사용자 선호 측정 단위, 미디어 재생 속도, etc.)을 저장하는 데 사용된다.- 암호화되지 않기 때문에 외부에서 접근해 변경이 가능한 점, 한번에 메모리에 올라가 용량이 커지면 앱이 느려질 수 있는 점에서 그 외의 사용 용도로는 추천되지 않는다.
- 이 프로젝트에서는 학습을 위해 사용했다.
- 앱이 시작되는 시점에 호출되는
application:didFinishLaunchingWithOptions:메소드 내에서 저장된 값을 불러와서VendingMachine객체를 대체하도록 했다. 기존에 저장된 값이 없다면 새로운 자판기 객체를 생성한다. - 음료 객체의 하위/상위 호환성을 유지하고 객체 그래프를 그대로 저장하기 위해 Keyed Archiving 방식을 사용했다.
- 이를 위해 저장에 필요한 각 객체가
NSCoding프로토콜을 채택하도록 개선했다. - 커스텀 클래스인
VendingMachine객체를NSData의 인스턴스로 만들기 위해NSKeyedArchiver로 인코딩을, 이렇게 저장된 값을 디코딩하기 위해선NSKeyedUnarchiver를 사용했다.
- 이를 위해 저장에 필요한 각 객체가
- 앱이 종료되는 시점에 호출되는
- MVC 패턴에서 Model과 Controller의 직접적인 참조 관계를 끊기 위해 NotificationCenter를 사용했다.
- 일반적인 Observer 패턴 대신
Combine을 import해서Publisher와Subscriber를 사용해 실습해보았다.
- 구매 목록이 계속 누적되어도 모두 확인할 수 있도록 구매 목록을
UIScrollView를 이용해 구현했다. UIScrollView내부에UIStackView를 추가했다. 음료를 구매할 때마다 스택뷰에subview를 추가할 수 있도록 했다. 스크롤뷰가 스택뷰의 너비에 따라 horizontal하게 스크롤이 되도록 Auto Layout을 설정했기 때문에 스택뷰가 스크롤뷰의 너비를 넘는 순간 스크롤이 가능해진다.
- 구현 예정
- 생각해봐야 할 점: 기존 뷰와 코드를 재사용하면서 명확하게 구조를 짤 수 있을지 고민해보기
- 구현 예정
AppDelegate내에 생성했던VendingMachine객체를 싱글톤으로 변경했다.- 변경 전에는
viewController에서VendingMachine객체를 가져다 쓰기 위해 상위 모듈에 접근했어야 했다. - 또한
AppDelegate도 구체 타입이고 의존성이 생기게 된다는 코드 리뷰도 받았었다.
- 변경 전에는
VendingMachine클래스에sharedVendingMachine라는optional VendingMachine타입의 static property를 생성했다. 또한 생성자 앞에private키워드를 붙여VendingMachine클래스 자신만 자기의 인스턴스를 만들 수 있도록 제한했다.sharedVendingMachine역시private접근 제어자를 붙인 후,shared()메소드를 통해 싱글톤 객체에 접근할 수 있게 해주었다.application:didFinishLaunchingWithOptions:대신 이 메소드 내에서UserDefaults에 저장된VendingMachine객체를 불러올 수 있도록 수정했다.
2주간의 프로젝트 기간 중 음료 클래스 설계에 거의 일주일을 투자할만큼 가장 고민이 되었던 부분이었다. 설계를 진행하며 '상품'으로서의 음료와 '종류'로서의 음료가 서로 충돌하는 느낌을 받았고 그래서 Beverage와 Product 타입을 따로 분리하기도 했다. '객체지향은 현실 세계의 모방'이라고 생각했기 때문에 객체가 최대한 실세계를 닮아 있어야 한다고 생각했다. 하지만 기본 설계가 복잡해지다 보니 다음 스텝을 구현하기가 쉽지 않아 애를 많이 먹었다.
리팩토링을 진행하면서는 조영호님의 [객체지향의 사실과 오해]를 읽고 이에서 배운 내용을 적용해보기로 했다. 책은 위에서 언급했던 '객체지향 세계는 현실 세계의 단순한 모방'이 그 제목처럼 오해임을 밝힌다. '현실의 모습을 조금 참조한 궁극적으로 다른 새로운 세계를 창조하는 것이 목적'이라는 말에 수긍이 되었기에 기존의 설계를 수정하기도 결정했다. 분리했던 Beverage와 product 타입을 Beverage로 일원화해 전체적인 구조를 보다 단순화했다. 설계를 다시 진행하며 클래스 다이어그램을 그려본 것도 클래스간의 관계를 파악하는 데 큰 도움이 되었다.
Storyboard에 전체 음료 목록의 구매 버튼을 하나씩 모두 생성하고 IBAction으로 각각 연결해 음료의 구매 처리를 한다면 그리 복잡하게 생각할 일은 없을 것이다. 하지만 이는 그다지 우아하지 못한 방법이라는 생각이 들었다. 그래서 커스텀셀을 만들어 반복되는 인터페이스를 처리하기로 했다. 이때 가장 고민이 되었던 것은 어떻게 탭이 되는 버튼이 속한 음료의 구매를 적절히 처리해주는가였다. 고민 끝에 델리게이트 패턴을 사용해 동작을 구현해주기로 결정했다.
SlotCollectionViewCellDelegate를 viewController가 채택해 버튼이 눌릴 때 함께 전달된 indexPath를 이용하여 viewController에 구현된 메소드대로 동작할 수 있게 했다.
MVC, OOP, App Life Cycle, Archive, UserDefaults, NotificationCenter, Delegation Pattern, Singleton Pattern
- Tests
- 단위 테스트
- 통합 시나리오 테스트
- Admin Mode 구현
- User Mode와 Admin Mode의 구분
- 파이 그래프 구현
🤖 이 프로젝트는 코드스쿼드 마스터스 코스의 일환으로 진행되었습니다.


