RUBY

루비 실버 자격증 완벽 합격 가이드: 핵심 개념 + 함정 분석 + 실전 문제 20선 해설

Ruby Silver 자격증은 "코드를 정확하게 읽는 능력"을 평가합니다. 이 글은 단순 요약집이 아닙니다. 시험에서 실제로 틀리기 쉬운 개념을 골라, 개념 설명 → 왜 헷갈리는가 → 함정 포인트 → 실전 문제 해설 순서로 완전히 익힐 수 있도록 구성했습니다.

Ruby Silver 시험 개요

Ruby Association Certified Ruby Programmer Silver 시험은 다음 영역을 평가합니다.

  • 문법 이해: 변수 스코프, 메서드 정의, 블록, 반복문, 조건문
  • 객체 모델: 클래스, 상속, 모듈, 메서드 탐색 순서(MRO)
  • 표준 라이브러리: Array, Hash, String, Range, Enumerable, Comparable
  • 예외 처리: begin/rescue/ensure/raise, 커스텀 예외
  • 특수 문법: 접근 제어자, self, 상수 탐색, Proc/Lambda

합격 기준: 75% 이상 (50문항 중 약 38문항 이상 정답). 코드를 보고 실행 결과를 고르는 문제가 70% 이상을 차지합니다.

1. 변수 스코프와 초기화

Ruby의 변수는 이름 앞의 기호로 종류와 스코프를 구분합니다. 시험에서 가장 기본이자 가장 많이 틀리는 파트입니다.

변수 종류와 스코프 표

종류표기유효 범위초기값
지역 변수name현재 메서드/블록NameError (미초기화)
인스턴스 변수@name인스턴스 전체nil
클래스 변수@@name클래스 + 서브클래스NameError (미초기화)
전역 변수$name프로그램 전체nil
상수NAME정의된 모듈/클래스NameError (미정의)

인스턴스 변수 초기값 확인

class Sample
  def show
    puts @undefined_var.inspect   # nil — 미초기화된 인스턴스 변수
    puts @undefined_var.nil?      # true
  end
end

Sample.new.show

핵심: @ 변수는 초기화 전에도 nil을 반환해 에러가 나지 않습니다. 반면 지역 변수는 초기화 전 접근 시 NameError가 발생합니다.

클래스 변수와 상속의 함정

class Animal
  @@count = 0

  def initialize
    @@count += 1
  end

  def self.count
    @@count
  end
end

class Dog < Animal; end
class Cat < Animal; end

Dog.new; Dog.new
Cat.new
puts Dog.count   # ?
puts Cat.count   # ?
puts Animal.count # ?

답: 전부 3. 클래스 변수 @@countAnimal과 모든 하위 클래스(Dog, Cat)가 공유합니다. 이 공유 특성 때문에 예상치 못한 버그가 생기기 쉬워 시험에 자주 나옵니다.

2. 메서드 인자 문법 완전 정리

Ruby의 메서드 인자 정의는 유연하지만, 순서 규칙을 어기면 SyntaxError가 발생합니다.

인자 선언 순서 규칙

# 올바른 순서: 일반 → 기본값 → *가변 → 키워드 → **이중해시 → &블록
def example(a, b = 10, *c, d:, e: 99, **opts, &block)
  p [a, b, c, d, e, opts]
  block.call if block
end

example(1, 2, 3, 4, d: 5)
# [1, 2, [3, 4], 5, 99, {}]

example(1, d: 5, e: 6, extra: 7)
# [1, 10, [], 5, 6, {extra: 7}]

자주 나오는 인자 문제 유형

def test(a, b = 1, *c)
  p [a, b, c]
end

test(10)          # [10, 1, []]
test(10, 20)      # [10, 20, []]
test(10, 20, 30, 40)  # [10, 20, [30, 40]]

함정: 기본값 인자와 가변 인자가 섞이면 인자 분배 방식을 정확히 추적해야 합니다. b = 1c에 인자가 충분할 때만 기본값을 유지합니다.

3. 블록, yield, block_given?

블록은 Ruby의 핵심 기능입니다. yield로 블록을 호출하고, block_given?로 전달 여부를 확인합니다.

