파이썬 최고 장점 : 일관성

이유 :
파이썬 데이터 모델이 존재하며 파이썬 데이터 모델이 제공하는
API를 이용해서 고유의 객체를 정의하면 대부분의 파이썬 사용구를 적용할 수 있다.

 

💡 특별메서드 ⇒ 던더 (같은 말 메소드)

 

특별 메소드 사용 시 장점

  • 클래스 자체에서 구현한 임의 메서드명을 암기할 필요가 없다.
  • 항목 수를 알기위해서 size() , length() 같은 불확실 성이 아닌 len()으로 통일된
  • 파이썬 표준 라이브러리에서 제공하는 기능을 별도 구현없이 바로 사용 가능

 

특별 메소드에서 객체 표현

문자열 표현 repr() (우선) str(나중)

 

💡 하나만 구현해야할 경우 repr() 이유 str() 은 repr()이 대체 해줌

 

요약

특별 메소드 구현 시 내장형 객체처럼 작동하게 되어 파이썬스러움 표현력 있는 코딩 스타일을 구사

서론

현재 회사에서 인터넷선을 연결할 수 없는 환경에서
IP Camera 를 활용해 실시간 스트리밍, 저장, 알림(이상감지) 를 서비스를 하려고 한다.

 

그중에서 여러 대안을 찾다가 AWS Media 관련 상품들이 나름 잘 되어있어서 어떻게 Live를 만들었는지 적어볼려고 한다.

환경

1. FHD 급 IP Camera (RTSP 지원이 필요함)

2. 고정 IP 되는 LTE ROUTER(Port Forwarding 지원이 필요함)

3. 컴퓨터(4Core, 2gb 이상)

 

준비

1. IP Camera 설치 완료

2. IP Camera 와 고정 IP 를 할당받은 Lte Router 와 연결 완료(고정 IP 가 필수는 아니지만 중간에 바뀌는 경우 다시 재설정 필요)

 

시작

1. MediaPackage Create Channel

MediaPackage 를 들어가서 현재 만들고자 하는 채널의 ID와 Description 을 적어준다.
(Description 은 Option 이기는 하지만 적는 것을 추천한다.)

MediaPackage 채널 생성

채널 만드는 것을 완료했다면 아래와 같이 두개의 HLS ingest endpoint 와 빈 Origin endpoint 가 있게된다.

초기 MediaPackage 채널

2. Endpoint 생성

위의 MediaPackage 에서 Managa Endpoint를 클릭하여 새로운 endpoint를 생성한다.

기본적으로 ID 만 적어도 문제없기에 ID만 적고 진행한다.

MediaPackage Endpoint 설정

3.MediaLive Channel 생성

MediaLive 상품으로 이동하여 Create Channel 을 눌러준다.

MediaLive Channel 생성과정

먼저 진행할 것은 Input attachments 를 Add 하는 것이다.

아래의 이미지 순서대로 클릭하여서 Create Input 을 한다.

Input 생성 버튼

그러면 아래와 같은 이미지가 생성이 되어진다.

 

Input Name을 입력 후 

Input Type은 현재 rtsp => rtmp로 변환해서 보낼 것이기에 RTMP(Push)를 클릭한다.

Create Input 이미지

그리고 밑으로 내려와 input security group 을 create 로 하고 0.0.0.0/0 create input security group을 하여 생성해준다.

 

security group 설정

 

이제 Input에 방금 만들어둔 test-input 을 설정하고 Confirm 을 클릭한다.

Input 완료

Channel and Input details 를 클릭하여
현재 Channel name과 role 은 use existing role 사용, Template 은 HTTP Live Streaming(MediaPackage)를 선택한다.

그리고 Output 으로 들어가 처음에 만들었던 MediaPackage Channel ID 와 연결시켜준다.

그리고 본인이 원하는 Output 만 남기고 삭제한다.

그리고 밑에 왼쪽 주황색 버튼 Create Channel 을 클릭하여 만들어준다.

이제 이렇게 뜨게 될텐데 Current State idle 인 상태이기에 Start 를 눌러서 동영상 소스를 받을 수 있는 상태로 만들어준다.

(바로는 안되고 약간의 딜레이가 있다.)

4. FFmpeg 를 이용해서 IP Camera rstp 를 rtmp 로 변경하여 전송

먼저 MediaLive 에 있는 inputs 탭에서 본인이 만들었던 input으로 들어가 endpoint를 기억해둔다.

저기가 우리가 보내야할 장소인 곳이다.

 

그리고 본인의 컴퓨터 혹은 ec2에 ffmpeg 설치 후 아래와 같은 코드를 작성하면 된다.

ffmpeg -re -rtsp_transport tcp -i rtsp://<User이름>:<Password>@<IP addr>:<PORT><RTSP 주소> -c:a aac -c:v h264 -f flv <MediaLive Input 주소>

여기서 참고해야할 것은 2가지이다.

 

1. FFmpeg version 4 이상일 것(3의 경우 에러가 발생하는 경우를 발생)

2. RTSP 주소의 경우 IP Camera 제조사마다 다르다는 것(제조사마다 URL 형식이 다르다)

 

MediaLive Input 의 주소는 위의 이미지에서 endpoint 가 2개가 있는데 그것을 이용하면 된다.

 

5. Preview 를 통해서 확인

여기까지 문제없이 설정이 완료되었다면 MediaPackage로 이동 본인의 Channel 에서 확인이 가능하다.

확인 방법은 Origin endpoints 탭에서 Preview 를 클릭하면 아래와 같이 실시간으로 동작하고 있는 영상이 나올 것이다.

