본문 바로가기

001Project

001프로젝트 / '이벤트'방식으로 레이싱카 구현하기

'이벤트'방식으로 레이싱카 구현하기

001 Project?
001 Project란 어떠한 과제가 있을 때 그의 기초 단계인 0.0.1v을 만들어 보는 개념입니다.
이벤트 방식의 기초적인 부분을 다루고 있습니다.
모든 코드는 github에 있습니다.

 

이번 포스팅에서는 간단한 콘솔 어플리케이션을 이벤트 방식으로 구현해보면서 '이벤트'에 대해 간단하게나마 이해하는 시간을 가져보겠습니다.

간단한 콘솔 어플리케이션을 구현하자🕹

구현하려는 어플리케이션은 다음과 같습니다.

레이싱카 구현하기
- 게임에 참여하는 Car를 입력한다.
- 매 라운드마다 참여하는 Car들 각자가 car.move(int count)를 통해 position을 이동한다.
- 한 라운드가 끝나면 현재 1등을 콘솔에 찍는다.
- 모든 라운드 끝나면 최종 1등과 함께 수여식을 콘솔에 찍는다.

어플리케이션을 완성한 상태라면 다음과 같은 콘솔 창을 확인할 수 있습니다.

 

 

어떻게? 이벤트 방식으로!🎃

이벤트?

javaScript를 사용해 본 경험이 있다면 아마 event를 많이 사용해 보셨을 겁니다.

$deleteButton.addEventListener("click", deleteItem)

위의 코드는 삭제 버튼을 "click"하면 아이템을 삭제한다는 의미입니다. 즉 'deleteButton이 클릭되는 이벤트가 발생하면 그에 대한 반응으로 아이템을 삭제하겠다' 로 해석할 수 있습니다.

이벤트가 발생한다는 것은 상태가 변경되었다는 것을 의미합니다.

보통은 거기서 끝나지 않고 그에 반응하여 수행할 기능을 구현하는 경우가 많습니다. 만약 '주문 취소 이벤트가 발생'하였다면 그에 반응하여 '환불' 로직이 수행되어야 하고 뿐만 아니라 경우에 따라서는 '문자 발송' 등 여타 다른 로직이 수행될 수 있습니다.

하지만 이런 모든 과정을 하나의 트랜잭션에서 관리하기에는 문제가 조금 있어보이네요.

 

주문 취소, 환불, 알림의 로직이 하나의 메서드에 모두 있다는 점!
환불이나 알림의 기능이 수정된다면 주문 관련 클래스에도 영향을 줄 수 있다는 점!
이후 수행할 로직이 추가된다면 해당 메서드가 더욱 커질 수 있다는 점...
...😨

 

위의 문제들은 모두 bounded context 간의 강결합 때문에 발생하게 됩니다. 그러나!

이벤트 방식을 사용한다면 이러한 강결합을 없앨 수 있습니다.

주문 취소 로직에서는 주문을 취소시키고 주문이 취소되었다는 이벤트만 발생시키는 일만을 합니다. 그러면 주문 취소 이벤트를 처리하는 곳에서 나머지 결제 관련 로직이나 알림 로직을 수행하는 방법입니다.

(말이 쉽지...) 사실 글로만 본다면 잘 안와닿을 수 있습니다.

그래서! 이번 포스팅에서는 '레이킹카'를 이벤트 방식으로 구현해보면서 이벤트를 발행하고 이를 처리하는 일련을 로직을 이해해보겠습니다.

구현 시작!⌨️

먼저 뼈대를 한번 잡아보죠.
Car 객체 3개를 만들고 RacingGame에 참여시킵니다.
그리고 1라운드에서는 car1 객체만 한칸 이동 시키겠습니다. 그러면 현재 1등은 car1이 되겠죠?
코드로 살펴보면 다음과 같습니다.

public class Car {
    private String name;
    private Integer position;

    private Car(String name, Integer position) {
        this.name = name;
        this.position = position;
    }

    public static Car of(String name) {
        return new Car(name, 0);
    }

    public void move(int count) {
        for (int i = 0; i < count; i++) {
            this.position++;
        }
    }
}

public class RacingGame {
    private final List<Car> players;

    public RacingGame(List<Car> players) {
        this.players = new ArrayList<>(players);
    }

    public void completeRound(int round) throws InterruptedException {
    //스레드 슬립은 이후 비동기를 위한 준비이니 혹시 코드를 따라 치신다면 생략하지 말아주세요
        Thread.sleep(1000);
        System.out.println(round + " 라운드 진행완료");
    }
}

public class OrdincodeApplication {
    public static void main(String[] args) throws InterruptedException {
        Car car1 = Car.of("car1");
        Car car2 = Car.of("car2");
        Car car3 = Car.of("car3");
        List<Car> players = Arrays.asList(
                car1,
                car2,
                car3
        );

        RacingGame racingGame = new RacingGame(players);

        car1.move(1);
        racingGame.completeRound(1);
    }
}

