독서란 [목적달성]이다.
시미즈 가쓰요시 등이 쓴 <성공하는 사람들의 독서습관>은 도쿄의 한 서점 주인이 그 서점과 인연이 닿은 각계각층의 사람들과 함께 쓴 독서와 책에 관한 얘기들입니다. 원제는 '혼초시(honchosi)'라고 하는데, 일본말로 '아주 좋은 상태' 또는 '일이 재대로 잘 되어감'을 뜻한다고 하는군요.

암튼 이 책에 보면 여러 사람들의 독서론이 등장하는데, 내가 특히 좋아하는 부분은 사업가인 사이토 히토리라는 사람이 쓴 부분입니다. 사업가라 그런지 그는 소위 '목적있는 책읽기'를 주장합니다. 그의 말을 빌자면 이렇습니다.

"인생을 어떻게 살아야 하는지 먼저 생각한 다음 자신의 목표를 확실하게 정하고 그와 관련된 책을 읽습니다. 자신은 도무지 쓸모없는 책에만 관심이 간다는 사람은 위대해지기 어렵습니다. 뭐, 인생이란 특별히 위대해지지 않아도 상관없지만. 그렇다면 책도 취미라고 말하세요. 취미라면 아무래도 좋으니까요."

똑같지는 않지만, 저 역시 사업을 도모하는 사람인지라, 비슷한 방식으로 책을 읽습니다. 주로 목적을 가지고 독서를 합니다. 일테면 이런 식이죠. 어느 날 새로운 사업에 대한 아이디어를 떠올리려고 하는데, 아무래도 잘 생각이 나지 않으면 하루 날을 잡아 가까운 공공도서관으로 갑니다. 그리고 열람실을 헤집고 다니며 서가 이곳저곳에서 내가 생각하는 분야에 관한 책들을 모조리 끄집어 냅니다. 그리고 종일 읽죠. 목표는 물론 '해답을 끄집어 낼 때까지'.

그렇게 본다면 저의 책읽기(독서)는 아마도 [목적달성]이라고 해야겠군요. 끝.


이 글은 Inuit님으로부터 시작되었고, 어슬렁님으로부터 전달받은 독서 릴레이 글입니다.

릴레이 규칙