endpoint 를 보면 endpoint url 이 있는데 그것을 통해서 재생이 가능하다.

 

겪었던 문제

비용관련 문제

처음에는 QHD 급의 IP Camera 를 사용했다.

그러다보니 서버의 사양이 올라가야했고 이것은 비용의 상승으로 이루어졌다.

(ec2의 경우 core의 개수만 늘리는 게 불가 - 8코어 16기가)

하지만 FHD 급의 IP Camera 로 낮추고 테스트를 해보니 4코어 정도면 처리하는데 문제가 없었다.

 

ffmpeg 관련 문제

ffmpeg 같은 경우에는 테스트방식이

1. 1분짜리 저장이 원배속이 나오게 저장이 되는지 테스트

2. 변환과정과 전송과정이 원배속이 나오는지 테스트

이렇게 진행을 했고 실패한 경우에는 원인을 찾아서 고치는 방식으로 진행했다.

 

ubuntu 의 경우 apt 로 ffmpeg 를 설치하면 3.x 버전이 설치되어서 snap 을 통해서 4.x 버전을 설치했다.

서론

개인적으로 진행하던 프로젝트 중 축제와 관련이 있는 웹 프로젝트가 있었다.

축제의 디자인 포맷은 대부분 동일하고 변경되어지는 것은 시간표, 장소, 일정, 문구, 이미지 등등... 이 있었는데

한번의 개발을 통해서 여러 축제에 사용 될 수 있는 범용적인 웹 어플리케이션을 구축해야했다.

 

그런데 그때 당시에 읽었던 글 중 요기요에서 어플리케이션 Server Driven UI 를 보고 영감을 받았고 그것을 참고해서 진행했다. https://techblog.yogiyo.co.kr/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-server-driven-ui-%EA%B0%9C%EB%B0%9C%EA%B8%B0-b1b80f47760b

 

우당탕탕 Server Driven UI 개발기

안녕하세요! 요기요 R&D Center의 BE Developer 정승원입니다. 저는 요기요의 홈 화면을 담당하는 Home Squad에서 백엔드 개발을 하고 있습니다.

techblog.yogiyo.co.kr

 

설계

일단 설계를 하기 이전에 사람이 필수적으로 할 수 밖에 없는 작업을 생각했다.

도메인 연결, 검색 엔진 관련, 프론트 배포 등등이 존재했고 이러한 부분을 제외하고는 시스템 설계 부분에 다 녹여서 진행했다.

프론트

프론트의 설계는 간단했다.

1. 서버로부터 축제정보를 가져와서 렌더링(기본정보, 축제이름, 일시, 장소 등등...)

2. 서버로부터 디자인정보를 가져와서 렌더링(표 정보, 이미지, 설명 글 등등...)

3. 서버로부터 백오피스 정보를 가져와서 렌더링 상호작용(수정, 제거, 등록, 등등... )

4. 모든 API 는 백엔드로 부터 초기에 발급되어진 토큰으로 이루어진다.

예시로

 table: {
        ths: [
            { ...thProps, children: '종목' },
            { ...thProps, children: '순위' },
            { ...thProps, children: '시상' },
            { ...thProps, children: '비고' }
        ],
        trs: [
            [
                { ...tdProps, children: '하프코스 부문', rowSpan: '4' },
                { ...tdProps, children: '1위' },
                { ...tdProps, children: '20만원, 상장, 트로피' },
                { ...tdProps, children: `1-3위 현장 시상\n상금은 계좌이체`, rowSpan: '4' },
            ]
        ]
}

이런 형태의 Json 이 온다면 해당하는 표를 자동적으로 만들어준다.

 

해당 Json 이 javascript (...)같은 경우는 초기 데이터의 경우에는

프론트에서 제공되어지는 것을 기반으로 하고 그 이후에 서버에서 제공되어지는 data를 기반으로 rendering 이 되어지기 때문이다.

 

백엔드

백엔드의 경우에는 해당 축제에 대한 정보 및 디자인 정보, 백오피스 정보 및 사이트 관리를 중점을 두어서 설계를 했다.

1. 프론트에 해당하는 축제 토큰 등록 후 축제 정보 API, 축제 디자인 정보 API 제공

2. 토큰 기반을 통한 축제 별 백오피스 기능 구현(A축제, B축제 축제별 참여인원 관련 API 제공)

 

구현

구현의 경우에는 어렵지 않았고 특별하게 한 것만 적으면

 

위의 Json을 손수 적는 것은 비효율이었기에 Json Generator 라는 함수를 만들어서 간단하게 정보만 입력하면 Json 형식이 나오도록 함수를 제공해줬다.(폰트 관련, 축제 정보 관련)

 

그리고 이메일 관련해서 단일 Thread 로 진행시 Blocking 이 되어 전체 서비스의 영향을 끼쳐 멀티 Thread 를 통해서 진행하도록 변경하여 서비스의 영향을 덜 끼치도록 변경했다.

 

백엔드의 경우에는 서버 하나만 올리면 되어서 끝났으며 프론트의 경우에는 build 이후에 토큰만 발급받아서 S3 를 통해서 올리는 형태로 

백엔드(1) - 프론트(N) 의 형태로 구현이 되었다.

 

후기

Server Driven Design 이라고 말하고 뒤돌아봐서 생각해보니 단순 작업을 줄이는 역할로 치우쳐져서 사용했다는 것을 조금은 느꼈다.

 

현재 설계로 했을 때 가장 큰 장점은 사용자 측면에서 개발과 관련된 수정, 배포 관련을 모르고도 진행을 할 수 있다는 점이다.

