299일차 팀 프로젝트 및 파이썬 기초

2021. 10. 24. 23:02Diary/201~300

어제 오후 2시에 첫 스터디 모임이 있었다.
게을러지거나 나태해지지않고
꾸준히 성실하게 자기개발 하는 것이
주된 목적인 모임이다.
어제 모여서 밥도 먹고 이야기 나누면서
서로 친분을 나누고,
첫 팀 프로젝트에 대한 미팅을 했다.

첫 팀 프로젝트의 개발은
어렵지 않으면서도 우리에게 필요해보이는
IT 영단어 앱으로
선정되었다.
각자 아이디어를 생각해오기로 약속했고,
그 중에 내 아이디어가 채택되었다.
IT 영단어 말고도 좋은 아이디어가
너무 많아서 다음 프로젝트 때,
개발해도 너무 좋은 아이디어들이었다.

간단한 팀 프로젝트 소개이다.

https://vast-pendulum-57a.notion.site/99bb33abe6874c10b5ae64db8990b04f?v=945f23ad596844bc9708ecedf8cf8cc3

 

IT 영어앱 프로젝트

2021.10.23 시작한 안드로이드, 아이폰 앱을 만들기 위한 스터디 모임의 IT 영어앱 만들기 프로젝트!😍 🧑🏽‍💻 팀원 소개 👨🏻‍💻 권태완 : Android(Kotlin), BackEnd(Spring) 개발 및 팀장 👨🏻‍

vast-pendulum-57a.notion.site

내가 맡은 역할은 안드로이드와 백엔드다.

백엔드는 장고(Django)가 아닌 스프링(Spring)으로 진행할 예정이다.

앞으로 스프링과 관련된 글도 꾸준히 공부해서 올릴 예정이다.

현재는 아이디어 채택을 마무리한 상태이고,

기획을 해야 하는 단계이다.

전문적으로 기획을 공부한 팀원이 없어서

각자 의견을 공유하고 전체적인 틀과

기능을 나누고 계획을 수립할 예정이다.


집에 도착했다.

현재 시간은 오후 9시 13분이다.

1~2시간 정도 파이썬 기초에 대해 공부하고,

남은 시간은 휴식을 취하려고 한다.

너무 과몰입해도 안좋다는 것을 이번에 알게 되었다.

그래서 이번 주 부터,

"일요일"은 내가 정한 푹~ 쉬는날이다.

 

앞으로는

팀 프로젝트

스프링(Spring)

파이썬 기초

이 3가지에 대한 내용을 중점적으로 다룰 예정이다.

 

https://github.com/KwonGeneral/Python_Basic.git

 

GitHub - KwonGeneral/Python_Basic: 파이썬 기초

파이썬 기초. Contribute to KwonGeneral/Python_Basic development by creating an account on GitHub.

github.com

 

1. 리스트를 함수 안에 매개변수로 던져서 값을 변경했을 때,
원본 리스트의 값이 변하는가?

# 매개변수로 리스트를 전달했을 때,
# 이 매개변수를 통해 값을 변경했을 시, 원본 리스트의 값이 변하는가?
origin_list = ["1", "2", "3"]


def check_list_memory_address(temp_list):
    temp_list.append("4")
    return temp_list


print("\n1-origin_list 메모리 주소 값 : ", id(origin_list))
print(check_list_memory_address(origin_list))
print("2-origin_list 메모리 주소 값 : ", id(origin_list))

===== 결과 =====
메모리 주소 값 1 :  2970726345536
메모리 주소 값 2 :  2970726345536
['1', '2', '3', '4']

위의 코드를 작성하면서, 스스로가

전역 변수와 리스트에 대한 개념이 모호했다고 느꼈다.

함수 안에서 리스트는

global을 선언하지 않아도 전역 변수에 접근할 수 있다는

사실은 개발을 하면서 경험적으로 알고 있었다.

( int, str, dict, tuple은 )

( global로 원본 데이터에 접근해야 함 )

 

하지만, 매개 변수로 던져서

이를 수정했을 때, 원본이 수정되는 지에 대해서는

확실하게 답을 내릴 수 없었었다.

Reference Counting에 대한 추가적인 공부가 필요해보인다.

 

 

2. 파이썬의 GIL에 대해서 설명해주세요.

 

GIL은 Global Interpreter Lock의 약어이다.

Global은 전역 변수를 뜻하고,

Interpreter는 위에서부터 차례대로 한줄씩 읽어서 작동한다.

반대로 Complier는 전체 소스코드를 실행 파일로 만든다.

 

우선 Global에 대해서 알아보자.

# Global에 대한 설명.
# global 전역변수 선언은 값을 넣어서 선언할 수 없다.
# 그렇다면 왜 값을 넣어서 선언할 수 없는지 궁금하지 않은가?
# 개인적인 견해이지만, 이는 Global의 쓰임새가 특정 되어 있기 때문이라고 생각한다.

# 직접 써보면서 알아보자.
print("\n === Global Interpreter Lock === \n")
global temp_global
temp_data = 0
temp_global_data = 0


