2020년 3월 2일 월요일

[Django] Django의 인증 시스템 (authentication system)

Django의 인증 시스템은 일반적인 프로젝트의 인증과 관련된 요구사항을 쉽게 해결해준다. 기능 확장에 대해서도 충분히 열려있기 때문에 기본적으로 제공되는 기능 바탕 위에 사용자가 원하는 기능을 추가할 수도 있다.
(* Django의 인증 기능은 인증(authentication)과 권한(authorization) 부여를 모두 제공하고 있으므로 일반적으론 인증 시스템(authentication system)이라고 한다.)


코어 클래스, User

Django에서 인증은 User 객체를 이용해 이루어진다. User는 주요 속성으로 username, password, email, first_name, last_name을 갖는다. 이 중 username, password, email 값은 반드시 지정되어야 한다. (required field) User 클래스는 django.contrib.auth.models 패키지에 정의되어 있다. 아래 링크로 들어가면 대략적인 느낌 정도는 파악할 수 있을 것이다. (코드는 좀 어렵다.)
https://docs.djangoproject.com/en/1.8/_modules/django/contrib/auth/models/

User에 대한 조작은 User.objects를 통해 가지고 올 수 있는 UserManager 객체를 통해 할 수 있다. 그렇지만 Django admin을 통해 GUI로 조작하는 것이 훨씬 간편하다.. 인증에서 username이 ID로 쓰인다고 보면 되고 password는 저장 시점에 암호화되어 데이터베이스에 기록된다. 암호화 과정에서 SHA256 해시 알고리즘과 함께 PBKDF2 알고리즘을 쓰는데 이는 충분히 안전하다. 이마저도 사용자가 알고리즘을 변경할 수 있게끔 구성되어 있다.


기본 인증

인증은 django.contrib.auth의 authenticate 메서드를 통해 수행되는데 입력된 username과 password가 유효하면 User 객체가 반환된다. (유효하지 않으면 None을 반환한다.)


권한

Django에는 권한 시스템이 내장되어 있다. 특정 사용자나 그룹에 권한을 부여할 수 있는 기능을 제공한다. 제공되는건 Model 객체에 대한 view, add, change, delete 권한이고, 이는 AbstractUser 클래스가 상속받는 PermissionsMixin 클래스를 통해 관리된다.


웹 요청의 인증

Django는 세션과 미들웨어를 사용해 인증 시스템을 request 객체에 연결해주고 있다.
이는 매우 편리한데 모든 request 객체로부터 로그인된 User 객체를 얻을 수 있기 때문이다. (request.user)
만약 사용자가 로그인된 상태가 아니면 AnonymousUser가 반환된다.

사용자의 인증 여부는 아래의 간단한 코드를 통해 확인할 수 있다.

if request.user.is_authenticated:
    # do something for authenticated users.

view 함수가 호출되기 전에 이를 확인할 수도 있는데 @login_required 데코레이터가 이러한 일을 해준다. (CBV 방식이면 LoginRequireMixin이 같은 일을 한다.) 비슷한 기능으로 특정 조건 체크를 위한 @user_passes_test, 권한 체크를 위한 @permission_required가 제공된다. 그냥 필요한게 다 만들어져 있다고 보면 된다. Django가 뚱땡이 프레임워크라는걸 잊지 말자.

@login_required
def my_view(request):
    # 로그인된 상태여야만 진입할 수 있음.


사용자 로그인, 로그아웃

사용자 로그인은 login() 함수를 이용해서 할 수 있다. login()은 request 객체와 user 객체를 인수로 받고, login()은 Django의 세션 프레임워크를 사용해 세션에 사용자의 ID를 저장한다.
로그아웃은 django.contrib.auth의 logout 함수를 이용하여 수행된다.

기타 기본 login.html, logout.html을 확장하거나 하는 내용들이 아래 링크에 자세히 설명되어 있다. 이 부분은 내용이 너무 길어서 생략하기로 한다.

마지막으로 언급하고 싶은 부분은 Django의 인증 시스템은 Django를 쓰지 않는 사용자에게도 좋은 참고 자료가 된다는 것이다.

