Flux는 MVC모델의 단점을 보완하기 위해 페이스북에서 고안한 패턴으로, 웹개발 환경에서 먼저 쓰여지기 시작하였습니다. 2014년에 페이스북에서 Flux를 발표하였는데, 발표 후 많은 웹개발자들의 관심과 함께 기술적인 논의도 활발히 이루어졌습니다.
저는 이것이 무엇이길래 다들 이렇게 주목하는가 궁금했습니다. 그리고 이것이 패턴이라면, 안드로이드에도 적용하여 구조개선을 이룰 수 있지 않을까, 기대감을 품고 학습을 시작하였습니다. 결론을 먼저 말씀드리면, 기대했던만큼 좋은 패턴은 아니라고 생각합니다. 단점도 있고, 새로운 패턴이라고 하기에 모호한 부분이 있습니다. 하지만 MVC, MVP 등의 전통 패턴과는 다른 관점으로 생각할 수 있도록 안내하고, 어떠한 경우에 있어서는 개발을 하기 쉽도록 도와줍니다.
이제부터 1. Flux가 무엇인지, 2. 어떻게 안드로이드에 적용하는지, 3. 어떠한 경우에 실제로 사용하기에 적절한지, 그리고 4. 정말 Flux가 좋은지에 대해 리멤버에 적용하면서 배웠던 것들을 공유하겠습니다.
Flux 알아보기
Flux를 짧게 설명하면, 데이터를 단방향으로 흐르도록 하여, 데이터만 바라보며 이밴트를 다룰 수 있도록 고안한 구조입니다. 이를 위해서 View 외에 Action, Dispatcher, Store가 존재합니다. Flux에 대한 자세한 설명은 링크를 참조해 주세요.
안드로이드에 Flux 적용하기
그러면 Flux를 어떻게 안드로이드에 적용할 수 있을까요? Github에 Flux 구조로 만든 간단한 예시 프로젝트가 있습니다. 이것을 참조하시면, Flux를 안드로이드에 적용하는 방법 뿐만 아니라, 위에서 설명한 Action, Dispatcher, Store, View의 관계를 좀더 구체적으로 이해할 수 있습니다.
Action은 어느 뷰에서나 생성될 수 있습니다. Dispatcher는 생성된 Action을 스토어에 보내고, 스토어는 Action을 수행 후 다시 Dispatcher를 통해 View에게 자신의 데이터가 갱신되었음을 알립니다. 이 일련의 과정에서 Action을 전역으로 전달하는 Dispatcher는 다양한 방법으로 구현할 수 있습니다. 위 프로젝트에서는 Bus라는 클래스로 직접 구현하였습니다. 하지만 이 글을 읽으시는 분들께서 만약 싱글톤을 이용한 전역 이밴트 관리가 익숙하지 않으시다면, 유명한 라이브러리인 이밴트 버스를 사용해서 Dispatcher를 대체할 수 있습니다.
리멤버에서 Flux 사용하기
리멤버에서는 명함 교환방 기능에서 Flux를 사용하였습니다. 명함 교환방은 오프라인 행사에서 여러명이 명함을 쉽게 온라인으로 교환할 수 있도록 돕는 기능입니다. 이 때 중요한 것은 ‘교환방’ 데이터 입니다. 명함 교환방은 교환방에 들어온 후에 시작되는 기능이기 때문에, 교환방에서 일어나는 대부분의 이밴트는 교환방 데이터에 의존합니다. 그리고 교환방의 데이터를 변화시킵니다. 다양한 뷰와 모델에서 하나의 데이터에 접근하고, 건드리는 것입니다.
처음에는 평소와 같이 이밴트에 집중하여 교환방 데이터를 갱신하였습니다. 하지만 교환방 안에서 이루어지는 기능들이 많아지면서 데이터가 흐르는 방향이 급격히 늘어났습니다. 그리고 내가 지금 있는 화면에서 ‘교환방’ 데이터가 어떤 경우로 변화되는지, 예상하기 힘든 핑퐁이 이루어지게 되었습니다. 대략 아래의 코드와 같습니다.
private void getRoom() { mRoomHelper.room(roomId).subscribe(room -> { // Room을 받고 뷰를 갱신한다. mRoom = room; updateToolbar(); }); } private void editRoom() { mRoomHelper.edit(room.getId(), inputRoomName.getText().toString()) .subscribe(room -> { ToastUtil.showShort(mContext, R.string.room_edit_success); // Room을 변경하고 뷰를 갱신한다. mRoom = room; updateToolbar(); }); } public void getMembers() { mRoomMemberHelper.exchangeMembers(room.getId()).subscribe(members -> { mAdapter.refresh(members); boxEmpty.setVisibility(members.isEmpty()? View.VISIBLE : View.GONE); // 맴버를 새로고침 할 때마다 Room의 Count를 변경하고 뷰를 갱신한다. mRoom.setMemberCount(members.size()); updateToolbar(); }); } . . // 그외 Room과 소통하는 많은 이밴트들이 있다. . . // 뷰를 갱신한다. private void updateToolbar() { mActivityFrag.setToolbarTitle(mRoom.getName()); mActivityFrag.setToolbarSubTitle(getString(R.string.word_participant), mRoom.getMemberCount())); mActivityFrag.showTabLayout(); }
이밴트와 함께 뷰를 갱신하는 코드가 반복됩니다. 이것은 여러곳에서 Room을 접근하도록 만들어, 여러 비동기 작업이 진행될 때 안전하지 못하도록 만듭니다. 그리고 위 코드는 편의를 위해 이밴트들을 하나의 클래스에 모았기 때문에 그나마 보기가 편합니다. 하지만 실제로는 여러 화면에서 따로 이밴트가 일어나기 때문에, 이밴트로부터 뷰를 갱신하기까지 이어지는 로직의 복잡도가 매우 높습니다.
그래서 데이터를 기반으로 바라보는 Flux를 도입해 보았습니다. 결과는 좋았습니다. 어디에서 무슨 작업이 일어나든, 단순하게 생각하며 ‘교환방’ 데이터를 갱신해주니, 뷰에서도 햇갈리지 않고 잘 최신화된 결과를 보여줄 수 있었습니다.
private void getRoom() { mRoomHelper.room(mRoomId).subscribe(room -> { // 받아온 Room으로 Store를 갱신하는 Action을 생성한다. // (RxBus는 제가 구현한 Dispatcher 입니다.) RxBus.get().send(new RxEvent<>(RoomStore.REFRESH_ROOM_STACK, room)); }); } // Store는 싱글톤으로 전역적으로 데이터를 관리한다. (여기서 싱글톤 코드는 생략하였습니다.) // Action을 받아 데이터를 변경하고, 뷰에게 갱신 알림을 보낸다. public class RoomStore { public static final int SYNC_ROOM_STACK = 100; public static final int REFRESH_ROOM_STACK = 101; public void setEvent() { Subject<RxEvent, RxEvent> subject = RxBus.createSubject(); subject.subscribe(event -> { switch (event.getCode()) { // Refresh Room Action을 받는다. case REFRESH_ROOM_STACK: // Store에 저장된 Room을 갱신한다. mRoom = (Room) event.getParam(); // Room이 갱신되었음을 뷰에게 알린다. RxBus.get().send(new RxEvent(SYNC_ROOM_STACK)); break; } }, Throwable::printStackTrace); RxBus.get().register(this, subject); } } // Room이 변화되었다는 Action이 잡히면, 뷰를 갱신한다. private void setEvent() { Subject<RxEvent, RxEvent> subject = RxBus.createSubject(); subject.subscribe(event -> { switch (event.getCode()) { // Room 갱신 Action을 잡는다. case RoomStore.SYNC_ROOM_STACK: // 뷰를 업데이트 한다. updateToolbar(); break; } }, Throwable::printStackTrace); RxBus.get().register(this, subject); }
위 코드에서는 View가 컨트롤러 이밴트에 종속되지 않고, 참조하는 Room 데이터만 바라보고 있습니다. Action에 관계없이 View는 데이터가 변화한 당시의 최신 상태로 업데이트 됩니다. 데이터의 최신상태만을 참조하기 때문에, 여러 비동기 작업 중에도 안전하게 적절한 데이터를 보여줄 수 있습니다. 그리고 비즈니스 로직, 데이터 관리, 뷰 갱신 로직을 분리해, Action을 보내는 것 외에 중복을 제거합니다.
Flux는 정말 좋은가?
페이스북은 Flux를 주로 MVC와 비교하였습니다. 발표에서는 MVC가 복잡도의 원인이며, 유연성을 낮추는 악의 근원으로 설명됩니다. 하지만 저는 Flux를 적용하면서, MVC에 Dispatcher를 섞어 Action 을 받아주도록 구현한다면, 유사할 것이라고 생각하였습니다.
위에 MVC로 짠 교환방을 Action 기반으로 구현하면 다음과 같습니다.
private void getRoom() { mRoomHelper.room(roomId).subscribe(room -> { // Dispatcher가 Room이 바뀌었다는 Action을 발행한다. RxBus.get().send(new RxEvent<>(RoomStore.CHANGE_ROOM, room)); }); } private void editRoom() { mRoomHelper.edit(room.getId(), inputRoomName.getText().toString()) .subscribe(room -> { ToastUtil.showShort(mContext, R.string.room_edit_success); // Dispatcher가 Room이 바뀌었다는 Action을 발행한다. RxBus.get().send(new RxEvent<>(RoomStore.CHANGE_ROOM, room)); }); } public void getMembers() { mRoomMemberHelper.exchangeMembers(room.getId()).subscribe(members -> { mAdapter.refresh(members); boxEmpty.setVisibility(members.isEmpty()? View.VISIBLE : View.GONE); // Dispatcher가 Room이 바뀌었다는 Action을 발행한다. mRoom.setMemberCount(members.size()); RxBus.get().send(new RxEvent<>(RoomStore.CHANGE_ROOM, mRoom)); }); } // 컨트롤러에서 Dispatcher를 세팅한다. private void setEvent() { Subject<RxEvent, RxEvent> subject = RxBus.createSubject(); subject.subscribe(event -> { switch (event.getCode()) { // Room 변경 Action을 받아 데이터와 뷰를 갱신한다. // 데이터 갱신은 원래 스토어에서 행하던 일이지만, Action 기반으로 하면 컨트롤러에서 한꺼번에 처리가 가능하다. case RxCode.CHANGE_ROOM: mRoom = (Room) event.getParam(); updateToolbar(); break; } }); RxBus.get().register(this, subject); }
위에서는 이밴트 후 공통적인 작업을 Action으로 묶어 보내어, 컨트롤러의 한 곳에서 처리를 합니다. 이것은 Store에서 수행하는 데이터 관리를 Action에 따라 컨트롤러에서 진행한다고 생각하면, Flux와 유사한 구조입니다. 결국 Flux는 MVC에서 Store로 한번 데이터 레이어를 감싸준 것 뿐인데, 굳이 새로운 이름을 붙이고 패턴이라 자칭하면서, 복잡도를 늘린 것으로 볼 수도 있습니다.
Flux의 장단점
장점
- Flux는 데이터가 한 방향으로 흐르므로, 이밴트를 다룰 때 고려해야 할 경우의 수를 줄일 수 있습니다. 이밴트가 일어나면 어떤 액션으로 분류되는지만 생각합니다. Store에서는 액션에 따라 데이터를 바꾸는 것만 바라봅니다. View는 데이터가 변할 때 어떻게 다시 그려야 하는지만 집중합니다. 이벤트, 데이터 갱신, 뷰 갱신에 이르는 과정을 독립적으로 관리할 수 있게 되는 것입니다.
- 데이터가 싱글톤 Store에 전역으로 관리되므로, 어디서든 쉽게 접근할 수 있습니다.
단점
- 글로벌하게 Action을 정의, 관리해야 하므로, 코드 가독성이 낮습니다.
- 이밴트를 바로 모델과 뷰에 적용하는 방식에 비해 직관적이지 않아, 학습비용이 필요합니다.
- Dispatcher를 섞어 MVC를 구현하면, 결국 Flux와 비슷합니다. Flux라는 새로운 이름으로 인해 더 복잡해졌을 뿐입니다.
결론
처음에는 Flux가 페이스북이 홍보한 것처럼 기존의 구조를 능가하는 새로운 개념이기를 기대하였습니다. 하지만 바랬던 만큼의 만능은 아니였습니다.
저는 Flux가 기존의 MVC, MVP와 대응되는 개념이라기 보다는, 데이터를 기반으로 구조를 바라볼 수 있도록 돕는 관점의 하나라고 보았습니다. 그래서 ‘교환방’ 이라는 다양한 곳에서 접근해야 하고, 여러 뷰에 영향을 주는 데이터가 있는 명함 교환방 기능에서 Flux를 활용하였습니다. 그리고 교환방에서 일어나는 뷰와 모델의 핑퐁을 줄이면서, 더 예상 가능한 안정적인 코드를 짤 수 있었습니다.
어느 패턴이든 학습비용이 존재하며, 과도하게 사용될 경우 코드의 접근성과 가독성을 낮출 위험이 있습니다. 하지만 잘 활용한다면, 코드의 안정성와 유연성을 매우 높일 수 있습니다. Flux를 통해 저는 다르게 모듈을 설계할 수 있는 시각을 배웠고, 실제로 적용하면서 성과를 볼 수 있어 좋았습니다.
감사합니다. 좋은 글 잘 읽었습니다. React + Flux로 개발중인데, 개념적으로 부족하던 부분을 채울수 있었습니다! 특히 React 외에 다른 System에 적용하는 부분이 많이 인상적이었네요
개인적으로 Flux의 가장 큰 장점은 뷰와 모델의 핑퐁이 줄어드는 부분이라 생각합니다. 작성자 부분이 말씀하신것 처럼 복잡한 시스템에서 Event의 logic을 따라가다보면 어느 순간 이 function이 저 function같고 이 function이 뭘 하려던 function이었지? 라는 생각에 다시 처음부터 Logic 을 따라가야하는경우도 많이 발생했습니다.
오히려 이러한 경우에는 전체 시스템의 관점에서 봤을때에는 가독성이 떨어지지만, 특정 Event의 logic을 파악하는 것에는 오히려 가독성이 증가할수도 볼수 있지 않을까요?
잘 읽으셨다니 기쁘네요 🙂 도움이 되었길 바랍니다. 말씀하신 부분이 맞습니다. 이벤트의 존재를 모른 상태에서 코드를 읽어가기에는 자연스럽지 않은 부분이 있지만, 인지 후에는 뷰모델의 핑퐁보다 훨씬 자연스러운 흐름으로 코드를 읽을 수 있습니다. 그 사이의 간격을 주석 또는 문서로 잘 채워준다면 단점을 더 보완할 수 있겠네요 ^^ 좋은 질문 감사합니다.