이제 우리는 하나의 라운드가 끝나고 현재 1등을 콘솔에 찍는 로직을 추가 하고 싶습니다.
이 로직을 racingGame.completeRound() 에 추가하는 방법도 있겠지만, racingGame.completeRound() 에서는 라운드 하나가 완료되었다는 이벤트만 발생시키고 콘솔에 1등을 찍는 일은 이벤트를 처리하는 EventHandler에서 수행하도록 해보겠습니다.

EventHandler?

잠시 EventHandler를 포함한 이벤트를 처리하는 구성 요소 3가지에 대해서 정리하고 가겠습니다.

- 이벤트 생성 주체
- 생성된 이벤트를 처리하는 EventHandler
- 위 둘을 이어주는 EventDispatcher

여기서 이벤트 생성주체는 라운드가 끝났다는 것을 알려주는 RacingGame이 되는 것입니다.
그러면 이제 EventHandler를 구현할텐데 그전에 무엇들이 필요한지 먼저 정리하고 넘어가는 것이 좋겠네요.

필요한 것들
1. 라운드가 끝났다는 이벤트 객체
2. 라운드가 끝났다는 이벤트 객체를 처리할 EeventHandler
3. 둘을 이어줄 Dispatcher

하나씩 구현해보죠.

1. 라운드가 끝났다는 이벤트 객체

public class RoundCompletedEvent {
    private final Car first;
    private final int round;

    public RoundCompletedEvent(Car first, int round) {
        this.first = first;
        this.round = round;
    }

    public Car getFirst() {
        return first;
    }

    public int getRound() {
        return round;
    }
}

이 객체는 현재 1등 차량과 라운드 정보를 가지고 있습니다.

2. 라운드가 끝났다는 이벤트 객체를 처리할 EventHandler

모든 EventHandler는 아래 인터페이스를 구현합니다.

public interface EventHandler<T> {
    void handle(T event);

    default boolean canHandle(Object event) {
        Class<?> typeArgs = TypeResolver.resolveRawArgument(EventHandler.class, this.getClass());
        return typeArgs.isAssignableFrom(event.getClass());
    }
}

여기서 canHandler의 TypeResolver는 typetools 라이브러리를 사용합니다.

compile group: 'net.jodah', name: 'typetools', version: '0.6.2'
public class RoundCompletedHandler implements EventHandler<RoundCompletedEvent> {
    private final BroadCaster broadCaster = new BroadCaster();

    @Override
    public void handle(RoundCompletedEvent event) {
        try {
         //스레드 슬립은 이후 비동기를 위한 준비이니 혹시 코드를 따라 치신다면 생략하지 말아주세요
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        broadCaster.broadcastWhileRacing(event.getRound(), event.getFirst());
    }
}
public class BroadCaster {
    public void broadcastWhileRacing(int round, Car firstCar) {
        System.out.println(
                String.format("%d 라운드: %s 이/가 선두입니다", round, firstCar.getName())
        );
    }

    public void broadcastGameResult(Car winner) {
        System.out.println(
                String.format("최종 우승자는 %s 입니다", winner.getName())
        );
    }
}

이로써 라운드 종료 이벤트를 처리할 RoundCompletedHandler.java 를 생성했습니다.
이제 roundCompletedHandler.handle(Event event)를 호출하게될 Dispatcher를 구현하도록 하겠습니다.

dispatcher에서는 이벤트 핸들러 목록을 가지고 있고 이벤트가 생성될 때마다 이벤트 핸들러 목록을 순회하면서 canHandler로 적합한 핸들러를 찾아 handle() 메서드를 실행하는 역할을 합니다.

3. 둘을 이어줄 Dispatcher

public class EventDispatcher {
    private static HandlerGroup handlers = new HandlerGroup();

    public static void raise(Object event) {
        List<EventHandler<?>> eventHandlers = handlers.getHandlers();
        for (EventHandler eventHandler : eventHandlers) {
            if (eventHandler.canHandle(event)) {
                eventHandler.handle(event);
            }
        }
    }
}
public class HandlerGroup {
    private final List<EventHandler<?>> eventHandlers;

  //현재 하나의 이벤트 핸드러만 존재합니다만 추가될 예정입니다.
    public HandlerGroup() {
        List<EventHandler<?>> eventHandlers = new ArrayList<>();
        eventHandlers.add(new RoundCompletedHandler());
        this.eventHandlers = eventHandlers;
    }

    public List<EventHandler<?>> getHandlers() {
        return eventHandlers;
    }
}

이렇게 모든 준비가 되었으니 이제 이벤트를 생성해보겠습니다.

RacingGame.java

public void completeRound(int round) throws InterruptedException {
    Thread.sleep(1000);
    System.out.println(round + " 라운드 진행완료");

  //추가된 코드
  //이벤트 생성
    EventDispatcher.raise(new RoundCompletedEvent(findFirst(), round));
}

public Car findFirst() {
  return this.players
    .stream()
    .reduce((Car::findFirst))
    .orElseThrow(IllegalArgumentException::new);
}

여기까지 구현하고 실행하면 다음과 같은 콘솔 창을 확인할 수 있습니다.

 

 

이제 몇 라운드만 추가하고 결과를 출력하는 racingGame.finish() 까지 추가하겠습니다.

main

public static void main(String[] args) throws InterruptedException {
  ...

  car1.move(1);
  racingGame.completeRound(1);

  car2.move(2);
  racingGame.completeRound(2);

  car3.move(3);
  racingGame.completeRound(3);

  car1.move(4);
  racingGame.completeRound(4);

  car2.move(5);
  racingGame.completeRound(5);
  racingGame.finish();
}
public class RacingGame {
...