https://docs.djangoproject.com/en/3.0/topics/auth/default/

[Django] DateField - auto_now, auto_now_add

Django Model Field(django.db.models.fields)에서 시간을 표현하는 클래스로 DateField, TimeField, DateTimeField가 있다.

 • class DateField(DateTimeCheckMixin, Field): # 날짜
 • class TimeField(DateTimeCheckMixin, Field): # 시간
 • class DateTimeField(DateField): # 날짜와 시간

시간과 관련해서 객체를 데이터베이스에 저장할 때 자주 쓰이는 항목이 있는데 그건 바로 생성 시간과 수정 시간에 관한 것이다. 속성 중 auto_now와 auto_now_add가 이러한 기능을 제공한다.

 • auto_now: 최종 수정 시간. True로 셋 되어 있으면 update 작업이 일어날 때마다 시간이 갱신된다.
 • auto_now_add: 최초 생성 시간. True로 셋 되어 있으면 최초 insert 작업이 일어날 때만 시간이 설정된다.

하나의 필드는 둘 중 하나만 True 값을 갖을 수 있고, 둘 중 하나에 True가 할당되면 이후 사용자가 직접 수정할 수 없게 되어 있다. 최소한의 이력 관리라도 되어야 한다면 아래와 같은 모델을 떠올릴 수 있을 것이다.

class MyModel(models.Model):
    created_on = models.DateTimeField(auto_now_add=True)
    updated_on = models.DateTimeField(auto_now=True)

https://docs.djangoproject.com/en/2.2/_modules/django/db/models/fields/#DateTimeField

2020년 2월 23일 일요일

[ML] LightGBM 파라미터 정리.

LightGBM은 더 적은 자원을 쓰면서 빠르기까지 해서 xgboost의 대안 알고리즘으로 자주 거론된다. xgboost의 성능은 매우 뛰어나지만 파라미터 튜닝이 복잡하고 학습 시간이 느린 단점이 있다. LightGBM은 xgboost 대비 비슷한 성능을 내면서도 빠른 학습 속도를 제공한다.

ML 알고리즘의 경우 성능이 조금 떨어지더라도 속도가 빠르면 여러가지 장점이 있는데, 속도가 빠르면 FE에 더 많은 시간을 할애할 수 있기도 하고, grid search 등으로 빠르게 튜닝해서 오히려 더 나은 결과를 뽑아낼 수도 있기 때문이다.

LightGBM은 기존 알고리즘의 level-wise(균형 트리 방식)가 아닌 leaf-wise 방식으로 학습이 진행된다. 기존 방식에선 트리의 균형을 우선하는데 트리의 균형을 위해 추가되는 연산이 학습 속도를 떨어뜨리는 요인이 되었다. LightGBM은 트리의 규형보다는 리프 노드를 지속적으로 분할하면서 학습을 진행한다. 결과로 비대칭적인 깊은 트리가 생성되지만 level-wise 방식에서 리프 노드가 생성될 때보다 손실을 줄이는 장점이 있다고 한다.


자주 참조되는 파라미터는 아래와 같다. 지원되는 부스터 중 dart는 따로 공부해야 할 주제로 보인다. 여기서 속도 개선을 위한 CPU, GPU 관련 파라미터도 별도로 존재하는데 관련 파라미터는 아래의 링크로 대신한다.
https://testlightgbm.readthedocs.io/en/latest/Parameters.html

• objective: 목표 설정. 기본 값은 'regression'.
 - regression : 회귀.
   - regression_l2 (별칭: mean_squared_error, mse)
  - regression_l1 (별칭: mean_absolute_error, mae)
 - binary : 이진 분류.
 - multiclass : 멀티-클래스 분류. num_class로 분류 가짓수를 정해주어야 한다.

• boosting: 부스팅 방식을 설정. gbdt가 가장 많이 쓰인다.
 - gbdt : gradient boosting decision tree
 - dart : dropouts meet multiple additive regression trees
 - goss : gradient-based one-side sampling

• data (별칭: train, train_data) : 학습용 데이터

