InfoGrab DocsInfoGrab Docs

Ruby 스타일 가이드

요약

이 문서는 Ruby 코드를 위한 GitLab 전용 스타일 가이드입니다. Ruby 스타일 가이드 규칙을 적용하기 위해 RuboCop을 사용합니다. RuboCop 규칙이 없는 경우, 관용적인 Ruby를 작성하기 위한 일반 지침으로 다음 스타일 가이드를 참조하세요:

이 문서는 Ruby 코드를 위한 GitLab 전용 스타일 가이드입니다. 이 페이지에 문서화된 내용은 모두 재논의를 위해 다시 열 수 있습니다.

Ruby 스타일 가이드 규칙을 적용하기 위해 RuboCop을 사용합니다.

RuboCop 규칙이 없는 경우, 관용적인 Ruby를 작성하기 위한 일반 지침으로 다음 스타일 가이드를 참조하세요:

일반적으로, 기존 RuboCop 규칙이나 위의 스타일 가이드에서 다루지 않는 스타일은 차단 요소가 되어서는 안 됩니다.

누구도 강한 의견을 가져서는 안 된다고 결정한 스타일도 있습니다.

참고:

규칙이 없는 스타일#

이 스타일들은 RuboCop 규칙으로 뒷받침되지 않습니다.

이 섹션에 추가되는 모든 스타일에 대해, 섹션의 히스토리 노트에서 논의 링크를 연결하여 맥락을 제공하고 참고 자료로 활용하세요.

attr_reader를 사용한 인스턴스 변수 접근#

클래스에서 인스턴스 변수에 접근하는 방법은 다양합니다:

# public
class Foo
  attr_reader :my_var

  def initialize(my_var)
    @my_var = my_var
  end

  def do_stuff
    puts my_var
  end
end

# private
class Foo
  def initialize(my_var)
    @my_var = my_var
  end

  private

  attr_reader :my_var

  def do_stuff
    puts my_var
  end
end

# direct
class Foo
  def initialize(my_var)
    @my_var = my_var
  end

  private

  def do_stuff
    puts @my_var
  end
end

공개 속성은 클래스 외부에서 접근하는 경우에만 사용해야 합니다. 속성이 내부적으로만 접근되는 경우, 관련 코드에서 일관성이 유지되는 한 어떤 전략을 사용할지에 대한 강한 의견은 없습니다.

개행 스타일 가이드#

일부 개행 스타일을 적용하는 RuboCop의 Layout/EmptyLinesAroundMethodBodyCop/LineBreakAroundConditionalBlock 외에도, RuboCop으로 뒷받침되지 않는 다음 지침들이 있습니다.

규칙: 관련 로직을 그룹화하기 위해서만 개행으로 코드 구분#

# bad
def method
  issue = Issue.new

  issue.save

  render json: issue
end
# good
def method
  issue = Issue.new
  issue.save

  render json: issue
end

규칙: 블록 앞에 개행 추가#

# bad
def method
  issue = Issue.new
  if issue.save
    render json: issue
  end
end
# good
def method
  issue = Issue.new

  if issue.save
    render json: issue
  end
end
예외: 코드 블록이 다른 코드 블록 바로 안쪽에서 시작하거나 끝나는 경우 개행 불필요#
# bad
def method
  if issue

    if issue.valid?
      issue.save
    end

  end
end
# good
def method
  if issue
    if issue.valid?
      issue.save
    end
  end
end

클래스 내 메서드 순서#

클래스 수준의 메서드 순서(public, protected, private 섹션)에 대해서는 RuboCop의 Layout/ClassStructure를 참조하세요.

각 가시성 섹션 내에서, 가독성을 높이기 위해 다음 원칙을 고려하세요:

규칙: 추상화 수준에 따라 메서드 순서 지정 (고수준 먼저)#

메서드는 일반적으로 가장 높은 추상화 수준에서 가장 낮은 수준 순으로 정렬해야 합니다. 이는 다음을 의미합니다:

  • 작업을 조율하거나 조정하는 메서드가 먼저 와야 합니다

  • 해당 조율자 메서드를 지원하는 도우미 메서드는 그 다음에 와야 합니다

이는 “신문 스타일” 또는 “stepdown 규칙” 원칙을 따르며, 코드를 위에서 아래로 이야기처럼 읽을 수 있어 가장 중요한 작업이 먼저 나오고 구현 세부 사항이 점진적으로 드러납니다.

# good - orchestrator method before helpers
class CommitMessageProcessor
  def execute
    other_method_calls
    process_commit_message
  end

  private

  def process_commit_message
    title = extract_title(commit.message)
    body = extract_body(commit.message)

    # ... process title and body
  end

  def extract_title(message)
    message.split("\n").first
  end

  def extract_body(message)
    message.split("\n")[1..]
  end
