티스토리 뷰

일단 우리가 Django에서 제공받는 기본 앱들 중에 "auth"라는 것이 있는데 이 녀석이 기본적인 사용자 객체와 로그인, 로그아웃, 권한 같은 기능들을 제공해준다. 그래서 이 녀석의 도움으로 우리만의 사용자 정보를 저장하는 객체를 만들어주려고 한다. 이때 몇 가지 전제조건들이 붙는데

 

  1. 우리는 로그인, 로그아웃과 관련된 기능들은 JWT(JSON Web Token)라고 하는 앞서서 몇몇 포스팅에도 말했던 기능을 이용해 로그인, 로그아웃을 구현할 거다.
    뭔가 대단해 보이고 어려워 보이지만 별거 없는 내용이니 긴장하거나 어려워할 필요는 없다. 그냥 어려워 보일뿐 실제 작업은 어렵지 않다.
  2. JWT를 위해 사용자 정보에는 secret_key를 추가할 예정이다. JWT로 발급된 토큰의 암호화된 서명의 키로 secret_key로 활용할 예정이기 때문이다.
  3. Django에서 제공하는 기본 사용자 정보는 사용하지 않고 내 입맛에 맞는 사용자 정보를 만들어 사용할 거다. 왜냐하면 제공되는 사용자 정보에는 쓸데없는 내용들이 너무 많다.
  4. Django에서 기본적으로 제공하는 권한체계는 사용하지 않고 Java Spring Security의 "다중 권한" 체계를 차용하여 개발할 예정이다. 이게 뭔 소린가 하면 아래의 부가 설명을 읽어주길 바란다.

다중 권한?

다중 권한이라는게 말이 어렵지 사실 별 이야기는 아니니 겁먹을 필요는 없는 개념이다. 예를 들어서 우리가 만드려는 사이트에 아래와 같은 기능들이 있다고 가정하자.

 

  • 일반회원 : 일반적인 회원
  • 우수회원 : 일반적인 회원보다 더 많은 기능을 갖는 회원
  • 컨텐츠 관리자 : 관리자로써 컨텐츠를 관리하는 권한
  • 회원 관리자 :  관리자로써 회원의 상태를 관리하는 권한
  • 슈퍼관리자 회원 : 사이트의 모든 권한을 갖는 회원

권한의 종류를 이렇게 세분화해서 관리할 수 있는데 관리자 중에서도 컨텐츠만 관리할 수 있거나 회원만을 관리할 수 있거나 혹은 시스템만 관리할 수 있는 권한만 갖을 수도 있다. 회원의 역할에 따라서 접근할 수 있는 컨텐츠 영역을 분리하거나 권한을 세부적으로 관리할 일이 많아질 것이다. 그래서 이러한 권한의 모양을 세부적으로 나눠서 사용자에게 권한의 등급을 매겨 상하로 나누는 것이 아니라 권한 하나 하나를 별개로 부여하여 여러 권한을 갖도록 하는 것을 다중 권한 체계라고 이해하면 된다.

 

분명히 쉽게 이야기하겠다고 했지만 쉽게 이해할 수 있다고는 말하지 않았다.

다시 간편하게 요약해서 말하자면 계층구조로 권한을 갖는 것이 아니라 업무별로 권한을 부여하는 것이라고 생각하면 된다는 이야기다. 이런 권한구조를 갖는 까닭은 현재 서비스에서는 필요없을지 모르지만 추후에 추가되는 어떠한 컨텐츠가 있다고 가정하자. 거기에 맞는 권한을 추가해야한다면 이런 다중권한 구조가 보다 유기적으로 권한 체계를 관리할 수 있다는 장점이 있기 때문이다.

 

사실 Django에서 제공해주고 있는 auth 앱에는 방금이야기 했던 것들보다 더 복잡하고 다양한 권한과 사용자 그룹을 지정할 수 있고 관리할 수 있지만 우리는 Django를 Restful API 라고 부르는 서버의 형태로 만들 것이고 이러한 개발 방향은 auth에서 제공해주는 형태의 권한체계와는 어울리지 않아 별도로 개발하여 적용할 예정이다.

 

