Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

AASM 젬으로 Ruby 상태 머신 구현하기: 신호등 예제로 배우는 상태 디자인 패턴

오늘은 상태 머신(state machine)의 개념과 동작 원리를 살펴보고, AASM 젬을 활용해 Ruby 프로젝트에서 상태 머신을 구현하는 방법까지 함께 알아보겠습니다.

신호등으로 이해하는 상태 머신

먼저 신호등을 떠올려 보세요.

신호등은 빨간색, 노란색, 초록색 중 하나의 상태를 가집니다.

그리고 색이 바뀔 때 다음 색상은 언제나 현재 색상에 따라 결정됩니다.

AASM 젬으로 Ruby 상태 머신 구현하기: 신호등 예제로 배우는 상태 디자인 패턴

여기서는 시각장애인이 건널 수 있는 시기를 알 수 있도록 소리를 내는 신호등이라고 가정해 보겠습니다.

이제 여러분이 이 신호등의 소프트웨어를 개발한다고 생각해 봅시다.

매번 어떤 소리를 재생해야 하는지, 다음 색상은 무엇이 되어야 하는지 어떻게 판단할 수 있을까요?

가장 단순한 방법은 아래처럼 if 문을 나열하는 것입니다.

if @light.state == "green"
  @light.play_green_sound
end

if @light.state == "green"
  @light.change_to_yellow
end

# ...

하지만 이런 식으로 작성하면 상태 검사 코드가 프로그램 곳곳에 흩어지게 됩니다!

어떻게 개선할 수 있을까요?

객체 지향 설계 원칙을 충실히 적용하다 보면 자연스럽게 상태(State) 디자인 패턴에 도달하게 됩니다.

상태 디자인 패턴이란?

상태 디자인 패턴은 상태 머신을 구현하는 대표적인 방법 중 하나입니다.

필요한 구성 요소는 3가지입니다:

  • Context(컨텍스트) 클래스 — 현재 상태가 무엇인지 알고 있는 클래스
  • State(상태) 클래스 — 각 개별 상태가 반드시 구현해야 할 메서드를 정의하는 클래스
  • 각 상태마다 하나씩 존재하는 클래스들 — State 클래스를 상속받습니다

신호등 예제에서 컨텍스트는 TrafficLight(신호등) 자체이며,

상태는 Green, Red, Yellow입니다.

모든 상태는 자기가 해야 할 일을 스스로 압니다.

가장 큰 장점은 무엇일까요?

각 상태가 자기 자신을 알고 있기 때문에 현재 상태를 일일이 검사할 필요가 없습니다. 그 결과 조건문이 크게 줄어들고, 복잡도의 주범인 분기 처리를 획기적으로 줄일 수 있습니다.

신호등 구현하기

이제 실제 코드로 이 패턴을 구현해 보겠습니다.

먼저 TrafficLight입니다:

class TrafficLight
  def initialize
    @state = nil
  end

  def next_state(klass = Green)
    @state = klass.new(self)

    @state.beep
    @state.start_timer
  end
end

다음은 기본이 되는 State 클래스입니다:

class State
  def initialize(light)
    @light = light
  end

  def beep
  end

  def next_state
  end

  def start_timer
  end
end

네, 이 3개의 메서드는 비어 있습니다.

Java 같은 다른 언어에서는 이런 식으로 "인터페이스"를 정의하는 것이 일반적이지만, Ruby에서는 그렇게 널리 쓰이지 않습니다.

여기서는 설명을 위해 보여드린 것입니다.

다만 모든 상태가 상태 변경을 알리기 위해 컨텍스트(TrafficLight 객체)가 필요하므로, initialize 메서드는 상태 클래스 간에 공유하고 싶습니다.

이제 3개의 상태 클래스는 서로 매우 비슷하게 생겼으므로, 그중 하나만 보여드리겠습니다.

Green 상태입니다:

class Green < State
  def beep
    puts "Color is now green"
  end

  def next_state
    @light.next_state(Yellow)
  end

  def start_timer
    sleep 5; next_state
  end
end

모든 상태는 다음 상태로 언제, 어떻게 전환해야 하는지 스스로 알고 있습니다.

AI 게임 예제

상태 머신은 현재 상태에 따라 행동이 달라지는 게임 문제를 푸는 데도 활용할 수 있습니다. 대표적인 예가 RubyWarrior입니다.

RubyWarrior에서는 플레이어 객체와 게임 보드가 주어집니다.

목표는 다음과 같습니다:

  • 보드 위의 모든 적을 물리치기
  • HP를 0 이상으로 유지하면서 출구에 도달하기

한 번에 한 번씩 움직일 수 있으며, 레벨을 클리어하려면 매 순간 최선의 선택을 해야 합니다.

바로 이때 현재 상태를 파악하는 것이 좋은 선택의 열쇠가 됩니다.

그렇기 때문에 상태 머신이 훌륭한 해결책이 됩니다.

예제를 보겠습니다:

class Attacking < State
  def play(warrior)
    warrior.attack!

    @player.set_state(Healing) unless enemy_found?(warrior)
  end
end

이것은 전사(warrior)가 가질 수 있는 상태 중 하나입니다. 시야에 적이 더 이상 없으면 전투로 인한 피해를 회복하기 위해 Healing 상태로 전환합니다.

AASM 젬 사용하기

현재 상태를 추적하면서 동시에 유효한 전환만 허용하고 싶다면, AASM 같은 상태 머신 젬을 사용하면 됩니다.

AASM은 이벤트(예: 스위치를 누르는 행위)가 발생하면 다른 상태로 전환된다는 개념을 중심으로 설계되었습니다.

예제입니다:

require 'aasm'

class Light
  include AASM

  aasm do
    state :on, :off

    event :switch do
      transitions :from => :on, :to => :off, :if => :on?
      transitions :from => :off, :to => :on, :if => :off?
    end
  end
end

이 클래스를 사용하는 방법은 다음과 같습니다:

light = Light.new

p light.on?
# true

light.switch

p light.on?
# false

이 상태 머신에서는 현재 상태가 "off"일 때만 "on" 상태로 전환할 수 있습니다. 또한 상태 전환 과정에서 특정 코드를 실행하도록 before/after 등 다양한 콜백을 설정할 수도 있습니다.

이런 콜백으로 활용할 수 있는 예는 다음과 같습니다:

  • 이메일 발송
  • 상태 변경 로그 기록
  • 실시간 모니터링 대시보드 업데이트

더불어 AASM은 ActiveRecord를 통해 현재 상태를 데이터베이스에 저장하는 기능도 제공합니다.

AASM 젬 관련 영상

마무리

지금까지 상태 머신, 상태 디자인 패턴, 그리고 AASM 젬에 대해 배워 보았습니다! 새로운 글과 구독자 전용 Ruby 팁을 놓치지 않으려면 구독자 7,000명이 넘는 Ruby 뉴스레터를 구독해 보세요.

이제 배운 내용을 직접 연습해 볼 차례입니다 🙂

읽어주셔서 감사합니다!