end
# bad - helper methods before the method that uses them
class CommitMessageProcessor
  def execute
    other_method_calls
    process_commit_message
  end

  private

  def extract_title(message)
    message.split("\n").first
  end

  def extract_body(message)
    message.split("\n")[1..]
  end

  def process_commit_message
    title = extract_title(commit.message)
    body = extract_body(commit.message)

    # ... process title and body
  end
end

이 순서는 독자가 다음을 이해하는 데 도움을 줍니다:

  • 코드가 무엇을 하는지 (고수준 메서드를 먼저 읽음으로써)

  • 코드가 어떻게 하는지 (도우미 메서드를 나중에 읽음으로써)

이 순서 패턴을 따르면 검토자와 향후 유지 관리자가 코드 흐름을 더 빨리 이해할 수 있으며, 특히 명확한 조율 패턴이 있는 서비스 객체, 프로세서 및 기타 클래스에서 유용합니다.

이 순서에 대한 예외가 적절한 경우:

  • 추상화 수준보다 더 중요한 도메인 개념으로 메서드를 그룹화한 경우

  • 유사한 메서드가 많은 경우 알파벳 순서가 중요한 가치를 제공하는 경우

클래스에 서로 다른 관련 없는 목적을 수행하는 여러 고수준 메서드가 있는 경우, 각 고수준 메서드를 지원 도우미 메서드와 함께 그룹화하세요. 또는 각 클래스가 단일 책임을 명확히 갖도록 구현을 별도의 서비스 클래스로 추출하는 것을 고려하세요.

두 단계 이상의 메서드 호출(한 메서드가 다른 메서드를 호출하고, 그 메서드가 또 다른 메서드를 호출하는 경우)을 넘어서면 이 패턴은 다루기 어렵고 따라가기 힘들어질 수 있습니다. 깊은 중첩이 발생하는 경우, 별도의 클래스로 리팩토링하거나 로직을 단순화하는 것을 고려하세요.

Rails / ActiveRecord#

이 섹션에는 Rails 및 ActiveRecord 사용에 관한 GitLab 전용 지침이 포함되어 있습니다.

ActiveRecord 콜백 사용 지양#

ActiveRecord 콜백을 사용하면 “객체 상태가 변경되기 전이나 후에 로직을 트리거”할 수 있습니다.

더 나은 대안이 없을 때만 콜백을 사용하되, 그 이유를 철저히 이해한 경우에만 사용하세요.

ActiveRecord 객체에 새로운 라이프사이클 이벤트를 추가할 때는 콜백 대신 서비스 클래스에 로직을 추가하는 것이 바람직합니다.

콜백을 지양해야 하는 이유#

일반적으로 콜백을 지양해야 하는 이유:

  • 콜백은 호출 순서가 명확하지 않고 코드 흐름을 방해하기 때문에 추론하기 어렵습니다.

  • 콜백은 일반적인 메서드 호출이 아닌 리플렉션에 의존하여 트리거되므로 찾아내고 탐색하기가 더 어렵습니다.

  • 콜백은 변경 사항이 항상 전체 콜백 체인을 트리거하기 때문에 객체 상태에 선택적으로 변경 사항을 적용하기 어렵게 만듭니다.

  • 콜백은 ActiveRecord 클래스에 로직을 가두어 버립니다. 이 강한 결합은 너무 많은 비즈니스 로직을 포함하는 비대한 모델을 유도하며, 이는 더 재사용 가능하고 조합 가능하며 테스트하기 쉬운 서비스 객체에 있어야 할 로직입니다.

  • 객체의 불법적인 상태 전환은 속성 유효성 검사를 통해 더 잘 적용할 수 있습니다.

  • 콜백을 많이 사용하면 팩토리 생성 속도에 영향을 미칩니다. 일부 클래스에는 수백 개의 콜백이 있어, 자동화된 테스트를 위해 해당 객체의 인스턴스를 생성하는 것이 매우 느린 작업이 될 수 있으며 느린 스펙을 유발합니다.

이러한 예시 중 일부는 thoughtbot의 영상에서 논의됩니다.

GitLab 코드베이스는 콜백에 크게 의존하며, 보이지 않는 의존성으로 인해 한번 추가되면 리팩토링하기 어렵습니다. 따라서 이 지침은 기존 콜백을 모두 제거하라고 요구하지는 않습니다.

콜백 사용 시기#

