Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby 타입 변환 완벽 가이드: 명시적 캐스팅(#to_s)과 암시적 강제 변환(#to_str)의 차이

타입 강제 변환(type coercion)은 객체의 타입을 그 값을 함께 다루면서 다른 타입으로 바꾸는 과정을 말합니다. 예를 들어 #to_s로 Integer를 String으로 바꾸거나, #to_i로 Float을 Integer로 바꾸는 것이 대표적입니다. 일부 객체가 구현하고 있는 다소 생소한 #to_str#to_int 메서드도 얼핏 보면 같은 역할을 하는 것처럼 보이지만, 실제로는 몇 가지 중요한 차이점이 있습니다.

이번 AppSignal 아카데미에서는 Ruby에서 타입을 명시적으로 캐스팅하는 방법과 암시적으로 강제 변환하는 방법을 자세히 살펴보고, 재미로 배우의 타입캐스팅에도 비유해 보겠습니다. 두 방식의 차이점과 각각이 실제로 어떻게 사용되는지 함께 알아봅니다.

먼저 Ruby에서 명시적 캐스팅 헬퍼를 사용해 값을 다른 타입으로 변환하는 일반적인 방식부터 시작하겠습니다.

명시적 캐스팅 헬퍼 (Explicit Casting Helpers)

가장 널리 쓰이는 캐스팅 헬퍼는 #to_s, #to_i, #to_a, #to_h입니다. 이들은 모두 명시적(explicit) 캐스팅 메서드로, 값을 한 타입에서 다른 타입으로 손쉽게 변환할 수 있게 도와줍니다.

명시적 헬퍼에는 분명한 약속이 담겨 있습니다. 객체에 #to_s를 호출하면, 해당 객체가 문자열로 잘 변환되든 아니든 항상 문자열을 반환합니다. 마치 코미디 배우인 마이클 키턴을 배트맨 역에 캐스팅하는 것과 같습니다. 배역에 딱 맞는 배우가 아니더라도 어쨌든 배트맨은 얻게 되는 셈입니다.

Ruby는 표준 라이브러리의 거의 모든 기본 객체에 이런 헬퍼 메서드를 제공합니다.


특히 #to_s를 비롯한 이 메서드들은 Ruby의 대부분 기본 타입에 구현되어 있습니다. 캐스팅은 거의 항상 어떤 값을 반환하지만, 그 결과가 우리가 기대한 것과 다를 수 있다는 점에 주의해야 합니다.


#to_s, #to_i, #to_a, #to_h 헬퍼를 호출하면 값의 상태와 무관하게 무조건 선택한 타입으로 강제 변환합니다. 즉, 값이 어떻게 처리되든 변환된 타입의 표현(representation)을 반환합니다.

암시적 강제 변환 메서드 (Implicit Coercion Methods)

변환하려는 타입처럼 동작하지 않는 값에 캐스팅 메서드를 호출하면 오류가 발생하거나 데이터가 손실될 수 있습니다. Ruby는 객체가 해당 타입처럼 동작할 때만 값을 반환하는 암시적(implicit) 강제 변환 메서드도 제공합니다. 덕분에 값이 원하는 타입처럼 동작한다는 것을 확신할 수 있습니다. 이러한 암시적 강제 변환 메서드에는 #to_str, #to_int, #to_ary, #to_hash가 있습니다.

암시적 강제 변환은 레너드 니모이를 스팍 외의 배역에 캐스팅하는 것과 비슷합니다. 캐릭터가 스팍과 충분히 비슷하면 연기를 해낼 수 있지만, 그렇지 않으면 실패합니다. #to_str 헬퍼는 문자열로 변환을 시도하지만, 객체가 이 메서드를 구현하지 않아 암시적 변환이 불가능하면 NoMethodError를 발생시킵니다.


Ruby가 요청된 타입으로 무엇을 변환하고 무엇을 변환하지 않을지 훨씬 엄격해진 것을 확인할 수 있습니다. 변환이 불가능하다면 객체에 해당 #to_* 메서드가 구현되어 있지 않은 것이므로, 호출 시 NoMethodError가 발생합니다.

#to_str 같은 암시적 강제 변환을 사용할 때는, 원래 타입 역시 String처럼 동작하는 경우에만 String 객체를 반환하도록 요청하는 것입니다. 이런 이유로 Ruby 표준 라이브러리에서 #to_str은 String에만 구현되어 있습니다.

Ruby 내부에서 암시적 강제 변환이 사용되는 경우

변환 요청을 더 정밀하게 할 수 있다는 점 외에, 암시적 강제 변환은 또 어떤 용도로 유용할까요? 흥미롭게도 Ruby 자체도 여러 상황에서 암시적 강제 변환을 활용합니다. 예를 들어 + 연산자로 객체를 결합할 때가 그렇습니다.


여기서 Ruby는 Name 타입을 String으로 암시적으로 변환할 수 없기 때문에 TypeError를 발생시킵니다.

클래스에 #to_str을 구현하면 Ruby는 Name 타입을 어떻게 변환해야 하는지 알게 됩니다.


배열에서도 #to_ary를 통해 같은 방식이 동작합니다.


그런데 #to_ary는 더 많은 곳에서 활용됩니다. 배열을 여러 개의 개별 변수로 분해(destructure)하는 데도 사용할 수 있습니다.


객체를 블록 매개변수로 변환할 때도 마찬가지입니다.


그 밖에도 암시적 강제 변환 메서드가 사용되는 사례가 있습니다. 예를 들어 **와 함께 쓰이는 #to_hash입니다. 이는 값을 parse_options 메서드에 전달하기 전에 #to_hash로 해시로 강제 변환합니다.


타입 강제 적용하기 (Enforcing Types)

Ruby는 값의 타입을 알 수 없고 올바른 타입을 확실히 받고 싶을 때 사용할 수 있는 더 견고한 강제 변환 메서드도 제공합니다. 모든 기본 타입마다 하나씩 존재합니다(String(...), Integer(...), Float(...), Array(...), Hash(...) 등).


String(...) 메서드는 먼저 값에 #to_str 호출을 시도하고, 실패하면 #to_s를 호출합니다. 모든 객체가 #to_str 메서드를 정의하는 것은 아니기 때문에, 암시적 강제 변환(#to_str)과 명시적 캐스팅(#to_s) 두 가지를 모두 확인하면 문자열 변환이 성공할 가능성이 높아지고 원하는 값을 얻을 수 있습니다. 암시적 강제 변환을 먼저 시도하면 "#<Object:0x00007f8e6d053a90>" 같은 결과가 아니라, 같은 값을 가지면서 변환된 타입의 결과를 얻을 가능성이 높습니다.


암시적 캐스팅 메서드는 변환될 타입처럼 동작하는 객체에만 구현해야 합니다. 예를 들어 직접 만든 String 클래스라면 #to_str을 구현하는 식입니다.

암시적 강제 변환을 먼저 시도하는 것 외에도, String(...) 헬퍼는 반환된 타입까지 검사합니다. #to_str은 단순한 메서드일 뿐이라 String이 아닌 어떤 타입의 값도 반환할 수 있습니다. 요청된 타입의 값을 확실히 받기 위해 String(...)은 타입이 일치하지 않으면 TypeError를 발생시킵니다.


여기서 Ruby가 #to_str의 결과를 무시하는 것을 볼 수 있는데, String 타입이 아닌 nil을 반환했기 때문입니다. 대신 #to_s의 결과로 폴백(fallback)합니다.

만약 #to_s마저 nil을 반환해 올바른 타입이 아니라면, String(...)TypeError를 발생시킵니다.


타입 강제 변환을 더 안정적으로 수행할 수 있긴 하지만, 캐스팅 헬퍼 메서드(String(...), Integer(...) 등)는 전달된 값에 대해 더 많은 검사를 수행해야 하므로 일반적으로 약간 느리다는 점에 유의하세요.

마치며

객체가 올바른 타입의 데이터를 다루고 있는지 확인하고 싶을 때 타입 강제 변환은 매우 유용한 과정입니다. 이번 글에서는 #to_s, #to_i, #to_a, #to_h 같은 명시적 캐스팅 헬퍼에 대한 지식을 되새겼습니다. 또한 #to_str, #to_int, #to_ary, #to_hash 같은 암시적 헬퍼가 유용한 상황과 Ruby 자체가 이를 활용하는 방식도 살펴보았습니다.

이번 타입 강제 변환 개요가 도움이 되었고, 배우 타입캐스팅 비유도 재미있었다면 좋겠네요. 언제든 다루었으면 하는 주제가 있다면 알려주세요. 질문이나 의견이 있다면 @AppSignal로 편하게 연락해 주세요.

Ruby 타입 변환 완벽 가이드: 명시적 캐스팅(#to_s)과 암시적 강제 변환(#to_str)의 차이

Tom de Bruijn