Models


장고의 models.Model 클래스를 활용하면 직접 SQL을 사용하지 않고도 파이썬에서 객체 타입을 선언하고 DB와 연결시킬 수 있다. models.Model을 상속한 모델 클래스를 만들면 장고가 제공하는 ORM(object-relational mapping)을 이용할 수 있다.
models.Model을 상속한 클래스 내부에는 필드를 정의할 수 있고, 이 필드들도 models 라이브러리의 세부 속성으로 선언할 수 있다.
DB와 연결되었을 때 하나의 클래스는 하나의 테이블이 되고, 클래스 내부의 필드들은 각 테이블 내부의 컬럼들이 된다.

다음은 장고 모델 클래스의 예시이다.

from django.db import models

class User(models.Model):
    id = models.AutoField()
    name = models.CharField()
    age = models.IntegerField()


User 클래스는 장고와 연결된 DB에서 user 테이블이 되고, id, name, age 속성들은 user 테이블 내부의 id, name, age라는 이름의 컬럼들이 된다. 참고로 실제 테이블 이름은 user이 아닐 수 있다.
장고에서는 테이블 이름을 지을 때 기본으로 사용되는 규칙이 있어서 별도의 이름을 지정하지 않으면 그 규칙대로 이름이 지정되고, 원하는 이름이 있으면 커스텀 속성으로 지정할 수 있다.

보통은 'app이름_model이름' 으로 테이블 이름이 지정된다. 만약 User 클래스가 account라는 app 내부에서 정의되었다면, 기본적으로 해당 테이블은 account_user이 된다. 직접 테이블 이름을 지정할 수도 있다. 각 모델 클래스의 내부에는 Meta 클래스를 선언하여 테이블 자체와 관련된 정보를 설정할 수 있다.
db_table 속성에다가 지정하고 싶은 테이블 이름을 입력하면 된다. 아래와 같은 경우 DB에 저장되는 테이블 이름도 User가 된다.

from django.db import models

class User(models.Model):
    id = models.AutoField()
    name = models.CharField()
    age = models.IntegerField()
    
    class Meta:
        db_table = "User"


장고에서 모델 클래스(models.Model을 상속한 클래스)를 정의하면 기본적으로는 DB에 테이블이 생기고, 변경되는 점들이 있으면 마이그레이션(migration)을 통해 연결되는 것이 대부분이다. 그러나 Meta 클래스에서 다른 옵션을 선택하면 이 규칙들을 바꿀 수 있다.


1) abstract = True

장고에는 추상 모델(abstract model)이 있다. 이렇게 선언하면 장고 내부에서는 해당 클래스를 모델로 인식하지만, 연동된 데이터베이스에 테이블이 생기지는 않는다.
보통은 추상 모델 클래스를 만들어 놓고, 다른 모델에서 해당 클래스를 상속하는 방식으로 사용한다. 한 모델 클래스를 상속받는 다른 모델 클래스는 그 모델 클래스에 정의된 필드를 사용할 수 있다.


2) managed = False

장고에서 모델 클래스에 변경사항이 있으면 마이그레이션 커맨드(makemigrations, migrate 등)를 통해 반영할 수 있는 이유는 테이블에서는 이 옵션이 기본으로 managed=True로 되어 있기 때문이다. 그러나 이 옵션을 False로 저장하면 해당 모델을 추가하거나 내부 필드를 변경해도 장고와 연결된 DB에 변경사항이 반영되지 않는다. 변경사항을 반영하려면 직접 해당 DB에 SQL문을 입력해야 한다.
이 옵션은 주로 해당 모델이 장고 프로젝트 내에서만 사용되는 모델이 아닐 때 등에 사용된다.


3) proxy = True

이 속성을 지정한 모델은 기존에 있던 다른 모델의 프록시 모델이 된다. 즉 다른 모델과 똑같이 작용한다고 보면 된다.

from django.db import models

class User(models.Model):
    # contents
    
class Person(User):
    # contents
    
    class Meta:
        proxy = True


모델 Person이 모델 User의 프록시 모델이라고 해 보자. User.objects.all()과 Person.objects.all()은 모두 같은 결과를 리턴한다.
프록시 모델을 선언하는 이유 중 하나는 DB와 장고 모델과의 연결은 그대로 두고, User 모델이 장고 내부에서 작동하는 방식이나 메소드를 바꾸고 싶을 때이다. 기본 모델인 User와 달리 Person에서는 정렬(ordering) 방식을 다르게 한다던가, 내부 메소드를 추가로 정의하고 싶을 때 프록시 모델을 사용한다.

 

Managers

 

쿼리 등으로 장고의 모델과 데이터베이스 사이에 통신이 일어날 때, 모델과 데이터베이스가 직접적으로 통신하지 않는다. 그 사이에는 Manager 클래스가 있다.
이 manager 클래스는 한 모델마다 최소 1개 이상이 있다. 즉 1:M의 관계다. 기본적으로는 1개이지만 원한다면 하나의 모델에 여러 개의 매니저 클래스를 둘 수 있다.
우리가 알고 있는 기본 매니저 클래스가 바로 objects이다. 항상 쿼리를 날릴 때 Model.objects.all() 이라고 썼었는데, 그 objects가 바로 매니저 클래스였던 것이다.

매니저 클래스는 쿼리셋을 만드는 방식이나 쿼리셋의 결과를 다르게 하고 싶을 때에도 사용한다.
예를 들면 Person 모델에는 employee_type이 crew, chef, administrator인 세 종류의 직원이 있다고 해 보자. 만약 employee_type이 crew인 직원들 안에서만 쿼리를 수행하고 싶다면 매번 filter() 메소드를 거는 방법도 있지만, 아예 crew_members 라는 매니저 클래스를 생성하고 필터링된 쿼리를 적용한다면 더 편하고 직관적으로 쿼리를 작성할 수도 있다.

# models.py
from django.db import models

class Person(models.Model):
    # contents
    objects = models.Manager()
    crew_objects = PersonCrewManager()
    administrator_objects = PersonAdministratorManager()
    chef_objects = PersonChefManager()
    
class PersonCrewManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="crew")
    
class PersonAdministratorManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="administrator")
        
class PersonChefManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="chef")
# example.py
from models import Person

Person.objects.all()
Person.crew_objects.all()
Person.administrator_objects.all()
Person.chef_objects.all()

 

 

QuerySet

 

장고 모델에서 DB에 연결하여 정보를 가져오기 위해서 사용하는 것이 쿼리셋이다. 장고 문법으로 쿼리를 작성하면 해당 쿼리셋은 DB의 SQL로 변환되어 DB에 전달되고, DB에서 나온 데이터는 다시 장고에서 쿼리셋으로 변환된다. 

 

예를 들어서 다음과 같은 쿼리를 작성하면, DB에는 이에 해당하는 SQL문이 입력된다. 

Person.objects.get(id=1)
SELECT * FROM myapp_person WHERE id = 1;

 

사실 쿼리셋은 코드가 실행되자마자 SQL에서 데이터를 가져오지는 않는다. 즉 쿼리셋은 작성되는 시점과 데이터를 가져오는 시점(evaluate 되는 시점)이 다르다. 그래서 'querysets are lazy'라는 말이 있다. 

변수에 쿼리셋을 할당할 때는 DB를 거치지 않고, 결과물을 콘솔 등으로 print()해서 보여줄 때 DB를 거쳐서 결과를 가져오게 된다. 

 

또한 쿼리는 캐싱을 사용한다. 한 번 evaluate 된 결과를 다음에 사용할 때는 또 데이터베이스를 거치지 않고(no hit), 캐싱해둔 데이터를 사용한다는 것이다. 그런데 그 결과값을 변수에 할당해 두지 않으면 같은 쿼리 결과를 사용할 때도 데이터베이스를 계속 hit 하게 된다. 

 

또한 한 모델 테이블과 연결된 여러 모델의 데이터도 select_related()prefetch_related()를 통해서 한 번에 불러올 수 있다. 이는 특히 for loop 등으로 반복적인 작업을 계속할 때 효율적이다. 

해당 모델이 참조하는 모델 데이터를 불러올 때는 select_related(), 해당 모델을 참조하는 모델 데이터를 불러올 때는 prefetch_related()를 불러온다. many-to-many 관계의 모델 데이터도 prefetch_related()로 불러올 수 있다. 

 

캐싱이 사용되는 경우 -> 효율적

people = Person.objects.select_related("company")		# Person, Company 데이터를 모두 가져옴
for person in people:
    person.company.is_valid = True		# 여기서는 database hit 발생 X

캐싱이 사용되지 않는 경우 -> 비효율적

people = Person.objects.all()		# 여기서 Person 모델 데이터만 가져옴
for person in people:
    person.company.is_valid = True		# 한 번 실행될 때마다 database hit

 

 

복잡한 쿼리셋

 

쿼리셋에서 filter(), exclude() 등으로 여러 개의 조건을 걸 수 있는데, 간혹 복잡하거나 구체적인 조건도 설정할 수 있다. 

 

1) field lookups

말 그대로 필드의 값과 동일한 값만 찾는 게 아니라, 숫자일 경우 gt, gte, lt, lte 등으로 작거나 큰 값을 조회할 수 있다. 문자열인 경우는 contains, iexact 등으로 특정 문자열을 포함하거나 대소문자를 구분하지 않은 결과들도 조회할 수 있다. 이런 field lookup등의 경우 필드 이름 뒤에 '__'을 붙인다. 

더 복잡한 field lookup도 있다. '__'을 붙이면 해당 모델과 관계 있는 다른 모델에서의 검색도 가능하다. 이 FK 검색은 연쇄적으로 계속 일어날 수 있는데, 실제로 이렇게 쿼리를 작성할 경우 SQL문에서는 JOIN 문을 사용해서 결과를 가져온다고 한다. 

# Person의 employee_type 필드값에 'c'를 포함한 모든 레코드 조회
Person.objects.filter(employee_type__contains="c")

# Person의 foreignkey인 Company 모델의 name 필드값에 'a'를 포함한 모든 레코드 조회
Person.objects.filter(company__name__contains="a")

 

2) Q()

Q() 안에는 filter(), exclude()에 사용하는 것처럼 조건문이 온다. 다만 Q()를 여러 개 사용해서 AND, OR, XOR 등 다양한 조건이 교차하는 필터링을 할 수 있다. 기존의 chaining 방법으로는 모든 조건들을 전부 만족하는 쿼리만 불러올 수 있지만 Q()를 사용하면 조건들을 다양하게 선언할 수 있다. 

# Person 중 name에 'peter'이 포함되고 27세 이상인 레코드 조회
Person.objects.filter(Q(name__contains="peter") | Q(age__gte=27))

 

3) F()

F()를 사용하면 기존의 다른 쿼리와는 달리 여러 필드들의 값을 비교할 수 있다. 기존에는 한 필드의 값에만 조건을 걸 수 있었다. 하지만 F()를 사용하면, 예를 들면 두 필드값을 비교하는 등의 필터링도 가능하다. 또한 필드값을 가져와서 연산 후에 조건을 거는 것도 가능하다. 

# House 레코드 중 number_of_people 필드와 number_of_cars 필드값이 같은 레코드만 조회
House.objects.filter(F("number_of_people") == F("number_of_cars"))

# House 레코드 중 price가 annual_people_income 필드값의 2배 이상인 레코드만 조회
House.objects.filter(price__gt=2*F("annual_people_income"))

 

4) Aggregation

또한 aggregate()를 사용하면 레코드의 총 개수, 특정 필드의 총합 등 특정 값을 추가해서 계산할 수 있다. Count, Avg, Max, Sum 등 다양한 기본 함수들을 사용할 수 있다. 

# 전체 Person 레코드의 개수 조회
Person.objects.aggregate(Count("id"))

 

5) Annotate

annotate()를 사용하면 쿼리셋에 포함되는 각 인스턴스 별로 aggregate()와 같은 쿼리를 생성할 수 있다. 

# 피자에 들어가는 토핑의 개수를 같이 조회
pizzas = Pizza.objects.annotate(number_of_toppings=Count("toppings")

# 첫 번째 피자에 들어간 토핑의 개수
pizzas[0].number_of_toppings

 

'server-side > Django' 카테고리의 다른 글

django routers  (1) 2023.12.21
django apps  (0) 2023.12.20
python - poetry 사용하기  (0) 2023.07.12
signals  (0) 2023.06.21
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11

poetry는 파이썬에서 패키지를 관리하는 도구이다. 이전에는 requirements.txt, setup.py 등 여러 파일에 의존성 관련 정보를 두고 사용했는데 poetry를 사용하면 pyproject.toml 파일 하나로 의존성을 관리할 수 있다. 또한 기존 requirements.txt를 이용한 방식은 pip라는 패키지 관리 프로그램을 같이 사용해야 했지만, poetry는 다른 패키지 관리 프로그램이나 파일을 더 사용하지 않고 poetry 가상환경을 따로 만들어서 관리할 수 있다.

 

poetry 환경 설정하기

우선은 poetry를 설치하는 방법부터 알아보자.

curl -sSL <https://install.python-poetry.org> | python3 -

 

기존에는 다른 url이었는데 바뀐 듯 하다.

이 명령어를 실행하기 전에 poetry 공식문서를 보고 명령어가 바뀐 게 있는지를 참고하면 좋을 것 같다.

poetry --version

 

터미널에 검색했을 때 버전이 정상적으로 나온다면 잘 설치된 것이다.

 

poetry를 실행하려면 pyproject.toml 파일을 작성해야 한다. 해당 문서를 참고하면 좋을 것 같다.

pyproject.toml 파일에 의존성 패키지 리스트를 작성할 때는 []를 사용해서 여러 상황마다 사용할 패키지를 다르게 설정할 수 있다. 기본값은 [tool.poetry]이고 이 뒤에 조건을 추가한다.

 

예를 들어 테스트 환경에서만 필요한 패키지들이 있다면 다음과 같이 적어주면 된다.

[tool.poetry.group.test]  # This part can be left out

[tool.poetry.group.test.dependencies]
pytest = "^6.0.0"
pytest-mock = "*"

 

상황이나 환경별로 의존성 패키지들을 따로 적을 필요가 없다면, 보통은 [tool.poetry.dependencies]로 선언하고 그 아래 필요한 패키지들을 적는다.

[tool.poetry.dependencies]  # main dependency group
httpx = "*"
pendulum = "*"
[tool.poetry.group.test.dependencies]
pytest = "^6.0.0"
pytest-mock = "*"

 

만약 실제 서버 환경과 배포 환경이 나눠져 있다면 [tool.poetry.dev.dependencies]로 선언하면 된다.

 

poetry 가상환경 만들기

앞서 poetry에서 가상환경을 만들 수 있다고 했었다. poetry 가상환경을 만들고 나면 pyproject.toml 파일의 의존성과 poetry 가상환경의 패키지들을 명령어로 동기화시킬 수 있다.

poetry shell

 

그러면 poetry 가상환경이 터미널에서 실행되고, 가상환경 관련 캐시는 Library/Caches/pypoetry/virtualenvs/ … 이 디렉토리에 저장된다.

deactivate

 

가상환경을 끄고 싶다면 이렇게 입력하자. 다시 켜고 싶으면 poetry shell을 다시 입력하면 된다.

 

만약 중간에 실행을 하다가 파이썬 인터프리터가 poetry 가상환경을 인식 못 해서 패키지 의존성 관련 에러가 난다면, 파이썬 인터프리터에 poetry 가상환경 디렉토리가 제대로 설정되지 않았을 수 있다. 즉 현재 실행중인 환경의 디렉토리가 poetry 가상환경 정보가 들어있는 디렉토리랑 다를 수 있다.

 

맥의 경우, File>Manage IDE Settings>Settings Sync.. 로 설정으로 들어간 뒤 왼쪽 탭에서 Project를 누르고, python interpreter를 누른다. 여기서 Add Interpreter를 누르고 왼쪽 탭에서 poetry environment를 누른다. 여기서 Existing environment를 누른 뒤 경로를 다시 설정해야 한다.

 

경로는 poetry shell을 입력했을 때 “Spawning shell within …. ”하면서 경로가 나오는데, 그 경로에서 뒤에다 /bin/python 을 붙여준 경로를 입력하면 실행이 잘 될 것이다.

 

poetry 기타 커맨드

poetry 삭제하기

 

poetry를 삭제할 때는 poetry를 어떻게 설치했는지에 따라 삭제 방법이 다르다. 공식 사이트의 명령어를 통해 설치하지 않은 경우 버전 업데이트 등 다른 쪽에서 에러가 날 수 있는데, 이 경우는 기존에 poetry를 설치했던 방법으로 삭제해줘야 정상적으로 삭제된다.

 

-pip을 통해 설치한 경우

pip uninstall poetry

 

-brew를 통해 설치한 경우

brew uninstall poetry

 

그리고 poetry가 제거되었는지 확인하려면 반드시 poetry —version을 입력해서 poetry가 없다는 말이 나와야 한다. 그렇지 않으면 poetry 파일이나 메타 데이터가 컴퓨터 어딘가에 남아 있는 것이다.

 

만약 poetry —version 커맨드에서 버전이 나온다면, 맥의 경우는 최상위 폴더로 이동한 뒤 /.local/bin/poetry 파일이 있는지 확인해보자. 있다면 지우고 다시 설치하자.

 

공식 사이트의 poetry 설치 커맨드를 통해 설치한 경우에는 위와 관련된 문제는 아마 없지 않을까 싶다. 이 경우에는 이렇게 제거할 수 있다.

curl -sSL <https://install.python-poetry.org> | python3 - --uninstall

 

poetry python 버전 바꾸기

poetry env use {version}

 

poetry 버전 바꾸기

poetry self update {version}

 

poetry 환경 리스트 보기

poetry env list

 

참고한 포스트

https://python-poetry.org/docs/

https://python-poetry.org/docs/managing-dependencies/

'server-side > Django' 카테고리의 다른 글

django apps  (0) 2023.12.20
models and databases  (0) 2023.09.16
signals  (0) 2023.06.21
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11

Definition

Signal은 전체 프레임워크 내에서 어떤 이벤트가 발생했을 때, 그 이벤트의 발생을 알려주는 notification의 역할을 한다.

 

모든 signal은 django.dispatch.Signal 클래스의 인스턴스들이며, 사용자는 signal을 받을 수도 있고, 직접 signal을 만들 수도 있고, 많이 사용되는 signal 객체들을 받을 수도 있다. (주로 django.core.signals, django.db.models.signals 등 ~signals. 파일에 명시되어 있다.)

 

signal을 받거나 준다는 것은 무엇일까? signal을 받는다는 것은 어떤 이벤트가 발생했을 때 알림(notification)을 받는다는 것이고, signal을 준다는 것은 어떤 이벤트가 발생했을 때 알림을 보낸다는 것이다. signal을 받을 때는 해당 알림을 받아서 어떤 일을 할지를 receiver function으로 정의한다. 

 

반면 signal을 줄 때는 위에서 언급한 것처럼 django.dispatch.Signal 클래스의 인스턴스를 만든 뒤 Signal의 send() 함수를 이용해서 signal을 보낸다. 

 

Example

예시를 통해 살펴보자. 한 피자집에서 주문한 피자가 만들어졌을 때 소비자한테 알림을 보내려고 한다. 이때 피자집에서는 Signal을 만들고, 손님은 해당 signal을 받게 된다. 

 

우선 피자집의 입장에서 보면, 피자가 만들어졌을 때 signal 인스턴스를 생성한 뒤 send 함수를 이용해서 보내면 된다. 이때 누구에게 보내는지는 중요하지 않다. Signal을 보내는 쪽은 발신자를 특정하지 않고 보내고, 받는 쪽에서 자신이 원하는 signal만 골라서 받게 된다. 

# pizzastore.py
import django.dispatch

pizza_done = django.dispatch.Signal()
pizza_soldout = django.dispatch.Signal()


class PizzaStore:

    def send_pizza(self, toppings, size):
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)

    def notify_soldout(self, toppings):
        pizza_soldout.send(sender=self.__class__, toppings=toppings)

 

손님의 입장에서는 주문한 피자가 완성되었다는 signal을 받아야 한다.

 

이때, 위에서는 간단한 예시로 PizzaStore 클래스만 정의하였지만 실제로는 여러 가게들이 메뉴가 완성되었을 때 signal을 보낼 것이다. 손님이 원하는 건 자신이 주문한 피자 가게에서 보낸 signal이므로 해당 피자 가게가 보낸 signal만 받도록 설정할 수 있다. 즉 특정 발신자(sender)가 보낸 signal만 받도록 설정할 수 있다. 

 

아래 코드에서는 PizzaStore에서 보낸 pizza_done signal에 대해서만 notify_pizza 함수를 signal과 연결시킨다. 

# customer.py
from pizzastore import PizzaStore, pizza_done
from django.dispatch import receiver

@receiver(pizza_done, sender=PizzaStore)
def notify_pizza(sender, **kwargs):
    print("Pizza is done!")
    # code

 

위에서 언급한 @(decorator)를 사용하는 방법 말고도 signal과 receiver 함수를 연결할 수 있다. 

# customer.py
from pizzastore import PizzaStore, pizza_done
from django.dispatch import receiver

def notify_pizza(sender, **kwargs):
    print("Pizza is done!")
    # code
    
pizza_done.connect(notify_pizza)

 

즉 signal과 receiver 함수를 연결하는 방법은 두 가지가 있다.

1. @(decorator)를 사용한 연결

2. connect 함수를 사용한 연결

 

sender 옵션은 필수 옵션은 아니다. sender 옵션을 지정하지 않을 경우, notify_pizza는 PizzaStore가 아닌 다른 클래스에서 생성한(sender가 다른 클래스로 되어 있는) pizza_done signal이 발생할 때에도 실행될 것이다. 

 

Signal.send(). vs Signal.send_robust()

signal을 보낼 때는 .send() 함수 외에도 .send_robust() 함수를 사용할 수 있다. 두 함수 모두 (해당 signal을 받는 receiver 함수, 해당 함수가 리턴하는 값)의 튜플 리스트를 리턴한다. 

 

다만 .send() 함수는 개별 receiver 함수로 signal을 보낼 때 에러가 발생할 경우 그 에러를 처리하지 않는다. 그래서 일부 receiver 함수에서 오류가 발생할 경우, 그 함수는 signal을 받지 못할 수 있다. 

 

반면 .send_robust() 함수는 signal을 보내는 도중 발생한 모든 에러를 처리한다(catch로 처리하는 것으로 보인다). 그래서 일부 receiver 함수에 signal을 보내다가 에러가 발생한 경우, .send_robust() 함수의 값으로 리턴되는 tuple 중 일부는 (에러가 발생한 함수, 발생한 에러의 종류)로 값이 리턴된다. 어떻게 해서든지 signal을 보낸다는 의미에서 send_robust라고 이름 붙여진 것 같다. 

 

Receiver 함수의 실행 횟수 제한

receiver 함수는 기본적으로 signal을 받을 때마다 실행되는데, 이 방식이 문제가 될 수도 있다. 

예를 들어 어떤 모델 인스턴스가 저장될 때마다 사용자에게 이메일이 가는 것보다, 한 번만 가는 것이 더 바람직하다. 

이때는 receiver 함수에 dispatch_uid라는 새로운 옵션을 선언하고 그 값으로는 해싱이 가능한 문자열이면 어떤 것이든 선언할 수 있다. 

pizza_done.connect(notify_pizza, dispatch_uid="evening_pizza")

 

참고한 포스트

https://docs.djangoproject.com/en/4.2/topics/signals/

 

'server-side > Django' 카테고리의 다른 글

models and databases  (0) 2023.09.16
python - poetry 사용하기  (0) 2023.07.12
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11
models: on_delete  (0) 2022.07.05

데이터베이스 성능을 효율적으로 관리하는 방법 중 하나로 '서브쿼리는 조인(join)으로 작성해라'는 말을 들은 적이 있다. 서브쿼리(sub-query)란 기존에 날린 쿼리의 캐시 데이터를 사용하는 또 다른 쿼리인데, 이런 서브 쿼리를 작성할 때는 join을 사용하라는 의미이다. 

 

join을 사용하지 않으면 작성자는 기존에 만든 쿼리의 데이터(캐시 메모리)를 이용해서 쿼리를 만드려고 했지만, 실제로는 데이터베이스에 또 다른 쿼리를 날리게 된다. 즉 한 번 날릴 수 있는 쿼리를 두 번 날리게 되므로 자원을 낭비하는 셈이다. 

 

select_related()prefetch_related() 모두 장고 ORM에서 데이터베이스에 접근할 때 사용하는 메소드이다. 또한 join을 사용해서 데이터를 합하고, 쿼리셋(QuerySet)을 리턴한다는 점에서 비슷하다. 하지만 두 메소드는 엄연히 다르다. 둘의 차이점에 대해서 알아보자. 

 

공통점

1. 장고 ORM에서 데이터베이스에 접근할 때 사용하는 메소드

2. 결과가 합해진 쿼리셋을 리턴

 

✅개별 특징 - select_related()

✔️사용 방법

사용하려는 모델[A]이 다른 모델[B]을 외래키(ForeignKey)로 참조하고 있을 때 사용한다. 

select_related('참조하는 외래키 필드명')

A의 데이터를 불러오는 쿼리를 작성할 때, A가 참조하는 외래키인 B의 데이터도 같이 캐시 데이터로 불러온다. 그러면 나중에 해당 데이터에서 외래키 정보를 사용해야 할 때, 추가로 DB에 쿼리를 날리지 않아도 된다. 

 

예시를 보자. 

# models.py
class Person(models.Model):
	name = models.CharField()
	age = models.IntegerField()
	home = models.ForeignKey(Home, on_delete=models.CASCADE)
    
class Home(models.Model):
	address = models.CharField()
# ORM query without select_related
p = Person.objects.get(id=22)
h = p.home
# ORM query with select_related
p = Person.objects.get(id=22).select_related('home')
h = p.home

select_related()를 사용하지 않은 경우, 해당 객체의 외래키에 대한 캐시 데이터가 없으므로 p 객체의 home 속성을 조회할 때 한번 더 데이터베이스로 쿼리를 날리게 된다. 

 

그러나 select_related()를 사용할 경우, p 객체를 불러올 때 이미 해당 객체의 외래키 데이터도 캐시 데이터로 불러온다. 따라서 데이터베이스에 쿼리를 날리지 않고 이미 불러온 데이터를 사용할 수 있다. 

 

✔️외래키 관계

외래키로 엮인 두 모델은 one-to-one, many-to-one, many-to-many 셋 중 하나의 관계를 갖는다. 

select_related()의 경우, many-to-many 관계인 모델의 데이터는 불러올 수 없다. select_related()는 SQL 쿼리에서 JOIN 문을 사용해서 해당 모델이 참조하는 다른 모델의 컬럼 데이터들을 불러오는데, many-to-many 관계인 모델의 데이터까지 불러오게 된다면 SQL 쿼리로 불러오는 결과 데이터 양이 너무 많아질 수 있기 때문이다. 따라서 one-to-one 관계나 many-to-one(해당 모델이 many 쪽) 관계로 참조하는 모델 데이터만 불러올 수 있다. 

 

✔️이중 참조

A가 참조하는 모델[B]이 참조하는 또 다른 모델[C]의 데이터를 가져오는 것도 가능하다. 

 

예시를 보자. 

# models.py
class Menu(models.Model):
	name = models.CharField()

class Dessert(models.Model):
	name = models.CharField()
	dessertType = models.ForeignKey(Menu, on_delete=models.CASCADE)
    
class Chocolate(models.Model):
	name = models.CharField()
	chocolateType = models.ForeignKey(Dessert, on_delete=models.CASCADE)
# ORM query
c1 = Chocolate.objects.filter(name__contains='white').select_related('chocolateType__dessertType')

c1 객체에는 name에 white를 포함한 Chocolate 객체들의 데이터가 포함되고, 해당 객체들의 chocolateType 필드가 참조하는 Dessert 객체들의 데이터와, 해당 객체들의 dessertType 필드가 참조하는 Menu 객체들의 데이터까지 포함되게 된다. 

 

✅개별 특징 - prefetch_related()

select_related()와 달리 many-to-many, many-to-one의 관계인 모델의 데이터도 가져올 수 있다. 

prefetch_related('참조하는 외래키 필드명')

 

다음 예시를 보자. Student과 Course는 many-to-many 관계이다. 

# models.py
class Course(models.Model):
	id = models.IntegerField()
	name = models.CharField()
    
class Student(models.Model):
	name = models.CharField()
	course = models.ManyToManyField(Course)
# ORM query
student = Student.objects.prefetch_related('course')

해당 쿼리는 Student 전체의 데이터와 함께, 개별 student 객체가 참조하는 Course 객체에 대한 데이터도 캐시 데이터로 같이 불러온다. 

 

prefetch_related()를 사용하지 않는다면 총 db에 등록된 students 객체 수 만큼의 쿼리가 실행되어야 할 것이다. 그러나 prefetch_related()를 사용하면 총 두 번의 쿼리로 같은 작업을 할 수 있다. 

 

✔️외래키 관계

prefetch_related()는 select_related()와 마찬가지로 이중 참조가 가능하다. select_related()에서 작성한 방법과 같은 방식으로 쿼리를 작성하면 된다. 

 

✔️사용할 수 없는 경우

prefetch_related()를 사용해서 기존 모델이 참조하는 다른 모델의 데이터를 가져왔지만, 가져온 데이터를 사용할 수 없는 경우도 있으니 주의하자. 

 

1. 가져온 데이터셋에 추가 메소드를 적용하였을 경우

student = Student.objects.prefetch_related('course')
student = students.filter(name__contains='Kim')		# 기존 prefetched 데이터 변경

students 변수에는 prefetch_related()를 사용해서 외래키 모델에 대한 데이터까지 저장되어 있었다. 그러나 추가로 filter() 메소드를 사용하면서 데이터가 변경되었다. 기존의 데이터에 추가 메소드를 적용한 경우, 장고에서는 추가로 메소드를 적용한 students.filter() 쿼리셋을 아예 다른 쿼리셋으로 인식한다. 

따라서 새 students 쿼리셋에는 prefetch로 불러왔다고 생각했던 데이터가 없는 상태이다. 

 

2. 기존 DB의 데이터가 변경되었을 경우

student = Student.objects.prefetch_related('course')
Student.objects.create(
	# code
)

기존 student 객체에는 prefetch_related()로 불러온 캐시 데이터가 있었으나, 이를 사용하기 전에 create, delete, update 등으로 기존 데이터가 변경되었다. 이렇게 DB 데이터가 변경된 경우, 기존에 저장되었던 캐시 데이터는 삭제되어서 이용할 수 없다. 

 

⚠️차이점 1 - 사용 가능한 외래키 참조 관계

외래키로 엮인 두 모델은 one-to-one, many-to-one, many-to-many 셋 중 하나의 관계를 갖는다. 

select_related()의 경우, one-to-one, many-to-one(해당 모델이 many 쪽) 관계인 모델의 데이터만 불러올 수 있다. (many-to-many 관계는 불러올 수 없다!)

반면 prefetch_related()의 경우, 참조 관계에 상관 없이 참조하는 모든 모델의 데이터를 불러올 수 있다. 

 

⚠️차이점 2 - JOIN 방식

select_related()는 DB에 엑세스할 때 변환되는 SQL 쿼리에서 JOIN문을 생성하고, 참조하는 모델의 다른 필드들을 SELECT문에 추가하는 방식으로 참조하는 다른 모델의 데이터를 가져온다. 그렇기 때문에 한 번에 너무 많은 데이터를 가져올 수 없어서 one-to-one 관계에만 사용하도록 제한된다. 

 

반면 prefetch_related()는 개별적으로 SQL 쿼리를 날린다. 그리고 DB에서 가져온 쿼리셋을 파이썬에서 합한다. 개별 쿼리에서 JOIN이 발생하지 않기 때문에, 한 번에 적은 데이터를 가져오지 않아도 된다. 따라서 many-to-many, many-to-one 관계에도 적용할 수 있다.

 

두 메소드 모두 JOIN 과정이 발생한다. select_related()는 SQL 쿼리에서 JOIN을 통해 데이터를 가져오고, prefetch_related()는 개별 쿼리로 데이터를 가져온 뒤 파이썬에서 별도의 JOIN 과정을 통해 데이터를 합한다. 즉 JOIN 과정이 언제 일어나는지의 차이가 있다. 

 

 

참고한 포스트

QuerySet API reference | Django documentation | Django (djangoproject.com)

Django에서 DB 액세스 최적화하기 – Myungseo Kang

MySQL 쓰면서 하지 말아야 할 것 17가지 – Lael's World

 

'server-side > Django' 카테고리의 다른 글

python - poetry 사용하기  (0) 2023.07.12
signals  (0) 2023.06.21
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11
models: on_delete  (0) 2022.07.05
admin: Inline, InlineModelAdmin  (0) 2022.07.02

Model.objects.get()Model.objects.filter() 모두 장고와 연결된 데이터베이스에서 조건에 맞는 데이터를 리턴하고 싶을 때 사용한다. 그러나 두 메소드는 엄연한 차이가 있다. 

 

쿼리셋(QuerySet)

둘의 차이를 이해하기 위해서는 쿼리셋이 무엇인지를 먼저 알아봐야 한다. 

쿼리셋(Queryset)이란 장고와 연결된 데이터베이스에 저장된 객체들의 모임을 의미한다. 쿼리셋은 SQL문으로 치면 SELECT문과 같다. 여기에 WHERE, LIMIT 등의 여러 필터를 사용해서 원하는 데이터만 포함한 쿼리셋을 만들 수 있다. 

+ 앞으로 '쿼리를 날린다'는 표현을 사용할 건데, '쿼리를 날린다' = '데이터베이스에 연결한다' 라고 보면 된다. 

 

쿼리셋과 데이터베이스 접근은 다르다. (Querysets are lazy)

기본적으로 쿼리셋은 메모리의 일종인 캐시(cache)를 사용해서 데이터베이스에 접근을 최소화 하려고 한다. 따라서 만약 어떤 메소드가 쿼리셋을 리턴하고 그 리턴된 쿼리셋이 캐시 메모리를 포함한다면, 나중에 같은 정보가 필요할 때 기존에 리턴된 쿼리셋을 사용할 수 있다. 

 

쿼리셋이 만들어지거나 쿼리셋에 추가적인 필터 메소드를 적용한다고 해서 항상 데이터베이스에 쿼리를 날리는 것이 아니다. 쿼리셋이 만들어진 이후, 쿼리셋을 계산(evaluate)하는 메소드가 실행되면 그 때 쿼리를 날리게 된다. 

 

쿼리셋을 계산하는 메소드

이 메소드를 사용할 때는 데이터베이스에 쿼리를 날리게 된다. 

  • 반복
  • (인덱스 기반의) 슬라이싱
  • 피클링/캐싱
  • 쿼리셋을 출력할 수 있는 문자열로 반환: repr()
  • 쿼리셋의 객체 수(길이) 리턴: len()
  • 쿼리셋을 리스트 타입으로 변경: list()
  • 쿼리셋 안에 객체가 존재하는지 판단: bool()

 

get()filter()의 차이

🌟SQL 쿼리의 차이

장고 내부에서 생성된 쿼리셋을 계산할 때는 데이터베이스에 쿼리를 날려야 한다. 즉 쿼리 메소드는 SQL문으로 변환될 수 있다. filter() 메소드의 경우 다음과 같이 변환될 수 있다. 

 

- ORM

Restaurant.objects.filter(name="seoul")

- SQL

SELECT 'id', 'name', 'category', 'email', 'menu'
FROM 'restaurant' WHERE 'restaurant'.'name' = 'seoul'

 

그러나 get() 메소드의 경우는 filter() 메소드와 달리 별다른 SQL 메소드로 변환될 수 없다. 

대신 filter() 메소드에 부가적인 처리를 해서 나타낼 수 있다. 

 

-ORM

Restaurant.objects.get(name='seoul')

-Code

rest = Restaurant.objects.filter(name='seoul')

if len(rest) == 1:
	return rest[0]
else:
	raise Exception

즉 get() 메소드는 filter() 메소드에 부가적인 처리를 한 결과를 리턴한다. 그러므로 같은 코드를 실행해도 filter()의 속도가 get()보다 빠르다.

 

뿐만 아니라, 인덱싱(rest[0])의 경우 쿼리셋에 해당하는 인덱스가 없다면 오류를 발생시킨다. 따라서 filter()는 조건에 해당하는 데이터의 개수에 상관 없이 결과를 리턴하지만, get()은 조건에 해당하는 데이터가 1개가 아닌 이상 오류를 반환한다. 

 

그러므로 get()은 반드시 한 개의 데이터만 리턴하고, 하나가 아니면 오류를 발생시킬 때만 사용하는 것이 더 효율적이라고 볼 수 있겠다! 

 

🌟쿼리셋을 리턴한다 vs 리턴하지 않는다

쿼리셋을 리턴하지 않는 메소드의 경우, 쿼리셋이 없기 때문에 캐시도 갖고 있지 않다. 그렇기 때문에 이 메소드들은 한 번 호출될 때마다 데이터베이스에 쿼리를 날리게 된다. get()은 쿼리셋을 리턴하지 않는 메소드들 중 하나이다. 

반면 filter()는 쿼리셋을 리턴한다. 따라서 filter()를 사용한다고 바로 쿼리를 날리는 것이 아니다. 그래서 메소드를 여러 번 사용할 경우, get() 보다는 filter()가 더 빠르다. 

 

Project라는 모델이 있다고 가정할 때, 아래의 두 코드의 성능은 다르다. 

# get()
p1 = Project.objects.get(id=1)
print(p1)
p2 = Project.objects.get(id=2)
print(p2)
p3 = Project.objects.get(id=3)
print(p3)
p4 = Project.objects.get(id=4)
print(p4)
# filter()
proj = Project.objects.filter(id__lte=4)
for p in proj:
	print(p)

위의 코드는 get을 사용하여 총 4번 데이터베이스에 쿼리를 날리지만, 아래 코드는 filter()를 사용해 쿼리셋을 생성하고, for문을 사용해서 쿼리셋 안의 객체를 문자열로 출력할 때만 데이터베이스에 쿼리를 날린다. 

 

따라서 순서(ordering)가 별 상관이 없다면, get()을 여러 번 사용하는 것 보다는 filter()를 사용해서 적합한 쿼리셋을 생성한 뒤, for문 등을 이용해서 데이터베이스 접근을 최소화하는 것이 더 성능이 좋다. 

 

🌟여러 개의 데이터를 포함할 수 있다 vs 하나의 데이터만 포함한다

또한 filter()는 쿼리셋을 리턴하기에 여러 개의 데이터를 쿼리셋으로 받을 수 있지만, get()은 하나의 데이터만 받을 수 있다. 만약 get() 실행 시 해당하는 데이터가 없다면 Model.DoesNotExist 에러가 발생하고, 또는 해당하는 데이터가 여러 개라면 Model.MultipleObjectsReturned 에러가 발생한다. 또한 해당하는 데이터가 여러 개인 경우, 오류가 발생하기 전에 쿼리의 실행 속도가 상당히 느려지기도 한다. 

 

따라서 get()을 사용해야 한다면 반드시 unique한 컬럼명 또는 인덱스 컬럼명, primary key 등을 사용해서 해당하는 데이터가 여러 개 나오지 않도록 주의하자. 

 

 

참고한 포스트

Django에서 DB 액세스 최적화하기 – Myungseo Kang

QuerySet API reference | Django 문서 | Django (djangoproject.com)

Making queries | Django documentation | Django (djangoproject.com)

django - filter().exists(), Q() 객체, F() 객체 (velog.io)

 

'server-side > Django' 카테고리의 다른 글

signals  (0) 2023.06.21
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11
models: on_delete  (0) 2022.07.05
admin: Inline, InlineModelAdmin  (0) 2022.07.02
conda 사용해서 가상환경 만들기  (0) 2022.06.29

models.ForeignKey(on_delete={})

models.ForeignKey(참조할 모델, on_delete={참조하는 인스턴스가 삭제될 시 해당 인스턴스를 처리하는 방법})

on_delete 옵션은 해당 인스턴스가 참조하는 인스턴스가 삭제되었을 시, 해당 인스턴스를 어떻게 처리할지를 지정한다. 

 

1) models.CASCADE

해당 인스턴스가 참조하는 인스턴스가 삭제된 경우, 해당 인스턴스도 같이 삭제한다. 

 

2) models.PROTECT

해당 인스턴스가 참조하는 인스턴스를 삭제하려고 할 때 ProtectedError를 발생시킨다. 

 

3) models.RESTRICT

해당 인스턴스가 참조하는 인스턴스를 삭제하려고 할 때 RestrictedError를 발생시킨다. 

 

두 옵션 모두 에러를 발생시키지만 서로 다르다. 

 

 

🗒️PROTECT와 RESTRICT 옵션의 차이점

해당 인스턴스 A, 해당 인스턴스가 참조하는 인스턴스 B, B가 참조하는 또 다른 인스턴스 C라고 하자.

이때 A는 B, B는 C 인스턴스를 참조하므로, A와 B는 ForeignKey 필드를 갖고 있다. 

 

[1] on_delete=PROTECT 인 경우

A는 on_delete=PROTECT, B는 on_delete=CASCADE라고 해 보자. 

 

B를 삭제하려고 한다면 A의 on_delete=PROTECT 옵션 때문에 B가 삭제되지 않고 ProtectedError가 발생한다. 

C를 삭제한다면, B는 on_delete=CASCADE 이므로 원래는 같이 삭제되어야 한다. 

그러나 B를 참조하는 A는 on_delete=PROTECT 이므로 B는 삭제될 수 없다. 

따라서 C를 삭제하려고 해도 ProtectedError가 발생한다. 

 

[2] on_delete=RESTRICT 인 경우

A는 on_delete=RESTRICT, B는 on_delete=CASCADE라고 해 보자. 

 

B를 삭제하려고 한다면 A의 on_delete=RESTRICT 옵션 때문에 B가 삭제되지 않고 RestrictedError가 발생한다.

C를 삭제한다면, B는 on_delete=CASCADE 이므로 같이 삭제된다. 

즉 RESTRICT 옵션은 직접적으로 B를 삭제하는 것은 제한하지만, B가 참조하는 다른 인스턴스에 의해서 B가 삭제되는 것은 제한하지 않는다. 

 

4) models.SET_NULL

해당 인스턴스가 참조하는 인스턴스가 삭제될 경우, 해당 인스턴스의 값을 NULL로 바꾼다. 

이때, 반드시 해당 인스턴스의 옵션이 null=True 으로 되어있어야 한다!

 

5) models.SET_DEFAULT

해당 인스턴스가 참조하는 인스턴스가 삭제될 경우, 해당 인스턴스의 값을 Default로 지정된 값으로 바꾼다. 

그러려면 반드시 해당 인스턴스의 default 값이 지정되어 있어야 한다!

 

6) models.SET({value})

해당 인스턴스가 참조하는 인스턴스가 삭제될 경우, 해당 인스턴스의 값을 SET 안의 값으로 바꾼다.

그러려면 반드시 SET의 괄호() 안에 값을 지정해 주어야 한다. 

 

7) models.DO_NOTHING

별다른 추가 처리를 하지 않는다. 

 

 

참고한 포스트

Model field reference | Django documentation | Django (djangoproject.com)

 

Inline

모델 A가 모델 B를 ForeignKey로 가질 때, 모델 A는 모델 B에 의존한다. 

모델 A의 각 데이터마다 해당하는 모델 B의 데이터를 보고 싶을 때, inline을 사용한다. 

 

# models.py
class Home(models.Model):
	id = models.IntegerField()
	address = models.CharField()

class Person(models.Model):
	id = models.IntegerField()
	name = models.CharField()
	home = models.ForeignKey(Home, on_delete=models.CASCADE)
# admin.py
class PersonInline(admin.StackedInline):
	model = Home	# 해당 클래스가 외래키로 갖는 모델 입력
    
class HomeAdmin(admin.ModelAdmin):
	inlines = [PersonInline]	# 해당 클래스를 참조하는 다른 inline 클래스 입력

 

예시에서는 Person이 Home을 ForeignKey로 갖는다.

하나의 home 인스턴스를 참조하는 person 객체는 여러개일 수 있다. 

만약 각각의 home을 참조하는 person 객체들을 한 번에 보고 싶다면 inline을 활용하면 된다. 

 

InlineModelAdmin

장고에서도 inline을 사용하기 위한 InlineModelAdmin 클래스를 제공한다. 

(InlineModelAdmin은 ModelAdmin의 하위 클래스이다.)

InlineModelAdmin 클래스는 두 개의 하위 클래스인 StackedInline, TabularInline으로 구성되어 있는데, 두 클래스의 차이는 렌더링에 사용되는 템플릿의 차이 정도라고 봐도 무방하다. 

 

*렌더링(rendering):

작성한 웹사이트 코드를 사용자가 보는 웹 페이지로 바꾸는 과정. 

렌더링 엔진을 사용하여 렌더링을 한다. 

 

+) 또한 각 인스턴스를 참조하는 inline은 개별 데이터를 조회하는 화면에서 볼 수 있다. 

 

 

InlineModelAdmin options

InlineModelAdmin의 옵션 중 ModelAdmin의 필드와 다른 것, 재정의한 필드 등을 살펴보자. 

 

1) models

Inline 클래스가 어떤 모델의 데이터를 나타내는지를 입력한다. 

 

2) can_delete

데이터 조회 페이지가 아니라 inline이 나온 페이지에서도 데이터를 삭제할 수 있는지를 결정한다. 

기본값은 True이며 이 경우 inline에서 바로 데이터를 삭제할 수 있다. 

False로 입력하면 inline에서는 데이터를 삭제할 수 없고, inline에 해당하는 클래스(모델)의 데이터 조회 페이지에서 데이터를 삭제할 수 있다. 

 

3) fields

inline에서도 필드를 선택적으로 보여줄 수 있다. 

 

4) exclude

fields와 기능은 같은 반대 옵션이다. inline에서는 사용자에게 이 안에 입력한 필드만 제외하고 보여준다. 

 

5) inlines

inline 클래스에서도 또 다시 inline을 지정할 수 있다. 

 

6) fk_name

해당 inline이 나타내는 클래스에 여러 개의 ForeignKey 필드가 있을 때 사용한다.

여러 개의 ForeignKey 필드 중에서 어떤 필드를 참조하여 inline을 생성할지를 결정한다. 

# models.py
class Request(models.Model):

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    request_state = models.IntegerField()
    name = models.CharField()
    deadline = models.DateTimeField()
    contents = models.TextField()
    price = models.IntegerField()


# admin.py
class RequestInline(NestedStackedInline):
    model = Request
    fk_name = 'project'

RequestInline 클래스는 Request 모델에 대한 inline을 담당한다. 

그러나 Request 모델은 2개의 ForeignKey(User, Project)를 갖고 있다. 

따라서 inline을 생성할 때 개별 User에 대해서 inline을 생성할지, 개별 project에 대해서 inline을 생성할지를 정해 주어야 한다. 

여기서는 fk_name = 'project' 으로 project 필드를 기준으로 inline을 생성한다고 지정했다. 

 

7) max_num

하나의 데이터를 조회할 때, 최대 몇 개의 inline 데이터를 표시할지를 지정한다. 

 

8) formfield_overrides

딕셔너리 형태의 값을 받는다. 

모델에서 정의한 특정 필드를 관리자 페이지에서만 지정된 형식으로 나타낼 수 있게 해 준다. 

 

# models.py
class Person(models.Model):
	id = models.BigIntegerField()
	name = models.CharField(max_length=30)
	introduce = models.CharField(max_length=500)
    
# admin.py
class PersonAdmin(admin.ModelAdmin):
	formfield_overrides = {
		models.BigIntegerField: {'widget': Textarea(attrs={'rows':1, 'cols':15})}
		models.BigIntegerField: {'widget': Textarea(attrs={'rows':2, 'cols':25})}
	}

모델 Person을 조회하는 관리자 페이지에서는 models.BigIntegerField 타입인 필드를 Textarea()의 형식으로 나타낼 수 있다. (CharField도 마찬가지이다. )

 

 

<잘 모르겠는 options> - 다음에 공부하기!

-formset

-form

-extra

 

 

참고한 포스트

The Django admin site | Django documentation | Django (djangoproject.com)

 

가상환경을 설치하는 방법은 anaconda, pip 등 여러 가지가 있다. 

오늘은 anaconda와 pip을 둘 다 사용해서 가상환경을 만들어 보자. 

pip는 저번에 구체적으로 다루었으니, 이번에는 conda를 사용해서 가상환경을 만든 뒤 그 안에서 pip를 사용해 볼 것이다. 

 