뭔가 말이 많아서 어렵고 복잡해 보이지만 그냥 잘 따라서 하다 보면 기능들이 완성이 될 테니 너무 걱정하지 말아라. 코딩은 문제를 걱정하는 것이 아니라 고민해야 하고 고민 뒤에 따라오는 아이디어들을 적용하고 수정하면서 해결하는 거니까.

 

DB 초기화

사용자 정보를 Django에서 제공해주는 것이 아니라 내 마음대로 커스터마이징하기 위해서는 DB를 초기화하는 과정이 필요하다. 나중에 다 만들어두고 DB에 반영하려고 하면 DB의 버전이 서로 맞지 않다면서 오류를 뱉어버리는 이슈가 생기기 때문인데 DB를 초기화 하는 과정은 꽤 단순하니 걱정하지 말자.

 

MySQL Workbench 프로그램을 실행하자.

시작을 누르고 MySQL Workbench를 입력하면 앱이 검색되니 실행만 하시면 됩니다.
지난번에 만들어둔 Localhost 커넥션 카드를 더블클릭하자
지난 번에 만들었던 funny_pictures 스키마에 마우스 오른쪽 버튼 클릭, 컨텍스트 메뉴에서 Drop Schema를 선택한다.
Drop Now를 클릭한다.

위의 과정을 진행했다면 지난 번 우리가 만들었던 funny_pictures 스키마가 지워진것을 알 수 있다. 당황하지 말고 funny_pictures 스키마를 다시 생성해주자.

스키마 패널 빈공간에 마우스 오른쪽 버튼 클릭 후 컨텍스트 메뉴에서 Create Schema 메뉴 클릭

오른쪽 패널에서 funny_pictures 스키마를 새로 만든다. 이때 주의할 점은 Charset/Collation 항목에서 utf8과 utf8_general_ci를 선택해야한다는 거다. 이 선택을 안하면 나중에 한글이 깨져서 저장되는 기이한 현상을 목격하게 될 거다. 우측 하단의 "Apply" 버튼을 클릭한다.

 

Apply 버튼을 클릭한다.
Finish 버튼을 클릭한다.
새로 생성된 funny_pictures 스키마를 확인할 수 있다.

초기화한다고 해놓고 통째로 지웠다가 새로 만들어내는... 조금 무식한 방법이지만 이게 제일 빠르고 편한 방법이니 너무 뭐라고 하지는 말자.

 

 

 

이제 account 앱에 있는 models.py 파일을 열어서 아래와 같이 코딩하도록 하자.

account.models

from datetime import datetime
import uuid

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager

USER_STATUS = {
    'ACTIVE'  : 'ACTIVE' # 정상
    , 'STOP'  : 'STOP' # 정지(탈퇴)
    , 'PAUSE' : 'PAUSE' # 계정 휴면
    , 'BAN'   : 'BAN' # 계정 정지
}

ROLES = {
    'ROLE_NORMAL'     : 'NORMAL' # 일반 사용자
    , 'ROLE_MANAGER'  : 'MANAGER' # 일반 관리자
    , 'ROLE_SUPER'    : 'SUPER' # 슈퍼 관리자
}

# 사용자 정보 관리 클래스
class UserManager(BaseUserManager):
    def _create_user(self, email, password, **extra_fields):
        
        if not email : # 이메일이 없다면
            raise ValueError('이메일은 필수 요소입니다.')

        if not password : # 패스워드가 없다면
            raise ValueError('패스워드는 필수 요소입니다.')
        
        # 이메일 주소를 소문자로 변환하는 과정을 거친 뒤에 저장한다.
        email = self.normalize_email(email) 
        
        # 사용자 모델 객체를 생성한다.
        user = self.model(
            email           = email
            , is_active     = extra_fields.get('is_active')
        )

        # 사용자 패스워드는 Django에서 제공해주는 해시화 과정(SHA 256)을 거쳐서 저장한다.
        user.set_password(password)

        # 실제로 DB에 사용자 정보를 저장한다.
        user.save(using=self._db)

        # 기본적으로 모든 사용자는 일반사용자 권한을 갖게된다.
        self.create_auth(user, ROLES.get('ROLE_NORMAL', 'NORMAL'))

        # 스탭 권한을 부여할지 확인 후 MANAGER 권한을 부여한다.
        if extra_fields.get('is_staff') is True :
            self.create_auth(user, ROLES.get('ROLE_MANAGER', 'MANAGER'))

        # 슈퍼 관리자 권한을 부여할지 확인 후 SUPER 권한을 부여한다.
        if extra_fields.get('is_superuser') is True :
            self.create_auth(user, ROLES.get('ROLE_SUPER', 'SUPER')) 

        return user
    

    # 일반 사용자 생성
    def create_user(self, email, password=None, **extra_fields): 
        extra_fields['is_active'] = False
        extra_fields['is_staff'] = False
        extra_fields['is_superuser'] = False
        
        return self._create_user(email, password, **extra_fields)

    # 스탭 사용자 생성
    def create_staff(self, email, password=None, **extra_fields):
        extra_fields['is_active'] = True
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = False
        
        return self._create_user(email, password, **extra_fields)

    # 슈퍼 관리자 생성
    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields['is_active'] = True
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = True

        return self._create_user(email, password, **extra_fields)

    # 권한 정보 등록
    def create_auth( self, user, role=ROLES.get('ROLE_NORMAL', 'NORMAL')):
        role_obj = Auth(
            user = user,
            role = role
        )

        role_obj.save(using=self.db)

        return role_obj


# 사용자 계정 테이블 모델
class User(AbstractBaseUser):
    id           = models.BigAutoField(primary_key=True)
    email        = models.EmailField(max_length=254, unique=True) # 이메일 주소
    secret       = models.UUIDField(default=uuid.uuid4) # 사용자 서명용 비밀키
    status       = models.CharField(max_length=10, default=USER_STATUS.get('ACTIVE', 'ACTIVE')) # 사용자 현재 상태
    is_active    = models.BooleanField(default=False) # 계정 활성화 여부
    created_at   = models.DateTimeField(auto_now_add=True) # 계정 생성일
    updated_at   = models.DateTimeField(auto_now=True) # 계정 수정일
    
    objects = UserManager() # 사용자 정보를 관리하는 클래스는 지정한다.

    USERNAME_FIELD = 'email' # 사용자 이름으로 사용될 필드의 이름을 지정한다.

    # 사용자 PK 값을 가져오기위한 함수
    def get_id(self):
        return self.id
    

# 권한 테이블 모델
class Auth(models.Model):
    user = models.ForeignKey(User, related_name='auths', on_delete=models.CASCADE)
    role = models.CharField(max_length=30, default=ROLES.get('ROLE_NORMAL', 'NORMAL'))

 

코드가 길어보이지만 찬찬히 뜯어보면 크게 겁먹을 내용은 아니니 걱정하지 말자.

일단 User 클래스에 대해서 먼저 설명을 진행하겠다.

 

User 클래스

from datetime import datetime
import uuid

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
  
/* 생략 */
  
# 사용자 계정 테이블 모델
class User(AbstractBaseUser):
    id           = models.BigAutoField(primary_key=True)
    email        = models.EmailField(max_length=254, unique=True) # 이메일 주소
    secret       = models.UUIDField(default=uuid.uuid4) # 사용자 서명용 비밀키
    status       = models.CharField(max_length=10, default=USER_STATUS.get('ACTIVE', 'ACTIVE')) # 사용자 현재 상태
    is_active    = models.BooleanField(default=False) # 계정 활성화 여부
    created_at   = models.DateTimeField(auto_now_add=True) # 계정 생성일
    updated_at   = models.DateTimeField(auto_now=True) # 계정 수정일
    
    objects = UserManager() # 사용자 정보를 관리하는 클래스는 지정한다.

    USERNAME_FIELD = 'email' # 사용자 이름으로 사용될 필드의 이름을 지정한다.

    # 사용자 PK 값을 가져오기위한 함수
    def get_id(self):
        return self.id

/* 생략 */

일단 User 클래스는 굉장히 중요한 클래스인데 위에서 보면 알겠지만 AbstractBaseUser라는 클래스를 상속받아 만들어진다. 이 클래스는 Django의 auth 앱에 있는 모델 클래스로 사용자 정보의 기본적인 형태를 갖는 추상적인 형태의 클래스라고 생각하면 된다. 궂이 이해할 필요는 없고 뭐.. 그냥 매우 기본적으로 사용자가 가져야할 최소한의 정보를 갖는 어떤 클래스를 상속받아서 User 클래스를 만들었다고 생각하면 된다.

 