콜백은 특수한 경우에 사용할 수 있습니다. 콜백 추가가 적합한 경우의 몇 가지 예:

  • 의존성이 콜백을 사용하며 해당 콜백 동작을 재정의하려는 경우.

  • 캐시 카운트 증가.

  • 현재 모델의 데이터에만 관련된 데이터 정규화.

콜백에서 서비스로 전환 예시#

다음과 같은 기본 데이터 모델을 가진 프로젝트가 있습니다:

class Project
  has_one :repository
end

class Repository
  belongs_to :project
end

프로젝트 생성 후 리포지터리를 생성하고 프로젝트 이름을 리포지터리 이름으로 사용하고 싶다고 가정해 봅시다. Rails에 익숙한 개발자는 즉시 이렇게 생각할 수 있습니다: ActiveRecord 콜백이 딱이겠다! 그리고 다음 코드를 추가합니다:

class Project
  has_one :repository

  after_initialize :create_random_name
  after_create :create_repository

  def create_random_name
    SecureRandom.alphanumeric
  end

  def create_repository
    Repository.create!(project: self)
  end
end

class Repository
  after_initialize :set_name

  def set_name
    name = project.name
  end
end

class ProjectsController
  def create
    Project.create! # also creates a repository and names it
  end
end

초보 Rails 앱에는 이것이 꽤 무해해 보이지만, Rails 앱이 커지고 복잡해지면 콜백을 통해 이런 로직을 추가하는 것은 많은 단점이 있습니다(이 문서에 모두 나열됨). 대신 이 로직을 서비스 클래스에 추가할 수 있습니다:

class Project
  has_one :repository
end

class Repository
  belongs_to :project
end

class ProjectCreator
  def self.execute
    ApplicationRecord.transaction do
      name = SecureRandom.alphanumeric
      project = Project.create!(name: name)
      Repository.create!(project: project, name: name)
    end
  end
end

class ProjectsController
  def create
    ProjectCreator.execute
  end
end

이렇게 간단한 애플리케이션에서는 두 번째 방식의 장점을 보기 어려울 수 있습니다. 하지만 이미 몇 가지 장점이 있습니다:

  • Project 생성 로직과 별개로 Repository 생성 로직을 테스트할 수 있습니다. 코드가 더 이상 데미터 법칙을 위반하지 않습니다(Repository 클래스가 project.name을 알 필요가 없음).

  • 호출 순서의 명확성.

  • 변경에 열려 있음: 프로젝트에 리포지터리를 생성하지 않으려는 시나리오가 있을 경우, ProjectRepository 클래스를 리팩토링할 필요 없이 새 서비스 클래스를 만들 수 있습니다.

  • Project 팩토리의 각 인스턴스가 두 번째(Repository) 객체를 생성하지 않습니다.

ApplicationRecord / ActiveRecord 모델 스코프#

새 스코프를 만들 때 다음 접두사를 고려하세요.

for_#

where(belongs_to: record)를 필터링하는 스코프에 사용합니다. 예를 들어:

scope :for_project, ->(project) { where(project: project) }
Timelogs.for_project(project)

with_#

joins를 사용하거나, where(has_one: record) 또는 where(has_many: record) 또는 결과 집합이 변경되는 where(boolean condition)을 필터링하는 스코프에 사용합니다.

예를 들어:

scope :with_status, ->(status) { where(status: status) }
Clusters::AgentToken.with_status(:active)

scope :with_due_date, -> { where.not(due_date: nil) }
Issue.with_due_date

scope :with_runner_type, ->(type) { joins(:runner).where(runner: { runner_type: type }) }
Ci::Build.with_runner_type(:instance_type)

커스텀 스코프 이름을 사용하는 것도 괜찮습니다. 예를 들어:

scope :undeleted, -> { where('policy_index >= 0') }
Security::Policy.undeleted

including_#

includes를 사용하여 연관 관계를 즉시 로드하는 스코프에 사용합니다. 결과 집합은 변경되지 않습니다. SQL 로딩 전략을 제어할 필요가 없을 때 N+1 쿼리를 방지하기 위해 including_을 사용하세요. ActiveRecord가 JOIN 또는 서브쿼리 사용 여부를 결정합니다.

예를 들어:

scope :including_tags, -> { includes(:tags) }
Package.including_tags

scope :including_project, -> { includes(:project) }
Issue.including_project

preload_#

preload를 사용하여 연관 관계를 즉시 로드하는 스코프에 사용합니다. 결과 집합은 변경되지 않습니다. 여러 has_many 연관 관계를 로드하거나 별도의 서브쿼리가 명시적으로 필요한 경우 including_ 대신 사용하세요.

예를 들어:

scope :preload_author, -> { preload(:author) }
MergeRequest.preload_author

scope :preload_access_levels, -> { preload(:push_access_levels, :merge_access_levels, :unprotect_access_levels) }
ProtectedBranch.preload_access_levels

order_by_#

order를 사용하는 스코프에 사용합니다. 예를 들어:

scope :order_by_name, -> { order(:name) }
Namespace.order_by_name

scope :order_by_updated_at, ->(direction = :asc) { order(updated_at: direction) }
Project.order_by_updated_at(:desc)

메모리에 이미 로드된 레코드 제외 시 excluding 선호#

메모리에 이미 로드된 특정 레코드를 제외할 때는 직접 작성한 where.not(id: record) 대신 excluding (별칭 without)을 선호합니다. 예를 들어 collection.excluding(self). 이 방법이 의도를 더 명확하게 표현합니다.

where.not(id: relation) 대체용으로 사용하지 마세요. 관계(relation)를 전달하면 별도의 쿼리로 ID를 로드하고 리터럴 목록으로 제외하여 메모리를 낭비하고 오래된 결과를 반환할 수 있습니다. 쿼리 결과를 제외하려면 단일 서브쿼리로 실행되는 where.not(id: relation)을 사용하세요.

클래스 로드 시 애플리케이션 로직 사용 지양#

클래스 수준 상수를 정의할 때 애플리케이션 로직을 호출하지 마세요. 이 표현식들은 요청 시가 아닌 클래스 로드 시 한 번 실행되며, 여러 문제를 야기합니다:

  • 로직이 오류를 발생시키면(예: 데이터베이스 누락) 부팅 실패가 발생합니다.

  • 프로세스가 시작된 후 발생하는 변경 사항은 상수에 반영되지 않습니다.

  • 애플리케이션 부팅이 느려집니다.

# bad - result is frozen at boot; Gitlab::ProjectTemplate.all returns different results depending on state
class GroupsController
  VALID_TEMPLATE_NAMES = Gitlab::ProjectTemplate.all.map(&:name).to_set.freeze
end

대신 메서드를 사용하여 로직이 호출 시점에 실행되도록 하세요:

# good
def valid_template_names
  Gitlab::ProjectTemplate.all.map(&:name).to_set
end

요청 중에 결과가 변경되지 않는 경우 메모이제이션을 사용하세요.

이는 데이터베이스를 쿼리하거나, 서비스를 호출하거나, _()와 같은 I18n 헬퍼를 호출하는 모든 로직에 적용됩니다.

의견이 없는 스타일#

RuboCop 규칙이 제안되었으나 추가하지 않기로 결정한 경우, 더 쉽게 발견할 수 있도록 이 가이드에 해당 결정을 문서화하고 참고 자료로 관련 논의 링크를 연결해야 합니다.

문자열 리터럴 따옴표#

수정해야 할 작업량이 너무 많아, 문자열 리터럴에 단일 따옴표 또는 이중 따옴표 사용에 대해 우리는 신경 쓰지 않습니다.

이전 논의:

개별 그룹은 자신이 소유한 바운디드 컨텍스트 내의 코드에 대해 따옴표 스타일의 일관성에 관한 의견을 가질 수 있습니다. 단, 이러한 결정은 해당 컨텍스트 내의 코드에만 적용됩니다.

타입 안전성#

Ruby 3으로 업그레이드했으므로 타입 안전성을 적용하기 위한 더 많은 옵션을 사용할 수 있습니다.

이러한 옵션 중 일부는 Ruby 구문의 일부로 지원되며 Sorbet 또는 RBS와 같은 특정 타입 안전성 도구 사용이 필요하지 않습니다. 하지만 향후 이러한 도구도 고려할 수 있습니다.

현재 타입을 정의하기 위해 YARD 어노테이션을 사용할 수 있습니다. RubyMine과 같은 IDE는 타입 기반 검사 오류를 표시할 때 YARD 지원을 제공합니다.

자세한 내용은 remote_development 도메인 README의 타입 안전성을 참조하세요.

함수형 패턴#

Ruby와 특히 Rails는 주로 객체 지향 프로그래밍 패턴을 기반으로 하지만, Ruby는 매우 유연한 언어이며 함수형 프로그래밍 패턴도 지원합니다.

특히 도메인 로직에서 함수형 프로그래밍 패턴을 사용하면 관용적이고 익숙한 Ruby 패턴을 사용하면서도 더 읽기 쉽고, 유지 보수하기 쉬우며, 버그에 강한 코드를 만들 수 있습니다. 하지만 일부 패턴은 혼란을 야기하고 Ruby에서 직접 지원하더라도 피해야 하므로 함수형 프로그래밍 패턴을 신중하게 사용해야 합니다. curry 메서드가 그 예입니다.

자세한 내용은 다음을 참조하세요:

