[에러 해결] Django Many-to-Many 관계 사용 시 “id 필드에 값이 필요합니다” 에러 원인과 해결 방법

Django 프로젝트에서 사용자 정의 User 모델과 그룹(Group) 간의 Many-to-Many 관계를 설정할 때, 사용자 ID가 분명히 존재함에도 불구하고 "needs to have a value for field "id" before this many-to-many relationship can be used."라는 에러가 발생하는 경우가 있습니다. 이번 기술 문서에서는 이 미묘한 에러의 원인을 분석하고, 개발자가 직관적으로 이해하고 해결할 수 있는 방법을 제시합니다.

1. 에러 발생 상황

개발자는 Django의 AbstractUser를 상속받아 사용자 정의 User 모델을 생성했습니다. 이 모델은 추가 필드(bio)를 포함하며, 사용자 생성 시 이 User 모델을 사용합니다.

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.first_name+' '+self.last_name

이어서 UserService 내의 create 메서드에서 새로운 사용자를 생성하고, 해당 사용자에게 여러 그룹(groups)을 할당하려는 로직을 구현했습니다. 이 과정은 @transaction.atomic 데코레이터로 감싸져 있습니다.

from app_auth.models import User
from django.contrib.auth.models import Group
from django.db import transaction

class UserService:

    @transaction.atomic
    def create(self,
               username: str,
               email: str,
               first_name: str,
               last_name: str,
               groups: list[Group],
               bio: str | None = None) -> User:

        user = User.objects.create_user(
            username=username,
            first_name=first_name,
            last_name=last_name,
            email=email,
            bio=bio, #TODO: XSS Sanitize
        )

        print("USER ID: "+str(user.id))

        for group in groups:
            print("GROUP ID: "+str(group.pk))

        user.groups.set(groups) # 에러 발생 지점

        user.save()
        return user

코드를 실행하면 다음과 같은 에러 메시지가 발생합니다.

"<User: XXXX XXXXX>" needs to have a value for field "id" before this many-to-many relationship can be used.

문제는 디버그 출력을 통해 USER ID: 4와 GROUP ID: 4와 같이 사용자(User)와 그룹(Group) 모두 유효한 ID를 가지고 있음을 확인했음에도 불구하고 위 에러가 발생한다는 점입니다. 이는 일반적으로 ‘저장되지 않은 객체에 Many-to-Many 관계를 설정하려 할 때’ 발생하는 에러 메시지와는 상충되어 혼란을 야기합니다.

2. 명확한 발생 원인

이 에러는 겉으로는 ‘ID가 없다’고 말하지만, 실제 원인은 조금 더 미묘합니다. 핵심은 Django ORM(Object-Relational Mapper)이 Many-to-Many 관계를 설정하려는 시점에서 해당 User 객체를 ‘완전히 데이터베이스에 저장되고 유효한 주 키(Primary Key)를 가진 인스턴스’로 인식하지 못하기 때문입니다.

  • User.objects.create_user()의 역할: create_user 메서드는 내부적으로 사용자를 생성하고 데이터베이스에 저장하며, 이에 따라 사용자 객체(user)에 ID(user.id)를 할당합니다. 그래서 print("USER ID: "+str(user.id))에서 ID가 정상적으로 출력되는 것입니다.
  • Many-to-Many 관계 설정의 요구사항: Django의 Many-to-Many 관계(예: user.groups.set(groups))는 내부적으로 중간 테이블(auth_user_groups 또는 커스텀 User 모델의 해당 테이블)에 데이터를 삽입하는 데이터베이스 작업을 수행합니다. 이 작업은 User 객체와 Group 객체 모두 데이터베이스에 이미 존재하고 유효한 primary key를 가지고 있어야만 가능합니다.
  • 내부적인 ORM 상태 불일치: create_user 메서드가 User 객체를 저장했음에도 불구하고, 특정 상황(특히 @transaction.atomic 블록 내에서)에서는 Django ORM의 Many-to-Many 관계 관리자가 User 객체의 상태를 ‘완전히 영속화(fully persisted)되고 관계 설정을 위한 준비가 완료된’ 상태로 즉시 인식하지 못할 수 있습니다. 이는 ORM 내부의 캐싱, 트랜잭션 관리, 또는 커스텀 모델의 특정 동작 방식과 미묘하게 얽혀 발생할 수 있는 ‘상태 동기화’ 문제로 볼 수 있습니다. 즉, user.id 값은 존재하지만, ORM 입장에서는 해당 객체가 Many-to-Many 관계를 맺을 준비가 ‘미흡’하다고 판단하는 것입니다.

3. 해결 방법 및 코드 예시

이 문제의 가장 확실하고 권장되는 해결책은 Many-to-Many 관계를 설정하기 직전에 user.save()를 명시적으로 한 번 더 호출하여 ORM에 User 객체의 ‘영속성’을 재확인시켜주는 것입니다. 비록 create_user()가 이미 저장 작업을 수행했지만, 명시적인 save() 호출은 ORM의 내부 상태를 최신화하고 M2M 관계 설정에 필요한 모든 조건이 충족되었음을 보장하는 역할을 합니다.

