* 세미나 발표자료: AssociationProxyTutorial.pdf
투명한 '뿌락찌'
액티브레코드 연관 클래스는 ActiveRecord::Associations::AssociationProxy 클래스의 확장이다. 이프록시 클래스는 일종의 "투명 프록시(transparent proxy)"로서 개발자들 몰래 숨어서 맡은 역할을 수행한다.그렇지만, 이 프록시의 존재여부를 아는 것과 모르는 것은 차이가 있다. 예를 들어, 우리가 만약 User 모델에서has_many :articles를 선언하게 되면, 이제 우리는 user.articles 와 같이 액세스할 수 있는데, 이 때반환되는 객체가 실제로는 프록시 객체다. 아래 그림은 이 프록시 클래스의 계층구조다. (요즘도 "뿌락찌"라는 말을 아는 사람들이있을까?)
프록시는 어떻게 생성되는가?
액티브 레코드의 소스코드를 보면 has_many, has_one, belongs_to, has_and_belongs_to_many와 같은 연관 메서드들이 정의되어 있는 것을 볼 수 있다. 그런데 이들 메서드의 내용을 유심히 보면, 메서드들마다 조금씩다르기는 하지만, 대체로 다음과 같은 로직이 들어 있다. 예를 들어, has_many의 경우를 보자.

