반응형
회사에서 입사해서 3년 반 정도 전에 그 당시 신입들 교육 목적으로 만들었던 평가 시스템이 있다. 한 번 쓰고 버릴 거라 생각해서 급한 불을 끄는 심정으로 만들었었다. 그런데 바로 버릴 것 같았던 이 시스템을 3년이나 쓰게 되었고 이번에도 쓰게 되어서, 기술도 익힐 겸 공부도 할 겸 새롭게 구현해 보았다. 몇 가지를 새로 적용해봤는데, 그 중 중요한 것은 다음과 같다.
- 이벤트 소싱(Event Sourcing)과 CQRS
- 스프링 부트(Spring boot)
- 앵귤러JS(AngularJS)
- 부트스트랩(Bootstrap)
이벤트 소싱(Event Sourcing) 적용
시스템 개편을 한 주된 이유는 이벤트 소싱을 작게나마 적용해보고 싶기 때문이었다. DDD와 CQRS 등을 학습하는 과정에서 이벤트 소싱을 만나게 되었는데, 평소 SQL을 사랑하지 않았던 나로서는 이벤트 소싱과 도메인의 만남이 매우 환상적으로 느껴졌다. (이벤트소싱 자체에 대한 내용이 궁금하다면 http://docs.geteventstore.com/introduction/event-sourcing-basics/ 글을 읽어보자.)
이벤트 소싱은 모든 상태 변경을 이벤트로 보관하고, 모든 이벤트를 차례대로 적용함으로써 최신 상태를 구할 수 있도록 만들어준다. 이런 이벤트 소싱을 도메인에 적용해보니, 보다 객체 지향적인 방법으로 도메인 코드를 구현할 수 있음을 알게 되었다.
그간 JPA와 같은 ORM 프레임워크를 사용해서 도메인 객체의 내부 상태와 DB 테이블 간의 매핑을 처리했었는데, 이는 자유롭게 도메인 코드를 만들지 못하고 테이블 구조에 억지로 맞춰 도메인 코드를 만들도록 유도했다. 예를 들어, 필자의 경우 Map, List, Set, Immutable 객체 등을 자유롭게 쓰고 싶어도, 테이블 구조/쿼리 성능/ORM 한계로 인해 사실상 도메인 코드가 테이블 설계에 영향을 받았다.
이벤트 소싱을 이용해서 도메인 코드를 구현하면, 도메인의 상태 변경 정보를 담은 이벤트 정보만 저장소에 보관하게 된다. 즉, 이벤트와 저장소 사이의 매핑만 처리하면 되기 때문에(예, 이벤트를 HBase에 보관하거나, 이벤트를 RDBMS 테이블에 보관하는 등), 도메인 자체의 코드는 ORM이나 SQL과 같은 데이터 매핑으로부터 자유로워진다. 원하는 클래스를 이용해서 객체 군(Aggregate)을 마음대로 구성할 수 있고, 기능 위주로 객체를 구현하는데 더 집중할 수 있도록 만들어준다. 이는 나에게 굉장한 해방감을 주었다.
이벤트 소싱을 적용하기 위해 Axon Framework를 사용했기 때문에 Axon Framework에 제한된 방식으로 도메인 코드를 구현해야 했지만, JPA나 테이블 구조에 따른 제한에 비하면 아무것도 아니다.
CQRS
이벤트 소싱을 이용해서 도메인을 구현하면, 객체 군 단위로 조회를 하게 된다. 이는 여러 객체 군을 동시에 조회하는 기능을 구현할 때 RDBMS 만큼의 조회 속도를 제공하지 못한다. 예를 들어, 테이블을 사용하면 "select * from PersonalEval e, PerfItem pi, PerfEval ev where ..."와 같이 필요한 데이터만 선택적으로 조회할 수 있는데 반해 이벤트 소싱의 경우는 한 타입의 도메인 객체 군에 해당하는 모든 이벤트 데이터를 로딩해야 하기 때문이다.
이런 문제를 해결하기 위해 CQRS(Command Query Responsiblity Segregation)는 필수다. 상태 변경과 관련된 기능을 제공하는 도메인 모델과 상태 조회를 위한 전용 모델을 구분해서 조회 성능을 높여주어야 한다. 평가 시스템의 경우 '평가 시즌' 도메인 객체는 '평가자 매핑'을 관리하는데, 이 평가자 매핑은 '피평가자-1차 평가자-2차평가자-동료평가자들'로 구성되어 있다.그런데, 1차 평가자 입장에서는 내가 평가해야 할 피평가자 목록이 필요하므로, 조회 시점에서는 '1차 평가자-피평가자목록'이 필요하다. 비슷하게 '2차 평가자-피평가자목록', '동료평가자-피평가자목록'도 조회 시점에 필요하다. 이렇게 조회 시점에 필요한 조회 모델을 별도로 만들어서 조회 성능 문제를 없앴다.
'평가자-피평가자' 매핑 조회 전용 데이터 모델은 도메인 객체가 생성한 이벤트를 이용해서 생성했다. 즉, 도메인 기능 실행 과정에서 발생한 이벤트를 수신해서 그 이벤트로부터 조회 모델의 데이터를 변경하는 방식을 사용했다. 조회 전용 모델을 보관하기 위해 DB를 사용할 수도 있지만, 이 프로젝트의 경우에는 데이터 양이 크지 않아 그냥 메모리에 상태 정보를 보관했다. 어플리케이션이 처음 구동될 때 관련 이벤트를 읽어와 메모리에 최종 상태의 조회 전용 데이터를 생성한 뒤, 이벤트가 발생할 때 마다 메모리의 상태를 수정하는 방식을 사용했다.
낯설고 복잡하게 느껴지지만, 일단 감 잡으면 장점
처음 이벤트 소싱을 공부하고 적용하는 과정은 낯설었다. 만약 SQL만 고집하는 개발자였다면, 심리적으로 저항항하게 될 뿐만 아니라 객체 중심 코드를 만드는데 힘겨워할 것 같다. 또한, CQRS를 처음 만나면 복잡하게 느껴진다. 물론, 최근에는 조회를 위해 데이터를 캐시에 담아두는 곳이 많기 때문에 뷰 전용 모델을 만드는 것에 대한 거부감이 이번보다 덜 하지만, 상태를 변경할 때 사용하는 모델과 조회할 때 사용하는 모델이 다르기 때문에 복잡하게 느껴진다.
하지만, 도메인 모델이 조금만 커져도, 상태 변경을 위한 SQL은 복잡해지는 반면에 객체 기반 코드는 상대적으로 단순해졌다. 새로운 기능을 추가한다거나 로직이 변경되더라도 ERD를 변경할 필요 없이 도메인 코드만 수정하면 되므로 변경에 유연하게 대처할 수 있게 된다. 또한, 조회 전용 모델도 얼마든지 원하는 방식으로 만들어낼 수 있기 때문에 조회를 위한 모델의 자유도도 높아졌다.
스프링 부트(Spring Boot)
작년부터 뜨고 있는 스프링 부트를 적용했다. 사용한 이유는 다음과 같다.
- 비교적 단순해진 의존 설정
- 임베디드 톰캣을 이용해서 실행 가능한 war를 배포해서 실행 과정을 단순화
위 두 가지는 확실히 장점이었다. 기본적인 설정을 모두 스프링 부트가 알아서 먹어주기 때문에, 필요한 설정들만 추가해 주면 되었다. 실행 가능한 war는 배포에서의 장점이었다. war를 배포하고 java 명령어를 이용해서 실행해주기만 하면 끝났다. 톰캣의 catalina.sh과 같은 쉘 파일을 만들어주긴 했지만 war를 바로 실행할 수 있기 때문에 배포와 실행이 매우 간단해졌다.
물론, 단점도 있다. 스프링 부트가 너무나 많은 걸 내부적으로 설정해버리기 때문에, 기본 설정에서 벗어나기 위해 부트의 내부를 이해해야 하는 경우도 있었다. 부트에 대한 이해도가 떨어지면, 처음에 설정과 관련해서 삽질할 가능성이 높아진다.
뷰 구현 기술로는 JSP를 사용했는데, 이유는 다른 구현 뷰 구현 기술이 익숙하지 않았기 때문이다. 다음에 임베디드 서버를 이용해서 실행 가능한 war를 만들 때에는 타임리프(thymeleaf)를 사용해보는 것도 고려하고 있다.
앵귤러JS와 부트스트랩
부트스트랩은 "사랑"이다. 디자인 감각이 없는 나에게 부트스트랩은 없어서는 안 될 존재이다. 이번 시스템의 경우도 부트스트랩을 사용한 덕에 디자인이 깔끔해졌다는 평을 들을 수 있었다. 직접 CSS를 만지작거렸다면 아마 무엇을 상상해도 그 이하의 디자인이 나왔을것이다.
프론트 기술이 약한 나에게 앵귤러JS도 '사랑'이다. 앵귤러JS 2 버전이 올해 나올 예정이지만 아직 알파 버전이기 때문에 앵귤러 JS 1을 사용했다. 앵귤러 JS가 없었다면 jQuery와 자바 복잡한 스크립트 코드를 만드느라 멘붕을 수도 없이 겪었을지 모른다. 앵귤러 JS 덕에 폼에 입력한 값을 쉽게 처리할 수 있었고, 서버와의 연동도 간단하게 할 할 수 있었다.
REST API를 연동하는 부분에서는 $q를 사용하면서 Promise에 대해 약간의 감을 잡을 수 있었다.
이 외에 앵귤러 JS의 다음 기능들을 사용했다.
- 폼 값 검증
- angular-elastic : textarea의 높이를 입력 값에 따라 자동으로 조절해주는 모듈듈
- angular-ui-bootstrap : 부트스트랩의 모달 다이얼로그를 앵귤러 컨트롤러로 처리하기 위해 사용
예제 코드
딱히 코드가 아름답진 않지만, 학습 과정에서 작성한 코드를 https://github.com/madvirus/evaluation 에서 확인할 수 있다.