def repeat(n)
  n.times { yield } if block_given?
end

repeat(3) { print "Hi " }   # Hi Hi Hi
repeat(3)                    # 아무것도 출력 안 함 (no error)

yield에 값 전달하기

def transform(arr)
  result = []
  arr.each { |x| result << yield(x) }
  result
end

p transform([1, 2, 3]) { |n| n ** 2 }  # [1, 4, 9]

명시적 블록 받기 (&block)

def execute(&block)
  puts block.class      # Proc
  block.call(10)
end

execute { |n| puts n * 3 }  # 30

핵심: &로 블록을 받으면 Proc 객체로 변환됩니다. 반대로 Proc&proc_obj로 메서드에 넘기면 블록으로 변환됩니다.

double = Proc.new { |n| n * 2 }
p [1, 2, 3].map(&double)   # [2, 4, 6]

4. Proc vs Lambda — 시험 최빈출 항목

이 두 가지는 외형이 비슷하지만 동작이 다릅니다. 반드시 차이를 정확히 외워야 합니다.

비교 표

항목ProcLambda
인자 수 검사느슨함 (초과분 무시, 부족분 nil)엄격함 (ArgumentError)
return의 범위메서드 전체에서 탈출lambda 내부에서만 종료
lambda? 메서드falsetrue
생성 방법Proc.new { }lambda { } 또는 ->( ) { }

return 차이 코드 예제

def test_proc
  p = Proc.new { return "proc" }
  p.call              # 여기서 메서드 전체가 종료됨
  "이 줄 실행 안 됨"
end

def test_lambda
  l = lambda { return "lambda" }
  l.call              # lambda 내부에서만 return
  "이 줄 실행됨"      # 이 값이 반환됨
end

puts test_proc    # proc
puts test_lambda  # 이 줄 실행됨

인자 수 차이 코드 예제

p1 = Proc.new { |x, y| p [x, y] }
p1.call(1)          # [1, nil]   인자 부족해도 nil로 채움
p1.call(1, 2, 3)    # [1, 2]    초과분 무시

l1 = lambda { |x, y| p [x, y] }
# l1.call(1)        # ArgumentError!
l1.call(1, 2)       # [1, 2]

5. 클래스와 객체지향 심화

initialize와 new

class Point
  attr_accessor :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def distance_to(other)
    Math.sqrt((@x - other.x) ** 2 + (@y - other.y) ** 2)
  end

  def to_s
    "(#{@x}, #{@y})"
  end
end

p1 = Point.new(0, 0)
p2 = Point.new(3, 4)
puts p1.distance_to(p2)   # 5.0
puts p2                    # (3, 4)  ← to_s 자동 호출

attr_reader / attr_writer / attr_accessor

class Person
  attr_reader   :name    # getter만: def name; @name; end
  attr_writer   :age     # setter만: def age=(v); @age = v; end
  attr_accessor :email   # getter + setter 둘 다

  def initialize(name, age, email)
    @name  = name
    @age   = age
    @email = email
  end
end

p = Person.new("Alice", 30, "a@b.com")
puts p.name          # Alice    (reader)
p.age = 31           # setter
p.email = "c@d.com"  # accessor setter
puts p.email         # c@d.com  (accessor getter)
# puts p.age         # NoMethodError — getter 없음

self 완전 정리

class Demo
  puts self         # Demo  (클래스 정의 본문에서 self = 클래스 자신)

  def instance_method
    puts self       # #<Demo:0x...>  (인스턴스)
    self.class      # Demo
  end

  def self.class_method
    puts self       # Demo  (클래스 메서드에서 self = 클래스)
  end
end

Demo.class_method  # Demo
Demo.new.instance_method  # #<Demo:...>

6. 상속 (Inheritance)

class Vehicle
  attr_reader :speed

  def initialize(speed)
    @speed = speed
  end

  def move
    "#{self.class}이(가) #{@speed}km/h로 이동"
  end
end

class Car < Vehicle
  def initialize(speed, brand)
    super(speed)        # 부모 initialize 호출
    @brand = brand
  end

  def honk
    "빵빵!"
  end
end

