[ Architecture, Technology ,Web ] SSO(Single Sign On) 그리고 SAML에 대해

이미지
이번 프로젝트 내부에서 어쩌다보니  유저 인증 관련 업무를 담당하게 되었고, 해야하는 업무는 내부에 사용했던 적이 없던  새로운 개발 플랫폼에서  SSO의 프로토콜 중  SAML을 이용해 앱의 인증을 구현해야만 했다. SSO를 생각해본적 조차 없는 상황에 이를 새로운 개발 플랫폼에 도입해야 했기 때문에 많은 시행착오를 겪었으나 구현에 성공하였으며 덕분에 SSO에 대한 전반적인 지식을 쌓을 수 있었다. 이번에는 그러한 과정에서 나온 지식들과 경험을  공유하고자 한다. SSO에 대한 정의 먼저 사전적 정의 부터 살펴보자. 다만, 기술적인 용어다보니 자주 사용하는 옥스포드 사전에 정의를 찾을 수 없기 때문에  검색으로 찾을 수 있는 정의를 몇 가지 살펴보고 교차 검증을 해보자. 첫 번째 정의를 살펴보자. Single sign-on (SSO) is an identification method that enables users to log in to multiple applications and websites with one set of credentials.  SSO는 웹사이트에서 한 번의 인증(one set of credentials)으로 복수의 어플리케이션에 로그인 할 수 있는 인증(identification) 방법(method) 이다. 두 번째는 위키피디아의 정의이다. Single sign-on (SSO) is an authentication scheme that allows a user to log in with a single ID to any of several related, yet independent, software systems. SSO는 독립적이지만 연관되어있는 몇몇 소프트웨어에 대해 하나의 ID로 로그인을 할 수 있도록 하는 인증 구조(scheme) 세부 설명에 조금 차이가 있어 보이지만 전체적인 틀은 매우 비슷해 보인다.  몇 가지 포인트가 되는 단어를 추출해 이를 연결해보자면 아래와 같은 의미를 산출 할 수 있다. 독립적이지만 연관되어 있

[ Django, Database, Algorithm, Data Structure ] Django에서 중첩된 댓글 기능 구현의 대한 솔루션에 대해

이전 글을 통해 
인접 리스트 모델과 MTPP에 대해 알아봤다.

하지만 결국 현재로서는 어느 것이 
최적의 솔루션인지를 판단하기는 힘들다.

물론 이에 대해 새로운 모델을 만든다면 
최고겠지만,이는 엔지니어의 몫은 아니며
컴퓨터 과학의 수 많은 학자들의 몫이다.

따라서 엔지니어 몫은 둘 중 어느것이 
해당 시스템에 적절한지에 대해 판단해야 한다.

그렇다면 어떤 것이 적절할까?

이에 대한 해답으로 
현재 인기가 많은 프레임 워크 중 하나인 Django에서는 
어떤 방식을 채택하고 있는지에 대해 엿본다면 
이에 대한 힌트를 얻을 수 있을 것이다.

다만 해당 프로젝트는 프레임 워크라 이야기하고 있지만
나는 이를 API로 부르려고 하는데
왜냐하면 웹 서버 프레임 워크와 혼동을 피하기 위함이다.

실제 이 APP은 프레임 워크의 속성인
오버라이딩을 지원하고 있기 때문에 
사실 프레임 워크라고 불러도 상관 없다고 생각한다.

이에 대해 관심이 있다면
이전에 포스팅한 아래의 글을 참고해보길 바란다. 

Django의 중첩된 댓글 기능 API


현재 Django에서는 공식적으로 
이 중첩된 댓글 기능에 대해 지원하지 않고 있다.

다만,
과거 Django 1.6에서는 이를 지원하고 있다는 사실은



위에 Django 1.6 공식 도큐먼트에서 확인할 수 있다.

어떤 이유에서 인지는 정확히 명시하지 않고 있기 때문에 
알 수 없지만 Django 1.6 이후 부터는 


위와 같이 새로운 프로젝트로써 분리되었음을 알 수 있다.

