Jeen - Yet anothere techlog

STFUAWSC

Catalyst - ActionRole

Action Role

오래된 이야기이지만 Catalyst 가 Moose 기반으로 만들어지면서 ActionRole 이라는 개념이 만들어졌었지요. 실제 업무에서 적용해볼 껀덕지가 없어서 만져보지는 못했습니다만, 최근에 업무에서 ActionRole 을 적용해서 간단한 수정작업을 진행했습니다.

~~~ perl package MyApp::Web::ActionRole::Logger; use Moose::Role; use namespace::autoclean;

after execute => sub {

my ($self, $controller, $c) = @_;

# ...

}

1; ~~~

일단 업무에서 사용한 ActionRole 의 기본형은 위와 같습니다.

Catalyst App 을 만들 때 사용한 네임스페이스를 기준으로 ActionRole::"RoleName" 을 사용합니다. 그러니 위에서 봤을 때 Catalyst App 의 이름은 MyApp::Web 이 되겠죠. 당연히 ActionRole 의 이름은 Logger 입니다.

또한, Logger 라는 ActionRole 을 참조할 때, Catalyst 는 MyApp::Web 뿐만 아니라 Catalyst::ActionRole::Logger 가 있으면 이를 참조할 수 있습니다.

MyApp::Web::ActionRole::LoggerCatalyst::ActionRole::Logger 두개가 존재할 시에는 앞의 MyApp::Web::ActionRole::Logger 를 우선적으로 사용하게 됩니다.

주의점

위의 코드의 after execute => sub { ... } 에서 주의할 점이 한가지 있습니다. Catalyst 의 auto, begin, end 등의 Private Action 들 또한 실행이 된다는 점 입니다. 예를들어 /user/login 이라는 수행하기 위해서 각 컨트롤러 마다 auto, begin 등의 Private Action 이 정의되어 있다고 하면, 아마 아래와 같이 나올 것입니다.

.——————————————————————————————+—————–. | Action | Time | +——————————————————————————————+—————–+ | /auto | 0.000600s | | /user/auto | 0.000208s | | /user/login | 0.000219s | | /user/end | 0.009542s | | –> MyApp::Web::View::Default->process | 0.008665s | ‘——————————————————————————————+—————–’

애시당초 /user/login 가 끝나면 무엇무엇을 한다라고 ActionRole::Logger 에 정의한 마당에서 해당 액션이 2-3회 중복해서 실행되는 경우가 발생하는 것입니다. 바로 auto, end 가 끝난 다음에도 해당 ActionRole 을 참고한다는 것이죠.

~~~ perl package MyApp::Web::ActionRole::Logger; use Moose::Role; use namespace::autoclean;

after execute => sub {

my ($self, $controller, $c) = @_;

return if $self =~ /(?:auto|begin|end)/;

# ...

}

1; ~~~

그래서 적절하게 auto, begin, end 는 필터링해줍니다. $self 변수에 어떤 액션에서 온 것인지 알 수 있습니다. 전 처음에 계속 $c->action 으로 알고 착각을 했더랬습니다.

아, 물론 ActionRole 을 추가해줬다고 바로 쓸 수 있는 것은 아닙니다. 해당 ActionRole 을 사용할 컨트롤러의 기본 상속 모듈을 Catalyst::Controller 에서 Catalyst::Controller::ActionRole 로 바꾸도록 합니다.

