농장

농장

학습 연습 문제

소개

이상적인 세계에서는 모든 것이 완벽하게 동작해요. 하지만 현실에서는 일이 잘못될 수 있고, 이런 상황을 어떻게 처리하느냐가 소프트웨어를 견고하고 안정적으로 만드는 데 중요해요. 예외는 오류와 예상치 못한 상황을 우아하게 처리할 수 있게 해 주는 프로그래밍의 중요한 개념이에요.

오류를 발생시켰을 때 처리하지 않으면 프로그램이 멈추고 오류 메시지를 출력해요. 대부분의 경우 오류가 발생했을 때 프로그램이 멈추는 것을 원하지 않고, 오류를 처리한 뒤 프로그램을 계속 실행하고 싶어 해요.

예외 발생시키기

Crystal에서는 raise 키워드로 예외를 발생시킬 수 있고, String이나 Exception 객체를 전달할 수 있어요. 처리하지 않으면 프로그램이 멈추고 오류 메시지를 출력해요.

raise "This is an error"

Crystal에는 ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError 등 여러 내장 예외가 있어요. 이런 예외에는 메시지를 전달해야 해요.

raise ArgumentError.new("This is an argument error")

예외 처리

예외가 발생했을 때 프로그램이 중단되는 것은 바라지 않아요. 그래서 오류가 발생하기 쉬운 코드가 있다면, 그 코드를 begin 블록으로 감싸고 rescue 블록으로 예외를 처리할 수 있어요. begin 블록은 예외가 발생할 수 있는 코드의 시작을 표시하고, rescue 블록은 예외를 처리해요.

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
end

rescue 블록에 변수를 지정해서 예외 객체를 받을 수도 있어요.

begin
 raise "This is an error"
rescue ex
 puts "An error occurred: #{ex.message}"
end

rescue 블록에 특정 예외 타입만 지정해서 그 예외만 잡을 수도 있어요.

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
end

# or

begin
 raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
 puts "An argument error occurred: #{ex.message}"
end

여러 개의 rescue 블록을 사용해서 서로 다른 타입의 예외를 처리할 수 있어요. 아래 예제에서 첫 번째 rescue 블록은 ArgumentError를 잡고, 두 번째 rescue 블록은 그 밖의 다른 예외를 잡아요.

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
rescue
 puts "An error occurred!"
end

begin 블록에는 else 블록을 둘 수도 있는데, 예외가 발생하지 않았을 때 실행돼요.

begin
 puts "No error occurred"
rescue
 puts "An error occurred!"
else
 puts "No error occurred"
end

마지막으로, 예외 발생 여부와 상관없이 항상 실행되는 ensure 블록이 있어요.

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
ensure
 puts "This is always executed"
end

메서드 관례

어떤 메서드는 !가 붙은 버전과 붙지 않은 버전, 두 가지가 있어요. 이는 두 가지 다른 의미일 수 있는데, 하나는 메서드가 객체를 변경한다는 뜻이고, 다른 하나는 메서드가 예외를 발생시킬 수 있다는 뜻이에요.

그런데 불리언 개념에서 언급한 것처럼, 메서드 이름을 ?로 끝내는 또 다른 관례도 있어요. 어떤 메서드는 기본적으로 예외를 발생시키지만, 예외를 발생시키는 대신 nil을 반환하는 ?로 끝나는 버전도 있어요.

오류가 발생하는 것을 피하고 싶을 때 이 방법이 이상적이에요. 스택트레이스를 만들 필요가 없어서 성능에 도움이 되고, 제대로 설정하면 코드를 더 안전하게 만들 수도 있어요.

사용자 정의 예외

Exception 클래스를 상속해서 나만의 예외를 만들 수도 있어요. 이때 선택적으로 initialize 메서드를 재정의해서 예외 메시지를 설정할 수 있어요. 메시지를 담은 @message라는 인스턴스 변수를 할당하면 돼요.

class MyException < Exception
 def initialize
   @message = "This is my exception"
 end
end

raise MyException.new

지침

그토록 오래 기다린 날이 마침내 왔어요. 이제 알프스에 있는 아름다운 농장의 자랑스러운 주인이 되었어요.

그래도 아침 일찍 일어나 소에게 먹이를 주는 건 여전히 마음에 들지 않아요. 뛰어난 엔지니어답게, 직접 먹이 공급기 FEED-M-ALL을 만들었어요.