추측해 보자면 과거 MTPP를 사용한 API로서 넣었으나 
이전 글에서도 언급했다시피 
MTPP 또한 성능면에서는 여전히 좋다고는 할 수 없기 때문에 
논란이 많아 떨어져나갔다고 추측해 볼 수 있을 것이다.

그렇다면 새로 떨어져나간 
Django Commnets라는 API의 내부에는 어떤 방식을 채택하고 있을까?

Django Comments APP의 내부


이를 알아보기 위해서는 매우 간단하다.

원본 소스의 Model이 어떻게 구성되어져 있는가를 보기만하면 된다.

해당 API 프로젝트는


위의 django-contrib-comments라는 이름으로 Github에 올라가져 있다.



이 API의 Model은 
abstracts.py 파일에서 확인할 수 있는데,


 
빨간색 박스의 Model의 코드를 보면 
left값과 right값이 없는 것을 보면 MTPP가 아니라
인접 리스트 모델을 사용하고 있음을 확인할 수 있다.

따라서 IT업계에서는 
사실상 MTPP는 사장된 솔루션이라 보는게 맞지 않나 싶다.

왜냐하면 MTPP 보다는 인접 리스트 모델의 쪽이 좀 더 이해하기 쉬우며
이는 가독성이 더 좋다는 의미이기 때문이다.

물론 두 개를 구현해보고 성능 테스트를 한 후에 
결론을 내리는 것이 좋겠지만,

이 글의 성격은 성능 보다는 구현에 포커스를 맞추고 있기 때문에 
성능 이슈를 논외로 하고 인접 리스트 모델을 사용하도록 하겠다.

Django에서 인접 리스트 모델(Adjacency List Model)로 댓글 기능 구현

그럼 이제 직접 Django 에서 인접 리스트 모델(Adjacency List Model)로 
중첩된 댓글 기능을 구현해보자.

이전 글에서 설명했듯이

인접 리스트 모델은 상위 parent 노드를 가르키는 번호가 들어가게 된다.

따라서 추가되는 것은 Django의 기존 Comment 관련 Model에 
자기 자신(Model)을 Parent로 가질 수 있도록 설정해주면 된다.



class Comment(models.Model):
    c_name = models.CharField('Commented name',max_length = 10, help_text = 'You can enter up to 10 characters.')
    c_date = models.DateField('Commented date',default = datetime.date.today)
    c_description = models.CharField('Text',max_length = 100, help_text = 'Enter your comment. n\You can enter up to 100 characters.')
    post = models.ForeignKey('post', on_delete = models.SET_NULL, null = True)
    #parent filed of Adjacency List Model
    parent = models.ForeignKey('self', on_delete = models.CASCADE, null = True)
위의 사진과 코드는 Comment 기능을 구현하기 위한 
Model인데, 빨간색 박스가 인접 리스트 모델로 구현하기 위해 추가 한 코드이다.

parent 번호를 저장할 필드를 추가하고,
이를 참조할 수 있게 ForeignKey로 만들어 준다.

그 다음은 당연히 Model 구조가 바뀌었기 때문에
마이그레이션을 다시 해야 한다.

그 후에 Admin App을 들어가보면 

위와 같이 수정 할 수 있는 Parent라는 새로운 필드가 생겼고
참고 가능한(추가 했던 댓글)들이 풀 다운에 모두 표시되게 된다.

확인이 끝났다면 View를 추가 해보자.

다만, 위의 방법은 서버 단에서 가장 적게 수정하는 방법이며,
클라이언트 단에서의 구현이 조금 복잡할 수 있다.

왜냐하면 우리가 원하는 것은 중첩된 표현을 해야하기 때문이며,
단순히 parent를 추가했다고 해서 
그것 만으로 클라이언트 단에서 표현하기에는 부족하기 때문이다.

따라서 이외에도 몇 가지 솔루션을 제시해 볼 수 있는데
다른 솔루션에 대해서는 아래와 같다.

① DB단에서 구현


