Codong's Development Diary RSS 태그 관리 글쓰기 방명록
python/기타 (1)
2021-11-18 16:39:41

🙋‍♂️ 개요


프로그래밍 공부를 하는 사람들이면 얕은 복사, 깊은 복사에 대해 들은 적이 있을 것이다. 나도 그랬었고 얕은 복사는 주소 값을 복사하여 하나가 변경되면 같이 변경되는 것으로 대강 알고 있었다. 이것이 중요하다고 하는 것을 많이 봤었는데, 나는 역시 직접 겪기 전까지 모르는 것 같다. 그래서인지 업무 중에 기어코 일을 내고 말았다.



🚨 상황


어떤 데이터를 보내는데 필요없는 키워드를 걸러주기 위해 filter keyword 모듈을 만들어서 키워드 필터링을 하고 있었다. 단순히 검색 단어에 따라 등록해둔 필터링할 키워드를 가져와서 공통 필터링할 키워드와 합쳐서 키워드를 걸러주는 로직이었다. 하지만 처음엔 검색 키워드에 필터링 키워드가 지워져서 정상적으로 작동했는데, 가면 갈수록 검색 키워드에 상관없이 등록해뒀던 모든 키워드로 필터링이 되는 것이다. 코드로 보면 더 이해가 빠르다.

FILTER_KEYWORD = {
    '공통' : {'ㅋ', 'ㅎ', '.', '?'},
    '커피' : {'스타벅스', '할리스', '커피빈'},
}

def get_filter_keywords(search_key: Optional[str] = None) -> Set:
    filter_keywords = FILTER_KEYWORD['공통'] # {'ㅋ', 'ㅎ', '.', '?'}
    if search_key:
        specific_filter_keywords = FILTER_KEYWORD[search_key] # {'스타벅스', '할리스', '커피빈'}
        filter_keywords |= specific_filter_keywords # 합집합 취해줌.
    return filter_keywords 

if __name__ == "__main__":
    print(FILTER_KEYWORD['공통']) # {'ㅋ', 'ㅎ', '.', '?'}
    get_filter_keywords('커피')
    print(FILTER_KEYWORD['공통']) # {'ㅋ', 'ㅎ', '.', '?', '스타벅스', '할리스', '커피빈'}

FILTER_KEYWORD를 전역 변수로 선언하고, 모든 검색어에도 적용되게 하기 위해 공통 필터 키워드를 사전에 추가하고, 사전에서 특정 검색 단어의 필터키워드와 합쳐서 리턴하는 함수를 만들었다.

문제는 리턴 값을 보면 알 수 있다. set은 mutable 객체기 때문에, 위 예제와 처럼 FILTER_KEYWORD의 공통 set을 단순 대입하는 것과 같이 얕은 복사로는 filter_keywords의 값이 변경되면 FILTER_KEYWORD의 공통 set의 값도 같은 값으로 업데이트 되어버리는 것이다.

이 부분은 마지막 print 부분을 보면 쉽게 이해가 가능하다. 결과적으로 변하지 않아야 할 FILTER_KEYWORD['공통'] 값이 바뀌어 버린 것이다.😱

➕ mutable, immutable / 얕은복사, 깊은복사 관련 글 : https://wikidocs.net/16038



😮‍💨 해결법


이런 얕은복사 문제를 해결할 수 있는 방법이 몇 가지 있다.

  1. copy 모듈의 deepcopy 메서드 사용
  2. mutable 객체의 .copy() 메서드 사용
  3. list의 경우 slicing 사용

나는 여기서 2번인 .copy() 메서드를 이용해서 다음과 같이 해결했다.


1️⃣ .copy() method 사용

FILTER_KEYWORD = {
    '공통' : {'ㅋ', 'ㅎ', '.', '?'},
    '커피' : {'스타벅스', '할리스', '커피빈'},
}

def get_filter_keywords(search_key: Optional[str] = None) -> Set:
    filter_keywords = FILTER_KEYWORD['공통'].copy() # copy method 사용
    if search_key:
        specific_filter_keywords = FILTER_KEYWORD[search_key] 
        filter_keywords |= specific_filter_keywords
    return filter_keywords 

