수행 내용

  • 쥬스메이커 프로젝트 Step 1 코드 가독성 향상을 위한 전체 코드 리팩터링
  • 리뷰어 피드백 답변 및 개선 내용 main에 merge

학습 내용

디버깅 로그 출력

현재까지 공부한 내용으로 볼 때, 실행한 메서드에서 에러가 발생할 때 두 가지 방식으로 에러 위치를 특정할 수 있다.

  1. 메서드별로 에러를 별도로 할당하여 던지고(throw) 호출한 쪽에서 처리(do-catch)하는 방법
  2. 에러가 발생한 메서드에서 "내가 문제를 일으켰소" 하는 프린트를 출력하는 방법

일관적인게 좋겠지만 리뷰어의 의견을 들어보기 위해 이번 프로젝트에서는 두 가지 방법을 모두 사용해봤는데, 이 중 에러를 던져서 처리하는 방법을 더 많이 사용하고 있다.
구현 단계이다보니 제한된 숫자의 오류만 정의하고 중복해서 사용하다 보니 에러가 발생해도 어떤 메서드에서 발생하였는지 특정하기 어려운 문제에 봉착했다. 추후에 에러 케이스를 늘리면 해결되는 문제겠지만 디버깅할 때 참고할 수 있도록 간편하게 해결할 방법이 없을까 고민했고, #변수라는 것을 알았다 (이건 뭐라고 부를까? #변수가 아닐 것 같다). print(_:)문 안에 #function, #file, #line과 같이 작성하면 print(_:) 함수를 호출한 메서드 이름, 파일(디렉토리 포함), print(_:) 호출 라인까지 특정할 수 있는데, 아래와 같이 로그를 찍어보는데 유용하게 활용할 수 있다.

print("\(Date()) \(#file.components(separatedBy: "/").last ?? "") \(#function) \(#line) 로그 내용")
  • Date(): 날짜
  • #file: 파일 이름(경로 포함), 위 코드와 같이 separatedBy:를 사용하여 분리하고 추가 처리하여 파일 이름만 출력하게 할 수 있다.
  • #function: print(_:)를 호출한 메서드의 이름과 매개변수. type(of:)와 같은 형식으로 출력
  • #line: print(_:)를 호출한 라인 번호
  • #column: 아직 잘 모르겠지만.. 열번호라면 설마 앞에서부터 글자수를 셌을 때 몇 번째 있는걸 말하는건가..? 맙소사..

    에러를 던지고 처리하는 방법을 채택해 사용하고 있지만, 매번 do-try-catch 과정을 작성하는 것이 맞는지 계속 의문이 든다. 코드 깊이가 깊어지는 문제도 있고.. 에러를 다르게 처리할 수 있는 방법이 없을까? Result 타입을 공부하면 도움이 될까?

고민한 점 / 문제점

타입 설계 시 수행 기능 범위에 대하여

코드를 작성하다보면 '이 기능이 이 타입에 있어도 되나' 하는 생각이 들 때가 있다. 야곰이 이야기한대로 구현하기 전에 정교한 설계를 하지 않으면 일어나는 일인데, 코드를 작성하는 입장에서도, 호출해서 사용하는 입장에서도 매우 불편한 상황을 만든다.

해결 방법

모둠원 강경과 타입별로 수행하는 기능과 가진 프로퍼티들을 재검토하여 타입별로 가져야할 기능들을 재분류하여 코드를 리팩터링하였고 메서드명과 하는 일이 동일하도록 코드를 일부 수정했다. 기능 분류 결과는 아래와 같다.

쥬스메이커 타입 {
- Stock 클래스 인스턴스 생성
- 쥬스만들기(make(of:))
- 쥬스를 만들기 위해 필요한 과일의 종류와 개수를 가져오기 (checkRequiredFruits(for:))
- 필요한 과일의 종류와 양을 재고와 비교하여 재료 충분 여부를 반환하기 (hasEnoughFruits(of:))
- 재료 사용하기 (consumeStockedFruits(for:))
- 쥬스 완성 문구 출력 (printOrderCompleted(for:))
- make 메서드에서 발생한 에러 처리하기 (handleErrorForMake(_:))
}

