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/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 메서드가 그 예입니다.
자세한 내용은 다음을 참조하세요: