Ruby 애플리케이션에서 지금 무슨 일이 일어나고 있는지 궁금하신 적 있으신가요?
화려한 GUI 도구는 없지만, 다행히 ObjectSpace 모듈이 있습니다!
ObjectSpace를 사용하면 애플리케이션의 현재 상태에 대한 정보를 얻을 수 있습니다. 지금부터 그 작동 방식을 함께 살펴보겠습니다.
객체 개수 세기
ObjectSpace를 활용하면 프로그램에서 현재 '살아있는(live)' 객체들이 무엇인지 파악할 수 있습니다.
'살아있는' 객체란 무엇일까?
객체를 가리키는 참조(reference)가 하나라도 존재하는 한, 그 객체는 살아있습니다. 참조란 변수나 상수처럼 객체에 접근할 수 있는 방법을 말합니다.
반대로 어떤 객체에도 접근할 수 없게 되면, 그 객체는 메모리에서 안전하게 제거될 수 있다는 의미입니다.
예제:
# 변수 'name'은 문자열 'Dave'에 대한 참조를 갖고 있습니다. name = 'Dave' # 이제 'name' 변수는 'John'을 가리킵니다. # 'Dave'는 더 이상 참조하는 곳이 없습니다. name = 'John'
이제 ObjectSpace가 실제로 동작하는 예제를 살펴보겠습니다.
require 'objspace'
# 유효한 Ruby 문법이지만 irb/pry에서는 동작하지 않습니다
ObjectSpace
.each_object
.inject(Hash.new 0) { |h,o| h[o.class] += 1; h }
.sort_by { |k,v| -v }
.take(10)
.each { |klass, count| puts "#{count.to_s.ljust(10)} #{klass}" }
# 복사 & 붙여넣기용 버전 (irb/pry에서는 이 코드를 사용하세요)
ObjectSpace.each_object.inject(Hash.new 0) { |h,o| h[o.class] += 1; h }.sort_by { |k,v| -v }.take(10).each { |klass, count| puts "#{count.to_s.ljust(10)} #{klass}" }
이 코드를 실행하면 객체 수 기준 상위 10개 클래스의 통계가 테이블 형태로 출력됩니다.
Count Class ------------------------- 5436 String 315 Class 251 Array 101 Encoding 69 Regexp 45 Hash 26 Module 25 Gem::Version 22 Gem::StubSpecification::StubLine 22 Gem::StubSpecification
메모리 누수가 의심된다면 이 데이터를 매시간 로그로 남겨보세요. 계속 증가만 하고 줄어들지 않는 객체 개수를 찾아낼 수 있습니다.
객체로 재미있는 것 해보기
ObjectSpace를 사용하면 단순한 정보가 아니라 실제 객체 자체에 접근할 수 있습니다. 덕분에 메모리에 있는 모든 문자열의 값을 출력하거나, 모든 File 객체의 경로를 출력하는 같은 재미있는 작업도 가능합니다.
예제:
ObjectSpace
.each_object(String)
.sort_by { |s| s.size }
.each { |s| p s }
이 코드는 메모리에 존재하는 모든 문자열을 크기순으로 정렬해 출력합니다. 출력 결과를 보면 직접 생성하지 않은 문자열이 많다는 것을 알 수 있는데, 이것들은 Ruby 인터프리터가 내부적으로 만든 것들입니다.
실용적인 용도가 있냐고요? 물론 주로 디버깅이나 앱 통계 수집에 활용할 수 있습니다 🙂
객체의 메모리 크기 확인하기
ObjectSpace.memsize_of를 사용하면 특정 객체가 차지하는 메모리 크기를 확인할 수 있습니다.
예제:
o = "a" * 100 ObjectSpace.memsize_of(o)
단, 공식 문서에는 다음과 같은 경고가 있으니 유의하세요:
“반환되는 크기는 불완전합니다. 이 정보는 어디까지나 힌트(HINT)로만 다뤄야 합니다.”
다양한 타입의 객체로 이 메서드를 실험해 보면 흥미로운 사실들을 발견할 수 있습니다. 예를 들어 Fixnum은 항상 0을 반환합니다.
ObjectSpace.memsize_of(42) # 0
그 이유는 Ruby가 내부적으로 Fixnum 객체를 별도로 생성하지 않기 때문입니다. 자세한 내용은 제가 Ruby 숫자에 관해 작성한 글에서 확인하실 수 있습니다.
문자열도 재미있는 결과를 보여줍니다:
ObjectSpace.memsize_of("A" * 22)
# 40
ObjectSpace.memsize_of("A" * 23)
# 40
ObjectSpace.memsize_of("A" * 24)
# 65
참고로 저는
"A" * size패턴을 활용해 긴 문자열을 일일이 타이핑하지 않고 만들곤 합니다 🙂
잠깐! 방금 무슨 일이 일어난 걸까요?
사실 Ruby에는 24자 미만의 짧은 문자열을 위한 최적화가 내장되어 있습니다. 그래서 24자를 넘어서는 순간 메모리 사용량이 확실히 증가하는 것입니다. 이와 관련된 더 자세한 내용은 Pat Shaughnessy의 포스트에서 확인하실 수 있습니다.
앨리어스(별칭) 메서드 찾는 방법
Ruby에 존재하는 모든 앨리어스 메서드를 한눈에 볼 수 있는 '마스터' 목록이 있다면 좋지 않을까요?
소원 들어드립니다!
다음 코드를 살펴보세요:
class Module
def aliased_methods
instance_methods(false)
.group_by { |m| instance_method(m) }
.map(&:last)
.keep_if { |symbols| symbols.length > 1 }
end
end
이 코드는 Stackoverflow 답변에서 가져온 것으로, Module 클래스에 aliased_methods 메서드를 정의합니다. 내부적으로는 instance_methods 메서드를 사용해 클래스에 정의된 모든 인스턴스 메서드 목록을 가져옵니다.
다소 복잡하게 들릴 수 있지만, 바로 이것이 메타프로그래밍의 매력입니다!
나머지 코드는 '살아있는' 객체가 하나 이상 존재하는 클래스 이름들의 배열을 만든 뒤, 각 클래스에 aliased_methods를 호출하고 결과를 출력하는 역할을 합니다.
objects = ObjectSpace.each_object.map(&:class).uniq
objects.each do |klass|
methods = "n#{klass}n#{'-'*20}n"
klass.send(:aliased_methods).each do |m1, m2|
methods << "#{m1.to_s.ljust(15)} #{m2}n"
end
puts methods
end
출력 결과는 다음과 같습니다:
Array -------------------- inspect to_s [] slice length size find_index index collect map collect! map!
마치며
ObjectSpace로 할 수 있는 멋진 것들을 배우는 시간이 즐거웠기를 바랍니다. 이제 직접 사용해 보시고, 흥미로운 발견이 있다면 꼭 공유해 주세요!
이 글을 프로그래머 친구들에게 널리 공유해 주시면, 친구들은 새로운 것을 배우고 저는 더 많은 독자를 만날 수 있습니다 🙂