AbstractBaseUser에는 password와 last_login 필드가 포함되어있어서 궂이 선언해주지 않아도 자동으로 포함되어 DB에 반영된다. 그리고 USERNAME_FIELD 항목에는 사용자 로그인 시 아이디로 사용되는 필드의 이름을 지정하면 된다.

우리는 email 로 지정했다.

 

그리고 앞으로도 계속 사용되는 개념인 primary_key 라는 것에 대해서 이야기 해보자.

통상적으로 Primary key(속칭 PK)라고 부르는 이 개념은 간단하게 이야기 하면 주민등록번호다. 테이블 내의 데이터를 특정지을 수 있는 고유한 값이라고 생각하면 된다. 그리고 거의 왠만한 테이블에는 이 PK 값이 존재한다.

보통 PK는 양의 정수로 표현하며 Big Integer라고 부르는 데이터 형식으로 저장하게 되는데 이는 2^64(2의 64제곱)만큼의 데이터를 저장할 수 있다. 양의 정수만 사용하는 경우에는 18,446,744,073,709,551,615 까지 숫자를 지정할 수 있다.

 

우리가 로그인할 때 사용하는 아이디(여기서는 email)도 다른 데이터와 구분할 수 있는 유일한 값인데 왜 궂이 id라는 숫자를 이용해 데이터를 구분하는 가에 대한 질문이 있을 수 있는데 이는 데이터의 크기와 DB의 속도와 관련이 있다. DB는 데이터를 빠르게 탐색하기 위해 인덱싱이라는 작업을 하게 되는데 이는 데이터를 미리 메모리라고 부르는 기억 장소에 저장하고 있다가 "어느어느 데이터 내놔"라고 하면 바로바로 전달 할 수 있게 하는 준비작업이라고 생각하면 된다.

근데 이 작업을 위해서 메모리에 올릴때 아무래도 문자(문자는 인코딩 방식에 따라 다르지만 보통 영문 한 글자당 2바이트의 메모리를 필요로 한다.)보다는 숫자가 공간을 덜 잡아먹고 공간을 덜 잡아먹는다는 이야기는 곧 더 빠르게 동작할 수 있고 저장공간을 절약할 수 있다는 이야기라서 그렇다.

 

위의 내용을 대충 요약하면 이렇다.

 

  • AbstractBaseUser를 상속받은 User클래스는 password와 last_login 필드를 기본적으로 갖고 있다.
  • USERNAME_FIELD는 로그인 시 사용하는 사용자 아이디 필드명을 지정하는 항목이다.(우리는 email 필드를 사용자 이름으로 사용한다)
  • PK(Prime key)는 데이터를 특정하기 위한 용도의 데이터 필드다.
  • PK는 테이블당 "하나의 필드만 존재"하며 일반적으로 BIG INTEGER로 데이터 타입을 잡아준다.
  • UUID 라는 것은 Universally unique identifier라는 것으로 범용 고유 식별자라는 의미란다. 그냥 고유한 중복되지 않은 어떠한 값을 의미하는데 이것을 만들어내는데 몇가지 규칙이 있고 이번에 uuid4 라는 규칙으로 secret 값을 만들어낸다.
  • BooleanField는 참과 거짓을 저장하는 필드지만 실제로 DB에서는 INT(Integer) 값으로 0 과 1을 이용해 참과 거짓을 분별한다.(0은 False, 1은 True)
  • EmailField는 기본적으로는 글자를 저장하는 Char(Character) 필드지만 Django에서 저장값이 이메일 형태인지 내부적으로 검사를 수행해주는 편리한 기능을 갖고 있어서 EmailField로 지정했다.
  • objects = UserManager() 라는 코드는 나중에 UserManager 클래스 설명할 때 같이 설명하겠다.

 

정도로 정리해볼 수 있겠다. 

이제 Auth 클래스를 정리해보자.

 

Auth 클래스

/* 생략 */
# 권한 테이블 모델
class Auth(models.Model):
    user = models.ForeignKey(User, related_name='auths', on_delete=models.CASCADE)
    role = models.CharField(max_length=30, default=ROLES.get('ROLE_NORMAL', 'NORMAL'))