• valid (별칭: test, valid_data, test_data) : 검증용 데이터

• num_iterations: 부스팅 반복 횟수. 학습에 사용할 트리 개수를 뜻한다. 멀티-클래스 분류 문제인 경우 num_class * num_iterations 만큼의 트리가 사용된다.

• learning_rate: 학습 속도. 기본 값은 0.1. 값이 크면 건성건성 학습하고, 작으면 학습용 데이터에 너무 피팅되므로 실험을 통해 적정값으로 조절해주어야 한다.

• num_leaves: 하나의 트리에서 사용될 수 있는 최대 리프 노드 개수. 기본 값은 31.

• max_depth: 트리 깊이의 제한. 데이터가 적은 가운데 트리 깊이가 깊어지면 오버-핏 되는 경향이 있다. 기본 값은 제한 없음인데 -1로 표시한다. 기본 값은 -1.

• min_data_in_leaf: 리프 노드가 되기 위한 최소 샘플 데이터 수. 기본 값은 20. 이것 역시 오버-핏이 발생하지 않게끔 해주는 파라미터. 기본 값은 20.

• feature_fraction: 각 이터레이션에서 사용할 feature의 비율. LightGBM은 각 이터레이션마다 여기에 설정된 비율 만큼의 feature를 랜덤하게 선택해 학습을 진행한다. 오버-핏을 다루는 파라미터이지만 부가적으로 학습 속도를 높이는 효과도 있다. 0~1 사이의 값을 지정해야 하고, 기본 값은 1.

• bagging_fraction: feature_fraction이 column 방향으로의 random selection이라면 bagging_fraction은 row 방향으로의 random selection이다. LightGBM이 각 이터레이션마다 쓸 데이터의 비율을 뜻한다. 기본 값은 1.

• bagging_freq: bagging 수행의 빈도. 몇 번의 이터레이션 마다 bagging을 할 것인지 지정한다. 기본 값은 0으로 사용하지 않음을 의미.

• early_stopping_round: 학습 과정에서 측정된 스코어가 향상되지 않으면 중간에 학습을 중단하게 한다. 기본 값은 0.

• lamgda_l1, lambda_l2: l1 정규화, l2 정규화. 어느 정도의 값을 지정해야 하는지 난 아직 잘 모르겠다.

• min_gain_to_split: 트리 분기를 수행하기 위한 최소 기준. 무작정 분기할 순 없으니 의미 있는 feature의 의미 있는 값 기준으로 트리가 분기할 수 있도록 알고리즘에 알려주기 위함이다. 마찬가지로 적정 값을 어떻게 정해주어야 하는지 잘 모르겠음.

• categorical_feature: 카테고리 변수를 LightGBM에 알려주는 용도. 카테고리 변수를 기존엔 원핫인코딩 방식으로 지정해주곤 했는데 이렇게 했을 경우 속도도 느려지고 트리의 밸런스가 안좋게 만들어지는 경우가 생긴다. LightGBM에 카테고리 변수를 따로 지정해주면 LightGBM이 알아서 잘 처리해준다.

• metric: 평가식. regression인 경우 l2(mean_squared_error)가 기본값. 이진 분류에선 binary_logloss가 기본 값이다. 멀티-클래스 분류인 경우 multi_logloss, multi_error가 쓰인다. auc 평가식도 많이 쓰이는 값.

https://lightgbm.readthedocs.io/en/latest/Python-Intro.html


2020년 2월 20일 목요일

[Java] MyBatis #{}과 ${} 차이점.

#{}는 MyBatis가 제공하는 매개 변수 표기법으로 MyBatis에게 PreparedStatement 매개 변수를 작성하도록 알려준다. SQL Injection을 예방해주기 때문에 MyBatis에서 권장되는 방식이다.

${}는 문자열 대체 기능으로 (매개 변수에 사용할 수도 있지만) 주 용도는 SQL 문의 메타 데이터(테이블 이름 또는 컬럼 이름)가 동적일 때 일부 문자를 대체해주기 위해 쓰인다. MyBatis는 ${} 내 데이터를 변경하거나 이스케이프 처리하지 않는다.