c = Car.new(100, "현대")
puts c.move    # Car이(가) 100km/h로 이동
puts c.honk    # 빵빵!
puts c.is_a?(Car)      # true
puts c.is_a?(Vehicle)  # true
puts Car.superclass    # Vehicle

super의 세 가지 사용법

class Parent
  def greet(name, msg)
    "#{name}: #{msg}"
  end
end

class Child < Parent
  def greet(name, msg)
    super              # 부모에 같은 인자 전달 (name, msg 그대로)
  end

  def greet2(name, msg)
    super(name, "안녕!")  # 인자를 직접 지정
  end

  def greet3(name, msg)
    super()            # 인자 없이 호출 (부모에 빈 인자 전달)
  end
end

7. 모듈(Module)과 메서드 탐색 순서(MRO)

Ruby Silver에서 가장 까다로운 항목입니다. 어떤 메서드가 먼저 호출되는지 정확히 알아야 합니다.

include, prepend, extend 비교

module M
  def hello
    "M"
  end
end

# include: 클래스 위, 상위 클래스 아래에 삽입
class A
  include M
  def hello; "A"; end
end
puts A.new.hello           # A  (클래스가 우선)
p A.ancestors              # [A, M, Object, ...]

# prepend: 클래스보다 앞에 삽입
class B
  prepend M
  def hello; "B"; end
end
puts B.new.hello           # M  (prepend된 모듈이 우선)
p B.ancestors              # [M, B, Object, ...]

include된 모듈이 여럿일 때

module X
  def who; "X"; end
end
module Y
  def who; "Y"; end
end

class Z
  include X
  include Y   # 나중에 include한 모듈이 우선순위 높음
end

puts Z.new.who  # Y
p Z.ancestors   # [Z, Y, X, Object, ...]

핵심 규칙: 나중에 include된 모듈이 조상 체인에서 더 앞에 위치합니다.

extend — 인스턴스가 아닌 특정 객체에 모듈 메서드 추가

module Greetable
  def hi
    "Hi from module!"
  end
end

class Person; end

alice = Person.new
alice.extend(Greetable)
puts alice.hi      # Hi from module!

bob = Person.new
# bob.hi           # NoMethodError — bob에는 적용 안 됨

클래스에 extend하면 모듈 메서드가 클래스 메서드가 됩니다.

class Service
  extend Greetable
end
puts Service.hi    # Hi from module!

8. 접근 제어자 (public / protected / private)

기본 규칙

제어자클래스 외부에서같은 클래스 내부하위 클래스 내부같은 클래스 인스턴스끼리
publicOOOO
protectedXOOO (수신자 명시 가능)
privateXO (수신자 없이만)O (수신자 없이만)X

private 핵심 규칙: 수신자 명시 불가

class Account
  def public_action
    secret_work         # OK: 수신자 없이 호출
    # self.secret_work  # Ruby 2.6 이하: NoMethodError
                        # Ruby 2.7 이상: self.private 허용됨 (시험 주의!)
  end

  private

  def secret_work
    "비밀 작업"
  end
end

a = Account.new
puts a.public_action    # 비밀 작업
# a.secret_work         # NoMethodError

protected: 같은 클래스 인스턴스끼리 비교할 때

class Wallet
  def initialize(amount)
    @amount = amount
  end

  def richer_than?(other)
    amount > other.amount   # other.amount 호출 가능 — 같은 클래스이므로
  end

  protected

  def amount
    @amount
  end
end

w1 = Wallet.new(1000)
w2 = Wallet.new(500)
puts w1.richer_than?(w2)  # true
# w1.amount               # NoMethodError — 외부에서 직접 호출 불가

9. Comparable과 Enumerable 모듈

Comparable — <=> 연산자 구현으로 비교 기능 획득

class Temperature
  include Comparable

  attr_reader :degrees

  def initialize(degrees)
    @degrees = degrees
  end

  def <=>(other)
    @degrees <=> other.degrees
  end
end

temps = [Temperature.new(30), Temperature.new(20), Temperature.new(25)]
p temps.sort.map(&:degrees)   # [20, 25, 30]
p temps.min.degrees           # 20
p temps.max.degrees           # 30

