[Django] Threading argument에 관한 문제 해결

David Im·2022년 8월 2일
0

본 글은 야간모드에 최적화 되어있습니다. 우측 상단에서 해 혹은 달모양을 클릭시어 velog 설정을 야간모드로 해주시면 더욱 편안하게 읽으실 수 있습니다.

🤔 Question Date : 22.07.26
👊 Solve Date : 22.07.26

What Problem?

회사에서 업무를 진행하다가 유저가 액션을 요청할 경우 유저 정보를 통째로 업데이트하는 기능과 휴면 회원의 데이터 삭제 기능에 대해서 변경점을 조금 주어야해서 그 부분의 기능을 수정하고 있었다.

프론트쪽애서는 코드를 구현해놓고, 데이터는 Mock-up 데이터를 사용했다가 API를 연결하고 나서 깨달았다.

당연히 처음에야 테스트 계정들로 진행하다보니 얼마 없는 정보들이었기 때문에
속도적인 측면에서 Request를 요청하면 바로 처리가 되는 수준이었지만
조금씩 스케일을 늘려서 테스트 계정 중에도 정보를 꽤나 많이 가지고 있는 계정들을 가지고서 테스트를 하기 시작했을 때 문제가 나타났다.

현재 서비스 DB 구조가 user 모델을 외래키로 가지고 있는 DB들은 전부 cascade로 엮여있어서 탈퇴처리를 하게 되는 경우, 맞물려있는 모든 DB 모델의 데이터를 지우기 때문에 발생하는 듯 했다.

데이터가 많아지다보니 Request를 요청했을때, 프론트엔드 측 코드에서는 Time-out이 5초로 되어있어
변경해야하는 데이터가 커질수록, 안의 처리 시간이 오래걸려 Request Time-out이 발생하는 것이었다.

실제로 데이터는 전부 처리를 완료했지만 이미 5초가 넘어가는 처리 시간때문에, 프론트에서는 Response를 Time-out Cancel로 내려받았다.

그래서 백그라운드 태스크로 돌려야 겠다 싶어, Threading을 적용 시켰다.

기존에 try-except로만 있던 기능을 function으로 빼서 delete_user_cascade라는 함수로 만들고 그 안에 user_email을 인자로 던져서 threading을 통한 background task로 전환하는 방식을 취했다.

  • 기존 유저 데이터 삭제 처리 코드
# Cascade로 엮여 있어, user모델에 대한 삭제 transaction 시 전체 삭제가 이루어짐
# transaction.atomic()을 통해 오류 발생시 rollback 시키고 실패 리턴
try:
	with transaction.atomic():
		target_user = User.objects.filter(email=user_email)
		target_user.delete()
	return True
except:
	return False

  • 변경된 유저 데이터 삭제 처리 코드 부분
# 
th = threading.Thread(target=self.delete_user_cascade, args=(self, user_email))

def delete_user_cascade(self, user_email):
try:
	with transaction.atomic():
		target_user = User.objects.filter(email=user_email)
		target_user.delete()
	return True
except:
	return False

이렇게 변경하고 나니, threading 구간에서 아래와 같은 에러가 발생했다.
TypeError: delete_user_cascade() takes 2 positional arguments but 3 were given


▶︎ Threading argument over count에 대한 원인 파악



function 자체가 2개의 positional argument를 요구하는데 15개의 argument가 입력되었다는 오류인데, 분명 delete_user_cascade() 메소드에는 self, user_email 2개의 인자를 갖고있어서 당연히 threading에도 그렇게 던져주었는데 뭐가 문제인가 싶었다.

👉 class 메소드는 threading 안에 쓸 수 없는건가?

처음에는 내가 Django 내에서 작성하는 함수 방식이 현재 Class 기반 함수형식으로 작성하고있어서,

'API call을 할 경우 url로 받게되는 main function 하위에 있는 Class method에 대해서는 threading으로 던지지 못하는건가?' 싶었다.

처음에는 그렇게 생각이 들어 이와 관련된 내용을 구글링해보았지만 Class method를 threading 내에 쓰지 못한다는 내용은 없었다.