${} 방식을 쓸 때엔 주의할점이 있다. 사용자로부터 입력 받은 값을 확인하지 않고 바로 구문에 전달해서는 안되는데 SQL 주입 공격으로부터 안전하지 않기 때문이다. 사용자 입력값을 ${}를 이용해 구문으로 전달해야만 하는 로직이 있다면(구체적으로 그럴 일이 있는진 모르겠다만.) 자체적으로 값에 이상이 없는지 검증 과정을 거쳐야 한다.

별도로 데이터베이스 수행 계획에 영향을 주기도 하는데 아래 링크에서 내용을 확인할 수 있다.
https://lng1982.tistory.com/246

테이블에 담긴 데이터의 성격에 따라 ${} 방식을 사용했을 때 성능 상 이점이 생길 수 있다는 내용이다.
그러나 이 정도까지의 튜닝이 필요한 환경에서 일하는 경우는 드물기 때문에 #{}로 사용이 가능한 경우엔 가급적 #{}를 쓰는게 나을 것이다.


공식 페이지: https://mybatis.org/mybatis-3/ko/sqlmap-xml.html

2020년 2월 12일 수요일

규모에 따라 서비스 확장하기 (Application Scaling)

웹과 모바일 앱 애플리케이션은 일반적으로 3개의 컴포넌트로 구성된다.

  • 데이터베이스
  • API
  • 클라이언트

데이터베이스는 데이터를 보관하고, API는 요청에 따른 데이터를 서빙한다. 그리고 클라이언트는 데이터를 렌더링해 사용자에게 표시한다. 클라이언트와 API를 구분하는 이유는, 클라이언트와 API를 완전히 별개의 개체로 생각하고 미리 구분해두면 애플리케이션 확장에 대한 추론이 훨씬 간단해지기 때문이다.


다음은 서비스를 시작한 뒤 서서히 사용자(트래픽)가 증가하게 될 때 애플리케이션을 어떻게 구성(대응)해야 하느냐에 대한 가이드다. 원문은 아래 링크에 있다. (아래의 내용은 원문에 대한 번역이 아니기 때문에 영어에 익숙하다면 원문의 내용을 참고하는 것이 좋다.)
https://alexpareto.com/scalability/systems/2020/02/03/scaling-100k.html


1~10 Users : 하나의 서버

아주 적은 사용자(1~10명)를 대상으로 할 땐 위의 세가지 컴포넌트를 하나의 서버에서 실행되게 하는 것이 좋을 것이다. 한 명의 엔지니어가 데이터베이스, API, 클라이언트를 모두 통제할 수 있기 때문이다.


10~100 Users : 데이터베이스 분리

사용자가 10~100명 수준이 된다면 가장 먼저 고려할 것은 데이터베이스를 물리적으로 분리하는 것이다. 클라우드 환경이라면 아마존 RDS 같은 것이 될 수 있는데, 비용이 조금 비싸지지만 멀티-리젼 중복, 읽기 전용 복제본 생성, 자동 백업 등 다양한 기능을 쉽게 추가할 수 있다.


100~1000 Users : 클라이언트 분리

사용자가 100명을 넘어서면서 트래픽이 안정적으로 나와주면 클라이언트 컴포넌트 까지도 분리를 고려해야 한다. 앞서 적힌 내용처럼 최초 구현 단계에서부터 클라이언트와 API 컴포넌트를 명확히 구분지어 놓으면 확장이 쉬워진다. (API 입장에선 클라이언트가 웹이든 데스크탑이든 그저 같은 API를 사용하는 클라이언트일 뿐이다.)


1000+ Users : Load balancer

하나의 API 인스턴스가 모든 트래픽을 감당하기 어려워지게 되므로 더 많은 컴퓨팅 파워가 필요하게 된다.

로드 밸런서가 투입된다. API 앞에 로드 밸런서를 배치하고 트래픽을 분산시키는 방식이다. 이를 통해 수평적 확장이 가능하게 된다. 동일한 코드를 실행하는 서버를 추가할수록 처리할 수 있는 요청량은 늘어나게 된다. 로드 밸런서는 트래픽이 가장 적은 인스턴스로 요청을 라우팅한다. (물론 처음 연결맺은 API 인스턴스와 계속 통신하게 할수도 있다.)


10,000+ Users : CDN

정적 컨텐츠를 API 서버로부터 구분한다. 이 작업은 초기에 해두는 것이 좋긴 하다. 그리고 CDN 도입을 고려해야 한다. CDN은 전 세계의 다른 데이터 센터에 정적인 데이터를 자동으로 캐시한다.


100,000+ Users: 데이터베이스에 대한 확장

데이터베이스에 대한 스케일링 작업은 매우 어려운 편이다. API 서버의 경우 무상태(stateless)로 구성할 수 있어 확장이 간편하지만 데이터베이스의 경우 '상태'를 갖기 때문이다.

데이터베이스의 부하를 낮추기 위한 가장 간편하고 효과적인 선택은 데이터 보관에 대한 캐시 레이어를 마련하는 것이다. Redis 또는 Memcached와 같은 키-밸류 기반의 메모리 저장 솔루션이 이를 지원한다. 특히 Redis의 경우 자체로 클러스터링을 지원하기 때문에 확장으로부터 안전하다. 여기서 더 할 수 있는 일은 읽기에 대한 복제본(Read replica)를 추가하거나 파티셔닝, 샤딩 등의 기법을 사용하는 것이 도움이 된다.


+ 모니터링 도구
New Relic 또는 Datadog과 같은 서비스를 이용해 시스템을 모니터링할 수 있다. 이를 통해 요청이 느리고 개선이 필요한 부분을 파악할 수 있을 것이다.

2020년 2월 11일 화요일

[WPF] ContentControl과 Template

들여다볼 때마다 헷갈리는 개념.
네이버 쪽 블로그 보니 이미 작년에 한번 정리했던 내용이다.
https://blog.naver.com/seokcrew/221584014717

ContentControl은 단일 내용으로 된 컨트롤을 나타낸다. Button의 부모인 ButtonBase나 ScrollViewer 같은 컨트롤은 이 클래스를 직접 상속받고 있다.

ContentControl은 외형을 결정짓는 속성으로 Template과 ContentTemplate을 가질 수 있다.
  • Template의 값 유형은 ControlTemplate이며 컨트롤의 모양을 정의한다.
  • ContentTemplate의 값 유형은 DataTemplate이며 데이터의 모양을 정의한다.

ContentPresenter는 ContrentControl의 ControlTemplate에 배치될 때 ContentControl의 Content를 자동으로 가져와서 표시해 준다. ContentControl 클래스에 의해 표시되는 Content가 런타임에 결합될 위치를 지정해준다.
  • ContentControl 상속 구조 : FrameworkElement → Control → ContentControl
  • ContentPresenter 상속 구조 : FrameworkElement → ContentPresenter

* ContentControl의 ContentTemplate을 정의하는 DataTemplate은 다시 ContentControl을 갖을 수 있고, ContentControl은 별도로 하나의 자식 요소를 가질 수 있는데 다시 각각의 ContentControl은 ControlTemplate을 갖을 수 있기 때문에 여러 단계로 중첩된 구조가 생기면 한 눈에 파악하기 어려워진다.


2020년 2월 9일 일요일

Socket.io 요약

socket.io는 브라우저와 서버 간 이벤트 기반으로 양방향 통신을 가능하게 하는 라이브러리다. 내부적인 heartbeat 메커니즘을 통해 자동 재접속과 연결 끊어짐 감지를 해주기 때문에 사용하는 입장에서 라이브러리를 믿고 간편하게 쓸 수 있다.

socket.io는 사용자가 원하는 어떤 형식으로든 이벤트와 데이터 형식을 만들어 쓸 수 있게끔 지원하고 있다. 일반적으론 JSON 포맷을 쓰게 되겠지만 binary 데이터 형식의 송수신도 지원한다.
브라우저와 서버 간 이벤트를 주고받는 방법은 단순하다. 기본적으로 아래 2개의 메서드로 구현된다.
  • emit: 이벤트를 발생시킨다.
  • on: 이벤트를 수신한다.