축제 관련자 수정 요청 -> 개발자 수정 요청 사항 반영 및 재배포 -> 축제 관련자 확인

이러한 순서를 갖추는데

지금의 장점은 축제 관련자가 문구 수정이나 테이블 수정 간단한 부분은 본인이 할 수 있다는 점이고 재배포랑 관계없이 서버에서 수정만 일어나는 경우 해당하는 부분을 바로 확인이 가능하다는 것이다.

 

그런데 이러한 부분도 있지만 어떻게 보면 중점이 템플릿화에 초점이 맞추어지다보니 프로젝트가 약간의 성향이 벗어난 감이 없지않아있다.

 

 

 

서론

Jira 를 통해서 이슈가 관리되어지고 있는 상황에서 회사 메신저가 Synology Chat 인 경우에는

일반적인 방법으로는 이슈알림을 받기가 어렵다.

Slack 의 경우 Jira에서 공식적으로 지원을 하고 있기에 이러한 부분에서 유용한 도움을 받을 수 있지만

만약에, Slack을 사용하지않고 다른 Chat 어플리케이션을 쓰는 경우에는 Jira 의 자동화 그 중에서 웹요청을 통해 이슈 알림을 받을 수 있다.

 

Synology Chat

일단 Synology Chat 의 경우 다른 외부에서 들어오는 요청을 통해서 채팅방 혹은 개인에게 메시지를 보내는 것이 가능하다.

물론 이 과정속에서 들어오는 Webhook 을 설정할 필요가 있다.

 

해당하는 부분은 설정 > 통합 > 들어오는 webhooks 을 통해서 가능하고 Bot 도 가능하다.

 

이러한 부분은 다른 곳에 자세히 설명이 되어있으니 참고하면 될 것 같다.

그리고 처음에는 shell script 로 한번 작성을 했었는데 더욱 더 로직이 많아지면서 Python 으로 작성하는 것이 유리하다고 판단해 Python으로 작성했다.

그러면 Python 을 통해서 어떻게 보내는 지 알아보자.

 

코드

환경은 Lambda 기반으로 Jira -> API Gateway -> lambda -> Synlogy Chat 흐름이다.

Lambda 의 경우에는 Layer 를 통해서 외부라이브러리를 올릴수 있지만 그렇게 하지는 않았다.

이유는 이 프로젝트에서 필요한 것이 requests 하나인데 그것 때문에 올리기에는 약간 애매했다.

그러다보니 문제가 있었는데

urllib3 기반으로 메시지데이터 전송하는 것과 requests 라이브러리로 데이터를 전송하는 것에 인코딩차이가 존재한다는 것이였고

requests 경우에는 사용자가 따로 별도의 로직을 작성할 필요가 없었다면, urllib3 는 별도의 로직 작성이 필요했다.

 

그래서 추가된 것이 to_key_val_list(), encode_params() 이다.

현재 보내는 데이터가 Iterable 한 데이터이기 때문에 그 Items 도 encoding 을 해줘야한다.

그래서 해당 부분까지 작성하는 코드를 추가적으로 작성했다.

참고로 해당 코드는 requests 의 라이브러리 코드를 참고했다.

 

코드의 흐름은 이벤트 호출 -> Synology Chat  에서 User list 조회 및 저장 -> 담당자 Email 과 Synology Email 비교 및 담당자 ID 저장 -> 워크플로우에 맞게 메시지 작성 -> Synology Chat 에 User id 기반으로 메시지 전송(인코딩 포함)

import json
import urllib3
import urllib

def to_key_val_list(value):
   
    if value is None:
        return None

    if isinstance(value, (str, bytes, bool, int)):
        raise ValueError("cannot encode objects that are not 2-tuples")

    value = value.items()

    return list(value)

def encode_params(data):
    
    basestring = (str, bytes)

    if isinstance(data, (str, bytes)):
        return data
    elif hasattr(data, "read"):
        return data
    elif hasattr(data, "__iter__"):
        result = []
        for k, vs in to_key_val_list(data):
            if isinstance(vs, basestring) or not hasattr(vs, "__iter__"):
                vs = [vs]
            for v in vs:
                if v is not None:
                    result.append(
                        (
                            k.encode("utf-8") if isinstance(k, str) else k,
                            v.encode("utf-8") if isinstance(v, str) else v,
                        )
                    )
        return urllib.parse.urlencode(result, doseq=True)
    else:
        return data


def get_user_list(domain, token):
    url = f'{domain}/webapi/entry.cgi?api=SYNO.Chat.External&method=user_list&version=2&token={token}'
    res = http.request('GET', url)
    res_data = json.loads(res.data.decode('utf-8'))
    users = []
    for user in res_data['data']['users']:
        users.append({'id': user['user_id'], 'email': user['user_props']['email']})
    return users

def returns_user_ids(email, users):
    return [user['id'] for user in list(filter(lambda u: u['email'] in email, users))]


def send_message(domain, token, ids, text):
    url = f'{domain}/webapi/entry.cgi?api=SYNO.Chat.External&method=chatbot&version=2&token={token}'
    json_data = json.dumps({
        'text': text, 'user_ids': ids
    })

    data = encode_params({'payload':json_data})
    
    res = http.request(
        "POST",
        url,
        body=data,
        )

def lambda_handler(event, context):
    domain = "" Synology Chat Domain
    token = "" Synology Chat API Token
    result = event
    assign = result['assignee']
    users = get_user_list(domain, token)
    assign_ids = returns_user_ids([assign], users)
    assignee_text = ''
    
    
    
    if result['type'] == 'workflow':
        assignee_text = f'*({result["key"]}) 이슈가 {result["status"]} 로 이동되었습니다* \n ({result["key"]}) - {result["summary"]} \n {result["url"]}'
        if assign != result['reporter']:
            reporter_ids = returns_user_ids([result['reporter']], users)
            send_message(domain, token, reporter_ids, assignee_text)
    elif result['type'] == 'comment':
        if assign != result['author']:
            assignee_text = f'*({result["key"]}) 이슈에 {result["displayName"]}님이 댓글을 작성하였습니다.* \n {result["comment"]} \n {result["url"]}'
    elif result['type'] == 'assign':
        assignee_text = f'*({result["key"]}) 이슈가 할당되었습니다.* \n {result["summary"]} \n {result["url"]}'
    elif result['type'] == 'create':
        assignee_text = f'*({result["key"]}) 이슈가 할당되었습니다.* \n {result["summary"]} \n {result["url"]}'

    if assignee_text:
        send_message(domain, token, assign_ids, assignee_text)

    return {'status':'good'}

 

후기

만약에 본인이 Synology Chat 을 통해서 이와 같이 개발한다고 말하면 나는 만류하고싶다.

이유는 크게 두가지인데 첫번째는 Synology Chat 의 경우 불안정하고 두번째는 API 가 불친절하다.

 

첫번째의 경우에는 Synology Chat 이 요청을 받고 메시지를 전송하는데 1분이 넘는 시간이 소요된다는 것이다.

그러다보니 실시간으로 받는 듯한 느낌이 덜하고 옆에서 이슈등록되었다고 말하는 것이 더 빠르다 그냥 메모용으로 전락할 가능성이 크다.

 

두번째의 경우에는 Python API 요청을 하면서 느꼈는데 메시지가 너무 불친절하게 나오고 스펙이 없으니

에러의 포인트를 잡는 것부터 난관이였다.

그러다보니 처음에는 무조건 되는 Shell script 기반부터 시작해서 requests 라이브러리 등 여러가지를 비교한 다음에 문제를 파악하고

거기에 맞는 스펙을 유저가 알아서 맞춰서 보냈다.

 

그러다보니 만약에 Synology Chat으로 한다면 나는 그들의 환경이 Synology Chat 가 밀접한 관련이 없다면 그냥 Slack을 쓰라고 권유를 하고싶다.

 

참고

Jira 웹 요청 설정은 아래와 같았다.

{
    "type" : "create",
    "assignee": "{{assignee.emailAddress}}",
    "summary": "{{issue.summary}}",
    "key" : "{{issue.key}}",
    "project_name": "{{issue.project.name}}",
    "url": "{{issue.url}}"
}

 

어플을 개발하게 되었던 이유

이전에 인연이 있었던 회사에서 도움을 너무 간곡히 요청을 했었다,(1달에 한번씩 잊을때쯤이면 연락했었다 최소 8번은 했었다.)

 

그러다가 마지막에 내가 그러면 도움을 드리겠다하고, 진짜 터무니없는 적은 금액으로 어플을 개발하기 시작했다.

지금도 그 금액에 어플을 만들었다고하면 대부분의 사람들이 이상하게 쳐다볼것이다.

 

간략한 정보로 보통 받아야 할 최소금액의 1/4 만 받고 진행을 했다.

 

그럼에도 너무 간절하게 요청을 했기에 맘이 약해져서 도움을 주었다.

 

내가 맡은 일

1. 프론트(React Native)

2. 백엔드(Django)

3. 기획 보완(기존 기획에서 말이 안되는 경우를 말이 되게 변경)

4. 시스템 설계

5. 디자인 피드백(기존 디자인에서 유저 불친절한 디자인 변경)

6. 배포

 

진짜로 내가 했던 일이다.

사실 이것만 해도 이미 돈은 버리고 시작했다고 봐도 되는 일이다.

 

그럼에도 진행을 했었던 이유가 어플을 내돈이 아닌 다른 사람의 돈으로 만드는 게 나름의 신선한 경험이 될 거라고 생각을 했고

나중에 또 이게 강점이 될 것이라고 생각했다.

 

 

어떤 어플을 개발

내가 만들었던 어플은 간단하게 말하면

보호자가 피보호자의 위치를 확인하고 맞추어진 스케줄에 그 위치에 존재하는지 파악하고 알람을 보내주는 어플이였다.

위치 기반을 통한 스케줄 알림 어플이였다.

 

React Native 를 통해서 Play Store, App Store 에 등록이 되었고 Django 를 통해서 백엔드를 개발하고 배포는 AWS 에 했다.

 

개발만 하면 편한데...

이게 어플을 말로 간단하게 설명을 했을 때에는 매우 쉬웠는데 진짜 쉽지않은 개발이었다.

 

만약에 기획이 완성도가 있고 또한 디자인도 이상하게 안나왔으면 문제가 없는 개발이 되었을 것이다.

그런데 기획을 처음에 봤을 때부터 말이 안되는 부분이 많았고 또, 미처 발견하지 못하고 개발을 진행하다가 발견한 문제도 많았다.

 

그러다보니 중간에 일을 하다가 다시 기획쪽에 관여를 해서 도움을 주었고 또, 개발을 하다가 디자인 쪽에 요청을 하는 번거로움이 계속 존재했다.

순간순간 그래서 이것을 왜 맡았지라는 생각을 했었던 것 같았다.

 

그래도 한번 맡은 일은 무조건 끝내자라는 신념이 있기에 또 끝내려고 노력은 했다.

 

이때 진짜 중요한게 대화다. 대화를 제대로 못하면 진짜 일이 방향이 이상하게 간다.

특히 여기 회사와 회의를 하면서 매번 느꼈는데 중간에 이상한 말을 꾿꾿이 하는 사람이 있었는데

그 사람 때문에 기를 빨려서 회의가 길어졌었다.

 

개발을 마치고 배포?

일전에도 React Native를 통해서 어플을 개발한 적은 있지만 배포는 하지않았었는데

이번에는 배포또한 내가 직접했었다.

 

그러면서 내가 할 수 없는 문제들은 회사에서 처리를 했어야했는데 그게 세금 쪽이었고 나머지는 내가 다 관리를 해서 배포를 했다.

 

그때 많은 에러사항이 있었다.

안드로이드의 경우에는 문제는 크게 없었지만 로그인 정보를 어떻게 제공해줘야하는 지 명확한 명시가 없어서 약간 헤맸지만 다행히 찾아서 해결을 했다.

 

애플은 솔직히 기억이 안난다. 이유는 너무 많은 리젝트가 있었기 때문이다.

그것 때문에 리젝트가 될 때마다 수정하고 다시보내고 하는 것을 계속했던 것 같다.

 

심지어 가장 큰게 유료결제가 들어있다보니 이게 한층 더 어려움을 더했던 것 같다.

 

내가 만들었던 기능

  1. 약 50개 정도가 되는 페이지 제작(프론트)
  2. 달력 라이브러리 만들기(프론트)
  3. 데이터 피커 라이브러리 만들기(프론트)
  4. 위치센서를 이용해서 데이터보내기(프론트)
  5. 유료 정기 결제(프론트, 백)
  6. 알림 기능 (백)
  7. 위치 정보를 통한 스케줄 확인(백엔드)
  8. 인증 (문자 및 카카오톡 ) (백엔드)
  9. 위치센서 부정확한 위치 필터(백엔드)
  10. 백오피스 개발(백엔드)
  11. 그외 인증관련, 매핑관련(보호자(1)-피보호자(m)), 등등...

이거 외에도 많을 것 같은데 일단은 간추리면서 말하면 저렇게 되어질 것 같다.

 

후기

일단은 어플을 출시하고 그래도 어느정도의 성과는 있었던 것 같다

어플 다운로드수가 출시하고 1,000명을 넘겼었고 리뷰도 4.0 이상대를 기록했었다.(플레이스토어 + 앱스토어)

 

그리고 많은 경험이 있었던 것 같다.

좋은 경험도 있었지만 안좋은 경험이 역시나 더욱 더 기억에 매우 남는다.

 

갑자기 박람회를 가야한다고 일정을 앞당기고 거기에 있었던 휴먼에러를 나한테 원인파악을 부탁하고

결국에는 인증 문제였었는데 문자 혹은 카톡이 안간다는 거 였었는데

그게 문자가 안 가는 경우는 모르는 번호를 안 받는 기능을 켜서 그렇고 카톡이 안되는 경우는 그쪽 계정에 대한 문제로 안 갔었다.

 

그리고 나중에는 말도 안되는 기능을 추가하거나 계약 밖의 일을 가져오는 등등... 을 해서 솔직히 별로 맘 좋게 끝냈던 것 같지는 않다.

 

뭔가 호의로 답했을 때 호의로 돌아왔으면 좋았을텐데 그렇게 하지않음이 너무 아쉬웠던 작업이었다.

 

뭔가 고생은 고생대로 하고, 돈은 돈대로 못받고, 남는 것은 어플밖에 없었던 일이었던 것 같다.

 

그 어플도 아마 거기에서 사후관리를 제대로 못해서 앱스토어는 내려갔었던 것 같다.

 

참고로 어플이름은 스케줄넛이다. (플레이스토어에 나온다.)

 

 

서론

특정 사람의 특정 날짜 이동경로를 취합해서 csv 로 제공해주는 API 가 있었다.

 

그런데 여기에서 문제가 발생한다.

특정 사람의 특정 날짜 이동경로를 csv로 받는데 시간이 너무 걸린다는 것이다.

그때 거짓말 보태서 한 5분에서 ~ 10분 정도가 걸렸던 것 같다.

 

참고로 이 프로젝트의 경우에는 csv 로 받는 것은 관리자가 수동으로 버튼을 클릭을 해야지 동작을 한다.

그렇게 많은 사람들이 호출하는 API는 아니었다.

 

그래도 이거는 큰 문제였다.

 

그런데 해당 소스코드를 봤을 때에는 문제를 바로 확인을 할 수가 없었다.

 

이유는 초기에는 이렇게 오래걸리는거면 N+1문제라고 생각을 했고 그쪽으로 확인을 하려고 할 때

기존 개발자가 그거는 문제없이 처리했다고 했고 그래서 그부분을 배제하고 코드를 봤었다.

그리고 이거외에도 나는 다른 일을 하고있어서 그렇게 시간을 할애할 수가 없었고 그러다보니

언뜻보고 넘어가면서 왜지? 라는 의문을 남긴체 넘어갔다.

 

그때 로직이 어떻게 되었냐면

1. 해당 사람, 해당 날짜의 이동경로 내역을 가져온다.

2. 이동 경로에는 이동수단, 시간, 평균 속도, 평균 가속도 등등이 표현되었다.

3. 이동 경로에 연결된 위치 정보를 가져온다.

4. csv 에 입력 후 저장

 

그런데 이게 하나의 함수 안에 있어서 파악이 어려웠다.

 

그러다가 나에게 할당된 일을 대부분 마치고 컨펌을 기다리고있는 남는 시간에

아직도 해당 문제로 골머리를 앓고있는 개발자를 보고 다시 코드를 살피면서 문제를 파악했다

문제해결

일단 접근 방법은 기능을 하나씩 빼면서 진행하는 형태로 했다.

기능순서의 역순으로 하나씩 배제하면서 시간을 찍었다.

csv-> ... -> 이동경로 내역 순으로 진행을 했다.

 

그렇게 하다가 문제를 발견했다.

기존에 N+1 문제를 해결했다고 했던 개발자의 말을 믿고 그 부분을 대충보고 넘어가기도 했고 변수명이 진짜 절묘하게

되어있어서 문제를 파악하는데에 오래걸렸다.

진짜로 나중에는 1줄씩 주석처리를 하면서 문제를 파악했다.

 

요지는 이랬는데

1번 시점에서 이동경로 내역을 가져오면서 eager loading 을 통해서 연결된 위치정보를 가져왔다.

 

3번 시점에서 이동경로에 연결된 위치 정보를 eager loading 을 통해서 가져온 걸 안쓰고 다시 DB에 query를 날려서 가져왔다

(lazy loading).

 

그렇게 되어지다보니 그때 이동경로에 포함된 위치정보가 약 5만개 정도였는데 그게 N+1 문제가 터져서 속도 저하의 주범이 된 것이였다.

 

근데 진짜 정말로 언뜻보면 눈에 진짜 안띄게 작성이 되어있었다.

 

그러다보니 찾는데에 진짜 애를 많이 먹었었다.

 

그런데 나중에는 진짜 엄청 작은 단위를 주석처리하다가 얘는 왜 이렇게 작성되었지 하면서 지우고 나니까 속도가 정상으로 나왔었다.

 

그때 문제 파악하고 수정을 해서 진행을 하니 동작이 잘 되었고 또한 겸사겸사,

코드 리팩토링을 통해서 조금 더 보기쉽게 작업하고, 여러번 db에서 가져와 연산을 하는 작업을 한번 db에서 가져오는 형태로 변경했었다.

 

 

결론

일단 속도는 정상적으로 나왔다.

그렇지만 이 문제를 해결하기 위해 할애했던 시간이 그렇게 크지는 않지만 짬짬히 한 시간을 합치면 그래도 1~2일 정도는 되었을 거다.

 

그러면서도 이 문제가 그렇게 오랜 시간을 걸려야했던 문제였을까?

라고 생각하면 내 생각은 그렇지가 않다.

 

왜냐하면?

이 문제같은 경우는 N+1 문제라기보다는 너무 복잡하게 짜여진 코드가 문제였다.

 

이렇게 말하는 이유는 간단한 예시로 요리를 들자면

 

재료를 마트에서 사오고, 요리 시작(준비, 다듬기, 본요리), 접시에 담아서 내놓기 이렇게가 순서라면

그때의 소스코드 작성은 재료를 마트에서 사왔다가 요리를 하다가 다시 재료를 마트에서 사오는 것의 반복이었기 때문이다.

 

그러다보니 소스코드를 읽는 입장에서도 매우 힘들고 문제 파악은 더욱 더 힘들다.

 

DB 에 원하는 정보를 가져오기 => 받은 정보를 통해서 계산 => CSV 에 담아 저장하기

이렇게 진행을 했어야 했다면

 

소스코드가 DB에서 원하는 정보가져오기 => 받은 정보를 통해서 계산 => 부족한 게 있네 다시 DB에서 가져오기 => 받은 정보를 통해서 계산 (반복)

 

이렇게 하다보니 문제가 발생할 수 밖에 없는 형태였다.

 

아마도 개인이 맡아서 하는 프로젝트라 코드를 의식에 흐름대로 짜다가 나중에 개발이 완료되어지고나서 리팩토링이 안되어지다보니

이러한 문제가 발생했던 것 같다.

 

그러다가 다른 개발자(본인)이 봤을 때 보기가 어려워서 더욱 더 그랬던 것 같다.

서론

기존 주식관련 정보제공 플랫폼에 관련하여 일을 진행한 적이 있다.

 

그때 배포환경이 EB로 구축이 되어있었고 비용이 월 약 100만원에서 200만원 사이로 나왔던 것으로 확인이 되었다.

그리고 사용자는 유료 사용자가 100명 정도가 있고 무료 사용자의 경우 약 1,000명 정도가 있었다. 

 

그래도 나름 초기 플랫폼치고 서비스의 이용자가 있는 플랫폼이였다.

 

그런데 나는 사실 여기서 보면 의아한 부분이 있었다. 대략 사용자가 1,000 명 정도인데 왜 서버 비용이 저렇게 많이 나오는가?

 

내가 생각 했을때에는 월 30만원 정도로 절감할 수 있다 생각했다.

그리고 또 하나의 문제가 무엇이냐면 특정 기능을 수행하면 서버가 순간 다운되었다가 다시 스케일링 되는 형태였다.

이때 지연시간이 존재했고 그로인해서 다른 사용자도 영향을 받게되는 것이였다.

물론 스케일링이 되고 시간이 지나면 괜찮아졌지만, 뭔가 매끄러움은 없었던 것 같았다.

 

그럼에도 여기에 대해 바로 개선하기에는 조금은 어려운 상황이기는 했다.

 

이유

기본적으로 리스크가 있는 작업이었고(기존 사용자가 있는 상태에서 어떠하든지 일정시간의 장애를 경험할 수 밖에 없는 상황),

