객체 지향의 레벨업까지 성공적으로 마치신 여러분, 정말 대단합니다! 👍 지난 Part 7: 객체 지향 레벨 업!에서는 상속, 모듈, 그리고 루비의 강력한 믹스인을 통해 코드의 재사용성과 확장성을 높이는 방법을 배웠습니다. 이제 루비 문법의 진정한 유연성과 표현력을 경험할 차례예요!
이번 파트에서는 루비의 가장 독특하고 강력한 기능 중 하나인 블록(Block), 그리고 블록을 객체처럼 다룰 수 있게 해주는 Proc과 람다(Lambda)에 대해 깊이 알아볼 겁니다. 이 개념들은 처음에는 조금 낯설 수 있지만, 일단 익숙해지면 코드를 놀랍도록 간결하고 우아하게 만들어주는 마법을 선사할 거예요! ✨
메소드에 코드 덩어리를 통째로 전달하는 신기한 경험, 루비의 숨겨진 보석들을 함께 찾아봅시다!
🧱 코드 덩어리를 전달한다? 블록 (Block)
블록(Block)은 이름 없는 코드 덩어리입니다. 메소드를 호출할 때 인자처럼 전달하여, 메소드 내부에서 그 코드 덩어리를 실행하게 할 수 있어요. 마치 메소드에게 "이 작업은 네가 알아서 하는데, 세부적인 이 부분은 내가 알려주는 대로 해줘!" 라고 말하는 것과 비슷합니다.
블록을 작성하는 방법은 두 가지입니다:
- 여러 줄의 코드는 `do ... end` 사용
- 한 줄의 간단한 코드는 중괄호 `{ ... }` 사용 (선호됨)
우리는 이미 이터레이터(.each, .map 등)를 사용하면서 블록을 만나봤습니다!
# .each 메소드에 블록 전달 (do...end 사용)
[1, 2, 3].each do |number|
puts "숫자 #{number}!"
end
# .times 메소드에 블록 전달 ({} 사용)
3.times { |i| puts "#{i+1}번째 안녕!" }
위 코드에서 `do ... end` 부분과 `{ ... }` 부분이 바로 블록입니다. .each나 .times 메소드는 내부적으로 이 전달받은 블록을 실행하는 로직을 가지고 있는 거죠.
🔑 블록을 실행시키는 마법 키워드: `yield`
그렇다면 메소드는 전달받은 블록을 어떻게 실행할까요? 바로 `yield` 키워드를 사용합니다! 메소드 안에서 `yield`가 호출되면, 그 시점에 함께 전달된 블록의 코드가 실행됩니다.
def run_block
puts "메소드 시작!"
yield # 여기서 블록 코드가 실행됩니다!
puts "메소드 끝!"
end
# run_block 메소드를 호출하면서 블록을 전달
run_block do
puts "블록 실행 중... ✨"
end
# 출력 결과:
# 메소드 시작!
# 블록 실행 중... ✨
# 메소드 끝!
`yield`로 블록에 값 전달하기
`yield` 뒤에 값을 써주면, 그 값을 블록으로 전달할 수 있습니다. 블록에서는 파이프(| |) 사이에 변수 이름을 넣어 이 값을 받을 수 있습니다.
def greet_twice
yield "철수" # 블록에 "철수" 전달
yield "영희" # 블록에 "영희" 전달
end
greet_twice do |name| # 블록에서 전달받은 값을 name 변수로 받음
puts "안녕, #{name}!"
end
# 출력 결과:
# 안녕, 철수!
# 안녕, 영희!
이것이 바로 .each 같은 이터레이터가 작동하는 방식입니다! 내부적으로 컬렉션의 각 요소를 `yield`를 통해 블록으로 넘겨주는 것이죠.
🔄 이터레이터와 블록: 환상의 콤비 다시 보기
이제 블록과 `yield`를 알았으니, 이터레이터들이 어떻게 작동하는지 더 명확하게 이해할 수 있습니다.
# Array 클래스의 .each 메소드가 내부적으로 이렇게 동작한다고 상상해보세요 (실제 구현은 더 복잡함)
class Array
def my_each # 직접 each 메소드를 만들어보자!
index = 0
while index < self.length # self는 배열 자신을 가리킴
yield self[index] # 배열의 각 요소를 블록으로 전달!
index += 1
end
self # 관례상 원래 배열을 반환
end
end
[10, 20, 30].my_each { |item| puts "아이템: #{item}" }
# 출력 결과:
# 아이템: 10
# 아이템: 20
# 아이템: 30
블록 덕분에 .each, .map, .select 등 다양한 이터레이터들이 공통된 반복 로직을 가지면서도, 각 요소에 대해 수행할 구체적인 작업(블록 코드)은 외부에서 자유롭게 지정할 수 있게 됩니다. 이것이 루비 코드의 유연성과 간결성의 비결 중 하나입니다!
💾 블록을 객체로 저장하기: Proc 객체
블록은 편리하지만, 그 자체로는 이름이 없고 메소드 호출 시에만 임시로 존재합니다. 만약 코드 블록 자체를 변수에 저장해두거나, 다른 메소드의 인자로 명시적으로 전달하고 싶다면 어떻게 해야 할까요? 이때 사용하는 것이 Proc 객체입니다.
Proc 객체는 블록을 객체로 감싼(Wrapping) 것입니다. Proc.new 에 블록을 전달하여 만들 수 있습니다.
# 블록을 Proc 객체로 만들어서 변수에 저장
greeter = Proc.new do |name|
puts "Hello, #{name}!"
end
# Proc 객체를 실행할 때는 .call 메소드 사용
greeter.call("Ruby") # 출력: Hello, Ruby!
greeter.call("World") # 출력: Hello, World!
메소드에서 Proc 객체를 주고받기 (`&` 연산자)
메소드가 마지막 인자로 `&` 기호가 붙은 매개변수(예: `&my_block`)를 가지면, 해당 메소드를 호출할 때 전달된 **암시적인 블록**이 자동으로 **Proc 객체로 변환**되어 그 매개변수에 할당됩니다.
반대로, 메소드를 호출할 때 Proc 객체 앞에 `&`를 붙여 인자로 전달하면, 그 Proc 객체가 **암시적인 블록**으로 변환되어 메소드 내부에서 `yield`로 실행될 수 있게 됩니다.
이 `&` 연산자는 블록과 Proc 객체 사이를 변환하는 마법 지팡이 역할을 합니다!
# 블록을 받아서 Proc 객체로 변환하고, 그 객체를 다시 블록으로 전달하는 예시
def receive_block(&block) # 전달된 블록이 block 변수에 Proc 객체로 저장됨
puts "Proc 객체를 받았습니다: #{block.inspect}"
# 받은 Proc 객체를 다른 메소드에 블록으로 전달 가능
execute_proc_as_block(block)
end
def execute_proc_as_block(proc_object)
puts "Proc 객체를 블록처럼 실행합니다..."
# 여기서 yield를 사용하려면, 호출 시 Proc 객체 앞에 &를 붙여야 함
# 하지만 여기서는 .call을 사용해 직접 실행해보자.
proc_object.call("값 전달!")
end
# receive_block 메소드에 블록 전달
receive_block { |value| puts "블록 실행됨! 받은 값: #{value}" }
# --- 다른 예시: Proc 객체를 블록으로 변환하여 전달 ---
def takes_implicit_block
puts "암시적 블록 실행:"
yield "암시적!"
end
my_proc = Proc.new { |val| puts "Proc 실행! 값: #{val}" }
# takes_implicit_block 메소드에 Proc 객체를 블록으로 변환하여 전달 (& 사용!)
takes_implicit_block(&my_proc)
💡 `&` 연산자는 조금 헷갈릴 수 있지만, '메소드 정의에서 `&`는 블록을 Proc으로', '메소드 호출에서 `&`는 Proc을 블록으로' 변환한다고 기억하면 좋습니다.stricter 한 블록 객체: 람다 (Lambda)
람다(Lambda)는 Proc 객체의 특별한 한 종류입니다. Proc과 매우 유사하지만 두 가지 중요한 차이점이 있습니다.
- 인자 개수 검사 (Arity Check): 람다는 자신이 정의될 때 기대하는 인자의 개수와
.call로 호출될 때 전달된 인자의 개수가 정확히 일치하는지 엄격하게 검사합니다. 개수가 다르면 오류를 발생시킵니다. 반면, 일반 Proc은 인자 개수가 달라도 관대하게 처리합니다 (부족하면nil, 많으면 무시). return동작 방식: 람다 내부의return은 람다 자체의 실행만 종료시키고 람다를 호출한 메소드로 값을 반환합니다 (일반 메소드의return과 유사). 하지만 일반 Proc 내부의return은 그 Proc을 정의한 주변 메소드(Context) 자체를 종료시켜 버립니다!
람다를 만드는 방법은 두 가지입니다:
lambda키워드 사용: `lambda { |params| ... }`- 'Stabby Lambda' 문법 (더 선호됨): `->(params) { ... }`
# 인자 개수 차이 비교
# Proc: 인자 개수 관대
my_proc = Proc.new { |a, b| puts "Proc: a=#{a.inspect}, b=#{b.inspect}" }
my_proc.call(1) # 출력: Proc: a=1, b=nil (부족해도 nil로 채움)
my_proc.call(1, 2, 3) # 출력: Proc: a=1, b=2 (많으면 무시)
# Lambda: 인자 개수 엄격
my_lambda = ->(a, b) { puts "Lambda: a=#{a.inspect}, b=#{b.inspect}" }
# my_lambda.call(1) # 오류 발생! (ArgumentError: wrong number of arguments)
# my_lambda.call(1, 2, 3) # 오류 발생! (ArgumentError)
my_lambda.call(1, 2) # 출력: Lambda: a=1, b=2 (정확히 맞아야 함)
# return 동작 차이 비교
def test_proc_return
my_proc = Proc.new { return "Proc에서 리턴!" }
my_proc.call
puts "Proc 호출 후 이 문장은 실행되지 않아요!" # Proc의 return이 메소드 자체를 종료시킴
end
def test_lambda_return
my_lambda = -> { return "Lambda에서 리턴!" }
result = my_lambda.call
puts "Lambda 호출 후에도 실행됩니다! Lambda 반환값: #{result}" # Lambda의 return은 Lambda만 종료
end
puts test_proc_return # 출력: Proc에서 리턴!
puts "------"
puts test_lambda_return # 출력: Lambda 호출 후에도 실행됩니다! Lambda 반환값: Lambda에서 리턴!
이런 차이 때문에 람다는 좀 더 예측 가능하고 일반 메소드처럼 동작하기를 기대하는 콜백(Callback) 등에 더 적합할 수 있습니다.
🎮 오늘의 실습: 커스텀 반복 메소드 만들기!
숫자 `n`과 블록을 받아서, 0부터 `n-1`까지 숫자를 블록으로 전달하며 `n`번 반복하는 `repeat` 메소드를 만들어 봅시다! (`times` 메소드와 유사하게)
요구사항:
- `repeat` 메소드를 정의합니다. 이 메소드는 정수 `n`을 첫 번째 인자로 받고, 암시적 블록을 받습니다.
- 메소드 내부에서 `while` 또는 다른 방법을 사용하여 0부터 `n-1`까지 반복합니다.
- 각 반복마다 현재 숫자를 `yield`를 통해 블록으로 전달합니다.
- `repeat` 메소드를 5번 반복하도록 호출하고, 블록에서는 "반복 [숫자+1]번째!" 를 출력합니다.
👇 아래는 정답 코드 예시입니다. 직접 도전해보세요!
# 1. repeat 메소드 정의
def repeat(n)
counter = 0
while counter < n
yield counter # 3. 현재 숫자를 블록으로 전달
counter += 1
end
end
# 4. repeat 메소드 호출 및 블록 전달
repeat(5) do |i|
puts "반복 #{i + 1}번째!"
end
💡 Part 8 핵심 요약
- 블록 (Block): 이름 없는 코드 덩어리. 메소드 호출 시 전달 가능 (
do..end또는{}).yield: 메소드 내에서 블록을 실행시키는 키워드. 블록에 값 전달 가능.- 이터레이터:
.each,.map등은 내부적으로yield를 사용하여 블록과 함께 동작.- Proc 객체: 블록을 객체로 만든 것.
Proc.new { ... }로 생성,.call로 실행. 변수에 저장하거나 인자로 전달 가능.&연산자: 메소드 정의 시 `&`는 블록을 Proc으로, 메소드 호출 시 `&`는 Proc을 블록으로 변환.- 람다 (Lambda): Proc의 특별한 종류 (
lambda { ... }또는-> { ... }).
- 인자 개수를 엄격하게 검사함.
return시 람다 자신만 종료됨 (Proc은 주변 메소드까지 종료).
🚀 다음 단계로!
루비의 숨겨진 보석, 블록, Proc, 람다의 세계를 탐험하신 것을 축하합니다! 🎉 이 개념들은 루비의 함수형 프로그래밍 스타일을 가능하게 하는 핵심 요소이며, 코드를 더욱 유연하고 표현력 있게 만들어 줍니다. 특히 이터레이터와 함께 사용될 때 그 강력함이 배가되죠!
처음에는 조금 어렵게 느껴질 수 있으니, 다양한 예제를 통해 블록을 전달하고 `yield`를 사용해보는 연습을 충분히 해보세요. Proc과 람다의 차이점도 직접 코드를 실행하며 느껴보는 것이 중요합니다.
다음 Part 9: 파일 다루기와 오류 처리 마스터 에서는 프로그램 외부의 파일을 읽고 쓰는 방법과, 예기치 못한 오류가 발생했을 때 프로그램을 안전하게 처리하는 예외 처리(Exception Handling)에 대해 배울 거예요. 프로그램의 활용도를 넓히고 안정성을 높이는 방법을 기대해주세요! 😉
'루비' 카테고리의 다른 글
| 🚀 루비 온 레일즈(Ruby on Rails) 첫걸음: MVC 패턴 이해하고 첫 앱 만들기! (0) | 2025.03.30 |
|---|---|
| 🌐 루비(Ruby) 생태계 부스터 ON: Gem과 Bundler 완벽 활용 가이드! (Part 10) (1) | 2025.03.30 |
| 🧬 루비(Ruby) 객체 지향 레벨 업: 상속, 모듈, 믹스인 마스터! (Part 7) (0) | 2025.03.30 |
| 🏛️ 루비(Ruby) 객체 지향 첫걸음: 클래스와 객체 완벽 이해! (Part 6) (2) | 2025.03.29 |
| 🧩 루비(Ruby) 메소드 완전 정복: 코드 재사용의 마법! (Part 5) (0) | 2025.03.29 |