Ruby에는 흥미로운 연산자가 정말 많습니다.
대표적인 예는 다음과 같습니다:
- 우주선 연산자 (
<=>) - 나머지 대입 연산자 (
%=) - 삼중 등호 (
===) 연산자 - 크다 (
>) & 작다 (<) - 같지 않음 (
!=)
그런데 많은 분들이 놓치는 사실이 있습니다. 이 연산자들 중 상당수는 사실 Ruby 메서드라는 점입니다.
이것이 의미하는 바는…
연산자의 동작을 직접 재정의(오버라이드)해서, 여러분이 만든 클래스에 맞는 커스텀 동작을 정의할 수 있다는 것입니다.
예를 들어 == 메서드를 정의하면, 같은 클래스의 두 객체를 어떻게 비교할지 Ruby에게 직접 알려줄 수 있습니다.
자, 그럼 몇 가지 예제를 살펴보면서 Ruby 연산자가 어떻게 동작하는지, 그리고 코드에서 어떻게 활용하는지 확실하게 익혀보겠습니다.
Ruby를 제대로 이해하고 싶다면 반드시 짚고 넘어가야 할 주제입니다.
Ruby 논리 연산자
먼저 논리 연산자부터 살펴보겠습니다.
논리 연산자는 두 객체를 비교하고, 그 결과를 바탕으로 판단을 내릴 때 사용합니다.
정리하면 다음 표와 같습니다:
| 연산자 | 설명 |
|---|---|
| < | 작다 |
| > | 크다 |
| >= | 크거나 같다 |
| <= | 작거나 같다 |
| == | 같다 |
| != | 같지 않다 |
| <=> | 크거나, 같거나, 작음을 반환 |
이 연산자들은 모두 메서드이며, 우주선 연산자(<=>)를 제외하고 불리언 값을 반환합니다. 우주선 연산자는 크면 1, 같으면 0, 작으면 -1을 반환합니다.
> 연산자 사용 예시입니다:
if orange.stock > 20 # ... end
직접 만든 클래스에서 등호 두 개(==) 연산자를 사용하면, 처음에는 의도대로 동작하지 않는다는 것을 발견하게 될 겁니다.
예제:
class Fruit
def initialize(name)
@name = name
end
end
orange1 = Fruit.new("orange")
orange2 = Fruit.new("orange")
orange1 == orange2
# false
그 이유는 ==의 기본 구현인 BasicObject#==가 object_id 메서드를 사용해 두 객체가 물리적으로 동일한 객체인지 확인하기 때문입니다.
다음과 같이 수정할 수 있습니다:
class Fruit
attr_reader :name
def initialize(name)
@name = name
end
def ==(other)
name == other.name
end
end
여기서 우리는 "두 과일이 같다"는 것이 무엇을 의미하는지 직접 정의한 것입니다.
즉, 이름이 같으면 같은 과일로 판단하는 것이죠.
Ruby 산술 연산자
다음은 산술 연산자입니다.
특별히 새로운 내용은 없지만…
5 + 5 # 10 10 * 2 # 20 10 ** 2 # 100
== 연산자와 마찬가지로, 이들 역시 전부 메서드입니다.
그래서 유용한 점은, "두 객체를 더한다"는 것이 무엇을 의미하는지 직접 정의할 수 있다는 것입니다.
예를 들어 두 개의 Order(주문) 객체가 있을 때, 둘을 더하면 지불해야 할 총액이 나오도록 하거나, 두 주문을 합친 새로운 주문 객체가 나오도록 만들 수 있습니다.
+ 메서드를 정의하면 원하는 동작을 정확히 구현할 수 있습니다.
또 하나 생소할 수 있는 연산자가 바로 모듈로(나머지) 연산자입니다.
백분율 기호(%)처럼 생겼으며,
나눗셈의 나머지를 구해줍니다.
예제:
10 % 2 # 0
모듈로 연산자는 실용적인 용도가 많습니다. 숫자가 짝수인지 홀수인지 판별하거나, 어떤 수가 다른 수로 나누어떨어지는지 확인하거나, 값의 범위를 특정 한도 안으로 제한하는 등 다양하게 활용됩니다.
대입 연산자 (==, +=, ||=)
다음은 대입 연산자입니다. 지금까지 본 연산자들과 달리, 대입 연산자는 메서드가 아닙니다.
기본 대입 연산자는 다음과 같습니다:
a = 1
그리고 복합 대입 연산자도 있습니다:
a += 5 # 6 a *= 2 # 12
복합 대입 연산자는 현재 값을 읽어와서 산술 연산자 중 하나로 계산한 뒤 결과를 저장하는 것과 동일합니다. 모듈로 연산자(%)를 포함한 모든 산술 연산자에 적용할 수 있습니다.
하지만 조금 다르게 동작하는 대입 연산자가 두 개 있습니다!
바로 ||=와 &&=입니다.
이들은 산술 연산자 버전과 동일하지 않기 때문에 특별합니다.
a ||= 100이 하는 일은 다음과 같습니다:
"
a가 아직 정의되지 않았거나false또는nil이라면100을 대입하고, 그렇지 않으면a의 기존 값을 그대로 반환한다"
가장 근사한 동등 표현식은 다음과 같습니다:
(defined?(a) && a) ? a : a = 100
이 연산자는 느린 계산 결과나 API 요청 결과를 저장해 둘 때 유용합니다. 이런 기법을 "메모이제이션(memoization)"이라고 부릅니다.
단항 연산자란 무엇일까?
지금까지 본 연산자들은 모두 두 개의 값을 다루었지만, 값 하나만 다루는 연산자도 있습니다. 이런 연산자를 "단항 연산자(unary operator)"라고 부릅니다.
예를 들면:
+"abc"
이 코드는 frozen(동결된) 문자열의 변경 가능한 복사본을 생성합니다.
단항 연산자(+ / -)도 직접 정의할 수 있는데, 약간 특별한 문법이 필요합니다.
예제:
class String
def +@
frozen? ? self.dup : self
end
end
str = "abc".freeze
p (+str).frozen?
# false
단항 연산자의 우선순위 때문에 괄호를 사용해야 했습니다.
또한 !!도 있는데, 이것은 메서드가 아닙니다:
!!123 # true !!nil # false
어떤 값이든 불리언으로 변환해 주기 때문에 매우 유용합니다.
그리고 !는 동일한 방식으로 동작하지만 반대의 불리언 값을 반환합니다.
예제:
!true # false !!true # true !false # true
Ruby 스플랫 연산자 (예제 포함)
스플랫 연산자(*)는 이 연산자 없이는 불가능한 일을 해내기 때문에 흥미롭습니다.
다음과 같은 배열이 있다고 가정해 보겠습니다:
attributes = [:title, :author, :category]
그리고 이 배열을 attr_reader처럼 가변 인자를 받는 메서드에 넘기고 싶다고 해봅시다.
이럴 때 이렇게 하면 됩니다:
attr_reader *attributes
스플랫 연산자는 배열을 요소들의 목록으로 펼쳐 줍니다. 즉, 배열을 벗겨내고 그 안에 담긴 모든 요소로 대체하는 것과 같습니다.
다시 말해, 위의 마지막 예제는 다음과 같이 변환됩니다:
attr_reader :title, :author, :category
이것이 바로 스플랫 연산자의 힘입니다 🙂
매칭 연산자 (=~)
물결표(~)가 붙어 있는 이 독특한 Ruby 연산자(=~)는 무엇일까요?
바로 매칭(matching) 연산자입니다!
정규표현식을 사용해 빠르게 인덱스 검색을 할 수 있게 해줍니다.
예제를 보겠습니다:
"3oranges" =~ /[0-9]/ # 0
숫자를 찾아서, 첫 번째 매칭이 발견된 문자열 내 인덱스를 반환합니다. 매칭이 없으면 nil을 반환합니다.
추가로 !~ 연산자도 있는데, 이것은 "매칭하지 않음(NOT match)" 연산자입니다.
예제:
"abc" !~ /[0-9]/ # false
이 연산자는 인덱스가 아니라 true 또는 false를 반환한다는 점을 기억하세요.
Ruby 삼항 연산자 (물음표 연산자)
간결하고 짧은 코드를 좋아한다면 Ruby의 삼항 연산자를 사랑하게 될 겁니다.
if/else 문을 한 줄로 압축해서 쓰는 방법입니다.
형태는 다음과 같습니다:
condition ? true : false
예제:
"".size == 0 ? "Empty string" : "Non-empty string"
동작 방식은 이렇습니다:
삼항 연산자의 첫 번째 부분은 조건을 정의합니다 ("".size == 0).
그다음 물음표 기호(?)가 옵니다.
물음표 뒤에는 조건이 참일 때 반환할 값이 옵니다.
그다음 콜론(:)이 오고,
마지막 부분은 조건이 거짓일 때 반환할 값입니다. 완전한 조건문으로 치면 else에 해당하는 부분입니다.
셔블 / 푸시 연산자 (<<)
이 연산자(<<) 역시 메서드이기 때문에, 어떤 객체에 사용하느냐에 따라 동작이 달라집니다.
예를 들어 배열에서는 단순히 push 메서드의 별칭(alias)입니다.
animals = [] animals << "cat"
문자열에서는 끝에 내용을 덧붙입니다:
"" << "cat"
그리고 Integer(정수)에서는 "왼쪽 시프트(left shift)", 즉 모든 비트를 왼쪽으로 회전시키는 연산을 수행합니다.
2 << 1 # 4 2 << 2 # 8 2 << 3 # 16
삼중 등호 연산자 (등호 그 이상)
오늘 소개할 마지막 연산자는 삼중 등호 연산자(===)입니다.
이것 역시 메서드이며, 생각지도 못한 곳에서 등장합니다.
예를 들어 case 문에서 말이죠:
case "bacon" when String puts "It's a string!" when Integer puts "It's an integer" end
Ruby는 여기서 클래스에 대해 === 메서드를 호출하고 있습니다.
구체적으로는 이렇습니다:
String === "bacon"
이 코드는 현재 클래스와 다른 객체의 클래스를 비교합니다.
즉, 이 연산자의 목적은 case 문이라는 맥락에서 '동등함'을 정의하는 것입니다.
안전 내비게이션 연산자 (&.)
어떤 객체에 메서드를 호출하고 싶은데, 그 객체가 nil일 수도 있습니다. nil에 메서드를 호출하면 에러가 발생하기 때문에 곤란하죠.
한 가지 해결책:
if user && user.active # ... end
더 좋은 방법:
if user&.active # ... end
이 &.가 바로 안전 내비게이션 연산자(Ruby 2.3에서 도입)로, user가 nil이 아닐 때만 active 메서드를 호출합니다.
정말 유용하죠!
연산자 우선순위 표
Ruby는 수학에서 곱셈과 괄호가 먼저 계산되는 것처럼, 우선순위 목록에 따라 소스 코드를 평가합니다.
이 우선순위를 제대로 이해하지 못하면 각종 오류의 원인이 될 수 있습니다.
우선순위가 높은 것부터 낮은 순으로 정리한 표입니다:
| 연산자 |
|---|
| !, ~, 단항 + |
| ** |
| 단항 - |
| *, /, % |
| +, - |
| <<, >> |
| & |
| |, ^ |
| >, >=, <, <= |
| <=>, ==, ===, !=, =~, !~ |
| && |
| || |
| ?, : |
| modifier-rescue |
| =, +=, -=, *=, /=, %= |
| defined? |
| not |
| or, and |
| modifier-if, modifier-unless, modifier-while, modifier-until |
| { } 블록 |
| do ... end 블록 |
modifier-something은 해당 키워드의 한 줄짜리(one-liner) 형태를 의미합니다.
예제:
puts "awesome" if blog_name == "rubyguides"
블록 우선순위가 의외로 동작하는 예제를 하나 보겠습니다:
# 대문자로 변환된 배열을 반환
p ["a", "b", "c"].map { |character| character.upcase }
# Enumerator 객체를 반환
p ["a", "b", "c"].map do |character|
character.upcase
end
첫 번째 경우는 기대대로 동작하지만, 두 번째 경우에는 블록의 우선순위가 낮아서 map이 블록이 없다고 판단하고 Enumerator를 반환하게 됩니다.
정리
이번 글에서는 산술 연산자부터 논리 연산자, 그리고 다소 생소한 단항 연산자까지 Ruby의 다양한 연산자를 살펴보았습니다.
이 연산자들 중 상당수는 실제로 메서드이므로, 여러분이 만든 클래스에서 직접 구현할 수 있다는 점도 배웠습니다.
이 글이 도움이 되고 재미있었다면 좋겠네요!
읽어주셔서 감사합니다 🙂