여느때와 같이 룰루랄라 프로그래밍을 하며 즐거운 시간을 보내던 어느날
생각지도 못한 버그가 발생하여 회고를 위해 블로그를 작성한다.
🔥이슈 발생 상황
OCR 제품의 특성상 모델에서 추론한 결과가 key/value를 갖는 dictionary 형태로 핸들링 하는 경우가 많다.
이번에 이슈가 났던 케이스를 간단하게 설명하자면 클래스 변수에 dictionary를 담을 수 있는 변수를 선언하고 for문을 돌면서 인스턴스를 생성하고 클래스 변수에 동일한 키값과 동적으로 바뀌는 value를 넣어서 저장하였다.(실제로는 이것보다 훨씬 복잡한 로직이 있다.)
class MyClass:
my_dict = dict()
def add_dict(self, key: str, value: int):
self.my_dict.update({
key: value
})
def get_my_dict(self):
return self.my_dict
my_class_list = []
for i in range(0, 3):
my_class = MyClass()
my_class.add_dict("my_key", i)
my_class_list.append(my_class)
for my_class in my_class_list:
print(my_class.get_my_dict())
위의 코드를 실행하면 아래와 같은 결과를 확인할 수 있다.
무엇이 문제인지 알겠는가?!
인스턴스를 계속 생성하여 인스턴스마다 클래스 변수를 업데이트 해줬지만 마지막 업데이트가 된 값으로 모든 인스턴스들의 값이 동기화가 되었다!
❓왜 발생했을까?
가장 먼저 놓쳤던점은 '클래스변수는 모든 인스턴스끼리 공유를 한다' 라는 개념이다.
이것을 이해하기 위해서는 기본적으로 파이썬은 모든것이 객체로 이루어지고 심지어 인스턴스를 생성하지 않고 클래스를 정의하는것 자체도 객체로서 힙 영역에 할당된다는 점을 알아야 한다.
클래스의 정의가 객체?
파이썬에서 클래스는 일급 객체(first-class object)로 간주된다. 이는 클래스 자체가 변수에 할당될 수 있고, 함수의 인수로 전달될 수 있으며, 함수의 반환 값으로 사용될 수 있다는것을 의미한다. 따라서 클래스를 정의하는 순간 해당 클래스에 대한 메모리 할당이 이루어지며 클래스변수 및 함수들도 메모리 할당이 이루어진다.
class MyClass:
class_variable = "Hello"
def test_func(self):
pass
print(MyClass.class_variable)
print(id(MyClass.class_variable))
print(id(MyClass.test_func))
위 코드에서 인스턴스를 생성하지 않고도 클래스변수 및 함수에 접근이 가능한 이유는 클래스가 정의된 순간 힙 메모리에 할당
이 이루어졌기 때문이다.
다음으로는, 네임스페이스 개념도 알아야 한다.
a=2
라고 하면 a라는 변수가 2라는 객체가 저장된 주소를 가지고 있는데, 이러한 연결 관계가 저장된 공간이 바로 네임스페이스
다.파이썬에서는 클래스와 인스턴스 각각 독립적인 네임스페이스를 가진다.
그렇기 때문에 아래와 같이 인스턴스의 네임스페이스에만 인스턴스 변수가 저장되어 있다.
그리고 인스턴스를 통해 속성(변수나 메서드)에 접근할 때, 파이썬은 먼저 해당 인스턴스의 네임스페이스를 확인한 후, 만약 해당 속성이 인스턴스 네임스페이스에 없다면 클래스 네임스페이스를 확인하게 된다. 이러한 특징으로 인스턴스를 통해 클래스 변수에도 접근
할 수 있다.
class MyClass:
class_variable = 1 # 클래스 네임스페이스에 저장됨
def __init__(self):
self.instance_variable = 2 # 인스턴스 네임스페이스에 저장됨
obj = MyClass()
print(obj.class_var) # 클래스 네임스페이스의 속성에 접근
print(obj.instance_var) # 인스턴스 네임스페이스의 속성에 접근
다시 문제 상황을보자,
먼저 나는 for문을 돌면서 my_class
라는 인스턴스를 생성하고 클래스에 선언된 메서드 add_dict()
에게 메세지를 보냈다.
메서드 add_dict()에서는 my_dict
라는 변수에 특정 key값과 value를 가지고 업데이트를 시도하는데, 먼저 인스턴스 네임스페이스
에서 my_dict를 찾지만 존재하지 않기때문에 클래스 네임스페이스
에서 my_dict를 찾았다.
이러한 이유로 생성된 모든 인스턴스들은 하나의 주소값을 가지는 MyClass
의 클래스변수인 my_dict에 같은 key값으로 업데이트를 하였고, 결국 모든 인스턴스는 for문의 마지막 value로 업데이트가 된 my_dict를 가지게 된것이다.
✅ 해결방법
사실 해결방법은 정말 간단하다. 바로 클래스 변수가 아닌 인스턴스 변수
로 선언을 하면 되는것이다.
class MyClass:
def add_dict(self, key: str, value: int):
# 클래스 변수가 아닌 인스턴스 변수 사용
self.my_dict = dict({
key: value
})
def get_my_dict(self):
return self.my_dict
이것만으로 우리가 원하는 값을 구할 수 있다.
공유해야 하는 값
에 사용을 하면 매번 같은 값을 인스턴스마다 생성하는 것보다 메모리 절약을 할 수 있다.생성 횟수를 추적
하는 경우(싱글톤 패턴)에도 사용이 가능하며 클래스에 관련된 상수
값을 정의해야 할 때 사용된다.❗여기서 끝이 아니다!
이번 블로그 작성을 위해 공부하고 테스트 하던 중 하나 더 재미있는것을 발견하였다.
분명 위에서 모든 인스턴스는 클래스 변수를 공유한다고 하였다.
class MyClass:
my_int = 1
def add_value(self, value: int):
self.my_int += value
def get_my_int(self):
return self.my_int
my_class_list = []
for i in range(0, 3):
my_class = MyClass()
my_class.add_value(i)
my_class_list.append(my_class)
for my_class in my_class_list:
print(f"my_int: {my_class.get_my_int()}")
위 코드는 클래스 변수를 int형으로 변경하고, 인스턴스를 생성할 때 마다 동적으로 클래스 변수에 값을 더해주는 코드다.
이전 상황을 생각해 본다면
'모든 인스턴스들은 클래스 변수를 공유하니깐 최종적으로 모든 인스턴스에서 my_int값이 3이 되겠군!'
이라고 생각할 수 있다.
하지만 실제 결과는 아래와 같다.
예상과는 다르게, 각각의 인스턴스에 클래스 변수가 다른값을 사용하고 있다.
왜 인스턴스들이 클래스 변수를 공유하지 않는걸까?
👉가변객체(mutable object)와 불변객체(immutable object)
앞서 말했듯이, 파이썬은 모든것이 객체로 이루어져 있다. 그리고 이 객체들은 가변객체(mutable object)
와 불변객체(immutable object)
로 나뉜다. 이 두 가지의 차이점은 바로 객체의 내용이 변경 가능한지 여부이다.
가변객체는 객체가 생성된 후에도 그 내용을 변경할 수 있다. 이러한 특성 때문에 해시가 불가능하고, 그러므로 딕셔너리의 키로 사용할 수 없다.
파이썬에서 주요 가변객체 유형은 아래와 같다.
list
: 리스트의 요소는 변경이 가능하다. 아이템 추가, 삭제, 변경이 가능dict
: 딕셔너리의 키나 값을 추가, 삭제, 변경할 수 있다.set
: 세트의 아이템은 추가, 삭제가 가능하다.bytearray
: 변경 가능한 바이트 시퀀스이다.불변객체는 객체가 한 번 생성되면 그 내용을 변경할 수 없다. 이런 특성 때문에 불변객체는 해시가 가능하고, 딕셔너리의 키로 사용이 가능하다.
파이썬에서 주요 불변객체 유형은 아래와 같다.
int
: 정수는 불변이다. 즉 숫자를 변경하는 연산을 수행하면 새로운 객체가 생성된다.float
: 부동소수점도 정수와 같이 불변이다.str
: 문자열 역시 불변이다. 문자열을 변경하면 항상 새로운 문자열 객체가 생성이 된다.tuple
: 튜플은 불변의 리스트와 같다. 한번 생성된 튜플은 변경할 수 없다.frozenset
: 변경이 불가능한 세트이다.불변성 != 고정된 크기
: 불변성이라 해서 꼭 고정된 크기를 가지는것은 아니다. 예를 들어, 튜플은 불변객체이지만 튜플의 요소로 리스트를 포함할 수 있다. 이 리스트는 변경이 가능하며, 따라서 튜플의 내용은 간접적으로 변경이 될 수 있다.불변객체와 가변객체의 혼합 사용
: 불변객체 안에서 가변객체를 포함하여 사용할 수 있다. 위의 예시와 같이 튜플에 리스트를 포함하는 경우가 이에 해당된다.이러한 가변객체와 불변객체의 특성을 숙지하고, 다시 실험한 결과를 살펴보자.
처음 실험에서는 클래스 변수에 dict형의 my_dict라는 이름을 가진 가변객체를 선언하였고,
생성한 각각의 인스턴스에서 my_dict에 접근을 하는데 인스턴스 네임스페이스에는 해당 이름이 없기 때문에 클래스 네임스페이스에서 해당 객체를 찾는다. 가져온 dict 객체를 수정하므로써 모든 인스턴스에는 같은 수정 결과가 반영이 된것이다.
이제 두번째 실험의 경우를 보자. 클래스 변수에 int형의 my_int라는 이름을 가진 불변객체를 선언하였다.
생성한 각각의 인스턴스에서 my_int에 접근을 하는데, 네임스페이스에 해당 이름이 없기 때문에 클래스 네임스페이스에서 해당 객체를 찾는다.
가져온 int 객체를 수정하려 하는데, int는 불변객체 이므로 새로운 값이 Heap 영역에 할당된다. 새로운 값이 할당되면서 해당 객체는 클래스 변수가 아닌 인스턴스 변수로 생성이 되기 때문에 인스턴스 네임스페이스에 my_int가 매핑이 된다.
그 후 각각의 인스턴스에서 my_int에 접근을 할 때, 인스턴스 네임스페이스에서 매핑이 된 객체를 가져오기 때문에 값이 다르게 나타난다.
이번 포스팅을 통해 객체에 관하여 조금은 더 자세하게 알 수 있는 좋은 기회가 되었다.
조금 더 효율적이고 안정적인 제품을 만들 수 있도록 초석을 닦는 계기가 되었으면 좋겠다!
Working with Spring layers like DAO, DTO, Repository and Entity is all about assembling the right pieces so your application behaves predictably.
indovina la parola 5 lettere
(Wordle Unlimited) is a free online word game site offering unlimited Wordle puzzles, multi-language support, and varied word-length modes for fun and learning — a handy little break that trains the same pattern-recognition skills you use when mapping DTOs to entities.
Taking a short Wordle round can refresh your focus and sharpen the mental routines you need for debugging complex data flows
파이썬 별로네요