Rails 애플리케이션 디버깅

이 가이드는 Ruby on Rails 애플리케이션을 디버깅하는 기술을 소개합니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 디버깅의 목적.
  • 테스트에서 발견되지 않는 애플리케이션의 문제와 이슈를 추적하는 방법.
  • 디버깅의 다양한 방법.
  • 스택 트레이스 분석 방법.

디버깅을 위한 뷰 헬퍼

변수의 내용을 확인하는 일은 일반적인 작업입니다. Rails는 이를 위해 세 가지 다른 방법을 제공합니다:

  • debug
  • to_yaml
  • inspect

debug

debug 헬퍼는 YAML 형식으로 객체를 렌더링하는 <pre> 태그를 반환합니다. 이를 통해 어떤 객체든 읽기 쉬운 데이터로 출력할 수 있습니다. 예를 들어 뷰에서 다음과 같은 코드를 사용하면:

<%= debug @article %>
<p>
  <b>Title:</b>
  <%= @article.title %>
</p>

다음과 같은 결과를 볼 수 있습니다:

--- !ruby/object Article
attributes:
  updated_at: 2008-09-05 22:55:47
  body: It's a very helpful guide for debugging your Rails app.
  title: Rails debugging guide
  published: t
  id: "1"
  created_at: 2008-09-05 22:55:47
attributes_cache: {}


Title: Rails debugging guide

to_yaml

또한 어떤 객체든 to_yaml을 호출하면 YAML 형식으로 변환할 수 있습니다. 이렇게 변환된 객체를 simple_format 헬퍼 메서드에 전달하면 출력을 포맷할 수 있습니다. 이것이 debug가 하는 일입니다.

<%= simple_format @article.to_yaml %>
<p>
  <b>Title:</b>
  <%= @article.title %>
</p>

위 코드는 다음과 같이 렌더링됩니다:

--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: It's a very helpful guide for debugging your Rails app.
title: Rails debugging guide
published: t
id: "1"
created_at: 2008-09-05 22:55:47
attributes_cache: {}

Title: Rails debugging guide

inspect

객체 값을 표시하는 또 다른 유용한 메서드는 inspect입니다. 특히 배열이나 해시를 다룰 때 유용합니다. 이 메서드는 객체 값을 문자열로 출력합니다. 예를 들어:

<%= [1, 2, 3, 4, 5].inspect %>
<p>
  <b>Title:</b>
  <%= @article.title %>
</p>

다음과 같이 렌더링됩니다:

[1, 2, 3, 4, 5]

Title: Rails debugging guide

The Logger

런타임 중에 로그 파일에 정보를 저장하는 것도 유용할 수 있습니다. Rails는 각 런타임 환경에 대해 별도의 로그 파일을 유지합니다.

로거란 무엇인가?

Rails는 ActiveSupport::Logger 클래스를 사용하여 로그 정보를 작성합니다. Log4r 등의 다른 로거로 대체할 수도 있습니다.

config/application.rb 또는 다른 환경 파일에서 대체 로거를 지정할 수 있습니다. 예를 들어:

config.logger = Logger.new(STDOUT)
config.logger = Log4r::Logger.new("Application Log")

또는 Initializer 섹션에 다음 중 하나를 추가할 수 있습니다:

Rails.logger = Logger.new(STDOUT)
Rails.logger = Log4r::Logger.new("Application Log")

팁: 기본적으로 각 로그는 Rails.root/log/ 아래에 생성되며, 로그 파일의 이름은 애플리케이션이 실행되는 환경 이름을 따릅니다.

로그 레벨

무언가가 로깅되면 메시지의 로그 레벨이 구성된 로그 레벨 이상인 경우에만 해당 로그에 출력됩니다. 현재 로그 레벨을 알고 싶다면 Rails.logger.level 메서드를 호출할 수 있습니다.

