Sorcery VS Devise

Sorcery 소개

ca62lrg

얘는 소서리스에요..

Devise는 워낙 유명해서 별도의 설명이 필요 없겠지만 Sorcery의 경우 상대적으로 덜 유명해서 간략하게 소개를 먼저 해본다.

이스라엘 개발자 Noam Ben Ari(이후 NoamB)가 만든 Authentication Gem으로 2010년 10월에 첫 커밋이 나왔다.
필자는 이전까진 restful-authentication를 사용했는데 Rails 3가 출시했음에도 불구하고 대응이 늦었다.
그때 혜성처럼 나타난 게 몇 가지 있었는데 Sorcery, Devise도 그것들 중 하나였다. (지금까지 살아남은 녀석들도 아마 이 두 녀석뿐일 듯)
처음에는 Devise를 사용했었지만 RailsCasts에서 Sorcery를 소개해줬고, 이때부터 쭉 Sorcery를 사용해왔다.

NoamB 거의 혼자서 개발을 해오다시피 했고, 지금은 개인 Repository가 아닌 Sorcery의 Repository를 만들어서 관리하고 있고, NoamB는 현제 일선에서 물러나 Chase GilliamJosh Buker 이 두 사람이 Maintainers로 관리를 하고 있다.

Sorcery만 소개해주면 Devise한테 미안하니 간략히 소개하자면 SimpleForm을 만든 plataformatec이 만들고 관리하고 있다.

기능 비교

사실 처음에는 그냥 느낀 점만 간략하게 쓸려고 했는데.. 그럼 너무 성의 없어 보일까 봐 간략하게 정리해봤다.
기능 위주로 선택하려 한다면 아래 표가 도움이 되시리라

기능설명 Sorcery Devise
Authentication Core Database Authenticatable
OAuth External Omniauthable
회원가입시 메일 인증 프로세스 User Activation Confirmable
암호 리셋 프로세스(메일 전송) Reset Password Recoverable
회원가입 Core Registerable
자동 로그인 Remember Me Rememberable
로그인/아웃등 유저 행적 기록 Activity Logging Trackable
Session Timout 설정 Session Timeout Timeoutable
Validation email, password Validatable
로그인 실패 여러번시 계정 잠금 Brute Force Protection Lockable
HTTP Authentication Basic HTTP Authentication Database Authenticatable

Validation이 Sorcery에는 없지만 이건 Rails의 Validators를 이용하면 해결된다. (Rails를 아시는 분이라면 Devise가 Validation을 왜 따로 제공하는지 이해가 안가실 수도 있는데, 그건 아래에 설명하겠다)
보면 알겠지만 방법의 차이만 있을 뿐 제공하는 기능은 동일하다.
그러므로 기능의 수는 안타깝게도 무승부.

간단히 Authentication 위주로만 쓸 경우

문서 살펴보고 맘에 드시는 걸로 선택하면 된다.

57f8la

장난 아니고, 진짜에요..;;

여러 부가기능을 쓰지만 수정 안 하고 쓸 경우

Devise를 추천한다.
Devise는 Boilerplate가 아주 많이 제공되어서 여러 부가 기능을 사용할 때 편리하다.
하지만 많은 커스터마이징이 필요한 경우에는 적합하지 않다. (우리가 알고 있듯 Boilerplate는 양날의 검인데, 그 단점이 여기에도 해당된다고 보면 된다)
왜냐하면 Devise는 많은 부분들이 감추어져 있다 보니, 구현 단계에서의 수정이 쉽지 않다.
그래서 커스터마이징이 필요한 사람들을 위해 Registerable, Validatable 같은 부가기능을 통해 커스터마이징을 하도록 하고 있지만 안타깝게 이마저도 쉽지 않다.  더욱이 문서 또한 레퍼런스가 아닌 API만 제공하다 보니 실제 Usage를 알기가 어렵다.

여러 부가기능 및 커스터마이징이 필요한 경우

Sorcery를 추천한다.
Sorcery는 Boilerplate가 없다고 봐도 무방하다. 그렇기 때문에 많은 부분들을 처음부터 쌓아올려야 한다.
별도의 수정 없이 사용할 때는 이게 그렇게나 귀찮고 짜증 나지만, 반대로 커스터마이징을 많이 해야 하는 경우 이처럼 좋을 수도 없다.
기본적인 건 다 Sorcery가 제공하고 그것들을 어떻게 사용할지는 내 입맛에 맞춰서 만들 수 있으니!!