DB단에서 구현도 가능한데 
위에서 언급한 바와 같이 parent라는 필드를 만드는 것으로는 
중첩된 댓글 기능을 구현할 수는 없다.

서버 단과 클라이언트 단은 별개이기 때문이기도 하며
서버단 에서 로직 상으로 문제가 있던 없건
값만 준다면 클라이언트단과는 딱히 상관 없기 때문이다.

왜냐하면 일반적으로 생각하는 중첩된 댓글들은 
댓글에 댓글이 그리고 그 댓글에 댓글이 달릴 수록 
화면 상으로 오른쪽으로 이동하기 때문이다.

이를 표현하기 위해서는 Level이나 Depth를 포함해 
경우에 따라는 더 많은 필드를 필요로 하는데

DB단(서버 단)에서 새로운 컬럼들을 더 추가하고,
이를 SQL로 받아오는 형식으로도 구현이 가능하다.

실제로 구글링을 해보면 이런 식의 구현도 꽤 나 찾아볼 수 있다.

다만 이 경우 DB에서 많은 처리가 이루어지기 때문에
DB에 부담이 많이 갈 수 있으며,
SQL로 구현해야하기 때문에 구현에 어려움이 있을 수도 있다.

하지만, 일반적으로 한 게시글에 다는 댓글은 그리 많지는 않기 때문에
운용하는데에는 큰 문제는 없어 보인다.

물론 이는 위에서도 언급한 것과 같이
실제 과부하 테스트를 해볼 필요가 있다.

② 중첩된 댓글의 새로운 모델을 활용한 구현


처음 제시한 솔루션은 이전에 설명했듯이 
재귀로 불러오기 때문에 
기본적으로 성능에 대한 의심이 있을 수 있으며,
인접 리스트 모델에 대한 이해가 필요하기 때문에 
다소 가독성이 떨어질 수 있다.

기존 Comment라는 모델에 
댓글이 달릴 경우 생성되는 모델을 새로 추가하는 방법이다.

이쪽이 다소 이해가 편할 수 있다.

다만, 계속해서 참고해나가는 것이 아니고,
새로운 모델을 계속해서 생성하기 때문에 
첫 번째 방법과 유사하게 성능 문제가 있을 수 있 수 있다.

③ 인접 리스트 모델의 오픈 소스를 활용한 구현


잘 구현된 인접 리스트 자료구조 오픈 소스를 활용하는 방법이다.

Python이 훌륭한 점은 오픈 소스를 활용하기 매우 편하다는 것인데 
이를 활용하는 방법이다.

시스템이 커지면 커질 수록, 엔터프라이즈 급의 시스템에서
활용하기 가장 좋은 솔루션이라고 생각 한다.

왜냐하면, 가장 퍼포먼스가 뛰어날 것으로 생각되기 때문이다.
(물론 성능 테스트를 해봐야 확실하겠지만)

후에 시간이 남는다면, 
바꿔보고 싶은 솔루션이다.

④ 중첩된 댓글의 새로운 View을 활용한 구현

Model이 아닌 View를 추가해서 구현할 수도 있다.

내가 소개 하고자하는 솔루션은 새로운 View를 추가하고,
클라이언트 단의 box를 활용한 level과 depth를 통해 화면에 
중첩된 댓글을 구현하는 방법이다.



def reply_comment_create(request, comment_id):

    comment = get_object_or_404(Comment, id = comment_id)
    post = comment.post
    form = CommentCreateForm(request.POST)
    blog_admin_flag = admin_permissions_confirm(request)

    #if 'POST' request
    if request.method == 'POST':

        form = CommentCreateForm(request.POST)

        if form.is_valid():

            in_create_description = form.cleaned_data['create_description']

            create_reply = Comment(
                post = post,
                parent = comment,
                c_name = request.user.username,
                c_date = datetime.date.today(),
                c_description = in_create_description,
            )

            create_reply.save()

            # redirect to 'post-detail' that 'blog/blogs/post_id'
            return HttpResponseRedirect(reverse('post-detail', args=[post.id]))


    #if 'GET' request
    else:

        form = CommentCreateForm()

    context = {
        'form': form,
        'post': post,
        'comment': comment,
        'blog_admin_flag': blog_admin_flag,
    }

    return render(request, 'dryblog/post_form.html', context)