사용 가능한 로그 레벨은 :debug, :info, :warn, :error, :fatal, :unknown이며, 각각 0부터 5까지의 로그 레벨 번호에 해당합니다. 기본 로그 레벨을 변경하려면 다음과 같이 하면 됩니다:

config.log_level = :warn # 모든 환경 initializer에서, 또는
Rails.logger.level = 0 # 언제든지

이는 개발 또는 스테이징 환경에서 불필요한 정보로 인해 프로덕션 로그가 넘치지 않도록 하는 데 유용합니다.

팁: Rails의 기본 로그 레벨은 :debug입니다. 그러나 production 환경의 config/environments/production.rb에서는 :info로 설정됩니다.

메시지 전송

컨트롤러, 모델 또는 메일러 내에서 현재 로그에 쓰려면 logger.(debug|info|warn|error|fatal|unknown) 메서드를 사용하면 됩니다:

logger.debug "Person attributes hash: #{@person.attributes.inspect}"
logger.info "Processing the request..."
logger.fatal "Terminating application, raised unrecoverable error!!!"

여기 로깅이 추가된 메서드의 예시가 있습니다:

class ArticlesController < ApplicationController
  # ...

  def create
    @article = Article.new(article_params)
    logger.debug "New article: #{@article.attributes.inspect}"
    logger.debug "Article should be valid: #{@article.valid?}"

    if @article.save
      logger.debug "The article was saved and now the user is going to be redirected..."
      redirect_to @article, notice: 'Article was successfully created.'
    else
      render :new, status: :unprocessable_entity
    end
  end

  # ...

  private
    def article_params
      params.require(:article).permit(: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"}
New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>false, "created_at"=>nil, "updated_at"=>nil}
Article should be valid: true
   (0.0ms)  begin transaction
  ↳ app/controllers/articles_controller.rb:31
  Article Create (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)  commit transaction
  ↳ app/controllers/articles_controller.rb:31
The article was saved and now the user is going to be redirected...
Redirected to http://localhost:3000/articles/1
Completed 302 Found in 4ms (ActiveRecord: 0.8ms)

이와 같이 추가적인 로깅을 추가하면 로그에서 예기치 않거나 비정상적인 동작을 쉽게 찾을 수 있습니다. 추가 로깅을 추가할 때는 불필요한 잡음을 피하기 위해 적절한 로그 레벨을 사용하는 것이 중요합니다.

자세한 쿼리 로그

로그에 표시되는 데이터베이스 쿼리 출력을 보면 단일 메서드 호출에 의해 여러 데이터베이스 쿼리가 트리거되는 이유가 즉시 명확하지 않을 수 있습니다:

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: "Well, actually...", 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: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">

각 데이터베이스 문에 대해 메서드가 위치한 소스 파일(및 행 번호)을 가리키는 화살표가 표시됩니다. 이를 통해 N+1 쿼리 문제(단일 데이터베이스 쿼리가 추가 쿼리를 생성하는 문제)를 식별하고 해결할 수 있습니다.

Rails 5.2 이후 자세한 쿼리 로그는 개발 환경 로그에서 기본적으로 활성화됩니다.

경고: 프로덕션 환경에서 이 설정을 사용하는 것은 권장되지 않습니다. 이는 Ruby의 Kernel#caller 메서드에 의존하며, 메서드 호출의 스택 트레이스를 생성하기 위해 많은 메모리를 할당하는 경향이 있습니다. 대신 쿼리 로그 태그(아래 참조)를 사용하세요.

자세한 Enqueue 로그

위의 “자세한 쿼리 로그"와 유사하게, 백그라운드 작업을 enqueue하는 메서드의 소스 위치를 출력할 수 있습니다.

개발 환경에서 기본적으로 활성화되어 있습니다. 다른 환경에서 활성화하려면 application.rb 또는 환경 initializer에 다음을 추가하세요:

config.active_job.verbose_enqueue_logs = true

자세한 쿼리 로그와 마찬가지로, 프로덕션 환경에서 사용하는 것은 권장되지 않습니다.