t1 = Temperature.new(30)
t2 = Temperature.new(20)
puts t1 > t2    # true
puts t1.between?(Temperature.new(25), Temperature.new(35))  # true

Enumerable — 컬렉션 순회 기능을 모두 획득

class NumberBag
  include Enumerable

  def initialize(*nums)
    @nums = nums
  end

  def each(&block)          # each만 구현하면 Enumerable 메서드 전부 사용 가능
    @nums.each(&block)
  end
end

bag = NumberBag.new(3, 1, 4, 1, 5, 9, 2)
p bag.sort               # [1, 1, 2, 3, 4, 5, 9]
p bag.select(&:odd?)     # [3, 1, 1, 5, 9]
p bag.min                # 1
p bag.sum                # 25

10. Enumerable/Array 빈출 메서드 심화

nums = [3, 1, 4, 1, 5, 9, 2, 6]

# map — 변환 (새 배열 반환)
p nums.map { |n| n ** 2 }         # [9, 1, 16, 1, 25, 81, 4, 36]

# select/filter — 조건 참인 것만
p nums.select { |n| n > 3 }       # [4, 5, 9, 6]

# reject — 조건 거짓인 것만 (select 반대)
p nums.reject { |n| n > 3 }       # [3, 1, 1, 2]

# inject/reduce — 누적
p nums.inject(:+)                  # 31  (심볼 전달)
p nums.inject(100, :+)             # 131 (초기값 100)
p nums.inject { |sum, n| sum + n } # 31  (블록 전달)

# each_with_object — 누적 객체 명시적으로 사용
result = nums.each_with_object(Hash.new(0)) do |n, h|
  h[n] += 1
end
p result   # {3=>1, 1=>2, 4=>1, 5=>1, 9=>1, 2=>1, 6=>1}

# flat_map — map 후 1단계 flatten
p [[1, 2], [3, 4]].flat_map { |a| a.map { |n| n * 10 } }  # [10,20,30,40]

# chunk — 연속된 동일 조건으로 그룹화
p [1, 1, 2, 2, 3, 1, 1].chunk { |n| n }.map { |k, v| [k, v.size] }
# [[1,2],[2,2],[3,1],[1,2]]

# each_slice / each_cons
[1,2,3,4,5].each_slice(2) { |s| p s }
# [1, 2]
# [3, 4]
# [5]

[1,2,3,4,5].each_cons(3) { |c| p c }
# [1,2,3]
# [2,3,4]
# [3,4,5]

11. Hash 심화

h = { a: 1, b: 2, c: 3, d: 4 }

# 변환
p h.map { |k, v| [k, v * 10] }.to_h       # {a:10, b:20, c:30, d:40}
p h.transform_values { |v| v * 10 }        # {a:10, b:20, c:30, d:40}
p h.transform_keys { |k| k.to_s }          # {"a"=>1, "b"=>2, ...}

# 필터
p h.select { |_, v| v > 2 }               # {c:3, d:4}
p h.reject { |_, v| v > 2 }               # {a:1, b:2}

# 집계
p h.min_by { |_, v| v }                   # [:a, 1]
p h.max_by { |_, v| v }                   # [:d, 4]
p h.sort_by { |_, v| v }.to_h             # {a:1, b:2, c:3, d:4}
p h.sum { |_, v| v }                      # 10

# merge — 중복 키는 기본적으로 오른쪽 우선
h1 = { a: 1, b: 2 }
h2 = { b: 99, c: 3 }
p h1.merge(h2)                             # {a:1, b:99, c:3}
p h1.merge(h2) { |key, old, new| old + new }  # {a:1, b:101, c:3}

12. String과 정규식 빈출 포인트

s = "Hello, Ruby World!"

# 자주 나오는 메서드
puts s.count("l")          # 3  (문자 l의 개수)
puts s.delete("l")         # Heo, Ruby Word!
puts s.squeeze("l")        # Helo, Ruby World!  (연속된 l을 하나로)
puts s.tr("aeiou", "*")    # H*ll*, R*by W*rld!  (문자 치환)