권한 관리가 필요한 경우

Devise의 막강한 기능 중 하나가 CanCan, Rolify 같은 녀석들 때문이다.
CanCan은 권한 관리 Gem으로 페이지, Row(DB의 row) 단위로도 권한 설정이 가능하다고 한다. (실 서비스에 써본 적은 없다. 사용하려고 했는데, 커스터마이징을 너무 많이 해야 해서 그냥 하나 만들어서 사용했다)
CanCan은 Object에 대한 권한 관리라면, Rolify는 역할 관리라고 보면 된다. 예를 들자면 ‘유저’, ‘관리자’, ‘스텝’ 등으로 역할별로 권한을 관리할 때 사용하는 Gem이다.
(역시 실 서비스에 써본 적은 없다. Rolify가 등장하기 전에 이런 기능이 필요해서 만들어서 쓰고 있..)

암튼 이러한 녀석들이 공식적으로 Devise에 잘 붙는다고 적혀있고, 많은 사람들이 Devise + CanCan + Rolify 이렇게 트리플 콤보세트로 사용한다.

근데 CanCan, Rolify를 써보니 어랏? 딱히 Devise와의 Dependency가 안 보인다. 즉 Sorcery에서도 사용할 수 있을 것 같긴 한데.. 나중에 시간 나면 한번 해봐야겠다.
혹 해보신 분 계시면 알려주세요~!

정리

Sorcery Devise
소상공인 기업
가난하다 부자다
Screen Shot 2018-04-04 at 11.19.22 PM Screen Shot 2018-04-04 at 11.19.09 PM
로고도 없다 부자는 다르다.
가난해서 Boiler못킨다. 돈 많아서 Boilerplate가 빵빵하다
가난해서 감출게 없다. 많은 부분들이 감추어져 있다.
그래서 커스터마이징이 쉽다. 그래서 커스터마이징이 어렵다.
github wiki가 아주 깔끔하게 정리가 되어있어서 다른 문서 볼 필요가 없다.
Boilerplate와 설명까지 다 빵빵하다.
github wiki 또한 보기 어렵게 되어있다.
공식 문서는 API만 제공되고, Usage는 찾기 힘들다.
Boilerplate가 있긴 하지만 설명도 없고, Boilerplate를 어떻게 사용해야 하는지 알려주는 문서 또한 없다.
github wiki 페이지 수가 25개로 아주 깔끔하게 되어 있다.
(내가 만들거나 기여한 페이지들도 몇 개 있..읍읍)
github wiki 페이지 수가 130개다. 역시 부자는 다르다.

Sorcery를 오랫동안 사용하기도 했고, 기여도 해서 그런지.. 나도 모르게 글 자체가 Sorcery에 조금 더 편향되게 쓰여진것긴 한데.. 사실 아주 오래전 Devise를 주제로 글을 썼었다. 그것도 2개나! (언제까지 restful-authentication을 쓸것인가! devise도 써보자!devise에서 sign_out시 서버가 기절할 경우. (Ruby Or Rails Bug인듯))
블로그 이사하면서 양식이 다 깨져서 지금은 내용을 알아보기 힘들지만, 어쨌든 썼었다!

아, 그게 중요한 게 아니고.. 흠흠
둘 다 모두 검증된 Authentication 툴이니 믿고 사용하시길

Advertisements

Rails AR 속도 테스트

전 포스팅에 이어 각 케이스별 Benchmark 결과 공유해볼까 한다.
Benchmark 사용법 및 자세한 내용은 아래 링크를 확인하시면 된다.
http://www.ruby-doc.org/stdlib-2.0/libdoc/benchmark/rdoc/Benchmark.html

1. 우리가 흔히 사용하는 100% AR을 사용한 경우.
puts Benchmark.measure{User.limit(100000).to_a;nil}
120.070000 1.790000 121.860000 (122.293380)
총 소모된 시간 약 122초

2. 컨넥션만 AR을 이용한 후 row값을 배열로 받는 경우
puts Benchmark.measure{ActiveRecord::Base.connection().select_rows(‘select * from users limit 100000’);nil}
8.110000 1.560000 9.670000 ( 9.995749)
총 소모된 시간 약 10초