연관 프록시를 사용하는 이점은 무엇인가?
그렇다면 이런 연관 프록시를 왜 사용할까? 연관 프록시를 사용하는 이점은 무엇일까?
연관 프록시를 사용하는 이점은 결국 연관(association)의 사용 사례를 살펴보면 알 수 있다. 연관을 사용하는 경우는크게 2가지인데, 하나는 액티브레코드 모델의 액세스 범위를 한정하는 것이고, 나머지 하나는 Custom Query를 생성하기위한 것이다.
예를 하나 들어보자.
어떤 사용자의 articles를 모두 찾는 기능을 만든다고 하자. Article 객체만을 사용하여 이 작업을 수행할 수도 있다.
그렇지만 이 방법보다는 연관을 사용하여 다음과 같이 표현하는 것이 훨씬 간단하고 표현 또한 직관적이다.
더 편리한 것은 이 상태에서 뭔가 추가로 작업을 더 해 줘야 할 경우다. 후자의 경우 user.articles.find(...)와 같은 식으로 표현하여 특정 사용자로 범위가 한정된 모델객체에 대하여 일반적인 CRUD 조작을 할 수가 있다. 전자의 경우라면이들 경우마다 일일이 별개의 메서드를 만들어야 했을 것이다.
이것 외에 또 어떤 이점들이 있을까?
연관을 사용하는 또 다른 경우는 커스텀 질의문을 확장하는 경우다.
예를 들어, 최근의 article들을 찾는다고 하자. Article 모델에 다음과 같이 클래스 메서드를 정의할 수도 있을 것이다.
이제 액티브레코드 연관을 사용해 보자. 위와 똑같은 효과를 내기 위해서 아래와 같이 하면 된다.
결과는 동일하다. 즉 위와 같이 하여 user.articles.recent를 호출한 것이나, 아래와 같이 하여user.recent_articles를 호출할 것이나 SQL 쿼리문장은 동일하게 "SELECT * FROM articlesWHERE (articles.user_id = 1 AND created_at > ...) " 과 같은 쿼리를 호출할것이다.
그렇다면, 차이점은 무엇일까?
장단점이 있다. 우선, 위의 경우는 연관관계가 아닌 경우에서도 사용할 수 있다는 장점이 있다. 즉,Article.recent 도 가능하며, 사실 이게 더 정상적인 사용 패턴일 것이다 (오히려 user.articles.recent가가능한 것은 연관프록시가 가지고 있지 않은 메서드에 대한 호출을 받은 경우 method_missing에서 실제 모델의 클래스메서드로 delegate하기 때문에 생기는 side-effect일 뿐이다.) 반면 아래의 경우는 반드시 연관관계 속에서만 사용이가능하다. 왜냐면 이 객체는 실제로연관 프록시 객체이며, 연관 프록시 객체는 연관관계 속에서만 생성되기 때문이다. 그렇지만 후자의 방법을 쓸 경우의 장점도 만만치않다. 우선 Scoped Access의 잇점을 누릴 수 있다. 즉, user.recent_articles.find(...) 와같은 식의 접근이 가능한 것이다. 또한 이 경우에도 마찬가지로 캐싱(caching)이 된다.
연관 프록시의 확장
연관 프록시는 확장이 가능하다. anonymous module을 사용할 수도 있고, 명시적으로 모듈을 만들어 extend하는 방법도 가능하다.
다음과 같은 식으로 연관을 확장하여 새로운 메서드를 추가하더라도 동일한 효과를 거둘 수 있다.
실제로 위와 같이 한 다음 user.articles.recent를 호출하면 SELECT * FROM articles WHERE(articles.user_id = 1 AND (created_at > '2007-08-27 11:51:26')) 가호출된다.
결론적으로 보면, 아래 표와 같이 약 3가지의 사용 패턴이 나온다. 장단점을 간단히 비교해 보자.
연관 모델과 관련된 3rd Party 라이브러리들
꼭 연관과 관련된 것은 아니지만, 연관과 유사한 작용을 해 주는 라이브러리들이 많이 있다. 그 중 몇 가지만 소개해 본
Scope_out 플러그인
* http://code.google.com/p/scope-out-rails/
이 플러그인은 제목 그대로 액세스 범위를 한정해 주는 플러그인이다. 내부적으로는 with_scope 문을 사용한다. 이 플러그인은 기본적으로 다음 3가지 메서드를 지원한다.
따라서 위와 같이 scope_out이 정의되어 있을 경우 다음과 같이 사용할 수 있다.
Article.find_recent(:all)
user.articles.find_recent(:all)
user.articles.calcuate_published(:count, :all)
user.articles.with_published do
user.articles
end
Scope_Proxy 플러그인
* http://www.jackchristensen.com/article/4/scopedproxy-plugin-for-ruby-on-rails
이 플러그인은 범위를 한정하는 것을 중복해서 가져갈 수 있게 해 주는 플러그인이다. 즉, Article.recent.published.find :all 과 같은 식의 액세스가 가능하다고 한다. 직접 사용해 보지는 않았다.
HasFinder 젬
* http://www.pivotalblabs.com/articles/2007/09/02/hasfinder-its-now-easier-than-ever-to-create-complex-re-usable-sql-queries
이젬은 구현패턴#1과 연관된 젬이다. 다음과 같은 식으로 정의를 하면, Article.recent.popular와 같은 식으로개별 쿼리 메서드를 복합하여 사용할 수 있다는 점이 특색이다. user.articles.recent.popular와 같은 식으로연관관계 속에서도 사용할 수 있으며, user.articles.recent.popular.find(...)와 같은 식의 쿼리확장도 가능하다. 다만, 아직 안정화되지 않아서 생성되는 쿼리가 최적화되지 않았다.
기타 연관과 관련된 트릭과 기법들
연관 메서드 중 loaded?라는 것이 있다. 이 메서드를 사용하여 연관이 eager loading되었는지 여부를 판단, 조건부로 처리할 수 있다.
* http://www.dcmanges.com/blog/24
Conditional Association Loading Trick
연관 프록시의 loaded? 메서드를 오버로드하면 특정 조건에 부합하는 경우에만 재로드 하는 식의 조건부 로딩도 가능하다.
* http://www.jroller.com/obie/entry/conditional_association_loading_trick
참고자료
투명한 '뿌락찌'
액티브레코드 연관 클래스는 ActiveRecord::Associations::AssociationProxy 클래스의 확장이다. 이프록시 클래스는 일종의 "투명 프록시(transparent proxy)"로서 개발자들 몰래 숨어서 맡은 역할을 수행한다.그렇지만, 이 프록시의 존재여부를 아는 것과 모르는 것은 차이가 있다. 예를 들어, 우리가 만약 User 모델에서has_many :articles를 선언하게 되면, 이제 우리는 user.articles 와 같이 액세스할 수 있는데, 이 때반환되는 객체가 실제로는 프록시 객체다. 아래 그림은 이 프록시 클래스의 계층구조다. (요즘도 "뿌락찌"라는 말을 아는 사람들이있을까?)

액티브 레코드의 소스코드를 보면 has_many, has_one, belongs_to, has_and_belongs_to_many와 같은 연관 메서드들이 정의되어 있는 것을 볼 수 있다. 그런데 이들 메서드의 내용을 유심히 보면, 메서드들마다 조금씩다르기는 하지만, 대체로 다음과 같은 로직이 들어 있다. 예를 들어, has_many의 경우를 보자.
def has_many(association_id, options = {}, &extension)설명을 덧붙이자면, 우선 주어진 association_id와 옵션정보를 사용하여 리플렉션(Reflection)을 생성한다.리플렉션은 액티브레코드의 클래스나 객체에 대한 메타데이터 정보들을 담는 객체라고 보면된다. 연관 모델인 경우AssociationReflection 객체가 생성될 것이다. 이어서 생성된 리플렉션 객체를 가지고HasManyAssociation 프록시 객체를 생성하고, 리플렉션 정보를 이용하여 각각의 연관관계에서 필요한 헬퍼 메서드들을생성하게 된다. 예를 들어, User 모델에서 has_many :articles 라고 선언 하였다면, 이 메서드에 의해 User모델에 articles(), articles=() 등의 메서드가 만들어지고, 이들 메서드가 호출될 때에 실제로는 아래 그림처럼연관 프록시 객체를 대상으로 작업이 일어나게 되는 것이다.
reflection = create_has_many_reflection(association_id, options, &extension)
// ...
collection_accessor_methods(reflection, HasManyAssociation)
// ...
end