여기서 유심히 살펴봐야할 부분은 
빨간색 박스 부분이다.

해당 View는 중첩된 댓글을 달 경우 참고하게 되는 View인데, 
기존에 있던 Comment 모델의 parent 필드에 
부모 노드가 되는 comment 객체를 넣어주는 것을 확인할 수 있다.



<head>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
{% extends "base_generic.html" %}
{% block content %}
  <section class="section">
    <div class="container">

        <h1 class="title">Title: {{ post.p_title }}</h1>
        <div class="content">
          <p><strong>Posted Name:</strong> <a href="{% url 'user_detail' post.p_name %}">{{ post.p_name }}</a></p> <!-- author detail link not yet defined -->
          <p><strong>Posted Date:</strong> {{ post.p_date }}</p>
          <p><strong>Description:</strong> {{ post.p_description }}</p>
        </div>
        <!-- <p><strong>Genre:</strong> {% for genre in book.genre.all %} {{ genre }}{% if not forloop.last %}, {% endif %}{% endfor %}</p> -->

        <!-- <div style="margin-left:20px;margin-top:20px"> -->
        <a href="{% url 'comment_create' post.id %}">Create</a>

        <hr>
        <h4 class="title is-5">Comments</h4>


        {% for comment in comment_list %}
        <hr>
        <!-- <p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">{{ copy.get_status_display }}</p>
        {% if copy.status != 'a' %}
          <p><strong>Due to be returned:</strong> {{copy.due_back}}</p>
          {% endif %} -->
          <div class="box">
            <p><strong>Commented Name:</strong> {{comment.c_name}}
              <a href="{% url 'comment_update' post.id comment.id %}">Update</a>
              <a href="{% url 'comment_delete' post.id comment.id %}">Delete</a>
            </p>
            <p><strong>Commented Date:</strong> {{comment.c_date}}</p>
            <p><strong>Description:</strong> {{comment.c_description}}</p>
            <a href="{% url 'reply_comment_create' comment.id %}">Reply</a>
            {% with reply_comment_list=comment.comment_set.all %}
              {% include 'dryblog/include/reply_comment_detail.html' %}
            {% endwith %}
      <!-- <p class="text-muted"><strong>Id:</strong> {{copy.id}}</p> -->
          </div>
          {% endfor %}
      </div>
  </section>
{% endblock %}
클라이언트 단에서의 로직은 빨간색 박스를 나타내며
각 댓글의 level 또는 depth는 section 태그와 container, box 태그를 통해 구현 한다. 

이를 통해 DB단에서 level이나 depth 컬럼을 추가하지 않고도
중첩된 댓글의 구현이 가능하다.

여기서 유심히 살펴봐야하는 곳은 
With 태그를 활용해서 계속해서 
dryblog/include/reply_comment_detail.html 안의 코드들을 불러오는 부분인데

dryblog/include/reply_comment_detail.html의 코드를 살펴보면 아래와 같다.


{% for reply_comment in reply_comment_list %}
    <div class="box">
      <p><strong>Commented Name:</strong> {{reply_comment.c_name}}</p>
      <p><strong>Commented Date:</strong> {{reply_comment.c_date}}</p>
      <p><strong>Description:</strong> {{reply_comment.c_description}}</p>
      <a href="{% url 'reply_comment_create' reply_comment.id %}">Reply</a>
      {% with reply_comment_list=reply_comment.comment_set.all %}
        {% include 'dryblog/include/reply_comment_detail.html' %}
      {% endwith %}
    </div>
{% endfor %}

이 부분에서 빨간색 박스 부분이 중요한데 
계속해서 reply_comment.id를 참고해 링크를 생성하게 되는데

처음 생성된 comment 객체가 아닌 reply 링크를 통해 생성된 
comment 객체를 참고하게 된다.