그리고 우리나라 주식 뿐만 아니라 해외주식까지 다루고 있다보니 변경할 수 있는 시간을 잡기가 어려웠다.

 

사용자가 없으면 사실 그냥 배포환경을 바꾸는 것에 그렇게 크게 어려움은 없었을 것 같다.

 

현재 상황 요약

문제점

1. 비용이 너무 많이 나온다

2. 하나의 기능 때문에 오토 스케일링이 일어나고 그 동안에 지연시간이 발생 -> 비용 증대 요인 중 하나

 

해결방법

1. 기존 서버의 사양을 적절한 테스트를 통해서 설정한다

2. 특정 기능만 따로 분리하는 형태로 변경 후, 기존 서버의 사양을 대폭 낮춤 

3. 서버리스 형태로 변경하기

 

선택에 도움을 줬던 부분

1. 서버에 그렇게 많은 노력을 기울일 수가 없다(인력부족).

2. 비용의 문제는 현재 심각하다.(기존에는 크레딧으로 결제 중, 이게 소진되면 바로 지출로 잡힌다.)

3. 전체적으로 AWS 에 그렇게 많이 친숙하지 않은 사람들

(해당 서비스를 외주를 통해서 납품을 받고 사용중, 외주측에서도 관리 측면으로는 도움을 줄 수 있는 환경이 아니었다)

 

해결방안

사실 스트레스 테스트를 통해서 적절한 사양을 찾는 것도 좋은 방법이 될 수도 있었을 것 같다.

서버에 관심이 있는 사람이 회사내에 있었다면 말이다.

 

하지만 현실은 그렇지 못했고 서버에 노력을 많이 할애할 수 있는 환경이 아니었다.

그래서 lambda 를 통해서 배포환경을 변경하게 되었다.

 

이유는 기존에도 Lambda를 통해서 많은 프로젝트들을 진행했었고

EB 같은 경우에는 설정하는 부분에서 많은 시간을 할애해야하다보니

지금같이 관리의 측면에서는 매우 벗어나는 영역이었다.

그러다보니 Lambda를 선택하게 되었고 또한, 분리라는 선택지를 고를 수도 있었지만

미래에도 이러한 경우가 생길 경우에 또 다시 이러한 대응을 해야할 환경이 생겨지다보니 일단은 잠깐의 시간유예를 두어서

조금 더 깊게 생각해보는 방향으로 진행하게되었다.

 

시간을 늘리기위해서는 크레딧 소진되는 시기를 뒤로 미룰 필요가 있었기 때문이다. 

 

실질적으로 Lambda를 통해서 배포하는 중에는 그렇게 크게 문제는 없었다.

워낙 많이 했던 작업이기도 했고 다행히 version 또한 맞았기에 따로 어려움이 없었던 작업이었다.

Zappa 를 통해서 배포를 진행했고 해당 주식장이 아예 멈추는 주말에 작업을 진행했다.

 

그리고 어떻게 배포를 했는지에 세세하게 적지않은 이유는 나보다 더 잘 쓴 사람들이 요즘은 너무 많다.

그냥 키워드만 던지고 검색만 하면 요즘은 손쉽게 찾을 수 있는 것 같다.

 

결론

결과는 우리가 원하는 예상치대로 나왔다.

시간을 벌게되었고(크레딧 소진시기를 미루게 되어짐), 신기능 개발에 몰두할 수 있었다.

 

이 문제가 발생한 가장 큰 요인은 외주를 통해서 받았을 때 적절한 사양으로 받지 못한 것이 가장 큰 요인이었다.

처음부터 적절한 사양으로 받았으면 아마 시간적 여유가 생겨서 더 나은 방안을 모색했을 것 같았지만

현실은 시간에 문제에 부딪혀 가장 익숙한 방법으로 해결하게 된 방법이기 때문이다.

 

그래서 그 부분이 아쉽기는 하다.

왜냐하면 그쪽에서는 템플릿 형태로 찍어내는 배포환경을 갖추다보니 문제가 생기면 템플릿을 바꾸어서 하다보니

적절한 세팅을 못한다는 것이 있고

그쪽에서는 따로 AWS 에 대해 잘 다루지도 못하고 다른 사람들에게 받은 설정파일을 통해서 작업을 하다보니

대응을 못하는게 매우 아쉬웠던 것 같다.

서론

기존 ERP를 기반으로 해서 새롭게 ERP 를 만들게 된 경험이 있다.

 

그때 기술 스택을 React + Django 로 개발 진행을 하게되었고

그때 내 밑으로 있었던 신입 개발자와 나는 사수로서 함께 작업을 하게 되었다.

 

그때 신입 개발자가 따로 어디쪽으로 개발을 진행한 경험이 없고 전공자이기만 해서

내가 프론트로 React를 해보는 게 어떻겠냐라고 제안을 했다.

왜냐하면 그전에 작업물이 그래도 node.js 기반으로 진행을 했기 때문에 그쪽이 더 적합할 것으로 판단했다.

제안하고 동생도 승낙을 해서 React(개발자 신입) + Django(본인) 이렇게 개발을 진행했다.

 

기한은 넉넉하게 잡아 2달이었고, 물론 기한내에 하기위해 내가 백엔드 작업을 빨리 끝내고 프론트를 도와주는 형태로 작업했다.

 

문제 발생

그러면서 막바지에 문제가 하나 생겼다.

 

처음에는 기존 테스트 데이터들만을 가지고 진행을 하다보니 데이터의 수가 그렇게 많지 않아 전체 주문에 대한 금액 계산이 오래 걸리지가 않았다.

 

