Ruby 스타일 가이드
GitLab v19.2요약
이 문서는 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/EmptyLinesAroundMethodBody와 Cop/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을 알 필요가 없음). -
호출 순서의 명확성.
-
변경에 열려 있음: 프로젝트에 리포지터리를 생성하지 않으려는 시나리오가 있을 경우,
Project와Repository클래스를 리팩토링할 필요 없이 새 서비스 클래스를 만들 수 있습니다. -
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 메서드가 그 예입니다.
자세한 내용은 다음을 참조하세요: