앞서 포스팅한 circular import 에 대한 생각 에서 해당 문제가 발생한 경우, 계층 구조를 먼저 생각해보자는 이야기를 했었다.

이번에는 이어서 class 의 공통된 기능을 뽑아낼 때 계층 구조를 잘 잡기 위한 방법을 생각해보았다.

일반적으로 Mixin class 는 하위 클래스들의 공통을 묶어 상위 abstract 로 뽑는데 사용되나, 가끔은 상위 클래스들에 공통된 기능을 붙이는 용도로도 사용된다.

django 로 가정하고 예시를 든다.

# A app
class A(models.Model):
    nickname = models.CharField(...)

    def validate_nickname(self):
        ...

# B app
class B(models.Model):
    nickname = models.CharField(...)

    def validate_nickname(self):
        ...

이렇게 공통된 field 와 method가 있는 경우, 파편화를 막기 위해 abstract model 을 이용하여 상위의 abstract class 를 만든다.

# C app
class NicknameInfo(models.Model):
    class Meta:
        abstract = True

    nickname = models.CharField(...)

    def validate_nickname(self):
        ...


# A app
from C import NicknameInfo

class A(NicknameInfo):
    ...

# B app
from C import NicknameInfo

class B(NicknameInfo):
    ...

이제 nickname 관련 feature 는 NicknameInfo 한군데만 고치면 A, B class 모두 영향을 받게 되었다.

계층 구조를 그려보면 다음과 같다.

    C(NicknameInfo)
   /  \
 A(A)  B(B)

이 경우 A, B app 은 C 에 의존하고 있다. 각 A, B 앱에 import 된 C를 보면 알 수 있다. 이는 A, B 앱을 쓰는 모든 곳에서 Nickname 을 사용할 수 있다고 상정한 것이다.

하지만 만약에 어떤 앱에서 A, B 앱의 여러 model을 호출하는 기능을 추가하고 싶다면 어떻게 해야할까?

예를 들어 A, B 를 이용하여 score 를 매기고 그 score 를 특정 앱에서만 쓰는 경우를 생각해보자.

    C(NicknameInfo)
   /  \
 A(A)  B(B)
   \  /
    D(Scorable)

A, B 를 참조하므로 D는 당연히 A, B 에 의존성을 가지게 된다. 하지만 Scorable 이라는 공통 class 로 묶어내고 싶다면 A, B 에서 D 앱 내의 model 을 상속 받는 경우를 가끔 보게 된다.

# D app
class Scorable(models.Model):
    class Meta:
        abstract = True

    necessary = models.Field
    field = models.Field
    items = models.Field

    def score(self):
        return len(necessary) + len(field) + len(items)

# A app
from D import Scorable

class A(Scorable):
    necessary = models.Field
    field = models.Field
    items = models.Field
    ...

# B app
from D import Scorable

class B(Scorable):
    necessary = models.Field
    field = models.Field
    items = models.Field
    ...

이렇게 된다면 개념도 상으로는 D 가 A, B 를 의존하기를 기대했지만 실상은 A, B 가 D를 의존하게 된다. 추후 D 에서 A 나 B 의 다른 model 을 참조하려 한다면 쉽게 순환 참조 오류를 만날 수 있다.

그렇기에 이런 경우는 A, B 에서 D 를 상속받을게 아니라 오히려 D에서 A, B 를 상속받아야 순서상 옳다.

# D app
from A import A
from B import B

class Scorable:
    neccssary: list[str]
    field: list[int]
    items: list[boolean]

    def score(self):
        return len(necessary) + len(field) + len(items)


class ScorableA(Scorable, A):
    ...

class ScorableB(Scorable, B):
    ...
    
# A app
class A(models.Model):
    ...

# B app
class B(models.Model):
    ...

이로써 A, B 앱 내에서는 D 와 아무런 의존성이 없고, D 에서는 ScorableA 혹은 ScorableB 만 사용하면 Scorable의 기능을 사용할 수 있도록 의존성 정리가 되었다.

또한 D 에서 추가로 A, B 앱 내의 다른 model 이 필요한 경우에도 순환 참조 위험 없이 import 할 수 있게 되었다.