해결 방법 1: Many-to-Many 관계 설정 전 user.save() 호출

user.groups.set(groups) 호출 이전에 user.save()를 추가합니다.

from app_auth.models import User
from django.contrib.auth.models import Group
from django.db import transaction

class UserService:

    @transaction.atomic
    def create(self,
               username: str,
               email: str,
               first_name: str,
               last_name: str,
               groups: list[Group],
               bio: str | None = None) -> User:

        user = User.objects.create_user(
            username=username,
            first_name=first_name,
            last_name=last_name,
            email=email,
            bio=bio, #TODO: XSS Sanitize
        )

        print("USER ID: "+str(user.id))

        for group in groups:
            print("GROUP ID: "+str(group.pk))

        # 핵심 해결책: user.groups.set() 호출 전에 명시적으로 user.save() 호출
        user.save() # 이 줄을 추가합니다.

        user.groups.set(groups)

        # 마지막 user.save()는 user 객체의 다른 필드 변경사항이 있다면 필요하지만,
        # groups.set()은 자체적으로 DB에 반영되므로 필수 아님.
        # 여기서는 중복되거나 의미 없을 수 있으므로 필요에 따라 제거 가능.
        # user.save()
        return user

위 코드에서 user.save()를 user.groups.set(groups) 직전에 추가하면, 에러 없이 Many-to-Many 관계가 올바르게 설정됩니다. Many-to-Many 필드 변경 후의 user.save()는 strictly 필요하지 않을 수 있지만, 안전을 위해 남겨두거나 다른 필드 변경사항이 있을 경우 유용합니다.

해결 방법 2 (고급/최후의 수단): user.refresh_from_db() 사용

만약 user.save()만으로 해결되지 않거나, ORM 상태 불일치가 더 복잡하게 나타나는 경우, user.refresh_from_db()를 사용하여 객체 상태를 데이터베이스에서 강제로 다시 로드하는 방법을 고려할 수 있습니다. 이는 객체가 데이터베이스에 확실히 존재하며, 현재 파이썬 객체의 상태가 DB와 동기화되었음을 보장합니다.

# ... (생략) ...
        user = User.objects.create_user(
            username=username,
            first_name=first_name,
            last_name=last_name,
            email=email,
            bio=bio, #TODO: XSS Sanitize
        )

        print("USER ID: "+str(user.id))

        # user.save() 대신 user.refresh_from_db()를 사용할 수 있음
        user.refresh_from_db() # 데이터베이스에서 객체 상태를 다시 로드

        user.groups.set(groups)
        # ... (생략) ...

이 방법은 일반적으로 필요하지 않지만, 매우 드물게 발생하는 ORM 상태 문제에 대한 강력한 해결책이 될 수 있습니다.

4. 향후 예방을 위한 팁

이러한 유형의 에러를 예방하고 견고한 Django 애플리케이션을 구축하기 위한 몇 가지 팁입니다.

  1. Many-to-Many 관계 설정 전 저장 필수: 모든 Many-to-Many 관계는 양쪽 객체가 모두 데이터베이스에 저장(즉, 유효한 primary key 보유)된 상태에서만 사용 가능합니다. 객체를 생성한 후에는 항상 .save()를 호출하여 데이터베이스에 저장했는지 확인하세요.
  2. 명시적인 .save() 호출 활용: create() 메서드나 create_user()와 같이 객체를 생성하며 저장하는 메서드를 사용했더라도, Many-to-Many 관계를 설정하는 등 복잡한 ORM 작업을 수행하기 전에는 .save()를 명시적으로 한 번 더 호출하는 것이 안전할 때가 있습니다. 이는 ORM의 내부 상태를 동기화하는 데 도움이 됩니다.
  3. 트랜잭션(@transaction.atomic)의 이해: 트랜잭션 블록 내에서 작업할 때는 모든 데이터베이스 변경 사항이 블록 종료 시점에 일괄적으로 커밋됩니다. 이로 인해 때때로 객체의 ORM 상태가 즉시 최신화되지 않는 것처럼 보이는 착시 현상이 발생할 수 있습니다. 문제가 발생하면 트랜잭션의 동작 방식을 다시 한번 검토해 보세요.
  4. 로깅 및 디버깅 활용: 에러 발생 시 user.id, group.pk 등의 값을 정확히 출력하여 예상과 실제 동작 간의 차이를 빠르게 파악하는 것이 중요합니다. 이는 문제 해결의 출발점이 됩니다.
  5. Django 공식 문서 참고: Django ORM의 동작 방식, 특히 Many-to-Many 관계와 커스텀 User 모델에 대한 공식 문서를 정기적으로 참고하여 최신 정보와 모범 사례를 따르는 것이 좋습니다.

이 문서를 통해 Many-to-Many 관계 설정 시 발생하는 "needs to have a value for field 'id'" 에러를 효과적으로 해결하고, 향후 유사한 문제가 발생하는 것을 예방하는 데 도움이 되기를 바랍니다.

댓글 남기기