def check_global_data():
    global temp_global
    global temp_global_data
    temp_global = 1
    temp_data = 2
    temp_global_data = 3


try:
    print("1-temp_global : ", temp_global)
except NameError:
    print("1-temp_global : NameError")

print("1-temp_data : ", temp_data)
print("1-temp_global_data : ", temp_global_data)

check_global_data()

print("\n2-temp_global : ", temp_global)
print("2-temp_data : ", temp_data)
print("2-temp_global_data : ", temp_global_data)

"""
1-temp_global : NameError
1-temp_data :  0
1-temp_global_data :  0

2-temp_global :  1
2-temp_data :  0
2-temp_global_data :  3
"""

# 위의 결과로 알 수 있는 것은
# 전역 변수로 선언 하더라도, 함수 내부에서는 값을 수정하더라도 영향이 없다라는 것이다.
# 함수 내부의 temp_data는 전역 변수가 아닌, 지역 변수로 선언 되었음을 의미한다.
# 즉, Global은 함수 내에서 전역 변수에 접근 하고 싶을 때, 사용하는 것이다.

# Global을 활용해, 1번 파일의 List Of Memory Address를 변형해보자.
print("\n\n === List Of Memory Address === \n")
origin_list = ["1", "2", "3"]


def check_list_memory_addresses():
    global origin_list
    origin_list.append("4")
    print("메모리 주소 값 2 : ", id(origin_list))
    return origin_list


print("1-origin_list : ", origin_list)
print("메모리 주소 값 1 : ", id(origin_list))
print("2-origin_list : ", check_list_memory_addresses())

 

 

이제 Python Wiki에서 GIL에 대해 알아보자.

 

< Python Wiki 참조 >

CPython에서의 GIL은 Python 코드(Bytecode)를

실행할 때에 여러 Thread를 사용할 경우,

단 하나의 Thread만이 Python Object에 접근할 수 있도록

제한하는 Mutex 이다.

그리고 이 Lock이 필요한 이유는

CPython이 메모리를 관리하는 방법이

Thread-Safe하지 않기 때문이다.

 

일단 위의 내용에서

CPython, Thread, Thread-Safe, Mutex 란 무엇일까?


CPython : Python이란 언어의 내부는

C언어를 베이스로 이루어져있다.

이를 C파이썬(CPython)이라고 부른다.

 

Thread : Thread는 쉽게 말해서

레이드를 뛰기 위한 파티 구성원이다.

게임에는 다양한 직업들이 존재한다.

"전사", "마법사", "힐러" 등등

그리고 레이드는 이러한 직업들이 모여서

보스를 잡는다.

하지만, 이 게임의 레이드는 특이하다.

각 레이드의 특정 조건에 따라

다양한 버프와 디버프를 받을 수 있다.

조건은, 특정 직업군과 구성원의 숫자이다.

 

예를 들어, 어떤 레이드 에서는 "전사", "마법사", "힐러"로

이루어진 3인 파티일 경우에 데미지 증가 버프를 받는다.

하지만, "전사", "전사", "힐러", "도적" 으로 이루어진

4인 파티인 경우에는 데미지 감소 디버프를 받는다.

 

즉, 우리는 레이드를 뛰기 위해서

최고 효율을 낼 수 있는 직업과 인원수를

파악해서 구성하는게 중요하다.


Thread는

우리가 어떠한 작업을 하기 위해서 필요한

도구의 개수이다.

 

추가로 예를 들어보자면,

밥을 먹는다. => 1개의 Thread

밥을 먹으면서 TV를 본다 => 2개의 Thread

이런 느낌이다.

 

우리가 흔히 말하는,

멀티태스킹 능력.

왼손으로는 밥을 먹고 오른손으로는 책을 읽는 행동.

이게 바로 Multi Thread이다.

 

단, Thread는

필요한 작업에 따라 효율적으로 구성해야 한다.

예를 들어, 오른손잡이인 사람이

왼손으로 밥을 먹고 오른손으로 밥을 먹는다.

이는 비 효율적일 수 있다.

양 손으로 밥을 먹는 것보다

오히려 오른손으로만 밥을 먹는게 더 빠를 수도 있다.

 

위의 예시처럼 Thread를 사용할 때에는

효율을 생각하며 사용해야한다.


Thread-Safe : Thread-Safe는 말 그대로

Thread의 안전성을 보장하는 방법이다.

2개의 Thread가 있다고 가정했을 때,

2개의 Thread가 동시에 1개의 변수에 접근한다면

문제가 생길 수도 있다.

 

그냥 쉽게 생각해서

양다리를 걸쳤는데,

각각의 상대에 맞춰서 겹치지 않게 일정을 짜야하는데

실수로 같은 장소, 같은 시간에

양다리 걸친 상대방들을 부른 것이다.

생각만해도 끔찍하지 않은가..?

 

위의 예시처럼 여러 개의 Thread를 사용할 때는,

다양한 문제점들이 생길 수 있다.

이를 방지하기 위해서

여러가지 방법을 제시하는게

Thread-Safe이다.

 

