RUBY

Ruby 기본 문법 완전 정복: 초보자를 위한 A to Z 실전 해설

이 글은 Ruby를 한 번도 사용해 본 적이 없는 초보자를 위해 기본 문법 전체를 예제 중심으로 아주 자세하게 설명한 실전 가이드입니다. 한 줄씩 따라 치면서 읽으면 글을 다 읽을 때쯤 Ruby로 간단한 프로그램을 직접 만들 수 있는 수준이 됩니다.

Ruby란 어떤 언어인가?

Ruby는 1995년 일본의 마츠모토 유키히로(Matz)가 만든 프로그래밍 언어입니다. 핵심 철학은 딱 한 가지, "개발자의 행복(Developer Happiness)"입니다. 다른 언어에서 복잡하게 써야 하는 것들을 자연스러운 문장처럼 쓸 수 있도록 설계되어 있습니다.

  • 동적 타입: 변수 타입을 미리 선언하지 않아도 됩니다.
  • 모든 것이 객체: 숫자, 문자열, 심지어 nil도 모두 객체입니다.
  • 표현력이 뛰어난 문법: 코드가 영어 문장처럼 읽힙니다.
  • Rails 생태계: 웹 프레임워크 Ruby on Rails로 유명합니다.

1. Ruby 설치와 첫 실행

터미널(맥/리눅스) 또는 명령 프롬프트(윈도우)에서 아래 명령으로 버전을 확인합니다.

ruby -v
# ruby 3.3.0 (2023-12-25 revision ...) ...

설치가 안 되어 있다면 rbenv 또는 공식 설치 패키지(ruby-lang.org)를 사용하세요. 설치 후 첫 번째 파일을 만들어 실행해봅니다.

# hello.rb
puts "Hello, Ruby!"    # Hello, Ruby! 출력
ruby hello.rb

출력 메서드를 비교해보면 각각 다른 용도로 쓰입니다.

puts "사과"       # "사과" 출력 후 줄바꿈
print "바나나"    # "바나나" 출력, 줄바꿈 없음
p 42              # 42  (Integer 타입 그대로 — 디버깅용)
p "hello"         # "hello"  (따옴표 포함)
p [1, 2, 3]       # [1, 2, 3]
p nil             # nil

puts는 배열을 받으면 요소마다 줄을 바꿔 출력합니다. p는 개발 중 값을 확인할 때 씁니다. print는 줄바꿈 없이 이어붙일 때 씁니다.

2. 주석

주석은 코드 실행에 영향을 주지 않는 메모입니다. Ruby에서는 #으로 시작합니다.

# 한 줄 주석입니다
puts "실행됩니다"  # 여기서부터 주석

=begin
여러 줄 주석은
=begin ... =end 로 작성합니다
보통 문서화에 사용합니다
=end

3. 변수와 상수

Ruby에서 변수는 값을 저장하는 상자입니다. 이름 앞에 붙는 기호로 종류가 구분됩니다.

3-1. 지역 변수 (Local Variable)

소문자나 밑줄(_)로 시작하며, 현재 스코프(메서드, 블록 등)에서만 유효합니다.

name    = "Alice"
age     = 28
_temp   = 0
user_id = 1001

puts name    # Alice
puts age     # 28

3-2. 인스턴스 변수 (Instance Variable)

@로 시작하며, 클래스의 각 인스턴스(객체)마다 독립적으로 존재합니다. 초기화 전까지는 nil입니다.

class Dog
  def set_name(n)
    @name = n          # 인스턴스 변수
  end

  def bark
    puts "#{@name}이(가) 짖습니다!"
  end
end

d = Dog.new
d.set_name("초코")
d.bark   # 초코이(가) 짖습니다!

3-3. 클래스 변수 (Class Variable)

@@로 시작하며, 클래스 자신과 모든 인스턴스가 공유합니다.

class Counter
  @@total = 0        # 모든 인스턴스가 공유

  def initialize
    @@total += 1
  end

  def self.total
    @@total
  end
end

Counter.new
Counter.new
Counter.new
puts Counter.total   # 3

3-4. 전역 변수 (Global Variable)

$로 시작하며, 프로그램 어디서든 접근 가능합니다. 남용하면 코드를 추적하기 어려우므로 꼭 필요할 때만 사용합니다.

$app_name = "MyApp"

def greet
  puts "#{$app_name}에 오신 것을 환영합니다!"
end

greet   # MyApp에 오신 것을 환영합니다!

3-5. 상수 (Constant)

대문자로 시작합니다. 관례상 전부 대문자로 씁니다. 재할당하면 경고가 나고, 권장하지 않습니다.

PI       = 3.14159265
MAX_SIZE = 100
APP_NAME = "FitSystem"

puts PI         # 3.14159265
puts MAX_SIZE   # 100

4. 기본 자료형

Ruby의 모든 값은 객체이며, 각 자료형마다 유용한 메서드가 내장되어 있습니다.

4-1. 정수 (Integer)

n = 42
big = 1_000_000   # 밑줄로 가독성 향상 (실제 값은 1000000)

puts n.class        # Integer
puts n.even?        # false  (짝수 여부)
puts n.odd?         # true   (홀수 여부)
puts n.to_f         # 42.0   (Float로 변환)
puts n.to_s         # "42"   (String으로 변환)
puts n.abs          # 42     (절댓값)
puts(-5.abs)        # 5
puts 10.gcd(4)      # 2      (최대공약수)
puts 2 ** 10        # 1024   (거듭제곱)

4-2. 실수 (Float)

f = 3.14
puts f.class     # Float
puts f.round     # 3     (반올림, 기본 소수점 0자리)
puts f.round(1)  # 3.1
puts f.ceil      # 4     (올림)
puts f.floor     # 3     (내림)
puts f.to_i      # 3     (Integer로 변환, 소수점 버림)
puts f.nan?      # false (NaN 여부)

4-3. 문자열 (String)