SQL 쿼리 주석

SQL 문에 런타임 정보(컨트롤러 또는 작업의 이름 등)가 계속해서 한국어 번역을 제공하겠습니다.

SQL 쿼리 주석

SQL 문에 런타임 정보(컨트롤러 또는 작업의 이름 등)가 포함된 태그를 추가하면 문제가 있는 쿼리를 애플리케이션의 해당 영역으로 추적할 수 있습니다. 이는 느린 쿼리(예: MySQL, PostgreSQL)를 로깅하거나, 현재 실행 중인 쿼리를 보거나, 엔드 투 엔드 추적 도구를 사용할 때 유용합니다.

활성화하려면 application.rb 또는 환경 initializer에 다음을 추가하세요:

config.active_record.query_log_tags_enabled = true

기본적으로 애플리케이션 이름, 컨트롤러 이름과 액션, 작업 이름이 로깅됩니다. 기본 형식은 SQLCommenter입니다. 예를 들어:

Article Load (0.2ms)  SELECT "articles".* FROM "articles" /*application='Blog',controller='articles',action='index'*/

Article Update (0.3ms)  UPDATE "articles" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = ? /*application='Blog',job='ImproveTitleJob'*/  [["title", "Improved Rails debugging guide"], ["updated_at", "2022-10-16 20:25:40.091371"], ["id", 1]]

ActiveRecord::QueryLogs의 동작은 요청 및 작업 ID, 계정 및 테넌트 식별자 등 SQL 쿼리와 연결할 수 있는 정보를 포함하도록 수정할 수 있습니다.

태그 로깅

다중 사용자, 다중 계정 애플리케이션을 실행할 때 로그를 사용자 지정 규칙으로 필터링할 수 있는 것이 유용할 수 있습니다. Active Support의 TaggedLogging은 하위 도메인, 요청 ID 및 기타 디버깅에 도움이 되는 모든 것으로 로그 라인에 스탬프를 찍어 이를 수행할 수 있습니다.

logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
logger.tagged("BCX") { logger.info "Stuff" }                            # Logs "[BCX] Stuff"
logger.tagged("BCX", "Jason") { logger.info "Stuff" }                   # Logs "[BCX] [Jason] Stuff"
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"

로그가 성능에 미치는 영향

로깅은 항상 Rails 앱의 성능에 약간의 영향을 미칠 것입니다. 특히 디스크에 로깅할 때 그렇습니다. 또한 몇 가지 미묘한 점이 있습니다:

:debug 레벨을 사용하면 :fatal보다 성능 저하가 더 클 것입니다. 훨씬 더 많은 문자열이 평가되고 로그 출력(예: 디스크)에 기록되기 때문입니다.

또 다른 잠재적인 문제는 코드에서 Logger를 너무 많이 호출하는 것입니다:

logger.debug "Person attributes hash: #{@person.attributes.inspect}"

위의 예에서는 허용된 출력 레벨에 debug가 포함되지 않더라도 성능 영향이 있습니다. 그 이유는 Ruby가 이러한 문자열을 평가해야 하며, 이는 다소 무거운 String 객체를 인스턴스화하고 변수를 보간해야 하기 때문입니다.

따라서 로거 메서드에 블록을 전달하는 것이 좋습니다. 이렇게 하면 출력 레벨이 허용 레벨과 같거나 포함되는 경우에만 내용이 평가됩니다(지연 로딩). 위의 코드를 다시 작성하면 다음과 같습니다:

logger.debug { "Person attributes hash: #{@person.attributes.inspect}" }

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 or /posts.json
     5|   def index
     6|     @posts = Post.all
=>   7|     debugger(do: "info")
     8|   end
     9|
    10|   # GET /posts/1 or /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
  # and 72 frames (use `bt' command for all frames)