3. 컨넥션만 AR을 이용한 후 row값을 해쉬로 받는 경우 (mysql2만 사용한것과 같은 리턴 타입)
puts Benchmark.measure{ActiveRecord::Base.connection().select(‘select * from users limit 100000’);nil}
8.790000 1.320000 10.110000 ( 10.415056)
총 소모된 시간 약 10초

4. 컨넥션만 AR을 이용한 후 날것(raw)로 받는 경우
Benchmark.measure{ActiveRecord::Base.connection().execute(‘select * from users limit 100000’);nil}
0.160000 0.030000 0.190000 ( 0.501308)
총 소모된 시간 약 0.5초

5. mysql2를 이용한 경우.
puts Benchmark.measure{mysql.query(“select * from users limit 100000”);nil}
0.150000 0.030000 0.180000 ( 0.469528)
총 소모된 시간 약 0.5초

6. mysql2에서 row값을 Array로 받는 경우.
puts Benchmark.measure{mysql.query(“select * from users limit 100000”, as: :array).to_a;nil}
0.140000 0.020000 0.160000 ( 0.474315)
총 소모된 시간 약 0.5초

7. mysql2에서 결과값을 Array로 변환한 경우.
puts Benchmark.measure{mysql.query(“select * from users limit 100000”).to_a;nil}
27.890000 3.050000 30.940000 ( 31.259377)
총 소모된 시간 약 31초

같은 결과값 타입끼리 비교를 해보면

결과값 타입 AR mysql
배열 2. 컨넥션만 AR을 이용한 후 리턴값을 배열로 받는 경우
약 10초
7. mysql2에서 타입을 Array로 요청한 경우.
약 0.5초
해쉬 3. 컨넥션만 AR을 이용한 후 리턴값을 해쉬로 받는 경우
약 10초
5. mysql2를 이용한 경우.
약 0.5초

mysql2의 압도적인 승리다.

사실 4.AR이용 날것(raw)과 5.mysql2의 리턴받는 객체는 똑같다.
둘다 Mysql2::Result를 따라간다.
하지만 4번은 Array로 형변환을 하고, 5번은 Hash로 형변환을 한다는 차이점이 있다.
Screen Shot 2013-06-14 at 10.22.25 PM

Rails의 AR이 대량의 데이터를 읽어올때 생기는 속도 문제.

DB에는 약 500,000개의 rows가 있다.

A액션을 하게되면
DB에 쿼리를 날리게 되고 약 75,000개의 rows가 return된다.
이 데이터를 가지고 알고리즘을 돌리게 되는데
문제는 고작 75,000개의 데이터를 처리하는데 시간이 너무 오래 걸리는 것이다.
약 80초가 걸리는데 이건 뭔가 문제가 있다.
SELECT 쿼리는 최적화가 되어있는 상태로 문제가 없었다.

도대체 어디서 이렇게 시간을 많이 잡아먹나 확인을 해보니
DB에 쿼리를 날리고 난 후 알고리즘 들어가기 전까지가 엄청나게 많은 시간을 잡아먹는 것이었다.

ActiveRecord가 DB 결과값을 변환하는데 걸린 시간: 약 75초 알고리즘 로직이 돌아간 시간: 약 3.4초

ActiveRecord가 DB 결과값을 변환하는데 걸린 시간: 약 75초
알고리즘 로직이 돌아간 시간: 약 3.4초
총 연산시간: 약 80초

쿼리도 문제없고, 알고리즘도 문제없다.
하지만 둘 사이에 알 수 없는 작업으로 시간이 오래 걸린다면..
의심해볼 건 ORM.. 바로 ActiveRecord(이하 AR)

그래서 과감하게 AR을 버리고 mysql2(gem)만 이용하도록 수정을 했다. (mysql2… mysql을 사용하면 필수적으로 설치하는 그 gem 맞다.)
그랬더니 시간이 급속적으로 향상되었다.

DB 결과값을 변환하는데 걸린 시간: 약 0.3초
알고리즘 로직이 돌아간 시간: 약 0.7초
총 연산시간: 약 1.1초

80초에서 1초로 줄었. 응?
눈을 비비고 다시 한번 확인.
혹시나 싶어서 여러번 A액션 재실행.
헐.. 대박 ㅋㅋ
말도 안되는 성과 도출!
쩔어 쩔어

