1 디버깅을 위한 View Helper
일반적인 작업 중 하나는 변수의 내용을 검사하는 것입니다. Rails는 이를 위한 세 가지 다른 방법을 제공합니다:
debug
to_yaml
inspect
1.1 debug
debug
헬퍼는 YAML 형식을 사용하여 객체를 렌더링하는 <pre> 태그를 반환합니다. 이는 모든 객체로부터 사람이 읽을 수 있는 데이터를 생성합니다. 예를 들어, 뷰에 다음과 같은 코드가 있다면:
<%= debug @article %>
<p>
<b>제목:</b>
<%= @article.title %>
</p>
다음과 같은 것을 볼 수 있습니다:
--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: Rails 앱을 디버깅하는데 매우 유용한 가이드입니다.
title: Rails 디버깅 가이드
published: t
id: "1"
created_at: 2008-09-05 22:55:47
attributes_cache: {}
Title: Rails 디버깅 가이드
1.2 to_yaml
다른 방법으로는, 어떤 객체에든 to_yaml
을 호출하면 YAML로 변환됩니다. 이렇게 변환된 객체를 simple_format
helper 메서드에 전달하여 출력 형식을 지정할 수 있습니다. 이것이 debug
가 마법을 부리는 방법입니다.
<%= simple_format @article.to_yaml %>
<p>
<b>제목:</b>
<%= @article.title %>
</p>
위 코드는 다음과 같이 렌더링될 것입니다:
--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: Rails 앱을 디버깅하는데 매우 유용한 가이드입니다.
title: Rails 디버깅 가이드
published: t
id: "1"
created_at: 2008-09-05 22:55:47
attributes_cache: {}
Title: Rails 디버깅 가이드
1.3 inspect
배열이나 해시로 작업할 때 객체 값을 표시하는 또 다른 유용한 메서드는 inspect
입니다. 이는 객체 값을 문자열로 출력할 것입니다. 예를 들면:
<%= [1, 2, 3, 4, 5].inspect %>
<p>
<b>제목:</b>
<%= @article.title %>
</p>
Will render:
렌더링 될 것입니다:
[1, 2, 3, 4, 5]
제목: Rails 디버깅 가이드
2 로거
런타임에 로그 파일에 정보를 저장하는 것이 유용할 수 있습니다. Rails는 각각의 런타임 환경에 대해 별도의 로그 파일을 관리합니다.
2.1 Logger란 무엇인가?
Rails는 로그 정보를 작성하기 위해 ActiveSupport::Logger
클래스를 사용합니다. Log4r
과 같은 다른 logger도 대체하여 사용할 수 있습니다.
config/application.rb
또는 다른 환경 파일에서 대체 logger를 지정할 수 있습니다. 예를 들면:
config.logger = Logger.new(STDOUT)
config.logger = Log4r::Logger.new("Application Log")
기본 logger는 다른 logger 객체로 대체될 수 있습니다. 예를 들어 Log4r
이나 다른 logging framework를 사용할 수 있습니다.
Initializer
섹션에서 다음 중 아무것이나 추가하세요
Rails.logger = Logger.new(STDOUT)
Rails.logger = Log4r::Logger.new("Application Log")
이를 통해 logger를 다른 logging utility로 대체할 수 있습니다. STDOUT에 기록하거나 Log4r logger를 사용하는 등의 방법이 있죠.
기본적으로 각 로그는 Rails.root/log/
아래에 생성되며 로그 파일의 이름은 애플리케이션이 실행되는 environment를 따서 지어집니다.
2.2 Log Levels
어떤 것이 로깅될 때, 메시지의 log level이 설정된 log level과 같거나 더 높은 경우에만 해당 로그에 출력됩니다. 현재 log level을 확인하고 싶다면 Rails.logger.level
메서드를 호출하면 됩니다.
사용 가능한 log level은 다음과 같습니다: :debug
, :info
, :warn
, :error
, :fatal
, 그리고 :unknown
이며, 각각 0부터 5까지의 log level 숫자에 해당합니다. 기본 log level을 변경하려면 다음을 사용하세요
config.log_level = :warn # 환경 initializer에서, 또는
Rails.logger.level = 0 # 언제든지
프로덕션 로그에 불필요한 정보가 넘치지 않게 하면서 development나 staging 환경에서 로깅을 하고 싶을 때 유용합니다.
Rails의 기본 로그 레벨은 :debug
입니다. 하지만 기본으로 생성되는 config/environments/production.rb
에서 production
환경의 경우 :info
로 설정됩니다.
2.3 메시지 보내기
현재 log에 작성하려면 controller, model, mailer 내에서 logger.(debug|info|warn|error|fatal|unknown)
method를 사용하세요:
logger.debug "사람 속성 해시: #{@person.attributes.inspect}"
logger.info "요청을 처리하는 중..."
logger.fatal "복구할 수 없는 오류가 발생하여 애플리케이션을 종료합니다!!!"
여기 로깅을 추가로 설정한 메서드의 예시가 있습니다:
class ArticlesController < ApplicationController
# ...
def create
@article = Article.new(article_params)
logger.debug "새 article: #{@article.attributes.inspect}"
logger.debug "Article이 유효해야 함: #{@article.valid?}"
if @article.save
logger.debug "article이 저장되었고 이제 사용자가 리다이렉트될 것입니다..."
redirect_to @article, notice: 'Article이 성공적으로 생성되었습니다.'
else
render :new, status: :unprocessable_entity
end
end
# ...
private
def article_params
params.expect(article: [:title, :body, :published])
end
end
다음은 이 컨트롤러 액션이 실행될 때 생성되는 로그의 예시입니다:
Started POST "/articles" for 127.0.0.1 at 2018-10-18 20:09:23 -0400
Processing by ArticlesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"XLveDrKzF1SwaiNRPTaMtkrsTzedtebPPkmxEFIU0ordLjICSnXsSNfrdMa4ccyBjuGwnnEiQhEoMN6H1Gtz3A==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>"0"}, "commit"=>"Create Article"}
새 article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>false, "created_at"=>nil, "updated_at"=>nil}
Article이 유효해야 함: true
(0.0ms) 트랜잭션 시작
↳ app/controllers/articles_controller.rb:31
Article 생성 (0.5ms) INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs."], ["published", 0], ["created_at", "2018-10-19 00:09:23.216549"], ["updated_at", "2018-10-19 00:09:23.216549"]]
↳ app/controllers/articles_controller.rb:31
(2.3ms) 트랜잭션 커밋
↳ app/controllers/articles_controller.rb:31
article이 저장되었고 이제 사용자가 리다이렉트됩니다...
Redirected to http://localhost:3000/articles/1
Completed 302 Found in 4ms (ActiveRecord: 0.8ms)
이런 식으로 추가 로깅을 하면 로그에서 예상치 못한 동작이나 비정상적인 동작을 쉽게 찾을 수 있습니다. 추가 로깅을 할 때는 production 로그가 불필요한 정보로 가득 차는 것을 방지하기 위해 log level을 적절하게 사용하도록 하세요.
2.4 Verbose Query Logs
데이터베이스 쿼리 출력 로그를 살펴볼 때, 단일 메서드가 호출됐는데도 여러 개의 데이터베이스 쿼리가 트리거되는 이유를 바로 파악하기 어려울 수 있습니다:
irb(main):001:0> Article.pamplemousse
Article Load (0.4ms) SELECT "articles".* FROM "articles"
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]]
=> #<Comment id: 2, author: "1", body: "그렇게 말하자면...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">
bin/rails console
세션에서 자세한 쿼리 로그를 활성화하기 위해 ActiveRecord.verbose_query_logs = true
를 실행하고 메서드를 다시 실행하면, 이러한 모든 개별 데이터베이스 호출을 생성하는 단일 코드 라인이 무엇인지 명확해집니다:
irb(main):003:0> Article.pamplemousse
Article Load (0.2ms) SELECT "articles".* FROM "articles"
↳ app/models/article.rb:5
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]]
↳ app/models/article.rb:6
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]]
↳ app/models/article.rb:6
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]]
↳ app/models/article.rb:6
=> #<Comment id: 2, author: "1", body: "음, 사실은...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">
각 데이터베이스 문장 아래에는 데이터베이스 호출을 발생시킨 메서드의 특정 소스 파일명(및 라인 번호)을 가리키는 화살표가 표시됩니다. 이는 N+1 쿼리로 인한 성능 문제를 식별하고 해결하는 데 도움이 될 수 있습니다. N+1 쿼리는 여러 개의 추가 쿼리를 생성하는 단일 데이터베이스 쿼리를 말합니다.
Rails 5.2 이후부터는 개발 환경 로그에서 상세한 쿼리 로그가 기본적으로 활성화되어 있습니다.
프로덕션 환경에서는 이 설정을 사용하지 않는 것을 권장합니다. 이는 Ruby의 Kernel#caller
메서드에 의존하는데, 메서드 호출의 스택트레이스를 생성하기 위해 많은 메모리를 할당하는 경향이 있습니다. 대신 쿼리 로그 태그(아래 참조)를 사용하세요.
2.5 Verbose Enqueue 로그
위의 "Verbose Query Logs"와 비슷하게, background job을 enqueue하는 메서드의 소스 위치를 출력할 수 있습니다.
development 환경에서는 기본적으로 활성화되어 있습니다. 다른 환경에서 활성화하려면 application.rb
또는 환경 initializer에 다음을 추가하세요:
config.active_job.verbose_enqueue_logs = true
job이 enqueue될 때의 상세 로그를 활성화합니다.
상세한 쿼리 로그이므로 production 환경에서의 사용은 권장되지 않습니다.
3 SQL Query Comments
SQL 구문에 컨트롤러나 job의 이름과 같은 런타임 정보를 포함하는 태그로 주석을 달 수 있습니다. 이를 통해 문제가 있는 쿼리를 이러한 구문을 생성한 애플리케이션 영역으로 추적할 수 있습니다. 이는 느린 쿼리를 로깅하거나(MySQL, PostgreSQL), 현재 실행 중인 쿼리를 확인하거나, end-to-end 추적 도구를 사용할 때 유용합니다.
활성화하려면 application.rb
또는 환경 initializer에 추가하세요:
config.active_record.query_log_tags_enabled = true
태그가 있는 database query log를 활성화/비활성화합니다. 기본값은 false입니다.
기본적으로 애플리케이션 이름, 컨트롤러의 이름과 action, 또는 job의 이름이 로깅됩니다. 기본 포맷은 SQLCommenter입니다. 예를 들어:
Article Load (0.2ms) SELECT "articles".* FROM "articles" /*애플리케이션='Blog',컨트롤러='articles',액션='index'*/
Article Update (0.3ms) UPDATE "articles" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = ? /*애플리케이션='Blog',작업='ImproveTitleJob'*/ [["title", "Improved Rails debugging guide"], ["updated_at", "2022-10-16 20:25:40.091371"], ["id", 1]]
ActiveRecord::QueryLogs
의 동작은 애플리케이션 로그의 request와 job id, account와 tenant 식별자 등과 같이 SQL 쿼리와의 연결을 도와주는 모든 것을 포함하도록 수정할 수 있습니다.
3.1 Tagged Logging
다중 사용자, 다중 계정 애플리케이션을 실행할 때는 커스텀 규칙을 사용하여 로그를 필터링할 수 있으면 유용한 경우가 많습니다. Active Support의 TaggedLogging
은 로그 라인에 subdomain, request id, 그리고 이러한 애플리케이션의 디버깅에 도움이 되는 다른 정보들을 태그로 추가할 수 있게 해줍니다.
다음은 TaggedLogging
을 사용하여 logger를 만드는 예제입니다. STDOUT으로 로그를 출력하고 태그를 사용하여 로그에 context를 추가할 수 있습니다:
logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
logger.tagged("BCX") { logger.info "Stuff" } # "[BCX] Stuff" 로그 출력
logger.tagged("BCX", "Jason") { logger.info "Stuff" } # "[BCX] [Jason] Stuff" 로그 출력
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # "[BCX] [Jason] Stuff" 로그 출력
3.2 로그가 성능에 미치는 영향
로깅은 특히 디스크에 로깅할 때 Rails 앱의 성능에 항상 약간의 영향을 미칩니다. 추가적으로 몇 가지 미묘한 점들이 있습니다:
:debug
레벨을 사용하면 :fatal
보다 더 큰 성능 저하가 발생합니다. 이는 훨씬 더 많은 문자열이 평가되어 로그 출력(예: 디스크)에 기록되기 때문입니다.
또 다른 잠재적인 함정은 코드에서 Logger
를 너무 많이 호출하는 것입니다:
logger.debug "Person 속성 해시: #{@person.attributes.inspect}"
위 예시에서는 허용된 출력 레벨이 debug를 포함하지 않더라도 성능에 영향을 미칩니다. 그 이유는 Ruby가 이 문자열들을 평가해야 하는데, 여기에는 비교적 무거운 String
객체 인스턴스화와 변수 보간이 포함되기 때문입니다.
따라서 logger 메소드에 블록을 전달하는 것이 권장됩니다. 이 블록들은 출력 레벨이 허용된 레벨과 동일하거나 포함된 경우에만 평가되기 때문입니다(즉, lazy loading). 같은 코드를 다시 작성하면 다음과 같습니다:
logger.debug { "Person 속성 해시: #{@person.attributes.inspect}" }
블록의 내용과 문자열 보간은 debug가 활성화된 경우에만 평가됩니다. 이러한 성능상의 이점은 대량의 로깅에서만 실제로 눈에 띄지만, 이는 좋은 사용 방법입니다.
이 섹션은 Stack Overflow의 Jon Cairns의 답변에서 작성되었으며 cc by-sa 4.0 라이선스를 따릅니다.
4 debug
Gem을 사용한 디버깅
코드가 예상치 못한 방식으로 동작할 때, 문제를 진단하기 위해 로그나 콘솔에 출력을 시도해볼 수 있습니다. 하지만 이러한 종류의 에러 추적이 문제의 근본 원인을 찾는 데 효과적이지 않은 경우가 있습니다. 실행 중인 소스 코드를 실제로 탐색해야 할 때는 디버거가 가장 좋은 동반자입니다.
Rails 소스 코드에 대해 배우고 싶지만 어디서부터 시작해야 할지 모를 때도 디버거가 도움이 될 수 있습니다. 애플리케이션에 대한 요청을 디버깅하고 이 가이드를 사용하여 작성한 코드에서 기본 Rails 코드로 이동하는 방법을 배우면 됩니다.
Rails 7은 CRuby로 생성된 새로운 애플리케이션의 Gemfile
에 debug
gem을 포함합니다. 기본적으로 development
와 test
환경에서 사용할 수 있습니다. 사용법은 문서를 확인해주세요.
4.1 디버깅 세션 시작하기
기본적으로 디버깅 세션은 앱이 부팅될 때 debug
라이브러리가 로드된 후에 시작됩니다. 하지만 걱정하지 마세요, 세션이 애플리케이션을 방해하지 않습니다.
디버깅 세션을 시작하려면 binding.break
와 그 별칭인 binding.b
와 debugger
를 사용할 수 있습니다. 다음 예제들에서는 debugger
를 사용할 것입니다:
class PostsController < ApplicationController
before_action :set_post, only: %i[ show edit update destroy ]
# GET /posts 또는 /posts.json
def index
@posts = Post.all
debugger
end
# ...
end
앱이 디버깅 구문을 평가하면 디버깅 세션으로 진입합니다:
Processing by PostsController#index as HTML로 처리 중
[2, 11] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
2| before_action :set_post, only: %i[ show edit update destroy ]
3|
4| # GET /posts 또는 /posts.json
5| def index
6| @posts = Post.all
=> 7| debugger
8| end
9|
10| # GET /posts/1 또는 /posts/1.json
11| def show
=>#0 PostsController#index (~projects/rails-guide-example/app/controllers/posts_controller.rb:7)
#1 ActionController::BasicImplicitRender#send_action(method="index", args=[]) (~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/action_controller/metal/basic_implicit_render.rb:6)
# 추가 72개의 프레임이 있습니다(모든 프레임을 보려면 `bt` 명령어를 사용하세요)
(rdbg)
디버깅 세션은 continue
(또는 c
) 명령어를 사용하여 언제든지 종료하고 애플리케이션 실행을 계속할 수 있습니다. 또는 디버깅 세션과 애플리케이션을 모두 종료하려면 quit
(또는 q
) 명령어를 사용하세요.
4.2 Context
디버깅 세션에 들어간 후에는 Rails console이나 IRB에 있는 것처럼 Ruby code를 입력할 수 있습니다.
(rdbg) @posts # ruby
[]
(rdbg) self
#<PostsController:0x0000000000aeb0>
(rdbg)
Ruby 식을 평가하기 위해 p
또는 pp
명령어를 사용할 수도 있습니다. 이는 변수명이 debugger 명령어와 충돌할 때 유용합니다.
(rdbg) p headers # 명령어
=> {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff", "X-Download-Options"=>"noopen", "X-Permitted-Cross-Domain-Policies"=>"none", "Referrer-Policy"=>"strict-origin-when-cross-origin"}
(rdbg) pp headers # 명령어
{"X-Frame-Options"=>"SAMEORIGIN",
"X-XSS-Protection"=>"1; mode=block",
"X-Content-Type-Options"=>"nosniff",
"X-Download-Options"=>"noopen",
"X-Permitted-Cross-Domain-Policies"=>"none",
"Referrer-Policy"=>"strict-origin-when-cross-origin"}
(rdbg)
직접적인 평가 외에도 debugger는 다음과 같은 다양한 명령어를 통해 풍부한 정보를 수집하는데 도움을 줍니다:
info
(또는i
) - 현재 프레임에 대한 정보backtrace
(또는bt
) - 백트레이스 (추가 정보 포함)outline
(또는o
,ls
) - 현재 스코프에서 사용 가능한 메서드, 상수, 로컬 변수 및 인스턴스 변수
4.2.1 info
명령어
info
는 현재 프레임에서 볼 수 있는 로컬 변수와 인스턴스 변수의 값에 대한 개요를 제공합니다.
(rdbg) info # 커맨드
%self = #<PostsController:0x0000000000af78>
@_action_has_layout = true
@_action_name = "index"
@_config = {}
@_lookup_context = #<ActionView::LookupContext:0x00007fd91a037e38 @details_key=nil, @digest_cache=...
@_request = #<ActionDispatch::Request GET "http://localhost:3000/posts" for 127.0.0.1>
@_response = #<ActionDispatch::Response:0x00007fd91a03ea08 @mon_data=#<Monitor:0x00007fd91a03e8c8>...
@_response_body = nil
@_routes = nil
@marked_for_same_origin_verification = true
@posts = []
@rendered_format = nil
4.2.2 backtrace
커맨드
어떤 옵션도 없이 사용되면, backtrace
는 스택의 모든 프레임을 나열합니다:
=>#0 ~/projects/rails-guide-example/app/controllers/posts_controller.rb:7의 PostsController#index #1 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-2.0.alpha/lib/action_controller/metal/basic_implicit_render.rb:6의 ActionController::BasicImplicitRender#send_action(method="index", args=[]) #2 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/abstract_controller/base.rb:214의 AbstractController::Base#process_action(method_name="index", args=[]) #3 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/action_controller/metal/rendering.rb:53의 ActionController::Rendering#process_action(#arg_rest=nil) #4 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/abstract_controller/callbacks.rb:221의 process_action 내 블록 #5 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activesupport-8.1.0.alpha/lib/active_support/callbacks.rb:118의 run_callbacks 내 블록 #6 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actiontext-8.1.0.alpha/lib/action_text/rendering.rb:20의 ActionText::Rendering::ClassMethods#with_renderer(renderer=#PostsController:0x0000000000af78) #7 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actiontext-8.1.0.alpha/lib/action_text/engine.rb:69의 Engine 클래스 내 블록 {|controller=#PostsController:0x0000000000af78, action=#<Proc:0x00007fd91985f1c0 /Users/st0012/...|} (4 레벨) #8 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activesupport-8.1.0.alpha/lib/active_support/callbacks.rb:127의 [C] BasicObject#instance_exec ..... 이하 생략
모든 프레임에는 다음이 포함됩니다:
- 프레임 식별자
- 호출 위치
- 추가 정보(예: block 또는 method 인수)
이를 통해 앱에서 어떤 일이 일어나고 있는지 잘 파악할 수 있습니다. 하지만 다음과 같은 점을 알 수 있습니다:
- 프레임이 너무 많습니다(일반적으로 Rails 앱에서 50개 이상).
- 대부분의 프레임이 Rails나 사용 중인 다른 라이브러리에서 온 것입니다.
backtrace
명령어는 프레임을 필터링하는 데 도움이 되는 2가지 옵션을 제공합니다:
backtrace [num]
-num
개수만큼의 프레임만 표시, 예:backtrace 10
backtrace /pattern/
- 식별자나 위치가 패턴과 일치하는 프레임만 표시, 예:backtrace /MyModel/
이러한 옵션들을 함께 사용하는 것도 가능합니다: backtrace [num] /pattern/
4.2.3 outline
명령어
outline
은 pry
와 irb
의 ls
명령어와 비슷합니다. 현재 scope에서 접근 가능한 다음 항목들을 보여줍니다:
- Local 변수들
- Instance 변수들
- Class 변수들
- Method들과 그들의 출처
ActiveSupport::Configurable#methods: config
AbstractController::Base#methods:
action_methods action_name action_name= available_action? controller_path inspect
response_body
ActionController::Metal#methods:
content_type content_type= controller_name dispatch headers
location location= media_type middleware_stack middleware_stack=
middleware_stack? performed? request request= reset_session
response response= response_body= response_code session
set_request! set_response! status status= to_a
ActionView::ViewPaths#methods:
_prefixes any_templates? append_view_path details_for_lookup formats formats= locale
locale= lookup_context prepend_view_path template_exists? view_paths
AbstractController::Rendering#methods: view_assigns
# .....
PostsController#methods: create destroy edit index new show update
인스턴스 변수:
@_action_has_layout @_action_name @_config @_lookup_context @_request
@_response @_response_body @_routes @marked_for_same_origin_verification @posts
@rendered_format
클래스 변수: @@raise_on_open_redirects
4.3 Breakpoints
디버거에서 breakpoint를 넣고 trigger하는 방법은 많이 있습니다. 코드에 직접 디버깅 문장(예: debugger
)을 추가하는 것 외에도, 다음 명령어로 breakpoint를 삽입할 수 있습니다:
break
(또는b
)break
- 모든 breakpoint 목록 보기break <num>
- 현재 파일의num
번째 줄에 breakpoint 설정break <file:num>
-file
의num
번째 줄에 breakpoint 설정break <Class#method>
또는break <Class.method>
-Class#method
또는Class.method
에 breakpoint 설정break <expr>.<method>
-<expr>
결과의<method>
메소드에 breakpoint 설정
catch <Exception>
-Exception
이 발생했을 때 멈추는 breakpoint 설정watch <@ivar>
- 현재 객체의@ivar
결과가 변경될 때 멈추는 breakpoint 설정(이는 느립니다)
그리고 이들을 제거하기 위해 다음을 사용할 수 있습니다:
delete
(또는del
)delete
- 모든 breakpoint 삭제delete <num>
- id가num
인 breakpoint 삭제
4.3.1 break
명령어
지정된 줄 번호에 breakpoint 설정하기 - 예: b 28
[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
20| end
21|
22| # POST /posts 또는 /posts.json
23| def create
24| @post = Post.new(post_params)
=> 25| debugger
26|
27| respond_to do |format|
28| if @post.save
29| format.html { redirect_to @post, notice: "Post가 성공적으로 생성되었습니다." }
=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
#1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
# 그리고 72개의 프레임 (모든 프레임을 보려면 `bt` 명령어 사용)
(rdbg) b 28 # break 명령어
#0 BP - Line /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (line)
(rdbg) c # continue 명령어
[23, 32] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
23| def create
24| @post = Post.new(post_params)
25| debugger
26|
27| respond_to do |format|
=> 28| if @post.save
29| format.html { redirect_to @post, notice: "Post was successfully created." }
30| format.json { render :show, status: :created, location: @post }
31| else
32| format.html { render :new, status: :unprocessable_entity }
=>#0 블록 {|format=#<ActionController::MimeResponds::Collec...|} in create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:28
#1 ActionController::MimeResponds#respond_to(mimes=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/mime_responds.rb:205
# 그리고 74개의 프레임들 (모든 프레임을 보려면 `bt` 명령어 사용)
중단점 #0 BP - 라인 /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (라인)
주어진 메서드 호출에 breakpoint를 설정합니다 - 예: b @post.save
.
[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
20| end
21|
22| # POST /posts 또는 /posts.json
23| def create
24| @post = Post.new(post_params)
=> 25| debugger
26|
27| respond_to do |format|
28| if @post.save
29| format.html { redirect_to @post, notice: "Post가 성공적으로 생성되었습니다." }
=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
#1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
# 그리고 72개의 프레임(모든 프레임을 보려면 `bt` 명령어 사용)
(rdbg) b @post.save # break 명령어
#0 BP - Method @post.save at /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:43
(rdbg) c # continue 명령어
[39, 48] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb
39| SuppressorRegistry.suppressed[name] = previous_state
40| end
41| end
42|
43| def save(**) # :nodoc:
=> 44| SuppressorRegistry.suppressed[self.class.name] ? true : super
45| end
46|
47| def save!(**) # :nodoc:
48| SuppressorRegistry.suppressed[self.class.name] ? true : super
=>#0 ActiveRecord::Suppressor#save(#arg_rest=nil) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:44
#1 block {|format=#<ActionController::MimeResponds::Collec...|} in create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:28
# 그리고 75개의 프레임 (모든 프레임을 보려면 `bt` 명령어를 사용하세요)
중단점 #0 BP - @post.save 메서드 - /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:43
4.3.2 catch
명령어
예외가 발생할 때 중지됩니다 - 예시: catch ActiveRecord::RecordInvalid
.
[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
20| end
21|
22| # POST /posts or /posts.json
23| def create
24| @post = Post.new(post_params)
=> 25| debugger
26|
27| respond_to do |format|
28| if @post.save!
29| format.html { redirect_to @post, notice: "Post가 성공적으로 생성되었습니다." }
=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
#1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
# 그리고 72개의 프레임 (모든 프레임을 보려면 `bt` 명령어 사용)
(rdbg) catch ActiveRecord::RecordInvalid # 명령어
#1 BP - Catch "ActiveRecord::RecordInvalid"
(rdbg) c # continue 명령어
[75, 84] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb
75| def default_validation_context
76| new_record? ? :create : :update
77| end
78|
79| def raise_validation_error
=> 80| raise(RecordInvalid.new(self))
81| end
82|
83| def perform_validations(options = {})
84| options[:validate] == false || valid?(options[:context])
=>#0 ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80
#1 ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53
# and 88 frames (`bt` 명령어로 모든 프레임을 볼 수 있음)
중단점 #1에 의해 정지됨 BP - Catch "ActiveRecord::RecordInvalid"
4.3.3 watch
명령어
인스턴스 변수가 변경될 때 중단합니다 - 예: watch @_response_body
.
[20, 29]는 ~/projects/rails-guide-example/app/controllers/posts_controller.rb에 있습니다
20| end
21|
22| # POST /posts 또는 /posts.json
23| def create
24| @post = Post.new(post_params)
=> 25| debugger
26|
27| respond_to do |format|
28| if @post.save!
29| format.html { redirect_to @post, notice: "Post가 성공적으로 생성되었습니다." }
=>#0 PostsController#create (~
/projects/rails-guide-example/app/controllers/posts_controller.rb:25)
#1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) (~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6)
# 그리고 72개의 프레임 (모든 프레임을 보려면 bt
명령어 사용)
(rdbg) watch @_response_body # 명령어
0 BP - Watch #PostsController:0x00007fce69ca5320 @responsebody =
(rdbg) c # continue 명령어
[173, 182] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal.rb
173| body = [body] unless body.nil? || body.respond_to?(:each)
174| response.reset_body!
175| return unless body
176| response.body = body
177| super
=> 178| end
179|
180| # render나 redirect가 이미 발생했는지 테스트합니다.
181| def performed?
182| response_body || response.committed?
=>#0 ActionController::Metal#response_body=(body=["<html><body>You are being <a href=\"ht...) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal.rb:178 #=> ["<html><body>You are being <a href=\"ht...
#1 ActionController::Redirecting#redirect_to(options=#<Post id: 13, title: "qweqwe", content:..., response_options={:allow_other_host=>false}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/redirecting.rb:74
# 그리고 82개의 프레임(모든 프레임을 보려면 `bt` 명령어 사용)
중단점 #0 BP - Watch #<PostsController:0x00007fce69ca5320> @_response_body = -> ["<html><body>You are being <a href=\"http://localhost:3000/posts/13\">redirected</a>.</body></html>"]
(rdbg)
4.3.4 Breakpoint 옵션
다양한 breakpoint 타입 외에도, 더 향상된 디버깅 워크플로우를 위한 옵션을 지정할 수 있습니다. 현재 debugger는 4가지 옵션을 지원합니다:
do: <cmd or expr>
- breakpoint가 트리거되면, 주어진 명령어/표현식을 실행하고 프로그램을 계속 진행합니다:break Foo#bar do: bt
-Foo#bar
가 호출되면, 스택 프레임을 출력합니다.
pre: <cmd or expr>
- breakpoint가 트리거되면, 정지하기 전에 주어진 명령어/표현식을 실행합니다:break Foo#bar pre: info
-Foo#bar
가 호출되면, 정지하기 전에 주변 변수들을 출력합니다.
if: <expr>
-<expr>
의 결과가 true인 경우에만 breakpoint가 정지합니다:break Post#save if: params[:debug]
-params[:debug]
가 true인 경우에만Post#save
에서 정지합니다.
path: <path_regexp>
- breakpoint를 트리거하는 이벤트(예: 메서드 호출)가 주어진 경로에서 발생하는 경우에만 정지합니다:break Post#save path: app/services/a_service
- 메서드 호출이app/services/a_service
를 포함하는 경로에서 발생하는 경우에만Post#save
에서 정지합니다.
앞서 언급했던 debug 문에도 처음 3개의 옵션인 do:
, pre:
, if:
를 사용할 수 있다는 점을 참고해주세요. 예를 들어:
[2, 11] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
2| before_action :set_post, only: %i[ show edit update destroy ]
3|
4| # GET /posts 또는 /posts.json
5| def index
6| @posts = Post.all
=> 7| debugger(do: "info")
8| end
9|
10| # GET /posts/1 또는 /posts/1.json
11| def show
=>#0 PostsController#index at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:7
#1 ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
# 그리고 72개의 프레임 (모든 프레임을 보려면 'bt' 명령어 사용)
(rdbg:binding.break) info
%self = #<PostsController:0x00000000017480>
@_action_has_layout = true
@_action_name = "index"
@_config = {}
@_lookup_context = #<ActionView::LookupContext:0x00007fce3ad336b8 @details_key=nil, @digest_cache=...
@_request = #<ActionDispatch::Request GET "http://localhost:3000/posts" for 127.0.0.1>
@_response = #<ActionDispatch::Response:0x00007fce3ad397e8 @mon_data=#<Monitor:0x00007fce3ad396a8>...
@_response_body = nil
@_routes = nil
@marked_for_same_origin_verification = true
@posts = #<ActiveRecord::Relation [#<Post id: 2, title: "qweqwe", content: "qweqwe", created_at: "...
@rendered_format = nil
4.3.5 디버깅 워크플로우 프로그래밍하기
이러한 옵션들을 사용하면 다음과 같이 한 줄로 디버깅 워크플로우를 스크립트화할 수 있습니다:
def create
debugger(do: "catch ActiveRecord::RecordInvalid do: bt 10")
# ...
end
그리고 debugger가 scripted command를 실행하고 catch breakpoint를 삽입할 것입니다
(rdbg:binding.break) ActiveRecord::RecordInvalid를 catch하려면: bt 10
#0 BP - Catch "ActiveRecord::RecordInvalid"
[75, 84] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb
75| def default_validation_context
76| new_record? ? :create : :update
77| end
78|
79| def raise_validation_error
=> 80| raise(RecordInvalid.new(self))
81| end
82|
83| def perform_validations(options = {})
84| options[:validate] == false || valid?(options[:context])
=>#0 ActiveRecord::Validations#raise_validation_error in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80
#1 ActiveRecord::Validations#save!(options={}) in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53
# 그리고 88개의 프레임 (모든 프레임을 보려면 'bt' 명령어를 사용하세요)
catch 브레이크포인트가 트리거되면, 스택 프레임이 출력됩니다
Stop by #0 BP - "ActiveRecord::RecordInvalid" 예외 캐치
(rdbg:catch) bt 10
=>#0 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80의 ActiveRecord::Validations#raise_validation_error
#1 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53의 ActiveRecord::Validations#save!(options={})
#2 ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/transactions.rb:302의 save! 블록 내부
이 기술은 반복적인 수동 입력을 줄여주고 디버깅 경험을 더 부드럽게 만들어줄 수 있습니다.
더 많은 명령어와 설정 옵션은 해당 documentation에서 찾을 수 있습니다.
5 web-console
Gem을 사용한 디버깅
Web Console은 debug
와 비슷하지만 브라우저에서 실행됩니다. 어떤 페이지에서든 view나 controller의 컨텍스트에서 콘솔을 요청할 수 있습니다. 콘솔은 HTML 콘텐츠 옆에 렌더링됩니다.
5.1 콘솔
컨트롤러 액션이나 view 내에서 console
메서드를 호출하여 콘솔을 실행할 수 있습니다.
예를 들어 컨트롤러에서는 다음과 같습니다:
class PostsController < ApplicationController
def new
console
@post = Post.new
end
end
또는 view에서:
<% console %>
<h2>새 게시물</h2>
뷰 내에서 console이 렌더링될 것입니다. console
호출의 위치는 신경 쓸 필요가 없습니다. 호출된 지점에서 렌더링되지 않고 HTML 컨텐츠 옆에 렌더링됩니다.
console은 순수한 Ruby 코드를 실행합니다. 직접 클래스를 정의하고 인스턴스화할 수 있으며, 새로운 모델을 생성하고 변수를 검사할 수 있습니다.
요청당 하나의 console만 렌더링할 수 있습니다. 그렇지 않으면 web-console
은 두 번째 console
호출에서 에러를 발생시킵니다.
5.2 변수 검사하기
instance_variables
를 호출하면 현재 컨텍스트에서 사용 가능한 모든 instance variable들을 나열할 수 있습니다. 모든 local variable들을 나열하고 싶다면 local_variables
를 사용하면 됩니다.
5.3 Settings
config.web_console.allowed_ips
: 승인된 IPv4 또는 IPv6 주소와 네트워크 목록(기본값:127.0.0.1/8, ::1
).config.web_console.whiny_requests
: console 렌더링이 방지될 때 메시지를 로그에 기록(기본값:true
).
web-console
은 서버에서 일반 Ruby 코드를 원격으로 실행하므로 production 환경에서는 사용하지 마세요.
6 메모리 누수 디버깅하기
Ruby 애플리케이션(Rails 기반이든 아니든)은 Ruby 코드나 C 코드 레벨에서 메모리 누수가 발생할 수 있습니다.
이 섹션에서는 Valgrind와 같은 도구를 사용하여 이러한 누수를 찾고 수정하는 방법을 배우게 됩니다.
6.1 Valgrind
Valgrind는 C 기반의 메모리 누수와 경쟁 상태를 탐지하기 위한 애플리케이션입니다.
많은 메모리 관리와 스레딩 버그를 자동으로 감지하고 프로그램을 자세히 프로파일링할 수 있는 Valgrind 도구들이 있습니다. 예를 들어, 인터프리터의 C extension이 malloc()
을 호출하지만 free()
를 제대로 호출하지 않는 경우, 이 메모리는 앱이 종료될 때까지 사용할 수 없게 됩니다.
Valgrind를 설치하고 Ruby와 함께 사용하는 방법에 대한 자세한 정보는 Evan Weaver의 Valgrind and Ruby를 참조하세요.
6.2 메모리 누수 찾기
메모리 누수를 감지하고 수정하는 것에 대한 훌륭한 글이 Derailed에 있습니다. 여기서 읽어보실 수 있습니다.
7 디버깅을 위한 플러그인
에러를 찾고 애플리케이션을 디버깅하는데 도움이 되는 Rails 플러그인들이 있습니다. 다음은 디버깅에 유용한 플러그인 목록입니다:
- Query Trace 로그에 쿼리 발생 위치 추적 기능을 추가합니다.
- Exception Notifier Rails 애플리케이션에서 오류가 발생했을 때 이메일 알림을 보내기 위한 메일러 객체와 기본 템플릿 세트를 제공합니다.
- Better Errors 기본 Rails 에러 페이지를 소스 코드와 변수 검사와 같은 더 많은 컨텍스트 정보를 포함한 새로운 페이지로 대체합니다.
- RailsPanel Rails 개발을 위한 Chrome 확장 프로그램으로 development.log 파일을 계속 확인할 필요가 없게 해줍니다. Rails 앱 요청에 대한 모든 정보를 브라우저의 Developer Tools 패널에서 확인할 수 있습니다. db/렌더링/전체 시간, 파라미터 목록, 렌더링된 뷰 등의 정보를 제공합니다.
- Pry IRB의 대안이자 런타임 개발자 콘솔입니다.