이 테이블에서 주요하게 봐야할 부분은 PK를 담당할 필드가 없다는 것과 user라는 ForeignKey(속칭 FK) 필드다. 이것은 User 테이블과 연계하여 저장한다는 이야긴데 하나씩 정리하면서 진행하도록 하자.

 

일단 Auth라는 테이블은 사용자의 권한을 저장하는 테이블로써 사실 PK 값이 필요하지 않다. 예를 들어 User의 id 필드가 1번인 사람의 권한에는 어떤 것이 있는지 확인할 때 user라는 FK 필드값이 1번인 데이터를 가져올 수 있기만 하면 되기 때문이다. 그래서 PK 값을 저장하는 필드를 궂이 포함하지 않았다.

 

그럼 FK가 무엇인지 알아볼 필요가 있다. 자세하게 설명하면 좀 복잡하니까 간단하게 설명하자면 다른 테이블과 연결하는 연결고리라고 생각하면 되겠다. 예를 들어 User 테이블의 데이터에는 회원의 로그인을 위한 이메일 정보와 패스워드가 있다면 Auth에는 회원의 권한들을 저장하는 역할을 한다고 하자. 이때 "어떤 회원"의 권한을 저장한 것인지 확인하기 위해서는 User데이터 중 식별할 수 있는 키가 필요한데 이 키를 Auth 입장에서는 FK라고 부르는 거다. (User테이블에서는 PK라고 부르고)

 

이런 테이블 구조에서는 세가지 형태의 테이블 관계가 나타나는데 One to One, One to Many, Many to Many 관계가 있다. 현재 User테이블과 Auth의 관계는 One to Many 관계를 갖고 하나의 사용자가 여러 권한을 갖을 수 있도록 설계했다고 생각하면 되겠다.

 

참고로 related_name 은 User에서 데이터를 조회할 때 Serializing 이라는 과정을 거치게 되는데 그 때 어떤 필드에 Auth 정보를 담을지 정의하는 거라고 생각하면 되겠지만 지금 시점에서는 이해가 어려울 테니 나중에 다시 한번 더 설명하는 시간을 갖겠다. 그리고 on_delete 라고 해서 현재 연결되어있는 데이터의 부모격인 User의 데이터가 삭제되었을 때 이 Auth의 데이터를 어떻게 처리할 지에 대한 옵션이라고 생각하면 되는데 이 옵션의 종류는 아래와 같다.

 

  • CASCADE : 원 데이터의 삭제 또는 수정시 참조 데이터를 함께 삭제 또는 수정한다.
  • SET_NULL: 원 데이터의 삭제 또는 수정시 참조 데이터는 NULL로 수정한다.
  • SET_DEFAULT: 원 데이터의 삭제 또는 수정시 참조 데이터를 기본 값으로 수정한다.
  • RESTRICT: 참조데이터가 남아있는 경우 원 데이터의 삭제, 수정이 불가하다.
  • NO_ACTION: 원 데이터의 삭제 또는 수정의 행위가 참조 데이터에 영향을 끼치지 않는다.(DB의 무결성 원칙이 깨지게 된다.)

일반적으로 CASCADE 방식을 사용하는 것이 좋다고 생각하면 되겠다.

왜냐하면 어떤 사용자 값을 삭제했는데 Auth 테이블에 값이 남아서 부모 값도 없이 의미 없는 데이터가 되기 때문인데 이런 값을 고아 참조 데이터라고 해서 아무데도 쓸 수 없는 데이터가 되어 용량만 차지하는 쓸모 없는 데이터가 되기 때문이다. 그러므로 이런 데이터가 생기지 않도록 CASCADE 방식을 추천한다.

 

자, 이제 UserManager 클래스에 대해서 설명할 타이밍인데 글이 너무 많아져버린 관계로 다음 포스트에 작성하도록 하겠다.

 

안녕~!

 

 

P.S setting.py 파일을 수정해줘야하는 것을 누락해서 뒤늦게 추가하니 참고 바란다.

 

funny_picture/setting.py

/* 생략 */

AUTH_USER_MODEL = 'account.User'

/* 생략 */

이 코드를 넣고 makemigrations, migrate 명령을 입력해야 정상적으로 동작하니 이점에 유의 할 것.

 

댓글
댓글쓰기 폼