여기서 주목할 점은 알고리즘부분까지 소모시간이 줄었다는 것이다.
이것 또한 같은 이유로 인함인데
여기서 AR의 동작원리라고 하기엔 좀 거창하지만를 이해할 필요가 있는데
mysql2은 결과값을 해쉬(Hash)로 보관하게 되는데
(엄밀히 따지면 Mysql2::Result에 Hash형태로 각 row가 보관이 된다.)
사실 AR도 mysql2를 이용해서 넘겨받은 해쉬를 AR(데이터 타입)로 변환하게 된다.
알다시피 AR(데이터 타입)에는 각종 모듈 및 헬퍼등 살이 디룩디룩 찌고 다리가 4개인 우르곳이라면
반면 해쉬는 Ruby Core lib 모듈 포함된 녀석으로 아주 앙상하게 마른 뼈밖에 없는 피들스틱이랄까?
(Rails에서 사용하는 해쉬는 포식시켜 스택이 3정도 쌓은 초가스랄까? 뭐래는겨 ㅋㅋㅋ)

결론
결국 AR이 무겁고, 이렇게 무거운 AR을 사용하기 때문에 생기는 이슈인 것이다.
그렇다고 AR이 나쁘다! 구리다! 하자다! 라고 하기엔 어폐가 있다.
Rails는 웹 개발 프래임웍이다. 웹 개발이 목적이고 이를 위해 만들어진 ORM이 AR이다.
이런 AR에게 데이타를 몇 만건씩 가져오는걸 기대하는것 자체가 문제가 있다.
보편적인 웹 서비스에선 몇 만건씩 데이터를 가져오는 일 따위는 일어나지 않을것이니

그러므로 배치성 작업 혹은 많은 량의 데이트를 컨트롤해야 하는 경우에
Rails가 필요 없다면 그냥 Ruby로만 짜는것이 좋고,
Rails를 써야만 한다면 AR사용은 고민해볼 필요가 있다.

덤.
3,500개의 rows를 AR이 변환하는데 걸리는데 1.8초정도 소모가 되었다.
고작 3,500개밖에 안되는데도 약 2초가 소모된다.
소량의 작업에선 속도가 문제가 안되지만..
1,000건이 넘어갈 경우에는 꼭 테스트를 해보길 권장한다.

+ 추가
아직 확인은 못해봤지만.. Sangmin Ryu님의 실서비스 테스트 결과를 보면 속도가 느리지 않다
물런 하드웨어 스펙이 차이가 좀 많이 나는것 같지만.. ㅋㅋ 그걸로 설명하기엔 속도차가 너무 크다.
모델의 덩치와도 관계가 있다고 추측된다.
일단 해당 이슈가 발생한 모델의 필드가 약 50개이고 그 중 10개는 text이다.
뿐만 아니라 메소드 또한 많은데 이 모델의 소스 코드가 약 600줄정도 된다.

AR이 해야할 작업이 많아져서 속도가 느려졌던걸로 판단된다.

+ 추가
AR 속도 테스트

Sorcery로 OAuth시 Landing page

왕복선을 엎은 747기 착륙 모습Sorcery를 이용해서 Facebook 로그인을 하는데..

로그인 성공 후 로그인한 페이지를 landing page로 할려고 하는데.. 이게 안되는거야.

session에 referer를 넣어놓았는데.. 값이 계속 초기화되.

이상하다 싶어서 Sorcery 까보니깐.. 아니나 다를까.

Sorcery가 reset_session을 하고 있었어 -_-;

https://github.com/NoamB/sorcery/blob/master/lib/sorcery/controller/submodules/external.rb#L58

그리고 위 코드 보면 알겠지만 session[:return_to_url]에 담아서 return url을 관리하고 있어.

우린 그냥 편하게 redirect_back_or_to만 사용하면 끝나.

정리를 하자면..

이건 그냥 Sample 코드니깐. 알아서 적용하고.

핵심은

  • store_location 위치 및 하는 일
  • session[:return_to_url]
  • redirect_back_or_to

사실 읽어보고 별거 없네? 생각했지?

근데말이야.. 이 내용 문서에 언급이 안되어있어 -_-