# scan — 매칭된 것 전부 배열로
puts "1a2b3c".scan(/\d/).inspect  # ["1", "2", "3"]
puts "hello world".scan(/\w+/).inspect  # ["hello", "world"]

# match — 첫 매칭 MatchData 반환
m = "2026-05-30".match(/(\d{4})-(\d{2})-(\d{2})/)
puts m[0]  # 2026-05-30 (전체 매칭)
puts m[1]  # 2026       (첫 번째 캡처 그룹)
puts m[2]  # 05

# =~ 연산자 — 매칭 위치(인덱스) 반환
puts ("hello" =~ /ll/)   # 2  (인덱스 2부터 매칭)
puts ("hello" =~ /xyz/)  # nil

정규식 앵커 비교

# ^, $ : 각 행의 시작/끝 (다중 행에서 주의)
# \A, \z : 문자열 전체의 시작/끝 (시험에서 자주 출제)
str = "hello\nworld"

puts str.match?(/^world/)   # true   (^ 는 줄 단위)
puts str.match?(/\Aworld/)  # false  (\A 는 문자열 전체 시작)

13. 예외 처리 완전 정리

rescue 계층 구조

begin
  raise RuntimeError, "런타임 에러"
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
rescue StandardError => e
  puts "StandardError: #{e.message}"   # 이 줄이 실행됨
rescue => e
  puts "기타: #{e.message}"
ensure
  puts "항상 실행"
end
# StandardError: 런타임 에러
# 항상 실행

핵심: RuntimeErrorStandardError의 하위 클래스이므로 rescue StandardError에서 잡힙니다. rescue 절은 위에서부터 순서대로 확인합니다.

예외 계층도 (시험 필수 암기)

# Exception
# └── StandardError         ← rescue =>  의 기본 포착 범위
#     ├── RuntimeError       ← raise "message"의 기본 예외
#     ├── ArgumentError
#     ├── TypeError
#     ├── NameError
#     │   └── NoMethodError
#     ├── ZeroDivisionError
#     ├── IndexError
#     ├── KeyError
#     └── IOError
# └── ScriptError
# └── SignalException (Interrupt 등)

retry와 raise (재시도 패턴)

attempts = 0

begin
  attempts += 1
  raise "실패" if attempts < 3
  puts "#{attempts}번째에 성공"
rescue
  retry if attempts < 3
  puts "3회 모두 실패"
end
# 3번째에 성공

커스텀 예외

class NetworkError < StandardError
  attr_reader :code

  def initialize(msg = "네트워크 오류", code: 500)
    super(msg)
    @code = code
  end
end

begin
  raise NetworkError.new("연결 실패", code: 503)
rescue NetworkError => e
  puts "#{e.message} (코드: #{e.code})"
end
# 연결 실패 (코드: 503)

14. 상수 탐색 규칙

상수는 어디서 어떤 순서로 탐색되는지가 시험에 자주 나옵니다.

X = "top-level"

module A
  X = "A"

  module B
    X = "B"

    def self.show
      puts X           # "B" — 현재 스코프 우선
      puts A::X        # "A" — 절대 경로
      puts ::X         # "top-level" — 최상위 상수
    end
  end
end

A::B.show

상수 탐색 순서

  1. 현재 클래스/모듈의 상수
  2. 외부를 감싸는 클래스/모듈 (안쪽 → 바깥쪽 순서)
  3. 상속 체인 (상위 클래스 순서)
  4. 최상위(Object) 상수

15. 비교 연산자와 동등성

# == : 값 동등 (각 클래스에서 오버라이드 가능)
# equal? : 같은 객체인지 (object_id 비교, 오버라이드 비권장)
# eql? : 같은 타입 + 같은 값 (Hash 키 비교에 사용)

puts 1 == 1.0       # true  (값이 같으면 true)
puts 1.eql?(1.0)    # false (타입이 다르면 false)
puts 1.equal?(1)    # true  (Integer는 같은 값이면 같은 객체)

s1 = "hello"
s2 = "hello"
puts s1 == s2       # true  (값 비교)
puts s1.eql?(s2)    # true  (같은 타입 + 같은 값)
puts s1.equal?(s2)  # false (다른 객체 — String은 매번 새 객체 생성)