anaconda와 pip은 모두 파이썬을 사용하는 가상환경을 만들어 준다는 점에서 동일하지만, anaconda에서 사용 가능한 라이브러리와 pip에서 사용 가능한 라이브러리가 다르다. 

(물론 numpy처럼 둘 다에서 사용 가능한 라이브러리도 있다.)

 

anaconda와 pip의 차이

anaconda와 pip이 어떻게 다른지를 알아보기 위해서 각 시스템의 특징을 보자. 

 

pip는 파이썬 환경에서만 사용할 수 있는 패키지가 포함된다.

반면 conda는 파이썬 외의 C, C++, Java 등에서도 사용 가능한 패키지도 포함된다. 

 

또한 pip의 패키지들은 소스 패키지라서 아직 빌드를 하지 않은 상태이다. 즉 빌드를 하다가 예외적인 상황으로 오류가 발생할 가능성이 있다. 

반면 conda의 패키지들은 사용자의 운영체제에 맞춰서 이미 빌드가 된 상태이고, 한 패키지의 버전을 변경하면 그 패키지와 의존관계가 있는 다른 패키지의 버전도 맞춰서 변경해 준다. 

 

conda의 단점을 얘기한 것 같은데, 그렇다기보다는 두 시스템이 다르다고 보면 될 것 같다. 

pip의 장점 중에서는 conda보다 용량이 가벼운 패키지들이 많다는 의견도 있다. 

 

conda의 패키지를 C나 Java에서 사용할 수 있는 이유

이는 파이썬 언어의 특징과 관련이 있다. 

파이썬은 C, C++와 잘 결합되는 언어이다.

일부 파이썬 라이브러리의 경우, C나 C++로 라이브러리를 개발한 뒤, 파이썬 언어를 사용해서도 해당 라이브러리를 다룰 수 있게 만들었다.

그래서 conda의 패키지를 파이썬과 C++ 환경 모두에서 사용할 수 있다. 

 

 

본격 설치하기

 

시작 전에, anaconda 공식 홈페이지에서 anaconda를 다운 받고 환경변수 설정을 마쳐야 한다. 

제대로 잘 되었다면, 윈도우 cmd 기준으로

conda --version

이렇게 입력했을 때 오류 없이 anaconda의 버전이 뜰 것이다. 

 

conda 가상환경 설치하기

지금부터는 cmd에서 conda라는 anaconda 환경변수를 이용해서 작업할 것이다. 

conda create -n {가상환경 이름} 

conda create -n condaenv

 

pip와 마찬가지로 특정 버전의 파이썬을 사용하는 가상환경을 만들고 싶다면 뒤에 python={버전} 을 붙여준다. 

conda create -n {가상환경 이름} python={파이썬 버전}

conda create -n condaenv python=3.6

당연한 말이지만 여기서 명시한 파이썬 버전 파일이 로컬 PC에 있어야 한다. 

 

conda 가상환경 삭제하기

만약 가상환경을 잘못 만들었다면 삭제해야 한다. 

conda remove -n {환경변수 이름} --all

conda remove -n condaenv --all

가상환경의 특정 부분만 삭제하려는 것이 아니라면 명령어 뒤에 반드시 --all 을 붙여주자. 그래야 환경 전체가 잘 삭제된다. 

환경이 완전히 삭제되지 않는 경우, 다음에 같은 이름으로 가상환경을 만든다면 conda 파일이 아닌 해당 이름의 파일이 이미 해당 디렉토리에 존재한다는 식의 에러가 뜬다. 

 

conda 가상환경 실행하기

아무튼 가상환경이 잘 만들어졌다면 이제 가상환경을 실행해야 한다. 

conda activate {가상환경 이름}

conda activate condaenv

 

CommandNotFoundError 오류가 난다면? 

나는 conda를 처음 사용했었는데, 처음에 이 명령어를 입력하니 오류가 났다. 

 

CommandNotFoundError: your shell has not been properly configured to use 'conda deactivate'

 

1) 윈도우

맨 처음 conda 가상환경을 만들었다면, 바로 실행하지 말고 conda init 명령어를 입력한다. 

그러면 conda와 Shell(여기서는 cmd)이 서로 연결되기 위해서 초기화 작업을 실행한다. 

이 작업이 끝나면 초기화 작업이 잘 반영되도록 cmd를 끄고 새 창을 열라는 말이 나온다. 

 

2) 맥, 리눅스

다른 커맨드를 입력해서 문제를 해결할 수 있다. 

source ~/anaconda3/etc/profile.d/conda.sh
conda activate my_env

 

conda 패키지 설치하기

conda install {설치할 패키지 이름}

conda install package-name

 

만약 기본 채널(channel)이 아니라 다른 채널의 패키지를 설치하고 싶다면 -c 옵션을 추가하자. 

conda install -c {채널 이름} {설치할 패키지 이름}

conda install -c conda-forge pythonocc-core occt

 

🗒️채널(Channel)

conda 패키지를 저장하는 저장소. conda에는 여러 개의 원격 저장소(채널)가 존재한다. 

예를 들어 다운받으려는 패키지가 A 채널에 있다면, -c 옵션으로 A 채널에서 패키지를 다운로드한다고 명시해 주어야 한다. 

대표적인 conda 채널에는 conda-forge가 있다. 

 

--append: 채널 추가하기

어떤 채널을 통해서 패키지를 다운 받고 싶다면 우선 그 채널을 채널 목록에 추가한다. 

conda config --append channels {채널 이름}

 

channel_priority: 채널 우선순위 정하기

기본 환경에서 conda는 default 채널을 사용한다. 

만약 본인이 conda-forge 등 거의 한 채널만 이용한다면 매번 -c 옵션으로 채널을 명시하는 것은 번거로울 수 있다. 

그럴 땐 채널 리스트의 여러 채널들 중에서 우선순위를 정하면 된다. 

conda config --set channel_priority strict

 

--show channels: 채널 우선순위 확인하기

현재 채널의 우선순위는 채널 리스트에서 확인할 수 있다. 

conda config --show channels

가장 위에 있는 것이 가장 우선순위가 높은 채널이다. 

 

pip로 패키지 설치하기

pip install -r {파일 이름}

해당 파일에 명시된 이름과 버전 그대로 패키지를 설치해 준다. 

 

 

이처럼 하나의 가상환경 안에서 pip와 conda를 모두 이용해서 패키지를 설치할 수 있다. 

 

 

 

참고한 포스트

[Anaconda] Conda 명령어, 기본 개념 모음 (tistory.com)

[아나콘다] conda 명령어 목록 (conda command list) (tistory.com)

[Anaconda] conda install 과 pip install 은 똑같은 걸까? (tistory.com)

[기고] 왜 파이썬(Python)인가? (b2en.com)

[Python] pip와 conda의 차이 — vg-rlo (tistory.com)

conda create — conda 4.13.0.post28+5138e307 documentation

Conda channels — conda 4.13.0.post28+5138e307 documentation

Can't execute `conda activate` from bash script · Issue #7980 · conda/conda (github.com)

[Anaconda]가상환경 설치,삭제 (tistory.com)

conda remove — conda 4.13.0.post28+5138e307 documentation

miniconda의 기본 채널 변경 : conda-forge (tistory.com)

 

+ Recent posts