큰따옴표("") 안에서는 문자열 보간(#{})과 이스케이프 시퀀스가 동작합니다. 작은따옴표('') 안에서는 있는 그대로 출력됩니다.

name = "Ruby"
ver  = 3.3

# 큰따옴표 — 보간(interpolation) 가능
puts "언어: #{name}, 버전: #{ver}"   # 언어: Ruby, 버전: 3.3
puts "줄바꿈: \n두 번째 줄"          # \n이 실제 줄바꿈으로 처리됨

# 작은따옴표 — 있는 그대로
puts '언어: #{name}'                 # 언어: #{name}  (보간 없음)
puts '줄바꿈: \n두 번째 줄'          # \n이 그대로 출력됨

문자열에서 자주 쓰는 메서드들입니다.

s = "Hello, Ruby World!"

puts s.length          # 18       (문자 수)
puts s.upcase          # HELLO, RUBY WORLD!
puts s.downcase        # hello, ruby world!
puts s.reverse         # !dlroW ybuR ,olleH
puts s.include?("Ruby")   # true
puts s.start_with?("He")  # true
puts s.end_with?("!")     # true
puts s.gsub("Ruby", "Python")   # Hello, Python World! (전체 치환)
puts s.sub("l", "L")            # HeLlo, Ruby World!  (첫 번째만 치환)
puts s.split(", ").inspect      # ["Hello", "Ruby World!"]
puts "  hi  ".strip             # "hi"  (앞뒤 공백 제거)
puts s[0, 5]                    # Hello  (0번 인덱스부터 5개)

4-4. 심볼 (Symbol)

심볼은 :이름 형태의 불변 식별자입니다. 같은 심볼은 프로그램 내에서 항상 동일한 객체를 가리키므로, 해시 키로 매우 자주 씁니다. 심볼은 문자열보다 메모리 효율이 좋습니다.

s = :hello
puts s.class        # Symbol
puts s.to_s         # "hello"  (문자열 변환)
puts :hello.upcase  # HELLO

# 심볼과 문자열 키 차이
h1 = { name: "Alice" }     # 심볼 키 (권장)
h2 = { "name" => "Alice" } # 문자열 키

puts h1[:name]    # Alice
puts h1["name"]   # nil  (심볼과 문자열은 다름!)
puts h2["name"]   # Alice

4-5. 불리언 (true / false)과 nil

Ruby에서 false와 nil만 거짓으로 처리됩니다. 숫자 0, 빈 문자열 "", 빈 배열 []은 모두 입니다. 이 점이 C/JavaScript와 다르니 주의하세요.

puts false ? "참" : "거짓"   # 거짓
puts nil   ? "참" : "거짓"   # 거짓
puts 0     ? "참" : "거짓"   # 참  ← 주의! (0은 참)
puts ""    ? "참" : "거짓"   # 참  ← 주의! (빈 문자열도 참)
puts []    ? "참" : "거짓"   # 참  ← 주의! (빈 배열도 참)

puts nil.nil?     # true
puts nil.to_s     # ""   (빈 문자열)
puts nil.to_a     # []   (빈 배열)
puts nil.to_i     # 0

4-6. 배열 (Array)

배열은 순서가 있는 값의 목록입니다. 여러 자료형을 섞어서 담을 수 있습니다.

fruits  = ["사과", "바나나", "딸기"]
numbers = [1, 2, 3, 4, 5]
mixed   = [1, "hello", :ok, nil, true]

# 인덱스 접근 (0부터 시작)
puts fruits[0]     # 사과
puts fruits[-1]    # 딸기  (뒤에서 첫 번째)
puts fruits[1, 2].inspect  # ["바나나", "딸기"]  (인덱스 1부터 2개)

# 추가/삭제
fruits.push("포도")   # 끝에 추가
fruits << "수박"       # << 도 push와 같음
fruits.unshift("레몬") # 앞에 추가
puts fruits.pop        # 수박  (끝 요소 꺼내서 반환)
puts fruits.shift      # 레몬  (앞 요소 꺼내서 반환)

# 자주 쓰는 메서드
puts numbers.length         # 5
puts numbers.sum            # 15
puts numbers.min            # 1
puts numbers.max            # 5
puts numbers.sort.inspect   # [1, 2, 3, 4, 5]
puts numbers.reverse.inspect# [5, 4, 3, 2, 1]
puts numbers.include?(3)    # true
puts numbers.count { |n| n.even? }  # 2  (짝수 개수)

# 집합 연산
a = [1, 2, 3, 4]
b = [3, 4, 5, 6]
puts (a + b).inspect        # [1,2,3,4,3,4,5,6]  (합치기)
puts (a - b).inspect        # [1,2]               (차집합)
puts (a & b).inspect        # [3,4]               (교집합)
puts (a | b).inspect        # [1,2,3,4,5,6]       (합집합, 중복 제거)

4-7. 해시 (Hash)

해시는 키-값 쌍의 집합입니다. Python의 dict, JavaScript의 Object와 비슷합니다.

user = { name: "Alice", age: 30, city: "Seoul" }

# 읽기
puts user[:name]                     # Alice
puts user.fetch(:city)               # Seoul
puts user.fetch(:email, "없음")      # 없음  (없을 때 기본값)

# 쓰기 및 삭제
user[:email] = "alice@example.com"
user[:age]   = 31
user.delete(:city)

# 순회
user.each do |key, value|
  puts "#{key}: #{value}"
end

# 자주 쓰는 메서드
puts user.keys.inspect        # [:name, :age, :email]
puts user.values.inspect      # ["Alice", 31, "alice@example.com"]
puts user.has_key?(:name)     # true
puts user.length              # 3

4-8. 범위 (Range)

연속된 값의 범위를 나타냅니다. ..는 끝값 포함, ...는 끝값 미포함입니다.

r1 = 1..5    # 1, 2, 3, 4, 5
r2 = 1...5   # 1, 2, 3, 4  (5 미포함)
r3 = 'a'..'e'  # a, b, c, d, e

puts r1.include?(5)   # true
puts r2.include?(5)   # false
puts r1.to_a.inspect  # [1, 2, 3, 4, 5]
puts r1.sum           # 15
puts r3.to_a.inspect  # ["a", "b", "c", "d", "e"]

5. 연산자

5-1. 산술 연산자

puts 10 + 3    # 13
puts 10 - 3    # 7
puts 10 * 3    # 30
puts 10 / 3    # 3  (정수끼리 나누면 소수점 버림)
puts 10.0 / 3  # 3.3333...  (Float가 섞이면 Float 결과)
puts 10 % 3    # 1  (나머지)
puts 10 ** 3   # 1000  (거듭제곱)

# 복합 대입
x = 10
x += 5   # x = x + 5  → 15
x -= 3   # x = x - 3  → 12
x *= 2   # x = x * 2  → 24
x /= 4   # x = x / 4  → 6
x **= 2  # x = x ** 2 → 36
x %= 10  # x = x % 10 → 6

5-2. 비교 연산자

puts 5 == 5     # true   (값 동일)
puts 5 != 3     # true   (값 다름)
puts 5 > 3      # true
puts 5 < 3      # false
puts 5 >= 5     # true

# 우주선 연산자: 크면 1, 같으면 0, 작으면 -1 반환
puts (5 <=> 3)  # 1
puts (3 <=> 3)  # 0
puts (1 <=> 3)  # -1

5-3. 논리 연산자

puts true && false   # false  (AND)
puts true || false   # true   (OR)
puts !true           # false  (NOT)

# 단락 평가 (short-circuit)
# || : 왼쪽이 true면 오른쪽 평가 안 함 → nil 기본값 패턴에 자주 사용
name = nil
puts name || "익명"   # 익명  (nil이 falsy라서 오른쪽 반환)

6. 조건문

6-1. if / elsif / else / end

if 문은 조건이 참일 때 블록을 실행합니다. end로 블록을 닫습니다.

score = 85

if score >= 90
  puts "A 등급"
elsif score >= 80
  puts "B 등급"
elsif score >= 70
  puts "C 등급"
else
  puts "재시험 대상"
end
# B 등급

6-2. 후위 if와 unless

한 줄짜리 조건은 뒤에 붙여 쓸 수 있습니다. unlessif not과 같습니다.

age = 20
puts "성인입니다"   if age >= 18
puts "미성년자입니다" unless age >= 18

6-3. 삼항 연산자

age    = 20
status = age >= 18 ? "성인" : "미성년자"
puts status   # 성인

6-4. case / when

여러 값을 비교할 때 if-elsif 대신 case를 씁니다. when 절에는 범위, 다중 값도 쓸 수 있습니다.

grade = "B"

case grade
when "A"
  puts "최우수"
when "B", "C"     # 여러 값 한번에
  puts "우수"
when "D"
  puts "보통"
else
  puts "재시험"
end
# 우수

# 범위도 사용 가능
point = 75
case point
when 90..100 then puts "A"
when 80..89  then puts "B"
when 70..79  then puts "C"
else              puts "F"
end
# C

7. 반복문

7-1. each

배열, 범위 등의 컬렉션을 순회할 때 가장 많이 씁니다.

fruits = ["사과", "바나나", "딸기"]

fruits.each do |fruit|
  puts fruit
end
# 사과
# 바나나
# 딸기

# 인덱스가 필요하면 each_with_index
fruits.each_with_index do |fruit, i|
  puts "#{i}: #{fruit}"
end
# 0: 사과
# 1: 바나나
# 2: 딸기

# 해시 순회
user = { name: "Alice", age: 30 }
user.each { |key, value| puts "#{key} = #{value}" }

7-2. times, upto, downto, step

# N회 반복
3.times do |i|
  puts "반복 #{i}"   # 0, 1, 2
end

1.upto(5)  { |n| print "#{n} " }   # 1 2 3 4 5
5.downto(1){ |n| print "#{n} " }   # 5 4 3 2 1
(0..10).step(2) { |n| print "#{n} " }  # 0 2 4 6 8 10

7-3. while과 until

i = 0
while i < 5
  print "#{i} "
  i += 1
end
# 0 1 2 3 4

j = 5
until j == 0
  print "#{j} "
  j -= 1
end
# 5 4 3 2 1

7-4. loop와 break, next

# loop는 무한 루프 — break로 탈출
count = 0
loop do
  count += 1
  next if count == 3    # 3은 출력 건너뜀
  break if count > 5    # 5 초과 시 종료
  puts count
end
# 1
# 2
# 4
# 5

8. 메서드 정의

메서드는 반복적으로 사용할 코드를 묶는 단위입니다. def 이름 ... end로 정의하며, 마지막 식의 값이 자동으로 반환됩니다.

8-1. 기본 메서드

def add(a, b)
  a + b        # 마지막 식의 값이 자동 반환
end

puts add(3, 4)  # 7

# 명시적 return — 중간에 조기 반환할 때 유용
def divide(a, b)
  return "0으로 나눌 수 없습니다" if b == 0
  a.to_f / b
end

puts divide(10, 3)  # 3.3333...
puts divide(10, 0)  # 0으로 나눌 수 없습니다

8-2. 기본값 인자

def greet(name, prefix = "안녕하세요")
  "#{prefix}, #{name}님!"
end

puts greet("Alice")             # 안녕하세요, Alice님!
puts greet("Bob", "반갑습니다") # 반갑습니다, Bob님!

8-3. 가변 인자 (*args)

def sum(*numbers)
  numbers.sum   # numbers는 배열이 됩니다
end

puts sum(1, 2, 3)        # 6
puts sum(10, 20, 30, 40) # 100
puts sum                 # 0

8-4. 키워드 인자

키워드 인자는 메서드를 호출할 때 이름을 명시해서 전달합니다. 인자 순서를 신경 쓰지 않아도 되고, 코드 가독성이 높아집니다.

def create_user(name:, age: 20, role: "user")
  puts "이름: #{name}, 나이: #{age}, 권한: #{role}"
end

create_user(name: "Alice")
# 이름: Alice, 나이: 20, 권한: user

create_user(name: "Bob", age: 30, role: "admin")
# 이름: Bob, 나이: 30, 권한: admin

8-5. 메서드 이름 관례

# ?로 끝나는 메서드 — true/false 반환
puts [].empty?               # true
puts "hello".include?("he") # true

# !로 끝나는 메서드 — 파괴적 메서드(원본을 직접 변경)
s = "hello"
puts s.upcase   # HELLO  (원본 유지, 새 문자열 반환)
puts s          # hello
s.upcase!       # 원본 변경
puts s          # HELLO

9. 블록(Block)

블록은 메서드에 전달하는 코드 조각입니다. do...end 또는 중괄호 { } 두 가지 방식으로 씁니다. 여러 줄이면 do...end, 한 줄이면 { }를 쓰는 것이 관례입니다.

# 여러 줄 블록
[1, 2, 3].each do |n|
  doubled = n * 2
  puts "#{n} * 2 = #{doubled}"
end

# 한 줄 블록
[1, 2, 3].each { |n| puts n * 2 }

# yield로 블록 호출 — 메서드가 블록을 실행하는 방법
def execute
  puts "메서드 시작"
  yield          # 전달된 블록을 여기서 실행
  puts "메서드 끝"
end

execute { puts "블록 실행!" }
# 메서드 시작
# 블록 실행!
# 메서드 끝

# yield에 값 전달
def double_yield(n)
  yield n
  yield n * 2
end

double_yield(5) { |x| puts x }
# 5
# 10

# 블록이 전달됐는지 확인
def optional_block
  if block_given?
    yield 42
  else
    puts "블록 없음"
  end
end

optional_block { |n| puts "값: #{n}" }  # 값: 42
optional_block                           # 블록 없음

10. Proc과 Lambda

블록을 객체로 저장하면 변수에 담아 여러 번 재사용할 수 있습니다. Proc과 lambda 두 가지 방법이 있습니다.

10-1. Proc

double = Proc.new { |n| n * 2 }

puts double.call(5)   # 10
puts double.call(10)  # 20
puts double.(7)       # 14   (짧은 호출 방법)
puts double[3]        # 6    (또 다른 호출 방법)

# 배열에 Proc 적용
numbers = [1, 2, 3, 4, 5]
puts numbers.map(&double).inspect  # [2, 4, 6, 8, 10]

10-2. Lambda

square = lambda { |n| n ** 2 }
cube   = ->(n) { n ** 3 }     # 화살표 문법 (동일)

puts square.call(4)   # 16
puts cube.call(3)     # 27

# 여러 인자
add = ->(a, b) { a + b }
puts add.call(3, 4)   # 7

10-3. Proc vs Lambda 핵심 차이

# 1. 인자 수 검사
p1 = Proc.new { |x, y| "#{x}, #{y}" }
l1 = lambda   { |x, y| "#{x}, #{y}" }

puts p1.call(1)     # "1, "   (인자 부족해도 nil로 채워서 실행)
# l1.call(1)        # ArgumentError: wrong number of arguments (1 for 2)

# 2. return의 범위 차이
def test_proc
  p_obj = Proc.new { return "proc 탈출" }
  p_obj.call
  "이 줄은 실행 안 됨"      # Proc의 return은 메서드 전체에서 탈출
end

def test_lambda
  l_obj = -> { return "lambda 종료" }
  l_obj.call
  "이 줄은 실행됩니다"      # lambda의 return은 lambda 내부에서만 종료
end

puts test_proc    # proc 탈출
puts test_lambda  # 이 줄은 실행됩니다

11. 컬렉션 핵심 메서드

배열/해시를 다룰 때 매일 쓰는 메서드들을 정리합니다.

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map — 각 요소를 변환해 새 배열 반환
p numbers.map { |n| n * 10 }            # [10, 20, 30, ...]

# select — 조건 참인 요소만 모아 새 배열 반환
p numbers.select { |n| n.even? }        # [2, 4, 6, 8, 10]

# reject — 조건 거짓인 요소만 모아 새 배열 반환
p numbers.reject { |n| n > 5 }          # [1, 2, 3, 4, 5]

# inject/reduce — 누적 계산
sum = numbers.inject(0) { |acc, n| acc + n }
p sum                    # 55
p numbers.inject(:+)     # 55  (심볼로 연산자 전달)

# any? / all? / none?
p numbers.any?  { |n| n > 9 }    # true   (하나라도 9 초과)
p numbers.all?  { |n| n > 0 }    # true   (모두 0 초과)
p numbers.none? { |n| n > 10 }   # true   (10 초과 없음)
p numbers.count { |n| n.odd? }   # 5      (홀수 개수)

# find/detect — 첫 번째 매칭 요소
p numbers.find { |n| n > 6 }     # 7

# flat_map — map 후 1단계 flatten
p [[1,2],[3,4]].flat_map { |a| a }  # [1, 2, 3, 4]

# group_by — 조건별로 그룹화
p numbers.group_by { |n| n.even? ? :even : :odd }
# {:odd=>[1,3,5,7,9], :even=>[2,4,6,8,10]}

12. 클래스와 객체지향

클래스는 객체의 설계도입니다. new로 인스턴스(객체)를 만들고, 메서드로 동작을 정의합니다.

class Person
  attr_reader   :name          # getter만 생성
  attr_writer   :email         # setter만 생성
  attr_accessor :age           # getter + setter 모두 생성

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

  def introduce
    "안녕하세요! 저는 #{@name}이고, #{@age}살입니다."
  end

  def to_s
    "Person(#{@name}, #{@age})"
  end
end

p1 = Person.new("Alice", 30, "alice@example.com")

puts p1.name          # Alice    (attr_reader)
puts p1.age           # 30       (attr_accessor의 getter)
p1.age = 31           # attr_accessor의 setter
puts p1.age           # 31
p1.email = "new@example.com"  # attr_writer
puts p1.introduce
puts p1               # Person(Alice, 31)  ← to_s 사용됨

12-1. 클래스 메서드와 클래스 변수

class BankAccount
  @@total_accounts = 0     # 클래스 변수
  INTEREST_RATE    = 0.05  # 상수

  attr_reader :balance, :owner

  def initialize(owner, balance = 0)
    @owner   = owner
    @balance = balance
    @@total_accounts += 1
  end

  def deposit(amount)
    @balance += amount
    puts "#{amount}원 입금 → 잔액: #{@balance}원"
  end

  def withdraw(amount)
    if amount > @balance
      puts "잔액 부족"
    else
      @balance -= amount
      puts "#{amount}원 출금 → 잔액: #{@balance}원"
    end
  end

  def self.total_accounts     # 클래스 메서드 (self. 접두사)
    @@total_accounts
  end
end

acc1 = BankAccount.new("Alice", 10000)
acc2 = BankAccount.new("Bob")
acc1.deposit(5000)            # 5000원 입금 → 잔액: 15000원
acc1.withdraw(3000)           # 3000원 출금 → 잔액: 12000원
puts BankAccount.total_accounts  # 2

13. 상속 (Inheritance)

class Child < Parent 문법으로 부모 클래스의 속성과 메서드를 물려받습니다. 공통 코드는 부모에 두고, 자식에서 특화된 동작만 추가하거나 오버라이드합니다.

class Animal
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def eat
    puts "#{@name}이(가) 먹이를 먹습니다."
  end

  def speak
    puts "..."
  end
end

class Dog < Animal
  def speak
    puts "#{@name}: 멍멍!"       # 부모 메서드 오버라이드
  end

  def fetch
    puts "#{@name}이(가) 공을 물어옵니다!"
  end
end

class Cat < Animal
  def initialize(name, indoor)
    super(name)                  # 부모 initialize 호출
    @indoor = indoor
  end

  def speak
    puts "#{@name}: 야옹~"
  end

  def info
    "#{@name} (#{@indoor ? '실내' : '실외'})"
  end
end

dog = Dog.new("초코")
cat = Cat.new("나비", true)

dog.eat          # 초코이(가) 먹이를 먹습니다.
dog.speak        # 초코: 멍멍!
dog.fetch        # 초코이(가) 공을 물어옵니다!
cat.speak        # 나비: 야옹~
puts cat.info    # 나비 (실내)

puts dog.is_a?(Dog)     # true
puts dog.is_a?(Animal)  # true  (상속 관계 확인)

14. 모듈 (Module)

모듈은 메서드를 묶는 네임스페이스이자, 클래스에 기능을 추가하는 믹스인(Mixin)입니다. Ruby는 단일 상속만 지원하지만, 모듈을 include하면 여러 기능을 자유롭게 조합할 수 있습니다.

module Greetable
  def greet
    puts "안녕하세요, 저는 #{name}입니다!"
  end
end

module Farewell
  def bye
    puts "#{name}이(가) 작별 인사를 합니다."
  end
end

class User
  include Greetable
  include Farewell

  attr_reader :name

  def initialize(name)
    @name = name
  end
end

u = User.new("Alice")
u.greet  # 안녕하세요, 저는 Alice입니다!
u.bye    # Alice이(가) 작별 인사를 합니다.

14-1. extend — 클래스 메서드로 추가

module ClassUtils
  def description
    "나는 #{self.name} 클래스입니다"
  end
end

class Product
  extend ClassUtils
end

puts Product.description  # 나는 Product 클래스입니다

15. 예외 처리

예외는 프로그램 실행 중 발생하는 오류입니다. 예외를 처리하지 않으면 프로그램이 종료됩니다. begin/rescue/ensure/end로 안전하게 처리합니다.

begin
  num = Integer("abc")   # 숫자가 아닌 문자열 변환 시도 → 예외 발생
  puts num
rescue ArgumentError => e
  puts "입력 오류: #{e.message}"
rescue ZeroDivisionError => e
  puts "0 나눗셈: #{e.message}"
rescue => e
  puts "기타 오류: #{e.message}"
ensure
  puts "항상 실행됩니다 (파일 닫기, 연결 해제 등)"
end
# 입력 오류: invalid value for Integer(): "abc"
# 항상 실행됩니다

15-1. raise로 예외 직접 발생

def withdraw(amount, balance)
  raise ArgumentError, "금액은 0보다 커야 합니다" if amount <= 0
  raise "잔액 부족" if amount > balance
  balance - amount
end

begin
  puts withdraw(0, 1000)
rescue ArgumentError => e
  puts "입력 오류: #{e.message}"
end
# 입력 오류: 금액은 0보다 커야 합니다

15-2. 커스텀 예외 클래스

class InsufficientFundsError < StandardError
  def initialize(amount)
    super("잔액이 #{amount}원 부족합니다")
  end
end

begin
  raise InsufficientFundsError.new(3000)
rescue InsufficientFundsError => e
  puts e.message  # 잔액이 3000원 부족합니다
end

16. 파일 입출력

# 파일 쓰기 (기존 내용 덮어씀)
File.write("log.txt", "첫 번째 줄\n")

# 파일에 추가 (append 모드)
File.open("log.txt", "a") do |f|
  f.puts "두 번째 줄"
  f.puts "세 번째 줄"
end   # 블록이 끝나면 자동으로 파일 닫힘

# 파일 전체 읽기
content = File.read("log.txt")
puts content

# 줄 단위로 읽기 (대용량 파일에 효율적)
File.foreach("log.txt") do |line|
  puts line.chomp    # chomp로 끝의 개행 제거
end

# 파일 존재 여부 확인
puts "파일이 있습니다" if File.exist?("log.txt")

17. 여러 줄 문자열 (Here Document)

# <<~TEXT 는 들여쓰기를 자동으로 제거해줘서 코드 정렬이 깔끔합니다
name = "Ruby"
welcome = <<~TEXT
  #{name}에 오신 것을 환영합니다!
  오늘도 즐거운 코딩 되세요.
TEXT
puts welcome

18. 자주 쓰는 편리한 문법

18-1. 안전 탐색 연산자 (&.)

# nil이면 NoMethodError 대신 nil을 반환 — 방어 코드를 줄여줍니다
user = nil
puts user&.name    # nil (에러 없음)

str = "hello"
puts str&.upcase   # HELLO

18-2. 다중 할당과 splat

a, b, c = 1, 2, 3
puts "#{a}, #{b}, #{c}"   # 1, 2, 3

# 값 교환 (temp 변수 불필요)
a, b = b, a
puts "#{a}, #{b}"          # 2, 1

# splat 연산자
first, *rest = [1, 2, 3, 4, 5]
puts first           # 1
puts rest.inspect    # [2, 3, 4, 5]

18-3. %w와 %i로 간편 배열 작성

# 문자열 배열 — 따옴표 없이 공백으로 구분
fruits = %w[사과 바나나 딸기 포도]
p fruits   # ["사과", "바나나", "딸기", "포도"]

# 심볼 배열
keys = %i[name age email]
p keys     # [:name, :age, :email]

18-4. tap — 체인 중 디버깅

result = [1, 2, 3]
  .tap  { |a| puts "원본: #{a.inspect}" }
  .map  { |n| n * 2 }
  .tap  { |a| puts "변환 후: #{a.inspect}" }
  .select { |n| n > 3 }
puts result.inspect
# 원본: [1, 2, 3]
# 변환 후: [2, 4, 6]
# [4, 6]

19. 파일 분리와 require

# user.rb
class User
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

# main.rb
require_relative "user"   # 현재 파일 기준 상대 경로 로드
u = User.new("Alice")
puts u.name               # Alice

20. 실전 미니 프로젝트: 간단한 할 일 관리 CLI

지금까지 배운 문법을 모두 활용한 작은 예제입니다. 직접 실행해보세요.

class TodoApp
  def initialize
    @todos = []
  end

  def add(title)
    @todos << { id: @todos.length + 1, title: title, done: false }
    puts "추가됨: #{title}"
  end

  def complete(id)
    todo = @todos.find { |t| t[:id] == id }
    if todo
      todo[:done] = true
      puts "완료됨: #{todo[:title]}"
    else
      puts "해당 ID의 할 일이 없습니다"
    end
  end

  def list
    if @todos.empty?
      puts "할 일이 없습니다."
      return
    end
    @todos.each do |t|
      status = t[:done] ? "[완료]" : "[미완]"
      puts "#{t[:id]}. #{status} #{t[:title]}"
    end
  end
end

app = TodoApp.new
app.add("Ruby 기본 문법 공부하기")
app.add("블로그 글 읽기")
app.add("예제 코드 직접 실행하기")
app.list
app.complete(1)
app.list
# 출력 예시
추가됨: Ruby 기본 문법 공부하기
추가됨: 블로그 글 읽기
추가됨: 예제 코드 직접 실행하기
1. [미완] Ruby 기본 문법 공부하기
2. [미완] 블로그 글 읽기
3. [미완] 예제 코드 직접 실행하기
완료됨: Ruby 기본 문법 공부하기
1. [완료] Ruby 기본 문법 공부하기
2. [미완] 블로그 글 읽기
3. [미완] 예제 코드 직접 실행하기

마무리: Ruby 학습 로드맵

이 글에서 다룬 내용을 기반으로, 다음 단계로 진행하면 Ruby 실력을 빠르게 키울 수 있습니다.

  1. 이 글의 예제 직접 실행 — 모든 코드를 손으로 타이핑하며 확인하세요.
  2. Ruby Silver 자격증 도전 — 문법 이해도를 공식으로 검증합니다.
  3. 간단한 CLI 프로그램 만들기 — To-do 앱, 계산기 등 작은 프로젝트를 완성해보세요.
  4. Ruby on Rails 입문 — 웹 앱 개발로 실전 활용 영역을 확장합니다.
  5. Gem 사용법 습득BundlerGemfile로 외부 라이브러리를 활용합니다.

Ruby의 철학은 "코드를 읽는 것이 즐거워야 한다"입니다. 이 글의 예제들을 직접 실행하며 한 줄씩 이해하다 보면, Ruby가 다른 언어보다 훨씬 직관적이고 유쾌한 언어라는 것을 느끼게 됩니다.

F

Fit System

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