mutable string

uchan·2023년 4월 17일
0

배경

java 와 python 을 공부했던 나는 현재 ruby on rails 로 어플리케이션을 관리하고 있다. python 과 비슷하면서도 다른 ruby 언어의 자유도와 패턴 매칭 기법은 매우 놀라웠다. 그러나 모듈 및 singleton 구조 등을 이해하기는 어려웠다. 지금 ruby 를 다룬지 1년이 넘었지만 자신있는 언어라 하면 ruby 를 언급할 수는 없을 거 같다. 이번에 작업하면서 맞닥뜨린 이슈가 있다. 해당 이슈를 혼자 해결못하였고 다른 팀원들의 도움을 받아 장작 6시간 넘게 삽질한 결과, 아주 놀라운 걸 발견했다(놀랍기보다는 무서운 쪽에 가까운 듯).

Setting

ref: https://github.com/rubyconfig/config
ruby on rails 에서 설정 값을 관리하는데 유용하게 사용되는 젬인 config!
이를 이용하면 다음과 같이 세팅 파일을 만들고 사용할 수 있다.

  • setting file
# config/setting.yml

...
groups:
	vip:
    	namespace: very_important_person
...
  • ruby code
class Person
...
	def send_mail
    	namespace = Settings.groups.vip.namespace.camelize # 'very_important_person' -> VeryImportantPerson
        ...
	end
end

bang(!) method

또 하나 루비에서 자주 사용되는 방법 중 하나인 bang method!
메서드 뒤에 ! 를 붙여주면 메서드의 결과 값을 호출한 자신에게 적용시킨다.

namespace = 'very_important_person'
namespace2 = namespace.tr('_', '-')
puts namespace # very_important_person
puts namespace2 # very-important-person

namespace.tr!('_', '-')
puts namespace # very-important-person

사건 발생

사이드킥으로 워커를 띄우고여러 잡을 돌리던 중 이상한 현상을 목격했다. AWS cloudwatch metric 에 지표를 보내는 잡이 갑자기 이상한 namespace 로 metric 을 전송하는 것이였다. 아래는 해당 사건을 토대로 재구성한 코드이다.

# config/setting.yml
school:
	metrics:
		namespace: student_count
# app/jobs/my_job.rb
class MyJob < ApplicationJob
	attr_accessor :namespace
    
	def perform
    	namespace = Settings.school.metrics.namespace
    	aws_client.put_metric(
        	namespace: namespace.camelize # studentCount 로 메트릭을 쏴줄 것으로 예상
            ...
        )
    end
end

당연히 MyJob 에서 'studentCount' 로 메트릭을 전송할 것으로 예상했으나, 이게 웬걸 'Student-count' 네임스페이스로 메트릭이 전송되었다. 이건 세팅 값을 kebab -> camel 케이스로 해야지 나오는 값인데 이렇게 만드는 코드를 도저히 찾을 수가 없었다.

6시간을 삽질하던 중, 한 팀원이 다른 잡에 의심쩍은 코드를 찾아냈다.

# app/jobs/my_job2.rb
class MyJob2 < ApplicationJob
	attr_accessor :namespace
    
	def perform
    	namespace = Settings.school.metrics.namespace
    	namespace.tr!('_', '-')
        ...
    end
end

도대체 MyJobMyJob2 는 어떻게 관련이 있는걸까?

루비에서 문자열은 mutable

사이드킥 워커는 프로세스를 띄우고 처리 가능한 쓰레드만큼 잡을 동시에 처리할 수 있다. 잡이 처리되더라도 TERM 시그널을 받지 않는 한 그대로 프로세스가 유지되고, 이후 다른 잡 처리 요청이 오면 바로 처리가 가능하다. 위 MyJobMyJob2 또한 같은 프로세스에서 실행될 것이다.

MyJob2 의 코드를 자세히 들여다 보자.
1. namespace 변수에 Settings.school.metrics.namespace 값을 할당받는다.
2. namespace 변수에 담긴 값에서 _- 로 치환하여 다시 namespace 에 할당받는다.

여기서 namespace 에 담긴 문자열은 당연히 immutable 할 줄 알았다. 그러나 루비에서는 그게 아니였던 것이다..... 즉, 루비에서는 문자열이 mutable 하다. 즉, namespace 에 세팅 값을 할당하였는데, 이때 값이 복사되어 할당되는게 아니라 namespaceSettings.school.metrics.namespace 가 같은 곳을 참조하고 있는 것이다. 파이썬이라면

a = 'abc'
b = a
b += 'd'
print(a) # 'abc'
print(b) # 'abcd'

이지만 루비는 a 도 abcd 가 나온다는 것이다.

따라서 namespace.tr!('_', '-') 를 호출한 순간 Settings.school.metrics.namespace 값도 변경되어 다음 MyJob 을 호출할 때 변경된 세팅 값으로 들어가게 된다.

정리

  • 루비에서 문자열은 mutable
  • bang method 사용할 때 조심할 것

0개의 댓글