Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

루비 코드 린팅 완벽 가이드: RuboCop과 StandardRB 비교

린팅(Linting)은 코드에서 잠재적인 문제를 찾아내기 위해 정적 분석을 수행하는 과정입니다.

무엇이 '문제'인지에 대한 기준은 프로그래밍 언어에 따라 다를 수 있고, 심지어 같은 언어 안에서도 프로젝트마다 달라집니다. 일반적으로 이러한 문제들은 다음과 같은 카테고리로 분류할 수 있습니다:

  • 프로그래밍 문제
  • 보안 문제
  • 스타일 문제
  • 성능 문제

지금부터 각 유형별 예시를 살펴보겠습니다.

스타일(Stylistic) 문제

코드 스타일에는 객관적으로 옳은 방식이란 없습니다. 전적으로 코드를 읽는 사람의 취향에 달려 있기 때문입니다. 다만 핵심은 일관성입니다. 흔히 논쟁이 되는 주제들은 다음과 같습니다:

  1. 더블 쿼트(") vs 싱글 쿼트(')
  2. 탭(tab) vs 공백(space)
  3. 최대 줄 길이
  4. 여러 줄에 걸친 메서드 호출의 들여쓰기 방식
# 항상 한 줄로 작성
foo(:a, :b, :c)
 
# 첫 번째 인자에 맞춰 정렬
foo(:a,
    :b,
    :c
)
 
# 메서드 이름에 맞춰 정렬
foo(
  :a,
  :b
)

이런 항목들은 완전히 주관적이지만, 프로젝트별로 표준을 미리 합의해 두면 코드베이스 전체의 일관성을 유지하는 데 큰 도움이 됩니다.

프로그래밍(Programmatic) 문제

여기에는 다음과 같은 문제들이 포함됩니다:

  • 지나치게 긴 메서드 본문 — 가독성과 유지보수성 문제를 야기합니다
  • 순환 복잡도(Cyclomatic Complexity) — 코드 복잡도를 측정하는 데 널리 쓰이는 지표입니다
  • 조건문 안의 할당if x = true라고 작성했다면 대부분 if x == true를 의도했을 가능성이 높습니다. 설령 할당이 목적이었더라도 직관적이지 못한 작성 방식입니다

보안(Security) 문제

일부 함수나 코딩 관행은 개발자가 인지하지 못한 잠재적 보안 취약점을 품고 있을 수 있습니다.

예를 들어, Ruby의 Kernel#open은 파일이나 외부 URL을 열 수 있는 유연한 함수입니다. 하지만 open("| ls") 같은 호출로 임의의 파일시스템 명령 실행까지 허용됩니다. 따라서 린터가 이를 경고해 주면, 개발자는 더 안전한 대안(File#open, IO.popen, URI.parse#open)을 사용하거나, 위험을 명확히 인지한 상태에서 의도적으로 해당 동작을 유지할 수 있습니다.

성능(Performance) 문제

Ruby의 내부 동작 방식에는 상황에 따라 어떤 선택이 더 나은 성능을 내는지를 좌우하는 세부 요소가 많습니다.

린터가 이런 부분을 알려주면, 프로그램의 세부 사항을 최적화하는 과정에서 자연스럽게 배울 수 있습니다.

예를 들어, Ruby 2.5에서는 문자열 끝부분의 부분 문자열을 제거하는 String#delete_suffix가 도입되었습니다. 아래 두 줄은 결과가 같지만, 후자가 범용 정규식 매칭에 의존하지 않으므로 더 나은 성능을 보입니다:

str = 'string_with_suffix'
 
# 나쁜 예
str.gsub(/suffix\z/, '')
 
# 좋은 예
str.delete_suffix('suffix')

자동 수정(Auto Fixing)

린터의 중요한 강점 중 하나는 발견된 문제 중 일부 또는 전부를 자동으로 고쳐주는 기능입니다.

줄 길이 같은 스타일 요소는 손쉽게 자동화할 수 있으므로, 이런 단순 반복 작업의 부담을 개발자에게서 덜어주는 것이 합리적입니다. 반면 대형 메서드 리팩토링처럼 주관적 판단이나 사람의 개입이 필요한 문제는 자동화가 불가능합니다.

컨벤션 vs 설정(Convention or Configuration)

커뮤니티나 프로젝트 안에서 "어떤 린팅 규칙이 합리적인가"에 대한 논쟁은 끊이지 않습니다.

전통적인 해결책은 각 팀이 내부적으로 논의해 결론을 내고, 자신들의 취향에 맞게 린팅 규칙을 직접 설정하도록 허용하는 것이었습니다. 하지만 최근 몇 년간 여러 언어에서 단일 컨벤션으로 표준화하려는 흐름이 나타났습니다. 모든 곳에서 강제되는 것은 아니지만, 기본 철학은 코드 스타일로 인한 개발자의 인지적 부담(cognitive overhead)을 완전히 제거하는 것입니다. 어떤 줄 길이가 가장 좋은지 토론하는 대신, 모두가 커뮤니티 합의된 규칙을 그대로 따르는 방식입니다.

Ruby 생태계에서는 이것이 두 가지 린터로 구현되어 있습니다. 완전한 커스터마이징을 허용하는 RuboCop과, 정반대 접근으로 공통 표준을 정의하는 StandardRB입니다.

RuboCop

RuboCop은 문서화된 규칙 집합을 제공하는 일반적인 방식을 취하며, 각 규칙은 특정 문제를 검사합니다. 개발자는 자신의 프로젝트에서 특정 규칙을 비활성화하거나 조정할 수 있습니다:

# 허용되는 최대 줄 길이 설정
Layout/LineLength:
  Max: 80
 
# 순환 복잡도 검사 비활성화
Metrics/CyclomaticComplexity:
  Enabled: false

이미 합리적인 기본값(default)이 포함되어 있으므로, 변경하고 싶은 규칙에 대해서만 추가 설정하면 됩니다.

bundle exec rubocop를 실행하면 RuboCop이 코드베이스 전체를 분석하고 발견한 모든 위반 사항을 나열합니다:

# test.rb
def badName
  if something
    return "inner result"
  end
 
  "outer result"
end
$ bundle exec rubocop
Inspecting 1 file
C
 
Offenses:
 
test.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
def badName
^
test.rb:1:5: C: Naming/MethodName: Use snake_case for method names.
def badName
    ^^^^^^^
test.rb:2:3: C: [Correctable] Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
  if something
  ^^
test.rb:3:12: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
    return "inner result"
           ^^^^^^^^^^^^^^
test.rb:6:3: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  "outer result"
  ^^^^^^^^^^^^^^
 
1 file inspected, 5 offenses detected, 4 offenses auto-correctable

이후 bundle exec rubocop --auto-correct를 실행하면 대다수의 문제가 설정한 규칙에 따라 자동으로 수정됩니다.

CI 파이프라인에 bundle exec rubocop를 포함시켜 두면, 린팅 규칙을 통과하지 못한 코드가 저장소에 병합되는 것을 원천적으로 차단할 수 있습니다.

StandardRB

StandardRB는 비교적 최근에 등장한 프로젝트로, 실제로는 내부적으로 RuboCop을 사용합니다. 목표는 완전히 새로운 린터를 만드는 것이 아니라, 모든 사람이 논쟁 없이 그대로 쓸 수 있는 하나의 표준에 도달하는 것입니다.

최초 발표 라이트닝 토크는 그 동기를 아주 명확하게 보여줍니다. 사람들이 구문 세부 사항을 두고 다투는 데 시간을 낭비하는 대신 커뮤니티 차원의 합의를 따른다면, 정말 중요한 일 — 훌륭한 제품과 라이브러리 만들기 — 에 더 많은 시간을 쓸 수 있다는 것입니다.

내부적으로 RuboCop을 사용하기 때문에 출력 형식도 동일합니다. 유일한 차이점은 어떤 규칙도 커스터마이징할 수 없다는 점입니다.

StandardRB는 최근 1.0.0 버전에 도달했으며, 이는 "어떤 규칙을 채택할 것인가"에 대한 대부분의 논의가 이미 이슈 페이지에서 진행되었다는 뜻입니다. 특정 규칙이 마음에 걸리거나 동의하지 않는다면, 관련 토론을 찾아볼 수 있을 가능성이 높습니다.

무엇보다 그 논의가 충분히 깊이 있었다고 믿을 수 있습니다. 커뮤니티 전체가 모든 사항에 100% 동의하는 것은 불가능하기 때문입니다. 이 접근 방식의 철학은 바로 '서로 의견이 달라도, 결정에는 따른다(disagree and commit)'는 유연함에 있습니다.

마무리 thoughts: 린팅, 어디까지 자동화해야 할까?

과거 프로젝트에서 린팅 규칙 하나하나를 두고 시시콜콜 다투며 생각 이상으로 많은 시간을 소모해 본 경험이 있다면, StandardRB 접근 방식의 가치를 실감할 것입니다. 저 역시 가능한 한 StandardRB 사용을 권장합니다.

일관성이라는 장점은 그대로 살리면서 논쟁 부담을 제거하고, 대부분의 규칙에 대한 자동 수정 기능까지 더하면, 더 나은 소프트웨어를 더 효율적으로 전달하고 정말 중요한 일에 집중할 수 있습니다.

사실 다른 언어들도 비슷하게 '설정 가능성을 낮춘' 코드 포매터를 채택하는 추세입니다. Elixir의 mix formatter와 Rust의 rustfmt는 어느 정도의 설정을 허용하지만, 커뮤니티는 놀랄 만큼 자발적으로 표준을 따르고 있습니다.

물론, 아이러니하게도 이러한 철학 자체에 동의하지 않는다면 RuboCop은 여전히 완벽하게 유효한 선택지입니다.


P.S. Ruby Magic의 새 포스트를 발행 즉시 받아보고 싶다면, Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!