if __name__ == "__main__":
    print(FILTER_KEYWORD['공통']) # {'ㅋ', 'ㅎ', '.', '?'}
    get_filter_keywords('커피')
    print(FILTER_KEYWORD['공통']) # {'ㅋ', 'ㅎ', '.', '?'}

이제 의도한 대로 전역 변수인 FILTER_KEYWORD의 값이 변하지 않게 되었다!

내가 처한 상황에서는 이 방법으로 충분히 해결이 가능하지만, 사실 이 방법으로도 완벽하게 복사되지 않을 때가 있다. 다음 예제를 통해 알아보자

➕ mutable 객체 안에 또 mutable 객체가 있는 경우

a 라는 이중 리스트를 b라는 변수에 복사를 하고 싶다고 가정하자.

>>> a = [[1,2],[2,3]]
>>> b = a.copy()
>>> print(a, b) # [[1, 2], [2, 3]] [[1, 2], [2, 3]]
>>> print(id(a), id(b)) # 140403959376304 140403959502640

.copy()를 사용하여 복사를 하면 주소 값이 다른 것을 보아 잘 복사된 것처럼 보인다. 하지만 항상 우린 의심을 하고, 테스트를 해봐야지 않겠는가? 리스트 안의 리스트의 요소를 바꿔보자.

>>> b[0][0]=2
>>> print(a, b) # [[2, 2], [2, 3]] [[2, 2], [2, 3]]

아니 이게 뭐람? b 리스트의 첫 번째 리스트(b[0])의 첫 번째 값(b[0][0])을 바꿔보니 a 리스트의 값도 같이 바뀌어버렸다. 무슨 일일까? 혹시 모르니 내부 리스트의 주소 값을 찍어보자.

>>> print(id(a[0]), id(b[0])) # 140403959502480 140403959502480

내부의 리스트들은 주소값이 여전히 같은 것을 알 수 있다. 이 말은 내부 리스트까지는 복사(다른 주소 값으로 할당)가 되지 않았다는 것이다. 정리하면, 딕셔너리나 리스트처럼 내부 요소에도 또 list, set, dict를 가질 수 있는 자료형들은 .copy() 통해서는 하위의 데이터까지는 복사가 되지 않는다는 것이다. (list slicing을 이용한 경우에도 동일하다.)

그럼 어떻게 해야하나..? 깊은 복사를 사용하자!


2️⃣ copy 모듈의 deepcopy method 사용

>>> import copy
>>> a= [[1,2],[2,3]]
>>> b = copy.deepcopy(a)
>>> print(a,b) # [[1, 2], [2, 3]] [[1, 2], [2, 3]]
>>> b[0][0]=2
>>> print(a,b) # [[1, 2], [2, 3]] [[2, 2], [2, 3]]
>>> print(id(a[0]), id(b[0])) # 140403959502240 140403958979392

deepcopy()를 사용하니 b의 값만 바뀐 것을 알 수 있고, 하위 리스트의 주소 값을 비교해봐도 달라진 것을 볼 수 있다. 이제야 제대로 값만 복사된 것이다.

오늘의 교훈 : 변하지 않아야 하는 변수는 꼭!! immutable 객체(tuple, frozenset 등)로 만들어주자!!
다른 분들은 저처럼 의도하지 않은 결과를 얻는 일이 없도록 바래요...😢



👋 마무리


나는 대충이라도 알고있던 개념을 실제 상황에서 만나보니 전혀 신경 쓰지 못하고, 대응하지 못했다. 이거 때문에 내가 아닌 다른 분께서 에러 잡아내느라 늦게 퇴근하셨다고 한다. 그러고 이런 에러가 있었다고 말씀해주셨었다... 진짜 듣는 순간 수치플...😵 내가 짠 코드에서 결함이 발견될 때가 제일 현타 온다. 내가 이것밖에 못하나.. 이런 생각이 들면서 😭

그래도 나를 생각해서 말하지말까 하시다가 솔직한 피드백을 주신 것에 감사했다. 덕분에 내가 제대로 알고 다음부터 더욱 신경 써서 코드를 짜지 않을까 싶다. 그래도 앞으로 이런 일이 생기지 않도록 배운 내용들을 토이 프로젝트에 적용시켜 보면서 실패를 겪어 봐야겠다는 생각이 들었다. 배운 것을 얕게만 알고 있지 말고 내 것으로 만들 수 있도록 노력하자..💪