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. 클래스 변수 @@count는 Animal과 모든 하위 클래스(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 = 1은 c에 인자가 충분할 때만 기본값을 유지합니다.
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 — 시험 최빈출 항목
이 두 가지는 외형이 비슷하지만 동작이 다릅니다. 반드시 차이를 정확히 외워야 합니다.
비교 표
| 항목 | Proc | Lambda |
|---|---|---|
| 인자 수 검사 | 느슨함 (초과분 무시, 부족분 nil) | 엄격함 (ArgumentError) |
| return의 범위 | 메서드 전체에서 탈출 | lambda 내부에서만 종료 |
| lambda? 메서드 | false | true |
| 생성 방법 | 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)
기본 규칙
| 제어자 | 클래스 외부에서 | 같은 클래스 내부 | 하위 클래스 내부 | 같은 클래스 인스턴스끼리 |
|---|---|---|---|---|
| public | O | O | O | O |
| protected | X | O | O | O (수신자 명시 가능) |
| private | X | O (수신자 없이만) | 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: 런타임 에러
# 항상 실행
핵심: RuntimeError는 StandardError의 하위 클래스이므로 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
상수 탐색 순서
- 현재 클래스/모듈의 상수
- 외부를 감싸는 클래스/모듈 (안쪽 → 바깥쪽 순서)
- 상속 체인 (상위 클래스 순서)
- 최상위(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의 name이 super를 호출하면 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문제를 아무것도 보지 않고 풀 수 있다면, 시험 합격 준비는 충분히 된 것입니다.