과일재고 타입 (Stock) {
- 딕셔너리 형식 과일 재고 저장 프로퍼티 (stock = [Fruit: Int])
- 과일 초기 재고 지정 이니셜라이저
- 지정한 과일의 재고 체크하기 (checkStock(for:))
- 유효하지 않은 과일 에러 구문 출력하기 (printInvalidFruitError())
- 지정한 과일의 재고 반환하기 (count(for:))
- 지정한 과일의 재고를 지정량만큼 빼기, 지정량 없을 경우 하나 빼기 (subtract(for:amount:))
- 지정한 과일의 재고 하나 더하기
}

쥬스 레시피 타입 (JuiceRecipe) {
- JSON에서 옵셔널 Recipe로 디코딩된 저장 프로퍼티 (wrappedRecipeBook)
- JSON 데이터 디코딩 이니셜라이저
- 지정된 쥬스 레시피를 반환하기 (find(for:))
}

// JSON 파일 디코딩을 위한 타입

레시피 타입 (Recipe) { 
- 모든 쥬스 레시피 모음 저장 프로퍼티 (juiceRecipes: [JuiceType])
}

// JSON 파일 디코딩을 위한 타입

쥬스 타입 (JuiceType) {
- 레시피 내 쥬스 이름 (name: String)
- 쥬스 재료 (ingredient: [Ingredient])
}

// JSON 파일 디코딩을 위한 타입

재료 타입 (Ingredient) {
- 과일 이름 (fruitName: Fruit?)
- 필요 개수 (quantity: Int?)
- 디코딩 시 받을 프로퍼티의 이름과 JSON 내 해당 자료 이름 매칭을 위한 CodingKeys 열거형 (Decodable 프로토콜 준수)
- JSON 디코딩 시 파싱된 내용을 프로퍼티에 사용자 지정 타입으로 받기 위한 이니셜라이저(init(from decoder: Decoder))
}

과일 에러 타입 (FruitError) {
- 에러 발생 메서드명 출력 전역함수 (informErrorLocation(functionName:))
- 유효하지 않은 과일 입력 케이스
- 에러 발생 시 반환할 사용자 지정 문구 연산 프로퍼티 (CustomStringConvertible 프로토콜)
}

쥬스 에러 타입 (JuiceError) {
- 유효하지 않은 쥬스 입력 케이스
- 에러 발생 시 반환할 사용자 지정 문구 연산 프로퍼티 (CustomStringConvertible 프로토콜)
}

레시피 에러 타입 (JuiceError) {
- 유효하지 않은 레시피 입력 케이스
- 에러 발생 시 반환할 사용자 지정 문구 연산 프로퍼티 (CustomStringConvertible 프로토콜)
}

과일 타입 (Fruit) {
- 딸기, 바나나, 파인애플, 키위, 망고 케이스
- 각 케이스별로 반환할 사용자 지정 문구 연산 프로퍼티 (CustomStringConvertible)
} 

쥬스 타입 (Juice) {
- 딸기쥬스, 바나나쥬스, 키위쥬스, 파인애플쥬스, 딸바쥬스, 망고쥬스, 망고키위쥬스 케이스
- 각 케이스별 이름을 반환하기 위한 연산 프로퍼티 (name: String)
}

리팩터링 과정을 통해 쥬스메이커의 핵심 메서드인 make(of:)메서드를 훨씬 간결하게 표현할 수 있게 되었다.

func make(of orderedJuice: Juice) {
  do {
    let requiredFruits: [Fruit: Int] = try checkRequiredFruits(for: orderedJuice)
    if try hasEnoughFruits(of: requiredFruits) {
      consumeStockedFruits(for: requiredFruits)
      printOrderCompleted(for: orderedJuice)
    }
  } catch {
    handleErrorForMake(error)
  }
}
profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글