여기가 재귀를 이용하는 부분으로 가장 중요한 부분으로
여기에 기존 comment 객체의 id를 넣게된다면
클라이언트 단에서 3단 이상으로는 표현되지 않기 때문에 주의해야 한다.




마지막으로 맨 상단에 위와 같은 시트 스타일을 추가할 필요가 있다는 점을 잊지 말자.



정상적으로 구현이되었다면 위와 같이 
중첩된 댓글이 브라우저 화면에 표시되는 것을 확인할 수 있다.

규모에 따른 솔루션에 대해


실제 많은 솔루션들을 보고 개발도 해본 결과 
패턴을 크게 두 가지로 나눌 수 있다는 결론을 내렸다.

여기서 규모란,
해당 서비스를 이용하는 사용자의 규모를 말한다.

① 이용자가 적은 시스템

이용자가 적은 시스템에서는 
어떤 솔루션도 나쁘지 않다고 생각 한다.

물론 시스템의 퍼포먼스는 당연히 
댓글의 댓글 정도만 허용하는 것이 좋지만,

중첩된 댓글 기능은 
사용자를 좀 더 편안하게 해줄 수 있기 때문에
중첩된 댓글 기능을 솔루션으로 활용하는 것이 좋을 수도 있다.

따라서 중첩된 댓글도, 댓글의 댓글 정도만 허용하는 것도 
모두 훌륭한 솔루션이라 생각 한다.

대표적으로 블로그의 댓글 기능을 예로 들 수 있다.




② 이용자가 많은 시스템

이용자가 많은 시스템에서는 
일부분 타협할 수 밖에 없는데, 

대부분의 큰 규모의 시스템에서는 
중첩된 댓글을 허용하지 않는다.

따라서 이용자가 많은 시스템을 개발한다면,
댓글의 댓글 까지만 허용하는 것이 좋을 수 있다.

다만, 이럴 경우 '대상'에 대한 혼란을 야기할 수 있기 때문에 
이를 보완할 수 있는 추가적인 솔루션이 필요할 것이다.

대표적으로 네이버 뉴스 댓글과 같은 시스템을 예로 들 수 있다. 







결론


DB에서 계층적 데이터를 구현하기 위해 
어떤 솔루션이 가장 좋을까에 대한 이야기의 결론으로서

여전히 인접 리스트 모델(Adjacency List Model)을 사용하는 것이 
적절하다는 결론을 조심스럽게 내려볼 수 있을 것이다.

하지만, 애초에 댓글이라는 것은 
한 게시물에 그렇게 많이 달리지 않기 때문에 
인접 리스트 모델로도 충분히 솔루션으로서 사용할 수 있다고 생각한다.

물론 위에서 언급했다시피 
최고의 솔루션은 이에 맞는 새로운 자료 구조를 만들어 
제시하는 것이 최고의 솔루션이며,

잘 구현되어 있는 인접 리스트 모델 등과 같은 
오픈 소스를 사용하는 것이 차선의 솔루션 일 것이다.

다만 여기서 다른 개발자들이 이해를 쉽게 하기 위해 
내가 제시한 방법을 통해 좀 더 가독성을 높일 것인지,

아니면 가독성을 좀 버리고, 
오픈 소스를 사용할 것인지에 대한 것은
이를 도입하려하는 엔지니어의 몫일 것이다.



참고 :






2020.09.13 초안 작성 및 개행 완료
2021.01.31 Django로 인접 리스트 구현 추가
2021.02.12 Django로 인접 리스트 구현 개선
2021.02.28 Django로 인접 리스트 구현 내용 추가







이 블로그의 인기 게시물

[ Web ] 웹 애플리케이션 아키텍처 (Web Application Architecture)

[ Web ] 서버 사이드(Sever Side) ? 클라이언트 사이드(Client Side)? 1 [서론, 클라이언트 사이드(Client Side)]

[ Web ] 웹 애플리케이션? 웹 사이트?(Web Application? Web Site?)