연관 프록시를 사용하는 이점은 무엇인가?
그렇다면 이런 연관 프록시를 왜 사용할까? 연관 프록시를 사용하는 이점은 무엇일까?
연관 프록시를 사용하는 이점은 결국 연관(association)의 사용 사례를 살펴보면 알 수 있다. 연관을 사용하는 경우는크게 2가지인데, 하나는 액티브레코드 모델의 액세스 범위를 한정하는 것이고, 나머지 하나는 Custom Query를 생성하기위한 것이다.
예를 하나 들어보자.
어떤 사용자의 articles를 모두 찾는 기능을 만든다고 하자. Article 객체만을 사용하여 이 작업을 수행할 수도 있다.
Article.find_all_by_user_id(user)
그렇지만 이 방법보다는 연관을 사용하여 다음과 같이 표현하는 것이 훨씬 간단하고 표현 또한 직관적이다.
user.articles
더 편리한 것은 이 상태에서 뭔가 추가로 작업을 더 해 줘야 할 경우다. 후자의 경우 user.articles.find(...)와 같은 식으로 표현하여 특정 사용자로 범위가 한정된 모델객체에 대하여 일반적인 CRUD 조작을 할 수가 있다. 전자의 경우라면이들 경우마다 일일이 별개의 메서드를 만들어야 했을 것이다.
이것 외에 또 어떤 이점들이 있을까?
- 연관관계를 다루는 표현이 간편하고 직관적이다.
- 내부적으로 캐싱이 구현되어 있다. 즉, user.articles(true)를 하기 전에는 다시 로드하지 않는다.
연관을 사용하는 또 다른 경우는 커스텀 질의문을 확장하는 경우다.
예를 들어, 최근의 article들을 찾는다고 하자. Article 모델에 다음과 같이 클래스 메서드를 정의할 수도 있을 것이다.
class Article < ActiveRecord::Base그러나, 이 방법을 사용하면 매번 필요할 때 마다 이런 식으로 클래스 메서드들을 만들어 줘야 한다는 단점이 있다.
def self.recent
find(:all, :conditions => ['created_at > ?', 10.days.ago])
end
end
이제 액티브레코드 연관을 사용해 보자. 위와 똑같은 효과를 내기 위해서 아래와 같이 하면 된다.
class User < ActiveRecord::Base
has_many :recent_articles,
:class_name => 'Article',
:conditions => ['created_at > ?', 10.days.ago]
end
결과는 동일하다. 즉 위와 같이 하여 user.articles.recent를 호출한 것이나, 아래와 같이 하여user.recent_articles를 호출할 것이나 SQL 쿼리문장은 동일하게 "SELECT * FROM articlesWHERE (articles.user_id = 1 AND created_at > ...) " 과 같은 쿼리를 호출할것이다.
그렇다면, 차이점은 무엇일까?
장단점이 있다. 우선, 위의 경우는 연관관계가 아닌 경우에서도 사용할 수 있다는 장점이 있다. 즉,Article.recent 도 가능하며, 사실 이게 더 정상적인 사용 패턴일 것이다 (오히려 user.articles.recent가가능한 것은 연관프록시가 가지고 있지 않은 메서드에 대한 호출을 받은 경우 method_missing에서 실제 모델의 클래스메서드로 delegate하기 때문에 생기는 side-effect일 뿐이다.) 반면 아래의 경우는 반드시 연관관계 속에서만 사용이가능하다. 왜냐면 이 객체는 실제로연관 프록시 객체이며, 연관 프록시 객체는 연관관계 속에서만 생성되기 때문이다. 그렇지만 후자의 방법을 쓸 경우의 장점도 만만치않다. 우선 Scoped Access의 잇점을 누릴 수 있다. 즉, user.recent_articles.find(...) 와같은 식의 접근이 가능한 것이다. 또한 이 경우에도 마찬가지로 캐싱(caching)이 된다.
연관 프록시의 확장
연관 프록시는 확장이 가능하다. anonymous module을 사용할 수도 있고, 명시적으로 모듈을 만들어 extend하는 방법도 가능하다.
다음과 같은 식으로 연관을 확장하여 새로운 메서드를 추가하더라도 동일한 효과를 거둘 수 있다.
class User < ActiveRecord::Base
has_many :articles do
def recent(reload=false)
@recent = nil if reload
@recent ||= find(:all, :conditions => ['created_at > ?', 10.days.ago])
end
end
end
실제로 위와 같이 한 다음 user.articles.recent를 호출하면 SELECT * FROM articles WHERE(articles.user_id = 1 AND (created_at > '2007-08-27 11:51:26')) 가호출된다.
결론적으로 보면, 아래 표와 같이 약 3가지의 사용 패턴이 나온다. 장단점을 간단히 비교해 보자.
구분 | 구현방법 | 범위(scope) | find() 메서드 사용 | 캐싱(caching) |
[사용패턴#1] Article.recent (user.articles.recent 도 가능) | Article 모델의 클래스 메서드 | 전체 모델 범위 (user 단위로 scoping 가능) | 불가 | 불가 |
[사용패턴#2] user.recent_articles | User 모델에서 연관프록시 사용(has_many :recent_articles) | 특정 user로 범위 한정 | 가능 | 가능 |
[사용패턴#3] user.articles.recent | User 모델에서 연관프록시 확장 (has_many :articles do ... end) | 특정 user로 범위 한정 | 불가 | 가능 (구현하기에 따라) |
연관 모델과 관련된 3rd Party 라이브러리들
꼭 연관과 관련된 것은 아니지만, 연관과 유사한 작용을 해 주는 라이브러리들이 많이 있다. 그 중 몇 가지만 소개해 본
Scope_out 플러그인
* http://code.google.com/p/scope-out-rails/
이 플러그인은 제목 그대로 액세스 범위를 한정해 주는 플러그인이다. 내부적으로는 with_scope 문을 사용한다. 이 플러그인은 기본적으로 다음 3가지 메서드를 지원한다.
- with_xxx
- find_xxx
- calculate_xxx
class Article < ActiveRecord::Base
scope_out :published, :conditions => { :published => 1 }
scope_out :recent do
{ :conditions => ['created_at > ?', 10.days.ago] }
end
따라서 위와 같이 scope_out이 정의되어 있을 경우 다음과 같이 사용할 수 있다.
Article.find_recent(:all)
user.articles.find_recent(:all)
user.articles.calcuate_published(:count, :all)
user.articles.with_published do
user.articles
end
Scope_Proxy 플러그인
* http://www.jackchristensen.com/article/4/scopedproxy-plugin-for-ruby-on-rails
이 플러그인은 범위를 한정하는 것을 중복해서 가져갈 수 있게 해 주는 플러그인이다. 즉, Article.recent.published.find :all 과 같은 식의 액세스가 가능하다고 한다. 직접 사용해 보지는 않았다.
HasFinder 젬
* http://www.pivotalblabs.com/articles/2007/09/02/hasfinder-its-now-easier-than-ever-to-create-complex-re-usable-sql-queries
이젬은 구현패턴#1과 연관된 젬이다. 다음과 같은 식으로 정의를 하면, Article.recent.popular와 같은 식으로개별 쿼리 메서드를 복합하여 사용할 수 있다는 점이 특색이다. user.articles.recent.popular와 같은 식으로연관관계 속에서도 사용할 수 있으며, user.articles.recent.popular.find(...)와 같은 식의 쿼리확장도 가능하다. 다만, 아직 안정화되지 않아서 생성되는 쿼리가 최적화되지 않았다.
class Article < ActiveRecord::Base
has_finder :published, :conditions => { :published => 1 }
has_finder :recent, :conditions => ['created_at > ?', 10.days.ago]
기타 연관과 관련된 트릭과 기법들
연관 메서드 중 loaded?라는 것이 있다. 이 메서드를 사용하여 연관이 eager loading되었는지 여부를 판단, 조건부로 처리할 수 있다.
* http://www.dcmanges.com/blog/24
class Team
has_many :players
def has_leader?
if players.loaded?
# players have been eager loaded
# loop through them looking for a leader
players.detect(&:leader?)
else
# players have not been loaded
# run a query to look for a leader
!players.find_leader(:first).nil?
end
end
end
Conditional Association Loading Trick
연관 프록시의 loaded? 메서드를 오버로드하면 특정 조건에 부합하는 경우에만 재로드 하는 식의 조건부 로딩도 가능하다.
* http://www.jroller.com/obie/entry/conditional_association_loading_trick
has_many :neighborhoods do
def loaded?
return true if (proxy_owner.population.nil? or proxy_owner.population < 50000)
super
end
end
참고자료
덧글