Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Run multiple sites from one Django source

Run multiple sites from one Django source

Django는 하나의 코드로 여러 사이트를 운영할 수 있는 Site 프레임워크를 제공합니다. 이를 이용하면 같은 레이아웃을 가지지만 서로 다른 콘텐츠를 제공하는 두 개 이상의 사이트를 쉽게 만들 수 있습니다. 하지만 내용뿐만 아니라 레이아웃까지 다르게 가져가려면 Site 프레임워크를 확장할 필요가 있습니다. 이 발표에서는 Django의 Site 프레임워크에 관해 먼저 알아보고, Site 프레임워크를 미들웨어 수준에서 확장하여 하나의 코드 기반으로 여러 개의 사용자 정의 가능한 사이트를 운영한 경험과 방법을 이야기하고자 합니다. 각 사이트 별로 서로 다른 템플릿과 리소스를 사용하되, 특정 사이트에 템플릿이 없을 경우 기본(fallback) 템플릿을 사용하는 방법에 관해 알아볼 것입니다. 사이트 별로 urls.py 파일을 별도로 구성하는 방법 및 사이트 URL에 따라 settings.py 값을 오버로딩 하는 법도 이야기할 것입니다. 이를 통해, 코드 개발의 편의성을 유지하면서도 다양성을 가지는 여러 사이트를 운영하는 경험을 공유하려고 합니다.

Jonghyun Park

August 17, 2019
Tweet

More Decks by Jonghyun Park

Other Decks in Programming

Transcript

  1. • 하나의 Django 소스를 이용해서 여러 개의 사이트 운영하기 •

    데이터 분리하기 - sites 프레임워크 • 설정 및 URL 분리하기 - middleware • 템플릿 불러오기 - custom template loader • 리소스 불러오기 - custom static finder 내용
  2. • 기본 뼈대는 동일하지만 기관 별로 다른 내용의 서비스가 필요한

    경우 • 래블업의 사례: 대학별 교육용 서비스를 별도 도메인으로 공급 • site1.service.com 사이트 사용자가 site2.service.com 에 올라간 학습 자료를 볼 수 있으면 안 됨 • 코드 베이스를 최대한 공유하여 빠르게 여러 서비스 제공 • 코드 개발의 편의성을 유지하면서 다양성을 가지는 사이트 운영 여러 사이트 운영이 필요한 경우
  3. • Overload 하위에 기본 사이트 이외 설정 및 자원 저장

    장난감 앱 구조 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ ├── settings.py │ └── urls.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html 데모 사이트: https://github.com/adrysn/multisite
  4. • DB 수준에서 사이트 지정 • 웹사이트의 domain과 name을 지정

    sites 프레임워크 class Site(models.Model): domain = models.CharField( _('domain name'), max_length=100, validators=[_simple_domain_name_validator], unique=True, ) name = models.CharField(_('display name'), max_length=50) https://github.com/django/django/blob/master/django/contrib/sites/models.py
  5. DB에서 site 지정 Site 1 domain name Entry id title

    body … sites Site 2 domain name Site 3 domain name 연결된 site에 관한 정보를 가지는 모델
  6. DB에서 site 지정 class Entry(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

    title = models.CharField(max_length=50) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) sites = models.ManyToManyField(Site, related_name='entries', blank=True) objects = models.Manager() on_site = CurrentSiteManager() Entry.on_site.all() 같이 사이트별 쿼리 가능
  7. View에서 사용 예 from django.contrib.sites.shortcuts import get_current_site def my_view(request): entry

    = Entry.on_site.get(title='hello world') current_site = get_current_site(request) if current_site.domain == 'foo.com': # Do something for site foo.com pass else: # Do something else for other sites pass
  8. • django.contrib.sites → INSTALLED_APPS • SITE_ID = 1 in settings

    (default site) •migrate sites 프레임워크 활성화 $ python manage.py migrate multisite_django | Operations to perform: multisite_django | Apply all migrations: admin, auth, contenttypes, sessions, sites multisite_django | Running migrations: multisite_django | Applying sites.0001_initial... OK multisite_django | Applying sites.0002_alter_domain_unique... OK . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ ├── settings.py │ └── urls.py └── templates └── base.html
  9. • site1: site1.local -> site1.mysite.com • site2: site2.local -> site2.mysite.com

    sites 프레임워크 활성화 $ ./manage.py shell >>> from django.contrib.sites.models import Site >>> site1 = Site.objects.first() >>> site1.domain = 'site1.mysite.com' >>> site1.name = 'Site 1' >>> site1.save() >>> >>> Site.objects.create(domain='site2.mysite.com', name='Site 2') <Site: site2.mysite.com> /etc/hosts 127.0.0.1 site1.local 127.0.0.1 site2.local
  10. • Django의 미들웨어 지원 • 요청(request)/응답(response)을 가로채 특정 작업을 수행

    User View do work make response 미들웨어 (middleware) Request Response Middlewares Session Authentication CsrfView …
  11. 미들웨어 (middleware) MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware’,

    ... 'multisite.middlewares.MultiSiteMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', ] request.site == 현재 사이트 def my_view(request): current_site = get_current_site(request) if current_site.domain == 'foo.com': ... def my_view(request): if request.site.domain == 'foo.com': ... 커스텀 미들웨어
  12. 사이트 구분을 위한 미들웨어 작성 class MultiSiteMiddleware: def __init__(self, get_response):

    self.get_response = get_response # One-time configuration and initialization. # Called only when web server starts def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. response = self.get_response(request) # Code to be executed for each request/response after # the view is called. return response . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ └── middlewares.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html
  13. 사이트 구분을 위한 미들웨어 작성 def __call__(self, request): domain, _

    = split_domain_port(request.get_host()) # Get the production domain corresponding to the current site. if settings.DEBUG: try: domain = MultiSiteMiddleware.OVERLOADING_MATCHING_TABLE[domain] except KeyError: raise ImproperlyConfigured( f'No matching overloaded domain for {domain}' ) # Get current site object and set SITE_ID. try: current_site = Site.objects.get(domain=domain) except Site.DoesNotExist: current_site = Site.objects.get(id=settings.DEFAULT_SITE_ID) request.current_site = current_site settings.SITE_ID = current_site.id settings.site_domain = domain 요청이 들어온 도메인으로 사이트를 구분 OVERLOADING_MATCHING_TABLE = { 'site1.local': 'site1.mysite.com', 'site2.local': 'site2.mysite.com', } 개발 도메인일 경우 프로덕션 도메인으로 이름 변경
  14. • overload 폴더 내에 사이트 도메인과 일치하는 이름의 폴더가 있을

    경우 그 하위 설정 파일을 찾아 적용. 없으면 기본 사이트 설정 적용. 사이트별 설정 적용 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ └── middlewares.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html def __call__(self, request): ... # Determine the module path which contains site-wise assets. site_path = settings.site_domain.replace('.', '-') # Override settings. try: new_settings = import_module(f'overload.{site_path}.settings') except (ModuleNotFoundError, ImportError): new_settings = import_module(f'overload.default_settings') finally: for field in self.OVERRIDING_FIELDS: # Override fields only in OVERRIDING_FIELDS. if hasattr(new_settings, field): new_value = getattr(new_settings, field) setattr(settings, field, new_value)
  15. • overload 폴더 내에 사이트 도메인과 일치하는 이름의 폴더가 있을

    경우 그 하위 URL 파일을 찾아 적용. 없으면 기본 사이트 URL 사용. URL 구분하기 . ├── apps │ └── entry │ ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ └── middlewares.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ └── urls.py └── templates └── base.html def __call__(self, request): ... # Override URL patterns. try: override_urls = import_module(f'overload.{site_path}.urls') if hasattr(override_urls, 'urlpatterns'): request.urlconf = f'overload.{site_path}.urls' except ImportError: # fallback to the default URL pass response = self.get_response(request) return response
  16. Template Loader 설정 추가 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates',

    'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': False, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'multisite.context_processors.common_settings', ], 'loaders': [ 'multisite.template_loaders.MultiSiteLoader', 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ], }, }, ]
  17. 사이트별 템플릿 구분하기 . ├── apps │ └── entry │

    ├── models.py │ ├── templates │ │ └── entry │ │ └── entry_list.html │ ├── urls.py │ └── views.py ├── multisite │ ├── middlewares.py │ └── template_loaders.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ ├── templates │ │ └── entry │ │ └── entry_list.html │ └── urls.py └── templates └── base.html class MultiSiteLoader(Loader): def get_template_sources(self, template_name): domain = settings.site_domain template_dir = (Path(settings.BASE_DIR) / 'overload' / domain.replace('.', '-') / 'templates').resolve() template_dir = str(template_dir) name = safe_join(template_dir, template_name) yield Origin( name=name, template_name=template_name, loader=self, ) def get_contents(self, origin): try: with open(origin.name, encoding=self.engine.file_charset) as fp: return fp.read() except FileNotFoundError: raise TemplateDoesNotExist(origin) Overload 하위에 위치하는 template 파일을 로드 template_name -> Origin Origin -> actual contents
  18. 사이트별 리소스 불러오기 . ├── apps │ └── entry │

    ├── models.py │ ├── templates │ │ └── entry │ ├── urls.py │ └── views.py ├── multisite │ ├── middlewares.py │ ├── static_finders.py │ └── template_loaders.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ ├── static │ │ └── logo2.png │ ├── templates │ │ └── entry │ └── urls.py └── templates └── base.html Overload 하위에 위치하는 리소스 파일을 로드 class MultiSiteFinder(BaseFinder): def __init__(self, app_names=None, *args, **kwargs): # List of locations with static files self.locations = [] # Maps dir paths to an appropriate storage instance self.storages = OrderedDict() self.overload_base_directory = os.path.join(BASE_DIR, 'overload') if not os.path.exists(self.overload_base_directory): raise ImproperlyConfigured('There is no overload static directory.’) for overload_dir_info in next(os.walk(self.overlaoad_base_directory))[1]: static_dir = os.path.join(BASE_DIR, 'overload', overload_dir_info, 'static') if os.path.isdir(static_dir): root = static_dir prefix = '' if (prefix, root) not in self.locations: self.locations.append((prefix, root)) for prefix, root in self.locations: filesystem_storage = FileSystemStorage(location=root) filesystem_storage.prefix = prefix self.storages[root] = filesystem_storage super().__init__(*args, **kwargs)
  19. 사이트별 리소스 불러오기 def find(self, path, all=False): """Looks for files

    in the overload directories.""" matches = [] for prefix, root in self.locations: if root not in searched_locations: searched_locations.append(root) matched_path = self.find_location(root, path, prefix) if matched_path: if not all: return matched_path matches.append(matched_path) return matches
  20. 요약 . ├── apps │ └── entry │ ├── models.py

    │ ├── templates │ │ └── entry/entries.html │ ├── urls.py │ └── views.py ├── multisite │ ├── middlewares.py │ ├── static_finders.py │ ├── settings.py │ ├── template_loaders.py │ └── urls.py ├── overload │ ├── default_settings.py │ └── site2-mysite-com │ ├── settings.py │ ├── static │ │ └── site2-mysite-com │ ├── templates │ │ └── entry/entries.html │ └── urls.py └── templates └── base.html Site 프레임워크 = + 하나의 소스 여러 사이트 서빙