Ruby 스타일 가이드

GitLab v19.1
원문 보기
요약

이 문서는 Ruby 코드를 위한 GitLab 전용 스타일 가이드입니다. Ruby 스타일 가이드 규칙을 적용하기 위해 RuboCop을 사용합니다. RuboCop 규칙이 없는 경우, 관용적인 Ruby를 작성하기 위한 일반 지침으로 다음 스타일 가이드를 참조하세요:

이 문서는 Ruby 코드를 위한 GitLab 전용 스타일 가이드입니다. 이 페이지에 문서화된 내용은 모두 재논의를 위해 다시 열 수 있습니다.

Ruby 스타일 가이드 규칙을 적용하기 위해 RuboCop을 사용합니다.

RuboCop 규칙이 없는 경우, 관용적인 Ruby를 작성하기 위한 일반 지침으로 다음 스타일 가이드를 참조하세요:

일반적으로, 기존 RuboCop 규칙이나 위의 스타일 가이드에서 다루지 않는 스타일은 차단 요소가 되어서는 안 됩니다.

누구도 강한 의견을 가져서는 안 된다고 결정한 스타일도 있습니다.

참고:

규칙이 없는 스타일#

이 스타일들은 RuboCop 규칙으로 뒷받침되지 않습니다.

이 섹션에 추가되는 모든 스타일에 대해, 섹션의 히스토리 노트에서 논의 링크를 연결하여 맥락을 제공하고 참고 자료로 활용하세요.

attr_reader를 사용한 인스턴스 변수 접근#

클래스에서 인스턴스 변수에 접근하는 방법은 다양합니다:

# public
class Foo
  attr_reader :my_var

  def initialize(my_var)
    @my_var = my_var
  end

  def do_stuff
    puts my_var
  end
end

# private
class Foo
  def initialize(my_var)
    @my_var = my_var
  end

  private

  attr_reader :my_var

  def do_stuff
    puts my_var
  end
end

# direct
class Foo
  def initialize(my_var)
    @my_var = my_var
  end

  private

  def do_stuff
    puts @my_var
  end
end

공개 속성은 클래스 외부에서 접근하는 경우에만 사용해야 합니다. 속성이 내부적으로만 접근되는 경우, 관련 코드에서 일관성이 유지되는 한 어떤 전략을 사용할지에 대한 강한 의견은 없습니다.

개행 스타일 가이드#

일부 개행 스타일을 적용하는 RuboCop의 Layout/EmptyLinesAroundMethodBodyCop/LineBreakAroundConditionalBlock 외에도, RuboCop으로 뒷받침되지 않는 다음 지침들이 있습니다.

규칙: 관련 로직을 그룹화하기 위해서만 개행으로 코드 구분#

# bad
def method
  issue = Issue.new

  issue.save

  render json: issue
end
# good
def method
  issue = Issue.new
  issue.save

  render json: issue
end

규칙: 블록 앞에 개행 추가#

# bad
def method
  issue = Issue.new
  if issue.save
    render json: issue
  end
end
# good
def method
  issue = Issue.new

  if issue.save
    render json: issue
  end
end
예외: 코드 블록이 다른 코드 블록 바로 안쪽에서 시작하거나 끝나는 경우 개행 불필요#
# bad
def method
  if issue

    if issue.valid?
      issue.save
    end

  end
end
# good
def method
  if issue
    if issue.valid?
      issue.save
    end
  end
end

클래스 내 메서드 순서#

클래스 수준의 메서드 순서(public, protected, private 섹션)에 대해서는 RuboCop의 Layout/ClassStructure를 참조하세요.

각 가시성 섹션 내에서, 가독성을 높이기 위해 다음 원칙을 고려하세요:

규칙: 추상화 수준에 따라 메서드 순서 지정 (고수준 먼저)#

메서드는 일반적으로 가장 높은 추상화 수준에서 가장 낮은 수준 순으로 정렬해야 합니다. 이는 다음을 의미합니다:

  • 작업을 조율하거나 조정하는 메서드가 먼저 와야 합니다

  • 해당 조율자 메서드를 지원하는 도우미 메서드는 그 다음에 와야 합니다

이는 “신문 스타일” 또는 “stepdown 규칙” 원칙을 따르며, 코드를 위에서 아래로 이야기처럼 읽을 수 있어 가장 중요한 작업이 먼저 나오고 구현 세부 사항이 점진적으로 드러납니다.

# good - orchestrator method before helpers
class CommitMessageProcessor
  def execute
    other_method_calls
    process_commit_message
  end

  private

  def process_commit_message
    title = extract_title(commit.message)
    body = extract_body(commit.message)

    # ... process title and body
  end

  def extract_title(message)
    message.split("\n").first
  end

  def extract_body(message)
    message.split("\n")[1..]
  end