    public void finish() {
        System.out.println("게임종료");
        EventDispatcher.raise(new GameFinishedEvent(findFirst()));
    }
}
public class GameFinishedEvent {
    private final Car first;

    public GameFinishedEvent(Car players) {
        this.first = players;
    }

    public Car getFirst() {
        return first;
    }
}

GameFinishedEvent를 처리할 Handler

public class GameFinishedHandler implements EventHandler<GameFinishedEvent> {
    private final BroadCaster broadCaster = new BroadCaster();
    private final Awards awards = new Awards();

    @Override
    public void handle(GameFinishedEvent event) {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

      //마지막 1등을 발표하는 로직과
      //수상 로직
        broadCaster.broadcastGameResult(event.getFirst());
        awards.givePrize(event.getFirst());
    }
}

GameFinishedHandler에는 1등 발표 로직과 수상 로직이 있습니다. 만약 이벤트 방식으로 구현하지 않았다면 RacingGame.finish()에 포함되어 유지보수를 어렵게하는 코드가 됐을 것입니다.

public class Awards {
    private final static int PRIZE = 10000;

    public void givePrize(Car winner) {
        System.out.println(
                String.format("%s 에게 %d 원을 수여하였습니다.", winner.getName(), PRIZE)
        );
    }
}

HandlerGroup에 추가

    public HandlerGroup() {
        List<EventHandler<?>> eventHandlers = new ArrayList<>();
        eventHandlers.add(new RoundCompletedHandler());
        eventHandlers.add(new GameFinishedHandler());
        this.eventHandlers = eventHandlers;
    }

이제 다시 어플리케이션을 실행해보겠습니다.

 

 

잘 동작하네요. 그런데 너무 느립니다. 자세히 보니 처음 보여드린 이미지와도 다른 모습이네요.
이유는 처음 보여드린 실행 이미지는 비동기 방식으로 진행했기 때문입니다.
지금 어플리케이션을 뜯어보면 크게 두가지로 나눌 수 있습니다.

  • RacingGame에서 차량들이 움직이는 기능
  • 라운드 혹은 게임이 종료될 때 이를 중계하는 기능

따지고 보면 중계하는 동안 레이싱게임이 멈출이유가 없습니다. 오히려 중계는 조금 늦을지라도 게임은 멈추지않고 계속 진행되는 것이 맞는 이야기입니다.
그래서 이번에는 게임은 계속 진행되도록 하고 이벤트를 처리하는 로직을 비동기로 수행하도록 수정해보도록 하겠습니다.

비동기 방식으로🚀

여기서는 비동기 방식으로 처리하기 위해 ThreadPoolExecutor를 사용합니다. 다만 이번 포스팅의 목적이 ThreadPoolExecutor가 아닌 만큼 ThreadPoolExecutor에 대한 자세한 설명은 생략하도록 하겠습니다. 만약 ThreadPoolExecutor에 대해 잘 모르신다면 다른 글을 참고 부탁드리겠습니다.

ThreadPoolExecutor를 다음과 같이 구현합니다.

public class EventThreadPoolExecutor {
    public static final int MAX_CORE = Runtime.getRuntime().availableProcessors();

    private final ExecutorService threadPoolExecutor;

    public EventThreadPoolExecutor() {
        this.threadPoolExecutor = Executors.newFixedThreadPool(MAX_CORE);
    }

    public void submit(Runnable runnable) {
        threadPoolExecutor.submit(() -> execute(runnable));
    }

    private void execute(Runnable runnable) {
        runnable.run();
    }
}

그리고 EventDispatcher에서 event를 실행할 때 EventThreadPoolExecutor를 사용하도록 수정합니다.

public class EventDispatcher {
    private static final HandlerGroup handlers = new HandlerGroup();
  //추가된 부분
    private static final EventThreadPoolExecutor executor = new EventThreadPoolExecutor();

    public static void raise(Object event) {
        List<EventHandler<?>> eventHandlers = handlers.getHandlers();
        for (EventHandler eventHandler : eventHandlers) {
            if (eventHandler.canHandle(event)) {
              //수정된 부분
                executor.submit(() -> eventHandler.handle(event));
            }
        }
    }
}

그럼 다시 한번 실행해보면 위와 같은 실행 화면을 볼 수 있습니다.

끝으로..

이것으로 이벤트 방식으로 레이싱카 구현을 마치도록 하겠습니다.
비동기 방식으로 이벤트 처리를 하는 방법에는 여러가지가 있는데 그 부분에 대해서 더 깊이 공부하면 좋을 것 같습니다.
참고 자료로는 최범균님의 'DDD start!' 책이 많이 도움이 되었습니다.
읽어주셔서 감사합니다.

반응형