Mutex : Mutex는 위의 Thread-Safe를 위한 기법이다.

공유 자원에 여러 Thread가 접근하는 것을 제한한다.

 


자, 다시 GIL에 대한 설명을 읽어보자.

 

CPython에서의 GIL은 Python 코드(Bytecode)를

실행할 때에 여러 Thread를 사용할 경우,

단 하나의 Thread만이 Python Object에 접근할 수 있도록

제한하는 Mutex 이다.

그리고 이 Lock이 필요한 이유는

CPython이 메모리를 관리하는 방법이

Thread-Safe하지 않기 때문이다.


 

즉, GIL은 파이썬 내부에서 돌아가는 C언어로 이루어진 코드이다.

GIL의 주된 역할은

공유 자원에 여러 Thread가 접근하는 것을 제한하는 것이다.

이러한 역할이 주어진 이유는,

파이썬 내부에서 메모리를 관리하는 방법이

Thread를 안전하게 제어해주지 않기 때문이다.

 

여기서 핵심은,

공유 자원에 여러 Thread가 접근하는 것을 제한한다는 부분이다.

즉, 1개의 Thread에 모든 자원을 허락하고

다른 Thread는 Lock을 걸어 실행할 수 없게 막아버린다.

 

import random
import threading
import time


def working():
    # 10000000개의 랜덤 숫자를 생성한 후, 그 중에서 최대값을 찾는다.
    max([random.random() for i in range(10000000)])


def sleep_working():
    time.sleep(0.1)
    max([random.random() for i in range(100000)])
    time.sleep(0.1)
    max([random.random() for i in range(100000)])
    time.sleep(0.1)
    max([random.random() for i in range(100000)])
    time.sleep(0.1)
    max([random.random() for i in range(100000)])
    time.sleep(0.1)
    max([random.random() for i in range(100000)])
    time.sleep(0.1)


if __name__ == '__main__':
    print("\n 1. 단일 쓰레드 작업 완료 시간")
    print("\n 2. 멀티 쓰레드 작업 완료 시간")
    print("\n 3. 단일 & 멀티 쓰레드 (1) 작업 완료 시간")
    print("\n 4. 단일 & 멀티 쓰레드 (2) 작업 완료 시간")
    select = input("\n선택 : ")

    if select == "1":  # 단일 쓰레드
        s_time = time.time()
        working()
        working()
        e_time = time.time()
        print("Thread-1 : ", f'{e_time - s_time:.5f}')
    elif select == "2":  # 멀티 쓰레드
        s_time = time.time()
        threads = []
        for i in range(2):
            threads.append(threading.Thread(target=working))
            threads[-1].start()

        for t in threads:
            t.join()

        e_time = time.time()
        print("Thread-2 : ", f'{e_time - s_time:.5f}')
    elif select == "3":  # 단일 & 멀티 쓰레드
        # 단일 쓰레드
        s_time = time.time()
        working()
        working()
        e_time = time.time()
        print("Thread-1 : ", f'{e_time - s_time:.5f}')

        # 멀티 쓰레드
        s_time = time.time()
        threads = []
        for i in range(2):
            threads.append(threading.Thread(target=working))
            threads[-1].start()

        for t in threads:
            t.join()

        e_time = time.time()
        print("Thread-2 : ", f'{e_time - s_time:.5f}')
    elif select == "4":  # 단일 & 멀티 쓰레드
        # 단일 쓰레드
        s_time = time.time()
        sleep_working()
        sleep_working()
        e_time = time.time()
        print("Thread-1 : ", f'{e_time - s_time:.5f}')

        # 멀티 쓰레드
        s_time = time.time()
        threads = []
        for i in range(2):
            threads.append(threading.Thread(target=sleep_working))
            threads[-1].start()

        for t in threads:
            t.join()

        e_time = time.time()
        print("Thread-2 : ", f'{e_time - s_time:.5f}')
    else:
        input("\n올바른 값을 입력해주세요")


===== 결과 =====

# 선택 : 1
Thread-1 :  1.42932

# 선택 : 2
Thread-2 :  1.46450

# 선택 : 3
Thread-1 :  1.45832
Thread-2 :  1.48733

# 선택 : 4
Thread-1 :  1.31013
Thread-2 :  0.66918

 

Python의 GIL로 인해, 일반적인 경우

단일 쓰레드의 속도가 멀티 쓰레드의 속도보다 빠르다.

하지만, 멀티 쓰레드에서 Sleep을 사용하면

다른 쓰레드로 전환되기 때문에,

효율이 개선된다.

 

여기서 더 확실하게 GIL을 이해하기 위해서는

Python의 Garbage CollectionReference Counting

대해서 알아야한다.

( Reference Counting은 1번 문제와 관련있다. )

이에 대한 공부는 내일 할 예정이다.

벌써 시간이 오후 11시다...

 


앞으로도 개인적으로 검색하고

내가 이해하고경험했던 부분을 종합해서

아~주 주관적인 글을 적을 예정이다.

내가 적은 정의가 틀릴 수도 있으니,

참고만 하시길..