16. 반복문과 루프 반환값

# each — 원본 컬렉션(수신자) 반환
result = [1, 2, 3].each { |n| n * 2 }
p result   # [1, 2, 3]  ← 변환이 아님!

# map — 변환된 새 배열 반환
result = [1, 2, 3].map { |n| n * 2 }
p result   # [2, 4, 6]

# times — 반복 횟수(Integer) 반환
result = 3.times { |i| i }
p result   # 3

# loop — 명시적 break 없으면 무한 루프, break 시 break 값 반환
result = loop { break "done" }
p result   # "done"

17. 객체 복사: dup vs clone

original = "hello"
original.freeze    # 불변(frozen) 상태로 만듦

d = original.dup   # dup은 frozen 상태 복사 안 함
c = original.clone # clone은 frozen 상태도 복사

puts d.frozen?     # false
puts c.frozen?     # true
# d << " world"    # OK
# c << " world"    # FrozenError

핵심: dup은 frozen 상태를 해제한 복사본을, clone은 frozen 상태까지 그대로 복사합니다.

18. freeze와 불변 객체

str = "hello"
str.freeze

puts str.frozen?   # true
# str << " world"  # FrozenError: can't modify frozen String
# str.upcase!      # FrozenError

str2 = str.upcase  # 비파괴적 메서드는 새 객체를 반환하므로 OK
puts str2          # HELLO
puts str           # hello  (원본 유지)

19. 실전 문제 20선 해설

문제 1 — each vs map 반환값

a = [1, 2, 3]
b = a.each  { |n| n * 10 }
c = a.map   { |n| n * 10 }
p b
p c

정답: [1, 2, 3], [10, 20, 30]

해설: each는 블록 결과를 무시하고 수신자(원본 배열)를 반환합니다. map은 블록 결과로 새 배열을 만들어 반환합니다. 시험에서 가장 자주 나오는 함정입니다.

문제 2 — inject 초기값

p [1, 2, 3].inject { |sum, n| sum + n }
p [1, 2, 3].inject(10) { |sum, n| sum + n }

정답: 6, 16

해설: 초기값 생략 시 첫 번째 요소(1)가 초기 누적값이 되고 나머지(2, 3)가 순회됩니다. 초기값 10을 지정하면 모든 요소(1+2+3=6)를 더한 뒤 16이 됩니다.

문제 3 — 클래스 변수 상속

class A
  @@val = "A"
  def self.val; @@val; end
end

class B < A
  @@val = "B"
end

puts A.val
puts B.val

정답: 둘 다 B

해설: @@val은 A와 B가 공유합니다. B에서 재할당하면 A의 값도 바뀝니다. 이 공유 특성 때문에 클래스 변수 대신 인스턴스 변수를 클래스 메서드에서 쓰는 방식(@val)이 권장됩니다.

문제 4 — include 순서와 ancestors

module P; def who; "P"; end; end
module Q; def who; "Q"; end; end

class C
  include P
  include Q
end

puts C.new.who
p C.ancestors

정답: Q, [C, Q, P, Object, ...]

해설: 나중에 include된 모듈(Q)이 조상 체인에서 앞에 위치합니다. 메서드 탐색은 앞에서 뒤로 진행되므로 Q의 메서드가 먼저 실행됩니다.

문제 5 — prepend와 ancestors

module M
  def hello
    "M-" + super
  end
end

class D
  prepend M

  def hello
    "D"
  end
end

puts D.new.hello
p D.ancestors

정답: M-D, [M, D, Object, ...]

해설: prepend는 M을 D보다 앞에 삽입합니다. D.new.hello를 호출하면 M의 hello가 먼저 실행되고, super로 D의 hello를 호출합니다.

문제 6 — Proc return

def test
  [1, 2, 3].each do |n|
    return n if n == 2
  end
  "끝"
end

puts test

정답: 2

해설: 블록 안의 return은 Proc처럼 동작해서 메서드 전체를 종료합니다. n == 2일 때 메서드가 즉시 2를 반환합니다.

문제 7 — private 메서드 호출 규칙

