기초
변수 & 타입
Ruby는 동적 타입 언어입니다—변수에 타입 선언이 필요 없습니다. 타입을 검사하려면 .class를, 타입 확인에는 is_a?를 사용하세요. 숫자와 불리언을 포함한 모든 것이 객체입니다.
name = "Alice" # string
age = 30 # integer
pi = 3.14 # float
is_dev = true # boolean
nums = [1, 2, 3] # array
puts name.class # String
puts age.is_a?(Integer) # trueSymbol
Symbol(:name)은 불변이며 인턴된 문자열입니다—메모리에 하나의 사본만 존재합니다. 해시 키, 메서드 이름, 내용보다 식별성이 중요한 식별자에 사용하세요. 반복 사용 시 문자열보다 메모리 효율적입니다.
status = :active
puts status.class # Symbol
puts status.to_s # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status] # activeNil & 진릿값
Ruby에서는 nil과 false만 거짓입니다—나머지 모든 것(0, '', [] 포함)은 참입니다. 이는 0이 거짓인 많은 언어와 다릅니다. 기본값에는 ||를, nil 확인에는 nil?을 사용하세요.
x = nil
puts x.nil? # true
puts x || "default" # default
# Only nil and false are falsy; 0 and "" are truthy!
puts 0 ? "truthy" : "falsy" # truthy
puts "" ? "truthy" : "falsy" # truthy
puts nil ? "truthy" : "falsy" # falsy타입 변환
to_i/to_s/to_f는 관대한 변환입니다(실패 시 0 반환). Integer()/Float()는 엄격합니다(ArgumentError 발생). 검증이 필요할 때는 엄격한 변환을, 우아한 저하를 원할 때는 관대한 변환을 사용하세요.
puts "42".to_i # 42
puts 42.to_s # "42"
puts "3.14".to_f # 3.14
puts 3.14.to_i # 3
puts "abc".to_i # 0 (no error)
puts Integer("42") # 42 (raises if invalid)문자열 보간
큰따옴표 문자열은 #{expr} 보간을 지원합니다—모든 Ruby 표현식을 넣을 수 있습니다. 작은따옴표 문자열은 리터럴입니다(\\와 \'를 제외한 보간이나 이스케이프 없음). 보간이나 이스케이프 시퀀스가 필요할 때 큰따옴표를 사용하세요.
name = "Alice"
age = 30
puts "Name: #{name}, Age: #{age}"
puts "5 + 3 = #{5 + 3}"
puts "Upper: #{name.upcase}"
# Single quotes don't interpolate
puts 'No #{name} here' # No #{name} here문자열
일반적인 문자열 메서드
Ruby 문자열은 풍부한 메서드를 가집니다. 대부분 새 문자열을 반환합니다(Ruby에서 문자열은 가변입니다). 제자리 수정을 위해 ! 변형(upcase!, gsub!)을 사용하세요—이들은 수신자를 수정하고 변경이 없으면 nil을 반환합니다.
s = "Hello, World"
puts s.length # 12
puts s.upcase # HELLO, WORLD
puts s.downcase # hello, world
puts s.reverse # dlroW ,olleH
puts s.split(", ") # ["Hello", "World"]
puts s.gsub("o", "0") # Hell0, W0rld문자열 변경 (Bang 메서드)
!로 끝나는 메서드는 객체를 제자리에서 수정하며 종종 더 효율적입니다. 변경이 없으면 nil을 반환할 수 있습니다. 복사본 생성을 피하고 싶을 때 사용하지만, 부작용에 주의하세요.
s = "hello"
s.upcase! # s is now "HELLO"
s.gsub!(/L/, "1") # s is now "HE11O"
puts s # HE11O
# Bang methods modify in-place; use with caution
arr = [3, 1, 2]
arr.sort! # arr is now [1, 2, 3]Heredoc & 여러 줄
Heredoc(<<TEXT ... TEXT)이 여러 줄 문자열을 생성합니다. <<~(물결표 heredoc)이 공통 선행 공백을 제거하여 코드를 깔끔하게 합니다. SQL 쿼리, HTML 템플릿, 긴 메시지에 유용합니다.
text = <<~HEREDOC
Hello,
World!
Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentation포맷팅 (sprintf)
% 연산자나 format/sprintf로 C 스타일 문자열 포맷팅을 사용하세요. %-10s는 10자에서 왼쪽 정렬, %08.2f는 8자로 0 채우기(소수점 2자)입니다. 표 형식 출력과 고정 너비 포맷팅에 유용합니다.
puts sprintf("%s is %d", "Alice", 30)
puts "%-10s|%5d" % ["Name", 42]
puts format("%.2f", 3.14159) # 3.14
puts "%08.2f" % 3.14 # 00003.14
# %s string, %d integer, %f float문자열 연결
+는 새 문자열을 생성하고, <<는 제자리에서 추가합니다(문자열 빌드에 더 효율적). *는 문자열을 반복합니다. join이 배열 요소를 구분자와 결합합니다. 성능을 위해 반복되는 +보다는 <<나 join을 선호하세요.
s1 = "Hello" + ", " + "World"
s2 = ["a", "b", "c"].join("-") # a-b-c
s3 = "Hello"
s3 << " " << "World" # in-place append
s4 = "x" * 3 # xxx (repetition)
puts s1, s2, s3, s4데이터 구조
배열
배열은 순서가 있고 0부터 시작하며 혼합 타입을 가질 수 있습니다. <<와 push가 끝에 추가합니다. 멤버십에는 include?를, 합계에는 sum을 사용하세요. 배열은 가변입니다—불변으로 만들려면 freeze를 사용하세요.
nums = [1, 2, 3, 4, 5]
nums.push(6) # [1,2,3,4,5,6]
nums << 7 # same as push
nums[0] = 0
puts nums.first # 0
puts nums.length # 7
puts nums.sum # 28
puts nums.include?(3) # true해시
해시는 키-값 사전입니다. Symbol 키(name:)이 관용적이고 효율적입니다. 존재 확인에는 key?, 안전한 접근에는 fetch(누락 시 발생), 일괄 업데이트에는 transform_values를 사용하세요.
user = { name: "Alice", age: 30 }
user[:email] = "[email protected]"
puts user[:name] # Alice
puts user.key?(:name) # true
user.each { |k, v| puts "#{k}: #{v}" }
puts user.values # ["Alice", 30, "[email protected]"]
puts user.transform_values(&:to_s)범위
'..'는 끝을 포함하고, '...'는 끝을 제외합니다. 범위는 지연 평가되며 큰 시퀀스에 메모리 효율적입니다. 반복, 슬라이싱, 시퀀스 생성에 유용합니다. Comparable 타입과 함께 사용할 수 있습니다.
(1..5).each { |n| puts n } # 1 2 3 4 5
puts (1...5).to_a # [1, 2, 3, 4]
puts (1..10).select(&:even?) # [2, 4, 6, 8, 10]
puts ('a'..'e').include?('c') # true
puts (1..5).map { |n| n ** 2 } # [1, 4, 9, 16, 25]Enumerable 메서드
Enumerable은 Ruby의 가장 강력한 mixin입니다—map, select, reject, reduce, find, group_by, sort_by 등. { |x| x.method }의 약어로 &:method를 사용하세요. 이를 통해 표현적인 데이터 변환 파이프라인이 가능합니다.
nums = [1, 2, 3, 4, 5]
puts nums.map { |n| n * 2 }.inspect # [2,4,6,8,10]
puts nums.select(&:even?).inspect # [2, 4]
puts nums.reject(&:odd?).inspect # [2, 4]
puts nums.reduce(0) { |sum, n| sum + n } # 15
puts nums.find { |n| n > 3 } # 4
puts nums.group_by(&:even?).inspect # {false=>[1,3,5], true=>[2,4]}집합 (Set)
Set('set' 라이브러리에서)은 O(1) 조회로 고유한 요소를 저장합니다. 중복 제거와 집합 연산(합집합, 교집합, 차집합)에 사용하세요. 배열이 필요할 때 to_a로 변환합니다. 'set' require가 필요합니다.
require 'set'
a = Set.new([1, 2, 3])
b = Set.new([3, 4, 5])
puts a.union(b).to_a.inspect # [1,2,3,4,5]
puts a.intersection(b).to_a.inspect # [3]
puts a.subtract([1]).to_a.inspect # [2, 3]
puts a.subset?(Set.new([1,2,3,4])) # true
a.add(6)제어 흐름
If / Elsif / Unless
if/elsif/else는 표준 분기입니다. unless는 if의 반대입니다(조건이 거짓일 때 실행). 수정자 형태(statement if condition)는 한 줄 가드에 관용적입니다—간단한 경우 가독성을 향상시킵니다.
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
end
# Modifier form
puts "Pass" if score >= 60
puts "Fail" unless score >= 60Case (When)
case/when은 매칭에 ===를 사용하여 범위, 클래스, 정규식을 가능하게 합니다. 여러 값을 쉼표로 구분하세요. then은 한 줄 본문을 허용합니다. 대상 없이 case는 더 깔끔한 if/elsif 체인처럼 작동합니다.
grade = "B"
case grade
when "A" then puts "Excellent"
when "B", "C" then puts "Good"
when "D".."F" then puts "Poor"
else puts "Unknown"
end
# Case without value = multi-condition if
case
when score > 90 then puts "Top"
when score > 60 then puts "Pass"
endWhile / Until / Loop
while은 참일 때 실행, until은 참일 때까지 실행(거짓인 동안)합니다. loop는 무한합니다—종료하려면 break를 사용하세요. 컬렉션에는 while보다 반복자(each, map)를 선호하세요—더 관용적이고 오류가 적습니다.
count = 0
while count < 3
puts count
count += 1
end
# until = opposite of while
n = 3
until n == 0
puts n
n -= 1
end
# Infinite loop with break
loop do
puts "forever"
break if rand > 0.8
end반복자 (Each/Times/Upto)
Ruby의 반복자는 for 루프보다 관용적입니다. 카운팅에는 times, 범위에는 upto/downto, 인덱스+값에는 each_with_index를 사용하세요. step이 증분을 제어합니다. 이것들이 Ruby 반복의 근간입니다.
3.times { |i| puts i } # 0 1 2
1.upto(3) { |n| puts n } # 1 2 3
3.downto(1) { |n| puts n } # 3 2 1
[1,2,3].each_with_index do |n, i|
puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n } # 1 2 3Break / Next / Redo
next는 다음 반복으로 건너뛰고(continue와 같음), break는 루프를 조기 종료합니다. break는 블록에서 값을 반환할 수 있습니다. redo는 조건을 다시 확인하지 않고 현재 반복을 다시 시작합니다—거의 사용되지 않습니다.
[1, 2, 3, 4, 5].each do |n|
next if n.even? # skip even
break if n > 4 # stop at 5
puts n # prints 1, 3
end
# break value returns from the block
result = [1,2,3].each { |n| break n * 10 if n == 2 }
puts result # 20메서드 & 블록
메서드 정의
메서드는 def/end를 사용합니다. 키워드 인자(key:)가 많은 매개변수의 가독성을 향상시킵니다. *args는 추가 위치 인자를 배열로 모으고, **kwargs는 키워드 인자를 해시로 모읍니다. 기본값은 =를 사용합니다.
def greet(name, greeting: "Hello")
"#{greeting}, #{name}!"
end
puts greet("Alice") # Hello, Alice!
puts greet("Bob", greeting: "Hi") # Hi, Bob!
def add(*nums) # splat (variadic)
nums.sum
end
puts add(1, 2, 3, 4) # 10블록 & Yield
블록은 메서드에 전달되는 익명 코드 조각입니다. yield가 블록을 호출합니다. 블록은 Ruby 어디에나 있습니다(each, map 등). 메서드를 유연하게 만들려면 yield를 사용하세요—호출자가 동작을 제공합니다.
def repeat(n)
n.times { yield }
end
repeat(3) { puts "hi" } # prints hi 3 times
def with_result
yield(5)
end
puts with_result { |x| x * 2 } # 10Proc & Lambda
Proc과 lambda는 변수에 저장된 재사용 가능한 블록입니다. 주요 차이: lambda는 인자 수를 확인(불일치 시 발생)하고 자신에게만 반환; proc은 관대하고 둘러싼 메서드에서 반환합니다. 엄격한 동작에는 lambda를 선호하세요.
square = proc { |x| x * x }
puts square.call(5) # 25
puts square.(5) # 25 (shortcut)
double = lambda { |x| x * 2 }
puts double.call(5) # 10
# Lambda checks arity; Proc doesn't
# Lambda returns from itself; Proc returns from enclosing method메서드 객체 (&)
&:method가 메서드 이름을 Proc으로 변환합니다. 간단한 한 메서드 블록의 관용적 약어입니다: map { |s| s.to_i } 대신 map(&:to_i). 간단한 변환에 더 깔끔하고 가독성이 좋습니다.
nums = ["1", "2", "3"]
ints = nums.map(&:to_i) # [1, 2, 3]
puts ints.inspect
# &:to_i is shorthand for { |s| s.to_i }
names = ["alice", "bob"]
up = names.map(&:upcase) # ["ALICE", "BOB"]
puts up.inspect반환값
Ruby 메서드는 마지막으로 평가된 표현식을 암시적으로 반환합니다—명시적 return이 필요 없습니다. 조기 종료에 return을 사용하세요. 여러 값이 배열로 반환되고 분해될 수 있습니다. 이것이 코드를 간결하게 만듭니다.
def status(ok)
return "error" unless ok
"ok" # implicit return of last expression
end
puts status(true) # ok
puts status(false) # error
# Multiple assignment
def coords
return 1, 2 # returns [1, 2]
end
x, y = coords클래스 & OOP
클래스 & 인스턴스 변수
@var = 인스턴스 변수(객체별), @@var = 클래스 변수(공유). attr_accessor가 getter+setter 생성, attr_reader는 getter만, attr_writer는 setter만 생성합니다. self.method가 클래스 메서드를 정의합니다.
class Person
attr_accessor :name, :age
attr_reader :id
@@count = 0 # class variable
def initialize(name, age)
@name = name # instance variable
@age = age
@@count += 1
end
def self.count; @@count; end
end
p = Person.new("Alice", 30)
puts p.name, Person.count상속 & Super
<는 상 속을 나타냅니다(단일 상속만). super가 부모의 현재 메서드 버전을 호출합니다. 부모 동작을 확장하려면 super(인자 전달 시 괄호, 같은 인자 전달 시 괄호 없이)를 사용하세요. Ruby는 단일 상속 + mixin을 사용합니다.
class Animal
def initialize(name); @name = name; end
def speak; "..."; end
end
class Dog < Animal
def speak; "#{@name}: Woof!"; end
end
class Puppy < Dog
def speak; "#{super} (small)"; end
end
puts Puppy.new("Rex").speak # Rex: Woof! (small)모듈 (Mixin)
모듈은 재사용 가능한 메서드를 그룹화합니다. include는 인스턴스 메서드 추가, extend는 클래스 메서드 추가. 이것이 Ruby의 다중 상속에 대한 해결책입니다. Enumerable이 유명한 mixin입니다—이를 include하고 each를 정의하면 map, select 등을 얻습니다.
module Walkable
def walk; "#{@name} is walking"; end
end
module Swimmable
def swim; "#{@name} is swimming"; end
end
class Duck
include Walkable # instance methods
extend Swimmable # class methods
def initialize(n); @name = n; end
end
puts Duck.new("Donald").walk # Donald is walking접근 제어
public(기본), private(명시적 수신자 없이만 호출 가능), protected(클래스 계층 내에서 호출 가능). 내부 헬퍼에는 private을, 같은 클래스 인스턴스 간 공유 메서드에는 protected를 사용하세요.
class BankAccount
def initialize(bal); @balance = bal; end
def deposit(amt); @balance += amt; end
def balance; @balance; end
private
def audit; "auditing..."; end
protected
def compare(other); @balance > other.balance; end
end
a = BankAccount.new(100)
puts a.balance # 100
# a.audit # Error: private methodSelf & 클래스 메서드
self는 현재 객체를 참조합니다. 클래스 본문 내에서 self는 클래스입니다—def self.method가 클래스 메서드를 정의합니다. 클래스 메서드는 클래스에서(Counter.total), 인스턴스 메서드는 인스턴스에서 호출합니다. alias_method가 메서드 별칭을 생성합니다.
class Counter
@@total = 0
def initialize; @@total += 1; end
def self.total; @@total; end
def self.reset!; @@total = 0; end
def instance_method; "I'm an instance"; end
alias_method :count, :instance_method
end
Counter.new; Counter.new
puts Counter.total # 2오류 처리
Begin / Rescue / Ensure
begin/rescue는 Ruby의 try/catch입니다. 대상 처리를 위해 특정 예외 클래스를 rescue하세요. => e가 예외 객체를 캡처합니다. ensure는 항상 실행됩니다—정리(파일 닫기, 락 해제)에 사용하세요.
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Caught: #{e.message}"
rescue => e
puts "Other error: #{e.class}"
ensure
puts "Always runs (cleanup)"
end
# ensure runs regardless of success/failureRaise & 커스텀 예외
raise가 예외를 throw합니다(인자 없는 raise는 재발생). 커스텀 예외는 StandardError(또는 더 구체적인 클래스)를 상속합니다. Error 접미사로 이름을 지으세요. 특정 실패 모드를 처리하려면 클래스별로 rescue하세요.
class InvalidAgeError < StandardError; end
def set_age(age)
raise InvalidAgeError, "Age cannot be negative" if age < 0
raise ArgumentError, "Must be Integer" unless age.is_a?(Integer)
@age = age
end
begin
set_age(-5)
rescue InvalidAgeError => e
puts "Custom: #{e.message}"
endRetry
retry가 begin 블록을 처음부터 다시 시작합니다. 일시적 실패(네트워크, 속도 제한)에 카운터와 함께 사용하여 무한 루프를 피하세요. 제한 없이 retry는 프로그램을 멈출 수 있습니다—항상 가드하세요.
attempts = 0
begin
attempts += 1
fetch_data # might fail
rescue NetworkError
retry if attempts < 3
puts "Failed after 3 attempts"
end
# retry re-runs the begin block from the topRescue 수정자
rescue를 수정자로 사용하면 대체 값을 제공하는 간결한 방법입니다. StandardError를 잡고 오른쪽을 반환합니다. 간단한 경우에 사용하세요—오류를 숨기므로 복잡한 로직에는 피하세요. 파싱이나 선택적 작업에 좋습니다.
result = risky_operation rescue "default"
puts result
# Same as:
# result = begin; risky_operation; rescue; "default"; end
# Catches StandardError only
json = JSON.parse(str) rescue nil
puts "Invalid JSON" if json.nil?Throw / Catch (제어 흐름)
throw/catch는 예외 처리가 아닙니다—깊은 중첩에서 조기 종료하기 위한 제어 흐름 메커니즘입니다(다른 언어와 달리). throw :symbol이 일치하는 catch로 점프합니다. 중첩 루프에서 빠져나올 때 사용하고, 실제 오류에는 begin/rescue를 사용하세요.
catch(:done) do
[1, 2, 3, 4, 5].each do |n|
throw :done, n if n > 3
puts n
end
end
# Prints 1, 2, 3 and returns 4
# Not for errors—use for early exit from nested loops파일 I/O
파일 읽기 & 쓰기
File.write/File.read는 간단한 일회성 메서드입니다. 블록과 함께 File.open은 파일을 자동으로 닫습니다. File.foreach는 전체 파일을 로드하지 않고 한 줄씩 읽습니다—큰 파일에 메모리 효율적입니다. chomp가 후행 줄바꿈 을 제거합니다.
# Write
File.write("test.txt", "Hello, File!")
# Read
content = File.read("test.txt")
puts content # Hello, File!
# Append
File.open("log.txt", "a") { |f| f.puts "new line" }
# Read line by line
File.foreach("test.txt") { |line| puts line.chomp }파일 블록 (자동 닫기)
항상 블록과 함께 File.open을 사용하세요—오류가 발생해도 파일이 닫힘을 보장합니다. 블록 형태가 파일을 다루는 관용적이고 안전한 방법입니다. 블록 없이 사용하면 수동으로 close를 호출해야 합니다.
File.open("data.txt", "w") do |f|
f.puts "Line 1"
f.puts "Line 2"
f.write("No newline")
end # file auto-closed here
File.open("data.txt", "r") do |f|
f.each_line.with_index { |line, i| puts "#{i}: #{line}" }
end파일 존재 & 정보
File 클래스가 파일 시스템 쿼리를 제공합니다. file?/directory?로 타입을 구분합니다. mtime/ctime/atime이 타임스탬프를 제공합니다. rename/delete가 파일 시스템을 수정합니다. 오류를 피하려면 작업 전에 존재를 항상 확인하세요.
puts File.exist?("test.txt") # true
puts File.file?("test.txt") # true (regular file)
puts File.directory?(".") # true
puts File.size("test.txt") # bytes
puts File.mtime("test.txt") # modification time
File.rename("old.txt", "new.txt")
File.delete("new.txt") if File.exist?("new.txt")디렉토리 작업
Dir이 디렉토리를 관리합니다. mkdir이 생성, chdir이 변경합니다(블록 형태는 후에 복원). glob이 파일 패턴을 매치합니다—*는 모두, **는 재귀적으로 매치. 파일 발견과 배치 처리에 유용합니다.
Dir.mkdir("test_dir") unless Dir.exist?("test_dir")
Dir.chdir("test_dir") do
File.write("a.txt", "a")
puts Dir.pwd # current path
end
Dir.glob("*.txt") { |f| puts f } # list .txt files
Dir.glob("**/*.rb") { |f| puts f } # recursiveCSV & JSON
CSV와 JSON은 표준 라이브러리에 있습니다. CSV.foreach가 행을 스트리밍합니다(메모리 효율적). JSON.parse가 문자열 키로 해시/배열을 반환합니다. to_json이 모든 객체를 직렬화합니다. 데이터 교환에 필수적입니다.
require 'csv'
require 'json'
CSV.write("data.csv", [["a", 1], ["b", 2]])
CSV.foreach("data.csv") { |row| puts row.inspect }
data = { name: "Alice", age: 30 }
File.write("data.json", data.to_json)
parsed = JSON.parse(File.read("data.json"))
puts parsed["name"] # Alice날짜/시간 & 정규식
시간 & 날짜
Time은 순간을 나타냅니다(타임존 포함). Date는 달력 날짜를 나타냅니다(시간 없음). Time의 산술은 초 단위를 사용합니다. strftime이 %Y(연도), %m(월), %d(일), %H:%M(시간)으로 포맷합니다. ISO8601 파싱을 위해 'time'을 require하세요.
require 'time'
now = Time.now
puts now # 2024-01-15 14:30:00 +0800
puts now.strftime("%Y-%m-%d %H:%M") # 2024-01-15 14:30
tomorrow = now + 86400 # +1 day (seconds)
puts tomorrow.strftime("%A") # weekday name
date = Date.today
puts date.next_day # tomorrow날짜 파싱 & 산술
날짜 산술은 자연스럽게 작동합니다—정수를 더하면 일이 추가됩니다. next_month/prev_month가 월 경계를 처리합니다. Date - Date는 Rational(일)을 반환합니다. upto/downto가 날짜 범위를 반복합니다. 달력 로직에는 Date, 타임스탬프에는 Time을 사용하세요.
require 'date'
d = Date.parse("2024-01-15")
puts d.year # 2024
puts d + 7 # 2024-01-22 (+7 days)
puts d.next_month # 2024-02-15
diff = (Date.today - d).to_i
puts "#{diff} days since"
puts Date.today.upto(Date.today + 6).map(&:wday)정규식 매칭
=~가 매치 위치나 nil을 반환합니다. $1, $2가 매치 후 캡처된 그룹을 저장합니다. .match가 더 자세한 MatchData 객체를 반환합니다. 간단한 확인에는 =~를, 캡처 추출에는 .match를 사용하세요. 정규식 리터럴은 /pattern/을 사용합니다.
s = "Phone: 123-4567"
if s =~ /(\d+)-(\d+)/
puts "Area: #{$1}, Number: #{$2}"
end
m = /\w+@(\w+)/.match("[email protected]")
puts m[1] # example
puts "abc123" =~ /\d/ ? "has digit" : "no digit"정규식 치환
gsub가 모든 매치를 치환합니다(sub는 첫 번째만). 동적 치환을 위해 블록을 전달하세요. scan이 모든 매치를 배열로 추출합니다. 치환 문자열의 역참조(\1, \2)가 캡처된 그룹을 참조합니다. 텍스트 처리에 강력합니다.
s = "Hello, World"
puts s.gsub(/o/, "0") # Hell0, W0rld
puts s.gsub(/\w+/) { |w| w.capitalize }
puts s.scan(/\w+/).inspect # ["Hello", "World"]
puts "2024-01-15".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1')
# => 15/01/2024정규식 옵션
정규식 옵션: i(대소문자 구분 없음), m(멀티라인—점이 줄바꿈 매치), x(확장—패턴 내 공백/주석 허용). %r{}은 패턴에 슬래시가 포함된 경우(URL 같은) 유용한 대체 구분자입니다.
puts /hello/i =~ "HELLO" # 0 (case-insensitive)
puts /line/m =~ "a\nb" # 0 (multiline: . matches \n)
puts /x.y/x =~ "x y" # 0 (extended: ignore whitespace)
# Common patterns
email = /[\w.]+@[\w]+\.[a-z]+/
url = %r{https?://[\w./]+}
puts email.match("[email protected]") ? "valid" : "invalid"메타프로그래밍 & 동시성
동적 메서드 정의
define_method가 런타임에 메서드를 생성합니다. 여러 유사한 메서드를 생성하거나 DSL을 빌드하는 데 사용하세요. 이것이 메타프로그래밍입니다—코드를 작성하는 코드. 강력하지만 신중하게 사용하세요; 코드 이해를 어렵게 만들 수 있습니다.
class Dynamic
[:foo, :bar, :baz].each do |name|
define_method(name) { puts "Called #{name}" }
end
end
d = Dynamic.new
d.foo # Called foo
d.bar # Called bar
# Useful for generating similar methodsmethod_missing
method_missing이 정의되지 않은 메서드 호출을 가로챕니다. 유연한 API나 프록시를 빌드하는 데 사용하세요. 리플렉션이 작동하도록 respond_to_missing?도 항상 재정의하세요. 신중하게 사용하세요—버그를 숨기고 정적 분석을 혼란스럽게 할 수 있습니다.
class Proxy
def method_missing(name, *args)
puts "Called: #{name} with #{args.inspect}"
end
def respond_to_missing?(name, include_private = false)
true
end
end
p = Proxy.new
p.anything(1, 2) # Called: anything with [1, 2]스레드
스레드가 코드를 동시에 실행합니다. join이 완료를 대기합니다. MRI(표준 Ruby)는 GIL을 가지고 있어 CPU 바운드 스레드는 진정한 병렬로 실행되지 않습니다—I/O 바운드 스레드는 그렇습니다. 진정한 병렬성을 위해 Ractor(Ruby 3.0+)이나 다중 프로세스를 사용하세요.
threads = [1, 2, 3].map do |n|
Thread.new { puts "Thread #{n}: #{n * n}" }
end
threads.each(&:join) # wait for all
# MRI has GIL—threads don't run truly parallel for CPU work
# Use for I/O concurrency (network, file)Fiber (협력적)
Fiber는 가벼운 협력적 동시성입니다—yield/resume으로 수동으로 일시 중지하고 재개합니다. 스레드와 달리 병렬로 실행되지 않습니다. 제네레이터, 지연 평가, 일시 정지 가능한 계산에 사용하세요. 스레드보다 오버헤드가 낮습니다.
fiber = Fiber.new do
Fiber.yield 1
Fiber.yield 2
3
end
puts fiber.resume # 1
puts fiber.resume # 2
puts fiber.resume # 3
# Fibers yield control cooperatively, not preemptivelySend & Eval
send가 이름으로 모든 메서드를 호출합니다(private 포함). public_send는 가시성을 존중합니다. eval이 문자열을 Ruby 코드로 실행합니다—매우 강력하지만 신뢰할 수 없는 입력에 위험합니다(코드 주입). 동적 디스패치에는 send를, 프로덕션에서는 eval을 피하세요.
class Obj
def secret; "hidden"; end
end
o = Obj.new
puts o.send(:secret) # hidden (calls private too)
puts o.public_send(:secret) # hidden (respects visibility)
x = 5
result = eval("x * 2")
puts result # 10
# eval runs a string as code—avoid with untrusted input!블록, Proc & Lambda
블록 기초
블록은 Ruby의 가장 흔한 클로저입니다—{ }나 do...end로 메서드에 전달되는 익명 코드. yield가 블록을 호출합니다. |n|이 블록 매개변수를 선언합니다. block_given?이 블록이 전달되었는지 확인합니다. 블록은 객체가 아닙니다(변수에 저장 불가)—이를 위해서는 Proc/Lambda를 사용하세요. 모든 메서드는 암시적 블록을 받을 수 있어 DSL을 자연스럽게 만듭니다(Rails가 이를 heavy하게 사용).
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n } # do-end for multi-line
[1, 2, 3].each do |n|
puts n * 2
end
# yield: call the block from a method
def greet
puts "before"
yield # invokes the block
puts "after"
end
greet { puts "in block" }
# before / in block / after
# yield with arguments
def compute
result = yield(10, 20)
puts "got: #{result}"
end
compute { |a, b| a + b } # got: 30
# block_given? checks if a block was passed
def maybe_yield
return "no block" unless block_given?
yield
endProc vs Lambda
Proc과 Lambda는 모두 호출 가능한 객체입니다(블록을 객체로 만든 것). Proc은 관대합니다: 추가 인자는 nil, 누락 인자는 nil, 'return'은 둘러싼 메서드를 종료. Lambda는 엄격합니다: 인자 수를 확인하고 'return'은 lambda만 종료. 메서드 같은 동작을 원할 때 lambda를, 블록 같은 동작을 원할 때 Proc을 사용하세요. ->(stabby lambda)가 현대의 간결한 구문입니다.
# Proc: a block stored as an object
p = Proc.new { |x| puts x * 2 }
p.call(5) # 10
p.(5) # 10 (shorthand)
p[5] # 10 (another shorthand)
# Lambda: stricter Proc
l = lambda { |x| puts x * 2 }
l = ->(x) { puts x * 2 } # stabby lambda syntax
l.call(5)
# Key differences:
# 1. Argument checking
p = Proc.new { |a, b| puts a }
p.call(1) # 1 (b is nil, no error)
l = ->(a, b) { puts a }
l.call(1) # ArgumentError (wrong number of args)
# 2. return behavior
def proc_test
p = Proc.new { return 1 }
p.call
return 2 # never reached—Proc return exits method
end
proc_test # 1
def lambda_test
l = lambda { return 1 }
l.call # returns from lambda, not method
return 2
end
lambda_test # 2메서드 객체
method(:name)이 메서드를 Method 객체(Proc처럼 호출 가능)로 검색합니다. &:symbol이 심볼을 해당 메서드를 보내는 proc으로 변환합니다—매우 흔한 관용구: array.map(&:to_s). & 접두사가 Proc을 블록으로(또는 그 반대로) 변환합니다. 이를 통해 메서드를 인자로 우아하게 전달할 수 있습니다. Method 객체는 수신자를 유지하므로 객체에 바인딩됩니다.
class Calculator
def add(a, b) a + b end
end
calc = Calculator.new
# method(:name) gets a Method object
m = calc.method(:add)
m.call(2, 3) # 5
m.(2, 3) # 5
m[2, 3] # 5
# Convert method to Proc
p = m.to_proc
p.call(4, 5) # 9
# &:method shorthand (common in map, each)
["a", "b", "c"].map(&:upcase) # ["A", "B", "C"]
# Equivalent to: .map { |s| s.upcase }
# Symbol#to_proc
:upcase.to_proc.call("hello") # "HELLOW"
# Useful for passing methods as blocks
[1, 2, 3].each(&method(:puts)) # prints 1, 2, 3클로저와 Binding
클로저(블록, proc, lambda)는 변수를 참조로 캡처합니다—캡처된 변수의 업데이트를 봅니다. 이를 통해 상태 저장 클로저(카운터, 누산기)가 가능합니다. Proc#binding이 클로저의 환경에 접근합니다(고급 메타프로그래밍용). Kernel#binding 메서드가 eval을 위한 현재 실행 컨텍스트를 캡처합니다. 클로저가 Ruby 블록을 콜백과 반복자에 강력하게 만드는 이유입니다.
# Closures capture surrounding variables
counter = 0
increment = lambda { counter += 1 }
increment.call # 1
increment.call # 2
puts counter # 2 (closure modified it)
# Multiple closures share the same variables
x = 10
add = lambda { |n| x += n }
get = lambda { x }
add.call(5)
get.call # 15
# Proc#binding: access the closure's environment
def make_counter
count = 0
lambda { count += 1 }
end
c = make_counter
c.call # 1
# c.binding.eval("count") # 1 (peek at captured var)
# Binding for eval in a specific context
b = binding
x = 42
eval("x", b) # 42블록으로 커스텀 반복자
Enumerable을 include하고 #each를 정의하면 map, select, reduce 및 50+ 메서드를 무료로 얻습니다—이것이 Ruby의 반복자 프로토콜입니다. 누산기 빌드에는 inject보다 each_with_object가 더 깔끔합니다. tap이 메서드 체인에 부작용을 삽입합니다(디버깅에 좋음). yield가 있는 커스텀 메서드로 자신만의 DSL(with_timing, with_database 등)을 생성할 수 있습니다. 블록이 Ruby의 반복과 콜백 패턴을 우아하게 만듭니다.
# Define your own iterator using yield
class LinkedList
include Enumerable # gets map, select, etc. for free
def each
node = @head
while node
yield node.value
node = node.next
end
end
end
# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}
# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
.tap { |arr| puts "after map: #{arr}" }
.select { |n| n > 2 }
# Custom method with block
def with_timing
start = Time.now
yield
puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }Enumerable & 반복자
핵심 Enumerable 메서드
Enumerable은 Ruby의 가장 강력한 mixin입니다—이를 include하고 #each를 정의하면 50+ 메서드를 얻습니다. map이 변환, select/reject가 필터, reduce/inject가 집계, find가 첫 매치 반환, group_by/partition이 클러스터. Symbol 단축키(reduce(:+))가 관용적입니다. 이 메서드들은 #each가 있는 배열, 해시, 범위, 파일 모두에서 작동합니다. Enumerable 숙달이 관용적 Ruby의 핵심입니다.
nums = [1, 2, 3, 4, 5, 6]
# map/collect: transform
nums.map { |n| n * 2 } # [2, 4, 6, 8, 10, 12]
# select/filter: keep matching
nums.select { |n| n.even? } # [2, 4, 6]
nums.reject { |n| n.even? } # [1, 3, 5]
# reduce/inject: accumulate
nums.reduce(0) { |sum, n| sum + n } # 21
nums.reduce(:+) # 21 (symbol shorthand)
nums.reduce(:*) # 720
# find/detect: first match
nums.find { |n| n > 3 } # 4
# group_by: cluster
nums.group_by { |n| n.even? } # {false=>[1,3,5], true=>[2,4,6]}
# partition: split into two
nums.partition { |n| n.even? } # [[2,4,6], [1,3,5]]
# chunk: consecutive grouping
[1,1,2,2,3].chunk { |n| n }.to_a지연 평가
lazy가 enumerable을 지연 버전으로 변환합니다—값이 필요할 때만 계산됩니다. 이를 통해 무한 시퀀스가 가능하고, 몇 가지 결과만 필요한 체인을 위해 전체 컬렉션을 계산하지 않아도 됩니다. lazy 없이 map/select가 중간 배열을 생성합니다. lazy를 사용하면 파이프라인이 한 번에 하나씩 값을 당겨옵니다. 큰/무한 데이터셋이나 비싼 변환에 lazy를 사용하세요. 트레이드오프: lazy는 요소별 오버헤드가 있어 작은 컬렉션에서는 더 느립니다.
# Lazy: evaluate on demand (infinite sequences possible)
require 'prime'
# Without lazy: infinite loop!
# primes = (1..Float::INFINITY).select(&:prime?).first(5)
# With lazy: works!
primes = (1..Float::INFINITY).lazy.select(&:prime?).first(5)
# [2, 3, 5, 7, 11]
# Chain without intermediate arrays
(1..1_000_000).lazy
.map { |n| n * 2 }
.select { |n| n > 1_000_000 }
.first(10) # only computes what's needed
# Build a lazy enumerator
enum = Enumerator::Lazy.new(1..Float::INFINITY) do |yielder, n|
yielder << n if n.prime?
end
enum.first(5) # [2, 3, 5, 7, 11]Enumerator와 외부 반복
Enumerator가 반복을 객체로 감쌉니다—블록(내부 반복) 대신 .next를 수동으로(외부 반복) 호출할 수 있습니다. 이를 통해 반복 일시 중지/재개, peeking, 무한 시퀀스 생성이 가능합니다. 블록과 함께 Enumerator.new로 커스텀 반복자(제네레이터)를 빌드할 수 있습니다. 대부분의 Enumerable 메서드는 블록 없이 호출 시 Enumerator를 반환합니다: [1,2,3].map이 체인할 수 있는 Enumerator를 반환합니다.
# Enumerator: an iterator object you can control
enum = [10, 20, 30].each
enum.next # 10
enum.next # 20
enum.next # 30
enum.next # StopIteration
# External iteration with loop (handles StopIteration)
enum = [1, 2, 3].each
loop do
puts enum.next
end
# Create custom Enumerator
fib = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end
fib.first(10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Convert to array
fib.take(10).to_a정렬과 비교
sort가 두 요소를 비교하는 블록을 받습니다(-1, 0, 1 반환). sort_by가 더 효율적입니다—매 비교마다가 아니라 요소당 한 번 정렬 키를 계산합니다(Schwartzian transform). 복잡한 키에는 sort_by를 사용하세요. <=>(우주선) 연산자가 -1/0/1을 반환하고 Ruby 정렬의 기초입니다. min/max/min_by/max_by가 극값을 찾습니다. Comparable을 include하고 <=>를 정의하여 객체의 자연스러운 순서를 지정하세요.
arr = [3, 1, 4, 1, 5, 9, 2, 6]
# sort: returns new array
arr.sort # [1, 1, 2, 3, 4, 5, 6, 9]
arr.sort { |a, b| b <=> a } # descending (9, 6, 5, ...)
# sort_by: more efficient (Schwartzian transform)
arr.sort_by { |n| -n } # descending
words = ["banana", "apple", "cherry"]
words.sort_by { |w| w.length } # ["apple", "banana", "cherry"]
# min/max
arr.min # 1
arr.max # 9
arr.minmax # [1, 9]
arr.min_by { |n| n.abs } # 1
# min/max with block
people.max_by { |p| p[:age] }
# <=> (spaceship): -1, 0, 1
1 <=> 2 # -1
2 <=> 2 # 0
3 <=> 2 # 1해시 반복과 변환
해시도 Enumerable입니다. each/each_pair가 키-값 쌍을 반복합니다. transform_keys/transform_values(Ruby 2.5+)가 키/값을 수정한 새 해시를 생성합니다. select/reject가 새 해시로 필터링합니다. merge가 해시를 결합합니다(블록이 충돌 해결). group_by가 배열에서 해시를 빌드합니다. 해시 반복은 Ruby 데이터 처리의 기초입니다—깔끔하고 표현적인 데이터 조작을 위해 이것들을 숙달하세요.
hash = { a: 1, b: 2, c: 3 }
# Iterate
hash.each { |k, v| puts "#{k}=#{v}" }
hash.each_key { |k| puts k }
hash.each_value { |v| puts v }
hash.each_pair { |k, v| puts "#{k}: #{v}" }
# Transform keys/values
hash.transform_keys { |k| k.to_s } # {"a"=>1, "b"=>2, "c"=>3}
hash.transform_values { |v| v * 10 } # {a:10, b:20, c:30}
# Filter
hash.select { |k, v| v > 1 } # {b:2, c:3}
hash.reject { |k, v| v > 1 } # {a:1}
# Merge
{ a: 1 }.merge({ b: 2 }) # {a:1, b:2}
{ a: 1 }.merge({ a: 2 }) { |k, old, new| old + new } # {a:3}
# Invert (values become keys)
{ a: 1, b: 2 }.invert # {1=>:a, 2=>:b}
# Group arrays into hash
%w[apple apricot banana].group_by { |w| w[0] }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}Gem & Bundler
Gem 기초
Gem은 Ruby 패키지(라이브러리)입니다. gem install이 시스템 gem을 관리합니다. 프로젝트의 경우 버전을 고정하고 의존성을 관리하기 위해 Gemfile과 함께 Bundler를 사용하세요. 버전 지정자: '~> 1.4'(비관적, 패치 허용), '>= 5.0'(낙관적). 그룹(:development, :test, :production)으로 환경별로 필요한 gem만 로드할 수 있습니다. require: false는 Bundler가 자동 require하지 않음을 의미합니다(필요시 수동으로 require).
# Install a gem
$ gem install rails
$ gem install rails -v 7.0.0
$ gem install rails --pre # pre-release
# List installed gems
$ gem list
$ gem list rails
# Uninstall
$ gem uninstall rails
# Gemfile: project dependencies
# Gemfile
source 'https://rubygems.org'
gem 'rails', '7.0.0'
gem 'pg', '~> 1.4' # pessimistic: >= 1.4, < 2.0
gem 'puma', '>= 5.0' # optimistic: >= 5.0
gem 'rspec', group: :test # group-specific
gem 'pry', require: false # don't auto-require
# Groups
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
end
group :production do
gem 'newrelic_rpm'
endBundler 명령
Bundler가 프로젝트가 정확한 gem 버전을 사용하도록 보장합니다. bundle install이 Gemfile을 읽고 Gemfile.lock을 작성합니다(재현성을 위한 정확한 버전—이것을 커밋하세요!). bundle exec가 올바른 gem 버전으로 명령을 실행합니다(충돌 방지). bundle update가 버전을 변경합니다(주의—문제를 일으킬 수 있음). 올바른 gem이 로드되도록 rake/rspec/rails에 항상 bundle exec를 사용하세요. Gemfile.lock이 배포를 재현 가능하게 만듭니다.
# Install all gems from Gemfile
$ bundle install
$ bundle install --without production # skip group
# Update gems
$ bundle update # update all
$ bundle update rails # update specific gem
$ bundle outdated # show outdated gems
# Run commands in bundle context
$ bundle exec rails server
$ bundle exec rspec
$ bundle exec rake db:migrate
# Check for dependency issues
$ bundle check
$ bundle doctor
# Lock file: Gemfile.lock
# Records exact versions installed (for reproducibility)
# Commit Gemfile.lock to version control!
# Add/remove gems
$ bundle add rspec
$ bundle remove rspec
# Clean old gems
$ bundle cleanGemspec (Gem 생성)
.gemspec 파일이 gem의 메타데이터와 의존성을 정의합니다. spec.files가 포함된 파일을 나열; require_paths가 Ruby에게 위치를 알림. 런타임 의존성은 add_dependency, 테스트/빌드 의존성은 add_development_dependency. required_ruby_version이 Ruby 버전을 강제. gem build가 .gem 패키지를 생성; gem install이 로컬에 설치. gem push로 rubygems.org에 게시. 구조: 코드는 lib/, 테스트는 spec/.
# my_gem.gemspec
Gem::Specification.new do |spec|
spec.name = "my_gem"
spec.version = "0.1.0"
spec.summary = "A useful Ruby gem"
spec.description = "Longer description..."
spec.authors = ["Alice"]
spec.email = ["[email protected]"]
spec.homepage = "https://github.com/alice/my_gem"
spec.license = "MIT"
spec.files = Dir["lib/**/*.rb"] + ["README.md"]
spec.require_paths = ["lib"]
spec.required_ruby_version = ">= 3.0"
spec.add_dependency "httparty", "~> 0.21"
spec.add_development_dependency "rspec", "~> 3.12"
end
# Directory structure:
# my_gem/
# lib/my_gem.rb (main file)
# lib/my_gem/version.rb
# my_gem.gemspec
# Gemfile
# spec/ (tests)
# Build and install
$ gem build my_gem.gemspec # creates my_gem-0.1.0.gem
$ gem install ./my_gem-0.1.0.gemRake 작업
Rake는 Ruby의 make입니다—작업 실행기. task :name do ... end로 작업을 정의합니다. Namespace가 관련 작업을 그룹화합니다. 파일 작업은 의존성을 가집니다(소스 변경 시 재빌드). sh가 쉘 명령을 실행합니다. Rake는 테스트, 빌드, 배포, 데이터베이스 작업에 사용됩니다(Rails가 heavy하게 사용). rake task_name으로 실행합니다. rake만 입력하면 default 작업이 실행됩니다. rake greet[Alice]로 인자를 전달하세요.
# Rakefile
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task default: :spec
# Custom task
task :greet, [:name] do |t, args|
puts "Hello, #{args.name}!"
end
# $ rake greet[Alice]
# Namespace
namespace :db do
task :migrate do
puts "migrating..."
end
task :seed do
puts "seeding..."
end
end
# $ rake db:migrate
# File tasks (build dependencies)
file 'output.txt' => 'input.txt' do |t|
sh "cp #{t.source} #{t.name}"
end
# Multi-line task
task :deploy do
sh 'git push heroku main'
sh 'heroku run rails db:migrate'
endRbenv와 RVM (Ruby 버전)
rbenv와 RVM이 한 기기에서 여러 Ruby 버전을 관리합니다. rbenv는 가벼움(shim); RVM은 더 무거움(쉘 명령 재정의). .ruby-version 파일(커밋됨)이 모두가 같은 Ruby 버전을 사용하도록 보장합니다. Gemset(RVM)이나 bundle config path가 프로젝트 gem을 격리합니다. 프로덕션의 경우 시스템 gem 오염을 피하기 위해 bundle config set path로 gem을 프로젝트에 로컬 설치하세요. 항상 .ruby-version에 Ruby 버전을 고정하세요.
# rbenv: lightweight Ruby version manager
$ rbenv install 3.2.0 # install a version
$ rbenv global 3.2.0 # set global version
$ rbenv local 3.1.0 # set per-project (.ruby-version)
$ rbenv versions # list installed
# .ruby-version file (committed to project)
# 3.2.0
# RVM: alternative version manager
$ rvm install 3.2.0
$ rvm use 3.2.0
$ rvm gemset create myapp # isolated gem sets
$ rvm use 3.2.0@myapp
# Bundler config
$ bundle config set path 'vendor/bundle' # install locally
$ bundle config set without 'development test' # for production
# Check versions
$ ruby -v
$ gem -v
$ bundle -vRails 기초
MVC 구조
Rails는 MVC 프레임워크입니다: Model(ActiveRecord)이 데이터를 처리, Controller(ActionController)가 HTTP 요청을 처리, View(ActionView)가 응답을 렌더링. resources가 7개의 RESTful 라우트를 자동 생성합니다. 라우트가 URL을 컨트롤러 액션에 매핑합니다. 관례가 설정보다 우선: 모델을 User로, 컨트롤러를 UsersController로 이름 지으면 Rails가 모두 연결합니다. 이 구조가 모든 Rails 앱의 근간입니다.
# Rails follows Model-View-Controller
# app/
# models/ (ActiveRecord: data + business logic)
# controllers/ (ActionController: handle requests)
# views/ (ActionView: render responses)
# helpers/ (view helpers)
# config/routes.rb (URL routing)
# Route → Controller → Model → View
# GET /users → UsersController#index → User.all → index.html.erb
# config/routes.rb
Rails.application.routes.draw do
resources :users # generates 7 RESTful routes
# GET /users → index
# GET /users/new → new
# POST /users → create
# GET /users/:id → show
# GET /users/:id/edit → edit
# PATCH /users/:id → update
# DELETE /users/:id → destroy
root 'pages#home' # root path
get 'about', to: 'pages#about' # custom route
endActiveRecord 모델
ActiveRecord는 Rails의 ORM입니다—모델이 데이터베이스 테이블에 매핑됩니다. validates가 데이터 무결성을 강제. 연관(has_many, belongs_to, has_one)이 관계를 정의. 콜백(before_save, after_create)이 수명 주기에 후크. Scope가 재사용 가능한 쿼리 조각. ActiveRecord는 관례를 사용: User 모델 → users 테이블, created_at/updated_at 컬럼. Rails의 심장입니다—효과적인 Rails 개발을 위해 숙달하세요.
# app/models/user.rb
class User < ApplicationRecord
# Validations
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
# Associations
has_many :posts, dependent: :destroy
has_one :profile
belongs_to :company
has_and_belongs_to_many :tags
# Callbacks
before_save :normalize_email
after_create :send_welcome_email
# Scopes (reusable queries)
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
private
def normalize_email
self.email = email.downcase
end
end
# Usage
User.create(name: "Alice", email: "[email protected]")
User.active.recent
User.find(1)
User.where("age > ?", 18)컨트롤러와 Strong Param
컨트롤러가 HTTP 요청을 처리하고 모델/뷰를 조정합니다. before_action이 필터를 실행합니다(인증, 리소스 로드). Strong parameter(permit)가 대량 할당 취약점을 방지합니다—화이트리스트된 필드만 설정 가능. redirect_to가 사용자를 다른 곳으로 보내고; render가 뷰를 보여줍니다. @instance 변수는 뷰에서 사용 가능합니다. RESTful 액션(index, show, new, create, edit, update, destroy)이 관례적입니다.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :set_user, only: %i[show edit update destroy]
def index
@users = User.all
end
def show; end # @user set by before_action
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: 'User created'
else
render :new, status: :unprocessable_entity
end
end
private
def set_user
@user = User.find(params[:id])
end
# Strong parameters: whitelist allowed fields
def user_params
params.require(:user).permit(:name, :email, :age)
end
end뷰와 헬퍼
ERB(Embedded Ruby)가 Rails의 기본 템플릿입니다: <%= %>가 출력, <% %>가 실행. link_to/button_to가 HTML 링크/폼을 생성. form_with가 모델에 연결된 폼을 빌드. Partial(_form.html.erb)이 render로 렌더링되는 재사용 가능한 뷰 조각. Path 헬퍼(new_user_path, user_path(user))가 라우트에서 URL을 생성. 헬퍼가 뷰를 깔끔하게 유지합니다. 복잡한 로직의 경우 뷰 헬퍼(app/helpers/)나 데코레이터를 사용하세요.
<!-- app/views/users/index.html.erb -->
<h1>Users</h1>
<%= link_to "New User", new_user_path %>
<ul>
<% @users.each do |user| %>
<li>
<%= link_to user.name, user_path(user) %>
(<%= user.email %>)
<%= link_to "Edit", edit_user_path(user) %>
<%= button_to "Delete", user, method: :delete %>
</li>
<% end %>
</ul>
<!-- Partials: reusable view fragments -->
<!-- app/views/users/_form.html.erb -->
<%= form_with model: user do |form| %>
<% if user.errors.any? %>
<div class="errors">
<% user.errors.full_messages.each do |msg| %>
<p><%= msg %></p>
<% end %>
</div>
<% end %>
<%= form.text_field :name %>
<%= form.email_field :email %>
<%= form.submit %>
<% end %>
<!-- Render partial -->
<%= render "form", user: @user %>마이그레이션과 데이터베이스
마이그레이션이 데이터베이스 스키마를 시간에 따라 진화시키며 버전 관리됩니다. create_table이 테이블을 정의; add_column/remove_column이 수정. t.references가 외래 키를 생성. t.timestamps가 created_at/updated_at을 추가. db:migrate가 대기 중인 마이그레이션을 적용; db:rollback이 마지막을 되돌림. schema.rb 파일이 현재 스키마의 권위 있는 소스입니다(자동 생성). schema.rb를 직접 편집하지 마세요—마이그레이션을 사용하세요. 이를 통해 환경 간 데이터베이스 변경이 재현 가능합니다.
# Generate a migration
$ rails generate migration CreateUsers name:string email:string
# db/migrate/20240101_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false, index: { unique: true }
t.integer :age, default: 0
t.text :bio
t.references :company, foreign_key: true
t.timestamps # created_at, updated_at
end
end
end
# Run migrations
$ rails db:migrate
$ rails db:rollback # undo last migration
$ rails db:seed # load seed data
$ rails db:reset # drop + create + migrate + seed
# Schema file (don't edit manually)
# db/schema.rb reflects current database structure
# Add a column later
$ rails generate migration AddAgeToUsers age:integer
# creates: add_column :users, :age, :integerRSpec 테스트
기본 구문 (describe, it, expect)
RSpec은 Ruby의 주요 테스트 프레임워크입니다. describe가 관련 테스트를 그룹화; it가 단일 테스트를 정의. expect(...).to / not_to가 어설션을 만듭니다. let이 지연 memoized 변수를 정의(처음 접근 시 한 번 계산). context는 describe의 별칭, 분기용으로 사용(when...). shoulda-matchers gem이 흔한 Rails validation/연관에 한 줄 문법을 제공. 테스트는 app/ 구조를 미러링하는 spec/에 있습니다.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Setup with let (lazy, memoized)
let(:user) { User.new(name: "Alice", email: "[email protected]") }
describe '#name' do
it 'returns the name' do
expect(user.name).to eq("Alice")
end
# Multiple expectations with context
context 'when name is blank' do
let(:user) { User.new(name: "") }
it 'is invalid' do
expect(user).not_to be_valid
end
end
end
# One-liner syntax
it { should validate_presence_of(:email) }
it { should have_many(:posts) }
end목과 스텁
스텁(allow)이 메서드 반환값을 교체; 목(expect)이 메서드가 호출되었는지 확인. Double이 테스트용 가짜 객체(실제 객체보다 빠름). 스텁으로 테스트 대상 코드를 외부 의존성(API, 데이터베이스)에서 격리. 목으로 상호작용을 확인. 과도한 목킹은 테스트를 취약하게 만듭니다—충분히 빠를 때 실제 객체를 선호. FactoryBot이 테스트 데이 터 생성; create(DB 저장)나 build(메모리만) 사용.
RSpec.describe PaymentService do
let(:user) { create(:user) }
it 'charges the card' do
# Stub: replace a method's return value
allow(user).to receive(:premium?).and_return(true)
# Mock: expect a method to be called
expect(Stripe::Charge).to receive(:create).with(
amount: 1000, currency: 'usd'
).and_return(double(id: 'ch_123'))
service = PaymentService.new(user)
result = service.charge(10)
expect(result).to eq('ch_123')
end
it 'handles failure' do
# Stub to raise an error
allow(Stripe::Charge).to receive(:create)
.and_raise(Stripe::StripeError.new("card declined"))
service = PaymentService.new(user)
expect { service.charge(10) }.to raise_error(PaymentError)
end
end
# Double: test stand-in for an object
fake_card = double('Card', last4: '4242', brand: 'Visa')FactoryBot과 Fixture
FactoryBot이 합리적인 기본값으로 테스트 객체를 생성. Trait이 변형 생성(:admin, :inactive). Sequence가 고유 값(이메일) 생성. create가 데이터베이스에 저장; build는 그렇지 않음. create_list가 여러 개 생성. 어떤 속성이든 전달하여 덮어쓰기. Factory가 fixture(YAML)보다 유연하지만 느림(DB 쓰기). DB에 도달하지 않는 빠른 테스트에는 build_stubbed를 사용. Factory를 단순하게 유지—복잡한 factory는 복잡한 모델을 나타냄.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { "Alice" }
email { "[email protected]" }
age { 30 }
active { true }
# Traits: variations
trait :admin do
role { "admin" }
end
trait :inactive do
active { false }
end
# Associations
company { association(:company) }
# Sequences for unique values
sequence(:email) { |n| "user#{n}@example.com" }
end
end
# Usage in specs
let(:user) { create(:user) } # saved to DB
let(:admin) { create(:user, :admin) } # with trait
let(:user) { build(:user) } # not saved
let(:users) { create_list(:user, 5) } # 5 users
# Override attributes
let(:alice) { create(:user, name: "Alice") }Before/After 훅
before 훅이 설정 코드를 실행: before(:each)(가장 흔함)가 매 테스트 전, before(:all)이 그룹당 한 번. after 훅이 정리. DatabaseCleaner가 테스트 데이터베이스 상태 관리(속도용 transaction, 철저함을 위해 truncation). before(:each)보다 let을 선호—let은 지연(사용시에만 계산)되고 memoized되지만, before는 테스트가 필요 없어도 실행. 부작용이 반드시 일어나야 할 때 before를 사용(로깅, 시간 동결).
RSpec.describe User do
before(:all) do
# Runs once before all tests in this group
@shared_data = load_expensive_data
end
before(:each) do # or just 'before'
# Runs before each test
@user = User.create(name: "test")
end
after(:each) do
# Runs after each test (cleanup)
User.delete_all
end
after(:all) do
# Runs once after all tests
@shared_data = nil
end
# Database cleaner for transactional tests
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
end
# Use before for setup, after for cleanup
# Prefer let over before(:each) for lazy evaluation통합 및 시스템 테스트
Request spec이 HTTP를 통해 전체 스택을 테스트(라우팅 → 컨트롤러 → 모델 → 뷰). System spec(Capybara)이 실제 브라우저를 구동—폼 채우기, 클릭, 페이지 내용 확인. 단위 테스트(모델 spec)는 빠르고 격리됨; 통합/시스템 테스트는 느리지만 연결 버그를 잡음. 테스트 피라미드 사용: 많은 빠른 단위 테스트, 적은 통합 테스트, 최소한의 시스템 테스트. have_http_status가 응답 코드 확인; visit/fill_in/click_button이 브라우저 구동.
# spec/requests/users_spec.rb (integration)
require 'rails_helper'
RSpec.describe 'Users API', type: :request do
describe 'GET /users' do
before { create_list(:user, 3) }
it 'returns all users' do
get '/users'
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).size).to eq(3)
end
end
describe 'POST /users' do
it 'creates a user' do
post '/users', params: { user: { name: 'Bob' } }
expect(response).to have_http_status(:created)
end
end
end
# spec/system/login_spec.rb (browser tests)
require 'rails_helper'
RSpec.describe 'Login', type: :system do
it 'logs in a user' do
user = create(:user, password: 'secret')
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'secret'
click_button 'Log in'
expect(page).to have_content('Welcome')
end
end파일 & 디렉토리 작업
파일 읽기
File.read가 전체 파일을 메모리에 로드—작은 파일에 적합. File.foreach가 한 줄씩 읽기—큰 파일에 필수(메모리 초과 방지). 블록과 File.open이 파일을 자동으로 닫음(RAII). readlines가 줄 배열을 반환(줄바꿈 포함—chomp 사용). 바이너리 파일은 binread. 파일이 없을 수도 있다면 읽기 전에 File.exist?를 확인하거나 Errno::ENOENT를 rescue하세요.
# Read entire file
content = File.read("data.txt")
# Read line by line (memory efficient)
File.foreach("large.txt") do |line|
puts line.chomp # chomp removes trailing newline
end
# Read all lines into array
lines = File.readlines("data.txt")
# With a block (auto-closes)
File.open("data.txt") do |f|
f.each_line { |line| puts line }
end
# Read with options
File.read("data.txt", encoding: "utf-8")
File.binread("image.png") # binary mode
# Check existence
File.exist?("data.txt")
File.file?("data.txt") # is it a regular file?
File.directory?("path") # is it a directory?파일 쓰기
File.write가 파일을 쓰는 가장 간단한 방법(기본 덮어쓰기). 추가하려면 mode: 'a' 사용. 블록과 File.open이 예외가 발생해도 파일이 닫힘을 보장. puts가 줄바꿈 추가; write는 그렇지 않음. <<는 write의 별칭(Ruby에서 흔함). 로그의 경우 한 번 열고 여러 번 쓰기(성능을 위해 버퍼링). 리소스 누수를 피하기 위해 항상 파일을 닫거나 블록 형태를 사용하세요.
# Write (overwrites)
File.write("output.txt", "Hello, World!")
# Append
File.write("output.txt", "More text\n", mode: "a")
# With a block (buffered, auto-closes)
File.open("log.txt", "w") do |f|
f.puts "Line 1"
f.puts "Line 2"
f.write("No newline")
f << "appended" # << is an alias for write
end
# Binary write
File.binwrite("data.bin", binary_data)
# Modes: r (read), w (write/truncate), a (append),
# r+ (read/write), w+ (read/write/truncate),
# b (binary, Windows)
# Flush buffer
f = File.open("log.txt", "w")
f.write("data")
f.flush # write without closing
f.close디렉토리 작업
Dir.glob이 패턴으로 파일을 찾습니다(**는 재귀). FileUtils가 강력한 파일 작업 제공: mkdir_p가 중첩 디렉토리 생성, cp_r이 재귀 복사, rm_rf가 강제 제거(주의!). Dir.chdir이 작업 디렉토리 변경(블록 형태로 임시 변경). Dir.entries는 .과 .. 포함; glob은 포함 안 함. 크로스 플랫폼 안전을 위해 수동 File 작업보다 FileUtils를 선호하세요.
require 'fileutils'
# List directory contents
Dir.entries(".") # [".", "..", "file.txt", ...]
Dir.glob("*.rb") # ["script.rb", "test.rb"]
Dir.glob("**/*.rb") # recursive
Dir.glob("src/**/*.{rb,erb}") # multiple extensions
# Create directories
Dir.mkdir("new_dir")
Dir.mkdir("nested/path") rescue Errno::ENOENT # fails if parent missing
FileUtils.mkdir_p("nested/path/deep") # creates all parents
# Remove
Dir.rmdir("empty_dir") # only works if empty
FileUtils.rm_rf("dir") # recursive force (dangerous!)
# Copy/Move
FileUtils.cp("a.txt", "b.txt")
FileUtils.mv("old.txt", "new.txt")
FileUtils.cp_r("src_dir", "dest_dir") # recursive
# Change directory
Dir.chdir("/tmp") { puts Dir.pwd } # temporarily
Dir.chdir("/tmp") # permanently for process
# Check if directory
File.directory?("path")
File.exist?("path")Pathname과 Tempfile
Pathname은 파일 경로의 객체 지향 래퍼—문자열 조작보다 깔끔. dirname, basename, extname, join 및 파일 확인을 메서드로 제공. Tempfile이 블록 종료 시(Tempfile.create)나 unlink 시(Tempfile.new) 자동 삭제되는 임시 파일을 생성. 메모리에 맞지 않는 큰 데이터나 경로로 외부 프로그램에 전달할 때 Tempfile 사용. ensure 블록으로 정리 보장. StringIO가 IO 코드 단위 테스트에 좋습니다.
require 'pathname'
require 'tempfile'
# Pathname: object-oriented path manipulation
path = Pathname.new("/home/user/docs/file.txt")
path.dirname # #<Pathname:/home/user/docs>
path.basename # #<Pathname:file.txt>
path.extname # ".txt"
path.parent # #<Pathname:/home/user/docs>
path.join("sub", "file.rb") # /home/user/docs/sub/file.rb
path.exist?
path.directory?
path.file?
path.readable?
# Tempfile: auto-deleted file
Tempfile.create("prefix") do |f|
f.write("temporary data")
f.rewind
puts f.read
end # file deleted after block
# Tempfile without block (must close/unlink manually)
tf = Tempfile.new("prefix")
tf.write("data")
tf.close
tf.unlink # delete
# Dir.mktmpdir for temp directories
Dir.mktmpdir do |dir|
# work in dir
end # dir deleted afterCSV와 JSON
CSV와 JSON은 Ruby 표준 라이브러리에 내장. CSV.foreach가 행 단위 읽기(메모리 효율적); CSV.read가 전체 로드. headers: true가 첫 행을 컬럼명으로 처리. JSON.parse가 JSON을 Ruby 해시/배열로 변환; to_json이 Ruby 객체를 직렬화. symbolize_names가 심볼 키 제공(더 깔끔). YAML의 경우 require 'yaml'과 YAML.load_file 사용. Ruby 스크립트와 웹 앱에서 데이터 교환에 필수적입니다.
require 'csv'
require 'json'
# CSV reading
CSV.foreach("data.csv", headers: true) do |row|
puts row['name'] # access by header
puts row[0] # access by index
end
# CSV reading all at once
rows = CSV.read("data.csv", headers: true)
rows.first['name'] # first row's name
# CSV writing
CSV.open("output.csv", "w") do |csv|
csv << ["name", "age"] # header
csv << ["Alice", 30]
csv << ["Bob", 25]
end
# JSON
data = { name: "Alice", age: 30 }
json = data.to_json # '{"name":"Alice","age":30}'
parsed = JSON.parse(json) # {"name"=>"Alice", "age"=>30}
# JSON with symbols
JSON.parse(json, symbolize_names: true) # {name: "Alice", age: 30}
# Read/write JSON files
File.write("data.json", data.to_json)
loaded = JSON.parse(File.read("data.json"))인코딩 & 문자열 내부
문자열 인코딩
Ruby 문자열은 인코딩을 가집니다(보통 UTF-8). force_encoding이 바이트를 다른 인코딩으로 재해석(변환 없음—바이트가 이미 그 인코딩임을 알 때 사용). encode가 실제로 인코딩 간 변환. valid_encoding?이 바이트가 문자열 인코딩에 유효한지 확인. 인코딩 문제가 두려운 Encoding::CompatibilityError를 일으킵니다. 항상 데이터의 인코딩을 알고; UTF-8을 기본으로 하세요.
# Ruby strings have an encoding
"hello".encoding # #<Encoding:UTF-8>
"café".encoding # #<Encoding:UTF-8>
# Default external encoding (for file I/O)
Encoding.default_external # #<Encoding:UTF-8>
# Force an encoding (reinterprets bytes, doesn't convert)
bytes = "café".bytes # [99, 97, 102, 195, 169]
latin1 = bytes.pack("C*").force_encoding("ISO-8859-1")
# "caf" + é (as single byte)
# Convert encoding (transcodes)
utf8 = "café"
latin1 = utf8.encode("ISO-8859-1") # converts
back = latin1.encode("UTF-8") # converts back
# Check if valid
"abc".valid_encoding? # true
"\xff".valid_encoding? # false (invalid byte)
# Common encodings: UTF-8, ASCII, ISO-8859-1, Windows-1252
# Always use UTF-8 unless you have a specific reason인코딩 변환과 I/O
파일 I/O는 읽기에 Encoding.default_external을 사용. encoding: 옵션으로 파일별 인코딩 지정. 'source:target' 구문(ISO-8859-1:UTF-8)이 소스 인코딩으로 읽고 타겟으로 변환. default_internal 설정 시 Ruby가 모든 읽은 문자열을 그 인코딩으로 자동 변환. 웹 앱의 경우 모두 UTF-8이어야 합니다. 레거시 데이터 처리 시 손상을 피하기 위해 인코딩을 명시적으로 지정하세요.
# Read a file with specific encoding
content = File.read("data.txt", encoding: "ISO-8859-1")
# content.encoding is ISO-8859-1
# Read and convert to UTF-8
content = File.read("data.txt", encoding: "ISO-8859-1:UTF-8")
# The "source:target" syntax converts while reading
# Write with specific encoding
File.write("output.txt", "café", encoding: "UTF-8")
# Open with encoding
File.open("data.txt", "r:ISO-8859-1") do |f|
f.read # ISO-8859-1 encoded string
end
# Convert while opening
File.open("data.txt", "r:ISO-8859-1:UTF-8") do |f|
f.read # UTF-8 encoded string
end
# Set default encodings
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8문자열 메서드 심화
Ruby 문자열은 풍부한 메서드를 가집니다. 검사(length, include?, start_with?)가 속성을 확인. 변환 메서드가 새 문자열을 반환(Ruby에서 문자열은 가변이지만, 이들은 변경하지 않음). 부분 문자열 접근은 [start, length]나 범위 사용. sub가 첫 매치 치환; gsub가 모두 치환(정규식과 블록 지원). split/join이 문자열과 배열 간 변환. 참고: Ruby 3.0+ frozen_string_literal pragma가 성능을 위해 문자열을 불변으로 만듭니다—빌드 시 <<나 + 사용.
s = "Hello, World"
# Inspection
s.length # 12
s.empty? # false
s.include?("World") # true
s.start_with?("Hello") # true
s.end_with?("World") # true
# Transformation (return new string)
s.upcase # "HELLO, WORLD"
s.downcase # "hello, world"
s.capitalize # "Hello, world"
s.swapcase # "hELLO, wORLD"
s.reverse # "dlroW ,olleH"
s.strip # remove leading/trailing whitespace
s.chomp(",") # remove trailing substring
s.chop # remove last char
# Substrings
s[0, 5] # "Hello" (start, length)
s[7..11] # "World" (range)
s[-5..] # "World" (negative index)
# Replace
s.sub("World", "Ruby") # first match
s.gsub("o", "0") # all matches
s.gsub(/\w+/) { |w| w.upcase } # with block
# Split/Join
"a,b,c".split(",") # ["a", "b", "c"]
["a", "b"].join("-") # "a-b"Frozen 문자열과 성능
frozen_string_literal: true(파일 상단의 매직 코멘트)가 모든 문자열 리터럴을 불변으로 만듭니다—이는 성능 최적화(frozen 문자열이 메모리 공유 가능)이며 우발적 변경 버그를 방지합니다. 문자열 빌드에는 <<(제자리 추가, O(n))를 사용하고 +(매번 새 문자열 생성, O(n²))는 피하세요. join이 배열에 가장 깔끔합니다. StringIO는 파일처럼 작동하지만 문자열에 쓰기—복잡한 출력 빌드에 유용. Ruby 3.x는 기본적으로 frozen 문자열을 권장합니다.
# frozen_string_literal: true (at top of file)
# Makes all string literals in the file frozen (immutable)
# Mutable vs frozen
s = "hello"
s << " world" # "hello world" (mutates)
f = "hello".freeze
# f << " world" # Error: can't modify frozen String
# Why freeze? Performance: frozen strings share memory
# "abc".freeze is the same object everywhere
"a".freeze.equal?("a".freeze) # true (same object)
# Building strings efficiently
# BAD: creates many intermediate strings
result = ""
items.each { |i| result += i.to_s } # O(n²)
# GOOD: use << or join
result = ""
items.each { |i| result << i.to_s } # O(n)
result = items.map(&:to_s).join # cleanest
# StringIO for buffered building
require 'stringio'
io = StringIO.new
io << "line 1\n"
io << "line 2\n"
result = io.stringSymbol vs 문자열
Symbol(:name)은 불변이며 싱글톤 식별자입니다—메모리에 :foo는 단 하나만 존재합니다. 문자열은 여러 인스턴스가 있는 가변 텍스트 데이터입니다. 해시 키(더 빠른 동등성 확인), 메서드 이름, enum 같은 값에는 symbol을 사용하세요. 실제 텍스트에는 문자열을 사용하세요. Symbol이 해시 키와 비교에 약간 더 빠릅니다. 현대 Ruby(2.2+)에서는 symbol이 가비지 컬렉션 가능하여, 예전의 'symbol 메모리 누수' 우려가 사라졌습니다. Rails가 키와 상태 값에 symbol을 광범위하게 사용합니다.
# Symbol: immutable, reusable identifier
:hello
:world
status = :active
# String: mutable, can have many instances
"hello"
"world"
status = "active"
# Key difference: identity
"hello".equal?("hello") # false (different objects)
:hello.equal?(:hello) # true (same object, singleton)
# Memory: symbols are singletons
1000.times { :foo } # one :foo object
1000.times { "foo" } # 1000 "foo" objects (unless frozen)
# Use symbols for:
# - Hash keys (faster comparison)
# - Method names (send(:method_name))
# - Enum-like values (:active, :pending, :closed)
# - Identifiers (not text data)
# Use strings for:
# - Text data (names, content)
# - Things that change
# Conversion
:hello.to_s # "hello"
"hello".to_sym # :hello
"hello world".to_sym # :"hello world" (valid but ugly)모듈 & Mixin
모듈 기초
모듈은 두 가지 목적을 제공합니다: namespace(관련 코드 그룹화, 이름 충돌 방지)와 mixin(상속 없이 동작 공유). 모듈 메서드(def self.method)는 모듈에서 호출됩니다. 인스턴스 메서드(def method)는 클래스에 mixin하기 위한 것입니다. 모듈은 인스턴스화할 수 없습니다. 모듈로 클래스를 namespace(MyApp::User)하고 상수과 유틸리티 함수를 조직하는 데 사용하세요. 이것이 Ruby의 다중 상속에 대한 대안입니다.
# Module: a namespace + collection of methods
module MathUtils
PI = 3.14159
def self.circle_area(radius) # module method
PI * radius ** 2
end
def square(x) # instance method (for mixins)
x * x
end
end
# Access constants and module methods
MathUtils::PI # 3.14159
MathUtils.circle_area(5) # 78.54
# Namespacing classes
module MyApp
class User
# MyApp::User
end
end
# Prevent instantiation (modules can't be instantiated)
# MathUtils.new # NoMethodErrorInclude vs Extend vs Prepend
모듈을 mixin하는 세 가지 방법: include(인스턴스 메서드 추가, 조회 시 클래스 아래에 위치), extend(클래스 메서드 추가), prepend(인스턴스 메서드 추가, 클래스 위에 위치—래핑/재정의 가능). prepend는 before/after 훅에 강력합니다(원본을 호출하려면 super 사용). 메서드 조회: prepend → 클래스 → include → 슈퍼클래스. 일반 mixin에는 include, 기존 메서드를 래핑할 필요가 있을 때 prepend, 클래스 수준 기능에는 extend를 사용하세요.
module Greetable
def greet
"Hello from #{self.class}"
end
end
class User
include Greetable # adds as instance methods
end
User.new.greet # "Hello from User"
class Service
extend Greetable # adds as class methods
end
Service.greet # "Hello from Service"
# Method lookup order:
# prepend → class → include → super
module Logging
def save
puts "before save"
super # calls the original save
puts "after save"
end
end
class Record
prepend Logging # Logging#save runs first
def save; puts "saving"; end
end
Record.new.save
# before save / saving / after saveEnumerable Mixin
Enumerable을 include하고 #each를 정의하면 map, select, reduce, sort, min, max 및 40+ 메서드를 얻습니다—이것이 Ruby의 반복자 프로토콜입니다. Comparable을 include하고 <=>를 정의하면 <, >, ==, between?, clamp 및 sort 지원을 얻습니다. 이 mixin이 Ruby 컬렉션이 강력한 이유입니다. 컬렉션이나 자연스러운 순서를 나타내는 모든 클래스는 이를 include해야 합니다. 상속보다 조합입니다.
# Include Enumerable + define #each = 50+ methods free
class Playlist
include Enumerable
def initialize(songs)
@songs = songs
end
def each
@songs.each { |song| yield song }
end
end
playlist = Playlist.new(["Song A", "Song B", "Song C"])
playlist.map { |s| s.upcase } # ["SONG A", "SONG B", "SONG C"]
playlist.select { |s| s.include?("A") } # ["Song A"]
playlist.reduce(:+) # "Song ASong BSong C"
playlist.sort # ["Song A", "Song B", "Song C"]
playlist.first(2) # ["Song A", "Song B"]
playlist.include?("Song B") # true
playlist.count # 3
# Comparable: define <=> for natural ordering
class Temperature
include Comparable
attr_reader :celsius
def initialize(c) @celsius = c end
def <=>(other) celsius <=> other.celsius end
end
t1 = Temperature.new(20)
t2 = Temperature.new(30)
t1 < t2 # true (uses <=>)
t1 == t2 # false싱글톤 메서드와 클래스 메서드
싱글톤 메서드는 하나의 특정 객체에 속합니다. 클래스 메서드는 클래스 객체의 싱글톤 메서드일 뿐입니다. 'class << self'가 싱글톤 클래스(eigenclass)를 열어 여러 클래스 메서드를 깔끔하게 정의합니다. 싱글톤 패턴은 클래스 변수를 사용해 하나의 인스턴스를 보유합니다. 싱글톤 클래스 이해가 Ruby 객체 모델의 핵심입니다—모든 객체는 고유한 메서드를 보유하는 싱글톤 클래스를 가집니다. 이를 통해 객체별 커스터마이징과 메타프로그래밍이 가능합니다.
# Singleton method: defined on one object only
str = "hello"
def str.shout
upcase + "!"
end
str.shout # "HELLO!"
# "world".shout # NoMethodError (only str has it)
# Class methods are singleton methods on the class
class Calculator
def self.add(a, b) # singleton method on Calculator
a + b
end
end
Calculator.add(1, 2) # 3
# Singleton class (eigenclass): where singleton methods live
class Calculator
class << self # opens the singleton class
def multiply(a, b) a * b end
def divide(a, b) a / b end
end
end
Calculator.multiply(2, 3) # 6
# Singleton pattern
class Logger
@instance = Logger.new
class << self
attr_reader :instance
end
end
Logger.instance.equal?(Logger.instance) # trueRefinement (범위 지정 몽키 패치)
Refinement(Ruby 2.1+)는 범위 지정 몽키 패칭을 허용합니다—기존 클래스에 메서드를 추가하지만 refinement를 명시적으로 'using'하는 곳에서만 적용. 이는 전역 몽키 패칭(다른 코드를 망가뜨릴 수 있음)보다 안전합니다. Refinement는 범위별(파일, 클래스, 메서드)로 활성화됩니다. 전역 namespace 오염 없이 편의 메서드를 추가하는 데 유용합니다. 성능과 일부 스코핑 특성으로 인해 있어야 할 만큼 흔하지 않지만, 코어 클래스를 확장하는 '올바른' 방법입니다.
# Refinement: limited monkey patching
module StringRefinements
refine String do
def shout
upcase + "!!!"
end
def palindrome?
downcase == downcase.reverse
end
end
end
# Without using: shout is undefined
# "hello".shout # NoMethodError
# With using: refinement is active in this scope
using StringRefinements
"hello".shout # "HELLO!!!"
"racecar".palindrome? # true
# Scoped to file or module
class App
using StringRefinements
def greet
"hi".shout # works here
end
end
# "hi".shout # NoMethodError (outside App)
# Safer than global monkey patching
# Refinements only affect code that opts in블록 Proc Lambda 심화
블록 기초
블록은 { }나 do...end를 사용하여 메서드에 전달되는 익명 클로저입니다. yield가 메서드 내에서 블록을 호출합니다. 블록은 |var|로 매개변수를 받을 수 있습니다. 메서드는 block_given?으로 블록이 전달되었는지 확인할 수 있습니다. 블록은 Ruby의 반복자 패턴과 DSL의 기초입니다. 둘러싼 스코프의 변수를 캡처합니다(클로저).
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n } # Single-line
[1, 2, 3].each do |n| # Multi-line
puts n
end
# yield invokes the block
def greet
print "Hello, "
yield # Calls the block
puts "!"
end
greet { print "World" } # Hello, World!
# yield with arguments
def calculate(a, b)
yield(a + b)
yield(a * b)
end
calculate(3, 4) { |result| puts "Result: #{result}" }Proc & Lambda
Proc과 lambda는 블록을 감싸는 객체로, 변수에 저장하고 전달할 수 있습니다. Proc은 관대한 인자 확인(추가 인자 무시)을 하고 둘러싼 메서드에서 반환합니다. Lambda는 엄격한 인자 확인을 하고 자신에게만 반환합니다. 유연성에는 Proc(블록을 받는 메서드 같은), 예측 가능한 동작의 익명 함수에는 lambda를 사용하세요. ->() {} 구문(stabby lambda)은 한 줄짜리에 간결합니다.
# Proc: object wrapping a block
my_proc = Proc.new { |x| puts x * 2 }
my_proc.call(5) # 10
my_proc.(5) # 10 (shorthand)
my_proc[5] # 10 (another shorthand)
# Lambda: stricter Proc
my_lambda = lambda { |x| puts x * 2 }
my_lambda = ->(x) { puts x * 2 } # Stabby lambda
# Difference 1: argument checking
my_proc.call(1, 2, 3) # OK (ignores extra args)
my_lambda.call(1, 2, 3) # ArgumentError (wrong number of args)
# Difference 2: return behavior
def proc_test
p = Proc.new { return 1 }
p.call
return 2 # Never reached
end
def lambda_test
l = lambda { return 1 }
l.call # Returns 1 to the lambda, not the method
return 2 # Reached
end메서드 객체
method() 메서드가 기존 메서드를 감싼 Method 객체를 반환합니다. & 연산자가 Method나 Proc을 블록으로(또는 그 반대로) 변환합니다. Symbol#to_proc이 :upcase를 { |x| x.upcase }로 변환하여 간결한 &:symbol 구문을 가능하게 합니다. 이것이 짧은 블록 작업에 관용적인 Ruby입니다. Method 객체는 수신자에 바인딩되어 있어, 전달될 때 self를 기억합니다.
# Convert method to Proc with method()
class Calculator
def add(a, b) a + b end
def multiply(a, b) a * b end
end
calc = Calculator.new
add_proc = calc.method(:add)
puts add_proc.call(2, 3) # 5
# Pass method as block with & operator
def apply_op(a, b, op)
op.call(a, b)
end
puts apply_op(2, 3, calc.method(:add)) # 5
puts apply_op(2, 3, calc.method(:multiply)) # 6
# Symbol#to_proc
words = ["hello", "world"]
upcased = words.map(&:upcase) # ["HELLO", "WORLD"]
# Equivalent to: words.map { |w| w.upcase }
# Common with &:map(&:to_i), select(&:positive?), sort_by(&:length)클로저 & 변수
클로저는 값이 아닌 참조로 변수를 캡처합니다. 여러 클로저가 상태를 공유할 수 있습니다(카운터 예제). 블록 로컬 변수(매개변수의 ; 뒤에 선언)가 외부 변수를 수정하지 않고 섀도잉합니다. 이를 통해 누산기, 제네레이터, memoization 같은 함수형 패턴이 가능합니다. 주의: 참조를 보유한 클로저가 해제되지 않으면 메모리 누수를 일으킬 수 있습니다. 이 패턴을 private 상태 캡슐화에 사용하세요.
# Blocks/Procs/Lambdas capture variables (closures)
counter = 0
increment = lambda { counter += 1 }
increment.call
increment.call
puts counter # 2
# Multiple closures sharing state
def make_counters
count = 0
[
lambda { count += 1 },
lambda { count },
lambda { count = 0 }
]
end
inc, get, reset = make_counters
inc.call; inc.call
puts get.call # 2
reset.call
puts get.call # 0
# Block-local variables (shadow outer)
x = 10
[1, 2, 3].each do |y; x| # x is block-local
x = y * 2
end
puts x # 10 (unchanged)커스텀 반복자
Enumerable을 include하고 each를 구현하면 클래스에 모든 반복자 메서드(map, select, reduce, sort 등)를 무료로 제공합니다. 블록을 &block으로 전달하고 호출하거나, yield를 사용하세요. & 연산자가 블록을 Proc으로 변환하고 되돌립니다. 이것이 커스텀 컬렉션을 반복 가능하게 만드는 관용적인 방법입니다. 전방향 반복에는 each를 구현하고; 양방향을 위해 reverse_each를 추가하세요.
class Tree
include Enumerable
def initialize(value, children = [])
@value = value
@children = children
end
def each(&block)
block.call(@value)
@children.each { |child| child.each(&block) }
end
end
tree = Tree.new(1, [
Tree.new(2, [Tree.new(4), Tree.new(5)]),
Tree.new(3)
])
tree.each { |v| puts v } # 1 2 4 5 3
puts tree.map { |v| v * 2 }.inspect # [2, 4, 8, 10, 6]
puts tree.select(&:even?).inspect # [2, 4]
puts tree.reduce(:+) # 15
# Including Enumerable gives you map, select, reduce, etc.메타프로그래밍
동적 메서드
method_missing이 정의되지 않은 메서드 호출을 가로채어 동적 디스패치를 가능하게 합니다. 항상 respond_to_missing?을 재정의하여 매치시키세요. define_method가 런타임에 메서드를 생성합니다. 유사한 메서드 생성(ActiveRecord의 find_by_* 같은)에 유용합니다. instance_variable_get/set이 인스턴스 변수를 이름으로 접근합니다. 메타프로그래밍을 신중하게 사용하세요—코드를 이해하고 디버그하기 어렵게 만듭니다. 가능하면 명시적 정의를 선호하세요.
class Person
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
# Method missing: catch undefined method calls
def method_missing(name, *args, &block)
if name.to_s =~ /^(.*)_with_prefix$/
attr_name = $1
"PREFIX_#{send(attr_name)}"
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s =~ /^(.*)_with_prefix$/ || super
end
end
p = Person.new("Alice", 30)
puts p.name_with_prefix # PREFIX_Alice
# Define methods dynamically
class Person
[:home, :work, :mobile].each do |type|
define_method("#{type}_phone") do
instance_variable_get("@#{type}_phone")
end
define_method("#{type}_phone=") do |value|
instance_variable_set("@#{type}_phone", value)
end
end
endOpen Class & 몽키 패칭
Ruby 클래스는 열려 있습니다: String 같은 빌트인을 포함한 모든 클래스에 메서드를 추가할 수 있습니다. 강력하지만 위험합니다(몽키 패칭이 다른 코드를 망가뜨릴 수 있음). Refinement(Ruby 2.1+)는 범위 지정 몽키 패치를 제공합니다: 모듈을 사용하는 파일/클래스 내에서만 적용. 더 안전한 메타프로그래밍을 위해 전역 몽키 패치보다 refinement를 선호하세요. 패치를 명확히 문서화하고 코어 동작 변경을 피하세요.
# Reopen existing classes (monkey patching)
class String
def shout
upcase + "!"
end
def word_count
split.size
end
end
puts "hello world".shout # HELLO WORLD!
puts "one two three".word_count # 3
# Refinements: scoped monkey patches
module ShoutRefinement
refine String do
def shout
upcase + "!"
end
end
end
class MyClass
using ShoutRefinement
def greet(name)
"hello #{name}".shout # Works here
end
end
# "test".shout # NoMethodError (refinement not active)훅 & 콜백
Ruby는 수명 주기 훅을 제공합니다: inherited(서브클래스 생성), included(모듈 include), prepended(모듈 prepend), method_added(메서드 정의), method_removed, method_undefined. 이를 통해 프레임워크가 클래스 변경에 자동으로 반응할 수 있습니다. ActiveRecord가 속성 추적에 사용하고, Rails가 라우팅에 사용합니다. 클래스 수준 이벤트에는 클래스 메서드(self.inherited)로, 메서드 이벤트에는 인스턴스 메서드로 훅을 재정의하세요.
class Observable
# Hook: called when class is subclassed
def self.inherited(subclass)
puts "#{subclass} inherits from #{self}"
end
# Hook: called when a module is included
def self.included(base)
puts "#{self} included in #{base}"
end
# Hook: called when a method is added
def method_added(name)
puts "Added method: #{name}"
end
# Hook: called when method is undefined
def method_removed(name)
puts "Removed method: #{name}"
end
end
class Child < Observable
def my_method; end
end
# Output:
# Child inherits from Observable
# Added method: my_method
# Other hooks: method_undefined, extended, prependedeval & Binding
eval이 문자열을 Ruby 코드로 실행합니다. 입력이 신뢰할 수 없으면 위험합니다(코드 주입). binding이 현재 실행 컨텍스트(변수, self)를 나중 eval을 위해 캡처합니다. class_eval이 코드를 클래스 컨텍스트에서 실행(메서드 정의). instance_eval이 self를 수신자로 변경합니다. DSL과 코드 생성에 사용하지만, 사용자 입력에는 eval을 피하세요. 동적 코드에는 블록과 define_method를 선호하세요. eval이 불가피한 경우 항상 입력을 살균하세요.
# eval: execute a string as Ruby code
result = eval("1 + 2 * 3")
puts result # 7
# eval with binding (context)
def eval_in_context(code)
x = 10
binding # Returns a Binding object capturing local variables
end
b = eval_in_context("binding")
puts eval("x * 2", b) # 20
# class_eval: execute code in class context
String.class_eval do
def palindrome?
self == reverse
end
end
puts "racecar".palindrome? # true
# instance_eval: execute in instance context
"hello".instance_eval do
puts length # 5 (self is the string)
end
# NEVER eval user input (security risk)리플렉션 & 인트로스펙션
Ruby는 풍부한 리플렉션을 제공합니다: methods가 모든 메서드 나열, instance_variables가 인스턴스 변수 나열, ancestors가 상속 체인 표시. parameters가 메서드의 매개변수 이름과 타입을 드러냅니다. respond_to?가 객체가 메서드에 응답하는지 확인합니다. 디버깅, 직렬화(속성 검사), 일반 도구(ORM 같은) 빌드에 리플렉션을 사용하세요. 리플렉션을 과용하지 마세요—타입 안전성을 우회하고 코드를 따르기 어렵게 만듭니다.
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
def save; end
def valid?; true; end
end
u = User.new("Alice", "[email protected]")
# Inspect methods
puts u.methods - Object.methods # [:name, :email, :save, :valid?, ...]
puts u.public_methods(false) # Only this class's methods
puts User.instance_methods(false) # [:save, :valid?, ...]
# Inspect variables
puts u.instance_variables.inspect # [:@name, :@email]
puts u.instance_variable_get(:@name) # Alice
# Inspect class
puts User.ancestors.inspect
puts User.instance_method(:save).parameters
# Check if responds to method
puts u.respond_to?(:save) # true
puts u.is_a?(User) # true테스트 (RSpec/Minitest)
RSpec 기초
RSpec은 BDD 스타일 테스트 프레임워크입니다. describe가 관련 테스트를 그룹화, context가 조건별로 그룹화, it가 단일 테스트를 정의. let이 지연 memoized 변수를 생성. expect(...).to matcher가 어설션 구문입니다. 흔한 matcher: eq, be_valid, include, raise_error. subject + is_expected가 보일러플레이트를 줄입니다. rspec(전체)이나 rspec path/to/spec(특정)으로 실행. 테스트 데이터에는 fixture 대신 factory(FactoryBot)를 사용하세요.
# spec/spec_helper.rb
require 'rspec'
require_relative '../lib/my_app'
# spec/models/user_spec.rb
require 'spec_helper'
RSpec.describe User do
# Setup with let (lazy, memoized)
let(:user) { User.new(name: 'Alice', email: '[email protected]') }
describe '#name' do
it 'returns the name' do
expect(user.name).to eq('Alice')
end
end
describe '#valid?' do
context 'with valid attributes' do
it 'is valid' do
expect(user).to be_valid
end
end
context 'without name' do
let(:user) { User.new(email: '[email protected]') }
it 'is invalid' do
expect(user).not_to be_valid
expect(user.errors[:name]).to include("can't be blank")
end
end
end
# Multiple examples
describe '#age' do
subject { user.age }
it { is_expected.to be >= 0 }
end
end
# Run: rspec spec/models/user_spec.rb
# Run all: rspecRSpec 목 & 스텁
스텁(allow)이 메서드 반환값을 교체; 목(expect)이 메서드가 호출되었는지 확인. double이 테스트 더블(가짜 객체) 생성. and_return이 반환값 설정, and_raise가 오류 시뮬레이션, with가 예상 인자 설정. 목은 신중하게 사용—과도한 목킹은 테스트를 취약하게 만듭니다. 구현이 아닌 동작을 테스트하세요. 가능할 때 실제 객체를 선호; 외부 서비스(API, 이메일, 결제 게이트웨이)에는 목을 사용하세요.
RSpec.describe PaymentProcessor do
let(:gateway) { double('PaymentGateway') }
let(:processor) { PaymentProcessor.new(gateway) }
describe '#charge' do
it 'charges the gateway' do
# Stub: replace method return value
allow(gateway).to receive(:charge).and_return(success: true)
result = processor.charge(100)
expect(result[:success]).to be true
end
it 'raises on gateway error' do
allow(gateway).to receive(:charge)
.and_raise(GatewayError, 'Network failure')
expect { processor.charge(100) }
.to raise_error(GatewayError, /Network/)
end
it 'verifies the gateway was called' do
# Mock: expect method to be called
expect(gateway).to receive(:charge).with(100)
processor.charge(100)
end
it 'receives multiple calls' do
allow(gateway).to receive(:charge).and_return(true, false, true)
expect(gateway.charge(1)).to be true
expect(gateway.charge(2)).to be false
end
end
endMinitest
Minitest는 Ruby의 기본 테스트 라이브러리입니다(Ruby에 포함). Unit 스타일(assert/refute)과 Spec 스타일(must_equal) 구문을 모두 지원합니다. setup이 각 테스트 전에 실행. 어설션: assert_equal, assert_nil, assert_raises, assert_includes. Minitest는 RSpec보다 빠르고 의존성이 적습니다. 목킹에는 Minitest::Mock이나 더 많은 기능을 위해 Mocha gem을 사용. Rails는 기본적으로 Minitest를 사용(ActiveSupport::TestCase).
# test/test_helper.rb
require 'minitest/autorun'
require_relative '../lib/my_app'
# test/models/user_test.rb
require 'test_helper'
class UserTest < Minitest::Test
def setup
@user = User.new(name: 'Alice', email: '[email protected]')
end
def test_name
assert_equal 'Alice', @user.name
end
def test_valid_with_attributes
assert @user.valid?
end
def test_invalid_without_name
@user.name = nil
refute @user.valid?
assert_includes @user.errors[:name], "can't be blank"
end
def test_raises_on_error
assert_raises(ArgumentError) { User.new! }
end
end
# Spec-style Minitest
class UserSpec < Minitest::Spec
let(:user) { User.new(name: 'Alice') }
it 'has a name' do
_(user.name).must_equal 'Alice'
end
end
# Run: rake test
# Run specific: ruby -Itest test/models/user_test.rb테스트 데이터 & Factory
FactoryBot이 기본값, 변형용 trait, 연관으로 테스트 객체를 생성. create는 DB에 저장; build는 그렇지 않음. Trait은 조합 가능(factory :admin_with_posts, traits: [:admin, :with_posts]). Factory가 fixture보다 유연하지만 느림. Fixture(YAML)는 더 빠르지만 유연성이 떨어짐. 필요에 따라 선택: 단순 데이터는 fixture, 복잡한 관계는 factory. 너무 많은 trait을 가진 factory는 피하세요—집중되게 유지하세요.
# FactoryBot (gem 'factory_bot')
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { 'Alice' }
email { '[email protected]' }
age { 30 }
trait :admin do
role { 'admin' }
end
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
factory :admin_user, traits: [:admin]
factory :user_with_posts, traits: [:with_posts]
end
end
# Usage in specs
let(:user) { create(:user) } # Saved to DB
let(:admin) { create(:admin_user) }
let(:user_with_posts) { create(:user_with_posts) }
let(:unsaved_user) { build(:user) } # Not saved
# Fixtures (alternative)
# test/fixtures/users.yml
# alice:
# name: Alice
# email: [email protected]
# In tests: users(:alice)통합 & 시스템 테스트
Request spec이 HTTP를 통해 전체 스택(라우팅, 컨트롤러, 모델)을 테스트. System spec(Capybara)이 실제 브라우저를 구동하여 JavaScript 상호작용을 테스트. API 엔드포인트에는 request spec, 사용자 흐름에는 system spec을 사용하세요. have_http_status가 응답 코드 확인. visit/fill_in/click_button이 사용자 액션 시뮬레이션. System 테스트는 느리지만 통합 버그를 잡습니다. JavaScript 기반 페이지에는 js: true로 system 테스트를 실행하세요.
# spec/requests/users_spec.rb (integration)
require 'spec_helper'
RSpec.describe 'Users API', type: :request do
describe 'GET /users' do
before { create_list(:user, 3) }
it 'returns all users' do
get '/users', as: :json
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).size).to eq(3)
end
end
describe 'POST /users' do
it 'creates a user' do
post '/users', params: { user: { name: 'Bob', email: '[email protected]' } }
expect(response).to have_http_status(201)
expect(User.last.name).to eq('Bob')
end
end
end
# System tests (browser automation)
# spec/system/login_spec.rb
require 'spec_helper'
RSpec.describe 'Login', type: :system do
it 'logs in a user' do
user = create(:user, password: 'password123')
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'password123'
click_button 'Log In'
expect(page).to have_content('Welcome')
end
end스레드 & Fiber
스레드 기초
Ruby(MRI)의 스레드는 VM이 스케줄링하는 그린 스레드입니다—GIL(Global Interpreter Lock)로 인해 진정한 병렬로 실행되지 않습니다. 하지만 I/O 동시성(네트워크, 파일 작업)에는 유용합니다. join이 스레드 완료를 대기. value가 반환값 검색. CPU 바운드 병렬성을 위해 다중 프로세스(fork, Sidekiq)나 JRuby(GIL 없음)를 사용하세요. 메인 스레드 종료 시 스레드가 죽는 것을 피하기 위해 항상 join하세요.
# Create and run threads
threads = (1..3).map do |i|
Thread.new(i) do |n|
puts "Thread #{n} started"
sleep(1)
puts "Thread #{n} finished"
end
end
# Wait for all threads
threads.each(&:join)
puts "All done"
# Thread with return value
thread = Thread.new { 1 + 2 }
result = thread.value # 3 (waits for completion)
# Thread status
thread = Thread.new { sleep(1) }
puts thread.status # 'run', 'sleep', false (finished), nil (error)
thread.join
# Current thread
Thread.current
Thread.list # All threads
Thread.main # Main thread스레드 동기화
Mutex.synchronize가 한 번에 하나의 스레드만 블록을 실행하여 경쟁 조건을 방지합니다. Queue는 스레드 안전한 FIFO—생산자가 push, 소비자가 pop(비어 있으면 블록). ConditionVariable이 스레드를 조정: wait가 mutex를 해제하고 대기, signal/broadcast가 대기 스레드를 깨움. 공유 가변 상태에는 항상 동기화를 사용하세요. 스레드가 서로를 기다릴 때 교착 상태 발생—일관된 순서로 락을 획득하세요.
# Mutex: mutual exclusion
counter = 0
mutex = Mutex.new
threads = 10.times.map do
Thread.new do
1000.times do
mutex.synchronize { counter += 1 }
end
end
end
threads.each(&:join)
puts counter # 10000 (without mutex, would be less)
# Queue: thread-safe FIFO
require 'thread'
queue = Queue.new
producer = Thread.new do
5.times { |i| queue << "item #{i}" }
queue << :done
end
consumer = Thread.new do
loop do
item = queue.pop
break if item == :done
puts "Processed: #{item}"
end
end
[producer, consumer].each(&:join)
# ConditionVariable: signal between threads
mutex = Mutex.new
cv = ConditionVariable.newFiber
Fiber는 협력적 가벼운 스레드입니다: 선점 대신 수동으로 제어를 양보합니다. resume이 fiber 시작/재개; Fiber.yield가 일시 중지하고 값을 반환. Fiber는 제네레이터, 지연 평가, 상태 기계 파싱에 유용합니다. 스레드와 달리 한 번에 하나의 fiber만 실행되어 동기화가 필요 없습니다. Fiber는 스레드보다 저렴하지만 멀티 코어를 사용할 수 없습니다. I/O 멀티플렉싱(EventMachine, Async)에 사용하세요.
# Fiber: cooperative concurrency (manual scheduling)
fiber = Fiber.new do
puts "Fiber started"
Fiber.yield "first yield"
puts "Fiber resumed"
Fiber.yield "second yield"
puts "Fiber ending"
"fiber done"
end
puts fiber.resume # "Fiber started", returns "first yield"
puts fiber.resume # "Fiber resumed", returns "second yield"
puts fiber.resume # "Fiber ending", returns "fiber done"
# Generator pattern with Fiber
def fibonacci
Fiber.new do
a, b = 0, 1
loop do
Fiber.yield a
a, b = b, a + b
end
end
end
fib = fibonacci
10.times { print fib.resume, " " }
# 0 1 1 2 3 5 8 13 21 34Async & 동시성 Ruby
async gem이 Fiber를 기반으로 현대 비동기 I/O를 제공하여 고동시성 네트워크 코드를 가능하게 합니다. concurrent-ruby가 스레드 안전 추상을 제공: Future(비동기 결과), Promise(체인 가능), 스레드 풀, 원자적 변수. I/O 바운드 작업(HTTP, 데이터베이스)에는 async, CPU 바운드 작업에는 스레드 풀을 사용하세요. 멈춤을 방지하기 위해 항상 풀을 종료하세요. 이 gem들이 MRI의 GIL을 우회하여 실질적 동시성을 제공합니다.
# Async gem for modern async I/O
# gem install async
require 'async'
require 'async/http/internet'
Async do
internet = Async::HTTP::Internet.new
# Run requests concurrently
tasks = 3.times.map do |i|
Async do
response = internet.get("https://httpbin.org/delay/#{i}")
puts "Request #{i}: #{response.status}"
end
end
tasks.each(&:wait)
end
# Concurrent Ruby (gem 'concurrent-ruby')
require 'concurrent'
# Future: async computation
future = Concurrent::Future.execute { sleep(1); 42 }
puts future.value # 42 (blocks until ready)
# Promise chain
Concurrent::Promise.execute { 1 }
.then { |v| v + 1 }
.then { |v| v * 2 }
.then { |v| puts v } # 4
# Thread pool
pool = Concurrent::FixedThreadPool.new(4)
10.times do |i|
pool.post { puts "Task #{i} on #{Thread.current.object_id}" }
end
pool.shutdownRactor (Ruby 3.0+)
Ractor(Ruby 3.0+)가 GIL을 피해 진정한 병렬성을 제공합니다. 각 Ractor는 자체 힙을 가지므로 데이터 경쟁이 없습니다. 통신은 메시지(send/receive, take)를 통해. 전송되는 객체는 격리를 유지하기 위해 복사(또는 Ractor.move로 이동)됩니다. Frozen 객체는 공유 가능. Ractor가 병렬 Ruby의 미래이지만 제한이 있습니다: 대부분의 gem이 아직 Ractor 안전하지 않습니다. 격리가 허용되는 CPU 바운드 병렬 계산에 사용하세요.
# Ractor: true parallelism (no GIL)
# Ruby 3.0+ feature for thread-safe parallel execution
ractor = Ractor.new do
value = receive # Receive from main
value * 2
end
ractor.send(21)
puts ractor.take # 42
# Parallel computation
ractors = (1..4).map do |i|
Ractor.new(i) do |n|
sleep(1)
n * n
end
end
results = Ractor.select(*ractors)
# Or: ractors.map(&:take) # [1, 4, 9, 16]
# Ractor restrictions:
# - Cannot share mutable objects
# - Communication only via messages
# - Copy or move semantics for sending
# Shared frozen objects are OK
SHARED = Ractor.new { [1, 2, 3].freeze }
# Multiple ractors can read SHARED safely인코딩 & IO
문자열 인코딩
모든 Ruby 문자열은 인코딩을 가집니다(기본 UTF-8). bytesize가 바이트 수; length가 문자 수(멀티바이트 인코딩에서 다름). force_encoding이 바이트 변환 없이 인코딩 태그 변경(실제 인코딩을 알 때 사용). encode가 인코딩 간 변환. 외부 데이터 처리 전에 항상 valid_encoding?을 확인하세요. 매직 코멘트 # encoding: utf-8로 소스 파일 인코딩 설정(Ruby 2.0+에서 UTF-8이 기본이지만).
# Ruby strings have encodings
s = "hello"
puts s.encoding # <Encoding:UTF-8>
puts s.bytesize # 5
puts s.length # 5
# Multibyte characters
japanese = "こんにちは"
puts japanese.encoding # UTF-8
puts japanese.bytesize # 15 (3 bytes per char)
puts japanese.length # 5 (5 characters)
# Force encoding (interpret bytes differently)
bytes = "caf\u00e9".force_encoding('ASCII-8BIT')
puts bytes.encoding # ASCII-8BIT
# Encode (convert to different encoding)
utf8 = "café"
latin1 = utf8.encode('ISO-8859-1')
puts latin1.encoding # ISO-8859-1
# Invalid bytes
bad = "\xFF\xFE".force_encoding('UTF-8')
puts bad.valid_encoding? # false
fixed = bad.encode('UTF-8', invalid: :replace, replace: '?')파일 IO
File.read가 전체 파일을 메모리에 로드; File.foreach가 한 줄씩 읽기(큰 파일에 메모리 효율적). File.write는 덮어쓰기; mode 'a'는 추가; 'r+'는 읽기와 쓰기. 바이너리 모드('rb', 'wb')는 인코딩 변환 방지. 파일이 닫힘을 보장하기 위해 항상 블록 형태(File.open)를 사용하세요. FileUtils가 더 높은 수준의 작업(cp, mv, mkdir_p) 제공. 읽기 전에 File.exist?로 존재를 확인하세요.
# Read entire file
content = File.read('data.txt')
# Read line by line (memory efficient)
File.foreach('large.log') do |line|
puts line.chomp
end
# Write to file
File.write('output.txt', 'Hello, World!')
# Append
File.open('log.txt', 'a') do |f|
f.puts "New log entry"
end
# Read/Write modes
File.open('file.txt', 'r+') do |f|
content = f.read
f.rewind
f.write("Updated: " + content)
end
# Binary mode
File.open('image.png', 'rb') do |f|
bytes = f.read
puts bytes.bytesize
end
# File operations
File.exist?('file.txt')
File.size('file.txt')
File.mtime('file.txt')
File.delete('file.txt')
File.rename('old.txt', 'new.txt')
FileUtils.cp('a.txt', 'b.txt') # require 'fileutils'StringIO & Tempfile
StringIO가 문자열을 IO 같은 인터페이스로 감싸서, 디스크를 건드리지 않고 파일 작업을 테스트하는 데 유용합니다. Tempfile이 블록 종료 시(Tempfile.create)나 unlink 시(Tempfile.new) 자동 삭제되는 임시 파일을 생성. 메모리에 맞지 않는 큰 데이터나 경로로 외부 프로그램에 전달할 때 Tempfile을 사용하세요. ensure 블록으로 정리를 항상 보장. StringIO가 IO 코드 단위 테스트에 좋습니다.
require 'stringio'
require 'tempfile'
# StringIO: in-memory file-like object
io = StringIO.new
io.puts "Hello"
io.puts "World"
io.rewind
puts io.read # "Hello\nWorld\n"
# Use like a file
io = StringIO.new("line1\nline2\nline3")
io.each_line { |line| puts line.chomp }
# Tempfile: auto-deleted temporary file
Tempfile.create('myapp') do |f|
f.write('temporary data')
f.rewind
puts f.read
end # File automatically deleted
# Tempfile with explicit cleanup
temp = Tempfile.new('myapp')
begin
temp.write('data')
temp.rewind
# Use temp.path to pass to external programs
ensure
temp.close
temp.unlink
end네트워크 IO
TCPSocket/TCPServer가 저수준 TCP 접근 제공. Net::HTTP가 표준 HTTP 클라이언트(빌트인). 복잡한 HTTP 요구사항(세션, 쿠키, 재시도)에는 httparty나 faraday gem을 사용하세요. HTTPS의 경우 항상 use_ssl = true를 설정하세요. 고성능 HTTP의 경우 async-http나 typhoeus를 고려하세요. URI가 URL을 안전하게 파싱. 견고한 네트워크 코드를 위해 Net::ReadTimeout과 Errno::ECONNREFUSED를 처리하세요.
require 'socket'
require 'net/http'
require 'uri'
# TCP Client
TCPSocket.open('example.com', 80) do |socket|
socket.write("GET / HTTP/1.0\r\n\r\n")
puts socket.read
end
# TCP Server
server = TCPServer.new(2000)
loop do
client = server.accept
client.puts "Hello from server"
client.close
end
# HTTP Client
uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)
puts response.code # 200
puts response.body
# HTTP POST
response = Net::HTTP.post_form(uri, key: 'value')
# HTTP with custom headers
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer token'
response = http.request(request)STDIO & 파이프라인
$stdout, $stderr, $stdin은 전역 IO 객체입니다(원본에는 상수 STDOUT/STDERR/STDIN 사용). 전역을 재할당하여 리다이렉트. StringIO가 테스트용 출력 캡처. Open3.popen3이 서브프로세스의 stdin/stdout/stderr를 완전히 제어. Open3.pipeline이 명령을 쉘 파이프처럼 체인. Capture3이 stdout, stderr, status 반환. 서브프로세스에 EOF를 알리기 위해 항상 stdin을 닫으세요. 종료 상태를 얻으려면 wait.value를 사용하세요.
# Standard IO
$stdout.puts "To stdout"
$stderr.puts "To stderr"
input = $stdin.gets # Read from stdin
# Redirect IO
$stdout = File.open('log.txt', 'w')
puts "This goes to file"
$stdout = $stdout # Restore (or use STDOUT constant)
# Capture output
captured = StringIO.new
original = $stdout
$stdout = captured
puts "Captured"
$stdout = original
puts captured.string # "Captured\n"
# Open3: run commands with full IO control
require 'open3'
Open3.popen3('grep hello') do |stdin, stdout, stderr, wait|
stdin.puts "hello world"
stdin.puts "goodbye"
stdin.close
puts stdout.read # "hello world"
puts wait.value.exitstatus
end
# Pipeline
output = Open3.pipeline('ls', 'grep .rb', 'wc -l')패턴 매칭 (3.0+)
기본 패턴 매칭
패턴 매칭(in 키워드)이 배열과 해시를 분해하여 변수에 바인딩합니다. => 가 매치된 값을 변수에 바인딩. *rest가 나머지 배열 요소를 캡처. 해시 매칭은 부분적: 추가 키는 무시됩니다. | 가 여러 패턴 매치. case/in이 기본 형태; 한 줄 형태는 expression in pattern. 패턴 매칭은 구조화된 데이터(JSON, AST) 파싱과 복잡한 if-else 체인 대체에 특히 강력합니다.
# in: pattern matching (Ruby 2.7+, stable in 3.0)
case [1, 2, 3]
in [1, *rest]
puts "Starts with 1, rest: #{rest}"
end
case {name: 'Alice', age: 30}
in {name: String => name, age: Integer => age}
puts "#{name} is #{age}"
end
# Multiple patterns with |
case status
in :success | :ok
puts "Good"
else
puts "Other"
end
# Array patterns
case [1, 2, 3, 4]
in [_, _, *rest]
puts "Rest: #{rest}" # [3, 4]
end
# Hash patterns (partial match by default)
case {a: 1, b: 2, c: 3}
in {a: Integer}
puts "Has a" # Matches (ignores b, c)
end변수 바인딩 & 가드
=> 가 매치된 값을 변수에 바인딩. pin 연산자(^var)가 변수의 현재 값에 매칭(바인딩 아님). 가드(if/unless)가 조건 추가. 배열 패턴은 임의 위치의 * 스플랫을 지원. Find 패턴 [*, target, *]이 배열 내 요소 검색. 패턴 매칭은 복잡한 데이터 추출에 선언적이고 간결합니다. 패턴에서 바인딩된 변수는 case 문 이후에 사용 가능합니다.
user = {name: 'Alice', age: 30, role: :admin}
case user
in {name: String => name, age: Integer => age, role: :admin} if age > 18
puts "#{name} is an adult admin"
end
# Pin operator (^): match against variable value
expected = 'Alice'
case user
in {name: ^expected}
puts "Matched expected name"
end
# Array destructuring with binding
case [1, 2, 3]
in [first, *middle, last]
puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end
# Find pattern (search within array)
case [1, 2, 3, 4, 5]
in [*, 3 => three, *]
puts "Found 3: #{three}"
end클래스 & 타입 패턴
클래스 패턴이 타입별로 매칭. 커스텀 클래스는 deconstruct(배열 패턴)와 deconstruct_keys(해시 패턴)를 구현하여 분해를 지원합니다. Class[args] 패턴은 deconstruct 사용; Class(key:)는 deconstruct_keys 사용. 많은 빌트인 클래스(Time, Date, MatchData)가 분해를 지원합니다. 이를 통해 도메인 객체에 대한 표현력 있는 매칭이 가능합니다. 클래스를 패턴 매칭 가능하게 하려면 이 메서드들을 구현하세요.
# Match by class
case value
in Integer
puts "Integer"
in String
puts "String"
in Array
puts "Array"
end
# Class with destructuring
class Point
attr_reader :x, :y
def initialize(x, y) @x, @y = x, y end
def deconstruct = [@x, @y] # Array pattern
def deconstruct_keys(keys) = {x: @x, y: @y} # Hash pattern
end
p = Point.new(1, 2)
case p
in Point[x, y]
puts "Point at (#{x}, #{y})"
end
case p
in Point(x:, y:)
puts "X: #{x}, Y: #{y}"
end
# Built-in classes support destructuring
case Time.now
in Time(hour: h) if h < 12
puts "Morning"
end한 줄 패턴 매칭
한 줄 형태(expr in pattern)는 NoMatchingPatternError를 발생시키지 않고 true/false를 반환. 성공적 매치 시 변수를 바인딩. 가드 조건과 조건부 추출에 유용. 오른쪽 형태(pattern => var)가 전체 매치를 바인딩. 한 줄 매칭은 간단한 경우에 간결; 복잡한 다중 분기 로직에는 case/in을 사용하세요. 실패한 매치에서 바인딩된 변수는 nil로 남습니다.
# One-line form (Ruby 3.0+)
{status: :ok, data: 42} in {status: :ok, data: Integer => data}
puts data # 42
# Returns true/false (does not raise)
[1, 2, 3] in [Integer, Integer, Integer] # true
[1, 2, 3] in [String, *, *] # false
# Useful for conditionals
if user in {role: :admin}
puts "Admin access"
end
# Guard clauses
def process(data)
return unless data in {type: String, value: Integer}
# ...
end
# Extracting from JSON
json = JSON.parse('{"user":{"name":"Alice","age":30}}')
json in {"user" => {"name" => String => name, "age" => Integer => age}}
puts "#{name}, #{age}" # Alice, 30실용적 사용 사례
패턴 매칭은 다음에 뛰어납니다: 구조화된 데이터(JSON, XML) 파싱, 상태 기계, AST 순회, 데이터 형태에 따른 디스패치. 선언적 패턴으로 장황한 if-else 체인을 대체. else 절이 예상치 못한 형식을 처리. 복잡한 조건에는 가드와 결합. 패턴 매칭은 복잡한 데이터 처리를 더 읽기 쉽고 유지보수하기 쉽게 만듭니다. Ruby 3의 가장 강력한 기능 중 하나로 깔끔하고 표현적인 코드를 제공합니다.
# 1. Parse JSON API responses
def handle_response(response)
case JSON.parse(response)
in {"status" => "success", "data" => Array => items}
items.each { |item| process(item) }
in {"status" => "error", "message" => String => msg}
raise "API error: #{msg}"
in {"status" => "error", "code" => Integer => code} if code >= 500
retry_request
else
raise "Unknown response format"
end
end
# 2. State machines
case state
in :idle, event: :start
transition_to :running
in :running, event: :pause
transition_to :paused
in :paused, event: :resume
transition_to :running
in :running | :paused, event: :stop
transition_to :idle
end
# 3. AST traversal
def evaluate(node)
case node
in {type: 'number', value: n}
n
in {type: 'add', left:, right:}
evaluate(left) + evaluate(right)
in {type: 'mul', left:, right:}
evaluate(left) * evaluate(right)
end
end메타프로그래밍
send & define_method
define_method가 동적으로 메서드를 생성. send가 이름으로 메서드 호출(private도). public_send는 가시성 존중. DSL과 보일러플레이트 감소에 유용. 보안 문제를 피하기 위해 사용자 입력에 주의. 메타프로그래밍은 강력하지만 코드를 이해하기 어렵게 만들 수 있습니다.
class Foo
define_method(:greet) do |name|
"Hello, #{name}"
end
end
Foo.new.greet("Alice") # "Hello, Alice"
# Dynamic method call
Foo.new.send(:greet, "Bob") # "Hello, Bob"
Foo.new.public_send(:greet, "Bob") # Respects visibilitymethod_missing
method_missing이 정의되지 않은 메서드 호출을 가로챕니다. 동적 디스패치와 DSL에 유용. 항상 respond_to_missing?을 재정의하여 매치시키세요. 느리고 버그를 숨길 수 있습니다. 메서드 집합을 알고 있을 때는 define_method를 선호. ActiveRecord가 속성 접근자에 사용.
class DynamicHash
def method_missing(name, *args)
if name.to_s.end_with?('=')
self[name.to_s.chomp('=')] = args.first
else
self[name.to_s]
end
end
def respond_to_missing?(name, include_private = false)
true
end
endeval
eval이 문자열을 Ruby 코드로 실행. 매우 강력하지만 위험. 신뢰할 수 없는 입력은 절대 eval하지 마세요(코드 주입). 특정 컨텍스트에는 binding.eval을 사용. 안전한 평가를 위해 Ripper 같은 파서나 샌드박스 사용. 대부분의 사용 사례에 더 안전한 대안이 있습니다.
# Execute a string as Ruby code
result = eval("1 + 2 * 3") # 7
# Dynamic method definition
eval("def dynamic_method; 42; end")
dynamic_method # 42
# WARNING: never eval untrusted input!Open Class
Ruby 클래스는 열려 있습니다: 빌트인을 포함한 모든 클래스에 메서드를 추가할 수 있습니다. 이것을 몽키 패칭이라고 합니다. 빠른 수정에 강력하지만 충돌과 혼란을 일 으킬 수 있습니다. 범위 지정 수정에는 refinement를 사용. 변경을 명확히 문서화. 몽키 패칭보다 조합을 선호하세요.
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflicts클래스 매크로
클래스 매크로는 다른 메서드를 정의하는 클래스 메서드입니다. has_many, belongs_to, attr_accessor가 예. 내부적으로 define_method를 사용. Rails가 동적 메서드를 생성하는 방법. RSpec과 Sinatra 같은 DSL이 이 패턴을 사용. 코드를 선언적이고 읽기 쉽게 만듭니다.
class ActiveRecord::Base
def self.has_many(name)
define_method(name) { [] }
end
end
class Post < ActiveRecord::Base
has_many :comments
end
Post.new.comments # []관련 Ruby 스니펫
Copy-paste ready code for common tasks.
Blocks, Procs, Lambdas
Use blocks with yield, Procs, lambdas, and the & operator.
Classes and Modules
Define classes with inheritance, mix in modules, and add class methods.
Iterators
Use each, map, select, reduce, group_by, and lazy enumerators.
Strings
Interpolate, trim, split, replace, and pattern-match strings.
Hashes
Build, default, transform, merge, and group with Hash.
Metaprogramming
Define methods dynamically, intercept with method_missing, and build DSLs.
Error Handling
Raise and rescue typed exceptions with ensure and retry.
File I/O
Read, write, append, traverse directories, and process CSV files.
Was this helpful?