참조: https://socket.io/docs/


Room

room 기능도 제공한다. 임의의 채널을 정의해서 특정 소켓을 그루핑 시킬 수 있는데 간단하게 생각하면 채팅방과 같은 개념으로 이해할 수 있을 것이다. namespace로 구분되는 room을 한정해서 제한된 데이터 교환이 가능하다. 이는 응용 측면에서만 아니라 설계 측면에서 리소스를 줄여주는 역할도 하기 때문에 유용하다.
기본 namespace 이름은 '/' 이다. namespace를 지정하고 싶으면 io.of() 함수를 이용하면 된다. (클라이언트에선 io()의 파라미터로 namespace 값을 전달.)


Broadcasting

브로드캐스팅의 경우 io객체의 emit() 함수를 이용하면 되는데, 발송자를 제외한 브로드캐스팅이 필요한 경우엔 socket.broadcast.emit() 함수를 이용하여야 한다. namespace를 한정해 이벤트를 발생시킬 수도 있는데 io.to() 함수를 이용하면 된다.

참조: https://socket.io/docs/rooms-and-namespaces/



** 채팅 솔루션이 급하다면 아래 링크를. 코드를 보면 알겠지만 FE가 할 일이 더 많다. 그만큼 편하게 되어있다는 뜻.
https://github.com/socketio/socket.io/tree/master/examples/chat



서버 기본 코드. 아래 꼴에서 크게 달라지지 않는다.

// Express initializes 'app' to be a function handler that you can supply to an HTTP server.
var app = require('express')();
var http = require('http').createServer(app);

var io = require('socket.io')(http);

// define a route handler '/' that gets called when we hit our website home.
app.get('/', function (req, res) {
    res.sendFile(__dirname + '/index.html');
});

// listen on the 'connection' event for incoming sockets.
io.on('connection', function (socket) {
    console.log('a user connected');
    socket.on('disconnect', function() {
        console.log('user disconnected');
    });

    socket.on('chat message', function (msg) {
        io.emit('chat message', msg);
    });
});

// http server listen on port 3000
http.listen(3000, function () {
    console.log('listening on *:3000');
});


위 코드와 대응되는 클라이언트 측 스크립트.

// client
$(function() {
    var socket = io();
    $('form').submit(function (e) {
        e.preventDefault();
        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
    });

    socket.on('chat message', function (msg) {
        $('#messages').append($('<li>').text(msg));
    });
});


아래는 공식 페이지에서 제공하는 emit cheatsheet.

io.on('connect', onConnect);

function onConnect(socket){

  // sending to the client
  socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

  // sending to all clients except sender
  socket.broadcast.emit('broadcast', 'hello friends!');

  // sending to all clients in 'game' room except sender
  socket.to('game').emit('nice game', "let's play a game");

  // sending to all clients in 'game1' and/or in 'game2' room, except sender
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

  // sending to all clients in 'game' room, including sender
  io.in('game').emit('big-announcement', 'the game will start soon');

  // sending to all clients in namespace 'myNamespace', including sender
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

  // sending to a specific room in a specific namespace, including sender
  io.of('myNamespace').to('room').emit('event', 'message');

  // sending to individual socketid (private message)
  io.to(`${socketId}`).emit('hey', 'I just met you');

  // WARNING: `socket.to(socket.id).emit()` will NOT work, as it will send to everyone in the room
  // named `socket.id` but the sender. Please use the classic `socket.emit()` instead.

  // sending with acknowledgement
  socket.emit('question', 'do you think so?', function (answer) {});

  // sending without compression
  socket.compress(false).emit('uncompressed', "that's rough");

  // sending a message that might be dropped if the client is not ready to receive messages
  socket.volatile.emit('maybe', 'do you really need it?');

  // specifying whether the data to send has binary data
  socket.binary(false).emit('what', 'I have no binaries!');

  // sending to all clients on this node (when using multiple nodes)
  io.local.emit('hi', 'my lovely babies');

  // sending to all connected clients
  io.emit('an event sent to all connected clients');

};