(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

이 섹션은 Jon Cairns의 Stack Overflow 답변에 의해 작성되었으며 cc by-sa 4.0 라이선스 하에 있습니다.

debug 젬을 사용한 디버깅

코드가 예상대로 동작하지 않을 때는 로그나 콘솔에 출력하여 문제를 진단할 수 있습니다. 불행히도 이러한 오류 추적 방식은 문제의 근본 원인을 찾는 데 효과적이지 않을 때가 있습니다. 실행 중인 소스 코드로 직접 들어가야 할 때는 디버거가 가장 좋은 동반자입니다.

디버거는 Rails 소스 코드를 학습하고 싶을 때도 도움이 됩니다. 애플리케이션에 대한 요청을 디버깅하고 이 가이드를 사용하여 작성한 코드에서 Rails 코드로 이동하는 방법을 배울 수 있습니다.

Rails 7은 새로 생성된 애플리케이션의 Gemfiledebug 젬을 포함합니다. 기본적으로 developmenttest 환경에서 준비되어 있습니다. 사용 방법은 문서를 참고하세요.

디버깅 세션 시작하기

기본적으로 디버깅 세션은 debug 라이브러리가 로드될 때 시작됩니다. 이는 애플리케이션이 부팅될 때 발생합니다. 걱정하지 마세요, 세션은 애플리케이션에 영향을 미치지 않습니다.

디버깅 세션에 들어가려면 binding.break와 그 별칭인 binding.bdebugger를 사용할 수 있습니다. 다음 예에서는 debugger를 사용합니다:

class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]

  # GET /posts or /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 or /posts.json
     5|   def index
     6|     @posts = Post.all
=>   7|     debugger
     8|   end
     9|
    10|   # GET /posts/1 or /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.2.0.alpha/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg)

언제든지 continue(또는 c) 명령으로 디버깅 세션을 종료하고 애플리케이션 실행을 계속할 수 있습니다. 또는 quit(또는 q) 명령으로 디버깅 세션과 애플리케이션을 모두 종료할 수 있습니다.

컨텍스트

디버깅 세션에 진입한 후에는 Rails 콘솔이나 IRB에서와 같이 Ruby 코드를 입력할 수 있습니다.

(rdbg) @posts    # ruby
[]
(rdbg) self
#<PostsController:0x0000000000aeb0>
(rdbg)

p 또는 pp 명령을 사용하여 Ruby 표현식을 평가할 수도 있습니다. 변수 이름이 디버거 명령과 충돌할 때 유용합니다.

