루비 맛보기
Rails 8 개발을 위해 Ruby를 처음부터 정리합니다. Java/Spring 경험자를 위한 비교 설명과 실전 예제를 포함합니다.
출처: The Odin Project — Full Stack Ruby on Rails / Ruby Course
대상 독자: Java/Spring 경험자 / 목표: Rails 8 개발을 위한 Ruby 핵심 이해 / 특징: Java 비교 설명, 실전 예제, 연습 문제, 요약 정리표
목차
Part 1: Ruby 기초 문법
Ch 1. Ruby 소개 및 환경 설정
1.1 Ruby란?
Ruby는 1995년 일본의 마츠모토 유키히로(Matz)가 만든 프로그래머의 행복을 위해 설계된 언어입니다.
“I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy.” — Matz
Ruby의 핵심 철학:
- 최소 놀람의 원칙(POLA): 코드가 예상한 대로 동작한다
- 모든 것이 객체: 숫자, 문자열, nil, true/false 모두 객체
- 표현력: 영어에 가까운 읽기 쉬운 문법
- DRY(Don’t Repeat Yourself): 코드 중복 최소화
1.2 Java와의 핵심 차이점
| 항목 | Java | Ruby |
|---|---|---|
| 타입 시스템 | 정적 타입 (컴파일 시 검사) | 동적 타입 (런타임 검사) |
| 세미콜론 | 필수 (;) | 불필요 |
| 타입 선언 | String name = "Alice" | name = "Alice" |
| 접근제어자 | public, private, protected | public, private, protected (문법 상이) |
| 상속 | class Dog extends Animal | class Dog < Animal |
| 패키지/모듈 | package com.example | module MyModule |
| null 개념 | null | nil |
| 예외 처리 | try/catch/finally | begin/rescue/ensure |
| 람다/익명함수 | (x) -> x * 2 | -> (x) { x * 2 } 또는 ->(x) { x * 2 } |
| 문자열 보간 | "Hello " + name | "Hello #{name}" |
| 컬렉션 순회 | for (String s : list) | list.each { |s| ... } |
| 출력 | System.out.println() | puts |
1.3 설치 및 환경 설정
1
2
3
4
5
6
7
8
# rbenv 를 이용한 Ruby 설치 (권장)
brew install rbenv ruby-build # macOS
rbenv install 3.3.0
rbenv global 3.3.0
# 버전 확인
ruby -v # ruby 3.3.0 ...
irb # 대화형 Ruby 셸 실행
1.4 IRB(Interactive Ruby)
IRB는 Ruby의 REPL(Read-Eval-Print Loop)로, Node.js의 node 명령과 유사합니다.
1
2
3
4
5
6
7
8
irb
3.3.0 :001 > puts "Hello, Ruby!"
Hello, Ruby!
=> nil
3.3.0 :002 > 2 + 3
=> 5
3.3.0 :003 > "hello".upcase
=> "HELLO"
1.5 Ruby 파일 실행
1
2
3
4
5
6
7
8
# hello.rb
puts "Hello, World!"
name = "chan"
puts "안녕하세요, #{name}님!"
ruby hello.rb
# Hello, World!
# 안녕하세요, chan님!
Ch 2. 기본 데이터 타입
Ruby의 모든 것은 객체입니다. 심지어 42, true, nil도 객체이며 메서드를 호출할 수 있습니다.
2.1 숫자 (Numbers)
Integer (정수)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 정수 리터럴
age = 30
big_number = 1_000_000 # 가독성을 위한 언더스코어 사용 (Java와 동일)
hex = 0xFF # 16진수: 255
binary = 0b1010 # 2진수: 10
octal = 0o777 # 8진수: 511
# 기본 산술
10 + 3 # => 13
10 - 3 # => 7
10 * 3 # => 30
10 / 3 # => 3 (정수 나눗셈 → 정수 결과, Java와 동일!)
10 % 3 # => 1 (나머지)
10 ** 2 # => 100 (거듭제곱, Java의 Math.pow(10, 2) 대신)
# 정수 메서드 (Java에서는 없는 기능!)
10.even? # => true
10.odd? # => false
10.zero? # => false
0.zero? # => true
-5.abs # => 5
42.to_f # => 42.0 (Float으로 변환)
42.to_s # => "42" (String으로 변환)
⚠️ Java 개발자 주의: Ruby에서 정수 나눗셈은 Java와 동일하게 정수를 반환합니다.
10 / 3 # => 3(소수점 버림) 소수점 결과가 필요하면 하나를 Float으로 변환:10.0 / 3 # => 3.333...
Float (부동소수점)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pi = 3.14159
gravity = 9.8
# Float 연산
1.5 + 2.5 # => 4.0
10.0 / 3 # => 3.3333333333333335
# 형 변환
3.14.to_i # => 3 (소수점 버림, Java의 (int) 캐스팅과 유사)
3.14.ceil # => 4 (올림)
3.14.floor # => 3 (내림)
3.14.round # => 3 (반올림)
3.75.round # => 4
# 유용한 상수
Float::INFINITY # => Infinity
-Float::INFINITY # => -Infinity
Float::NAN # => NaN
숫자 관련 요약 정리표
| 메서드 | 설명 | 예시 |
|---|---|---|
+, -, *, / | 기본 사칙연산 | 10 / 3 # => 3 |
** | 거듭제곱 | 2 ** 10 # => 1024 |
% | 나머지(모듈로) | 10 % 3 # => 1 |
.to_i | 정수로 변환 | 3.9.to_i # => 3 |
.to_f | 부동소수점으로 변환 | 5.to_f # => 5.0 |
.to_s | 문자열로 변환 | 42.to_s # => "42" |
.even? | 짝수 여부 | 4.even? # => true |
.odd? | 홀수 여부 | 3.odd? # => true |
.abs | 절댓값 | -5.abs # => 5 |
.round | 반올림 | 3.5.round # => 4 |
.ceil | 올림 | 3.1.ceil # => 4 |
.floor | 내림 | 3.9.floor # => 3 |
2.2 문자열 (Strings)
Ruby의 문자열은 Java와 달리 변경 가능(mutable)합니다 (기본적으로).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 문자열 리터럴
single_quote = 'Hello, World!' # 작은따옴표: 보간 없음
double_quote = "Hello, World!" # 큰따옴표: 보간 지원
# 문자열 보간 (Java의 String.format() 대신)
name = "chan"
age = 31
puts "이름: #{name}, 나이: #{age}" # => 이름: chan, 나이: 31
puts "계산: #{1 + 2}" # => 계산: 3 (표현식 사용 가능!)
# 작은따옴표는 보간 안 됨
puts '이름: #{name}' # => 이름: #{name} (그대로 출력)
# 이스케이프 문자
puts "줄바꿈\n탭\t따옴표\""
# 줄바꿈
# 탭 따옴표"
# 여러 줄 문자열 (Heredoc) - Java에서 Java 15+ TextBlock과 유사
sql = <<~SQL
SELECT *
FROM users
WHERE active = true
SQL
# 문자열 연결
"Hello" + " " + "World" # => "Hello World"
"Ha" * 3 # => "HaHaHa" (Java에서 불가능한 문법!)
# 중요 문자열 메서드
str = "Hello, Ruby!"
str.length # => 12
str.size # => 12 (length의 별칭)
str.upcase # => "HELLO, RUBY!"
str.downcase # => "hello, ruby!"
str.reverse # => "!ybuR ,olleH"
str.include?("Ruby") # => true
str.start_with?("Hello") # => true
str.end_with?("!") # => true
str.split(", ") # => ["Hello", "Ruby!"]
str.strip # => 앞뒤 공백 제거
str.chomp # => 끝 줄바꿈 제거
str.gsub("Ruby", "World") # => "Hello, World!" (전체 치환)
str.sub("l", "L") # => "HeLlo, Ruby!" (첫 번째로 찾은 문자열만 치환)
str[0] # => "H" (인덱스 접근)
str[0, 5] # => "Hello" (시작, 길이)
str[0..4] # => "Hello" (범위)
# 형 변환
"42".to_i # => 42
"3.14".to_f # => 3.14
"hello".to_sym # => :hello
42.to_s # => "42"
# 문자열 포맷 (Java의 String.format과 유사)
"%.2f" % 3.14159 # => "3.14"
"%d개 항목" % 5 # => "5개 항목" (% 연산자를 포맷으로 사용하는 독특한 문법)
Bang(!) 메서드 - Java에 없는 Ruby 개념
Ruby에서 !로 끝나는 메서드는 원본 객체를 변경(mutate)합니다.
Java에서 문자열은 불변객체, 하지만 위에서 Ruby의 문자열은 변경가능하다고 했다.
1
2
3
4
5
6
7
8
9
10
str = "hello"
str.upcase # => "HELLO" (원본 변경 없음, 새 문자열 반환)
puts str # => "hello" (그대로)
str.upcase! # => "HELLO" (원본 변경!)
puts str # => "HELLO"
# Rails에서 자주 사용
name = " chan "
name.strip! # 원본에서 공백 제거
2.3 심볼 (Symbols)
심볼은 Ruby에서만 볼 수 있는 독특한 타입으로, 콜론(:)으로 시작하는 불변 식별자입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 심볼 생성
:hello
:user_name
:"hello world" # 공백 포함 시
# 문자열 vs 심볼
"hello".object_id # => 70123456789 (매번 다른 ID, 새 객체 생성)
"hello".object_id # => 70987654321 (다른 ID!)
:hello.object_id # => 12345 (항상 같은 ID, 하나의 객체만 존재)
:hello.object_id # => 12345 (동일!)
# 심볼은 메모리 효율적
# → 해시 키로 사용 시 성능 우수
# 변환
:hello.to_s # => "hello"
"hello".to_sym # => :hello
Java 개발자를 위한 비유: 심볼은 Java의
enum상수와 비슷합니다. 변경 불가능하고, 동일한 심볼은 메모리에 하나만 존재합니다. Rails에서는 해시 키, 메서드 이름 참조 등에 광범위하게 사용됩니다.
2.4 Booleans와 Nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Boolean
true.class # => TrueClass
false.class # => FalseClass
# Ruby의 진리값 규칙 (Java와 중요한 차이!)
# Ruby에서 false와 nil만이 거짓, 나머지는 모두 참!
if 0 then "0 is truthy in Ruby!" end # 실행됨! (Java에서는 오류)
if "" then "empty string is truthy!" end # 실행됨!
if [] then "empty array is truthy!" end # 실행됨!
if nil then "이건 안 됨" end
if false then "이것도 안 됨" end
# nil: Java의 null에 해당
name = nil
name.nil? # => true
name.class # => NilClass
# nil 처리
value = nil
value || "기본값" # => "기본값" (nil 대체 패턴, Java의 Optional.orElse와 유사)
value.to_s # => "" (nil.to_s는 빈 문자열)
value.to_i # => 0 (nil.to_i는 0)
value.to_a # => [] (nil.to_a는 빈 배열)
데이터 타입 요약
| 타입 | 예시 | Java 대응 |
|---|---|---|
Integer | 42, 1_000 | int, long, Integer |
Float | 3.14, 1.0e5 | double, float |
String | "hello", 'hi' | String |
Symbol | :name, :status | enum (유사) |
TrueClass | true | boolean (true) |
FalseClass | false | boolean (false) |
NilClass | nil | null |
연습 문제 — Ch 2
- 정수
17이 홀수인지 확인하는 코드를 작성하세요. "Hello, World!"에서"World"부분을"Ruby"로 교체하세요.1부터100까지의 합을 구하세요 (힌트:(1..100).sum).:active_record를 문자열로 변환하고, 대문자로 출력하세요.nil,0,"",[]중 Ruby에서 거짓(falsy)인 것은 무엇인가요?
정답 보기
```ruby # 1. puts 17.odd? # => true # 2. puts "Hello, World!".gsub("World", "Ruby") # => "Hello, Ruby!" # 3. puts (1..100).sum # => 5050 # 4. puts :active_record.to_s.upcase # => "ACTIVE_RECORD" # 5. nil만 거짓, 나머지는 모두 참 ```Ch 3. 변수와 상수
3.1 변수 종류
Ruby는 변수 이름의 접두사(prefix)로 변수의 범위(scope)를 나타냅니다. Java와 완전히 다른 개념입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 지역 변수 (Local Variable): 소문자 또는 _로 시작
local_var = "나는 지역 변수"
_also_local = "이것도 지역 변수"
# 전역 변수 (Global Variable): $로 시작 (사용 지양!)
$global_var = "나는 전역 변수"
# 인스턴스 변수 (Instance Variable): @로 시작 (클래스 내부)
@instance_var = "나는 인스턴스 변수"
# 클래스 변수 (Class Variable): @@로 시작
@@class_var = "나는 클래스 변수"
# 상수 (Constant): 대문자로 시작
MAX_SIZE = 100
PI = 3.14159
변수 범위 요약
| 변수 종류 | 접두사 | 범위 | Java 대응 |
|---|---|---|---|
| 지역 변수 | (없음) | 현재 메서드/블록 | 지역 변수 |
| 전역 변수 | $ | 프로그램 전체 | static 전역 |
| 인스턴스 변수 | @ | 해당 객체 | this.field |
| 클래스 변수 | @@ | 클래스 전체 | static 멤버 변수 |
| 상수 | 대문자 | 클래스/모듈 | static final |
3.2 변수 할당의 특징
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 다중 할당 (Java에서 불가능!)
a, b, c = 1, 2, 3
puts a # => 1
puts b # => 2
puts c # => 3
# 스왑 (임시 변수 없이!)
a, b = b, a
puts a # => 2
puts b # => 1
# 배열에서 다중 할당
first, *rest = [1, 2, 3, 4, 5]
puts first # => 1
puts rest # => [2, 3, 4, 5]
*beginning, last = [1, 2, 3, 4, 5]
puts last # => 5
puts beginning # => [1, 2, 3, 4]
# 조건부 할당
x = nil
x ||= "기본값" # x가 nil 또는 false이면 할당
puts x # => "기본값"
y = 10
y ||= "기본값" # y가 이미 값이 있으므로 변경 없음
puts y # => 10
# Rails에서 자주 보이는 패턴
@user ||= User.find(params[:id]) # 이미 로드됐으면 재사용
3.3 상수
1
2
3
4
5
6
7
8
9
10
MAX_USERS = 1000
DATABASE_URL = "postgresql://localhost/mydb"
# 상수 재할당 시 경고 (에러는 아님, Java와 다름!)
MAX_USERS = 2000 # warning: already initialized constant MAX_USERS
# Rails에서 상수 사용 예
class ApplicationController < ActionController::Base
ALLOWED_FORMATS = [:json, :html].freeze # .freeze로 변경 방지
end
Ch 4. 입출력 (I/O)
4.1 출력 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# puts: 줄바꿈 포함 출력 (Java의 System.out.println)
puts "Hello" # Hello\n
puts 42 # 42\n
puts [1, 2, 3] # 1\n2\n3\n (배열은 각 원소를 한 줄씩!)
puts nil # (빈 줄)
# print: 줄바꿈 없이 출력 (Java의 System.out.print)
print "Hello "
print "World" # Hello World (줄바꿈 없음)
# p: 디버그 출력 (값의 타입 정보 포함)
p "hello" # => "hello" (따옴표 포함)
p 42 # => 42
p nil # => nil
p [1, 2, 3] # => [1, 2, 3]
# pp: 구조화된 디버그 출력 (Pretty Print)
require 'pp'
pp({ name: "Alice", scores: [95, 87, 92] })
# {:name=>"Alice", :scores=>[95, 87, 92]}
언제 무엇을 쓸까?
puts: 사용자에게 보여주는 출력p: 개발 중 디버깅 (값의 실제 형태 확인)
4.2 입력 받기
1
2
3
4
5
6
7
8
9
# gets: 줄바꿈 포함하여 입력받음 (Java의 Scanner.nextLine)
print "이름을 입력하세요: "
name = gets # "chan\n" (줄바꿈 포함)
puts "안녕하세요, #{name.chomp}!" # chomp로 줄바꿈 제거
# gets.chomp: 줄바꿈 없이 입력받기
print "나이를 입력하세요: "
age = gets.chomp.to_i # 문자열 → 정수 변환
puts "내년이면 #{age + 1}살이 됩니다."
4.3 형 변환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 문자열 → 다른 타입
"42".to_i # => 42
"3.14".to_f # => 3.14
"hello".to_sym # => :hello
"123abc".to_i # => 123 (숫자 부분까지만, Java와 다름!)
"abc".to_i # => 0 (숫자 없으면 0)
# Integer(), Float(): 엄격한 변환 (실패 시 예외 발생)
Integer("42") # => 42
Integer("abc") # ArgumentError 발생!
Float("3.14") # => 3.14
# 다른 타입 → 문자열
42.to_s # => "42"
3.14.to_s # => "3.14"
:hello.to_s # => "hello"
[1,2,3].to_s # => "[1, 2, 3]"
nil.to_s # => ""
true.to_s # => "true"
Ch 5. 조건 로직
5.1 if / elsif / else
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 기본 if
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
elsif score >= 70
puts "C"
else
puts "F"
end
# => "B"
# Java와의 차이: end 키워드 사용, () 불필요
# Java: if (score >= 90) { ... }
# Ruby: if score >= 90 ... end
# 한 줄 if (후치 if)
puts "성인입니다" if age >= 18
puts "양수입니다" if number > 0
# unless: if not과 동일 (Ruby 특유의 표현)
unless score < 60
puts "합격!"
end
# 또는
puts "합격!" unless score < 60
5.2 비교 연산자와 논리 연산자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 비교 연산자
5 > 3 # => true
5 < 3 # => false
5 >= 5 # => true
5 <= 4 # => false
5 == 5 # => true (동등 비교)
5 != 3 # => true (부등 비교)
# 우주선 연산자 (Spaceship Operator) — Java에 없음!
1 <=> 2 # => -1 (왼쪽이 작음)
2 <=> 2 # => 0 (같음)
3 <=> 2 # => 1 (왼쪽이 큼)
# 정렬, 비교에 자주 사용
# 논리 연산자
true && false # => false (AND)
true || false # => true (OR)
!true # => false (NOT)
# 영어 버전 (낮은 우선순위, 조건문 제어에 사용)
true and false # && 와 유사하나 우선순위 다름
true or false # || 와 유사하나 우선순위 다름
not true # ! 와 유사하나 우선순위 다름
# 문자열 비교
"apple" == "apple" # => true
"apple" <=> "banana" # => -1 (알파벳 순서)
5.3 case / when (Java의 switch-case)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Java의 switch-case에 해당
grade = "B"
case grade
when "A"
puts "Excellent!"
when "B"
puts "Good job!"
when "C"
puts "Average"
when "D", "F" # 여러 조건 가능
puts "Need improvement"
else
puts "Invalid grade"
end
# 범위(Range)를 사용하는 case
score = 85
result = case score
when 90..100 then "A" # .. : 경계 포함
when 80..89 then "B"
when 70..79 then "C"
when 0..69 then "F"
else "Invalid"
end
puts result # => "B"
# 타입 체크에도 사용
def describe(value)
case value
when String then "문자열: #{value}"
when Integer then "정수: #{value}"
when Array then "배열 (#{value.length}개 원소)"
when NilClass then "nil이에요"
end
end
puts describe("hello") # => "문자열: hello"
puts describe(42) # => "정수: 42"
puts describe([1,2,3]) # => "배열 (3개 원소)"
5.4 삼항 연산자와 조건부 할당
1
2
3
4
5
6
7
8
9
10
11
12
13
# 삼항 연산자 (Java와 동일)
age = 20
status = age >= 18 ? "성인" : "미성년자"
puts status # => "성인"
# 조건부 할당
x = nil
x ||= 10 # x가 nil/false이면 10 할당
puts x # => 10
y = 5
y &&= y * 2 # y가 truthy이면 y * 2 할당
puts y # => 10
연습 문제 — Ch 5
score가 60 미만이면 “불합격”, 그 이상이면 “합격”을 출력하는 코드를 작성하세요.case/when을 사용하여 월(1~12)을 계절(봄/여름/가을/겨울)로 변환하는 메서드를 작성하세요.a = nil일 때,a ||= "기본"의 결과는?
정답 보기
```ruby # 1. score = 75 puts score >= 60 ? "합격" : "불합격" # 2. def season(month) case month when 3, 4, 5 then "봄" when 6, 7, 8 then "여름" when 9, 10, 11 then "가을" when 12, 1, 2 then "겨울" else "잘못된 월" end end # 3. a = nil a ||= "기본" puts a # => "기본" ```Ch 6. 반복문 (Loops)
6.1 loop (무한 루프)
1
2
3
4
5
6
7
8
# 기본 무한 루프
i = 0
loop do
i += 1
puts i
break if i >= 5 # break로 탈출
end
# 1, 2, 3, 4, 5
6.2 while / until
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# while: 조건이 true인 동안 반복 (Java와 동일)
i = 0
while i < 5
puts i
i += 1
end
# until: 조건이 false인 동안 반복 (while의 반대)
i = 0
until i >= 5
puts i
i += 1
end
# 후치 while/until
i = 0
i += 1 while i < 5
6.3 for 루프와 times/upto/downto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# for...in (잘 사용하지 않음, each 권장)
for i in 1..5
puts i
end
# times: N번 반복
5.times { puts "Hello" }
5.times { |i| puts "반복 #{i}" } # 0, 1, 2, 3, 4
# upto / downto
1.upto(5) { |i| print "#{i} " } # 1 2 3 4 5
5.downto(1){ |i| print "#{i} " } # 5 4 3 2 1
# step: 증가값 지정
1.step(10, 2) { |i| print "#{i} " } # 1 3 5 7 9
6.4 each (가장 Ruby다운 반복문)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 배열 순회 (Java의 for-each에 해당)
[1, 2, 3, 4, 5].each do |num|
puts num
end
# 한 줄 블록
[1, 2, 3].each { |n| puts n * 2 }
# 해시 순회
{name: "Alice", age: 30}.each do |key, value|
puts "#{key}: #{value}"
end
# 인덱스 포함 순회
["a", "b", "c"].each_with_index do |item, index|
puts "#{index}: #{item}"
end
# 0: a
# 1: b
# 2: c
6.5 루프 제어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# break: 루프 즉시 탈출 (Java와 동일)
[1, 2, 3, 4, 5].each do |n|
break if n == 3
puts n
end
# 1, 2
# next: 다음 반복으로 건너뜀 (Java의 continue)
[1, 2, 3, 4, 5].each do |n|
next if n.even?
puts n
end
# 1, 3, 5
# redo: 현재 반복 재실행 (잘 사용하지 않음)
반복문 선택 가이드
| 상황 | 권장 방법 |
|---|---|
| 컬렉션 순회 | each |
| N번 반복 | times |
| 변환 결과 필요 | map |
| 필터링 | select / reject |
| 누적 계산 | reduce / inject |
| 무한 루프 | loop |
Ch 7. 배열 (Arrays)
7.1 배열 생성
1
2
3
4
5
6
7
8
9
10
11
12
# 기본 생성
arr = [1, 2, 3, 4, 5]
mixed = [1, "hello", :symbol, true, nil, [1, 2]] # 혼합 타입 가능!
# Array.new
Array.new(3) # => [nil, nil, nil]
Array.new(3, 0) # => [0, 0, 0]
Array.new(3) { |i| i * 2 } # => [0, 2, 4]
# 문자열 배열 단축 표기
words = %w[apple banana cherry] # => ["apple", "banana", "cherry"]
syms = %i[red green blue] # => [:red, :green, :blue]
7.2 배열 접근 및 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arr = [10, 20, 30, 40, 50]
# 접근
arr[0] # => 10 (첫 번째)
arr[-1] # => 50 (마지막, Java에서 불가!)
arr[1..3] # => [20, 30, 40] (범위)
arr[1, 3] # => [20, 30, 40] (시작 인덱스, 길이)
arr.first # => 10
arr.last # => 50
arr.first(2) # => [10, 20]
arr.last(2) # => [40, 50]
# 수정
arr[0] = 99 # => [99, 20, 30, 40, 50]
arr.push(60) # 끝에 추가 (Java의 add)
arr << 70 # << 연산자로 추가 (Ruby 특유!)
arr.unshift(0) # 앞에 추가
arr.pop # 마지막 제거 및 반환
arr.shift # 첫 번째 제거 및 반환
arr.delete(30) # 특정 값 제거
arr.delete_at(1) # 인덱스로 제거
7.3 주요 배열 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# 정보
arr.length # => 10
arr.size # => 10
arr.empty? # => false
arr.include?(5) # => true
arr.count(5) # => 2 (5가 몇 개?)
# 정렬
arr.sort # => [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
arr.sort.reverse # => [9, 6, 5, 5, 4, 3, 3, 2, 1, 1]
arr.sort_by { |n| -n } # 내림차순
# 중복 제거
arr.uniq # => [3, 1, 4, 5, 9, 2, 6]
# 평탄화
[[1, 2], [3, 4], [5]].flatten # => [1, 2, 3, 4, 5]
[[1, [2, 3]], [4]].flatten(1) # => [1, [2, 3], 4] (1단계만)
# 합집합, 교집합, 차집합
[1, 2, 3] | [3, 4, 5] # => [1, 2, 3, 4, 5] (합집합)
[1, 2, 3] & [2, 3, 4] # => [2, 3] (교집합)
[1, 2, 3] - [2, 3] # => [1] (차집합)
# 합치기
[1, 2] + [3, 4] # => [1, 2, 3, 4]
[1, 2, 3].concat([4, 5]) # => [1, 2, 3, 4, 5]
# 변환
arr.map { |n| n * 2 } # 각 원소를 변환한 새 배열
arr.select { |n| n > 3 } # 조건에 맞는 원소만
arr.reject { |n| n > 3 } # 조건에 안 맞는 원소만
arr.min # => 1
arr.max # => 9
arr.sum # => 39
arr.sample # 랜덤 원소 반환
arr.shuffle # 무작위 섞기
# zip: 배열 병합
[1, 2, 3].zip([4, 5, 6]) # => [[1, 4], [2, 5], [3, 6]]
# flatten + map의 합체: flat_map
[[1, 2], [3, 4]].flat_map { |a| a.map { |n| n * 2 } }
# => [2, 4, 6, 8]
26.03.01 추가… 메서드가 이해가 안가서
1 2 3 4 5 6 7 8 # [[1, 2], [3, 4]].map { |a| print a } # => [1, 2][3, 4] [[1, 2], [3, 4]].map { |a| a.map { |n| n * 2 } } # => [[2, 4], [6, 8]] [[1, 2], [3, 4]].flat_map { |a| a.map { |n| n * 2 } } # => [2, 4, 6, 8]
1 2 3 4 5 6 7**동작 흐름 정리** ``` [[1,2], [3,4]] ↓ 블록 실행 (각 요소에 *2) [[2,4], [6,8]] ← map 결과 ↓ flatten (한 겹 펼치기) [2, 4, 6, 8] ← 최종 결과클로드에 설명에 따르면…
1 2 3 4 5 List.of(List.of(1,2), List.of(3,4)) .stream() .flatMap(a -> a.stream().map(n -> n * 2)) .collect(toList()); // => [2, 4, 6, 8]
7.4 Rails에서의 배열 활용 예시
1
2
3
4
5
6
7
8
9
# Active Record 쿼리 결과는 배열처럼 동작
users = User.where(active: true) # ActiveRecord::Relation (Enumerable 포함)
users.map { |u| u.email } # 이메일 목록
users.select { |u| u.admin? } # 관리자만 필터링
users.first(5) # 상위 5명
# 자주 쓰는 패턴
user_ids = [1, 2, 3, 4, 5]
User.where(id: user_ids) # IN 쿼리로 자동 변환!
Ch 8. 해시 (Hashes)
8.1 해시 생성
해시는 Ruby에서 가장 많이 사용하는 자료구조 중 하나로, Java의 HashMap에 해당합니다.
1
2
3
4
5
6
7
8
9
10
11
# 옛날 방식 (=> 사용, 하시 로켓)
old_style = { "name" => "Alice", "age" => 30 }
# 심볼 키 방식 (권장! Rails에서 주로 사용)
person = { name: "Alice", age: 30, city: "Seoul" }
# 이는 { :name => "Alice", :age => 30 } 와 동일
# 빈 해시
empty = {}
empty = Hash.new # 기본값 nil
empty = Hash.new(0) # 기본값 0 (카운터에 유용)
8.2 해시 접근 및 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
person = { name: "Alice", age: 30 }
# 접근
person[:name] # => "Alice"
person[:age] # => 30
person[:missing] # => nil (없는 키, Java의 get은 null)
# 기본값 설정
person.fetch(:name) # => "Alice"
person.fetch(:missing) # KeyError 발생!
person.fetch(:missing, "없음") # => "없음" (기본값)
person.fetch(:missing) { "없음" } # 블록으로 기본값
# 수정
person[:name] = "Bob" # 값 변경
person[:email] = "b@x.com" # 새 키 추가
person.delete(:age) # 키 삭제
# 키/값 확인
person.key?(:name) # => true
person.has_key?(:age) # => false (위에서 삭제됨)
person.value?("Bob") # => true, key말고 value중에 이런 값이 있냐? 그런거 물어보는거임.
person.include?(:name) # => true
8.3 해시 순회
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
user = { name: "Alice", age: 30, city: "Seoul" }
# 전체 순회
user.each do |key, value|
puts "#{key}: #{value}"
end
# 키만
user.keys # => [:name, :age, :city]
user.each_key { |k| puts k }
# 값만
user.values # => ["Alice", 30, "Seoul"]
user.each_value { |v| puts v }
# map/select/reject
user.map { |k, v| [k, v.to_s] }.to_h
# => {name: "Alice", age: "30", city: "Seoul"}
user.select { |k, v| v.is_a?(String) } # is_a : 특정 클래스/모듈의 인스턴스인지 확인하는 메서드
# => {name: "Alice", city: "Seoul"}
user.select { |k, v| v.is_a?(Integer) }
# => {age: 30}
user.reject { |k, v| v.is_a?(Integer) }
# => {name: "Alice", city: "Seoul"}
user.reject { |k, v| v.is_a?(String) }
# => {age: 30}
8.4 해시 변환 및 병합
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 배열 ↔ 해시 변환
arr = [[:a, 1], [:b, 2], [:c, 3]]
hash = arr.to_h # => {a: 1, b: 2, c: 3}
# 또는
arr.each_with_object({}) { |(k, v), h| h[k] = v }
# 해시 → 배열
{a: 1, b: 2}.to_a # => [[:a, 1], [:b, 2]]
# 병합 (merge)
h1 = { a: 1, b: 2 }
h2 = { b: 3, c: 4 }
h1.merge(h2) # => {a: 1, b: 3, c: 4} (h2가 우선)
h2.merge(h1) # => {b: 2, c: 4, a: 1} (h1이 우선)
# 충돌 처리 블록
h1.merge(h2) { |key, old_val, new_val| old_val + new_val }
# => {a: 1, b: 5, c: 4}
# transform_keys / transform_values (Ruby 2.5+)
{ name: "Alice", age: 30 }.transform_values { |v| v.to_s }
# => {name: "Alice", age: "30"}
{name: "Alice", age: 30}.transform_values { |v| v.to_s + "xxx" }
# => {name: "Alicexxx", age: "30xxx"}
{ "name" => "Alice" }.transform_keys(&:to_sym)
# => {name: "Alice"}
26/03/01 추가
each_with_object : 누적 객체를 유지하면서 순회하는 메서드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 arr = [[:a, 1], [:b, 2], [:c, 3]] arr.each_with_object({}) { |(k, v), h| h[k] = v } # ^^ ^^^^^^^^ ^^^^^^^^ # 초기값 블록 인자 동작 # (k, v) → 현재 요소 [:a, 1]을 구조분해 # h → 계속 누적되는 Hash 객체 (처음엔 {}) arr = [[:a, 1], [:b, 2], [:c, 3]] # 1회차: 요소 [:a, 1] # k = :a, v = 1, h = {} # h[:a] = 1 → h = {a: 1} # 2회차: 요소 [:b, 2] # k = :b, v = 2, h = {a: 1} # h[:b] = 2 → h = {a: 1, b: 2} # 3회차: 요소 [:c, 3] # k = :c, v = 3, h = {a: 1, b: 2} # h[:c] = 3 → h = {a: 1, b: 2, c: 3}
8.5 Rails에서의 해시 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 컨트롤러에서 Strong Parameters
def user_params
params.require(:user).permit(:name, :email, :password)
end
# 옵션 해시 패턴 (Rails에서 매우 흔한 패턴!)
def create_user(name:, email:, role: :user) # 키워드 인수
User.create(name: name, email: email, role: role)
end
create_user(name: "Alice", email: "a@x.com")
# 해시를 이용한 조건 빌딩
conditions = { active: true }
conditions[:admin] = true if current_user.super_admin?
User.where(conditions)
연습 문제 — Ch 7 & 8
- 배열
[5, 2, 8, 1, 9, 3]을 오름차순으로 정렬하고, 5보다 큰 숫자만 선택하세요. { a: 1, b: 2, c: 3 }해시에서 모든 값을 2배로 만드세요.- 문자열 배열
["apple", "banana", "cherry"]에서 각 단어의 글자 수를 값으로 가지는 해시를 만드세요.
정답 보기
```ruby # 1. [5, 2, 8, 1, 9, 3].sort.select { |n| n > 5 } # => [8, 9] # 2. { a: 1, b: 2, c: 3 }.transform_values { |v| v * 2 } # => {a: 2, b: 4, c: 6} # 3. ["apple", "banana", "cherry"].each_with_object({}) do |word, hash| hash[word] = word.length end # => {"apple"=>5, "banana"=>6, "cherry"=>6} # 또는: ["apple", "banana", "cherry"].map { |w| [w, w.length] }.to_h ```Ch 9. 메서드 (Methods)
9.1 메서드 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 기본 메서드
def greet
puts "Hello!"
end
greet # => Hello!
# 인수가 있는 메서드
def add(a, b)
a + b # 마지막 표현식이 자동으로 return값 (implicit return)
end
puts add(3, 5) # => 8
# 명시적 return (중간에 반환할 때)
def divide(a, b)
return "0으로 나눌 수 없습니다" if b == 0
a / b
end
Java 개발자 주의: Ruby는
return키워드 없이 마지막 표현식의 값이 자동으로 반환됩니다!
9.2 기본값 인수
1
2
3
4
5
6
def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
greet("Alice") # => "Hello, Alice!"
greet("Bob", "Hi") # => "Hi, Bob!"
9.3 키워드 인수 (Keyword Arguments)
1
2
3
4
5
6
7
8
# 키워드 인수 (Rails에서 매우 자주 사용!)
def create_post(title:, body:, published: false)
"제목: #{title}, 내용: #{body}, 공개: #{published}"
end
create_post(title: "Ruby 가이드", body: "내용...")
create_post(body: "내용", title: "제목") # 순서 무관!
create_post(title: "제목", body: "내용", published: true)
9.4 가변 인수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 스플랫 연산자 (*) — Java의 varargs
def sum(*numbers)
numbers.sum
end
sum(1, 2, 3) # => 6
sum(1, 2, 3, 4, 5) # => 15
# 더블 스플랫 (**) — 해시 인수
def log(message, **options)
puts "#{message} | #{options}"
end
log("에러 발생", level: :error, code: 500)
# => 에러 발생 | {:level=>:error, :code=>500}
9.5 ? 메서드와 ! 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ? 메서드: Boolean 반환 (관례)
[].empty? # => true
"hello".include?("ell") # => true
5.zero? # => false
5.nil? # => false
nil.nil? # => true
# ! 메서드: 객체 자체를 변경 (관례)
str = "hello"
str.upcase # => "HELLO" (str은 그대로)
str.upcase! # str을 "HELLO"로 변경
arr = [3, 1, 2]
sorted = arr.sort # arr은 그대로, 새 배열 반환
arr.sort! # arr 자체를 정렬
9.6 메서드를 객체처럼 다루기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# method 메서드로 참조
def double(n)
n * 2
end
m = method(:double)
m.call(5) # => 10
[1, 2, 3].map(&method(:double)) # => [2, 4, 6]
# to_proc으로 변환
:upcase.to_proc.call("hello") # => "HELLO"
["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"]
# 이 패턴을 Rails에서 매우 자주 사용!
Ruby의
method메서드와to_proc변환 상세 설명 - 26/03/04 추가1.
method(:double)— 메서드를 객체로 참조
1 2 3 def double(n) n * 2 endRuby에서
def로 정의한 메서드는 그냥 호출만 할 수 있는 것이 기본입니다. 하지만method(:double)을 사용하면 메서드 자체를 객체로 “꺼내서” 변수에 담을 수 있습니다.
1 2 3 4 5 6 m = method(:double) # m은 이제 Method 객체 # double이라는 메서드 자체를 가리키고 있음 m.call(5) # => 10 # .call()로 실행Java로 비유하면:
1 2 3 // Java에서는 메서드를 직접 변수에 담을 수 없어서 // 함수형 인터페이스로 래핑해야 했음 Function<Integer, Integer> double = n -> n * 2;Ruby는
method(:이름)하나로 간단하게 해결합니다.2.
&연산자 — proc/메서드를 블록으로 변환
1 [1, 2, 3].map(&method(:double)) # => [2, 4, 6]
map은 원래 블록({ }또는do...end) 을 받습니다:
1 [1, 2, 3].map { |n| double(n) } # 일반적인 방식
&를 앞에 붙이면 Method 객체 → 블록으로 자동 변환해서 넘겨줍니다.
1 2 3&method(:double) ↓ { |n| double(n) } 와 동일하게 동작즉,
&는 “이 객체를 블록처럼 써라” 라는 신호입니다.3.
Symbol#to_proc— 심볼을 proc으로 변환
1 :upcase.to_proc.call("hello") # => "HELLO"
:upcase는 그냥 심볼(문자열 이름표)입니다..to_proc을 호출하면 아래와 같은 proc이 생성됩니다:
1 2 # :upcase.to_proc 의 내부 동작 원리 proc { |obj| obj.upcase }그래서:
1 2 3 :upcase.to_proc.call("hello") # => "hello".upcase # => "HELLO"4.
&:upcase— Rails/Ruby에서 가장 자주 보는 패턴
1 ["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"]
&+ Symbol 조합이 일어나는 과정을 단계별로 보면:
1 2 3 4 5&:upcase 1단계: :upcase.to_proc 자동 호출 → proc { |obj| obj.upcase } 생성 2단계: proc → 블록으로 변환 → { |obj| obj.upcase } 와 동일결국 아래 세 줄은 완전히 동일한 동작입니다:
1 2 3 ["hello", "world"].map { |s| s.upcase } # 풀어쓴 방식 ["hello", "world"].map { |s| s.send(:upcase) } # send 방식 ["hello", "world"].map(&:upcase) # 심볼 shortcut5. Rails에서 자주 쓰는 실전 패턴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 문자열 처리 [" hello ", " world "].map(&:strip) # => ["hello", "world"] # 타입 변환 ["1", "2", "3"].map(&:to_i) # => [1, 2, 3] # nil 제거 (select/reject과 함께) [1, nil, 2, nil, 3].reject(&:nil?) # => [1, 2, 3] # ActiveRecord에서 User.all.map(&:email) # => ["a@a.com", "b@b.com", ...] # User 객체 배열에서 email만 뽑아낼 때 Post.all.select(&:published?) # published? 가 true인 것만 필터링핵심 정리
문법 하는 일 method(:foo)메서드를 Method 객체로 꺼냄 &method(:foo)Method 객체를 블록으로 변환해서 전달 :foo.to_proc심볼을 proc { \|obj\| obj.foo }로 변환&:footo_proc 자동 호출 + 블록 변환 (shortcut)
&:메서드명패턴은 “각 요소에 이 메서드를 호출해라” 라는 뜻으로 기억하면 Rails 코드 읽을 때 매우 편합니다.
Ch 10. 디버깅
10.1 기본 디버깅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# p를 이용한 값 확인
def calculate(x, y)
result = x * y
p result # 개발 중 확인
result
end
# pp를 이용한 구조 확인
data = { users: [{name: "Alice", age: 30}] }
pp data
# puts 디버깅
puts "--- DEBUG ---"
puts "x = #{x.inspect}"
10.2 예외 처리 (begin/rescue/ensure)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Java의 try/catch/finally에 해당
begin
# 위험한 코드
result = 10 / 0
rescue ZeroDivisionError => e
puts "0으로 나눌 수 없습니다: #{e.message}"
rescue ArgumentError, TypeError => e
puts "잘못된 인수: #{e.message}"
rescue => e # 그 외 모든 예외
puts "예기치 않은 오류: #{e.message}"
ensure
# 항상 실행 (Java의 finally)
puts "정리 작업"
end
# raise로 예외 발생
def fetch_user(id)
raise ArgumentError, "ID는 양수여야 합니다" unless id > 0
# ...
end
Java vs Ruby 예외 처리 비교
| Java | Ruby |
|---|---|
try { } | begin ... end |
catch (Exception e) { } | rescue Exception => e |
finally { } | ensure |
throw new Exception() | raise Exception.new(...) |
throw new RuntimeException() | raise "메시지" (RuntimeError) |
10.3 Pry / Byebug (개발 도구)
1
2
3
4
5
6
7
8
9
10
# Gemfile에 추가
gem 'pry', group: :development
gem 'byebug', group: :development
# 코드 내 중단점 설정
def my_method
x = 10
binding.pry # 여기서 실행 멈추고 IRB처럼 디버깅 가능
y = x * 2
end
Part 2: 컬렉션과 Enumerable
Ch 11. 기본 Enumerable 메서드
Enumerable은 Ruby의 강력한 모듈로, 컬렉션(배열, 해시 등)에 포함되어 있습니다. Java의 Stream API와 유사하지만 더 간결합니다.
11.1 map / collect (변환)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 배열의 각 원소를 변환하여 새 배열 반환 (Java의 Stream.map에 해당)
numbers = [1, 2, 3, 4, 5]
squared = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]
# 문자열 변환
names = ["alice", "bob", "charlie"]
names.map(&:capitalize) # => ["Alice", "Bob", "Charlie"]
names.map(&:upcase) # => ["ALICE", "BOB", "CHARLIE"]
# &:메서드명 패턴: Rails에서 매우 자주 사용!
# 해시에서 사용
users = [{name: "Alice", age: 30}, {name: "Bob", age: 25}]
users.map { |u| u[:name] } # => ["Alice", "Bob"]
users.map { |u| "#{u[:name]}(#{u[:age]})" }
# => ["Alice(30)", "Bob(25)"]
# Java 비교
# Java: list.stream().map(n -> n * n).collect(Collectors.toList())
# Ruby: list.map { |n| n * n }
11.2 select / filter (필터링)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 짝수만 선택
numbers.select { |n| n.even? } # => [2, 4, 6, 8, 10]
numbers.select(&:even?) # 축약 표현
# 5보다 큰 수만
numbers.select { |n| n > 5 } # => [6, 7, 8, 9, 10]
# reject: select의 반대
numbers.reject { |n| n.even? } # => [1, 3, 5, 7, 9]
# Java 비교
# Java: list.stream().filter(n -> n % 2 == 0).collect(Collectors.toList())
# Ruby: list.select { |n| n.even? }
11.3 reduce / inject (누적)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 합계 계산
[1, 2, 3, 4, 5].reduce(:+) # => 15
[1, 2, 3, 4, 5].reduce(0, :+) # 초기값 0으로 합계
[1, 2, 3, 4, 5].inject { |sum, n| sum + n } # => 15
# 최대/최솟값 (reduce로 구현)
[3, 1, 4, 1, 5].reduce { |max, n| n > max ? n : max } # => 5
# 배열을 해시로 변환
words = ["apple", "banana", "cherry"]
words.reduce({}) do |hash, word|
hash[word] = word.length
hash
end
# => {"apple"=>5, "banana"=>6, "cherry"=>6}
# Java 비교
# Java: list.stream().reduce(0, Integer::sum)
# Ruby: list.reduce(:+) 또는 list.reduce(0) { |sum, n| sum + n }
reduce/inject상세 설명 - 26/03/04 추가
reduce와inject는 완전히 같은 메서드입니다 (alias 관계).기본 동작 원리
배열의 요소들을 왼쪽부터 하나씩 누적(accumulate) 해가는 메서드입니다.
1 2 3 4 5 6 7 8[1, 2, 3, 4, 5].reduce(:+) 1단계: 1 + 2 = 3 2단계: 3 + 3 = 6 3단계: 6 + 4 = 10 4단계: 10 + 5 = 15 ↑ 최종 반환값Case 1:
reduce(:+)— 심볼로 연산자 지정
1 [1, 2, 3, 4, 5].reduce(:+) # => 15
:+는+연산자를 심볼로 표현한 것- 초기값 없음 → 배열의 첫 번째 요소(1)가 초기 누산값이 됨
- 앞서 배운
&:upcase패턴과 같은 원리 (심볼 → 메서드 호출)
1 2 3 4 5누산값(acc) | 현재값(n) | 결과 1 | 2 | 3 3 | 3 | 6 6 | 4 | 10 10 | 5 | 15Case 2:
reduce(0, :+)— 초기값 지정
1 [1, 2, 3, 4, 5].reduce(0, :+) # => 15
- 첫 번째 인자
0이 초기 누산값으로 설정됨- 배열의 첫 번째 요소부터 연산에 참여
1 2 3 4 5 6누산값(acc) | 현재값(n) | 결과 0 | 1 | 1 ← 초기값 0에서 시작 1 | 2 | 3 3 | 3 | 6 6 | 4 | 10 10 | 5 | 15초기값이 중요한 경우:
1 2 [].reduce(:+) # => nil (빈 배열이면 nil 반환) [].reduce(0, :+) # => 0 (초기값이 있으면 안전)Case 3:
inject { |sum, n| sum + n }— 블록으로 직접 작성
1 [1, 2, 3, 4, 5].inject { |sum, n| sum + n } # => 15
|sum, n|→sum은 누산값,n은 현재 요소- 블록의 반환값이 다음 단계의
sum이 됨:+심볼 방식과 완전히 동일한 결과블록 방식은 복잡한 로직을 쓸 때 유용합니다:
1 2 3 4 5 6 7 # 짝수만 더하기 [1, 2, 3, 4, 5].reduce(0) { |sum, n| n.even? ? sum + n : sum } # => 6 (2 + 4) # 최댓값 구하기 [3, 1, 4, 1, 5, 9].reduce { |max, n| n > max ? n : max } # => 9실전 활용 패턴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 곱셈 [1, 2, 3, 4, 5].reduce(:*) # => 120 # 문자열 합치기 ["Hello", " ", "World"].reduce(:+) # => "Hello World" # Hash 만들기 (Rails에서 자주 사용) [[:a, 1], [:b, 2], [:c, 3]].reduce({}) do |hash, (key, value)| hash[key] = value hash end # => {a: 1, b: 2, c: 3} # 중첩 배열 합산 orders = [{price: 100}, {price: 200}, {price: 300}] orders.reduce(0) { |sum, order| sum + order[:price] } # => 600핵심 정리
방식 초기값 특징 reduce(:+)첫 번째 요소 가장 간결 reduce(0, :+)직접 지정 빈 배열에 안전 reduce { |acc, n| ... }첫 번째 요소 복잡한 로직 가능 reduce(초기값) { |acc, n| ... }직접 지정 가장 유연
reduce/inject는 “배열 → 단일 값” 으로 줄일 때 사용한다고 기억하면 됩니다.
11.4 each_with_object (해시/배열 빌드)
1
2
3
4
5
6
7
8
9
10
11
# reduce보다 해시/배열 생성에 편리
result = [1, 2, 3, 4, 5].each_with_object({}) do |n, hash|
hash[n] = n ** 2
end
# => {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}
# 배열에 누적
result = [1, 2, 3].each_with_object([]) do |n, arr|
arr << n * 2 if n.odd?
end
# => [2, 6]
each_with_object상세 설명 - 26/03/04 추가기본 개념
누적 객체를 직접 수정하면서 순회하는 메서드입니다.
reduce와 비슷하지만 핵심 차이가 있습니다.
1 2 3 배열.each_with_object(누적객체) do |현재요소, 누적객체| # 누적객체를 직접 수정 end블록 인자 순서에 주의:
1 2 reduce : |누산값, 현재요소| ← 누적값이 첫 번째 each_with_object : |현재요소, 누적객체| ← 현재요소가 첫 번째Case 1: Hash 만들기
1 2 3 4 result = [1, 2, 3, 4, 5].each_with_object({}) do |n, hash| hash[n] = n ** 2 end # => {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}단계별 진행:
1 2 3 4 5 6 7초기 hash = {} n=1: hash[1] = 1**2 → {1=>1} n=2: hash[2] = 2**2 → {1=>1, 2=>4} n=3: hash[3] = 3**2 → {1=>1, 2=>4, 3=>9} n=4: hash[4] = 4**2 → {1=>1, 2=>4, 3=>9, 4=>16} n=5: hash[5] = 5**2 → {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}
{}를 초기값으로 넘기고, 블록 안에서 직접 키/값을 추가합니다.Case 2: 조건부 배열 만들기
1 2 3 4 result = [1, 2, 3].each_with_object([]) do |n, arr| arr << n * 2 if n.odd? end # => [2, 6]단계별 진행:
1 2 3 4 5 초기 arr = [] n=1: 1.odd? → true → arr << 2 → [2] n=2: 2.odd? → false → 아무것도 안 함 → [2] n=3: 3.odd? → true → arr << 6 → [2, 6]
select+map을 한 번에 처리할 수 있습니다:
1 2 # 위 코드와 동일한 결과, 다른 방식 [1, 2, 3].select(&:odd?).map { |n| n * 2 } # => [2, 6]
reduce와의 차이점
1 2 3 4 5 6 7 8 9 10 11 # reduce — 블록의 반환값이 다음 누산값이 됨 [1, 2, 3].reduce({}) do |hash, n| hash[n] = n ** 2 hash # ← 반드시 hash를 마지막에 반환해야 함! end # each_with_object — 객체를 직접 수정, 반환값 신경 안 써도 됨 [1, 2, 3].each_with_object({}) do |n, hash| hash[n] = n ** 2 # 반환값 신경 안 써도 됨 end
reduce로 Hash/Array를 만들 때 마지막에 객체를 반환하는 실수를 자주 합니다.each_with_object는 그 실수를 방지해줍니다.실전 활용 패턴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 단어 빈도수 세기 ["apple", "banana", "apple", "cherry", "banana", "apple"] .each_with_object(Hash.new(0)) do |word, counts| counts[word] += 1 end # => {"apple"=>3, "banana"=>2, "cherry"=>1} # 그룹핑 (group_by 대신) [1, 2, 3, 4, 5].each_with_object({odd: [], even: []}) do |n, hash| n.odd? ? hash[:odd] << n : hash[:even] << n end # => {odd: [1, 3, 5], even: [2, 4]} # Rails — ActiveRecord 결과를 Hash로 변환 User.all.each_with_object({}) do |user, hash| hash[user.id] = user.email end # => {1=>"a@a.com", 2=>"b@b.com", ...}핵심 정리
reduceeach_with_object블록 인자 순서 |누산값, 요소||요소, 객체|블록 반환값 다음 누산값으로 사용됨 무시됨 Hash/Array 만들 때 마지막에 객체 반환 필요 그냥 수정만 하면 됨 숫자 누산 적합 부적합 Hash나 Array를 만들 때 →
each_with_object숫자/문자열 하나로 줄일 때 →reduce
11.5 flat_map, group_by, sort_by, min_by, max_by
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# flat_map: map + flatten
[[1, 2], [3, 4], [5]].flat_map { |a| a.map { |n| n * 2 } }
# => [2, 4, 6, 8, 10]
# group_by: 그룹 분류
[1, 2, 3, 4, 5, 6].group_by { |n| n.even? ? :even : :odd }
# => {:odd=>[1, 3, 5], :even=>[2, 4, 6]}
users = [{name: "Alice", dept: "개발"}, {name: "Bob", dept: "기획"}, {name: "Carol", dept: "개발"}]
users.group_by { |u| u[:dept] }
# => {"개발"=>[...Alice, Carol...], "기획"=>[...Bob...]}
# sort_by: 기준으로 정렬
users.sort_by { |u| u[:name] } # 이름 알파벳순
users.sort_by { |u| u[:name].length } # 이름 길이순
# min_by / max_by
users.min_by { |u| u[:name].length } # 이름 가장 짧은 사람
users.max_by { |u| u[:name].length } # 이름 가장 긴 사람
# tally: 빈도 계산 (Ruby 2.7+)
["apple", "banana", "apple", "cherry", "banana", "apple"].tally
# => {"apple"=>3, "banana"=>2, "cherry"=>1}
Ch 12. Predicate Enumerable 메서드
12.1 any?, all?, none?, one?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
numbers = [1, 2, 3, 4, 5]
# any?: 하나라도 조건 만족하면 true
numbers.any? { |n| n > 4 } # => true (5 > 4)
numbers.any?(&:zero?) # => false
# all?: 모두 조건 만족하면 true
numbers.all? { |n| n > 0 } # => true
numbers.all? { |n| n > 2 } # => false
# none?: 하나도 조건 만족하지 않으면 true
numbers.none? { |n| n > 10 } # => true
numbers.none? { |n| n > 4 } # => false
# one?: 정확히 하나만 조건 만족하면 true
numbers.one? { |n| n == 3 } # => true
numbers.one? { |n| n > 3 } # => false (5, 4, 5가 해당되어 여러 개)
# count: 조건 만족하는 개수
numbers.count { |n| n.even? } # => 2
numbers.count # => 5 (전체 개수)
12.2 find / detect
1
2
3
4
5
6
7
8
9
10
# 첫 번째로 조건을 만족하는 원소 반환
[1, 2, 3, 4, 5].find { |n| n > 3 } # => 4
[1, 2, 3, 4, 5].detect { |n| n.even? } # => 2
# 없으면 nil 반환
[1, 2, 3].find { |n| n > 10 } # => nil
# Java의 stream().filter().findFirst()에 해당
# Java: list.stream().filter(n -> n > 3).findFirst().orElse(null)
# Ruby: list.find { |n| n > 3 }
Ch 13. 중첩 컬렉션 (Nested Collections)
13.1 중첩 배열
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 2차원 배열
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
matrix[0] # => [1, 2, 3]
matrix[0][1] # => 2
matrix[1][2] # => 6
# 중첩 배열 순회
matrix.each do |row|
row.each do |cell|
print "#{cell} "
end
puts
end
# 평탄화
matrix.flatten # => [1, 2, 3, 4, 5, 6, 7, 8, 9]
matrix.flatten(1) # 1단계만 (이미 1단계라 같은 결과)
# 중첩 map
matrix.map { |row| row.map { |n| n * 2 } }
# => [[2,4,6], [8,10,12], [14,16,18]]
13.2 중첩 해시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# JSON과 유사한 구조
user = {
name: "Alice",
address: {
city: "Seoul",
district: "강남구"
},
scores: [95, 87, 92]
}
user[:address][:city] # => "Seoul"
user[:scores][0] # => 95
# dig: 안전한 중첩 접근 (Ruby 2.3+)
user.dig(:address, :city) # => "Seoul"
user.dig(:contact, :phone) # => nil (KeyError 없이)
# 중첩 해시 수정
user[:address][:city] = "Busan"
user[:address][:zipcode] = "12345" # 새 키 추가
# 중첩 해시 순회
user.each do |key, value|
if value.is_a?(Hash)
puts "#{key}:"
value.each { |k, v| puts " #{k}: #{v}" }
else
puts "#{key}: #{value}"
end
end
Part 3: 객체지향 프로그래밍 (OOP)
Ch 14. 클래스와 객체
14.1 기본 클래스 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Java와 비교하며 학습
# Java:
# public class Person {
# private String name;
# private int age;
# public Person(String name, int age) { this.name = name; this.age = age; }
# public String getName() { return name; }
# }
# Ruby:
class Person
def initialize(name, age) # 생성자 (Java의 constructor)
@name = name # 인스턴스 변수 (Java의 this.name)
@age = age
end
def name # getter
@name
end
def name=(new_name) # setter (Java의 setName)
@name = new_name
end
def introduce
"안녕하세요, #{@name}입니다. #{@age}살이에요."
end
def to_s # Java의 toString()
"Person(#{@name}, #{@age})"
end
end
# 객체 생성 (Java의 new Person("Alice", 30))
alice = Person.new("Alice", 30)
alice.name # => "Alice"
alice.name = "Bob" # setter 호출
puts alice.introduce # => "안녕하세요, Bob입니다. 30살이에요."
14.2 attr_accessor / attr_reader / attr_writer
Ruby는 반복적인 getter/setter를 자동으로 생성하는 기능을 제공합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person
attr_accessor :name, :age # getter + setter 모두 생성
attr_reader :id # getter만
attr_writer :email # setter만
def initialize(id, name, age)
@id = id
@name = name
@age = age
end
end
# Java의 Lombok @Getter @Setter @Data와 유사!
alice = Person.new(1, "Alice", 30)
alice.name # getter
alice.name = "Bob" # setter
alice.id # getter only
# alice.id = 2 # NoMethodError! (setter 없음)
| Ruby | Java (Lombok) |
|---|---|
attr_reader :name | @Getter |
attr_writer :name | @Setter |
attr_accessor :name | @Getter @Setter |
14.3 클래스 메서드와 클래스 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Counter
@@count = 0 # 클래스 변수 (Java의 static 변수)
def initialize
@@count += 1
@id = @@count
end
def self.count # 클래스 메서드 (Java의 static 메서드)
@@count
end
def self.reset
@@count = 0
end
def id
@id
end
end
c1 = Counter.new
c2 = Counter.new
c3 = Counter.new
Counter.count # => 3 (Java의 Counter.count() 대신 Counter.count)
c1.id # => 1
c3.id # => 3
14.4 Comparable 모듈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Rectangle
include Comparable # Comparable 모듈 포함
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
@width * @height
end
def <=>(other) # 우주선 연산자 구현 필수
area <=> other.area
end
def to_s
"#{@width}x#{@height}(넓이: #{area})"
end
end
rects = [Rectangle.new(3, 4), Rectangle.new(2, 8), Rectangle.new(1, 1)]
rects.sort # area 기준 정렬 자동으로!
rects.min # 가장 작은 것
rects.max # 가장 큰 것
<=>(우주선 연산자) - 26/03/05 추가두 값을 비교해서 -1, 0, 1 중 하나를 반환합니다.
1 2 3 1 <=> 2 # => -1 (왼쪽이 작음) 2 <=> 2 # => 0 (같음) 3 <=> 2 # => 1 (왼쪽이 큼)Java로 치면
Comparator.compare(a, b)랑 똑같은 개념입니다.sort, min, max가 area 기준으로 동작하는 이유
①
include Comparable선언
1 include ComparableComparable 모듈을 include하면
sort,min,max,between?등을 자동으로 쓸 수 있게 됩니다.② 단, 조건이 하나 있음
Comparable은 내부적으로<=>를 호출해서 동작합니다.
그래서<=>만 직접 정의해주면 나머지는 알아서 됩니다.③
<=>를 area 기준으로 정의했으므로
1 2 3 def <=>(other) area <=> other.area # area끼리 비교 end
sort,min,max가 내부적으로<=>를 호출할 때, area를 기준으로 비교하게 됩니다.전체 흐름 요약
1 2 3 4 5 6 7rects.sort 호출 ↓ Comparable이 내부적으로 <=> 를 호출 ↓ 개발자가 정의한 <=> 실행 → area끼리 비교 ↓ area 기준으로 정렬 완료Java의
Comparable<T>인터페이스에서compareTo()를 오버라이드하는 것과 정확히 같은 패턴입니다.
1 2 3 4 5 6 7 // Java class Rectangle implements Comparable { @Override public int compareTo(Rectangle other) { return Integer.compare(this.area(), other.area()); } }
1 2 3 4 # Ruby - 완전히 같은 개념 def <=>(other) area <=> other.area end
Ch 15. 상속 (Inheritance)
15.1 기본 상속
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 부모 클래스
class Animal
attr_reader :name, :sound
def initialize(name, sound)
@name = name
@sound = sound
end
def speak
"#{@name}가 #{@sound}하고 울어요."
end
def breathe
"호흡 중..."
end
end
# 자식 클래스 (Java의 extends 대신 <)
class Dog < Animal
def initialize(name)
super(name, "멍멍") # 부모 생성자 호출 (Java의 super()와 동일)
end
def fetch(item)
"#{@name}가 #{item}을 가져와요!"
end
# 메서드 오버라이딩
def speak
super + " (꼬리를 흔들며)" # 부모 메서드 호출 후 추가
end
end
class Cat < Animal
def initialize(name)
super(name, "야옹")
end
def purr
"그르르르..."
end
end
dog = Dog.new("바둑이")
dog.speak # => "바둑이가 멍멍하고 울어요. (꼬리를 흔들며)"
dog.breathe # => "호흡 중..." (상속된 메서드)
dog.fetch("공") # => "바둑이가 공을 가져와요!"
puts dog.is_a?(Dog) # => true
puts dog.is_a?(Animal) # => true (상속 관계)
puts dog.class # => Dog
puts dog.superclass # NoMethodError! superclass는 클래스 메서드
puts Dog.superclass # => Animal
Ch 16. 모듈과 믹스인 (Modules & Mixins)
Ruby는 다중 상속을 지원하지 않지만, 모듈을 통한 믹스인(Mixin)으로 유연한 기능 공유가 가능합니다.
16.1 모듈 정의와 include
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# ※이렇게 모듈을 만들어서 다른 파일에서도 사용할 수 있게 만들어줍니다.
module Greetable
def greet
"안녕하세요, 저는 #{name}입니다."
end
def farewell
"안녕히 가세요!"
end
end
module Serializable # 직렬화
def to_json
# 간단한 구현
vars = instance_variables.map do |var|
# puts var => var가 @name, @email 처럼 출력되기 때문에 아래에서 [1..] 처리를해 @를 잘라준다.
"\"#{var.to_s[1..]}\": \"#{instance_variable_get(var)}\""
end
"{ #{vars.join(', ')} }"
end
end
class User
include Greetable
include Serializable
attr_reader :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
class Admin
include Greetable
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
end
user = User.new("Alice", "a@x.com")
puts user.greet # => 안녕하세요, 저는 Alice입니다.
puts user.to_json # => {"name": "Alice", "email": "a@x.com"}
admin = Admin.new("Bob", 5)
puts admin.greet # => 안녕하세요, 저는 Bob입니다.
16.2 extend (클래스 메서드로 포함)
1
2
3
4
5
6
7
8
9
10
11
module ClassMethods
def create_table
"테이블 생성: #{name}"
end
end
class Post
extend ClassMethods # 클래스 메서드로 포함
end
Post.create_table # => "테이블 생성: Post"
16.3 메서드 탐색 순서 (Method Lookup Path)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module M1
def hello
"M1의 hello"
end
end
module M2
def hello
"M2의 hello"
end
end
class MyClass
include M1
include M2 # 나중에 include된 것이 우선순위 높음
def hello
"MyClass의 hello"
end
end
# 탐색 순서 확인
MyClass.ancestors
# => [MyClass, M2, M1, Object, Kernel, BasicObject]
obj = MyClass.new
obj.hello # => "MyClass의 hello" (직접 정의된 것이 우선)
16.4 Java 인터페이스 vs Ruby 모듈
| Java 인터페이스 | Ruby 모듈(Mixin) |
|---|---|
interface Flyable | module Flyable |
implements Flyable | include Flyable |
| 메서드 시그니처만 정의 | 메서드 구현 포함 가능 |
| 다중 implements 가능 | 다중 include 가능 |
| 상태(필드) 없음 | 상태(변수) 없음 |
| Java 8부터 default 메서드 | 기본적으로 구현 포함 |
Ch 17. 접근 제어와 캡슐화
17.1 public / private / protected
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class BankAccount
attr_reader :balance
def initialize(owner, balance)
@owner = owner
@balance = balance
end
# public 메서드 (기본값)
def deposit(amount)
validate_amount!(amount) # private 메서드 호출
@balance += amount
log_transaction("입금", amount)
end
def withdraw(amount)
validate_amount!(amount)
raise "잔액 부족" if amount > @balance
@balance -= amount
log_transaction("출금", amount)
end
def transfer_to(other_account, amount)
withdraw(amount)
other_account.receive_transfer(amount) # protected 호출
end
protected # 같은 클래스 또는 하위 클래스에서만 호출 가능
def receive_transfer(amount)
@balance += amount
end
private # 클래스 내부에서만 호출 가능 (Java와 동일 의미)
def validate_amount!(amount)
raise ArgumentError, "금액은 양수여야 합니다" unless amount > 0
end
def log_transaction(type, amount)
puts "[로그] #{@owner}: #{type} #{amount}원"
end
end
acc = BankAccount.new("Alice", 1000)
acc.deposit(500) # OK
acc.withdraw(200) # OK
acc.balance # => 1300
# acc.validate_amount!(100) # NoMethodError: private method
# acc.receive_transfer(100) # NoMethodError: protected method
Ruby에서
!는 문법적 강제가 아니라 개발자들 사이의 관례(convention) 입니다. - 26/03/05 추가의미
“이 메서드는 위험하거나, 예외를 발생시킬 수 있다”
를 호출하는 쪽에 경고하는 네이밍 컨벤션입니다.
이 코드에서의 의미
ruby
1 2 3 def validate_amount!(amount) raise ArgumentError, "금액은 양수여야 합니다." unless amount > 0 end
raise로 예외를 던질 수 있는 메서드라는 걸 이름만 봐도 알 수 있게 합니다.!없이validate_amount라고 지어도 동작은 완전히 동일합니다.주로 쓰이는 두 가지 패턴
① 예외를 발생시키는 경우 (이 코드)
ruby
1 2 3 def validate_amount!(amount) raise ArgumentError unless amount > 0 # 예외 발생 가능 end② 원본을 직접 변경하는 경우 (Ruby 표준 라이브러리)
ruby
1 2 3 4 str = "hello" str.upcase # => "HELLO" (원본 유지) str.upcase! # => "HELLO" (원본 자체를 변경) puts str # => "HELLO"정리
설명 !있음위험하다, 예외가 날 수 있다, 원본을 바꾼다는 경고 !없음비교적 안전한 메서드 강제성 없음 - 순수 컨벤션 Java로 치면 메서드명에
OrThrow를 붙이는 관례랑 비슷합니다.java
1 2 3 // Java 관례 findById(id) // null 반환 가능 findByIdOrThrow(id) // 없으면 예외 던짐
Part 4: 고급 Ruby
Ch 18. 파일 처리와 직렬화
18.1 파일 읽기/쓰기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 26/03/06 주석 추가!
# File.exist?("hello.txt"): "hello.txt"라는 파일이 현재 폴더에 있는지 확인합니다.
# ! (not): 파일이 없을 때만 아래 코드를 실행하라는 뜻입니다.
# File.write: 파일을 생성하고 내용을 적습니다. 이미 내용이 있다면 덮어씁니다.
if !File.exist?("hello.txt")
File.write("hello.txt", "Hello, Ruby!\n두 번째 줄!")
end
# File.read: 파일의 전체 내용을 하나의 큰 **문자열(String)**로 가져옵니다. 파일 크기가 아주 클 때는 메모리를 많이 사용할 수 있으니 주의해야 합니다.
content = File.read("hello.txt")
puts content
puts
# File.foreach: 파일을 한 줄씩 읽어서 블록(do...end)으로 넘겨줍니다. 메모리 효율이 좋아 대용량 파일을 읽을 때 권장되는 방식입니다.
# .chomp: 각 줄 끝에 포함된 줄바꿈 문자(\n)를 제거하여 출력 시 줄 간격이 벌어지지 않게 합니다.
File.foreach("hello.txt") do |line|
puts line.chomp
end
puts
# File.open(..., "w"): 파일을 쓰기 모드로 엽니다.
# 블록(do...end) 사용: 블록이 끝나면 루비가 자동으로 파일을 닫아(close) 주기 때문에 안전하고 편리합니다.
# puts vs write: puts는 자동으로 끝에 줄바꿈을 붙여주고, write는 입력한 문자열만 그대로 적습니다.
File.open("output.txt", "w") do |file|
file.puts "첫 번째 줄"
file.puts "두 번째 줄"
file.write "세 번째 줄"
end
puts
# "r": 읽기 전용 모드로 파일을 엽니다. 위에서 만든 "output.txt"의 내용을 출력합니다.
File.open("output.txt", "r") do |file|
puts file.read
end
puts File.exist?("hello.txt") # 파일 존재 여부 (true/false)
puts File.size("hello.txt") # 파일 크기 (바이트 단위)
puts File.directory?(".") # 해당 경로가 디렉토리(폴더)인지 확인
18.2 JSON 직렬화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
require 'json'
# Ruby 객체 → JSON 문자열
data = { name: "Alice", age: 30, scores: [95, 87, 92] }
json_str = JSON.generate(data)
# => '{"name":"Alice","age":30,"scores":[95,87,92]}'
# 보기 좋게 출력
puts JSON.pretty_generate(data)
# {
# "name": "Alice",
# "age": 30,
# "scores": [95, 87, 92]
# }
# JSON 문자열 → Ruby 객체
parsed = JSON.parse(json_str)
puts parsed["name"] # => "Alice"
puts parsed["scores"] # => [95, 87, 92]
# 파일로 저장
File.write("data.json", JSON.pretty_generate(data))
# 파일에서 읽기
loaded = JSON.parse(File.read("data.json"))
# Rails에서 API 응답으로 JSON 사용
# render json: @user
# render json: { status: :ok, data: @users }
18.3 YAML 직렬화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require 'yaml'
# YAML 주요 사용처: Rails 설정 파일 (database.yml, config/application.yml)
data = {
database: {
host: "localhost",
port: 5432,
name: "myapp_development"
},
redis_url: "redis://localhost:6379"
}
# Ruby → YAML
yaml_str = YAML.dump(data)
puts yaml_str
# ---
# :database:
# :host: localhost
# ...
# YAML → Ruby
config = YAML.safe_load(yaml_str, permitted_classes: [Symbol])
# 파일로 저장/읽기
File.write("config.yml", YAML.dump(data))
config = YAML.load_file("config.yml")
18.4 Marshal (바이너리 직렬화)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class GameState
attr_accessor :level, :score, :items
def initialize(level, score, items)
@level = level
@score = score
@items = items
end
end
state = GameState.new(5, 1500, ["검", "방패", "물약"])
# Marshal.dump(state): 루비 객체인 state를 파일에 쓸 수 있는 **이진 바이트 스트림(Byte Stream)**으로 변환합니다.
# "wb" 모드: 파일을 Write(쓰기) 하되, 텍스트가 아닌 Binary(이진) 데이터로 다루겠다는 뜻입니다.
# 이렇게 하면 메모리에 있던 객체의 모습 그대로 save.dat라는 파일에 박제(!)됩니다.
File.open("save.dat", "wb") { |f| f.write(Marshal.dump(state)) }
# File.binread("save.dat"): 이진 파일의 내용을 통째로 읽어옵니다.
# Marshal.load(...): 읽어온 이진 데이터를 분석해 다시 원래의 루비 객체(GameState 인스턴스)로 재구성합니다.
# 이제 loaded 변수는 저장하기 전의 state와 똑같은 속성값들을 가진 객체가 됩니다.
loaded = Marshal.load(File.binread("save.dat"))
puts loaded.level
puts loaded.score
puts loaded.items
# 💡 Marshal 사용 시 주의할 점 (꿀팁)
# 속도와 편리함: 별도로 JSON이나 CSV 형식을 설계할 필요 없이 객체를 통째로 던지면 되므로 매우 빠르고 편리합니다.
# 보안 주의: Marshal.load는 신뢰할 수 없는 소스(예: 인터넷에서 다운로드한 파일)로부터 데이터를 불러올 때 위험할 수 있습니다. 악의적인 코드가 실행될 수 있기 때문입니다.
# 버전 호환성: 루비 버전이 크게 바뀌거나 클래스 구조가 수정되면 예전에 저장한 Marshal 파일을 읽어오지 못할 수도 있습니다.
직렬화 포맷 비교
| 포맷 | 속도 | 가독성 | 사용 사례 |
|---|---|---|---|
| Marshal | 가장 빠름 | 바이너리(불가) | Ruby 내부 캐시 |
| JSON | 빠름 | 좋음 | API 통신, 웹 |
| YAML | 느림 | 매우 좋음 | 설정 파일, 소형 데이터 |
Ch 19. 패턴 매칭 (Pattern Matching)
Ruby 2.7+에서 도입된 패턴 매칭은 복잡한 조건 분기를 우아하게 처리합니다.
19.1 기본 패턴 매칭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
response = {
status: 200,
body: {
user: {
name: "Alice", age: 30
}
}
}
# 1. 구조 확인: response[:status]가 200인지 확인합니다.
# 2. 중첩 탐색: body 키 안에 user 키가 있고, 그 안에 다시 name 키가 있는지 깊숙이 들어갑니다.
# 3. 타입 검사 및 할당 (String => name):
# - name 키의 값이 String 타입인지 확인합니다.
# - 조건이 맞다면, 그 값을 name이라는 변수에 **바인딩(할당)**합니다.
# 4. 결과: response[:body][:user][:name]이 "Alice"이므로 조건이 충족되어 "사용자: Alice"가 출력됩니다.
case response
in { status: 200, body: { user: { name: String => name } } }
puts "사용자: #{name}"
in { status: 404 } # 단순히 status가 404인지만 확인합니다. body가 무엇이든 상관없이 status가 일치하면 매칭됩니다.
puts "찾을 수 없음"
in { status: 500, body: { error: String => msg } } # 상태가 500일 때, body[:error]의 내용을 msg라는 변수에 담아 출력합니다. 에러 메시지를 동적으로 처리할 때 유용합니다.
puts "서버 오류: #{msg}"
end
# Java switch expression과 비교
# Java (14+): switch(status) { case 200 -> ...; case 404 -> ...; }
# Ruby: case status; in 200 then ...; in 404 then ...
19.2 배열 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
# 배열 분해
case [1, 2, 3]
in [Integer => first, *rest]
puts "첫 번째: #{first}, 나머지: #{rest}"
end
# => 첫 번째: 1, 나머지: [2, 3]
# Find 패턴 (배열 내에서 찾기)
case [1, "hello", 3, "world", 5]
in [*, String => s, *]
puts "첫 번째 문자열: #{s}"
end
# => 첫 번째 문자열: hello
19.3 해시 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# API 응답 처리 예시 (Rails에서 매우 유용!)
def handle_api_response(response)
case response
in { success: true, data: { users: [*, { name:, email: }, *] } }
puts "사용자: #{name} (#{email})"
in { success: false, error: { code: Integer => code, message: } }
puts "오류 #{code}: #{message}"
in { success: true, data: nil }
puts "데이터 없음"
end
end
handle_api_response({
success: true,
data: { users: [{ name: "Alice", email: "a@x.com" }] }
})
19.4 한 줄 패턴 매칭 (Ruby 3.0+)
1
2
3
4
5
6
7
# => 연산자로 분해 할당
{ name: "Alice", age: 30 } => { name: }
puts name # => "Alice"
# in 연산자 (true/false 반환)
case_result = { status: 200 } in { status: 200 }
puts case_result # => true
Ch 20. 블록, Proc, Lambda
20.1 블록 (Blocks)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# do...end 블록
[1, 2, 3].each do |n|
puts n * 2
end
# {} 블록 (한 줄)
[1, 2, 3].each { |n| puts n * 2 }
# yield: 메서드 안에서 블록 실행
def execute
puts "시작"
yield # 블록 실행
puts "끝"
end
execute { puts "블록 실행!" }
# 시작
# 블록 실행!
# 끝
# yield에 인수 전달
def repeat(n)
n.times { |i| yield i }
end
repeat(3) { |i| puts "반복 #{i}" }
# 반복 0, 반복 1, 반복 2
# 블록 있는지 확인
def maybe_yield
if block_given?
yield
else
"블록 없음"
end
end
20.2 Proc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Proc: 블록을 객체로 저장
greeter = Proc.new { |name| puts "Hello, #{name}!" }
greeter.call("Alice") # => Hello, Alice!
greeter.("Bob") # => Hello, Bob! (call 생략)
greeter["Charlie"] # => Hello, Charlie! ([] 표기법)
# 메서드에 전달
def run_proc(p)
p.call("World")
end
run_proc(greeter)
# &로 블록에서 Proc 변환 및 전달
def capture(&block)
block # &block → Proc 객체
end
my_proc = capture { |x| x * 2 }
my_proc.call(5) # => 10
# Proc은 인수 개수에 관대함
loose = Proc.new { |a, b| puts "#{a}, #{b}" }
loose.call(1) # => "1, " (b는 nil)
loose.call(1, 2, 3) # => "1, 2" (3은 무시)
20.3 Lambda
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Lambda: 엄격한 Proc (메서드처럼 동작)
double = lambda { |n| n * 2 }
double.call(5) # => 10
# 화살표 표기법 (Lambda shorthand)
triple = ->(n) { n * 3 }
triple.call(5) # => 15
triple.(5) # => 15
add = ->(a, b) { a + b }
add.call(3, 4) # => 7
# Lambda는 인수 개수에 엄격
strict = ->(a, b) { puts "#{a}, #{b}" }
strict.call(1, 2) # OK
# strict.call(1) # ArgumentError: wrong number of arguments
# 배열과 함께 사용
[1, 2, 3, 4, 5].map { |n| n * 2 }
# 또는
doubler = ->(n) { n * 2 }
[1, 2, 3, 4, 5].map(&doubler) # => [2, 4, 6, 8, 10]
20.4 Proc vs Lambda 비교
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# return 동작 차이
def proc_return
p = Proc.new { return "Proc에서 return" }
p.call
"메서드 끝" # 실행 안 됨!
end
def lambda_return
l = -> { return "Lambda에서 return" }
l.call
"메서드 끝" # 실행됨!
end
puts proc_return # => "Proc에서 return" (메서드 즉시 종료)
puts lambda_return # => "메서드 끝" (lambda는 자신만 return)
| 특성 | Proc | Lambda |
|---|---|---|
| 인수 개수 | 관대함 | 엄격함 (메서드처럼) |
return 동작 | 포함한 메서드 종료 | lambda 자체만 종료 |
| 생성 방법 | Proc.new { } | lambda { } 또는 ->{ } |
| 타입 | Proc | Proc (lambda?) |
| 용도 | 블록을 객체로 저장 | 함수형 프로그래밍 |
20.5 실전 활용 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Symbol#to_proc: Rails에서 가장 많이 사용하는 패턴
users = ["alice", "bob", "charlie"]
users.map(&:upcase) # => ["ALICE", "BOB", "CHARLIE"]
users.map(&:capitalize) # => ["Alice", "Bob", "Charlie"]
# :메서드명 → proc으로 변환
# &:upcase 는 { |s| s.upcase } 와 동일
# Method 참조
def double(n)
n * 2
end
[1, 2, 3].map(&method(:double)) # => [2, 4, 6]
# 함수 합성 (Ruby 2.6+)
double = ->(n) { n * 2 }
increment = ->(n) { n + 1 }
double_then_increment = double >> increment # (n * 2) + 1
increment_then_double = double << increment # (n + 1) * 2
double_then_increment.call(5) # => 11
increment_then_double.call(5) # => 12
Part 5: 컴퓨터 과학 기초
Ch 21. 재귀 (Recursion)
21.1 재귀의 기본
재귀는 메서드가 자기 자신을 호출하는 기법입니다. 반드시 종료 조건(Base Case)을 가져야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 팩토리얼 (반복 vs 재귀)
# 반복문 방식
def factorial_iterative(n)
result = 1
n.downto(1) { |i| result *= i }
result
end
# 재귀 방식
def factorial_recursive(n)
return 1 if n <= 1 # Base case: 종료 조건
n * factorial_recursive(n - 1) # Recursive case
end
factorial_iterative(5) # => 120
factorial_recursive(5) # => 120
# 실행 흐름:
# factorial(5)
# = 5 * factorial(4)
# = 5 * (4 * factorial(3))
# = 5 * (4 * (3 * factorial(2)))
# = 5 * (4 * (3 * (2 * factorial(1))))
# = 5 * (4 * (3 * (2 * 1)))
# = 120
21.2 피보나치 수열
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 단순 재귀 (비효율적)
def fib_simple(n)
return n if n <= 1
fib_simple(n - 1) + fib_simple(n - 2)
end
# 메모이제이션 (캐싱으로 최적화)
def fib_memo(n, cache = {})
return n if n <= 1
cache[n] ||= fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
end
fib_simple(10) # 느림 (2^n 호출)
fib_memo(40) # 빠름 (n 호출)
21.3 실전 재귀: 파일 시스템 탐색
1
2
3
4
5
6
7
8
9
10
11
12
13
# 디렉토리 구조를 재귀로 탐색
def list_files(path, depth = 0)
indent = " " * depth
if File.directory?(path)
puts "#{indent}📁 #{File.basename(path)}"
Dir.entries(path).each do |entry|
next if entry.start_with?(".") # . 과 .. 제외
list_files(File.join(path, entry), depth + 1)
end
else
puts "#{indent}📄 #{File.basename(path)}"
end
end
Ch 22. 시간 복잡도
22.1 Big-O 표기법
시간 복잡도는 입력 크기 n에 따른 알고리즘의 실행 시간 증가율을 나타냅니다.
| Big-O | 이름 | 예시 |
|---|---|---|
| O(1) | 상수 시간 | 해시 키 접근, 배열 인덱스 접근 |
| O(log n) | 로그 시간 | 이진 탐색, 균형 이진 트리 |
| O(n) | 선형 시간 | 배열 순회, 선형 탐색 |
| O(n log n) | 선형 로그 | 병합 정렬, 퀵 정렬 |
| O(n²) | 이차 시간 | 버블 정렬, 중첩 루프 |
| O(2ⁿ) | 지수 시간 | 재귀 피보나치, 브루트포스 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# O(1) - 상수 시간
def first_element(arr)
arr[0] # 배열 크기와 무관
end
# O(n) - 선형 시간
def find_max(arr)
max = arr[0]
arr.each { |n| max = n if n > max } # n번 순회
max
end
# O(n²) - 이차 시간
def bubble_sort(arr)
n = arr.length
n.times do |i|
(n - i - 1).times do |j| # 중첩 루프 → O(n²)
arr[j], arr[j+1] = arr[j+1], arr[j] if arr[j] > arr[j+1]
end
end
arr
end
# O(log n) - 이진 탐색
def binary_search(arr, target)
low, high = 0, arr.length - 1
while low <= high
mid = (low + high) / 2
if arr[mid] == target
return mid
elsif arr[mid] < target
low = mid + 1
else
high = mid - 1
end
end
-1 # 없으면 -1
end
Ch 23. 공간 복잡도
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# O(1) 공간 - 추가 메모리 최소
def sum_in_place(arr)
total = 0 # 변수 하나만 추가
arr.each { |n| total += n }
total
end
# O(n) 공간 - 입력 크기에 비례
def double_all(arr)
arr.map { |n| n * 2 } # 새 배열 생성 → O(n) 공간
end
# 재귀의 공간 복잡도 (콜 스택)
def factorial(n)
return 1 if n <= 1
n * factorial(n - 1) # n번의 스택 프레임 → O(n) 공간
end
Ch 24. 자료 구조 및 알고리즘
24.1 연결 리스트 (Linked List)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Node
attr_accessor :value, :next_node
def initialize(value, next_node = nil)
@value = value
@next_node = next_node
end
end
class LinkedList
def initialize
@head = nil
end
def prepend(value)
@head = Node.new(value, @head)
end
def append(value)
if @head.nil?
@head = Node.new(value)
else
current = @head
current = current.next_node until current.next_node.nil?
current.next_node = Node.new(value)
end
end
def find(value)
current = @head
until current.nil?
return current if current.value == value
current = current.next_node
end
nil
end
def to_s
result = []
current = @head
until current.nil?
result << current.value
current = current.next_node
end
result.join(" -> ")
end
end
list = LinkedList.new
list.append(1)
list.append(2)
list.append(3)
puts list.to_s # => "1 -> 2 -> 3"
24.2 스택과 큐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 스택 (Stack) - LIFO: 배열로 구현
stack = []
stack.push(1) # push
stack.push(2)
stack.push(3)
stack.pop # => 3 (마지막 삽입이 먼저 나옴)
stack.last # => 2 (peek)
# 큐 (Queue) - FIFO: 배열로 구현
queue = []
queue.push(1) # enqueue
queue.push(2)
queue.push(3)
queue.shift # => 1 (먼저 삽입된 것이 먼저 나옴)
# Ruby 표준 라이브러리의 Queue
require 'thread'
q = Queue.new
q.push("작업1")
q.push("작업2")
q.pop # => "작업1"
24.3 해시맵 (HashMap)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Ruby의 Hash가 HashMap에 해당
# 평균 O(1) 삽입/조회/삭제
# 빈도 카운터 (파이썬의 Counter와 유사)
def frequency_count(arr)
arr.tally # Ruby 2.7+: { "a"=>3, "b"=>2, ... }
end
# Hash.new(0)을 이용한 카운터
def word_count(str)
counts = Hash.new(0)
str.split.each { |word| counts[word] += 1 }
counts
end
puts word_count("hello world hello ruby world hello").inspect
# => {"hello"=>3, "world"=>2, "ruby"=>1}
24.4 이진 탐색 트리 (BST)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class BSTNode
attr_accessor :value, :left, :right
def initialize(value)
@value = value
@left = nil
@right = nil
end
end
class BinarySearchTree
def initialize
@root = nil
end
def insert(value)
@root = insert_node(@root, value)
end
def include?(value)
search_node(@root, value)
end
private
def insert_node(node, value)
return BSTNode.new(value) if node.nil?
if value < node.value
node.left = insert_node(node.left, value)
elsif value > node.value
node.right = insert_node(node.right, value)
end
node
end
def search_node(node, value)
return false if node.nil?
return true if node.value == value
if value < node.value
search_node(node.left, value)
else
search_node(node.right, value)
end
end
end
bst = BinarySearchTree.new
[5, 3, 7, 1, 4, 6, 8].each { |n| bst.insert(n) }
bst.include?(4) # => true
bst.include?(9) # => false
부록: Rails 8 준비 체크리스트
Rails 8을 시작하기 전에 아래 개념들을 확실히 이해했는지 점검해보자.
필수 Ruby 개념 체크리스트
기초 문법
- 동적 타입 시스템과 모든 것이 객체임을 이해
nil의 truthy/falsy 동작 (특히 Java와의 차이)- 문자열 보간
"#{}"사용 - 심볼(
:symbol)의 역할과 해시 키로의 활용 ||=(조건부 할당) 패턴 이해
컬렉션
map,select,reject,reduce활용&:method_name단축 표현 이해- 해시 심볼 키 표기법 (
{name: "Alice"}) dig메서드로 중첩 해시 안전 접근
OOP
attr_accessor,attr_reader,attr_writer활용initialize생성자- 상속과
super키워드 include로 모듈 믹스인private/protected접근 제어
고급
- 블록, Proc, Lambda 차이
yield로 블록 실행- JSON 파싱 (
JSON.parse,JSON.generate) - 예외 처리 (
begin/rescue/ensure) - 패턴 매칭 기초
Rails 8에서 자주 만날 Ruby 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 1. 심볼 + 해시 (모든 곳에서 사용)
render json: { status: :ok, data: @users }
# 2. &:method 패턴
@users.map(&:email)
@posts.select(&:published?)
# 3. 조건부 할당
@user ||= User.find(params[:id])
# 4. 블록으로 설정
Rails.application.routes.draw do
resources :users
end
# 5. 체이닝
User.where(active: true)
.order(:name)
.limit(10)
.map(&:email)
# 6. 키워드 인수
def update_user(user, name:, email:, role: :user)
user.update!(name: name, email: email, role: role)
end
# 7. Proc/Lambda as 필터
ADMIN_FILTER = ->(user) { user.admin? }
active_admins = users.select(&ADMIN_FILTER)
추가 학습 자료
- The Odin Project — Ruby Course
- Ruby 공식 문서
- Ruby 스타일 가이드
- RubyGems — Ruby 패키지 생태계
이 글은 The Odin Project Ruby 커리큘럼을 기반으로, Java/Spring 백엔드 경험자를 위해 정리한 내용입니다. Rails 8 학습을 시작하기 전에 각 챕터의 연습 문제를 꼭 풀어보세요.