본문 바로가기

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

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

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

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


Service Layer를 사용하는 개발 가이드라인

이 글을 쓴 저자는 Service / Decorator Layer를 사용해서 개발을 할 때, 아래의 가이드라인을 따른다고 합니다. 

1. ActiveRecord 모델은 association과 constant만 갖고 있어야합니다. 그 외에는 아무것도 하지 않아야합니다.

model에서는 서비스 객체를 사용하지 않아야하고, 유효성 체크는  ActiveModel Form object를 사용해서 해결해야합니다. 


2. 컨트롤러는 Service Object만 호출해야합니다. 컨트롤러의 코드는 짧게 구성되어야합니다. 

서비스 객체 호출 외에, 컨트롤러에서 해야하는 기능들은 아래와 같습니다.

-> HTTP 라우팅, 파라미터 파싱, 인증처리, 콘텐츠 중개, 서비스 객체 호출, 예외 처리, 응답 포맷팅, HTTP 상태 코드 리턴


3. Service는 Query 객체를 호출하는 역할을 합니다. 그리고 stateless한 상태여야합니다. 즉, 서비스는 상태값을 갖고 있으면 안됩니다. 

sevice 메서드는 인스턴스 메서드여야합니다. 그리고 단일 책임 원칙을 위하여, public 메서드는 최대한 적게 사용해야합니다.


4. Query는 모델에만 있어야합니다. 그리고 모델의 메서드는 객체, 해시, 배열을 리턴해야합니다. ActiveRecord Association을 리턴하면 안됩니다.


5. Helper를 사용하지말고, Decorator를 사용하는게 좋습니다.

Rails Helper는 여러 클래스에서 쉽게 공유되고, 접근할 수 있기 때문에 코드가 뒤섞일 가능성이 있습니다.

레일즈 헬퍼 클래스에는 유틸리티성 메서드가 들어가야합니다. 특정한 상황에서만 사용되는 메서드는 헬퍼에 포함되면 안됩니다. 


6. Concern을 사용하지 말고, Decorator/Delegator를 사용하는게 좋습니다.

Concern은 여러 모델에서 공유되어야하는 함수일 때, 레일즈 코드를 dry하게 해줘서 좋습니다. 그러나, 여러 모델에서 쉽게 사용할 수 있다보니, 모델 객체가 독립적으로 구성되기 어렵습니다. 


7. 모델에서 사용되는 value object를 추출해서 사용하는게 좋습니다.  

그러면 속성들을 그룹핑할 수도 있고, 코드의 가독성을 높여줍니다. 


8. view에는 하나의 인스턴스만 내려줘야 합니다.


Service Layer를 꼭 사용해야하나?

샘플 코드를 리팩토링하기 전에, 하나 더 이야기할게 있습니다.

서비스 클래스와 데코레이터를 사용하도록 리팩토링하기 전에, 보통 이런 고민을 하게 됩니다. 

" 서비스 레이어를 사용해서 꼭 리팩토링을 해야하나? -_- ^"

새로운 코드나 파일을 추가할 때, 어디에 넣는게 좋을지 고민이된다면, 서비스 클래스를 사용한 리팩토링을 사용하기 좋다는 신호로 보면 됩니다 !


이 글에서는 컨트롤러에 있는 로직을 모델로 옮기는 방법에 대해서는 이야기하지 않으려고 합니다. Skinny Controller and Fat Model 방식으로 레일즈를 사용하고 있을거라는 가정하에, Service Layer를 사용하는 방법을 설명하려고 합니다.

그리고 본론에 집중하기 위해서, 테스트 코드를 작성하는 방법이나 테스트 코드를 작성해야하는 이유에 대해서는 설명하지 않으려고 합니다.

그러나 리팩토링을 했을 때, 이전처럼 코드가 돌아가는지 확인하기 위해서는, 테스트코드가 꼭! 작성되어 있어야합니다. 그래야 코드 변경이 끝났을 때, 서비스가 잘 돌아가는지 전체 테스트를 할 수 있습니다.



Value Object란?

마틴파울러는 value object를 아래와 같이 설명했다고 합니다.

Value Object is a small object, such as a money or date range object. Their key property is that they follow value semantics rather than reference semantics.

개발을 할때, 때로는 추상화할만한 데이터들을 발견하고는 합니다. 예로는 루비의 date, uri, pathname 등이 있습니다. 개별 의미를 갖고있는 데이터를 value object나 domain model으로 빼내면, 엄청 유용하다고 합니다.

그런데 왜 value object라는 개념을 사용해야 할까요? 귀찮은데 말이죠.. 

value object는 데이터를 추상화시켰기 때문에, 코드를 읽기 쉽게 해주고, 버그를 줄여준다는 장점이 있습니다. 그리고 객체 이름만 보고 어떤 기능을 할지 추측하기도 쉬워지죠.

그리고 Value Object를 사용하면 객체를 immutability하게 사용할 수 있단 장점도 얻을 수 있다고 합니다. Value Object를 immutability하게 써야하는 이유는 Entity와 Value Object의 차이를 참고하면 어떤 느낌인지 이해할 수 있을 것 같아요.


Value Object를 사용하는 방법

1. Value Object는 여러 속성을 가집니다.

2. Value Object는 Entity Object에 포함되는 객체입니다.

3. 그런데 이 Entity Object가 살아있는 동안에는, VO의 속성값은 변경될 수 없습니다. 

    즉, 한번 참조된 VO의 값을 중간에 변경할 수 없단 거죠. 만약 VO값을 변경해야한다면, 새로운 VO객체를 생성해서 대입해야합니다. 

3. Value Object는 식별자에 의해서 같은 같음(equality)이 결정되지 않습니다.

   예를 들어 Date가 같은 객체를 두 개 생성했다면 그 두 객체는 같은 객체입니다. 반면에 entity라고 볼 수 있는 유저 객체는 id 라는 고유한 식별자로 같음을 판단합니다. 같은 이름을 가진 두 개의 사람 객체가 있다고 해도 두 객체는 같지 않을 수 있습니다. ( 참고 - Entity와 Value Object의 차이 )


샘플코드에 Value Object 개념 적용해보기

지난 블로그에 설명되어있던 entry 모델에 있던 status_weather, status_landform 속성을 Value Object으로 빼내보겠습니다. 

entry_status은 Plain Old Ruby Object (PORO)는 ActiveRecord::Base를 상속받지 않습니다.

value object에는 reader 메서드와 initialize 메서드, 그리고 comparable 모듈을 믹스인해서, 객체의 값을 비교하는 메서드만 포함되어 있습니다. 그리고 entry_status를 사용하도록 entry 모델과 컨트롤러를 함께 수정해줬습니다.


Service Object를 추출하는 방법은 이어서 정리해보고자 합니다.