Mixin

김동현·2024년 3월 19일

Django

목록 보기
1/6

다중 상속

상속은 기존에 정의된 클래스에 추가된 기능을 추가하여 새로운 클래스를 정의하는 것이다. Python은 다중 상속을 지원하는 언어이다.

다중 상속이란 하나 이상의 부모 클래스로부터 상속받을 수 있는 것을 말한다.

다이아몬드 상속


다중 상속의 문제점 중 하나로 다이아몬드 상속이 꼽힌다.

class A:
    def greeting(self):
        print('안녕하세요. A입니다.')
 
class B(A):
    def greeting(self):
        print('안녕하세요. B입니다.')
 
class C(A):
    def greeting(self):
        print('안녕하세요. C입니다.')
 
class D(B, C):
    pass
 
x = D()
x.greeting()    # 안녕하세요. B입니다.
D.mro()

이렇게 되어 있을 때 인스턴스 x는 B의 greeting을 호출해야 하는지, C의 greeting을 호출해야 하는 지 모호한 상황이 연출된다.

MRO(Method Resolution Order, MRD)

The Python 2.3 Method Resolution Order의 다이아몬드 상속에 대한 해결책을 제시한다. 이는 메서드 탐색 순서를 보장하는 데 D.mro()를 찍어보면 아래와 같은 결과를 확인할 수 있다.

해당 배열의 0번째 클래스 부터 선형으로 탐색하며 찾고자 하는 메소드가 정의 되어 있는 클래스를 만난다면 해당 메소드를 호출하게 된다.

class D(B, C):
    pass

class E(C, B):
    pass


x = D()
x.greeting()  # 안녕하세요. B입니다.
print(D.mro())

x2 = E()
x2.greeting()  # 안녕하세요. C입니다.
print(E.mro())

위 코드를 실행하면 아래와 같은 결과가 나온다.

이렇게 여러 개의 상속을 받은 경우 메서드를 찾아가는 방법을 보장하여 다중 상속의 이슈를 이겨내는 방법을 제공한다.

Mixin

믹스인의 역사
... mixin은 매사추세츠 소머빌 지역의 스티브 아이스크림 가게에서 영감을 받았다고 한다.
아이스크림 가게 주인은 기본적인 맛(바닐라, 초콜릿 등)과 추가적인 재료(땅콩, 쿠키, 사탕 등)의 조합으로 버무려진 아이스크림을 판매했다. 이를 믹스 인(mix-in)이라고 불렸으며 고유 상표가 되었다.
출처: 위키피디아 믹스인

객체 지향 프로그래밍 언어에서 믹스인(mixin 또는 mix-in)[1][2][3]은 다른 클래스의 부모클래스가 되지 않으면서 다른 클래스에서 사용할 수 있는 메서드를 포함하는 클래스이다. 다른 클래스가 믹스인의 메소드에 액세스하는 방법은 언어에 따라 다르다. 믹스인은 때때로 "상속"이 아니라 "포함"으로 설명된다.

믹스인은 코드재사용성을 높이고 다중상속으로 인해 발생할 수 있는 상속의 모호성 문제("다이아몬드 문제")를 제거하거나[4] 언어에서 다중상속에 대한 지원부족을 해결하기 위해 사용될 수 있다. 믹스인은 구현된 메서드가 포함된 인터페이스로 볼 수도 있다. 이 패턴은 종속성 역전 원칙을 적용하는 예가 되기도 한다.
출처: 위키피디아 믹스인

믹스인은 다중 상속의 약점을 이겨 내는 방법이다.
위키피디아에서 믹스인은 상속의 모호성 문제를 제거할 수 있고, 다중 상속에 대한 지원 부족의 문제를 해결할 수 있다고 설명되어 있었다.

아래의 예시 코드를 보면 Duck Class에서 다이아몬드 상속이 일어난다.

class Animal:
    def eat(self):
        print("Eating")


class FlyingAnimal(Animal):
    def fly(self):
        print("Flying")


class SwimmingAnimal(Animal):
    def swim(self):
        print("Swimming")


class Bird(FlyingAnimal):
    def fly(self):
        print("Flying")
    pass


class Fish(SwimmingAnimal):
    pass


class Duck(Bird, SwimmingAnimal):  # 다이아몬드 상속
    pass

duck = Duck()
duck.eat()          # Eating
duck.fly()          # Flying
duck.swim()         # Swimming

✅ 믹스인을 활용해보자


class Animal:
    def eat(self):
        print("Eating")
        
        
class FlyingMixin:
    def fly(self):
        print("Flying")


class SwimmingMixin:
    def swim(self):
        print("Swimming")

class Bird(Animal, FlyingMixin):        # 먹는 건 Animal로 받고, 나는 걸 섞자
    pass

class Fish(Animal, SwimmingMixin):      # 먹는 건 Animal로 받고 수영하는 걸 섞자
    pass

위와 같이 동물독립적인 기능(날기, 수영하기)등을 혼합(Mixin) 하면서 다이아몬드 상속을 이겨낼 수 있는 것이다.
-> 💡 믹스인의 역사(아이스크림 기본 맛에 땅콩, 쿠키, 사탕 등을 넣어주기)와 비슷해 보이기도 한다.

  1. 동물 + 날으는 특징 -> 새
  2. 동물 + 수영하는 특징 -> 물고기

📝 믹스인도 그저 동일한 일반적인 상속이며, 다중 상속이다.
하지만, 상속이 특정한 기준을 가지게 되는 데 상위 클래스의 모든 기능을 상속 하는 것이 아니라, 특정한 기능들을 상속하는 것이다.

Mixins들은 대개, 스스로 인스턴스화 되지 않고 다른 클래스와 결합하여 특정한 기능 세트들을 제공하는 데 중점을 둔 클래스들이다!
즉, 다른 사용자 정의된 클래스들과 함께 상호작용하는 것이다.

🧐 아래 Django rest_framwork의 Mixins 예시들을 살펴보자

rest_framework Mixins

rest_frameworkReadOnlyModelViewSet의 코드를 보면 mixins의 RetrieveModelMixin, ListModelMixin, GenericViewSet다중 상속 받고 있는 것을 확인할 수 있다.

class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    """
    A viewset that provides default `list()` and `retrieve()` actions.
    """
    pass

✅ 먼저 RetreieveModelMixin을 보자

class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()					# get_object
        serializer = self.get_serializer(instance)		# get_serializer
        return Response(serializer.data)

🤔🤔 get_object, get_serializer 메서드는 어디에 구현되어 있을까?

⛔️ 파이참의 도움을 받아 선언부를 찾아가보려 시도하면 아래와 같은 문구가 발생한다.

어디에도 구현되어 있지 않다고 말한다!!!
⛔️ ListModelMixin도 마찬가지이다!!

✅ 그럼 마지막 GenericViewSet을 들어가보자
ViewSetMixinGerericAPIView를 다중 상속 받고 있다!

class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass

GenericViewSet에 포함되어 있는 주석을 보면 기본적으로 어떤 동작도 제공하지 않지만 get_object, get_queryset과 같은 일반 뷰의 기본 집합은 포함한다고 되어 있다!

GenericAPIView의 코드를 보자

class GenericAPIView(views.APIView):
    """
    Base class for all other generic views.
    """
    # You'll need to either set these attributes,
    # or override `get_queryset()`/`get_serializer_class()`.
    # If you are overriding a view method, it is important that you call
    # `get_queryset()` instead of accessing the `queryset` property directly,
    # as `queryset` will get evaluated only once, and those results are cached
    # for all subsequent requests.
    queryset = None
    serializer_class = None

    # If you want to use object lookups other than pk, set 'lookup_field'.
    # For more complex lookup requirements override `get_object()`.
    lookup_field = 'pk'
    lookup_url_kwarg = None

    # The filter backend classes to use for queryset filtering
    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS

    # The style to use for queryset pagination.
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def get_queryset(self):
		...
        
    # 여기!
    def get_object(self):
		"""
        Returns the object the view is displaying.

        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj
    
    # 여기!
    def get_serializer(self, *args, **kwargs):
		"""
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)

    def get_serializer_class(self): 
    	...

    def get_serializer_context(self):
    	...

    def filter_queryset(self, queryset):
    	...

📝 GenericAPIView의 맴버를 보면 ListModelMixinRetreieveModelMixin에 구현되어 있지 않은 get_object, get_serializer등과 같은 메서드들이 구현되어 있는 것을 볼 수 있다!!

🔥 즉! GenericViewSet에 [ListModelMixin, RetreieveModelMixin]와 같은 특정 mixin 기능들이 합해져 ReadOnlyModelViewSet ViewSet이 완성되는 것이다!!

Mixin

Mixin 클래스는 혼자 사용될 수가 없다.
우리는 Mixin에 정의되지 않았지만 사용하는 코드들은 다른 어딘가에 정의되어 있을 것으로 예상한다.
즉, 우리가 정의한 Mixin 코드들은 다른 사용자 정의 코드들과 혼합 될 것을 전제하여 개발하고 구현하는 것이다!

Mixin은 Class에 선택적으로 기능을 제공하고 싶을 때, 다양한 클래스에서 반복되는 특정 기능을 사용하고 싶을 때 활용할 수 있다.

Mixin도 특별할 것 없은 클래스이다. 헷갈릴 필요가 없다!
하지만, 구체적인 특정한 기능만을 구현하여 다른 사용자 정의에 도움을 주는 클래스로 활용되는 특징을 가지고 있는 것 뿐이다! <- 이런 기능과 특징을 지닌 Class를 Mixin이라고 부르자고 약속한 것!

이를 통해 클래스, 모듈간 의존성을 줄이고 조합이라는 특징을 활용해 더 유연하게 코드를 구현할 수 있게 되는 것이다!

클래스에 필요한 특징이 구현된 기능 클래스를 가져와 조합 할 수 있도록 활용할 수 있는 방법인 것이다!

참고 자료

profile
달려보자

0개의 댓글