rubyonrails.org에서 더 보기:

이 파일을 GITHUB에서 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Rails 애플리케이션 디버깅하기

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

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

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

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로 생성된 새로운 애플리케이션의 Gemfiledebug gem을 포함합니다. 기본적으로 developmenttest 환경에서 사용할 수 있습니다. 사용법은 문서를 확인해주세요.

4.1 디버깅 세션 시작하기

기본적으로 디버깅 세션은 앱이 부팅될 때 debug 라이브러리가 로드된 후에 시작됩니다. 하지만 걱정하지 마세요, 세션이 애플리케이션을 방해하지 않습니다.

디버깅 세션을 시작하려면 binding.break와 그 별칭인 binding.bdebugger를 사용할 수 있습니다. 다음 예제들에서는 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 명령어

outlinepryirbls 명령어와 비슷합니다. 현재 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> - filenum 번째 줄에 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의 대안이자 런타임 개발자 콘솔입니다.

8 참고자료



맨 위로