그런데 기존에 있던 데이터베이스의 양이 많았고 옮기고 테스트를 돌려보니 시간이 너무 오래 걸리는 것이였다.

 

그때의 DB가 간추려서

주문내역(날짜, 구매자, 수금금액, 할인율, 부가세) - 아이템(수량, 금액, 이름, 입고일, 출고일, 입고금액) 이렇게 있었다.

 

계산방식

주문내역의 총액은 해당 주문내역을 부모키로 가진 아이템을 가져와 sum(아이템 별(수량 * 금액)) 이런 형태로 가져왔고(subquery)

 

다시 여기에 할인율, 부가세를 가져와 계산을 진행 후 수금이 되어진 금액을 통해서 만약에 수금이 안되었으면 -, 수금이 되었으면 + 로

나타내었다.(앱 내에서 처리)

 

그리고 주문금액의 총합이 전체 매출액으로 계산이 되어졌다.

 

그런데 당초 진행을 했을 때의 데이터는 100개 정도로만 하다가 갑자기

주문내역이 200,000건 정도로 늘어나니 총액 계산에 약 1분 30초정도가 소요되었다.

 

임시방편

그래서 내가 여기에서 먼저 진행한 것은 API 분리를 먼저 했다.

 

총매출액 계산이 포함된 API 호출이 되어지는 페이지가 로그인이 되면 제일 처음에 마주하는 대시보드 페이지였다.

그 페이지에서 GET API 호출을 했는데 다른 항목들은 준비가 되었지만

총매출액 계산에서 저렇게 시간이 오래걸리니 다른 곳에 영향을 끼쳐 렌더링이 되어지지않은 문제가 발생하니

API 를 따로 먼저 분리를 진행하고 개선을 시도했다.

 

일단은 그때 당시에 저 부분의 경우에는 먼저 subquery를 통해서 주문내역에 들어있는 총합은 계산이 되어져서 나오고

그다음에 계산들은 앱내에서 계산을 진행했다.

 

그렇게 하게 된 이유는 Subquery로 전체적으로 계산하기에는 일단 query가 너무 길어져 보기가 힘들기도 했고 속도개선이 그렇게 되어지지는 않았다.

 

그렇다보니 그냥 절대적인 양에 따른 시간이 문제임을 깨달았고 다른 방식으로 이 문제를 해결을 진행했다.

 

문제 해결

일단 절대적인 양 때문에 시간을 큰 폭으로 개선을 할 수 없음을 깨닫고 진행한 방식은

Redis 를 이용해 처음에 계산한 전체 매출액을 저장했다가
전체 매출액에 영향을 끼칠만한 행동이 있을 때 업데이트를 진행하는 방식으로 개선했다.

 

그렇게 될 경우에 처음에 1분30초 정도로 걸리고 다음에 호출을 할 때에는

조회에서는 당연히 Redis 에 접근하는 시간만큼 들 것으로 예상했고, 다른 작업에서는 조회 + 계산 + 저장 만큼의 시간으로 들것으로 판단했다.

 

예시로

만약에 주문 내역이 발생하면 주문내역의 금액 계산 진행 -> Redis 에 있는 전체 매출액을 가져오고 그 값을 계산 후 Redis 에 저장

기존에 (+)10,000 매출액이 있고 주문내역으로 (+)2,000이 발생한 경우 (+)10,000 + (+)2,000 = (+)12,000을 Redis 에 저장

 

이렇게 Update, Create, Delete 부분에 해당 로직을 적용시켰다.

 

결과

일단 결과는 매우 만족스러웠다.

 

예상했던대로

기존 1분 30초가 걸리는 API가 사실상 100ms 이내로 동작을 하게되었다.

(해당 부분 Redis에 정보만 가져오는 것만 했으면 더 짧았을 것이다.)

 

GET API 가 하는 것은 진짜 별것이 없었다. 그냥 Redis 에서 데이터만 가져오는 것이면 되었기 때문이다.

 

그리고 주기적으로 사람이 이용을 안하는 때(새벽)에 Cron을 이용해서 Redis 에 있는 값이 맞는지 검증하는 로직도 돌렸었다.

만약에 맞지않을 경우에는 업데이트 후 알림을 보내도록 했었는데 딱히 알림이 오는 일은 없었다.

 

이게 절대적인 데이터의 양이 많아지면 어쩔 수 없이 시간이 걸리는 문제가 발생한다.

뭔가 offset pagination 같은 느낌이다.

그냥 읽기만 해도 시간이 걸리는 느낌?

그런데 거기에 계산을 해? -> 더 걸린다. 이 느낌이다.

 

그래도 이때 프로젝트가 의미있었던 시간이었던게 해당문제를 처리하는 방법이 많이 있었겠지만

빠르게 문제 요인을 파악하고 그때 당시에 가장 적절한 방법을 취했던 것 같았다.

그리고 그것을 검증하는 로직을 통해서 이상이 있을 경우에는 해당 부분을 다시 검토 후 저장하다보니

문제가 발생할 수 있는 순간도 줄였던 것 같다.

 

그리고 처음으로 누군가를 가르치면서 했던 경험이라서 내가 새롭게 배운 느낌도 있었다.

뭔가 내가 처음에 React를 했을 때에는 경험했던 부분들에 대해 의문이 안 들었던 부분이 있었는데

신입 개발자가 의문을 가지고 질문을 했을 때 일단 내가 생각했던 느낌을 말하고 그래도 확실하지는 않기에

문서를 찾아서 다시 재확증하는? 경험을 많이 했는데 그러면서 다시 재각인하는 시간이 되었던 것 같다.

 

+ Recent posts