프로젝트를 끝내기 위해 마지막으로 필요한 것은, 각 소가 받아야 할 사료의 양을 계산하는 코드예요. 모든 소가 같은 양을 받는 게 중요해요. 다툼이 생기지 않도록 해야 하죠. 소는 아주 예민하거든요.

다행히도 사료 양을 계산하는 모든 공식을 직접 만들어 낼 필요는 없어요. 인터넷에서 찾은 신비한 외부 라이브러리를 사용하면 되거든요. 이 라이브러리는 가장 행복한 소를 만들어 준다고 해요. 이 라이브러리는 다음 인터페이스를 구현하는 클래스를 제공해요. 직접 작성하는 코드에서 이 클래스에 의존하게 될 거예요.

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

코드를 작성하다 보면, 나중에 농장 일상에서 사용할 때 더 견고하고 디버깅하기 쉽도록 오류 처리를 개선하게 될 거예요.

1. 먹이를 똑같이 나누기

먼저, 소 한 마리당 사료의 양을 계산하는 데 필요한 코드를 작성하는 데 집중해요.

FodderCalculator 객체와 소의 수(정수)를 인자로 받는 메서드 TheFarm.divide_food를 구현해요. 이 과제에서는 전달받는 소의 수가 항상 0보다 크다고 가정해요. 이 메서드는 소 한 마리당 먹이의 양을 반환해야 해요.

계산을 하려면 먼저 모든 소를 위한 전체 사료 양을 가져와야 해요. 이는 fodder_amount! 메서드를 호출하면서 소의 수를 전달하면 돼요. 또한 이 양에 곱해야 하는 계수가 필요해요. 이 계수는 fattening_factor! 메서드를 호출해서 얻어요. 이 두 값과 소의 수를 가지고, 이제 소 한 마리당 먹이의 양을 (float64로) 계산할 수 있어요.

호출한 메서드 중 하나가 오류를 반환하면, 실행을 멈추고 0을 반환해야 해요.

fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0

fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0

2. 소의 수 확인하기

위의 첫 번째 과제를 하다 보면, 사용하는 외부 라이브러리가 생각만큼 품질이 좋지 않다는 걸 알게 돼요. 예를 들어, 잘못된 입력을 제대로 처리하지 못하죠. 이 한계를 피하기 위해 직접 작성한 코드에서 입력값을 검사하기로 해요.

위의 TheFarm.divide_food와 같은 시그니처를 가진 함수 TheFarm.validate_input_and_divide_food를 작성해요.

  • 전달받은 소의 수가 0보다 크면, 함수는 TheFarm.divide_food를 호출하고 그 결과를 반환해야 해요.
  • 소의 수가 0 이하이면, 함수는 "Number of cows must be greater than 0"라는 메시지와 함께 오류를 발생시켜야 해요.
TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15

TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0

3. 오류 처리 개선하기

소의 수를 전달하기 전에 확인한 건 좋은 선택이었지만, 두루뭉술한 오류 메시지가 마음에 들지 않아요. InvalidCowsError라는 사용자 정의 오류 클래스를 만들어서 더 나아지게 하기로 해요.

이 사용자 정의 오류는 소의 수(Int32)와 사용자 정의 메시지(String)를 담고 있어야 하고, Expection 메서드는 데이터를 다음 형식으로 직렬화해야 해요:

{number of cows} cows are invalid: {custom message}

직접 만든 오류를 갖추었으니, 소의 수를 정수로 받아 오류(또는 nil)를 반환하는 함수 TheFarm.validate_number_of_cows를 구현해요.

  • 소의 수가 0보다 작으면, 함수는 사용자 정의 메시지가 "there are no negative cows"로 설정된 InvalidCowsError를 반환해요.
  • 소의 수가 0이면, 함수는 사용자 정의 메시지가 "no cows don't need food"로 설정된 InvalidCowsError를 반환해요.
  • 그렇지 않으면, 함수는 검증이 성공했음을 나타내는 nil을 반환해요.
TheFarm.validate_number_of_cows(5)
# => nil

TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows

이 검증 함수를 만드는 힘든 작업을 마치고 나면, 어느새 저녁이 되어 자리에서 일어나 산 너머로 지는 노을을 즐기러 가요. 새 검증 함수를 실제로 코드에 추가하는 일은 다음 날로 미뤄 둬요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Crystal Exercism

농장 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Crystal 트랙을 개념 26개연습 문제 142개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.