(rdbg) p headers    # command
=> {"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    # command
{"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)

직접 평가 외에도 디버거는 다양한 명령을 통해 풍부한 정보를 수집할 수 있습니다. 예를 들면 다음과 같습니다:

  • info (또는 i) - 현재 프레임에 대한 정보.
  • backtrace (또는 bt) - 백트레이스(추가 정보 포함).
  • outline (또는 o, ls) - 현재 범위에서 사용 가능한 메서드, 상수, 지역 변수 및 인스턴스 변수.

info 명령

info는 현재 프레임에서 볼 수 있는 지역 변수와 인스턴스 변수의 값을 개괄적으로 보여줍니다.

(rdbg) info    # command
%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

backtrace 명령

옵션 없이 backtrace를 사계속해서 한국어 번역을 제공하겠습니다.

backtrace 명령

옵션 없이 backtrace를 사용하면 스택의 모든 프레임이 나열됩니다:

=>#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-2.0.alpha/lib/action_controller/metal/basic_implicit_render.rb:6
  #2    AbstractController::Base#process_action(method_name="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.2.0.alpha/lib/abstract_controller/base.rb:214
  #3    ActionController::Rendering#process_action(#arg_rest=nil) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.2.0.alpha/lib/action_controller/metal/rendering.rb:53
  #4    block in process_action at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.2.0.alpha/lib/abstract_controller/callbacks.rb:221
  #5    block in run_callbacks at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activesupport-7.2.0.alpha/lib/active_support/callbacks.rb:118
  #6    ActionText::Rendering::ClassMethods#with_renderer(renderer=#<PostsController:0x0000000000af78>) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actiontext-7.2.0.alpha/lib/action_text/rendering.rb:20
  #7    block {|controller=#<PostsController:0x0000000000af78>, action=#<Proc:0x00007fd91985f1c0 /Users/st0012/...|} in <class:Engine> (4 levels) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actiontext-7.2.0.alpha/lib/action_text/engine.rb:69
  #8    [C] BasicObject#instance_exec at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activesupport-7.2.0.alpha/lib/active_support/callbacks.rb:127
  ..... and more

각 프레임에는 다음과 같은 정보가 포함됩니다:

  • 프레임 식별자
  • 호출 위치
  • 추가 정보(예: 블록 또는 메서드 인수)

이를 통해 애플리케이션에서 무슨 일이 일어나고 있는지 잘 알 수 있습니다. 그러나 다음과 같은 점을 알아차릴 수 있습니다:

  • 프레임이 너무 많습니다(일반적으로 Rails 앱에서 50개 이상).
  • 대부분의 프레임은 Rails나 사용하는 다른 라이브러리에서 온 것입니다.

backtrace 명령은 프레임을 필터링하는 데 도움이 되는 2가지 옵션을 제공합니다:

  • backtrace [num] - num 개의 프레임만 표시합니다. 예: backtrace 10.
  • backtrace /pattern/ - 식별자 또는 위치가 패턴과 일치하는 프레임만 표시합니다. 예: backtrace /MyModel/.

이 옵션을 함께 사용할 수도 있습니다: backtrace [num] /pattern/.

outline 명령

outlinepryirbls 명령과 유사합니다. 현재 범위에서 접근할 수 있는 것들을 보여줍니다. 여기에는 다음이 포함됩니다:

  • 지역 변수
  • 인스턴스 변수
  • 클래스 변수
  • 메서드와 그 출처
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
instance variables:
  @_action_has_layout  @_action_name    @_config  @_lookup_context                      @_request
  @_response           @_response_body  @_routes  @marked_for_same_origin_verification  @posts
  @rendered_format
class variables: @@raise_on_open_redirects

중단점

코드에 직접 디버깅 문을 추가하는 것 외에도 다양한 방법으로 중단점을 삽입하고 트리거할 수 있습니다.

  • break (또는 b)
    • break - 모든 중단점 나열
    • break <num> - 현재 파일의 num 행에 중단점 설정
    • break <file:num> - filenum 행에 중단점 설정
    • break <Class#method> 또는 break <Class.method> - Class#method 또는 Class.method에 중단점 설정
    • break <expr>.<method> - <expr> 결과의 <method> 메서드에 중단점 설정.
  • catch <Exception> - Exception이 발생할 때 중단되는 중단점 설정
  • watch <@ivar> - 현재 객체의 @ivar가 변경될 때 중단되는 중단점 설정(느림)

그리고 이를 제거하려면 다음을 사용할 수 있습니다:

  • delete (또는 del)
    • delete - 모든 중단점 삭제
    • delete <num> - num ID의 중단점 삭제

break 명령

지정된 행 번호에 중단점 설정 - 예: b 28

[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 was successfully created." }
=>#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
  # and 72 frames (use `bt' command for all frames)
(rdbg) b 28    # break command
#0  BP - Line  /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (line)
(rdbg) c    # continue command
[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    block {|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
  # and 74 frames (use `bt' command for all frames)

Stop by #0  BP - Line  /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (line)

메서드 호출에 중단점 설정 - 예: b @post.save.

[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 was successfully created." }
=>#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
  # and 72 frames (use `bt' command for all frames)
(rdbg) b @post.save    # break command
#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 command
[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
  # and 75 frames (use `bt' command for all frames)

Stop by #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

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 was successfully created." }
=>#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
  # and 72 frames (use `bt' command for all frames)
(rdbg) catch ActiveRecord::RecordInvalid    # command
#1  BP - Catch  "ActiveRecord::RecordInvalid"
(rdbg) c    # continue command
[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/계속해서 한국어 번역을 제공하겠습니다.

#### `catch` 명령

예외가 발생할  중단 - : `catch ActiveRecord::RecordInvalid`.

```rb
[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 (use `bt' command for all frames)

Stop by #1  BP - Catch  "ActiveRecord::RecordInvalid"

watch 명령

인스턴스 변수가 변경될 때 중단 - 예: watch @_response_body.

[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 was successfully created." }
=>#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
  # and 72 frames (use `bt' command for all frames)
(rdbg) watch @_response_body    # command
#0  BP - Watch  #<PostsController:0x00007fce69ca5320> @_response_body =
(rdbg) c    # continue command
[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|     # Tests if render or redirect has already happened.
   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
  # and 82 frames (use `bt' command for all frames)

Stop by #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가지 옵션을 지원합니다:

  • do: <cmd or expr> - 중단점이 트리거되면 주어진 명령/표현식을 실행하고 프로그램을 계속 실행합니다:
    • break Foo#bar do: bt - Foo#bar가 호출될 때 스택 프레임을 출력합니다.
  • pre: <cmd or expr> - 중단점이 트리거되면 중단되기 전에 주어진 명령/표현식을 실행합니다:
    • break Foo#bar pre: info - Foo#bar가 호출될 때 주변 변수를 출력합니다.
  • if: <expr> - <expr>의 결과가 true인 경우에만 중단점이 중지됩니다:
    • break Post#save if: params[:debug] - params[:debug]도 true인 경우 Post#save에서 중단됩니다.
  • path: <path_regexp> - 중단점을 트리거하는 이벤트(예: 메서드 호출)가 주어진 경로에서 발생하는 경우에만 중단점이 중지됩니다:
    • break Post#save path: app/services/a_service - app/services/a_service를 포함하는 경로에서 Post#save 메서드 호출이 발생하면 중단됩니다.

앞서 언급한 디버그 문에도 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 or /posts.json
     5|   def index
     6|     @posts = Post.all
=>   7|     debugger(do: "info")
     8|   end
     9|
    10|   # GET /posts/1 or /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
  # and 72 frames (use `bt' command for all frames)
(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

디버깅 워크플로 프로그래밍

이러한 옵션을 사용하면 한 줄로 디버깅 워크플로를 스크립팅할 수 있습니다:

def create
  debugger(do: "catch ActiveRecord::RecordInvalid do: bt 10")
  # ...
end

그러면 디버거가 스크립트된 명령을 실행하고 catch 중단점을 삽입합니다.

(rdbg:binding.break) catch ActiveRecord::RecordInvalid do: 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 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 (use `bt' command for all frames)

catch 중단점이 트리거되면 스택 프레임을 출력합니다.

Stop by #0  BP - Catch  "ActiveRecord::RecordInvalid"

(rdbg:catch) bt 10
=>#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
  #2    block in save! at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/transactions.rb:302

이 기술을 사용하면 반복적인 수동 입력을 피할 수 있어 디버깅 경험을 더 부드럽게 만들 수 있습니다.

더 많은 명령과 구성 옵션은 문서에서 확인할 수 있습니다.

web-console 젬을 사용한 디버깅

Web Console은 debug와 비슷하지만 브라우저에서 실행됩니다. 뷰나 컨트롤러의 컨텍스트에서 콘솔을 요청할 수 있습니다. 콘솔은 HTML 콘텐츠 옆에 렌더링됩니다.

콘솔

모든 컨트롤러 액션이나 뷰 내에서 console 메서드를 호출하여 콘솔을 호출할 수 있습니다.

예를 들어 컨트롤러에서:

class PostsController < ApplicationController
  def new
    console
    @post = Post.new
  end
end

또는 뷰에서:

<% console %>

<h2>New Post</h2>

이렇게 하면 뷰 내에 콘솔이 렌더링됩니다. console 호출의 위치를 신경 쓰지 않아도 됩니다. 호출 지점에 렌더링되지 않고 HTML 콘텐츠 옆에 렌더링됩니다.

콘솔에서는 순수 Ruby 코드를 실행할 수 있습니다. 사용자 정의 클래스를 정의하고 인스턴스화하거나, 새 모델을 만들고, 변수를 검사할 수 있습니다.

참고: 요청당 하나의 콘솔만 렌더링할 수 있습니다. 그렇지 않으면 web-console에서 두 번째 console 호출에 대해 오류를 발생시킵니다.

변수 검사

instance_variables를 호출하여 현재 컨텍스트에서 사용 가능한 모든 인스턴스 변수를 나열할 수 있습니다. 지역 변수를 나열하려면 local_variables를 사용할 수 있습니다.

설정

  • config.web_console.allowed_ips: 허용된 IPv4 또는 IPv6 주소 및 네트워크 목록(기본값: 127.0.0.1/8, ::1).
  • config.web_console.whiny_requests: 콘솔 렌더링이 방지될 때 메시지 기록(기본값: true).

web-console는 서버에서 일반 Ruby 코드를 원격으로 평가하므로 프로덕션에서 사용하지 마세요.

메모리 누수 디버깅

Ruby 애플리케이션(Rails 여부와 상관없이)은 Ruby 코드 또는 C 코드 수준에서 메모리 누수가 발생할 수 있습니다.

이 섹션에서는 Valgrind와 같은 도구를 사용하여 계속해서 한국어 번역을 제공하겠습니다.

메모리 누수 디버깅

Ruby 애플리케이션(Rails 여부와 상관없이)은 Ruby 코드 또는 C 코드 수준에서 메모리 누수가 발생할 수 있습니다.

이 섹션에서는 Valgrind와 같은 도구를 사용하여 이러한 누수를 찾고 수정하는 방법을 배웁니다.

Valgrind

Valgrind는 C 기반 메모리 누수와 경쟁 상태를 자동으로 감지하는 애플리케이션입니다.

Valgrind 도구는 많은 메모리 관리 및 스레딩 버그를 자동으로 감지하고 프로그램을 자세히 프로파일링할 수 있습니다. 예를 들어, 인터프리터의 C 확장이 malloc()을 호출하지만 free()를 적절히 호출하지 않으면 이 메모리를 애플리케이션 종료까지 사용할 수 없습니다.

Valgrind 설치 및 Ruby와 함께 사용하는 방법에 대한 자세한 내용은 Evan Weaver의 Valgrind and Ruby를 참조하세요.

메모리 누수 찾기

메모리 누수를 감지하고 수정하는 방법에 대한 훌륭한 문서가 Derailed에 있습니다. 여기에서 읽어볼 수 있습니다.

디버깅을 위한 플러그인

애플리케이션의 오류를 찾고 디버깅하는 데 도움이 되는 Rails 플러그인이 몇 가지 있습니다. 유용한 디버깅 플러그인 목록은 다음과 같습니다:

  • Query Trace 로그에 쿼리 원본 추적을 추가합니다.
  • Exception Notifier Rails 애플리케이션에서 오류가 발생할 때 이메일 알림을 보내는 메일러 객체와 기본 템플릿을 제공합니다.
  • Better Errors 표준 Rails 오류 페이지를 소스 코드와 변수 검사와 같은 더 많은 컨텍스트 정보를 포함하는 새 페이지로 대체합니다.
  • RailsPanel Rails 개발을 위한 Chrome 확장으로, development.log를 따라갈 필요가 없습니다. 브라우저의 개발자 도구 패널에서 Rails 앱 요청에 대한 모든 정보를 확인할 수 있습니다. db/rendering/total 시간, 매개변수 목록, 렌더링된 뷰 등을 제공합니다.
  • Pry IRB 대체 및 런타임 개발자 콘솔.

참고 자료