class Sample
  def pub
    pri
  end

  private

  def pri
    "secret"
  end
end

puts Sample.new.pub

정답: secret

해설: private 메서드는 클래스 내부에서 수신자 없이(묵시적 self로) 호출 가능합니다. 외부에서 Sample.new.pri를 호출하면 NoMethodError가 발생합니다.

문제 8 — 블록 변수 스코프

n = 10
[1, 2, 3].each do |n|
  n = n * 2
end
puts n

정답: 10

해설: 블록 매개변수 |n|은 외부 변수 n과 완전히 별개의 지역 변수입니다. 블록 내에서 아무리 수정해도 외부의 n에 영향을 미치지 않습니다.

문제 9 — 블록 지역 변수 선언

n = 10
[1, 2, 3].each do |x; n|   # ; 뒤는 블록 전용 지역 변수 선언
  n = x * 100
  puts n
end
puts n

정답: 100, 200, 300, 그리고 10

해설: |x; n|에서 ; n은 블록 전용 지역 변수를 명시 선언합니다. 이 n은 외부의 n과 완전히 독립됩니다.

문제 10 — fetch vs []

h = { a: 1, b: 2 }

p h[:c]
p h.fetch(:c, 99)
# p h.fetch(:c)   # KeyError 발생

정답: nil, 99

해설: []로 없는 키를 접근하면 nil을 반환합니다. fetch는 키가 없으면 기본값이 없을 경우 KeyError를 발생시키고, 기본값을 제공하면 그 값을 반환합니다.

문제 11 — eql? vs ==

p 1 == 1.0
p 1.eql?(1.0)
p 1.equal?(1)
p "a".equal?("a")

정답: true, false, true, false

해설: ==는 값 비교(Integer 1과 Float 1.0은 같은 값), eql?는 타입+값 비교, equal?는 동일 객체 비교(object_id). String은 같은 내용이라도 매번 새 객체이므로 equal?는 false.

문제 12 — protected

class Box
  def initialize(v)
    @v = v
  end

  def bigger?(other)
    val > other.val
  end

  protected

  def val
    @v
  end
end

puts Box.new(10).bigger?(Box.new(5))

정답: true

해설: val은 protected이므로 외부에서 직접 호출은 불가하지만, 같은 클래스의 인스턴스끼리는 수신자를 붙여서 호출할 수 있습니다.

문제 13 — 상수 탐색

LANG = "Ruby"

module Outer
  LANG = "Outer"

  class Inner
    def show
      puts LANG
    end
  end
end

Outer::Inner.new.show

정답: Outer

해설: Inner 클래스 내부에서 LANG을 탐색할 때, Inner 내부에 없으면 바깥쪽 모듈(Outer)에서 찾습니다. Outer의 LANG이 "Outer"이므로 그 값이 출력됩니다.

문제 14 — lambda 인자 수 검사

p1 = Proc.new { |a, b| [a, b] }
l1 = lambda    { |a, b| [a, b] }

p p1.call(1)
begin
  p l1.call(1)
rescue ArgumentError => e
  puts "Error: #{e.message}"
end

정답: [1, nil], Error: wrong number of arguments (given 1, expected 2)

해설: Proc은 인자 수가 맞지 않아도 nil로 채워 실행합니다. lambda는 인자 수를 엄격히 검사해 ArgumentError를 발생시킵니다.

문제 15 — dup vs clone (frozen)

s = "hello".freeze
d = s.dup
c = s.clone

p d.frozen?
p c.frozen?

정답: false, true

해설: dup은 frozen 상태를 해제한 복사본을 반환합니다. clone은 frozen 상태까지 그대로 복사합니다.

문제 16 — Enumerable any?/all?/none?

p [1, 2, 3].any? { |n| n > 5 }
p [1, 2, 3].all? { |n| n > 0 }
p [1, 2, 3].none? { |n| n > 5 }
p [].any?

정답: false, true, true, false

해설: 빈 배열에서 any?는 false, all?은 true(공허 참), none?은 true입니다. 특히 [].all?이 true인 점이 함정으로 자주 나옵니다.

문제 17 — super 호출 방법

class A
  def greet(msg)
    "A: #{msg}"
  end