1. 독서란 [ ]다. 의 빈 칸을 채우고 보충 자료를 제공한다.
2. 앞선 릴레이 주자의 족보를 건다.
3. 족보를 이어갈 주자 두 명을 지정한다.
4. 6월 20일이 지나면 이 릴레이는 무효.
(자세한 규칙 참조: http://inuit.co.kr/1712)

앞선 릴레이 주자의 족보

-Inuit님(독서란 자가교육이다)
-buckshot님(독서는 월아이다)
-고무풍선기린님(독서란 소통이다)
-mahabanya님(독서란 변화다)
-어찌할가님(독서란 습관이다)
-김젼님(독서란 심심풀이 호두다)
-엘군님(독서란 삶의 기반이다)
-무님(독서란 지식이다)
-okgosu님(독서란 지식섭식이다)
-hyomini님(독서란 현실 도피다)
-Raylene님(독서란 머리/마음용 화장품이다)
-하느니삽형님(독서란 운동이다)
-foog님(독서란 삶이다)
-펄님(독서란 짝사랑이다)
-mepay님(독서란 연산작용이다)
-ego+ing님 (독서란 되새김질이다)
-어슬렁님(독서란 스스로 번식하는 생물체이다)

다음 릴레이 주자

릴레이 마감이 다된 관계로 저는 따로 다음 주자에 바통을 넘기는 대신
앞서 소개한 책 속에 등장하는 인물들에게로 바통을 넘기겠습니다.
by thinkr | 2009/06/19 13:59 | 트랙백(1) | 덧글(1)
<크리에이티브 커먼즈와 시맨틱 웹> 세미나 안내
제가 자원활동가로 참가하고 있는 크리에이티브 커먼즈에서 오픈세미나를 개최합니다.

주로 웹 개발자가 세미나의 주요 대상이 되겠지만, 주제를 보면 아시겠지만, 저작권이나 GPL 라이센스, 또는 크리에이티브 커먼즈 라이센스에 관심이 있는 분들이라면 누구든 참가하셔도 좋을 것 같습니다.

특히 자신(또는 자신이 개발/운영하는) 웹사이트에 들어가는 콘텐츠의 저작권 문제 때문에
크리에이티브 커먼즈 라이센스를 부착하고 싶은 분들에게는 "딱"인 세미나가 될 것 같습니다.

정확하지는 않지만 세미나 끝마칠 무렵에 조그만 기념품도 증정할거라는 소문이 있네요.

행사에 관한 더 자세한 내용은 온오프믹스를 참고하시면 되겠습니다.

세미나 안내 바로가기(온오프믹스)

by thinkr | 2009/05/26 19:09 | 트랙백 | 덧글(1)
Cache Me If You Can!

인사이트 출판사에서 최근에 출간된 "쉽고 빠른 웹 개발 Django"를 읽다 문득 재미있는 실험이 하고 싶어 졌다. 이 책에 나오는 예제를 루비로 한번 만들어 보면 어떨까?


마침 이 책은 책 전체가 하나의 예제다. 소셜 북마킹이라는 주제로 웹 서비스를 만들어나가는 가운데 장고(Django)프레임워크의 주요 개념들을 적재적소에서 소개하는 방식을 취한다. 내 실험은 여기에 나온 파이썬 코드들을 모두 루비로 바꾸기만하면 되는 것이다. 한번 발동한 호기심은 좀처럼 끝 모르고 치닫더니 급기야 프레임워크의 선택에 까지 이른다. 뭘로 해 볼까?파이썬 진영도 그렇지만 루비 진영에도 훌륭한 웹 프레임워크가 여럿 나와 있다. 그 중 가장 인기있고 힘 센 놈은 단연 레일스(Rails) 겠지만, 이번에는 시나트라(Sinatra)라는 신예를 한번 기용해 보기로 하였다. 버전 번호가 비슷한 것도 이유가 되었지만(장고의 현재 버전이 1.1인 반면 시나트라는 이글을 쓰는 현재 버전이 0.9.1이다), 무엇보다도 둘 다 '딴따라' 출신이라는 점이 선택의 가장 큰 이유가 되었다. 장고는재즈 기타리스트인 장고 라인하르트(Django Reinhardt)의 이름을 땄다고 알려져 있고, 시나트라는 '마이웨이'로 유명한가수 프랭크 시나트라(Frank Sinata)의 얼굴이 그려져 있는 프레임워크이기 때문이다.

객체관계맵핑(ORM)을 사용한다는 점, 정규표현식을 사용하여 URL맵핑을 쉽게 할 수 있게 해 준다는 점, 구글앱엔진에서도 작동할수 있다는 점 등은 서로 비슷한 점이라고 하겠지만, 시나트라가 DataMapper, ActiveRecord, Sequal 등여러 가지 ORM 도구들을 골라 쓸 수 있는 반면 장고는 하나의 ORM만 제공하는 점, REST 기반 URL 맵핑이 조금 더쉬운 점 등에서는 시나트라가 조금 더 편리해 보였다.


그렇지만 결국은 장고의 손을 들어주어야 했다. 멋드러진 관리자(admin)기능이 자동으로 생성되는 거야 뭐 장고의 트레이드 마크니 그렇다손 치더라도, 장고에서는 프레임워크 차원에서 지원하고 있는캐싱(caching) 처리에 대한 지원 부분이 아직 시나트라에는 없기 때문이었다. 물론 시나트라는 루비 웹 표준 인터페이스라고할 랙(rack)에 기반하고 있어 랙 미들웨어인 Rack::Cache를 적용할 수는 있겠지만, 장고나 레일스 프레임워크에서 제공하는 소위 '서버측 캐시'라면 아직은 직접 구현하는 수 밖에 없다는 점이 아쉬웠다.


프레임워크의 무게는 프레임워크의 기능과 반비례하는 것 같다. 가벼운 프레임워크는 사용자에게 "감놔라 대추놔라" 하는 게 적어서입맛이 좋은 반면 그만큼 직접 처리해야 할 것들도 많아진다. 물론 갖출 건 다 갖추면서도 너무 무겁지도 또 너무 가볍지도 않은그런 프레임워크가 최고겠지만, 문제는 이 무거움의 정도가 받아들이는 사람들마다 그리고 진행해야 하는 프로젝트의 상황마다 다 다르다는 것이 아닐까.

아직은 신인인 시나트라가 장고처럼 음악을 열심히 익혀 다시 경연에 나올 날을 기대해 본다.

by thinkr | 2009/04/22 12:15 | 트랙백 | 덧글(2)
레일스 메탈(Metal)과 Sinatra

레일스가 2.3 버전으로 업데이트되면서 이제 레일스도 Rack 기반에서 작동한다(참고: Rack 속에 들어간 레일스). Rack은 웹서버와 루비 웹 애플리케이션(프레임워크) 간의 최소한의 표준 인터페이스다. 따라서 Rack 표준에 따라 설계된 웹서버 또는 웹 프레임워크는 자유롭게 호환된다.  Rack을 기반으로 하면 여러 가지 이점이 생기게 되는데, 그 중 하나가 소위 "미들웨어(middleware)" 라고 하는 것을 쓸 수 있게 된다는 것이다. 여기서 "미들웨어"란 웹서버와 웹 애플리케이션 중간에 위치하면서 HTTP의 요청/응답을 변형/제어할 수 있게 해주는 일련의 구성요소/모듈을 의미한다. 게다가 레일스 2.3에서는 메탈(Metal)이라고 부르는 일종의 Rack 미들웨어 래퍼(wrapper)도 제공한다. 메탈(즉, 미들웨어)을 사용할 수 있는 영역은 여럿 있지만, 특히 성능이 중요한 부분에서 사용하면 좋은 효과를 낼 수가 있는 것이, 레일스의 전체 라이프사이클을 타지 않아도 되기 때문이다.

 

그런데 많은 루비 웹 프레임워크들이 Rack 기반으로 동작함에 따라 이제 여러 프레임워크를 하나의 애플리케이션에서 섞어서 쓰는 일도 쉬워졌다. 예를 들어, 간단하면서도 성능이 좋은 루비 웹 프레임워크인 Sinatra 역시 Rack 기반으로 동작하는 관계로, 이제 레일스 애플리케이션에서 Sinatra를 Rack 미들웨어로 사용하는 것도 어렵지 않다.


방법1. Metal에 태우는 방법

 

첫번째 방법은 레일스 2.3에 새로 추가된 Metal 을 이용하는 것이다. 레일스 메탈은 Rack 미들웨어의 기능을 이용하여 레일스 애플리케이션에서 좀 더 빠른 액션을 만들 수 있게 해준다. 다음과 같이 레일스 rake middleware 명령을 해 보면 레일스 애플리케이션에 장착된 미들웨어들을 확인해 볼 수 있으며, 이 중 Metal도 들어 있음을 알 수 있다.(참고로 레일스 Metal 애플리케이션들은 Rails::Rack::Metal 미들웨어에 의해 실행된다)

 

  1. $ rake middleware
    (in /Users/coti22/works/sinatra_works/rails_with_sinatra_as_a_middleware)
    use Rack::Lock
    . . .
    use Rails::Rack::Metal
    . . .
    run ActionController::Dispatcher.new

 

그러므로 레일스 애플리케이션의 app/metals 디렉터리에 다음과 같이 Sinatra 코드를 둘 수 있다.

 

  1. require 'sinatra/base'

    class Hello < Sinatra::Base
         get '/sinatra' do
              "Hello, sinatra"
         end
    end


방법2. Sinatra 애플리케이션을 직접 사용하는 방법

 

두번째 방법은 Metal을 태우지 않고 직접 구현하는 방법이다. 참고로 메탈 애플리케이션은 메탈 체인(chain)을 타야 하기 때문에 HTTP 404 코드를 반환할 수 없으며, 따라서 Rack 미들웨어를 직접 구현할 일도 생길 수 있다. Sinatra 애플리케이션의 코드는 위의 예제 소스와 동일하지만, 이번에는 app/metals 가 아닌 다른 적당한 디렉터리에 두면 된다.
그런 다음 config/environment.rb 파일을 열어 Rails::Initializer.run do |config| 아래에 다음과 같이 미들웨어를 추가해 주면 된다.

 

  1. config.middleware.use 'Hello'

 

이제 Sinatra 애플리케이션이 레일스의 미들웨어로서 장착된다. 확인하려면 rake middleware 명령을 주면 된다.
올 봄, 레일스가 Rack 기반으로 오면서 여러 가지 재미있는 일들이 많이 생길 것 같은 예감이다. 

레일스와 Rack에 대한 더 자세한 내용은 여기를 참조하면 좋을 것이다.(또는 여기)

이 글은 스프링노트에서 작성되었습니다.

by thinkr | 2009/04/09 13:09 | 트랙백(1) | 덧글(0)
레일스 애플리케이션에 OpenID 인증 추가하기

루비온레일스로 새 애플리케이션을 시작하다 보면 늘 성가신 부분 중 하나가 바로 인증이 아닐까. 물론 인증과 관련하여서는 이미 여러 가지 좋은 플러그인들이 나와 있어 그냥 플러그인만 장착하면 그만 아니냐고도 말할 수도 있지만, 사실 인증 처리 만큼 애플리케이션마다 요구사항이 달라지는 부분도 없으며, 그게 바로 레일스가 사용자 인증 처리 부분을 레일스 core에 포함시키지 않는 이유이기도 하다.

 

레일스 애플리케이션에서 사용하는 인증 플러그인 중 가장 보편적인 것은 아직까지는 뭐니뭐니해도 restful_authentication 일 것이다. 여기서는 restful_authenticaion을 통한 통상적인 로그인 방식의 인증과 함께 OpenID 인증도 함께 사용하는 방법을 소개하기로 한다.

 

레일스 애플리케이션에 OpenID 인증을 적용하는 데 관하여는 이미 엄청나게 많은 자료들이 나와 있다. (특히 aproxacs님의 스프링노트 참조). 이 글이 기존의 글들과 조금 다른 점이 있다면 통상적인 ID/패스워드 방식의 로그인과 OpenID 인증 방식을 함께 사용할 경우 생길 수 있는 문제점들을 조금 더 고민해 보고 해법을 시도해 보았다는 것 정도가 될 것이다. 예제는 레일스 2.3.2 버전을 기준으로 하였으며 restful_authentication 플러그인 등 몇 가지 플러그인은 이미 설치했다고 가정하였으니, 플러그인의 설치와 관련된 파일은 각 플러그인의 README 파일을 참조하자.

 

참고로 레일스 2.3에 새로 추가된 템플릿 기능을 사용하여 다음과 같이 새 레일스 애플리케이션을 시작해도 된다.

 

  1. $ rails openid_demo -m /path/to/basic.rb

 

이 예제에서 사용한 템플릿 파일(basic.rb)은 다음과 같다. 여기서 auto_migrations는 필자가 주로 사용하는 방법일 뿐이며, 반드시 필요한 부분은 아니다.

 

  1. # Install plugins
    plugin 'restful-authentication',  :git => 'git://github.com/technoweenie/restful-authentication.git'
    plugin 'open_id_authentication',  :git => 'git://github.com/rails/open_id_authentication.git'
    plugin 'auto_migrations',         :git => 'git://github.com/pjhyett/auto_migrations.git'

    # Setting the plugins
    generate :authenticated, "user sessions --include-activation"
    generate :forgot_password,  "password user"

    rake "open_id_authentication:db:create
    route "map.open_id_complete 'session', :controller => 'sessions', :action => 'create', :requirements => { :method => :get }"

    # Make empty schema.rb for auto_migration
    file "db/schema.rb" <<-CODE
    ActiveRecord::Schema.define() do
    end
    CODE

 

User 모델에 identity_url 필드 추가하기

오픈ID 인증을 위해 필요한 최소한의 필드는 User 객체에 identity_url 필드를 추가하는 것이다. User 모델의 attr_accessor 에도 :identity_url 을 추가하는 것도 잊지 말자.

 

  1.     t.column :identity_url, :string

 

관련 뷰 파일 수정하기

우선 쉬운 것부터 먼저 하자. open_id_authentication 플러그인 README 파일을 참조하여 관련 뷰 파일을 수정하자. 노파심에서 하는 말이지만, 여기 소개된 예제는 그저 하나의 구현 참조일 뿐 반드시 이대로 해야 할 필요는 전혀 없다. TMTOWTDI!

 

  1. <p>
      <label for="openid_identifier">OpenID:</label>
      <%= text_field_tag "openid_identifier" %>
    </p>

 

Session 컨트롤러 수정하기

마찬가지로 open_id_authentication 플러그인 README 파일을 참조하여 session 컨트롤러를 대략 다음과 같은 식으로 수정하자. 이 때 유의할 것은 꼭 여기에 나와 있는 코드 대로 할 필요는 없다는 것이다. 무엇보다도 OpenID의 원리와 spec을 이해하고, 그 기준에 맞게 프로그래밍하는 것이 중요하다. 여기서 open_id_authentication 메서드에 어떠한 인수도 넘기지 않았음에 유의하자. 물론 params[:openid_identifier] 같은 인수를 명시적으로 넘겨도 되지만, open_id_authentication 라이브러리 속에서 자동으로 params 값을 받는 로직이 있기 때문에 굳이 명시적으로 넘길 필요가 없기 때문이다. 더 자세한 것은 open_id_authentication 플러그인의 소스코드를 참조. 또 한가지. 아래 코드 속에서 @user를 로컬변수로 두지 않고 굳이 인스턴스 변수로 둔 이유는 향후 다른 기능을 추가할 때 조금 더 편리하기 때문이다.

 

  1.   def create
        logout_keeping_session!
        if using_open_id?
          open_id_authentication
        else
          password_authentication(params[:name], params[:password])
        end
      end

    protected

      def open_id_authentication
        authenticate_with_open_id(nil, :required => [:nickname, :email]) do |result, identity_url, registration|
          if result.successful?
            @user = User.find_or_initialize_by_identity_url(identity_url)
            if @user.new_record?
              @user.login = registration['nickname']
              @user.email = registration['email']
              @user.identity_url = identity_url
              @user.save(false) # skip validation
            end
            self.current_user = @user
            successful_login
          else
            failed_login result.message
          end
        end
      end

      def password_authentication(name, password)
        if self.current_user = User.authenticate(params[:name], params[:password])
          successful_login
        else
          failed_login "Sorry, that username/password doesn't work"
        end
      end

      def successful_login
        flash[:notice] = "Logged in successfully"
        redirect_back_or_default('/')
      end

      def failed_login(message = "Authentication failed.")
        flash[:error] = message
        redirect_to login_path
      end

 

개인정보 변경하기

이것으로 기본적인 구현은 모두 끝이 났다. 이제 바로 사용하면 된다. 간단하다. 그렇지만 무언가 부족한 부분이 보인다. 예를 들면, 어떤 사용자가 OpenID와 기존 로그인 방식을 병행하여 로그인할 수도 있고, 또 오픈ID로 로그인한 사용자가 자신의 개인정보를 변경할 수도 있다. 또 오픈아이디 사용자와 기존 ID 사용자 간에 ID가 충돌할 경우도 있을 수 있다. 지금부터는 이런 경우에 대해 고려해 보자.

 

우선 간단한 것부터 해보자. 사용자가 자신의 개인정보를 수정할 수 있게 해보자. edit 폼을 만들고 edit와 update 액션을 구현하는 일은 통상적이며 특별한 것이 없다. 다음은 User 컨트롤러의 해당 액션들의 구현 예이다. 로그인 기반이니 login_required를 before_filter로 잡아줘야 함을 잊지 말자. edit 액션의 뷰의 코드는 생략한다.

 

  1.   def edit
        @user = User.find(current_user.id)
      end
     
      def update
        @user = current_user
        if @user.update_attributes(params[:user])
          redirect_to root_path
        else
          render :action => 'edit'
        end
      end

 

한가지 유의할 점은 유효성 검증과 관련된 부분이다. openid를 사용할 경우 별도의 password가 필요 없으며, 따라서 restful_authentication에 들어 있는 password validation과의 충돌이 일어난다. User 모델 객체에서 다음과 같은 식으로 이 부분을 오버라이드해 주면 한다. 여기서는 그냥 간단하게 identity_url이 있는 경우에는 비밀번호 검사를 건너뛰는 걸로 처리하였다.

 

  1.   protected
       
        def password_required?
          identity_url.blank? && (crypted_password.blank? || !password.blank?)
        end

 

Signup 시에 부가정보 받기

오픈ID를 사용할 경우 인증, 즉 자기 스스로 A라고 주장하는 사람이 진짜 A가 맞는지에 대한 검증은 오픈iD 인증으로 끝이 난다. 그리고 오픈ID 인증 시에 인증된 사용자의 등록정보를 넘겨받을 수 있기 때문에 이 정보를 내 서비스에서도 사용할 수 있다. 그렇지만 경우에 따라서는 등록정보 외에 부가적으로 필요한 정보들이 있을 수 있고, 이런 정보들을 회원 가입 시에 추가로 받고자 할 경우가 있다. 여기서는 이름과 휴대폰정보를 부가적으로 받기로 하자. 

 

또 한가지 고려할 사항은 ID가 중복될 가능성이다. 인증 방식으로 오픈ID만 사용하든 아니면 오픈ID와 기존 로그인 방식을 병행하여 사용하든 결국 사용자에게는 고유한 ID 개념이 있어야 하며, 오픈 ID 인증인 경우는 통상적으로 오픈ID의 별명(nickname)을 로그인ID로 사용하게 된다. 그런데 그 별명을 누군가 다른 사람이 이미 자신의 로그인ID로 사용하는 경우가 있을 수 있다. 레일스에서 이 상황은 액티브레코드의 유효성 검증(validation) 처리의 영역에 속한다.

 

우선 가입 시에 부가 정보를 추가할 수 있도록 코드를 개선해 보자. User 모델에 mobile 이란 항목을 하나 추가했다고 가정한다. (mobile 필드를 User모델의 attr_accessor 항목에 추가하는 것도 잊지 말자). 맨 먼저 해야할 일은 Sessions 컨트롤러의 open_id_authentication  메서드를 다음과 같이 약간 변경하여 신규 오픈ID인 경우에는 부가 정보입력 폼을 띄워 부가정보를 입력할 수 있도록 하는 것이다. 

 

  1.   def open_id_authentication
        authenticate_with_open_id(nil, :required => [:nickname, :email]) do |result, identity_url, registration|
          if result.successful?
            @user = User.find_or_initialize_by_identity_url(identity_url)
            if @user.new_record?
              @user.login = registration['nickname']
              @user.email = registration['email']
              @user.identity_url = identity_url
              render :template => 'users/new'
            else
              self.current_user = @user
              successful_login
            end 
          else
            failed_login result.message
          end
        end
      end

 

여기서는 편의상 별도의 폼을 만들지 않고 기존의 new 폼을 사용하는 걸로 하였으며,  오픈ID로 로그인하는 경우에는 비밀번호 입력 부분이  폼 상에서 표시되지 않도록 처리하였다. new폼에서 identity_url을 히든 필드로 넘기고 있음에 유의하자. 이 필드가 있어야 User 객체가 저장될 때 password 유효성 검증을 skip할 수 있기 때문이다.

 

  1. <% form_for :user, :url => users_path do |f| -%>

    <%= f.hidden_field :identity_url %>

    <p><%= label_tag 'login' %><br/>
    <%= f.text_field :login %></p>

    <p><%= label_tag 'email' %><br/>
    <%= f.text_field :email %></p>

    <p><%= label_tag 'name' %><br/>
    <%= f.text_field :name %></p>

    <p><%= label_tag 'mobile' %><br/>
    <%= f.text_field :mobile %></p>

    <% if @user.identity_url.blank? %>
        <p><%= label_tag 'password' %><br/>
        <%= f.password_field :password %></p>

        <p><%= label_tag 'password_confirmation', 'Confirm Password' %><br/>
        <%= f.password_field :password_confirmation %></p>
    <% end %>

    <p><%= submit_tag 'Sign up' %></p>
    <% end -%>

 

마지막으로 User 컨트롤러의 create 액션을 다음과 같이 약간 변경하여, current_user 세션 셋팅을 처리해 주면 된다.

 

  1.   def create
        logout_keeping_session!
        @user = User.new(params[:user])
        success = @user && @user.save
        if success && @user.errors.empty?
          self.current_user = @user
          redirect_back_or_default('/')
          flash[:notice] = "Thanks for signing up!  We're sending you an email with your activation code."
        else
          flash[:error]  = "We couldn't set up that account, sorry.  Please try again, or contact an admin (link is above)."
          render :action => 'new'
        end
      end

 

2개 이상의 오픈ID 사용하기(오픈ID 목록 관리하기)

한 사람이 2개 이상의 오픈ID를 가지는 경우도 있을 수 있다. 이럴 경우 둘 중 어느 ID로 로그인하든 동일인으로 인식하여 동작하도록 하면 좋을 것이다. 지금부터 그 기능을 구현해 보기로 하자. 기존 계정에 오픈ID를 새로 추가하거나 삭제할 수 있게 하는 것이다. 우선 한 사람이 여러 개의 오픈ID를 가질 수 있는 부분을 구현하자. UserOpenid 모델을 만들면 될 것이다. User와  UserOpenid 간에 has_many 관계를 설정하는 등의 작업은 생략한다.

 

  1.   create_table :user_openids do |t|
        t.column  :openid_url, :string,  :null => false
        t.column  :user_id,    :integer, :null => false
        t.timestamps
      end

 

이어서 routes.rb 파일도 연관관계에 맞도록 변경해 주자.

 

  1.   map.resources :users, :has_many => :openids

 

마지막으로 openid를 처리할 Openids 컨트롤러도 하나 생성하자.  이제 코딩을 시작하자. 우선 간단한 것부터 시작하자. users/edit 페이지에 OpenID를 추가하는 부분을 추가하자. edit 뷰 하단에 다음 코드를 추가한다.

 

  1. <h3>Existing Openids</h3>
    <ul>
        <% current_user.openids.each do |openid| %>
            <li><%= openid.openid_url %></li>
        <% end %>
    </ul>

    <%= link_to 'Add New OpenID', new_user_openid_path(@user) %>

 

이 때 'Add New OpenID' 링크를 클릭했을 경우 보게될 openids/new 페이지는 통상적인 폼이므로 생략하기로 하고, 이 new 폼을 제출했을 경우 호출될 openids 컨트롤러의 create 액션에서는 앞서 sessions 컨트롤러의 create 액션에서 수행한 것과 동일한 인증 로직이 수행되어야 한다. 즉, 사용자가 자기 것이라고 우기는 OpenID가 실제 자기 것인지를 검증하는 절차가 필요하며, 이 때 사용되는 로직은 앞서 sessions 컨트롤러에 있던 open_id_authentication 메서드의 로직과 비슷하다. 비슷한 로직을 따로따로 구현할 수도 있지만 여기서는 기존에 Sessions 컨트롤러 속에 있던 open_id_authentication 코드를 Application 컨트롤러로 이동하고 다음과 같이 약간 수정하는 걸로 하였다. 즉, 아직 로그인하지 않은 사용자가 이 메서드를 호출하는 경우에는 기존처럼 작동하고, 이미 로그인한 사용자가 이 메서드를 호출하는 경우에는 OpenID를 추가하는 요청으로 인식하여 그렇게 작동하게 한 것이다.

 

  1. protected

      def open_id_authentication
        authenticate_with_open_id(nil, :required => [:nickname, :email]) do |result, identity_url, registration|
          if result.successful?
            unless logged_in?
              @user = User.find_or_initialize_by_identity_url(identity_url)
              if @user.new_record?
                @user.login = registration['nickname']
                @user.email = registration['email']
                @user.identity_url = identity_url
                # @user.save(false) # skip validation
                render :template => 'users/new'
              else
                self.current_user = @user
                successful_login
              end 
            else
              @user = current_user
              @openid = @user.openids.build(:openid_url => identity_url)
              if @openid.save
                redirect_to edit_user_path(current_user)
              else
                render :template => 'openids/new'
              end
            end 
          else
            failed_login result.message
          end
        end
      end

 

이걸로 끝일까? 유감스럽게도 아니다. 이직도 해야할 일들이 몇 가지 남아 있다. 우선 다음부터 이 사용자가 새로 추가한 오픈ID로 로그인할 수도 있게끔 해 줘야 한다. 또 이미 추가한 오픈ID를 삭제할 수도 있어야 할 것이다. 우선 다른 오픈ID로 로그인하는 경우부터 처리하도록 하자. 앞서 만든 open_id_authentication 로직을 다음과 같이 조금 변경하여, 오픈ID를 검색하여 User를 추출하는 로직을 추가하자. 코드가 길어지면서 코드에서 안좋은 냄새가 나긴 하지만 리팩터링은 일단 뒤로 미루자.

 

  1.       if result.successful?
            unless logged_in?
              @user_openid = UserOpenid.find_by_openid_url(identity_url)
              if @user_openid
                self.current_user = @user_openid.user
                successful_login
              else
                @user = User.find_or_initialize_by_identity_url(identity_url)
                if @user.new_record?
                  @user.login = registration['nickname']

 

또 한가지 구현할 것은 오픈ID의 중복과 관련된다. OpenID는 unique해야 하기 때문이다. UserOpenid 모델에 다음과 같이 유효성 검증을 추가하면 될 것이다.

 

  1. class UserOpenid < ActiveRecord::Base
      belongs_to :user

      validates_presence_of :openid_url
      validates_uniqueness_of :openid_url
    end

 

끝맺음 - 리팩터링과 DRY!

여기서는 단계별로 구현을 하다보니 User모델에 identity_url 필드가 있고 또 UserOpenid 모델 속에도 openid_url 필드가 존재하는 엉성한 구조가 되었지만, 만약 처음부터 여러 개의 오픈ID를 상정하고 디자인했다면 User모델의 identity_url 필드는 불필요했을 것이며, open_id_authentication 메서드의 코드도 조금 더 간명해 졌을 것이다. 또한 여기 적힌 코드들이 완전한 것은 아니며 군데군데 헛점도 몇 군데 있을 수 있다. 이 글에서 제시하려고 한 것은 완전하고 완벽한 OpenID 인증은 아니며 어디까지나 구현에 참조할 만한 내용을 보이고자 한 것이니,  리팩터링이나 DRY! 및 기능 개선과 관련된 부분의 처리는 독자 여러분께 숙제로 남긴다.

 

이 글은 스프링노트에서 작성되었습니다.

by thinkr | 2009/03/27 16:05 | 트랙백 | 덧글(2)
< 이전페이지 다음페이지 >