오늘은 지난번 글에서 작성했던 '스피드 연산 게임' 코드를 클래스를 사용해서 수정해 보도록 하자.
아래 기존 코드와 클래스를 사용해 수정한 코드가 있다.
require 'timeout'
def make_quiz
n1 = rand(2..9)
n2 = rand(1..9)
["#{n1} x #{n2} = ", n1 * n2]
end
def exam
question, answer = make_quiz
print question
input = STDIN.gets
return answer == input.to_i
end
quiz_cnt = 0
correct_cnt = 0
begin
Timeout.timeout(10) do
while true
quiz_cnt += 1
if exam
correct_cnt += 1
puts "정답"
else
puts "오답"
end
end
end
rescue
end
puts "\n총 #{quiz_cnt}문제 중 #{correct_cnt}문제를 맞췄습니다."
require 'timeout'
class Game
attr_accessor :time
def initialize(time = 10)
@time = time
end
def make_quiz
n1 = rand(2..9)
n2 = rand(1..9)
["#{n1} x #{n2} = ", n1 * n2]
end
def exam
question, answer = make_quiz
print question
@quiz_cnt += 1
input = STDIN.gets
if answer == input.to_i
@correct_cnt += 1
puts "정답"
else
puts "오답"
end
end
def play
@quiz_cnt = 0
@correct_cnt = 0
begin
Timeout.timeout(time) do
while true
exam
end
end
rescue
end
puts "\n총 #{@quiz_cnt}문제 중 #{@correct_cnt}문제를 맞췄습니다."
end
end
Game 이라는 클래스에 기존 코드에 있던 두 개의 메서드를 옮겨왔다. 그 중 make_quiz 메서드는 코드 수정 없이 그대로 옮겨왔고 exam 메서드는 메서드 안에서 두 개의 인스턴스 변수(@quiz_cnt 와 @correct_cnt)를 사용하여 총 문제 수와 정답을 맞춘 수를 카운트하도록 하였다. 그리고 정답과 오답에 대한 화면 출력도 exam 메서드 안으로 넣어서 실제 게임을 진행시키는 코드(play 메서드)가 기존 코드에 비해서 많이 깔끔해진 걸 볼 수 있다.
초기화 메서드(initialize)의 코드를 보면 Game 객체를 생성할 때 인수로 게임 시간을 줄 수 있게 하였고 인수 없이 생성할 경우 게임 시간이 10초가 되도록 기본값(default value)을 time 파라미터에 설정하였다.
아래 그림처럼 Game 클래스 코드를 irb 에서 로드한 후 테스트해 보자.
>> require './game'
=> true
>> game = Game.new
=> #<Game:0x0000020090d298c0 @time=10>
>> game.play
2 x 5 = 10
정답
2 x 4 = 8
정답
9 x 8 = 72
정답
9 x 1 = 9
정답
3 x 4 =
총 5문제 중 4문제를 맞췄습니다.
=> nil
Game 객체를 생성한 후 play 메서드를 호출하여 게임을 두 번 진행해 보았다.
게임을 새로 진행할 때는 총 문제 수와 정답을 맞춘 수를 처음부터 다시 카운트해야 하므로 play 메서드에서 두 개의 인스턴스 변수를 0으로 초기화 시켰다.
그런데 아래처럼 Game 객체를 생성한 후 play 메서드가 아니라 exam 메서드를 호출하게 되면 예외가 발생하게 된다. 빨간색 테두리로 표시된 부분을 보면 NoMethodError 예외이고 nil (nil도 NilClass 클래스의 객체임을 알 수 있다.) 에 정의되지 않은 메서드(+)를 호출했다는 내용이다. 우리가 사용한 + 연산자가 메서드라니 신기할 것이다. 이 부분에 대해서는 나중에 알아보기로 하고 우선 어디서 왜 이런 예외가 발생했는지 찾아보자.
>> require './game'
=> true
>>
>> game = Game.new
=> #<Game:0x00000205af144ae0 @time=10>
>> game.exam
6 x 4 =
Traceback (most recent call last):
...생략
1: from D:/blog/ruby/speed_quiz/game.rb:19:in `exam'
NoMethodError (undefined method `+' for nil:NilClass)
game.exam 코드를 실행한 바로 다음 줄을 보면 "7 x 7 = " 이 출력된 걸 볼 수 있는데 이것은 exam 메서드 안에서 make_quiz 메서드를 호출한 후 print 메서드로 구구단 문제를 출력하여 표시된 것이다. 여기까지는 실행이 잘 되었고 예외가 발생한 곳은 바로 그 다음 줄의 @quiz_cnt += 1 부분이다. 이전 글에서 += 에 대해 설명을 했었는데 + 가 먼저 실행이 되고 그 결과값이 좌측 변수에 할당(=)된다고 했다. 즉 + 가 실행되면서 예외가 발생한 것이다. 루비에서는 + 도 객체에 대해 호출하는 메서드로 인식하기 때문에 @quiz_cnt 변수가 참조하는 객체에 대해 1을 인수값으로 하여 + 메서드를 호출하는 것으로 처리가 된다. 즉 @quiz_cnt.+(1) 과 같다. 결국 이 예외는 @quiz_cnt 인스턴스 변수에 대한 초기화 과정 없이 바로 사용을 하게 되어 인스턴스 변수의 값 대신 nil 을 돌려주어 발생한 것이다. nil 도 엄연히 객체이기는 하지만 NilClass 클래스에는 + 메서드가 정의되어 cl있지 않다.
아래 코드 실행 결과를 보면 실제 NilClass 클래스에는 + 인스턴스 메서드가 없지만 정수(객체)를 표현하는 Integer 클래스에는 + 인스턴스 메서드가 존재하는 걸 알 수 있다.
>> nil.class
=> NilClass
>> NilClass.instance_methods.include?(:+)
=> false
>> 1.class
=> Integer
>> Integer.instance_methods.include?(:+)
=> true
>>
이러한 예외가 발생하지 않게 하기 위해 두 가지 방법을 생각해 볼 수 있는데, 그 중 첫 번째 방법은 아래 코드처럼 Game 객체 생성 시 인스턴스 변수를 초기화해 주는 것이다.
require 'timeout'
class Game
attr_accessor :time
def initialize(time = 10)
@time = time
@quiz_cnt = 0
@correct_cnt = 0
end
...생략
end
또 다른 방법은 Game 객체를 사용하는 코드에서 굳이 필요하지 않은 메서드에 대해서는 호출하지 못하도록 막아버리는 거다. 아래 코드를 보면 Game 클래스 정의 가장 아래 부분에 'private :make_quiz, :exam' 코드가 보일 것이다.
이것은 Game 클래스의 인스턴스 메서드인 make_quiz 와 exam 에 대해 해당 객체 외부에서는 호출할 수 없도록 호출 범위를 한정하는 것이다. 즉 두 메서드를 프라이빗하게 만든다.
require 'timeout'
class Game
attr_accessor :time
def initialize(time = 10)
@time = time
@quiz_cnt = 0
@correct_cnt = 0
end
...생략
private :make_quiz, :exam
end
실제 아래 테스트 결과를 보면 Game 객체에 대한 make_quiz 메서드와 exam 메서드 호출이 모두 예외를 발생시킨 것을 볼 수 있다. 예외 메시지를 보면 'private method' 를 호출했다고 나온다.
>> require './game'
=> true
>> game = Game.new(20)
=> #<Game:0x000001574a93a7e0 @time=20, @quiz_cnt=0, @correct_cnt=0>
>> game.time
=> 20
>> game.make_quiz
NoMethodError (private method `make_quiz` called for #<Game:0x000001574a93a7e0 @time=20, @quiz_cnt=0, @correct_cnt=0>)
>> game.exam
NoMethodError (private method `exam` called for #<Game:0x000001574a93a7e0 @time=20, @quiz_cnt=0, @correct_cnt=0>)
다음 글에서는 구구단뿐만 아니라 덧셈이나 뺄셈 문제도 풀어볼 수 있게 코드를 개선해 보자.
See you again~~