그리고 예상치 못했던 argument 문제였기에 조금 당황했던 걸 누르고 생각해보니 class method라고 못쓸 이유가 전혀 없기도 하고, 전에 진행했던 프로젝트에서도 다른 Class에 있는 method를 threading으로 가져와 사용한 적도 있었기 때문에 해당 내용은 아닌 것으로 결론 짓고 다음 예상 원인으로 넘어가보기로 했다.

👉 argument의 갯수가 계속해서 변한다? 이게 원인일까?

두 번째로 의심이 들었던건 argument에 대해 계속에서 변화하는 에러 값이었다.

처음에는 아래 코드대로 실행을 시켰을 때는

th = threading.Thread(target=self.delete_user_cascade, args=(self, user_email))

>> TypeError: delete_user_cascade() takes 2 positional arguments but 15 were given

이렇게 15개의 인자라고 출력이 되었었는데

코드를 변경해서 아래처럼 user_email 하나만 넣고 보니 23개의 인자라고 출력이 되는 것이었다.

th = threading.Thread(target=self.delete_user_cascade, args=(user_email))

>> TypeError: delete_user_cascade() takes 2 positional arguments but 23 were given

Class 메인 함수 아래에 자기 자신을 바라보게 하는 method라서 self가 빠질 수 없는 상태인데, self를 빼주었더니 이번에는 23 arguments로 인자가 늘었다?

argument를 까봐야 해답이 나올 것만 같았다.

그래서 *args, **kwargs 로 받아서 argument를 로컬창에 출력해보기로 했다.

>> args : (‘t’, ‘e’, ‘s’, ‘t’,1, ‘@’, ‘e’, ‘b’, ‘r’, ‘i’, ‘d’, ‘g’, ‘e’,-, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’,., ‘c’, ‘o’, ‘m’) 

테스트용 이메일을 전부다 문자 단위로 쪼개져서 입력이 들어가는 것이었다.

그렇다면 처음에 했던 self, user_email은?

>> args : (<main.administrator.views_admin_user.VwManagementUser object at 0x12576d4c0>, 'ttest04@ebridge-world.com')
>> kwargs : {}

이렇게 출력이 되는 것을 보고 깨달았다.



▶︎ Threading argument에 대한 이해와 해결



위 코드에서 threading argument가 인자를 tuple 형식으로 가져간다라는 것을 깨달았다.

즉, 처음 코드에서 나는 self, user_email로 argument 2개를 던진걸로 생각을 했지만, threading에서 받아 처리할 때는

"self로 넘긴 class method 인자(self와 user_email) + 내가 추가한 user_email 1개 = 3개"

이렇게 가져간 것이다.

그리고 두번째 user_email을 인자로 넘겼을때에는 마치 1개의 인자인것처럼 보였지만, 튜플 형태이기 때문에

"user_email을 문자단위로 쪼갠 23개"

로 변환하여 tuple로 만들어 넘긴것.

궁금해서 stack overflow를 더 찾아보다 보니 아래와 같은 내용이 나왔다.


The reason why your code doesn’t work without the comma is that the args parameter must contain a tuple, and the common way of creating a single-element tuple is exactly like (elem,). If you remove the comma, the python interpreter will just ignore the parentheses and pass 1 as argument.

대충 해석해보면 threading method에서 인자로 쓰는 녀석은 tuple 개체로 하여금 전달하게되는데, tuple을 만들때 사용되는 방식은 보통 (elem,) 과 같이 쉼표를 포함해서 전달해야한다고 한다.

쉼표를 붙이지 않으면 해당 인자를 1개로 인식하고 그 인자에 대해서 tuple 처리를 해버린다는 것이다.

그리고 추가적으로 검색하다보니 해당 내용에 대해서도 찾을 수 있었다.

threading args: target으로 넣은 함수의 args 파라미터 값을 iterable 한 객체로 넣어야 합니다.

그래서 코드를 아래와 같이 최종적으로 수정했더니 정상적으로 잘 동작하였다!

th = threading.Thread(target=self.delete_user_cascade, args=(user_email,))

쉼표 하나의 차이로 이렇게 에러가 날 수 있는 것을 보면 역시 코드에서는 indent 하나, 쉼표 하나 조차도 중요하고 세심하게 살펴야 한다는 것을 다시금 깨닫는다.




References

profile
코더보다 개발자로, 결과와 과정의 시너지를 만들어 가고 싶은 주니어 개발자

0개의 댓글