인상적인 코드

구경회·2023년 1월 15일
0
post-thumbnail

코드 읽기의 중요성

나는 개발자들과 학교 교육 과정에서 코드 읽기의 중요성이 너무 누락되어있다는 생각을 자주 했다. 우리가 외국어를 배울 때 여러 구문을 접하는 것처럼 어떤 프로그래밍 언어를 배울 때도 그 언어에 걸맞는 여러 숙어를 배워야한다고 생각한다. 이 과정을 놓칠 경우, 문법적으로는 옳지만 그 커뮤니티에 속한 개발자들이 보기엔 이질적인 형태로 코드를 빚게 된다.

가령 다음과 형태로 설정을 하는 것은 구문은 루비 개발자들에게는 익숙한 모습이지만, 다른 개발자들에게는 완전히 낯설어보일 수 있다.

RSpec.configure do |config|
  config.use_transactional_fixtures = true
  config.use_instantiated_fixtures = false
  config.fixture_path = Rails.root

  config.verbose_retry = true
  config.display_try_failure_messages = true
end

이런 형태의 문법을 지원하기 위해 라이브러리들은 보통 다음과 같이 작성한다.

module Gruf
  module Configuration
  	def configure
    	yield self
  	end
  
  	# ... 생략 ...
  end
end

코드 읽기의 중요성에 대한 설명은 김창준씨의 글을 참고하는게 더 도움이 될 수 있으므로 위로 갈음한다.

요새 읽는 코드

그래서 요새는 여러 좋은 코드를 읽으려고 노력 중이다. 회사에서는 레일즈를 사용해서 작업하고 있는데, 다음 두 코드를 볼 일이 많다.

루비의 웹 서버와 Gitlab이다. 전자의 경우 트러블슈팅이나 작동 원리 이해를 위해, 후자는 좀 더 레일즈스러운 코드를 짜기 위해 많이 참고하고 있다. 보았던 인상적인 코드를 좀 적어보려고 한다.

Gitlab N+1 Detector

module Gitlab::GitalyClient
  def self.enforce_gitaly_request_limits(call_site)
    # Only count limits in request-response environments
    return unless Gitlab::SafeRequestStore.active?

    # This is this actual number of times this call was made. Used for information purposes only
    actual_call_count = increment_call_count("gitaly_#{call_site}_actual")

    return unless enforce_gitaly_request_limits?

    # Check if this call is nested within a allow_n_plus_1_calls
    # block and skip check if it is
    return if get_call_count(:gitaly_call_count_exception_block_depth) > 0

    # This is the count of calls outside of a `allow_n_plus_1_calls` block
    # It is used for enforcement but not statistics
    permitted_call_count = increment_call_count("gitaly_#{call_site}_permitted")

    count_stack

    return if permitted_call_count <= MAXIMUM_GITALY_CALLS

    raise TooManyInvocationsError.new(call_site, actual_call_count, max_call_count, max_stacks)
  end
  
    def self.get_call_count(key)
      Gitlab::SafeRequestStore[key] || 0
    end
    private_class_method :get_call_count

    def self.increment_call_count(key)
      Gitlab::SafeRequestStore[key] ||= 0
      Gitlab::SafeRequestStore[key] += 1
    end
    private_class_method :increment_call_count

    def self.decrement_call_count(key)
      Gitlab::SafeRequestStore[key] -= 1
    end
    private_class_method :decrement_call_count
    
    class TooManyInvocationsError < StandardError
      attr_reader :call_site, :invocation_count, :max_call_stack

      def initialize(call_site, invocation_count, max_call_stack, most_invoked_stack)
        @call_site = call_site
        @invocation_count = invocation_count
        @max_call_stack = max_call_stack
        stacks = most_invoked_stack.join('\n') if most_invoked_stack

        msg = "GitalyClient##{call_site} called #{invocation_count} times from single request. Potential n+1?"
        msg = "#{msg}\nThe following call site called into Gitaly #{max_call_stack} times:\n#{stacks}\n" if stacks

        super(msg)
     end
   end
end

Gitlab::Utils::StrongMemoize

# frozen_string_literal: true

module Gitlab::Utils::StrongMemoize
  def strong_memoize(name)
    key = ivar(name)

    if instance_variable_defined?(key)
      instance_variable_get(key)
    else
      instance_variable_set(key, yield)
    end
  end
  
  # .. 생략 ..
  
  private

  # Convert `"name"`/`:name` into `:@name`
  #
  # Depending on a type ensure that there's a single memory allocation
  def ivar(name)
    case name
    when Symbol
      name.to_s.prepend("@").to_sym
    when String
      :"@#{name}"
    else
      raise ArgumentError, "Invalid type of '#{name}'"
    end
  end
end

루비에서 흔히 쓰는 패턴인 메모이즈 패턴을 편하게 쓰려고 한 것이다. 루비에서 흔히 쓰는 패턴은 다음과 같다.

class Foo
  def memoized_bar
    @bar ||= begin
      # method
    end
  end
end

이 때 문제는 @bar에 저장하는 값이 nil이거나 false라면 begin 블락을 다시 평가한다는 것이다. 다음과 같은 예시를 보자.

class Foo
  def memoized_bar
    @memoized_bar ||= bar
  end

  def bar
    puts "calculating bar"
    nil
  end
end

이제 실행해보자.

irb(main):011:0> x = Foo.new
=> #<Foo:0x000056501fc76710>
irb(main):012:0> x.memoized_bar
calculating bar
=> nil                                                   
irb(main):013:0> x.memoized_bar
calculating bar
=> nil         

기대와 다르게, memoized_bar를 호출할 때마다 bar를 재계산하는 걸 알 수 있다. 그래서 메모의 좀 더 정확한 구현은 다음과 같다.

class Foo
  def accurate_memoize_bar
    return @bar if instance_variable_defined?(:@bar)

    @bar = bar
  end

  def bar
    puts "calculating bar"
    nil
  end
end
irb(main):040:0> x.accurate_memoize_bar
calculating bar
=> nil
irb(main):041:0> x.accurate_memoize_bar
=> nil

이제 기대처럼 제대로 메모이제이션이 되는 걸 볼 수있다. 하지만 이런 식으로는 코드가 보기 힘들어진다는 단점이 있다. 저 StrongMemoize 유틸을 활용할 경우 좀 더 우아하게 코드를 작성 할 수 있다.

class Foo
  include Gitlab::Utils::StrongMemoize

  def memoized_bar
    strong_memoize(:memoized_bar) do
      bar
    end
  end

  def bar
    puts "calculating bar"
    nil
  end
end
profile
즐기는 거야

0개의 댓글