end
# bad - helper methods before the method that uses them
class CommitMessageProcessor
  def execute
    other_method_calls
    process_commit_message
  end

  private

  def extract_title(message)
    message.split("\n").first
  end

  def extract_body(message)
    message.split("\n")[1..]
  end

  def process_commit_message
    title = extract_title(commit.message)
    body = extract_body(commit.message)

    # ... process title and body
  end
end

이 순서는 독자가 다음을 이해하는 데 도움을 줍니다:

  • 코드가 무엇을 하는지 (고수준 메서드를 먼저 읽음으로써)

  • 코드가 어떻게 하는지 (도우미 메서드를 나중에 읽음으로써)

이 순서 패턴을 따르면 검토자와 향후 유지 관리자가 코드 흐름을 더 빨리 이해할 수 있으며, 특히 명확한 조율 패턴이 있는 서비스 객체, 프로세서 및 기타 클래스에서 유용합니다.

이 순서에 대한 예외가 적절한 경우:

  • 추상화 수준보다 더 중요한 도메인 개념으로 메서드를 그룹화한 경우

  • 유사한 메서드가 많은 경우 알파벳 순서가 중요한 가치를 제공하는 경우

클래스에 서로 다른 관련 없는 목적을 수행하는 여러 고수준 메서드가 있는 경우, 각 고수준 메서드를 지원 도우미 메서드와 함께 그룹화하세요. 또는 각 클래스가 단일 책임을 명확히 갖도록 구현을 별도의 서비스 클래스로 추출하는 것을 고려하세요.

두 단계 이상의 메서드 호출(한 메서드가 다른 메서드를 호출하고, 그 메서드가 또 다른 메서드를 호출하는 경우)을 넘어서면 이 패턴은 다루기 어렵고 따라가기 힘들어질 수 있습니다. 깊은 중첩이 발생하는 경우, 별도의 클래스로 리팩토링하거나 로직을 단순화하는 것을 고려하세요.

Rails / ActiveRecord#

이 섹션에는 Rails 및 ActiveRecord 사용에 관한 GitLab 전용 지침이 포함되어 있습니다.

ActiveRecord 콜백 사용 지양#

ActiveRecord 콜백을 사용하면 “객체 상태가 변경되기 전이나 후에 로직을 트리거”할 수 있습니다.

더 나은 대안이 없을 때만 콜백을 사용하되, 그 이유를 철저히 이해한 경우에만 사용하세요.

ActiveRecord 객체에 새로운 라이프사이클 이벤트를 추가할 때는 콜백 대신 서비스 클래스에 로직을 추가하는 것이 바람직합니다.

콜백을 지양해야 하는 이유#

일반적으로 콜백을 지양해야 하는 이유:

  • 콜백은 호출 순서가 명확하지 않고 코드 흐름을 방해하기 때문에 추론하기 어렵습니다.

  • 콜백은 일반적인 메서드 호출이 아닌 리플렉션에 의존하여 트리거되므로 찾아내고 탐색하기가 더 어렵습니다.

  • 콜백은 변경 사항이 항상 전체 콜백 체인을 트리거하기 때문에 객체 상태에 선택적으로 변경 사항을 적용하기 어렵게 만듭니다.

  • 콜백은 ActiveRecord 클래스에 로직을 가두어 버립니다. 이 강한 결합은 너무 많은 비즈니스 로직을 포함하는 비대한 모델을 유도하며, 이는 더 재사용 가능하고 조합 가능하며 테스트하기 쉬운 서비스 객체에 있어야 할 로직입니다.

  • 객체의 불법적인 상태 전환은 속성 유효성 검사를 통해 더 잘 적용할 수 있습니다.

  • 콜백을 많이 사용하면 팩토리 생성 속도에 영향을 미칩니다. 일부 클래스에는 수백 개의 콜백이 있어, 자동화된 테스트를 위해 해당 객체의 인스턴스를 생성하는 것이 매우 느린 작업이 될 수 있으며 느린 스펙을 유발합니다.

이러한 예시 중 일부는 thoughtbot의 영상에서 논의됩니다.

GitLab 코드베이스는 콜백에 크게 의존하며, 보이지 않는 의존성으로 인해 한번 추가되면 리팩토링하기 어렵습니다. 따라서 이 지침은 기존 콜백을 모두 제거하라고 요구하지는 않습니다.

콜백 사용 시기#

콜백은 특수한 경우에 사용할 수 있습니다. 콜백 추가가 적합한 경우의 몇 가지 예:

  • 의존성이 콜백을 사용하며 해당 콜백 동작을 재정의하려는 경우.

  • 캐시 카운트 증가.

  • 현재 모델의 데이터에만 관련된 데이터 정규화.

콜백에서 서비스로 전환 예시#

다음과 같은 기본 데이터 모델을 가진 프로젝트가 있습니다:

class Project
  has_one :repository
end

class Repository
  belongs_to :project
end

프로젝트 생성 후 리포지터리를 생성하고 프로젝트 이름을 리포지터리 이름으로 사용하고 싶다고 가정해 봅시다. Rails에 익숙한 개발자는 즉시 이렇게 생각할 수 있습니다: ActiveRecord 콜백이 딱이겠다! 그리고 다음 코드를 추가합니다:

class Project
  has_one :repository

  after_initialize :create_random_name
  after_create :create_repository

  def create_random_name
    SecureRandom.alphanumeric
  end

  def create_repository
    Repository.create!(project: self)
  end
end

class Repository
  after_initialize :set_name

  def set_name
    name = project.name
  end
end

class ProjectsController
  def create
    Project.create! # also creates a repository and names it
  end
end

초보 Rails 앱에는 이것이 꽤 무해해 보이지만, Rails 앱이 커지고 복잡해지면 콜백을 통해 이런 로직을 추가하는 것은 많은 단점이 있습니다(이 문서에 모두 나열됨). 대신 이 로직을 서비스 클래스에 추가할 수 있습니다:

class Project
  has_one :repository
end

class Repository
  belongs_to :project
end

class ProjectCreator
  def self.execute
    ApplicationRecord.transaction do
      name = SecureRandom.alphanumeric
      project = Project.create!(name: name)
      Repository.create!(project: project, name: name)
    end
  end
end

class ProjectsController
  def create
    ProjectCreator.execute
  end
end

이렇게 간단한 애플리케이션에서는 두 번째 방식의 장점을 보기 어려울 수 있습니다. 하지만 이미 몇 가지 장점이 있습니다:

  • Project 생성 로직과 별개로 Repository 생성 로직을 테스트할 수 있습니다. 코드가 더 이상 데미터 법칙을 위반하지 않습니다(Repository 클래스가 project.name을 알 필요가 없음).

  • 호출 순서의 명확성.

  • 변경에 열려 있음: 프로젝트에 리포지터리를 생성하지 않으려는 시나리오가 있을 경우, ProjectRepository 클래스를 리팩토링할 필요 없이 새 서비스 클래스를 만들 수 있습니다.

  • Project 팩토리의 각 인스턴스가 두 번째(Repository) 객체를 생성하지 않습니다.

ApplicationRecord / ActiveRecord 모델 스코프#

새 스코프를 만들 때 다음 접두사를 고려하세요.

for_#

where(belongs_to: record)를 필터링하는 스코프에 사용합니다. 예를 들어:

scope :for_project, ->(project) { where(project: project) }
Timelogs.for_project(project)

with_#

joins를 사용하거나, where(has_one: record) 또는 where(has_many: record) 또는 결과 집합이 변경되는 where(boolean condition)을 필터링하는 스코프에 사용합니다.

예를 들어:

scope :with_status, ->(status) { where(status: status) }
Clusters::AgentToken.with_status(:active)

scope :with_due_date, -> { where.not(due_date: nil) }
Issue.with_due_date

scope :with_runner_type, ->(type) { joins(:runner).where(runner: { runner_type: type }) }
Ci::Build.with_runner_type(:instance_type)

커스텀 스코프 이름을 사용하는 것도 괜찮습니다. 예를 들어:

scope :undeleted, -> { where('policy_index >= 0') }
Security::Policy.undeleted

including_#

includes를 사용하여 연관 관계를 즉시 로드하는 스코프에 사용합니다. 결과 집합은 변경되지 않습니다. SQL 로딩 전략을 제어할 필요가 없을 때 N+1 쿼리를 방지하기 위해 including_을 사용하세요. ActiveRecord가 JOIN 또는 서브쿼리 사용 여부를 결정합니다.

예를 들어:

scope :including_tags, -> { includes(:tags) }
Package.including_tags

scope :including_project, -> { includes(:project) }
Issue.including_project

preload_#

preload를 사용하여 연관 관계를 즉시 로드하는 스코프에 사용합니다. 결과 집합은 변경되지 않습니다. 여러 has_many 연관 관계를 로드하거나 별도의 서브쿼리가 명시적으로 필요한 경우 including_ 대신 사용하세요.

예를 들어:

scope :preload_author, -> { preload(:author) }
MergeRequest.preload_author

scope :preload_access_levels, -> { preload(:push_access_levels, :merge_access_levels, :unprotect_access_levels) }
ProtectedBranch.preload_access_levels

order_by_#

order를 사용하는 스코프에 사용합니다. 예를 들어:

scope :order_by_name, -> { order(:name) }
Namespace.order_by_name

scope :order_by_updated_at, ->(direction = :asc) { order(updated_at: direction) }
Project.order_by_updated_at(:desc)

메모리에 이미 로드된 레코드 제외 시 excluding 선호#

메모리에 이미 로드된 특정 레코드를 제외할 때는 직접 작성한 where.not(id: record) 대신 excluding (별칭 without)을 선호합니다. 예를 들어 collection.excluding(self). 이 방법이 의도를 더 명확하게 표현합니다.

where.not(id: relation) 대체용으로 사용하지 마세요. 관계(relation)를 전달하면 별도의 쿼리로 ID를 로드하고 리터럴 목록으로 제외하여 메모리를 낭비하고 오래된 결과를 반환할 수 있습니다. 쿼리 결과를 제외하려면 단일 서브쿼리로 실행되는 where.not(id: relation)을 사용하세요.

클래스 로드 시 애플리케이션 로직 사용 지양#

클래스 수준 상수를 정의할 때 애플리케이션 로직을 호출하지 마세요. 이 표현식들은 요청 시가 아닌 클래스 로드 시 한 번 실행되며, 여러 문제를 야기합니다:

  • 로직이 오류를 발생시키면(예: 데이터베이스 누락) 부팅 실패가 발생합니다.

  • 프로세스가 시작된 후 발생하는 변경 사항은 상수에 반영되지 않습니다.

  • 애플리케이션 부팅이 느려집니다.

# bad - result is frozen at boot; Gitlab::ProjectTemplate.all returns different results depending on state
class GroupsController
  VALID_TEMPLATE_NAMES = Gitlab::ProjectTemplate.all.map(&:name).to_set.freeze
end

대신 메서드를 사용하여 로직이 호출 시점에 실행되도록 하세요:

# good
def valid_template_names
  Gitlab::ProjectTemplate.all.map(&:name).to_set
end

요청 중에 결과가 변경되지 않는 경우 메모이제이션을 사용하세요.

이는 데이터베이스를 쿼리하거나, 서비스를 호출하거나, _()와 같은 I18n 헬퍼를 호출하는 모든 로직에 적용됩니다.

의견이 없는 스타일#

RuboCop 규칙이 제안되었으나 추가하지 않기로 결정한 경우, 더 쉽게 발견할 수 있도록 이 가이드에 해당 결정을 문서화하고 참고 자료로 관련 논의 링크를 연결해야 합니다.

문자열 리터럴 따옴표#

수정해야 할 작업량이 너무 많아, 문자열 리터럴에 단일 따옴표 또는 이중 따옴표 사용에 대해 우리는 신경 쓰지 않습니다.

이전 논의:

개별 그룹은 자신이 소유한 바운디드 컨텍스트 내의 코드에 대해 따옴표 스타일의 일관성에 관한 의견을 가질 수 있습니다. 단, 이러한 결정은 해당 컨텍스트 내의 코드에만 적용됩니다.

타입 안전성#

Ruby 3으로 업그레이드했으므로 타입 안전성을 적용하기 위한 더 많은 옵션을 사용할 수 있습니다.

이러한 옵션 중 일부는 Ruby 구문의 일부로 지원되며 Sorbet 또는 RBS와 같은 특정 타입 안전성 도구 사용이 필요하지 않습니다. 하지만 향후 이러한 도구도 고려할 수 있습니다.

현재 타입을 정의하기 위해 YARD 어노테이션을 사용할 수 있습니다. RubyMine과 같은 IDE는 타입 기반 검사 오류를 표시할 때 YARD 지원을 제공합니다.

자세한 내용은 remote_development 도메인 README의 타입 안전성을 참조하세요.

함수형 패턴#

Ruby와 특히 Rails는 주로 객체 지향 프로그래밍 패턴을 기반으로 하지만, Ruby는 매우 유연한 언어이며 함수형 프로그래밍 패턴도 지원합니다.

특히 도메인 로직에서 함수형 프로그래밍 패턴을 사용하면 관용적이고 익숙한 Ruby 패턴을 사용하면서도 더 읽기 쉽고, 유지 보수하기 쉬우며, 버그에 강한 코드를 만들 수 있습니다. 하지만 일부 패턴은 혼란을 야기하고 Ruby에서 직접 지원하더라도 피해야 하므로 함수형 프로그래밍 패턴을 신중하게 사용해야 합니다. curry 메서드가 그 예입니다.

자세한 내용은 다음을 참조하세요: