본문 바로가기

소프트웨어-이야기/프로그래밍 언어와 프레임워크

레일즈에 Service/Decorator Layer 적용하기 (3) - Service Object

이 포스팅은 Build Sleek Rails Components With Plain Old Ruby Objects을 정리한 글입니다 :)

레일즈에 Service/Decorator Layer 적용하기 (2) - Value Object이 길어져서, 두번째 글을 정리해보고자 합니다.


Service Object 추출하기

Service Object는 비즈니스 로직의 일부를 옮긴 클래스입니다.


Skiny Controller Fat Model 스타일에서는, 한 객체에 여러 비즈니스 로직이 포함되게 됩니다. 

반면, Service Object를 사용하면 여러 클래스가 생성되고, 각 클래스는 하나의 목적을 위해 존재하게 됩니다. 



왜 Service Object를 사용해야할까요?


1. 디커플링

서비스 객체를 사용하면 객체 간에 독립성을 유지하는 데에 도움이 됩니다.


2. 가독성

이름이 잘 지어진 서비스 객체를 사용하면 어플리케이션이 무슨 일을 하는 건지 찾기 쉽습니다. 서비스 디렉토리만 보면, 이 애플리케이션이 어떤 역할을 하는지 쉽게 알아차릴 수 있습니다.


3. 모델과 컨트롤러를 깔끔하게 만듭니다

서비스 객체를 사용하게 되면, 컨트롤러는 request ( params, session, cookie )를 서비스 객체의 매개변수로 넘기게 됩니다. 그리고 서비스 객체에서 응답한 값에 따라 렌더링 혹은 리다이렉트 처리를 합니다. 그리고 모델은 association과 persistence만 처리하게 됩니다.

컨트롤러와 모델에서 서비스 객체를 추출하는건 단일책임원칙을 지킬 수 있게 해주고, 코드의 커플링을 줄여줍니다. 그리고 이는 더 좋은 설계와 더 좋은 유닛테스트를 만들 수 있게 해줍니다. 


4. 코드를 DRY하게 만들어주고, 유연한 시스템을 만들 수 있게 해줍니다.

서비스 객체는 작고, 간단하게 구성되어 있습니다. 그래서 다른 서비스 객체를 만들 때에도 재사용할 수 있습니다.


5. 테스트 케이스를 빠르고, 깔끔하게 만들어줍니다.

서비스 클래스는 작은 루비 클래스로 구성되어있기 때문에, 쉽고 빠르게 테스트코드를 작성할 수 있습니다.

여러 서비스로 구성된 복잡한 서비스는, 테스트를 손쉽게 테스트를 잘게 쪼갤 수 있습니다.

그리고 서비스 객체를 사용하면 쉽게 mock처리를 할 수 있어서, 테스트 시, 전체 레일즈 환경을 읽어오지 않아도 된다는 장점이 있습니다.


6. 어디서든지 호출이 가능합니다.

서비스 객체는 컨트롤러 뿐만 아니라 다른 서비스 객체에서도 호출될 수 있습니다. DelayedJob / Rescue / SideKiq Jobs/ Rake Task / Console 등에서도 호출이 가능합니다.

완벽한건 없기 때문에 서비스 객체에도 단점은 있습니다. 간단한 액션도 더 복잡하게 만드는 경우도 있기 때문입니다. 


서비스 객체는 아래와 같은 상황일 때, 빼내면 좋습니다 :) 


1. 액션이 복잡할 때

2. 액션에서 여러 모델을 처리할 때

3. 액션이 모델의 핵심 기능이 아닌 로직을 처리할 때

4. 액션에 여러가지 기능이 섞여있을 때


서비스 객체를 어떻게 설계할까요?


저자가 서비스 객체를 설계할 때 지키는 가이드라인과 컨벤션은 아래와 같습니다.


1. 서비스 객체는 객체의 상태를 저장하지 않아야합니다


2. 인스턴스 메서드를 사용하고, 클래스 메서드는 사용하지 않아야합니다


3. 단일 책임 원칙을 지키기 위해서, 퍼블릭 메소드는 최소한으로 구성해야합니다


4. 메서드는 의미있는 응답값을 리턴해줘야 합니다. ( boolean 타입을 리턴하면 안됩니다 )


5. 서비스는 app/services 하위 디렉토리에 존재해야합니다. 비즈니스 로직이 헤비한 도메인은 서브 디렉토리를 사용해서 구성하는것도 좋습니다.

예를 들어, app/services/report/generate_weekly.rb 파일에는 Report::GenerateWeekly라는 클래스가 존재하구요!

그리고 app/services/report/publish_monthly.rb는 Report::PublishMonthly라는 클래스를 정의하면 됩니다 ~


6. 서비스 이름은 동사로 시작해야 합니다. 그리고 클래스명은 Service로 끝나지 않아야 합니다.

예를 들어, ApproveTransaction, SendTestNewsLetter, ImportUserFromCsv 처럼 동사가 먼저 나오게 이름을 짓는게 좋습니다.


7. 서비스는 call 메서드로 응답값을 리턴하면 됩니다.

ApproveTransaction.approve() 이런 식으로 또다른 동사를 호출하면, 메서드명이 장황해지고 복잡하기 쉽습니다.


샘플 코드 수정해보기

StatisticsController#Index를 봐보면, weeks_to_date_from, weeks_to_date_to, avg_distance 등의 메서드가 있습니다. 그런데 이 것들은 컨트롤러에 의존적인 메서드여서 정말 좋은 사례로 보기는 어렵습니다. 그래서 이 함수들도 서비스 객체들로 이전되었습니다.