package MyApp::Web::Controller::Root;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller::ActionRole` };

Conclusion

위의 예제에서는 그냥 단순히 Root 에서만 사용하고 있는 것 처럼 보이지만, 업무상에서는 특정 컨트롤러 몇개를 제외한 나머지 모든 컨트롤러에서 공통으로 사용하고 있습니다.

거의 모든 컨트롤러 상의 액션이 끝날 때, 시작될 때, 혹은 양쪽 모두에서 공통적으로 특정 행동을 정의할 때 ActionRole 과 함께하면 좀 더 유연하고 아름다운 코드작성에 도움이 되지 않을까 생각하고 있습니다.

Using Plack::Middleware::OAuth

Before Using Plack::Middleware::OAuth

사실은 Plack::Middleware::OAuth 를 사용하기 이전에는 OAuth 관련 callback 처리등을 직접 파라메터를 확인해가면서 해당 액션을 처리했었습니다. 그게 P::M::OAuth 를 사용해서 각 OAuth Provider 들을 좀 더 유연하게 다룰 수 있게 되지 않았나 합니다.

~~~ perl

sub redirect_to_foursquare {
    my $self = shift;

    # redirect to foursquare authentication url
    $self->response->redirect("https://foursquare.com/oauth2/authenticate?client_id=$client_id&response_type=code&redirect_uri=$callback_url");
}

sub receive_token_foursquare {

  my $self = shift;

  my $res = Furl->new->get("https://foursquare.com/oauth2/access_token?client_id=$client_id&client_secret=$client_secret&grant_type=authorization_code&redirect_uri=$redirect_url&code=".$self->request->param('code'));
  ...

} ~~~

예, 뭐 위처럼 OAuth 인증시에 client_idclient_secret 그리고 grant_type 등 여러가지 파라메터의 지정이나 인증 URL 등등이 Provider 마다 다르고 이러니 뭐 일일이 Provider 마다 매번 어플리케이션 단에서 코드를 써나가는 것이 참 불쾌하기 마련이었습니다.

나름 플러거블하게 만든다고 해봤지만 역시 이런 부분은 Middleware 단에서 처리하는 게 좋지 않을까 생각하고 있었지요. 그런의미에서 P::M::OAuth 를 사용하게 되었습니다.

Custom Provider

P::M::OAuth 는 OAuth v1/v2 를 지원합니다. OAuth 버젼마다 파라메터가 바뀌기 때문에 사용하고자 하는 OAuth Provider 가 어떤 버젼을 지원하는 지 확인해봐야 합니다.

P::M::OAuth 는 기본적으로 Github, Twitter, Facebook, Live, Google 의 다섯가지 OAuth Provider 를 지원합니다.

저는 FourSquare 의 OAuth 를 사용하려고 하는 데, 기본적으로 제공해주지 않으니, 약간 손질을 해줘야 되겠죠.

~~~ perl package Plack::Middleware::OAuth::Foursquare; use strict; use warnings;

sub config {

+{
    version          => 2,
    authorize_url    => 'https://foursquare.com/oauth2/authenticate',
    access_token_url => 'https://foursquare.com/oauth2/access_token',
    response_type    => 'code',
    grant_type       => 'authorization_code',
};

}

1; ~~~

위처럼 Plack::Middleware::OAuth::Foursquare 와,

~~~ perl package Plack::Middleware::OAuth::UserInfo::Foursquare; use strict; use warnings; use parent qw(Plack::Middleware::OAuth::UserInfo); use LWP::UserAgent; use JSON;

sub create_handle {}

sub query {

my $self = shift;

my $uri = URI->new('https://api.foursquare.com/v2/users/self');
$uri->query_form( oauth_token => $self->token->access_token );
my $res = LWP::UserAgent->new->get($uri);
my $body = $res->decoded_content;
return unless $body;

my $obj = decode_json($body) || {};
return $obj->{response}->{user};

}

1; ~~~

Plack::Middleware::OAuth::UserInfo::Foursquare 를 추가합니다.

이렇게 쉽게 OAuth Provider 마다 해당 네임스페이스 맞는 모듈을 추가해줍니다.

MyApp

~~~ perl

my $app = MyApp->psgi_app; builder {

mount '/oauth' => builder {
    enable 'OAuth',
        on_success => sub {
            my ($mw, $token) = @_;

            # get Foursquare's UserInfo
            my $info = Plack::Middleware::OAuth::UserInfo->new( config => $mw->config, token => $token );

            if ($token->is_provider('Foursquare')) {
            return $mw->redirect('/');  
          }
          on_error => sub { #... },
          providers => {
              'Foursquare' => {
                  client_id => 'YOUR CLIENT_ID',
                  client_secret => 'YOUR CLIENT_SECRET',
              },
          };
        }
};
mount '/' => $app;

}; ~~~

대충 위처럼 사용을 합니다. 좀 더 자세한 예제는 P::M::OAuth 모듈의 예제 psgi 파일 또는 , 이 모듈을 사용한 제 프로젝트 Bobby-Akawa 의 특정 모듈에서 찾아볼 수 있습니다.

Conclusion

이로써 간략하게나마 Plack::Middleware::OAuth 를 이용해서 OAuth 인증을 Plack Middleware 단에서 구현할 수 있게 되었습니다. 조금 설명이 빈약해서 차후에 좀 더 구현단으로 들어가서 좀 더 깊게 파고들어가 볼까 합니다.

DBIx::Class - Using GIS Extensions

DBIx::Class 에서의 GIS!?

ORM 을 사용함에 있어서 단순한 SELECT 문은 그렇게 큰 고민없이 사용할 수 있습니다. 하지만 GIS Extension 을 사용한 쿼리들을 ORM 에서 사용한다면 일단 고민이 들어가기 마련입니다.

DBIx::Class 로 사용할 GIS 용 Query 확인

우선 대상으로 할 테이블은 아래와 같습니다.

~~~ sql

CREATE TABLE venue (
  id            INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  name          VARCHAR(255) NOT NULL,
  latitude      VARCHAR(64) NOT NULL,
  longitude     VARCHAR(64) NOT NULL,
  location      Point NOT NULL,
  SPATIAL INDEX(location),
  created_on    DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=MyISAM DEFAULT CHARSET UTF8;

~~~

위의 CREATE TABLE 문에서 알 수 있듯, locationSPATIAL INDEX 를 사용하게 됩니다. 이제 이 location 컬럼을 대상으로 온갖 GIS 관련 쿼리를 ORM 에서 사용하게끔 합니다.

INSERT

우선 DB 스키마는 덤프되어 있다고 가정하며 Venue 라는 결과 클래스에 데이터를 등록해보도록 합니다.

~~~ perl

$self->resultset('Venue')->create({
    name     => 'Silex',
    latitude => '32.123512',
    longitude => '127.0123123',
    location  => \"GeomFromText('POINT(32.123512 127.0123123)')",
});

~~~

위와같은 코드로 location 에 대해서 Scalar Reference 를 지정함으로 GIS 관련 GeomFromText 라는 Function 을 사용할 수 있게 됩니다.

SELECT

일단 위치정보의 등록은 위와같은 코드로 가능했고, 이제 결과값을 찾아보도록 합니다. 인자로 사용될 값은 모바일기기의 화면단의 좌상단 Latitude/Longitude 좌표와 우하단 Latitude/Longitude 좌표입니다.

~~~ perl

$self->resultset('Venue')->search({
    -and => \[
        \"Intersects(location, Envelope(GeomFromText('LineString(32.0 127.0, 33.0 128.0)')))",
    \]
});

~~~

위처럼 또다시 Scalar Reference 의 힘을 빌려서 사태를 회피할 수 있습니다. 위의 코드로 좌상단의 점과 우하단의 점을 잇는 사각형의 영역에 대해서 결과를 얻어낼 수 있습니다.

SELECT + ORDER_BY

여기까지는 그저 Scalar Reference 의 마법을 이용해서 잘도 헤쳐왔지만 이제부터가 문제입니다. 결과값을 뽑아낼 때에, 중점을 기준으로 결과값을 정렬해서 보여줄 필요가 생겼습니다.

이제껏 DBIx::Class 를 사용하며 숱하게 회피방법을 연구해봤지만 도저히 답이 나오지 않아서 결국은 GIS 관련 등의 복잡한 쿼리에서 ORM 의 사용을 피하고 쌩 SQL 을 날릴 수 밖에 없었습니다만, 이런저런 시련의 세월 속에서 결국은 방법을 찾아냈습니다.

그 방법이란 Dynamic View 를 이용하는 방법입니다. 일반적으로 View 를 만듬에 있어서 컬럼들의 비교나 의미가 정해진 정적인 값을 써서 미리 View 를 만들어서 사용하는 방법을 주로 사용해왔었는데, Dynamic View 를 통해서 해당 위치정보에 해당하는 값들을 바인딩해서 즉석에서 View 를 생성하는 방법입니다.

우선은 VenueComplex 라는 View Table Class 를 작성합니다.

~~~ perl

package MyApp::Schema::Result::VenueComplex;
use strict;
use warnings;
use base qw/DBIx::Class::Core/;

__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('venue_complex');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{
    SELECT v.*, SQRT(POW(ABS(X(v.location) - X(GeomFromText(?))), 2) + POW(ABS(Y(v.location) - Y(GeomFromText(?))), 2)) as distance FROM venue v WHERE Intersects(v.location, Envelope(GeomFromText(x))) ORDER BY distance asc
});

__PACKAGE__->add_columns( ... );

~~~

여타 컬럼정보, 릴레이션 설정등은 기존의 Venue 결과클래스에서 정의한 것을 그대로 사용하도록 합니다. 위의 view_definition 의 Prepared Statement SQL 에 바인딩 될 값은 3개입니다.

자, 준비는 끝났으니, 이제 중점기준으로 값을 구해봅니다.

~~~ perl

my ($nw_lat, $nw_lng) = split ',', $args->{location1};
my ($se_lat, $se_lng) = split ',', $args->{location2};

my $center_point_x = ($se_lat - $nw_lat) / 2 + $nw_lat;
my $center_point_y = ($se_lng - $nw_lng) / 2 + $nw_lng;

my $r = $self->resultset('VenueComplex')->search({}, {
     bind => \[ "POINT($center_point_x $center_point_y)",
               "POINT($center_point_x $center_point_y)",
               "LineString($nw_lat $nw_lng , $se_lat $se_lng)",
             \],
});

~~~

기존의 Venue 클래스가 아닌 VenueComplex 로 대상을 바꾸고, bind 로 세개의 값을 넘겨줍니다. 중점기준으로 정렬은 위의 VenueComplex 의 SQL 상에서 처리하고 있습니다.

Conclusion

ORM 을 사용할 때에는 수많은 장점에도 불구하고 아직까지 논의되고 있는 수많은 단점들을 명확하게 인지해야 합니다. 상황에 맞게 쓰는 것이 당연히 중요하겠죠. 위에서 제가 제시한 코드들이 바른 길이라고는 장담할 수 없겠습니다만, 다양한 요구사항을 수렴할 수 있다면 그걸로 괜찮다고 그냥 생각하고 있습니다.