MVC 웹 프레임워크의 문제점


나는 2014년 10월에 레일즈를 처음 접했고 2년 동안 거의 모든 웹 프로젝트에 레일즈를 사용했다.

그리고 레일즈를 배우면서 루비를 배웠다. 그 때의 나에게 루비는 “파이썬과 비슷한 동적 스크립트 언어” 그 이상도 이하도 아니었다. C나 Java와는 달리 컴파일도 필요없고 변수에 타입도 없다. 딱 이 수준이었다.

C, Java, Python을 조금씩 해봤기 때문에 변수, 반복문, 조건문 이런 기본적인 개념은 금방 익혔고 이 정도만 알아도 레일즈로 개발하는데는 큰 문제가 없었다. 웬만한 필요한 기능은 Gem으로 만들어져 있었고 막히는 부분은 가이드 문서나 튜토리얼 아니면 스택오버플로우를 찾아 봤다. 시간이 좀 지나고나니 웬만한건 레일즈로 만들어낼 수 있게 되었다.

프로젝트의 규모가 조금 커지자 문제가 발생했다. 기존의 기능을 수정하거나 새로운 기능을 추가하는 등 시스템에 변화가 생길 때마다 코드를 짜기가 너무 힘들어졌다. 비즈니스 로직들은 한 눈에 알아보기도 힘들 정도로 여기저기 흩어져 있어서 이 코드가 무슨 코드였는지 파악하기 위해 전체 코드들을 다 살펴봐야 했다. 대체 뭐가 문제였을까?

MVC

레일즈는 MVC 패턴을 적용한 웹 프레임워크이며 View는 사용자와, Model은 데이터베이스와 상호작용하며 Controller는 View와 Model 사이에서 다리 역할을 한다.

레일즈를 처음 접했을 때 한 번씩은 읽어봤을법한 MVC 패턴 설명이다. 레일즈는 완벽한 MVC 프레임워크이다. 모델은 ActiveRecord를 상속받아서 데이터베이스와 쉽게 통신할 수 있고, 뷰는 HTML으로 이루어져있으며 컨트롤러는 모델 객체를 활용하여 특정 기능을 수행한 뒤 사용자에게 뷰를 렌더한다.

그렇다면 우리가 개발할 앱의 비즈니스 로직은 대체 어디에 들어가야할까? Controller일까? 아니면 Model일까? (View는 당연히 아니다)

1. Controller

다음은 Rails Tutorial에 작성된 로그인 코드다. 유저를 데이터베이스에서 찾아서 비밀번호를 검사하는 간단한 비즈니스 로직이 컨트롤러에 들어가있는 것을 볼 수 있다.

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      # Create an error message.
      render 'new'
    end
  end

  def destroy
  end
end

단일 책임 원칙(Single Responsibility Principle)이라는 객체지향 원칙이 있다. 이 원칙을 지키려면 “코드를 변경하는 이유”가 딱 한가지만 있으면 된다. 위 코드는 크게 2가지 이유로 변경될 수 있다.

  1. 로그인 처리 후 렌더링하는 페이지가 바뀔 경우
  2. 로그인 방식이 바뀔 경우(ex. 이메일이 아니라 이름으로 로그인)

컨트롤러에는 고유의 책임이 이미 주어져 있다. 유저로부터 요청을 받아서 적당한 응답을 던져주는 것이 바로 컨트롤러의 책임이다. 이미 책임이 부여된 컨트롤러에 비즈니스 로직을 작성하는 것은 당연히 단일 책임 원칙을 위배하며 따라서 컨트롤러에 비즈니스 로직을 넣는 것은 좋은 방법이 아니다.

2. Model

Skinny controllers, Fat models

레일즈 개발자라면 한 번쯤은 들어봤을만한 문장이다. 컨트롤러는 최대한 가볍게 유지하고, 웬만한 코드는 모델에 넣으라는 뜻이다.

하지만 생각해보면 이것도 틀렸다는 것을 알 수 있다. 컨트롤러는 고유의 책임이 이미 존재해서 비즈니스 로직을 넣을 수 없었다. 그렇다면 모델은 주어진 책임이 없을까? 모델은 ActiveRecord를 상속받음으로써 데이터베이스와 통신할 수 있게되며 이 때 상속받는 메서드만 200개가 훨씬 넘는다고 한다.

즉, 모델은 ActiveRecord를 상속받는 순간 Persistence layer의 책임을 갖게된다. 위의 로그인 코드에서 User.find_by(...)가 바로 모델이 해야하는 일이다. 결국 모델도 비즈니스 로직을 담을만한 곳은 아니다.

문제점?

레일즈에 비즈니스 로직을 넣을만한 곳이 없는 것이 문제일까? 레일즈는 프레임워크다. 프레임워크는 대부분의 일반적인 상황에서 사용할 수 있는 코드를 제공한다. 반면에 비즈니스 로직은 우리가 개발하는 애플리케이션에 specific하다. 따라서 프레임워크에 비즈니스 로직을 넣을 곳이 없는 것은 문제가 되지 않는다. 없으면 만들면 되기 때문이다.

문제는 바로 프레임워크 위에 모든 코드를 작성하게 하는 가이드와 수많은 튜토리얼들, 가장 큰 문제는 그것을 이상하다고 여기지 않고 그대로 받아들였던 나 자신이다. 아마 프레임워크를 통해 언어를 배웠기 때문에 기본기가 부족해서 그러지 않았나 싶다. 뭐든지 기본기가 탄탄해야 한다는 것을 다시 한 번 배우게 되었다.

해결 방법

몇 달 전부터 비즈니스 로직을 프레임워크로부터 분리시키는 방법을 찾으려고 했다. 이전에 번역한 [[번역] 액티브레코드 모델을 리팩토링하는 7가지 방법] 도 그런 의도에서 작성한 것이었다. 몇 가지 종류의 Plain Old Ruby Objects를 작성해서 비즈니스 로직을 분리시킬 수 있다.

그리고 좋은 해결책이 될 수 있는 밥 아저씨의 Clean Architecture 강의 영상을 찾았는데 다음 포스트에서는 이 영상을 다뤄봐야겠다.