end

class B < A
  def greet(msg)
    super + " / B"
  end
end

class C < A
  def greet(msg)
    super() + " / C"   # 인자 없이 호출
  end
end

puts B.new.greet("hi")
puts C.new.greet("hi")

정답: A: hi / B, 그리고 C.new.greet("hi")는 ArgumentError — A의 greet는 msg를 요구하는데 super()로 인자 없이 호출했기 때문입니다.

문제 18 — inject와 심볼 전달

p [1, 2, 3, 4].inject(:+)
p [1, 2, 3, 4].inject(:*)
p [1, 2, 3, 4].inject(10, :+)

정답: 10, 24, 20

해설: :+는 더하기 연산을 심볼로 전달한 것입니다. 초기값 없이 :*를 쓰면 1부터 시작해 순차 곱셈합니다.

문제 19 — String freeze

s = "hello"
s.freeze
s2 = s.upcase

puts s
puts s2
puts s.frozen?
puts s2.frozen?

정답: hello, HELLO, true, false

해설: upcase는 비파괴적 메서드로 새 String 객체를 반환합니다. 원본 s는 frozen이지만 새로 생성된 s2는 frozen이 아닙니다.

문제 20 — 종합: 메서드 탐색과 super

module M
  def name
    "M::" + super
  end
end

class Base
  def name
    "Base"
  end
end

class Child < Base
  include M

  def name
    "Child::" + super
  end
end

puts Child.new.name
p Child.ancestors

정답: Child::M::Base, [Child, M, Base, Object, ...]

해설: 메서드 탐색 순서는 Child → M → Base입니다. Child의 namesuper를 호출하면 M의 name이 실행되고, M에서 다시 super를 호출하면 Base의 name이 실행됩니다. 각 단계의 접두어가 순서대로 이어집니다.

20. 시험장 최종 체크리스트

반드시 암기할 차이점

  • each vs map: each는 원본 반환, map은 변환된 새 배열 반환
  • Proc vs Lambda: Proc은 return 탈출 + 인자 느슨, Lambda는 내부 return + 인자 엄격
  • include vs prepend: include는 클래스 뒤, prepend는 클래스 앞에 삽입
  • include vs extend: include는 인스턴스 메서드, extend는 클래스(싱글턴) 메서드
  • private vs protected: private는 수신자 불가, protected는 같은 클래스끼리 수신자 가능
  • dup vs clone: dup은 frozen 해제, clone은 frozen 유지
  • == vs eql? vs equal?: 값 / 타입+값 / 동일 객체
  • [] vs fetch: []는 없으면 nil, fetch는 없으면 KeyError(기본값 없을 때)

4주 합격 학습 플랜

1주차: 기본 문법 완전 이해 — 변수 스코프, 메서드 인자, 블록, 조건문, 반복문
2주차: 클래스·모듈 심화 — 상속, include/prepend/extend, 접근 제어자, self, ancestors
3주차: 표준 라이브러리 — Array/Hash/String의 핵심 메서드, Comparable, Enumerable, 예외 처리
4주차: 실전 문제 반복 — 틀린 문제 원인 분석, 함정 리스트 암기, 시간 관리 훈련(50문항 65분)

시험 직전 마지막 점검

  • 코드를 한 줄씩 실행 흐름 추적하는 습관이 있는가?
  • ancestors 배열을 보고 메서드 호출 순서를 빠르게 읽을 수 있는가?
  • Proc/lambda return 차이를 예제 없이 설명할 수 있는가?
  • private/protected 메서드 호출 가능 경우를 표로 정리했는가?
  • inject에 초기값 유무에 따른 결과 차이를 안다면 OK

Ruby Silver 합격의 핵심은 "암기"가 아니라 코드를 한 줄씩 정확하게 해석하는 훈련입니다. 이 글의 20문제를 아무것도 보지 않고 풀 수 있다면, 시험 합격 준비는 충분히 된 것입니다.

F

Fit System

10년 이상의 소프트웨어 엔지니어링 경험을 가진 개발자입니다. 고성능 시스템 설계와 클라우드 네이티브 아키텍처를 전문으로 합니다.