그리고 지금 생각난건데, 꽤 오래전에 이 이슈로 삽질했던게 이제서야 기억났어. -_-

쩝. 암튼 이 내용 Github wiki에 올렸는데 아마 짤리겠지? ㅋㅋㅋ

carrierwave. (re)create image

carrierwave사용시, 새로운 version을 추가하거나 기존의 version중 process를 변경해야 하는 경우가 종종 발생합니다.

이때 새로 만든 혹은 변경된 version의 이미지를 생성을 해야 하는데, 방법은 크게 2가지 입니다.

  • 이미지을 재업로드해서, 이미지를 만드는 방법
  • 해당 이미지만 만드는 방법

1. 이미지 재업로드 하는 방법

은 웹페이지에서 일일이 하나씩 해주는 훌륭한 방법이 있습니다.

책 삽질마스터 표지

하지만 좀 더 아름다운 방법은 rails console에서 코드 한줄로 해결하는 방법이죠.

User.all.each { |u| u.update_attributes(profile: "#{Rails.root}/public/#{u.profile_url}") if u.profile.present? }

version 변경이 자주 일어나는게 아니라면, 위 코드 한줄로 해결하는것도 나쁘지 않은 방법입니다.
다만.이렇게 하게되면 model에 걸려있는 filter들도 함께 동작을 하게 됨으로 불필요한 리소스를 낭비하게 됩니다.

그래서

2. 해당 이미지만 만드는 방법

으로 가는것이 좀 더 나은 방법입니다.

여기서 miniMagick을 이용하는 모듈로 만들수 도 있는데, 상황에 따라서 RMagick을 쓸 수 도 있는것이라 Carrierwave에 모듈을 추가하는 방법이 안정적입니다.

사실 Carrierwave에 recreate_versions! 이란 메소드가 있습니다.
recreate_versions!은 모든 version을 다 만드는건데, (하단 ps.2참고)
특정 version 하나만 만드는 함수는 없습니다.

그래서 특정 version 하나만 만드는 함수를 만들어야 하는데.. 다행스럽게도 누군가가 코드를 짜놓았더군요.

감사하는 마음을 가지고 그대로 써먹습니다.

아래 코드를 아무대나 넣어줍니다.

저의 경우 config/initializers에 넣었습니다.

그리고 난 후

console창으로 가셔서 uploader.recreate_version!([VERSION]) 해주시면 됩니다.

예)

User.all.each { |user| user.profile.recreate_version!([VERSION]) }

근데, version 수정이 비일비재하게 일어난다면..

그때마다 console들어가서 일일이 타이핑해주기가 여간 귀찮은게 아닙니다.

그래서 간결하게 밖에서 과업을 수행할 수 있도록 task로 만들어봤습니다.

방법은 간단합니다.

위 코드를  lib/안에 적당한 파일명으로 넣습니다.

사용법은

rake generate_photo_version[“모델명”,”업로더이름”,”새로운 썸네일을 생성할 버전명”,”오버라이트 할것인지”]
입니다.
예를들면
  • User라는 모델에
  • profile이라는 uploader가 있고
  • normal 이라는 version을 추가했다면

rake generate_photo_version[user,profile,normal,false]

다른예로..

version :normal의 process를 변경하여.. 이미지를 새로 만들어야 할때


rake generate_photo_version[user,profile,normal,true]

하시면 됩니다.

PS

  1.  속도차이는 원본의 용량과 관계가 있을뿐,
    이미지 한개를 만들든 10개를 만들든.. 속도 차이는 미비합니다.
  2. 만약 모든 version을 다 다시 만들고 싶은 경우
    recreate_versions!를 사용하시면 됩니다. (https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/versions.rb)
    사용예) User.first.profile.recreate_versions!
  3. http://menonrails.com/articles/33 에 투고되었습니다.

참고링크

Rails 3.1 Overview

 
아시다시피 5월 5일 Rails 3.1 Beta 1 이 Release되었다.
많은것들이 바뀌었는데 그것들중 알고 넘어가면 좋을법한것 몇가지만 추려 간략하게 Overview해보았다.

설치

rvm 1.9.2@railspre –create
gem install rails –pre

 

Javascript

Rails 3.1부터 prototype.js가 아닌 jQuery와 CoffeeScript가  Default로 탑재되어 있다.
만약 jQuery가 아닌 prototype.js를 계속 사용하고자 할 경우,

rails new [APP_NAME] -j prototype

해주면 된다.

새로 추가된 Gems

  • sass: haml에 포함되어 있던 sass가 독립되어 나왔다.
  • coffee-script
  • uglifier: JS를 하나의 파일로 합쳐준다.
  • jquery-rails

 

디렉토리 구조 변경

Public아래에 있던 Javascript, Stylesheets, Images가
/app/assets/ 로 이동되었다.
그러므로 웹엔진(Nginx등)에서 Js, Css, Image를 Rails한테 안넘기고 직접 처리하도록 세팅이 되어 있었다면, 관련 부분은 수정이 있어야 한다

Sprockets

application.css, application.js에 보면

 //= require jquery

처럼 주석처리된 require들이 있는데, 이건 주석이 아니라 sprockets의 require(include)하는 문법이므로 해당 구문을 삭제하면 include가 안된다.

Migration

migration file의 change 메소드가 추가되었다.
과거에는 up, down으로 메소드가 분리되어 있었지만, change로 통합되었다.
하지만 종종 제대로 동작하지 않을때도 있는데 그럴땐 up, down을 만들어서 사용하면 된다.

그리고 :belongs_to가 추가되었는데, 과거 relationship을 같게 될 경우 직접 index key(article_id:integer)를 만들어 줬었다.
3.1 에서는 belongs_to를 이용하면 되는데.

rails g scaffold comment article:belongs_to body:text

이렇게 써주면되며, belongs_to로 만들면 add_index도 시켜준다.

Console

콘솔에서 DB Query가 노출이 되었는데.. 상황에 따라선 이게 과잉친절이 될 수 도 있을 듯 하다.

Active Record

  • :has_many에서 :through를 다중깊이까지 사용할 수 있게 되었다.
  • 그리고 :through에서 :dependent 도 지원을 한다.
  • :as 가 추가되었다.
  • default_scope가 block으로 처리할 수 잇게 되었으며 lambda, method를 사용할 수 있다.

    default_scope { … }
    default_scope lambda { … }
    default_scope method(:foo)

  • update_column이 추가되었는데, update_attribute와 동작은 유사하지만 validation과 callback을 무시하고 update한다.

    User.first.update_column(:name, “sebastian”)

  • inverse_of가 추가되어서 예제 코드처럼 post.tags.build가 가능하다.
     

    class Post < ActiveRecord::Base
    has_many :taggings
    has_many :tags, :through => :taggings
    end

    class Tagging < ActiveRecord::Base
    belongs_to :post
    belongs_to :tag, :inverse_of => :tagging # :inverse_of must be set!
    end

    class Tag < ActiveRecord::Base
    has_many :taggings
    has_many :posts, :through => :taggings
    end

    post = Post.first
    tag = post.tags.build :name => “ruby”
    tag.save # will save a Taggable linking to the post

     

  • has_many의 :conditions에서 sql을 직접 쓰는것의 사용법이 바뀌었다.
    // Before
    has_many :things, :conditions => ‘foo = #{bar}’

    // After
    has_many :things, :conditions => proc { “foo = #{bar}” }

  • has_secure_password라는 메소드가 추가가 되었는데, Authentication을 직접 만들어서 쓸 경우 아주 유용하게 사용할 수 있다.
    새삼스럽지만, 3.1되면서 부터 Authentication Tool은 안써도 될 듯 하다.

 

Action Pack

 

  • auto_link가 제거되었다. 만약 계속 사용할것이라면 rails_autolink gem을 이용하면 된다.
  • :authenticity_token 을 form_tag에서 option으로 바뀌었다. 생략하거나 수정이 가능하다.
  • rhtml, rxml이 제거되었다.
  • csft_meta_tag가 복수로 rename되었다. csft_meta_tags
  • HTML5의 “data” attribute를 hash로 제공한다
    tag(“div”, :data => {:name => ‘Johan Kim’, :city_state => %w(Seoul)})

 

Active Support

 

  • Object#in?
  • weeks_ago
  • pre_week

간략하게 이 정도 인 것 같다.
관심이 있다면 https://gist.github.com/958283 를 확인하면 자세한 내역이 나온다.

http://menonrails.com/articles/14 에 기고된 포스팅 입니다.

참고링크