스토리 홈

인터뷰

피드

뉴스

조회수 1204

AWS Batch 사용하기

OverviewAWS Batch는 배치 컴퓨팅 작업을 효율적으로 실행할 수 있게 도와줍니다. 배치 작업량과 리소스 요청을 기반으로 최적의 리소스 수량 및 인스턴스 유형을 동적으로 프로비져닝합니다. AWS Batch에서는 별도의 관리가 필요 없기 때문에 문제 해결에 집중할 수 있습니다. 별도의 추가 비용은 없습니다. 배치 작업을 저장 또는 실행할 목적으로 생성된 AWS 리소스(인스턴스 등)에 대해서만 비용을 지불하면 됩니다. 이번 포스팅에서는 간단한 튜토리얼로 AWS Batch 사용 방법을 크게 11개의 Step으로 알아보겠습니다. 이렇게 진행하겠습니다.AWS에서 제공하는 Dockerfile, fetch&run 스크립트 및 myjob.sh 다운로드Dockerfile를 이용하여 fetch&run 스크립트를 포함한 Docker 이미지 생성생성된 Docker 이미지를 ECR(Amazon Elastic Container Registry)로 푸쉬간단한 샘플 스크립트(myjob.sh)를 S3에 업로드IAM에 S3를 접속 할 수 있는 ECS Task role 등록Compute environments 생성Job queues 생성ECR을 이용하여 Job definition 생성Submit job을 통해 S3에 저장된 작업 스크립트(myjob.sh)를 실행하기결과 확인 STEP1. AWS에서 제공하는 Dockerfile, fetch&run 스크립트 및 myjob.sh 다운로드AWS Batch helpers페이지에 접속합니다.    2. /fetch-and-run/에서 Dockerfile, fetchandrun.sh, myjob.sh 다운로드합니다.STEP2. Dockerfile을 이용하여 fetch&run 스크립트를 포함한 Docker 이미지 생성Dockerfile을 이용해서 Docker 이미지를 빌드합니다.잠시 Dockerfile의 내용을 살펴보겠습니다.FROM amazonlinux:latestDocker 공식 Repository에 있는 amazonlinux 의 lastest 버젼으로 빌드RUN yum -y install which unzip aws-cliRUN을 통해 이미지 빌드 시에 yum -y install which unzip aws-cli를 실행ADD fetch_and_run.sh /usr/local/bin/fetch_and_run.shADD를 통해 Dockerfile과 같은 디렉토리에 있는 fetch_and_run.sh를 /usr/local/bin/fetch_and_run.sh에 복사 WORKDIR /tmp컨테이너가 동작할 때 /tmp를 기본 디렉토리로 설정USER nobody컨테이너 실행 시 기본 유저 설정 ENTRYPOINT [“/usr/local/bin/fetch_and_run.sh”]컨테이너 실행 시 /usr/local/bin/fetch_and_run.sh를 call shell에 docker 명령을 통해 이미지 생성shell : docker build -t fetch_and_run . 실행하면 아래와 같은 결과가 출력됩니다.[ec2-user@AWS_BRANDI_STG fetch-and-run]$ docker build -t fetch_and_run . Sending build context to Docker daemon 8.192kB Step 1/6 : FROM amazonlinux:latest latest: Pulling from library/amazonlinux 4b92325dc37b: Pull complete Digest: sha256:9ee13e494b762db41b9db92a200f6784b78da5ac3b0f974fb1c38feb7f636474 Status: Downloaded newer image for amazonlinux:latest ---> 81bb3e78db3d Step 2/6 : RUN yum -y install which unzip aws-cli ---> Running in 1f5293a2294d Loaded plugins: ovl, priorities Resolving Dependencies --> Running transaction check ---> Package aws-cli.noarch 0:1.14.9-1.48.amzn1 will be installed --> Processing Dependency: python27-jmespath = 0.9.2 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: python27-botocore = 1.8.13 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: python27-rsa >= 3.1.2-4.7 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: python27-futures >= 2.2.0 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: python27-docutils >= 0.10 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: python27-colorama >= 0.2.5 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: python27-PyYAML >= 3.10 for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: groff for package: aws-cli-1.14.9-1.48.amzn1.noarch --> Processing Dependency: /etc/mime.types for package: aws-cli-1.14.9-1.48.amzn1.noarch ---> Package unzip.x86_64 0:6.0-4.10.amzn1 will be installed ---> Package which.x86_64 0:2.19-6.10.amzn1 will be installed --> Running transaction check ---> Package groff.x86_64 0:1.22.2-8.11.amzn1 will be installed --> Processing Dependency: groff-base = 1.22.2-8.11.amzn1 for package: groff-1.22.2-8.11.amzn1.x86_64 ---> Package mailcap.noarch 0:2.1.31-2.7.amzn1 will be installed ---> Package python27-PyYAML.x86_64 0:3.10-3.10.amzn1 will be installed --> Processing Dependency: libyaml-0.so.2()(64bit) for package: python27-PyYAML-3.10-3.10.amzn1.x86_64 ---> Package python27-botocore.noarch 0:1.8.13-1.66.amzn1 will be installed --> Processing Dependency: python27-dateutil >= 2.1 for package: python27-botocore-1.8.13-1.66.amzn1.noarch ---> Package python27-colorama.noarch 0:0.2.5-1.7.amzn1 will be installed ---> Package python27-docutils.noarch 0:0.11-1.15.amzn1 will be installed --> Processing Dependency: python27-imaging for package: python27-docutils-0.11-1.15.amzn1.noarch ---> Package python27-futures.noarch 0:3.0.3-1.3.amzn1 will be installed ---> Package python27-jmespath.noarch 0:0.9.2-1.12.amzn1 will be installed --> Processing Dependency: python27-ply >= 3.4 for package: python27-jmespath-0.9.2-1.12.amzn1.noarch ---> Package python27-rsa.noarch 0:3.4.1-1.8.amzn1 will be installed --> Processing Dependency: python27-pyasn1 >= 0.1.3 for package: python27-rsa-3.4.1-1.8.amzn1.noarch --> Processing Dependency: python27-setuptools for package: python27-rsa-3.4.1-1.8.amzn1.noarch --> Running transaction check ---> Package groff-base.x86_64 0:1.22.2-8.11.amzn1 will be installed ---> Package libyaml.x86_64 0:0.1.6-6.7.amzn1 will be installed ---> Package python27-dateutil.noarch 0:2.1-1.3.amzn1 will be installed --> Processing Dependency: python27-six for package: python27-dateutil-2.1-1.3.amzn1.noarch ---> Package python27-imaging.x86_64 0:1.1.6-19.9.amzn1 will be installed --> Processing Dependency: libjpeg.so.62(LIBJPEG_6.2)(64bit) for package: python27-imaging-1.1.6-19.9.amzn1.x86_64 --> Processing Dependency: libjpeg.so.62()(64bit) for package: python27-imaging-1.1.6-19.9.amzn1.x86_64 --> Processing Dependency: libfreetype.so.6()(64bit) for package: python27-imaging-1.1.6-19.9.amzn1.x86_64 ---> Package python27-ply.noarch 0:3.4-3.12.amzn1 will be installed ---> Package python27-pyasn1.noarch 0:0.1.7-2.9.amzn1 will be installed ---> Package python27-setuptools.noarch 0:36.2.7-1.33.amzn1 will be installed --> Processing Dependency: python27-backports-ssl_match_hostname for package: python27-setuptools-36.2.7-1.33.amzn1.noarch --> Running transaction check ---> Package freetype.x86_64 0:2.3.11-15.14.amzn1 will be installed ---> Package libjpeg-turbo.x86_64 0:1.2.90-5.14.amzn1 will be installed ---> Package python27-backports-ssl_match_hostname.noarch 0:3.4.0.2-1.12.amzn1 will be installed --> Processing Dependency: python27-backports for package: python27-backports-ssl_match_hostname-3.4.0.2-1.12.amzn1.noarch ---> Package python27-six.noarch 0:1.8.0-1.23.amzn1 will be installed --> Running transaction check ---> Package python27-backports.x86_64 0:1.0-3.14.amzn1 will be installed --> Finished Dependency Resolution Dependencies Resolved ================================================================================ Package                              Arch   Version            Repository                                                                           Size ================================================================================ Installing:  aws-cli                              noarch 1.14.9-1.48.amzn1  amzn-main 1.2 M  unzip                                x86_64 6.0-4.10.amzn1     amzn-main 201 k  which                                x86_64 2.19-6.10.amzn1    amzn-main  41 k  Installing for dependencies:  freetype                             x86_64 2.3.11-15.14.amzn1 amzn-main 398 k  groff                                x86_64 1.22.2-8.11.amzn1  amzn-main 1.3 M  groff-base                           x86_64 1.22.2-8.11.amzn1  amzn-main 1.1 M  libjpeg-turbo                        x86_64 1.2.90-5.14.amzn1  amzn-main 144 k  libyaml                              x86_64 0.1.6-6.7.amzn1    amzn-main  59 k  mailcap                              noarch 2.1.31-2.7.amzn1   amzn-main  27 k  python27-PyYAML                      x86_64 3.10-3.10.amzn1    amzn-main 186 k  python27-backports                   x86_64 1.0-3.14.amzn1     amzn-main 5.0 k  python27-backports-ssl_match_hostname                                       noarch 3.4.0.2-1.12.amzn1 amzn-main  12 k  python27-botocore                    noarch 1.8.13-1.66.amzn1  amzn-main 4.1 M  python27-colorama                    noarch 0.2.5-1.7.amzn1    amzn-main  23 k  python27-dateutil                    noarch 2.1-1.3.amzn1      amzn-main  92 k  python27-docutils                    noarch 0.11-1.15.amzn1    amzn-main 1.9 M  python27-futures                     noarch 3.0.3-1.3.amzn1    amzn-main  30 k  python27-imaging                     x86_64 1.1.6-19.9.amzn1   amzn-main 428 k  python27-jmespath                    noarch 0.9.2-1.12.amzn1   amzn-main  46 k  python27-ply                         noarch 3.4-3.12.amzn1     amzn-main 158 k  python27-pyasn1                      noarch 0.1.7-2.9.amzn1    amzn-main 112 k  python27-rsa                         noarch 3.4.1-1.8.amzn1    amzn-main  80 k  python27-setuptools                  noarch 36.2.7-1.33.amzn1  amzn-main 672 k  python27-six                         noarch 1.8.0-1.23.amzn1   amzn-main  31 k Transaction Summary ================================================================================ Install 3 Packages (+21 Dependent packages) Total download size: 12 M Installed size: 51 M Downloading packages: -------------------------------------------------------------------------------- Total 1.0 MB/s | 12 MB 00:12 Running transaction check Running transaction test Transaction test succeeded  Running transaction   Installing : python27-backports-1.0-3.14.amzn1.x86_64                    1/24   Installing : python27-backports-ssl_match_hostname-3.4.0.2-1.12.amzn1    2/24   Installing : python27-setuptools-36.2.7-1.33.amzn1.noarch                3/24   Installing : python27-colorama-0.2.5-1.7.amzn1.noarch                    4/24   Installing : freetype-2.3.11-15.14.amzn1.x86_64                          5/24   Installing : libyaml-0.1.6-6.7.amzn1.x86_64                              6/24   Installing : python27-PyYAML-3.10-3.10.amzn1.x86_64                      7/24   Installing : mailcap-2.1.31-2.7.amzn1.noarch                             8/24   Installing : python27-ply-3.4-3.12.amzn1.noarch                          9/24   Installing : python27-jmespath-0.9.2-1.12.amzn1.noarch                  10/24   Installing : python27-futures-3.0.3-1.3.amzn1.noarch                    11/24   Installing : python27-six-1.8.0-1.23.amzn1.noarch                       12/24   Installing : python27-dateutil-2.1-1.3.amzn1.noarch                     13/24   Installing : groff-base-1.22.2-8.11.amzn1.x86_64                        14/24   Installing : groff-1.22.2-8.11.amzn1.x86_64                             15/24   Installing : python27-pyasn1-0.1.7-2.9.amzn1.noarch                     16/24   Installing : python27-rsa-3.4.1-1.8.amzn1.noarch                        17/24   Installing : libjpeg-turbo-1.2.90-5.14.amzn1.x86_64                     18/24   Installing : python27-imaging-1.1.6-19.9.amzn1.x86_64                   19/24   Installing : python27-docutils-0.11-1.15.amzn1.noarch                   20/24   Installing : python27-botocore-1.8.13-1.66.amzn1.noarch                 21/24   Installing : aws-cli-1.14.9-1.48.amzn1.noarch                           22/24   Installing : which-2.19-6.10.amzn1.x86_64                               23/24   Installing : unzip-6.0-4.10.amzn1.x86_64                                24/24   Verifying  : libjpeg-turbo-1.2.90-5.14.amzn1.x86_64                      1/24   Verifying  : groff-1.22.2-8.11.amzn1.x86_64                              2/24   Verifying  : unzip-6.0-4.10.amzn1.x86_64                                 3/24   Verifying  : python27-pyasn1-0.1.7-2.9.amzn1.noarch                      4/24   Verifying  : groff-base-1.22.2-8.11.amzn1.x86_64                         5/24   Verifying  : aws-cli-1.14.9-1.48.amzn1.noarch                            6/24   Verifying  : python27-six-1.8.0-1.23.amzn1.noarch                        7/24   Verifying  : python27-dateutil-2.1-1.3.amzn1.noarch                      8/24   Verifying  : python27-docutils-0.11-1.15.amzn1.noarch                    9/24   Verifying  : python27-PyYAML-3.10-3.10.amzn1.x86_64                     10/24   Verifying  : python27-botocore-1.8.13-1.66.amzn1.noarch                 11/24   Verifying  : python27-futures-3.0.3-1.3.amzn1.noarch                    12/24   Verifying  : python27-ply-3.4-3.12.amzn1.noarch                         13/24   Verifying  : python27-jmespath-0.9.2-1.12.amzn1.noarch                  14/24   Verifying  : mailcap-2.1.31-2.7.amzn1.noarch                            15/24   Verifying  : python27-backports-ssl_match_hostname-3.4.0.2-1.12.amzn1   16/24   Verifying  : libyaml-0.1.6-6.7.amzn1.x86_64                             17/24   Verifying  : python27-rsa-3.4.1-1.8.amzn1.noarch                        18/24   Verifying  : freetype-2.3.11-15.14.amzn1.x86_64                         19/24   Verifying  : python27-colorama-0.2.5-1.7.amzn1.noarch                   20/24   Verifying  : python27-setuptools-36.2.7-1.33.amzn1.noarch               21/24   Verifying  : which-2.19-6.10.amzn1.x86_64                               22/24   Verifying  : python27-imaging-1.1.6-19.9.amzn1.x86_64                   23/24   Verifying  : python27-backports-1.0-3.14.amzn1.x86_64                   24/24 Installed:   aws-cli.noarch 0:1.14.9-1.48.amzn1        unzip.x86_64 0:6.0-4.10.amzn1   which.x86_64 0:2.19-6.10.amzn1   Dependency Installed:   freetype.x86_64 0:2.3.11-15.14.amzn1   groff.x86_64 0:1.22.2-8.11.amzn1   groff-base.x86_64 0:1.22.2-8.11.amzn1   libjpeg-turbo.x86_64 0:1.2.90-5.14.amzn1   libyaml.x86_64 0:0.1.6-6.7.amzn1   mailcap.noarch 0:2.1.31-2.7.amzn1   python27-PyYAML.x86_64 0:3.10-3.10.amzn1   python27-backports.x86_64 0:1.0-3.14.amzn1   python27-backports-ssl_match_hostname.noarch 0:3.4.0.2-1.12.amzn1   python27-botocore.noarch 0:1.8.13-1.66.amzn1   python27-colorama.noarch 0:0.2.5-1.7.amzn1   python27-dateutil.noarch 0:2.1-1.3.amzn1   python27-docutils.noarch 0:0.11-1.15.amzn1   python27-futures.noarch 0:3.0.3-1.3.amzn1   python27-imaging.x86_64 0:1.1.6-19.9.amzn1   python27-jmespath.noarch 0:0.9.2-1.12.amzn1   python27-ply.noarch 0:3.4-3.12.amzn1   python27-pyasn1.noarch 0:0.1.7-2.9.amzn1   python27-rsa.noarch 0:3.4.1-1.8.amzn1   python27-setuptools.noarch 0:36.2.7-1.33.amzn1   python27-six.noarch 0:1.8.0-1.23.amzn1   Complete! Removing intermediate container 1f5293a2294d  ---> 5502efa481ce Step 3/6 : ADD fetch_and_run.sh /usr/local/bin/fetch_and_run.sh  ---> 1b69173e586f Step 4/6 : WORKDIR /tmp Removing intermediate container a69678c65ee7  ---> 8a560dd25401 Step 5/6 : USER nobody  ---> Running in e063ac6e6fdb Removing intermediate container e063ac6e6fdb  ---> e5872fd44234 Step 6/6 : ENTRYPOINT ["/usr/local/bin/fetch_and_run.sh"]  ---> Running in e25af9aa5fdc Removing intermediate container e25af9aa5fdc  ---> dfca872de0be Successfully built dfca872de0be Successfully tagged awsbatch-fetch_and_run:latest docker images 명령으로 새로운 로컬 repository를 확인할 수 있습니다.shell : docker images [ec2-user@AWS_BRANDI_STG fetch-and-run]$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE fetch_and_run latest dfca872de0be 2 minutes ago 253MB amazonlinux              latest              81bb3e78db3d        2 weeks ago         165MB STEP3. ECR에서 repository 생성아래는 ECR 초기 화면입니다.fetch_and_run이란 이름으로 Repository 생성합니다. 3. Repository 생성이 완료되었습니다.STEP4. ECR로 빌드된 이미지를 pushECR에 docker login후 빌드된 Docker 이미지에 태그합니다. shell : aws ecr get-login --no-include-email --region ap-northeast-2 빌드된 docker 이미지에 태그하세요.shell : docker tag fetch_and_run:latest 000000000000.dkr.ecr.ap-northeast-2.amazonaws.com/fetch_and_run:latest 태그된 docker 이미지를 ECR에 push합니다.shell: docker push 000000000000.dkr.ecr.ap-northeast-2.amazonaws.com/fetch_and_rrun:latest 아래는 ECR fetch_and_run Repository에 푸쉬된 Docker 이미지입니다.STEP5. 간단한 샘플 스크립트(myjob.sh)를 S3에 업로드아래는 간단한 myjob.sh 스크립트입니다.#!/bin/bash date echo "Args: $@" env echo "This is my simple test job!." echo "jobId: $AWS_BATCH_JOB_ID" sleep $1 date echo "bye bye!!" 위의 myjob.sh를 S3에 업로드합니다.shell : aws s3 cp myjob.sh s3:///myjob.sh STEP6. IAM에 S3를 접속할 수 있는 ECS Task role 등록Role 등록 화면에서 Elastic Container Service 선택 후, Elastic Container Service Task를 선택합니다.AmazonS3ReadOnlyAccess Policy를 선택합니다.아래 이미지는 Role에 등록 하기 전 리뷰 화면입니다.Role에 AmazonS3ReadOnlyAccess가 등록된 것을 확인합니다.STEP7. Compute environments 생성AWS Batch 콘솔에서 Compute environments를 선택하고, Create environment 선택합니다.Compute environment type은 Managed와 Unmanaged 두 가지를 선택할 수 있습니다. Managed는 AWS에서 요구사항에 맞게 자원을 관리해주는 것이고, Unmanaged는 직접 자원을 관리해야 합니다. 여기서는 Managed를 선택하겠습니다.Compute environment name을 입력합니다.Service Role을 선택합니다. 기존 Role을 사용하거나 새로운 Role을 생성할 수 있습니다. 새 Role을 생성하면 필수 역할 (AWSBatchServiceRole)이 생성됩니다.Instnace Role을 선택합니다. 기존 Role을 사용하거나 새로운 Role을 생성할 수 있습니다. 새 Role을 생성하면 필수 역할(ecsInstanceRole)이 생성됩니다.EC2 key pair에서 기존 EC2 key pair를 선택합니다. 이 key pair를 사용하여 SSH로 인스턴스에 접속할 수 있지만 이번 글의 예제에서는 선택하지 않겠습니다.Configure your compute resources Provisioning Model은 On-Demand와 Spot이 있습니다. 차이점은 Amazon EC2 스팟 인스턴스를 참고해주세요. 여기서는 On-Demand를 선택합니다.Allowed instance types에서는 시작 인스턴스 유형을 선택합니다. optimal을 선택하면 Job queue의 요구에 맞는 인스턴스 유형을 (최신 C, M, R 인스턴스 패밀리 중) 자동으로 선택합니다. 여기서는 optimal을 선택하겠습니다.Minimum vCPUs는 Job queue 요구와 상관없이 Compute environments에 유지할 vCPU 최소 개수입니다. 0을 입력해주세요.Desired vCPUs는 Compute environment에서 시작할 EC2 vCPU 개수입니다. Job queue 요구가 증가하면 필요한 vCPU를 Maximum vCPUs까지 늘리고 요구가 감소하면 vCPU 수를 Minimum vCPUs까지 줄이고 인스턴스를 제거합니다. 0을 입력해주세요.Maximum vCPUs는 Job queue 요구와 상관없이 Compute environments에서 확장할 수 있는 EC2 vCPU 최대 개수입니다. 여기서는 256을 입력합니다.Enable user-specified Ami ID는 사용자 지정 AMI를 사용하는 옵션입니다. 여기서는 사용하지 않겠습니다.Networking VPC Id 인스턴스를 시작할 VPC를 선택합니다.Subnet을 선택합니다.Security groups를 선택합니다.그리고 EC2 tags를 지정하여 생성된 인스턴스가 이름을 가질 수 있게 합니다. Key : Name, Value : AWS Batch InstanceCreate을 클릭해 Compute environment를 생성합니다.아래 이미지는 생성된 Compute environment입니다.STEP8. Job queues 생성AWS Batch 콘솔에서 Job queues - Create queue를 선택합니다.Queue name을 입력합니다.Priority는 Job queue의 우선순위를 입력합니다. 우선순위가 1인 작업은 우선순위가 5인 작업보다 먼저 일정이 예약됩니다. 여기서는 5를 입력하겠습니다.Enable Job queue가 체크되어 있어야 job을 등록할 수 있습니다.Select a compute environment에서 Job queue와 연결될 Compute environment을 선택합니다. 최대 3개의 Compute environment를 선택할 수 있습니다.생성된 Job queue, Status가 VALID면 사용 가능합니다.STEP9. ECR을 이용하여 Job definition 생성AWS Batch 콘솔에서 Job definitions - Create를 선택합니다.Job definition name을 입력하고 이전 작업에서 만들 IAM Role을 선택하세요, 그리고 ECR Repository URI를 입력합니다. 000000000000.dkr.ecr.ap-northeast-2.amazonaws.com/fetch_and_runCommand 필드는 비워둡시다.vCPUs는 컨테이너를 위해 예약할 vCPU의 수, Memory(Mib)는 컨테이너에 제공할 메모리의 제한, Job attempts는 작업이 실패할 경우 다시 시도하는 최대 횟수, Execution timeout은 실행 제한 시간, Ulimits는 컨테이너에 사용할 사용자 제한 값입니다. 여기서는 vCPUs는 1, Memory(MiB)는 512, Job Attempts는 1로 설정, Execution timeout은 기본값인 100 그리고 Limits는 설정하지 않습니다.vCPUs: 컨테이너를 위해 예약할 vCPU의 개수Memory(Mib): 컨테이너에 제공할 메모리의 제한Jop attempts: 작업이 실패할 경우 다시 시도하는 최대 횟수Execution timeout: 실행 제한 시간Ulimits: 컨테이너에 사용할 사용자 제한 값User는 기본값인 nobody로 선택 후, Create job definition을 선택합니다.Job definitions에 Job definition이 생성된 것을 확인할 수 있습니다.STEP10. Submit job을 통해 S3에 저장된 작업 스크립트(myjob.sh)를 실행하기AWS Batch 콘솔에서 Jobs를 선택합니다. Job을 실행할 Queue를 선택하고 Submit job을 선택합니다.Job run-time1)Job name을 입력합니다.2)Job definition을 선택합니다.3)실행될 Job queue를 선택합니다.Environment Job Type을 선택하는 부분에서는 Single을 선택합니다. Array 작업에 대한 자세한 내용은 어레이 작업 페이지를 참고해주세요.Job depends on은 선택하지 않습니다.자세한 내용은 작업 종속성 페이지를 참고해주세요.Environment Command에서 컨테이너에 전달할 명령을 입력합니다. 여기서는 [“myjob.sh”, “30”] 를 입력해주세요. vCPUs, Memory, Job attempts와 Execution timeout은 job definition에 설정된 값을 가져옵니다. 이 Job에 대한 설정도 가능합니다.Parameters를 통해 job을 제출할 때 기본 작업 정의 파라미터를 재정의 할 수 있습니다. Parameters에 대한 자세한 내용은 작업 정의 파라미터 페이지를 참고해주세요.Environment variables는 job의 컨테이너에 환경 변수를 지정할 수 있습니다. 여기서 주의할 점은 Key를 AWS_BATCH로 시작하면 안 된다는 것입니다. AWS Batch에 예약된 변수입니다.Key=BATCH_FILE_TYPE, Value=script Key=BATCH_FILE_S3_URL, Value=s3:///myjob.shSubmit job을 선택합니다.Job이 Submitted 된 화면입니다.Dashboard를 보시면 Runnable 상태로 대기 중인 것을 확인할 수 있습니다.STEP11. 결과 확인CloudWatch > Log Groups > /aws/batch/job에서 실행 로그를 확인할 수 있습니다.Conclusion간단한 튜토리얼로 AWS Batch를 설정하고 실행하는 방법을 알아봤습니다.(참 쉽죠?) 다음 글에서는 AWS Batch의 Array 또는 Job depends on등의 확장된 기능들을 살펴보겠습니다. 참고1) AWS Batch – 쉽고 효율적인 배치 컴퓨팅 기능 – AWS2) AWS Batch 시작하기 - AWS Batch3) Amazon ECR의 도커 기본 사항 - Amazon ECR글윤석호 이사 | 브랜디 [email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 2743

PHP CI 환경에서 완전한 Vue 사용하기

편집자 주Vue 또는 VUE로 혼용하나 공식 사이트의 표기에 맞춰 아래와 같이 통일함-Vue-Vuex-Vue-Router목차1.Controller2.VIEW3.webpack Vue 소스 진입점4.webpack 설정5.Package.json6.Vue-Router7.Vuex8.공통 처리 mixin9.요약10.마치며시작하며드디어 브랜디 관리자 서비스에 Vue를 도입하고자 떠났던 여정의 마지막 장입니다. 브랜디 관리자 서비스는 PHP Codeigniter와 jQuery로 구성되어 있습니다. 사실 잘 운영되고 있는 서비스에 리스크가 큰 신기술을 도입하는 것은 도박에 가깝습니다. 몇 시간만 운영이 정지되어도 회사에 엄청난 피해를 안겨줄 수도 있으니까요. 하지만 여러 번의 검증과 실험으로 도박에서 이길 확률을 100%에 가깝게 끌어올린다면 한번 도전해볼 만하지 않을까요?이전 글인 PHP Codeigniter 환경에서 VUE 사용해보기에서 기본적인 webpack + Vue + Codeigniter 환경 구축 방법을 알아봤는데요. 하지만 단순히 webpack과 Vue만 적용했다고 해서 “우리 시스템은 UI 프레임워크로 Vue를 사용하고 있습니다.”라고 말할 순 없습니다. 아주 중요한 숙제가 남았죠.Vue에는 활용도를 대폭 끌어올려주는 Vue-Router와 Vuex Store1)가 있는데 그중 Vue-Router를 이번 글에서 자세히 다루려고 합니다.2) Vue-Router는 Vue.js의 공식 라우터입니다. 공식 홈페이지의 소개는 아래와 같습니다.중첩된 라우트/뷰 매핑모듈화된, 컴포넌트 기반의 라우터 설정라우터 파라미터, 쿼리, 와일드카드Vue.js의 트랜지션 시스템을 이용한 트랜지션 효과세밀한 내비게이션 컨트롤active CSS 클래스를 자동으로 추가해주는 링크HTML5 히스토리 모드 또는 해시 모드(IE9에서 자동으로 폴백)사용자 정의 가능한 스크롤 동작한마디로 정리하면 입력된 URL에 반응해 부분에 해당 URL의 view를 보여주는 기능인 것입니다. 다시 말해 URL이 변경될 때 한 페이지에서 화면 전체를 갈아끼우거나, 화면의 일부분(부분)을 치환해주는 역할을 한다는 것이죠. 더 나아가 해당 화면이 로드되기 전후로 전처리, 후처리 기능까지 가능합니다.착안점Vue와 Vue-Rotuer를 알게 되었을 땐 PHP 기반 프로젝트에 Vue-Router를 적용할 수 없으니 처음부터 새로 만들어야 한다고 생각했습니다. 로그인 인증 문제, 메뉴의 권한 관리 등 모든 것이 Vue 아래에 있어야 한다고 생각했기 때문입니다.어느 날 관리자 서비스에 TDD를 구현해보려고 Python Flask + webpack + Vue 프로젝트를 구성하고 있었습니다. 그러던 중 우연히 Flask + Vue-Router에서 404 페이지를 처리하려면 Flask Fallback 페이지를 Vue-Router 메인 페이지(가 있는 페이지)로 보내고, Vue-Router에서 진짜 매핑된 URL이 없으면 404 처리를 하는 식으로 구성한다는 글을 읽고 문득 호기심이 생겼습니다.‘관리자 서비스에서도 컨트롤러로 여러 URL을 한 가지 페이지로 보낸다면?’PHP를 거쳐 페이지로 이동한 것이므로 권한 관리와 메뉴 트리까지는 PHP에서 처리되면서 URL이 변할 것이고, 실제로 화면을 보여주는 Contents 영역만 를 사용한다면 어떻게 될지 궁금해졌습니다. 바로 하던 일을 멈추고 관리자 소스에 Vue-Router를 활용한 테스트 소스코드를 작성해봤습니다.예상했던 대로 PHP의 로그인 인증 처리를 거치면서 실제로 보이는 부분에는 부분만 정상적으로 치환되었습니다. 이 간단한 실험을 바탕으로 통계 시스템의 일부를 구현하는 데에 Vue-Router와 Vuex Store, 공통 처리 Mixin을 추가해 제작했습니다.1.Controller4개의 페이지를 가진 통계 시스템의 Codeigniter 컨트롤러 모습입니다. 기존의 서비스 URL들이 존재하기 때문에 Fallback을 통으로 Vue-Router로 보낼 순 없었고, 라우터를 사용할 페이지들을 하나의 페이지로 보냈습니다.1-1) /application/controllers/[컨트롤러 경로]... 생략 /* [라우터 view]에서 태그를 포함하고 있습니다. */ public function salesAnalysisProduct() { $this->load->view('[라우터 view]'); } public function salesAnalysisSeller() { $this->load->view('[라우터 view]'); } public function trendAnalysisProduct() { $this->load->view('[라우터 view]'); } public function trendAnalysisSeller() { $this->load->view('[라우터 view]'); } ... 생략 2.VIEWCodeigniter 환경에 반영하는 것이므로 CI에서 인식시킬 PHP view와 webpack에서 빌드 결과를 자동으로 바인딩할 html 파일로 구성됩니다.2-1)/application/views/[Vue용 view 경로]/index.php// [index.php] Vue 를 매핑할 php파일 컨트롤러의 view로딩용 [라우터 view]입니다. ... header, menu 생략 ... //바인딩될 부분 //자동매핑 html 인클루드 <?php include('index.html'); ?> ... footer 생략 ... 2-2)/application/views/[Vue용 view 경로]/index.html webpack의 빌드 결과로 자동으로 생성되는 파일입니다. [removed][removed] 위는 webpack의 HtmlWebpackPlugin에 의해 자동으로 바인딩된 모습입니다. 빌드되기 전 index.html은 다음 항목에 있습니다.3.webpack Vue 소스 진입점관리자에서는 프로젝트 폴더 안에 webpack과 Vue 용 서브 폴더를 두고 webpack.config.js에서 output 옵션을 통해 빌드 결과를 삽입하는 구조입니다. webpack 루트 폴더는 application 폴더와 같은 레벨에 위치하며, 폴더 구조나 파일 위치는 어디에 둬도 상관없습니다. webpack.config.js에서 entry 속성으로 잡아주시면 됩니다.3-1)/[webpack루트]/index.html// HtmlWebpackPlugin으로 스크립트를 삽입하기 위한 빈 템플릿 파일 3-2)/[webpack루트]/index.js/** * 진입용 index.js */ import Vue from 'vue' import axios from 'axios' import router from './router' import App from './App.vue' Vue.prototype.$http = axios new Vue({ el : '#app', router, components : { App }, template : '' }); 3-3)/[webpack루트]/App.vue [removed] import mixin from './common/common-mixin.js' import store from './vuex/store' export default { store, name : 'App' } [removed] Vuex와 통신 모듈 axios, Vue-Router 등을 루트 Vue 객체에 추가해줍니다. 브랜디 관리자의 webpack은 babel을 사용하고 있기 때문에 위의 store처럼 축약해서 작성하면 빌드된 파일에는 store: store와 같이 입력됩니다.Vue-Router는 태그에 자동으로 매핑되며, 위와 같은 구조로 상위 컴포넌트에서 할당되어 있어야 합니다. Vuex와 Vue-Router 설정은 글 아래에서 다루겠습니다.4.Webpack 설정이번에 Vue-Router와 Vuex를 도입하면서 webpack의 설정도 실제 서비스용과 개발용으로 분리했습니다. 폴더는 편의상 추가하였으며, package.json에서 자신이 설정한 경로로 설정하면 됩니다.Webpack 설정 파일은 Webpack의 시작과 끝이라고도 할 수 있습니다. Webpack 설정 파일에서 빌드할 소스의 경로와 빌드 결과 파일의 명명 규칙을 정하고, html 파일에 스크립트파를 자동으로 주입시키거나, Babel 플러그인을 통해 최신 스크립트 작성법을 브라우저를 신경쓰지 않고 사용할 수도 있습니다.그중에서도 중요한 옵션이 있는데 바로 Code Splitting에 관련된 옵션입니다.관리자 초기 Vue 모델에는 Vue-Router가 없었기 때문에 js 번들 파일의 크기가 그렇게 크지 않았습니다. 하지만 Vue-Router를 사용해 싱글 페이지 어플리케이션이 되거나 화면의 UI가 복잡해 컴포넌트 수가 많아지면 번들 js 파일의 크기가 매우 커집니다. 즉, 캐시를 사용하지 않는 익스플로러라면 소스에서 한 글자만 바뀌더라도 모든 페이지에서 거대한 번들 js를 새로 로딩하게 되고, 상당한 서버 자원을 소모합니다.Code Splitting 적용 전위의 이미지는 Code Splitting을 적용하기 전의 번들 js 정보입니다. 실제로 완성된 Vue 프로젝트의 번들 js는 더욱 큽니다. 정말 단순한 페이지 하나를 띄우는데 매번 뚱뚱한 js를 로딩해야 하는 것은 서비스 제공자와 서비스 사용자를 모두 괴롭게 할 것입니다.Code Splitting 적용 후하지만 위처럼 작은 조각으로 나눠 필요한 시점마다 필요한 번들 js만 로드하면 매우 빠른 페이지를 제작할 수 있습니다. 따라서 Code Split 기능은 매우 중요한 이슈입니다.물론 개발을 진행하다 보면 역시 어느 것 하나 쉽게 넘어가지지 않습니다. 관리자의 웹팩은 4.x 버전대를 사용하고 있습니다. 예전에 TF에서는 Webpakc 3.x 버전대를 사용하였는데 당시에는 CommonChunkPlugin 설정을 통해 Code Splitting을 사용할 수 있었습니다. 그대로 관리자에 적용하려 했는데..Removed라고 쓰여 있습니다. 찾아보니 CommonChunkPlugin이 옵티마이즈 옵션 하위의 splitChunk 속성으로 들어가면서 설정 방법이 바뀌었더군요. 머리를 싸매고 설정을 잡습니다.4-1) /[webpack루트]/build/webpakc.config.js : 공통 설정파일'use strict' const HtmlWebpackPlugin = require('html-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader'); const path = require('path'); module.exports = { entry: { //string, object, array 가능 - 기본은 ./src app: path.join('[스크립트 파일 경로]', 'index.js') //진입점 스크립트 파일입니다. }, output: { path: '[빌드된 js 목적지 경로]', publicPath: '[이미지등의 웹상 리소스 경로]', filename: './[name].[chunkhash].js', // 엔트리 파일명명규칙 chunkFilename: '[id]_[chunkhash].js', // chunk파일 명명 규칙 // --mode development에서는 [id]에도 chunkName들어갑니다. }, //vue와 js, css 로드 규칙을 설정합니다. module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', include: [ /[Vue 소스 경로]/ ] }, { test: /\.js$/, use: { loader: 'babel-loader?cacheDirectory', }, include: [ /[Vue 소스 경로]/ ] }, { test: /\.css$/, oneOf: [ { use: [ 'vue-style-loader', 'css-loader' ] } ] } ] }, resolve: { alias: { '@': '[Vue소스 경로]', // 편의상 소스단축경로를 설정합니다. }, //파일 확장자 자동인식 임포트시 해당 확장자는 생략가능합니다. extensions: ['.js', '.vue', '.json'], }, plugins: [ // Vue 파일 로더 new VueLoaderPlugin(), // html 자동 바인딩 // 아래의 플러그인으로 인해 index.html에 해시네임으로 빌드된 index.js가 자동으로 매핑됩니다. new HtmlWebpackPlugin({ // index.php에서 include할 파일이 생성될 경로와 파일명 입니다. filename: path.join('[View경로]', 'index.html'), // 자동으로 매핑할 진입점파일을 지정합니다. template: path.join('[Vue소스 경로]', 'index.html'), inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true } }), ], optimization: { //웹팩 4.x 버전에서 옵티마이즈 속성으로 추가된 CodeSplitting 기능입니다. splitChunks: { //initial - static파일만 분리, async - 동적로딩파일만 분리, all - 모두 분리 chunks: 'async', minSize: 30000, minChunks: 1, maxAsyncRequests: 5, //병렬 요청 chunk수 maxInitialRequests: 3, //초기 페이지로드 병렬 요청 chunk수 automaticNameDelimiter: '_', //vendor, default등 prefix 구분문자 (default : '~') name: true, //development모드일때 파일에 청크이름 표시여부 cacheGroups: { default: { minChunks: 2, //2개 이상의 chunk priority: -20, reuseExistingChunk: true //minChunks이상에서 사용할경우 공통사용 }, //axios, vue 같은 공통 모듈은 vendor로 관리합니다. vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 } } } } }; 4-2) /[webpack루트]/build/webpack.dev.config.js 개발용 설정 파일 (네이밍은 자유)'use strict' const merge = require('webpack-merge') const webpack = require('webpack') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const baseWebpackConfig = require('./webpack.config') const config = require('../config').dev //개발용설정 const devWebpackConfig = merge(baseWebpackConfig, { //mode는 chunk[id], 디버깅 코드 등에 영향을 줍니다. webpack 3.x 버전에서는 Env 속성을 통해 관리했다고 합니다. mode: 'development', plugins: [ new BundleAnalyzerPlugin(), //번들 무게 분석기 제대로 스플릿 되었는지 확인할 때 사용합니다. new webpack.DefinePlugin({ env : config.env }), ], watch: true, //코드의 변화를 감지해 자동으로 재빌드해주는 옵션입니다. cache: true, //캐시 사용을 활성화하면 변경사항이 있는 코드만 재빌드합니다. optimization: { //uglify 플러그인 코드 압축 여부를 설정합니다 압축 시 용량을 매우 줄일 수 있으나 빌드 속도가 크게 저하되므로 개발 시에는 꺼줍니다. minimize: false, } }) module.exports = new Promise((resolve, reject) => { resolve(devWebpackConfig); }) 4-3) /[webpack루트]/build/webpack.prod.config.js 서비스용 설정파일 (네이밍은 자유)'use strict' const merge = require('webpack-merge') // 설정파일 결합에 사용합니다. const webpack = require('webpack') const baseWebpackConfig = require('./webpack.config') //베이스 설정파일 const config = require('../config').prod //서비스용 설정 const prodWebpackConfig = merge(baseWebpackConfig, { mode: 'production', //chunk[id], 디버깅 코드등 영향 있음 plugins: [ new webpack.DefinePlugin({ env : config.env }), ], //개발용과 반대로 용량은 줄이고 필요 없는 기능은 꺼줍니다. watch: false, cache: false, optimization: { minimize: true, } }) module.exports = prodWebpackConfig 5.package.json웹팩 설정 파일이 분리되면서 package.json의 런 스크립트도 변경했습니다.... "scripts": { "build": "webpack --config build/webpack.prod.config.js --progress", "build-dev": "webpack --config build/webpack.dev.config.js --progress" }, ... 6.Vue-RouterVue-Router는 위에서 설명한 대로 Vue의 컴포넌트와 밀접하게 결합된 라우터입니다. 그런데 여기서 webpack의 Code Split을 사용하려면 컴포넌트 import 방법이 매우 중요한데요.import './testComponent' 위처럼 import 구문을 사용해 컴포넌트를 불러오면 코드가 쪼개지지 않고 한 덩어리로 빌드되므로 아래와 같은 형태로 사용해야 합니다.const testComponent = () => import('./testComponent') webpack 공식 문서에도 나와있듯이 위처럼 ES2015 Loader spec에 있는 import()를 사용하여 컴포넌트를 생성해야 번들 js가 제대로 분리되며, Dynamic Import가 가능해집니다.Vue-Router를 쓰는 순간 싱글 페이지 어플리케이션이 되기 때문에 이곳에서 설정을 잘못 잡아주는 순간 육중한 컴포넌트 한 덩어리가 튀어나오면서 Code Splitting은 물거품이 되어버립니다. 조심합시다!또한 import 함수 안쪽엔 아래와 같은 주석을 달아야 청크 이름이 적용됩니다.const testComponent = () => import( /* webpackChunkName: '[청크이름]'*/ './testComponent') 라우터 경로 속성인 path 와 Codeigniter의 컨트롤러 경로를 맞춰주는 것이 포인트입니다!6-1) /[webpack루트]/router/index.js - 경로와 파일명은 자유입니다!import Vue from 'vue' import Router from 'vue-router' // 주석의 webpackChunkName = 코드 스플릿 chunk Name으로 사용됩니다. // 꼭 컴포넌트와 청크 이름을 같게 설정할 필요는 없습니다. const SalesAnalysisProduct = () => import(/* webpackChunkName: "salesAnalysisProduct" */ '[컴포넌트 파일 경로]') const SalesAnalysisSeller = () => import(/* webpackChunkName: "salesAnalysisSeller" */ '[컴포넌트 파일 경로]') const TrendAnalysisProduct = () => import(/* webpackChunkName: "trendAnalysisProduct" */ '[컴포넌트 파일 경로]') const TrendAnalysisSeller = () => import(/* webpackChunkName: "trendAnalysisSeller" */ '[컴포넌트 파일 경로]') Vue.use(Router) const router = new Router({ mode: 'history', routes: [ /* 통계 */ { path: '[CI컨트롤러 url]/salesAnalysisProduct', name: 'salesAnalysisProduct', component: SalesAnalysisProduct }, { path: '[CI컨트롤러 url]/salesAnalysisSeller', name: 'salesAnalysisSeller', component: SalesAnalysisSeller }, { path: '[CI컨트롤러 url]/trendAnalysisProduct', name: 'trendAnalysisProduct', component: TrendAnalysisProduct }, { path: '[CI컨트롤러 url]/trendAnalysisSeller', name: 'trendAnalysisSeller', component: TrendAnalysisSeller }, ] }) // 아래의 함수로 전처리 후처리도 가능합니다! router.beforeEach((to, from, next) => { // ... }) router.afterEach((to, from) => { // ... }) export default router 7.Vuex앞서 Vue와 Vuex, 컴포넌트간 통신과 상태 관리에서 소개했던 상태 관리와 통신을 위한 Vuex도 추가합니다. Vuex는 하나의 Store만 쓸 경우 상태 변수의 과포화로 인해 유지 보수가 어려워질 수 있으므로 namespace: true 옵션을 통해 도메인별로 관리합니다.7-1) /[webpack루트]/vuex/store.js - Vuex 진입파일import Vue from 'vue' import Vuex from 'vuex' // 각 도메인별 store들이 들어있는 modules 를 임포트해줍니다. import * as modules from './modules/index' Vue.use(Vuex) export default new Vuex.Store({ state : { }, getter: { }, mutations : { }, actions : { }, modules : modules.default }) 7-2) /[webpack루트]/vuex/modules/index.js - 도메인별 Store 자동 바인딩 스크립트const files = require.context('.', false, /\.js$/) const modules = {} //자신(index.js)를 제외한 파일들을 파일이름을 Key로 modules에 담습니다. files.keys().forEach((key) => { if (key === './index.js') return modules[key.toLowerCase().replace(/(\.\/|\.js)/g, '')] = files(key).default }) export default modules 7-3) /[webpack루트]/vuex/modules/statistics.js(통계 store 파일) - 예시입니다.export default { namespaced : true, //해당 속성을 통해 파일명을 namespace로 사용합니다. state: { /* 상태값 및 데이터 */ ... }, getters: { }, mutations: { /* state 변경처리 */ ... }, actions: { /* 통신처리 */ ... } } namespace: true로 되어있으므로 파일명인 statistics를 namespace로 사용하게 됩니다. 따라서 store 각 항목에 대한 접근은 다음과 같이 이뤄지며 computed 속성에 state: this.$store.state.statistics 처럼 정의해두면 편리합니다.dispatch는 this.$store.dispatch(‘statistics/[action 이름]’)commit은 this.$store.commit(‘statistics/[mutation 이름]’)state 변수 접근은 this.$store.state.statistics.[state 이름]8.공통 처리 mixinapi 통신에 사용되는 통신 라이브러리와 그 라이브러리의 복잡한 설정 코드, 단순한 Toast 출력 함수, 로딩 이펙트를 보여주는 함수 등 모든 항목들이 매 페이지마다 있으면, 통일되지 못한 UI, 페이지마다 일관되지 못한 설정 등으로 휴먼 에러가 발생할 확률이 높아집니다. 유지 보수 측면에서도 비용이 높아집니다. 이러한 단순 반복 코드들은 한번만 정의하고 재사용하는 것이 바람직합니다. 나중에 수정할 때도 용이하죠.공통사항을 묶어 Vue 전역 믹스인으로 Vue 루트 객체에 추가합시다. 단, global 옵션인 만큼 조심해서 써야 합니다. 시스템에 영향을 줄 것 같으면 하위 컴포넌트 mixins 속성에 넣어 해당 스코프에서만 사용하는 것이 바람직합니다.8-1) /[webpack루트]/common/common-mixin.js (파일이름, 경로는 자유입니다!)import Vue from 'vue' import Vue from 'axios' import Cookies from 'js-cookie' const TIMEOUT = '[타임아웃 시간(ms)]' /* mixin의 기본 형태는 Vue 컴포넌트의 형태와 동일합니다. 주로 전역 통신과 상태 관리는 vuex store에서, 전역 data 속성과 전역 함수는 mixin에서 관리합니다. */ Vue.mixin({ /* 전역 사용 data속성 선언 */ data: () => { return { ... //이곳에 선언하는 data 속성은 전역에서 this로 접근 가능합니다. } }, created: function() { // 공용 axios 객체 생성 this.axios = axios.create({ timeout: TIMEOUT, withCredentials: true, //공통해더는 여기에 headers : { } }); //axios 의 success와 error를 mixin method에서 처리 하도록 등록 this.axios.interceptors.response.use(this.onSuccess, this.onError) }, /* 전역 사용 함수 선언 */ methods: { /* axios의 response handling 함수*/ onSuccess : response => { }, onError : function (error) { }, /*GET, POST 등의 통신 함수, Toast(alert) 표출함수, 에러핸들링함수 등 선언*/ /*... 내용이 너무 길어서 생략 ...*/ } }); 9.요약지금까지의 내용은 파일 경로를 토대로 요약하면 다음과 같습니다. 참고로 아래의 폴더 구조는 절대적인것은 아닙니다. 모든 폴더 구조는 자율이며, 폴더 구조에 맞게 webpack.config.js에서 조정해주면 됩니다.[프로젝트 루트] └ [웹팩 루트] └ package.json └ [Vue 소스 루트] └ [common] └ [router] └ index.js // 라우터 설정파일 - CI 컨트롤러와 url 맞춰줘야함 └ [vuex] └ index.js // 도메인별 store module export 스크립트 └ [modules] └ 도메인별 store.js └ [컴포넌트 폴더] //예시에서는 ststistics └ App.vue //진입점 vue파일 Vuex와 전역 mixin 세팅 └ index.html //index.js가 주입될 껍데기 └ index.js //진입점 js Vue-Router와 App.vue 세팅 └ [build] // 빌드파일경로 └ webpack.config.js //베이스 설정파일 └ webpack.dev.config.js //개발용 설정파일 └ webpack.prod.config.js //서비스용 설정파일 └ [application] //Codeigniter 루트 └ [controllers] └ [컨트롤러 경로] // 예시의 통계부분 └ [views] └ [웹팩빌드 결과 폴더] └ [index.php] // CI 에서 로드하는 view (index.html include) └ [index.html] // js 번들이 자동 주입된 빌드결과 파일 └ [include] └ [scripts] └ [빌드결과 js 경로] //public path 속성 경로 └ 빌드 결과 js chunk들 마치며관리자 서비스에서 완전한 Vue를 사용하기 위해 꽤 험난한 과정을 거쳤습니다. 지금도 잘 돌아가는 서비스에 리스크를 감수하면서도 새로운 것을 도입하려는 이유를 찾아야 했고, 한동안은 레거시와 Vue로 된 소스를 2중으로 개발해야 했습니다.게다가 이 글을 작성하기 시작했을 땐 Code Splitting 설정 방법이 바뀌어 적용하지도 못한 상황이었기 때문에 사실 Code Splitting 내용이 없었습니다. 그런데 글을 작성하면서 splitChunk옵션을 성공해버렸어요! 덕분에 이 글도 모두 수정해야 했죠. Vue의 도입을 고려하는 개발자분들에게 도움이 되길 바라는 마음으로 글을 마칩니다.참고1)Vuex Store는 Vue와 Vuex, 컴포넌트간 통신과 상태 관리에 자세히 정리해두었다.2) 브랜디 관리자 서비스는 jQuery로 작성되어 있다. 따라서 jQuery를 베재할 수만은 없는 상황이었다. 이에 따라 기존 jQuery 컴포넌트들에 대한 해결책은 천보성 팀장님이 작성한 JQuery 프로젝트에 VUE를 점진적으로 도입하기를 참고했다. props와 emit 기능을 이용해 jQuery로 제작한 컴포넌트를 깔끔하게 Wrapping 하는 방법에 대해 자세히 기술되어 있으며, 이를 활용하면 레거시 UI 플러그인을 마치 네이티브 Vue 플러그인처럼 사용할 수 있다.글강원우 과장 | R&D 개발2팀[email protected]브랜디, 오직 예쁜 옷만
조회수 1895

빠른 프로토타이핑을 위한 도구 소개

새로운 아이디어를 검증하는 방법은 여러 가지가 있습니다. 비슷한 도구들을 사용하면서 간접체험을 해보기도 하고, 종이, 혹은 목업 도구를 활용한 프로토타입을 만들어보기도 합니다.스포카 팀은 새로운 아이디어를 검증하기 위해 더욱 직접적인 프로토타입을 만들어야 했습니다. 매장에서의 오프라인 경험에서부터 Facebook, Twitter 등의 온라인 경험까지 이어지는 총체적인 경험 선을 시험하기 위해선 실제로 어느 정도 동작하는 프로토타입을 만드는 것이 제일 확실하였기 때문이죠.하지만 막상 그럴싸하게 동작하는 프로토타입을 만드는 것은 생각보다 시간이 오래 걸리는 일입니다. 최소의 UI 디자인, 빠른 기능 개발, 배포 환경이 제대로 준비되어있지 않다면 실제로 유효한 수준까지 만드는 것이 많은 시간이 필요하게 될 것입니다.스포카 팀은 아이디어가 떠오를 때 어떻게 하면 그것을 빠르게 구현해서 확인할 수 있을지에 대해 많이 고민하였습니다. 그러면서도 해당 아이디어가 좋을 때 제대로 된 서비스로 확장하거나 기존 기능에 통합하는 것도 수월하게 가능하다면 더 좋겠지요. 이번 글에서는 제대로 작동하면서 확장성도 고려한 고 수준의 프로토타이핑을 빠르게 할 수 있게 도와주는 도구들을 모두 소개해보고자 합니다.어떤 언어를 고를까?특별히 교육의 목적이 있는 것이 아니라면 언어는 자신이 가장 잘 활용할 수 있게 미리 교육된 언어가 효과적입니다. 새로운 언어를 공부하면서 프로토타이핑을 한다면 지엽적이고 모르는 문제에 부딪혀 시간을 허비하는 상황이 많아 프로토타이핑 속도가 지연되기 쉽기 때문입니다. 다만 컴파일 가능한 언어와 불가능한 언어 중 선택해야 한다면 대부분 컴파일 과정이 필요없는 언어를 선택하는 것이 큰 효과를 경험하실 수 있습니다.스포카 팀은 서버 개발에 Python을 주 언어로 활용하며, 그 외에 Ruby나 Node.js 같은 언어도 추천합니다.마이크로 프레임워크를 활용하자규모가 커지면 구조에 손을 대야 하지만, 다양한 기능을 빨리 구현해서 넣고자 할 때 마이크로 프레임워크로 시작하는 것이 좋습니다. 간편하면서도 초기 구조를 아주 간결하게 들고 갈 수 있기 때문입니다.웹 서비스나 앱 서비스의 서버로 이용할 HTTP 프로토콜 서버를 구축한다면 Sinatra 스타일의 마이크로 프레임워크를 활용하는 것이 효과적입니다.아래는 주요 언어에서 볼 수 있는 마이크로 프레임워크입니다. 이 외에도 Sinatra style microframework을 검색해보시면 여러 언어에서 비슷한 형태로 구현된 마이크로 프레임워크를 보실 수 있습니다.Sinatra (Ruby)Flask (Python)Express (Node.js)스포카팀에서는 Flask를 즐겨쓰고 있습니다. Flask에 관심이 있으시다면 지난 기술 블로그의 소개글을 참조해주세요.디자인을 빠르게 하는 툴킷들기본적인 기능들을 빠르게 구현하였다면 이를 활용할 사용자 인터페이스를 만들어야 합니다. 하지만 웹 서비스나 웹뷰를 기반으로 하는 서비스를 만든다면 HTML/CSS/JS 기반의 디자인을 하는 일도 상당히 시간이 많이 필요한 일입니다. 이 때, 각 목적에 맞는 툴킷들을 이용한다면 디자인을 크게 고민하지 않으면서도 보기 좋은 서비스를 만들어 볼 수 있습니다.Bootstrap from Twitter는 디자인에 대한 여러 가지 기초적인 고민을 상당히 잘 흡수해주는 훌륭한 툴킷입니다. 크로스 브라우징을 지원하며, 우리가 쓰는 컴포넌트 대부분에 대해 심미적으로, 기능적으로 우수한 디자인을 제공합니다. 그리드 인터페이스를 제공해서 레이아웃도 간편하게 잡을 수 있으며, 곧 출시 예정인 2.0에선 반응형 디자인도 정식으로 지원하고 있습니다.Bootstrap은 LESS로도 제공해주기 때문에, 디자인 튜닝이 간편하고 Mixin을 활용해 의미적인 HTML 마크업을 하면서 디자인을 적용할 수도 있습니다.위의 툴킷과 같은 인터페이스를 가지고 디자인만 Facebook 형태로 바꾼 Fbootstrapp도 있습니다. Facebook 앱을 만든다면 이쪽을 쓰시는 편이 더 좋을 것 같습니다.터치 환경에 한정한 서비스를 디자인 중이라면 범용성이 조금 떨어지지만 jQuery Mobile을 추천합니다. 여러 기기의 웹뷰 환경을 지원하는 다양한 컴포넌트를 제공하고 있습니다.서비스를 최대한 쓰기모든 기능을 직접 전부 구현할 필요는 없습니다. 여러 회사에서 한 두 줄의 추가만으로 사용할 수 있는 서비스를 제공하고 있습니다. Google은 특히 Maps API, Chart Tools, QR Code, Font API 등 개발에 도움이 되는 수많은 기능들을 간단한 API로 쓸 수 있게끔 공개하고 있으며, Facebook 또한 소셜 플러그인으로 다양한 소셜 도구들(Like Button, Comments, Registration 등)을 제공하고 있습니다. 이런 서비스들을 잘 알고 있다면 가끔은 단지 여러 서비스 기능을 연결하는 것만으로 새로운 서비스를 만들 수 있기도 합니다.서비스 배포는 Platform as a Service(PaaS)를 활용하자위 도구의 협력으로 서비스를 만들었다면 이제 배포를 해야 합니다. 어디서나 접근할 수 있는 공용 서버에 서비스를 올리고, 서버를 세팅하고, 도메인을 연결해야 합니다. 이 과정들 또한 시간을 많이 필요로 하는 일들입니다.최근 Heroku를 시작으로 미국에서 Amazon Web Service를 기반으로 한 많은 Platform as a Service가 출시되고 있습니다. 이 서비스들은 대체로 Failover System, 쉬운 서비스 규모 스케일링, 잘 설계된 서버 스택, 편리한 배포환경을 강점으로 내세우고 있으며, 특히 처음 사용자가 가입부터 서비스 배포까지 아주 간편하고 빠른 속도로 진행할 수 있게끔 도구를 제공하고 있습니다. 게다가, 대부분 무료 플랜이 존재하기 때문에 비용 부담이 없다는 장점도 가지고 있습니다.Heroku의 서비스 배포 과정을 보시면 그 과정이 얼마나 편리한지 쉽게 알 수 있습니다.$ heroku createCreated sushi.herokuapp.com | [email protected]:sushi.git$ git push heroku master-----> Heroku receiving push-----> Rails app detected-----> Compiled slug size is 8.0MB-----> Launching... done, v1http://sushi.herokuapp.com deployed to Herokuview rawgistfile1.sh hosted with ❤ by GitHub단 두 줄로 git에 의해 관리되는 애플리케이션을 서버에 배포하고 접근 URL을 받았습니다.아래는 다양한 플랫폼에서 쉽게 이용 가능한 PaaS 목록입니다.저장소 이용아무리 빠르게 하고 싶다고 해도 저장소는 두고 하세요. 개인이 작업하는 것이라면 로컬에서도 저장소 관리가 가능한 분산형 버전관리 시스템 (git, mercurial)로 바로 이용하시고, 2명 이상이 동시에 작업한다면 반드시 저장소 호스팅 서비스를 이용해서 작업하시기 바랍니다. 변경사항을 공유하는 방법에 대해 버전관리 시스템보다 빠르고 깔끔한 방법은 아직까진 없기 때문입니다.저장소 호스팅은 많은 곳에서 제공해주고 있지만, 돈을 조금 투자해서 Github를 쓰시는 것을 추천해 드립니다. 저장소뿐만이 아닌 훌륭한 협업 플랫폼을 제공해주고 있기 때문입니다. 당장은 무료로 시작해야 한다면 Bitbucket의 무료 비공개 저장소를 이용하는 것도 좋은 방법니다.실제 케이스아래는 최근 사내에서 이루어진 아이디어 서비스 프로토타이핑이 이루어진 과정을 나열해보았습니다.Github에 저장소 생성. 팀원들에게 전달한 명은 Flask로 서버 사이드 개발QR코드 생성이 필요한 부분을 Google API로 해결한 명은 Bootstrap from Twitter로 뷰 작업을 진행작업이 되는대로 Github, Heroku에 배포개발에 필요한 시간은 약 5시간 정도였으며, 사실 이 기간은 그 이전에 해당 아이디어의 가치에 대해 토론하는 데 쓴 시간과 비슷한 시간이었습니다. 토론에선 답이 나오지 않은 채로 끝났지만, 프로토타입을 이용해보고 답을 내는 것은 그리 오랜 시간이 걸리지 않았습니다.마치며실용 가능한 프로토타이핑은 앱의 첫인상과 인터페이스 전반에 대한 이해를 넘어 아이디어의 가치평가를 확신할 수 있는 좋은 방법입니다. 우리가 토론에서 의견이 많이 갈리는 이유는 사실 보지 못한 것에 관해 이야기하기 때문인 경우가 많아서, 만약 토론하는 시간보다 더 짧은 시간 안에 말하는 것을 볼 수 있다면 의사 결정을 더 빠르고 정확하게 할 수 있습니다. 이 글은 그 방법에 대해 구체적으로 설명하였습니다.이번에 소개한 도구와 방법은 단지 돌아가는 것을 확인하는 것을 넘어 장기적인 확장성도 갖추고 있습니다. 언급한 언어들 모두 대형 서비스에서 실제 이용 중인 언어들이며, 마이크로 프레임워크들도 모두 커지는 구조에 대한 대응법을 준비하고 있습니다. 디자인은 Bootstrap의 일부 코드를 재작성하거나 튜닝하는 것으로 서비스에 최적화시킬 수 있으며, PaaS는 애초에 Fast scaling이 주요 강점이기 때문에 손쉽게 커지는 서비스의 사용량에 유연하게 대처할 수 있습니다.새로운 아이디어를 준비하고 계신다면, 이 글에서 소개한 도구들을 십분 활용하여 빠르게 실용할 수 있고, 확장 가능한 프로토타입을 반복해서 만들어 보시는 것을 적극 추천해 드립니다.#스포카 #개발 #개발자 #꿀팁 #스킬스택 #스택소개 #조언
조회수 2700

React + Decorator + HOC = Fantastic!!

React + Decorator + HOC = Fantastic!!지난 포스팅에서는 ES7의 Decorator 문법을 이용해 선언된 클래스와 그 프로퍼티들을 디자인 시간에 변경하는 법을 알아보았습니다. 그렇다면 리액트 컴포넌트와 Decorator가 만나면 어떤 시너지가 발생할까요?만약 ES7의 Decorator에 대해 모르신다면 지난 포스팅을 읽고 오시는 걸 권장합니다. 이 포스팅은 독자들이 Decorator에 대해 이미 알고 있다고 가정하고 작성됐습니다.Higher Order Component리액트 공식 문서를 보면 Higher Order Component(이하 HOC)를 다음과 같이 설명하고 있습니다.리액트 컴포넌트 로직을 재활용할 수 있는 고급 기법리액트에서 공식적으로 제공하는 API가 아니라 단순히 아키텍쳐이 설명으로는 HOC가 어떤 역할을 하는지 이해하기는 역부족이기 때문에 간단한 예제를 통해 HOC를 어떻게 작성하는지 알아보겠습니다.function withSay(WrappedComponent) {     return class extends React.Component {     say() {       return 'hello'     } render() {       return (                   {...this.props}           say={this.say} />       )     }   } } withSay 함수는 WrappedComponent를 인자로 받아 원하는 속성들을 결합해 새로운 컴포넌트를 반환합니다. 이렇게 만들어진 withSay 함수는 아래와 같이 사용 가능합니다.@withSay class withOutSay extends React.Component {     render() {     return (               {this.props.say()}           )   } } withOutSay 컴포넌트는 say 메소드를 가지고 있지 않습니다. 하지만 withSay 함수를 사용하니 say 메소드를 사용할 수 있게 됐습니다. 이처럼 컴포넌트를 인자로 받아 입맛에 맞게 바꾼 뒤 새로운 컴포넌트로 반환하는 기법을 HOC라고 부릅니다.그렇다면 HOC는 리액트에서 어떻게 사용을 해야 효율적일까요?Cross Cutting Concerns개발을 하다 보면 다음과 같은 상황에 직면하는 경우가 종종 있습니다.개발 전반에 걸쳐 반복해서 등장하는 로직그럼에도 불구하고 모듈화가 쉽지 않은 로직예를 들어 방명록 작성, 게시글 작성, 게시글 스크랩을 하는 컴포넌트들에서 유저 인증과 에러 처리의 과정이 필요하다고 했을 때 어떻게 코드를 디자인해야 할까요? 컴포넌트와 직접적으로 연관이 없는 기능들이 컴포넌트와의 결합이 너무 강해 쉽게 모듈화를 시키지 못합니다.그림 1. Cross Cutting Concerns의 예시이렇듯 코드 디자인적인 측면에서 공통적으로 발생하지만 쉽게 분리를 시키지 못하는 문제를 Cross Cutting Concerns라고 합니다. 이 문제를 끌어안고 가면 프로젝트의 코드는 쉽게 스파게티가 되고 나중에는 유지 보수를 하기 힘들어집니다.하지만 우리게에는 HOC와 Decorator가 있고 이를 이용해 이 문제를 쉽게 해결할 수 있습니다.유저 인증 문제를 HOC로 해결아래는 인증이 안된 유저에게 다른 페이지를 보여주는 코드입니다.class TeamChat extends React.Component {     constructor() {     super()     this.state = {       unAuthenticated: false     }   } componentWillMount() {     if (!this.props.user) {       this.setState({ unAuthenticated: true })     }   } render() {     if (this.state.unAuthenticated) {       return     }     return I'm TeamChat   } } 유저 인증을 전통적인 if-else 구문으로 구현했습니다. 당장 이 컴포넌트를 본다면 문제가 없어 보입니다. 어떻게 보면 정답처럼 보이기도 합니다. 하지만 유저 인증이 필요한 컴포넌트가 많아지면 상황이 달라집니다.100개의 컴포넌트에서 위와 같은 방식으로 유저 인증을 하고 있는데 유저 인증을 하는 로직이 변경된 상황을 생각해 봅시다. 100개의 컴포넌트 모두 유저 인증 코드를 바꿔야 하는 상황에 직면하게 됩니다. 전부 다 바꾸는 것도 일이지만 실수로 몇 개의 컴포넌트를 수정하지 않을 확률이 농후합니다. 당장에는 간단하지만 잠재적 위험을 안고 있는 위 코드는 아래와 같이 수정되어야 합니다.function mustToAuthenticated(WrappedComponent) {     return class extends React.Component {     constructor() {       super()       this.state = {         unAuthenticated: false       }      } componentWillMount() {       if (!this.props.user) {         this.setState({ unAuthenticated: true })       }     } render() {       if (this.state.unAuthenticated) {         return       }       return     }    } } HOC를 이용해 확장이 용이한 유저 인증 로직이 탄생했습니다!! 이렇게 만들어진 HOC는 아래와 같이 적용이 가능합니다.@mustToAuthenticated class TeamChat extends React.Component {     render() {     return I'm TeamChat   } } @mustToAuthenticated class UserChat extends React.Component {     render() {     return I'm UserChat   } } 기존의 코드와 비교했을 때 코드가 훨씬 간단해진 것을 확인할 수 있습니다. 비단 코드만 간단해진 것뿐만 아니라 아래와 같은 추가 효과를 기대할 수 있습니다.유저 인증 로직이 컴포넌트와 분리가 되어 자신이 맡은 역할에만 집중할 수 있습니다.유저 인증 로직이 바뀌어도 코드를 수정해야 할 곳은 하나의 컴포넌트뿐입니다.예시로 작성한 HOC는 최소한의 코드로만 작성된 예시입니다. 실제 제품에서 사용되기 위해서는 몇 가지 고려해야 할 사항이 있는데 이는 리액트 공식 문서를 참고해주세요.i18n 컴포넌트를 HOC로 작성채널 서비스는 한국어, 영어, 일본어를 지원하기 때문에 번역 기능이 필요했습니다. 초기에는 번역 서비스를 아래와 같이 구현했습니다.@connect(state => ({   locale: getLocale(state) }) class Channel extends React.Component {     render() {     const local = this.props.locale     const translate = TranslateService.get(locale)     return (               {translate.title}         {translate.description}           )   } } 처음에는 위와 같은 방식으로 번역 서비스를 구현하는 것이 괜찮았습니다. 하지만 번역을 제공해야 하는 컴포넌트가 많아지면 많아질수록 중복되는 코드가 많아지는 것을 보고 아래과 같이 HOC를 이용해 코드의 중복을 제거했습니다.function withTranslate(WrappedComponent) { @connect(state => ({     locale: getLocale(state)   }))   class DecoratedComponent extends React.Component {     render() {       const locale = this.props.locale       const translate = TranslateService.get(locale) return (                   {...this.props}           translate={translate} />       )    }   } } 이렇게 작성된 HOC는 아래와 같이 사용이 가능합니다.@withTranslate class Channel extends React.Component {     render() {     const translate = this.props.translate     return (               {translate.title}         {translate.description}           )   } } HOC의 작성 방법은 예시로 작성한 두 개의 HOC에서 크게 벗어나지 않습니다. 이를 응용해 자신의 프로젝트에 맞는 코드를 작성해보세요.중첩 가능한 HOCHOC는 여러 개를 중첩해서 사용할 수 있습니다.. 예를 들어 유저 인증과 i18n 서비스를 동시에 제공하고 싶을 때 두 HOC를 중첩해서 사용하면 됩니다.@mustToAuthenticated @withTranslate class Channel extends React.Component {     render() {     return (               {`Hello!! ${this.props.user.name}`         {translate.title}         {translate.description}           )   } } 마무리이상으로 리액트에서 HOC를 사용할 수 있는 상황과 작성 방법을 알아보았습니다. 본 포스팅에서 다루지는 않았지만 만능처럼 소개한 HOC에도 몇 가지 단점은 존재합니다.Component Unit Test를 할 때 문제가 있을 수 있습니다.HOC를 몇 개 중첩하면 디버깅이 힘들 수 있습니다.WrappedComponent에 직접적으로 ref를 달 수 없어 우회 방법을 사용해야 합니다.비동기 작업과 같이 사용하다 보면 예상치 못한 결과를 만날 수 있습니다.하지만 이러한 단점에도 불구하고 상속을 제공하지 않은 리액트에서 HOC는 많은 문제를 효율적으로 해결해주는 단비와 같은 존재입니다. 유명한 리액트 라이브러리들(react-redux, redux-form 등)은 이미 예전부터 HOC를 사용해 사용자들에게 편의를 제공해 왔습니다. 이러한 라이브러리들과 자신의 프로젝트가 직면하고 있는 문제에 맞는 HOC를 작성해 같이 사용한다면 우아하고 아름다운 설계에 한층 더 다가간 프로젝트를 발견할 수 있습니다.마지막으로 한 문장을 남기고 본 포스팅을 마치도록 하겠습니다.React + Decorator + HOC = Fantastic!!본 포스팅은 2017 리액트 서울에서 발표한 내용입니다. 발표 자료와 발표 영상을 확인해보세요.#조이코퍼레이션 #개발자 #개발팀 #인사이트 #경험공유 #일지
조회수 1268

EOS Token 생성과 발행, 전송

이번시간에는 배포한 Contract를 통해 Token 발행과 전송을 해보겠습니다. 이를 위한 준비는 아래 2미디엄 글을 참조해주세요EOS Smart Contract 를 위한 준비EOS Smart Contract 배포먼저 저번 시간에 배포한 token 발행 abi 를 확인해 보겠습니다.$ cleos get abi hexlanthenryget abiabi를 확인하다보면 actions 라는 항목에 총 3개의 action이 있음을 확인할 수 있습니다. 이 3개의 name이 실행할 수 있는 action입니다. token발행은 create action을 통해 진행할 수 있습니다.Token 생성$ cleos push action hexlanthenry create '["hexlanthenry", "10000000000.0000 HEX"]' -p hexlanthenrycreate action 실행 결과create action 을 통해 ‘HEX’ 토큰을 100억개 생성했습니다. create 라는 action의 인자는 account_name(hexlanthenry), maximum_supply(10000000000.0000 HEX) 입니다. 즉 첫번째 인자는 토큰의 발행자를 나타내며, 두번째 인자는 토큰의 최대 수량을 나타냅니다.이 인자가 어떻게 들어가는지는 abi 의 struct 를 확인하면 알 수 있습니다.abi의 create structparameter 1 : account_name type— issuerparameter 2 : asset type — maximum_supply+ 저번 강의에서 공지한데로 다음 포스팅에서는 abi가 무엇을 뜻하는지, 이를 통해 어떻게 action을 실행할 수 있는지 알아보도록 하겠습니다.Token 발행생성과 발행 이 2개의 개념이 헷갈릴 수 있습니다. create action을 통한 생성은 최대 발행량을 결정 하는 것이며, issue action 은 토큰을 유통 시키는 것입니다.create : token 생성과 동시에 최대 발행량 결정issue : token 의 유통따라서 issue action을 통해 이전에 생성한 HEX token을 발행해보겠습니다.$ cleos push action hexlanthenry issue '["hexlanthenry", "10000.0000 HEX", "initial issue"]' -p hexlanthenryissue contract 실행 결과issue action 역시 data로 어떤 인자가 들어가는지는 abi를 통해 확인 가능합니다.abi의 issue structparameter 1 : account_name type — toparameter 2 : asset type — quantityparameter 3 : string type — memomemo 는 transfer 가 어떤 목적인지에 대해 설명해주는 인자 입니다. 생략해도 되는 값으로, 원하시면 parameter 개수를 유지하는 선에서 empty string을 넣으시면 됩니다. memo를 어떻게 쓰면 유용한지에 대해서도 다른 포스팅에 담도록 하겠습니다.issue가 잘 실행 되었는지 확인해 보겠습니다.$ cleos get currency balance hexlanthenry hexlanthenry저는 issue 를 4번 수행한 후 balance 를 체크 했기 때문에 총 40000개의 HEX token이 존재하는 것을 확인 할 수 있습니다.hexlanthenry 의 HEX token개수예외사항1create 하지 않은 token을 issue 할 경우해당 symbol 이 존재하지 않음예외사항2생성한 token 수보다 많은 양을 issue 할 경우maximum supply를 초과함Token transfer마지막으로 token을 다른 계정에 전송 해보도록 하겠습니다. 다른계정에 token을 보내야 하기 때문에 계정을 생성하거나 존재하고 있는 계정을 사용하시면 됩니다.아래 명령으로 hexlanthenry 계정이 babylion1234 계정으로 10000개의 HEX 토큰을 보냅니다.$ cleos push action hexlanthenry transfer '["hexlanthenry", "babylion1234", "10000.0000 HEX", "first"]' -p hexlanthenrytransfer 실행결과transfer 시 들어가는 data에 대해서도 abi를 확인해보겠습니다. 다른 action보다 많은 인자를 필요로 합니다. [“hexlanthenry”, “babylion1234”, “10000.0000 HEX”, “first”]abi의 transfer structparameter 1 : account_name type — fromparameter 2 : account_name type — toparameter 3 : asset type — quantityparameter 4 : string type — memo실제로 babylion1234 계정을 확인해 보면, 방금 배포한 HEX token을 보유하고있는 것을 확인할 수 있습니다.babylion1234의 HEX 보유이번 포스팅에서는 token을 생성과 발행 그리고 전송을 다뤄봤습니다. EOS는 Ethereum 과 달리 토큰 발행을 매우 쉽게 진행할 수 있습니다. 이 두 dapp의 차이에 대해서도 포스팅을 하고 싶으나 우선 다음 포스팅에서는 contract 개발의 기초를 다루도록 하겠습니다.감사합니다.#헥슬란트 #HEXLANT #블록체인 #개발자 #개발팀 #기술기업 #기술중심
조회수 10213

파이썬의 시간대에 대해 알아보기(datetime.timezone)

안녕하세요. 스포카 크리에이터 김두리입니다.  스포카는 많은 프로덕트에서 국제화 서비스를 제공하고 있습니다. 그래서 시간대와 시간을 제대로 정확하게 처리하는 것은 중요합니다. 하지만 파이썬의 datetime.datetime은 날짜(datetime.date)와 시각(datetime.time)의 정보를 담고 있고, 시간대(datetime.timezone)의 정보는 담거나 담지 않을 수도 있으므로 헷갈리는 부분이 존재합니다.     시간을 처리할 때 시간대는 왜 중요할까요? 시간대가 명시되지 않은 시각은 충분한 정보를 내포하고 있지 않습니다. 저는 얼마 전, Google Calendar API를 이용하여 작업할 때 골치 아픈 일을 겪었습니다. 오늘의 일정을 불러오고 싶어서 오늘 0시~24시로 데이터를 요청했지만, 계속해서 결괏값에 다음 날의 일정도 포함되어서 반환되었습니다.   왜 다음날 일정도 포함되었던 걸까요? 아래와 같은 코드를 작성하여 Google Calendar API에 요청했습니다.   today = datetime.date.today() from_ = datetime.datetime(today.year, today.month, today.day, 0, 0, 0) to = datetime.datetime(today.year, today.month, today.day, 23, 59, 59) events = get_events_from_google_calendar(from_, to)   몇 시간 동안 머리를 싸매고 코드를 한 줄 한 줄 따져가며 고민을 했습니다. 결국, 제가 요청한 시각에 시간대가 지정되어 있지 않아 get_events_from_google_calendar() 함수 내부에서 from_과 to가 의도하지 않은 시간대의 시각으로 인식되어서 발생했던 문제라는 것을 알게 되었습니다.  # 원래 의도했던 시간대: 대한민국 시간대(KST)에서 오늘 0시 0분 0초 KST = datetime.timezone(datetime.timedelta(hours=9)) from1 = datetime.datetime(today.year, today.month, today.day, 0, 0, 0, tzinfo=KST) # get_events_from_google_calendar()가 받아들인 시간대: UTC 시간대에서 오늘 0시 0분 0초 from2 = datetime.datetime(today.year, today.month, today.day, 0, 0, 0, tzinfo=datetime.timezone.utc)   위 예제에서 from2 - from1를 하게 되면 timedelta(hours=9)가 계산됩니다. 우리가 원했던 것은 KST 기준 오늘 0시부터의 일정이었지만, Google Calendar API에서는 시간대를 UTC로 취급하여 KST 기준 오늘 9시부터 다음날 9시까지의 일정을 불러왔던 것입니다.  이렇듯 시간 관련 작업을 할 때 시간대에 대해 제대로 알고 있지 않으면 의도치 않게 많은 시간을 소모하게 될 수도 있습니다.  오늘은 제가 파이썬으로 시간대 관련 처리를 하며 모았던 정보를 정리하여 공유하고자 글을 작성하게 되었습니다.  시간대  나라 또는 지역마다 살아가는 시각이 다르기 때문에 시간대에 따른 편차가 존재합니다. 이 차이가 피부로 잘 와닿지 않은 채 살아가더라도 캘린더 API나 국제화 서비스 준비 등등 시간과 관련된 작업을 진행하다 보면 시간대 문제에 직면하게 됩니다.  시간대는 영국의 그리니치 천문대(본초 자오선, 경도 0도)를 기준으로 지역에 따른 시간의 차이, 다시 말해 지구의 자전에 따른 지역 사이에 생기는 낮과 밤의 차이를 인위적으로 조정하기 위해 고안된 시간의 구분 선을 일컫는다. 시간대는 협정 세계시(UTC)를 기준으로 한 상대적인 차이로 나타낸다.     UTC에 대한 더 자세한 내용은 여기를 참고해주세요.   시간대에 대한 더 자세한 내용은 여기를 참고해주세요.   파이썬의 datetime.datetime.now()는 실행 환경의 시간대에 따라서 시각을 표시합니다.  2019-01-01 00:00:00 +09:00에 시간대가 Asia/Seoul로 설정된 제 랩탑에서 현재 시각을 가지고 오면, 아래와 같은 시각이 표시됩니다.  >>> print(datetime.datetime.now()) 2019-01-01 00:00:00.000000   그런데, 같은 시각에 Asia/Taipei로 설정된 랩탑에서는 현재 시각이 아래와 같이 표시됩니다.  >>> print(datetime.datetime.now()) 2018-12-31 23:00:00.000000  위의 예제처럼 시간대에 따라 시각이 다를 수 있다는 것을 알 수 있습니다.  나라별 시간대 비교해보기  UTC를 기준으로 시간이 빠르면 +시차, 시간이 느리면 -시차로 표시합니다.                                                                                                                                시간대나라코드UTC-5미국(동부)ESTUTC영국GMTUTC+8대만TWUTC+9대한민국KSTUTC+9일본JSTUTC+10오스트레일리아(동부)AEST     나라별 시간대 차이에 대한 더 자세한 내용은 여기를 참고해주세요.   시간대를 명확히 표시하지 않은 시각은 혼동을 일으킬 수 있습니다. 예를 들어서, 서울에 살고 있는 점주가 2019년 1월 1일 0시 0분에 방문한 고객을 알고 싶어 한다고 가정해봅시다. 이 데이터를 파이썬으로 표현하면 아래와 같이 적을 수 있습니다.  KST = datetime.timezone(datetime.timedelta(hours=9)) korea_1_1 = datetime.datetime(2019, 1, 1, 0, 0, 0, tzinfo=KST)   만약, 대만에 사는 점주가 이를 요청했다면 아래와 같이 적을 수 있습니다.  TW = datetime.timezone(datetime.timedelta(hours=8)) taipei_1_1 = datetime.datetime(2019, 1, 1, 0, 0, 0, tzinfo=TW)   위 예제에서 보이는 것 같이 대한민국과 대만에 있는 점주가 같은 시각을 요청했더라도, 시간대(KST/TW)에 따라서 별도로 처리해야 합니다.  assert korea_1_1 != taipei_1_1 assert taipei_1_1 - korea_1_1 == datetime.timedelta(hours=1) # 같은 시각이지만 시간대에 따라서 시간차가 있습니다.   그렇기 때문에 시간대가 표시되어 있지 않은 2019년 1월 1일이라는 정보만으로는 정확한 시각을 알 수 없습니다.  naive_1_1 = datetime.datetime(2019, 1, 1, 0, 0, 0) assert korea_1_1 != naive_1_1 assert taipei_1_1 != naive_1_1   이런 상황을 해결하기 위해 시각은 어떤 한 시각을 기준으로 하여 그 차이가 표시되어야 합니다. 그 기준으로 정한 것이 UTC입니다. 대한민국은 UTC를 기준으로 아홉시간 빠르기 때문에 korea_1_1의 시각을 UTC 시간대로 표현하면 2018-12-31 15:00:00+00:00입니다. 대만은 UTC를 기준으로 여덟시간 빠르기 때문에 taipei_1_1의 시각을 UTC 시간대로 표현하면 2018-12-31 16:00:00+00:00입니다. 위의 시각은 각각 대한민국(2019-01-01 00:00:00+09:00), 대만(2019-01-01 00:00:00+08:00)으로 표시할 수 있습니다. 이렇게 시간대와 같이 표시하면 혼란 없이 정상적으로 처리할 수 있습니다.  datetime  datetime은 파이썬에서 기본으로 제공하는 표준 라이브러리로, 간단하거나 복잡한 방식으로 날짜와 시각을 조작하기 위한 클래스를 제공합니다.  The datetime module supplies classes for manipulating dates and times in both simple and complex ways.  datetime은 시간대 포함 여부에 따라서 naive datetime, aware datetime 두 가지로 나눕니다.  naive datetime / aware datetime  datetime의 타입을 알아봅시다. 파이썬에서 시간 관련 연산을 하다 보면 종종 아래와 같은 에러 문구를 만날 수 있습니다.  >>> a = datetime.datetime.now() >>> b = datetime.datetime.now(datetime.timezone.utc) >>> a - b Traceback (most recent call last): File "", line 1, in TypeError: can't subtract offset-naive and offset-aware datetimes      naive datetime : naive datetime 객체는 그 자체만으로 시간대를 찾을 수 있는 충분한 정보를 포함하지 않습니다. (e.g. datetime.datetime(2019, 2, 15, 4, 58, 4, 114979))   aware datetime(timezone-aware) : 시간대를 포함합니다. (e.g.datetime.datetime(2019, 2, 15, 4, 58, 4, 114979, tzinfo=)) aware datetime 객체는 자신의 시각 정보를 다른 aware datetime 객체와 상대적인 값으로 조정할 수 있도록 시간대나 일광 절약 시간 정책 혹은 적용 가능한 알고리즘 정보를 담고 있습니다.   tzinfo는 UTC, 시간대 이름 및 DST 오프셋에서 로컬 시간의 오프셋을 나타내는 방법을 담고 있습니다. 더 자세한 내용은 공식 문서를 확인해주세요.  naive datetime은 어느 시간대를 기준으로 하는 시각인지 모호하므로 aware datetime을 이용하는 것을 권장합니다.  직접 확인해보기  준비한 몇 가지 코드를 보며 확인해봅시다. naive datetime과 aware datetime의 차이를 확인하고, 시간대 지정 방법에 대한 내용을 다룹니다.  개발환경     Python 3.7   pytz   여기서는 datetime을 쉽게 다루기 위해 pytz 라이브러리를 사용합니다. pytz는 아래와 같은 장점이 있습니다.    시간대를 시간차가 아닌 사람이 알아보기 쉬운 지역 이름으로 비교적 쉽게 설정할 수 있습니다.   원하는 시간대의 aware datetime으로 변경해주는 localize() 메소드를 제공합니다.   pytz 사용에 앞서, pytz가 제공하는 시간대 식별자를 확인하시려면 다음을 따라 해주세요. import pytz for tz in pytz.all_timezones: print(tz)  혹은 여기를 참고하셔도 좋습니다.  naive datetime  naive datetime은 날짜와 시각만을 갖습니다.  import datetime datetime.datetime.utcnow() # UTC 기준 naive datetime : datetime.datetime(2019, 2, 15, 4, 54, 29, 281594) datetime.datetime.now() # 실행 환경 시간대 기준 naive datetime : datetime.datetime(2019, 2, 15, 13, 54, 32, 939155)   aware datetime naive datetime과 달리 aware datetime은 시간대 정보(tzinfo) 도 갖습니다. import datetime from pytz import utc utc.localize(datetime.datetime.utcnow()) # UTC 기준 aware datetime : datetime.datetime(2019, 2, 15, 4, 55, 3, 310474, tzinfo=)   now는 UTC를 기준으로 현재 시각을 생성합니다. 하지만, naive한 시각입니다.  now = datetime.datetime.utcnow()   이 시각은 naive한 시각이므로 pytz.timezone.localize를 통해 timezone-aware한 시각으로 변환된 시각과 동일하지 않습니다.  assert now != utc.localize(now)   시간대 제대로 지정하기  시간대가 무엇이고, 명시하는 것이 왜 중요한지 알게 되셨다면 시간대를 원하는 의도에 맞게 지정하는 방법에 대해 알아봅시다.  import datetime from pytz import timezone, utc KST = timezone('Asia/Seoul') now = datetime.datetime.utcnow() # UTC 기준 naive datetime : datetime.datetime(2019, 2, 15, 4, 18, 28, 805879) utc.localize(now) # UTC 기준 aware datetime : datetime.datetime(2019, 2, 15, 4, 18, 28, 805879, tzinfo=) KST.localize(now) # UTC 시각, 시간대만 KST : datetime.datetime(2019, 2, 15, 4, 18, 28, 805879, tzinfo=) utc.localize(now).astimezone(KST) # KST 기준 aware datetime : datetime.datetime(2019, 2, 15, 13, 18, 28, 805879, tzinfo=)   replace() 메소드로 날짜나 시간대를 변경할 수 있습니다.  KST = timezone('Asia/Seoul') TW = timezone('Asia/Taipei') date = datetime.datetime.now() # datetime.datetime(2019, 2, 15, 13, 59, 44, 872224) date.replace(hour=10) # hour만 변경 # datetime.datetime(2019, 2, 15, 10, 59, 44, 872224) date.replace(tzinfo=KST) # tzinfo만 변경 # datetime.datetime(2019, 2, 15, 13, 59, 44, 872224, tzinfo=) date.replace(tzinfo=TW) # tzinfo만 변경 # datetime.datetime(2019, 2, 15, 13, 59, 44, 872224, tzinfo=)   하지만 replace는 그 속성 자체만을 바꿔버리는 것이기 때문에 사용에 주의할 필요가 있습니다.  now = datetime.datetime.utcnow() assert utc.localize(now) == now.replace(tzinfo=utc) assert KST.localize(now) != now.replace(tzinfo=KST) assert TW.localize(now) != now.replace(tzinfo=TW)  그뿐만 아니라 replace()를 이용할 경우 의도하지 않은 시간대로 설정될 수도 있으므로 유의해야 합니다. 그 이유는 아래와 같습니다.     시간대는 생각보다 자주 바뀝니다(더 자세한 내용은 스포카의 규칙 2번을 참고해주세요). 이렇게 변경되는 사항들은 tz database에 기록되는데, pytz는 이에 기반합니다. pytz의 버전이 2018.9와 같은 날짜로 되어있는데 2018.9 버전은 2018년 9월 기준 시간대 테이블을 기준으로 시간대를 만들어주는 버전입니다. 이 버전에선 Asia/Seoul의 시간대는 UTC+9입니다.   pytz는 무슨 이유에서 인지 datetime.replace()나 datetime.astimezone()에서 호출될 때 이 tz database 타임 테이블의 맨 첫 번째(가장 오래된) 기록을 가지고 변환을 시도합니다. 서울의 경우 초기에 UTC+8:28이었기 때문에 이 정보를 기반으로 변환합니다.   그래서 pytz를 사용할 때는 pytz.timezone.localize()를 항상 써야 하고, .astimezone()같은 파이썬의 표준 메서드들을 사용하고 싶다면 datetime.timezone을 사용해야 합니다.  스포카의 규칙 스포카에서 datetime을 다룰 때 흔히 따르는 두 가지 큰 원칙이 있습니다.  1. naive datetime은 절대 사용하지 않습니다. 가장 큰 이유는 naive datetime과 aware datetime을 서로 섞어서 쓰지 못한다는 것입니다.  >>> from datetime import datetime, timezone >>> datetime.utcnow() + datetime.now(tz=timezone.utc) Traceback (most recent call last): File "", line 1, in TypeError: unsupported operand type(s) for +: 'datetime.datetime' and 'datetime.datetime'   동적 타입 언어에서 쓸 수 있는 가장 간단한 타입 검사 수단인 isinstance() 체크로도 이 둘을 구별할 수가 없으므로, 코드의 어느 지점에서 naive datetime이 섞이기 시작하면 예기치 않은 지점에서 버그 발생 가능성이 급격히 올라갑니다. Python 2에서 str과 unicode를 섞으면 안 되는 것과 비슷한 이유라고 생각하시면 됩니다.  2. 장기적으로 보존해야 하는 datetime은 항상 UTC를 기준으로 저장합니다. 지역 시간대는 지정학적 또는 정치적인 이유로 생각보다 자주 바뀝니다. 예컨대 1961년 이전까지 한국은 UTC+08:30을 지역 시간대로 사용했었고, 1988년 올림픽 즈음에는 일광 절약 시간대를 시행하고 있었습니다. 시간대 데이터베이스(tz database)는 이런 변경 내역을 담고 있고, pytz가 제공하는 시간대 객체의 동작에도 반영되어 있습니다. 그 때문에 시간대 데이터베이스가 제때 업데이트되지 않거나, 갑작스러운 시간대 변경으로 데이터베이스에 반영이 늦어지거나 하면, 시간 계산에서 오차가 발생할 여지가 있습니다. 또한 같은 aware datetime 이어도 서로 다른 시간대를 가진 datetime끼리 연산하거나 하는 상황도 문제를 복잡하게 만들고, DB나 다른 서비스의 API를 사용할 때, 그 서비스가 시간대를 제대로 다루는 데에 필요한 복잡도를 감수하는 대신 단순히 UTC 기준의 고정 오프셋 시간대만 사용하는 등의 이유로 서로 지원 범위가 맞지 않아 곤란을 겪을 수도 있습니다.  혼선을 줄일 수 있는 좋은 규칙 중 하나는, str과 unicode를 다루던 것과 비슷하게 모든 내부적인 계산에서 UTC 기준의 aware datetime만 사용하고, 사용자에게 보여줘야 할 때만 필요한 시간대로 변환해서 보여 주는 것입니다.  스포카에서는 메인 서버의 dodo.datetime 유틸리티 모듈도 이런 규칙을 따르고 있으며, 대부분의 SQLAlchemy DB 모델 객체의 DateTime 컬럼에서 timezone=True 옵션을 켜서 사용하고 있습니다.  정리  시간 관련 작업을 하신다면 아래 사항을 꼭 기억해주세요.시간대를 명시합시다.시각을 애플리케이션 로직이나 데이터베이스에서 저장할 때는 UTC로 사용하고, 유저에게 표시할 때만 유저의 시간대로 변환하여 보여주도록 합시다.    백엔드 서버끼리 통신할 때도 항상 UTC를 사용한다는 가정을 하면, 시간대가 없더라도 robust하게 처리할 수 있습니다.
조회수 1091

스푼 EX팀의 Chuck을 만나보세요!

스푼을 만드는 사람들 열두 번째 이야기누구라도 '내 주변에도 이런 사람 한 명쯤은 있으면 좋겠다'라고 할법한 그런 사람.핑크색 아이폰이 너무나도 잘 어울리는 남자! 회사에서 보면 좋은 동료 같고, 때론 편한 동네 언니(?) 같이 카페에서 5시간 동안 함께 수다 떨 수 있을 법한 그런 다양한 매력이 있는, 멋진 척을 소개합니다!남자는 턱수염이죠!"제가 처음에 스푼에 입사 전에 물어본 게 있어요. 바로 '수염'을 안 깎아도 되는지에 대한 질문이었어요. 근데 웬걸.. 복장도 자유, 모자 쓰고 오시는 분들도 있고 저의 수염이 막 튀거나, 남다르게 느껴지지 않더라고요. 신선했습니다! 나와 코드가 잘 맞는 곳이구나!라고 생각했죠. 저 수염 기르고 싶거든요."EX 멤버들과 Chuck (오른쪽)듣고 싶은 당신의 스푼 라이프Q. 스푼에 입사하시게 된 계기가 궁금해요"저는 사실 취직을 조금 늦게 한 편인데요. 예전에 첫 직장을 다니다가 몸이 안 좋아져서 조금 오랫동안 쉬었어요. 충분히 쉬고 나서 회복되었을 때, 다시 구직활동을 하려던 차, 스푼에 근무하고 있는 지인이 추천을 해주시더라고요. 사실 그전부터 저는 라이브 스트리밍에 관심이 많은지라  스푼에 대해서 이미 알고 있었고 지인이 스푼을 너무 즐겁게 그리고 열심히 다니시는 모습을 보고 궁금하기도 하고 관심이 생겼었는데, 기회가 닿아서 입사를 하게 되었어요." Q. 척은 어떤 업무를 담당하고 있나요?"저는 사실 처음에 총무 포지션으로 들어왔다가, EX팀 업무도 함께 병행하면서 May의 제안으로 EX팀에서 노무 업무를 맡고 있어요! 예를 들면, 회사 규정을 만드는 업무 있잖아요? 규칙 등 그런 일들을 합니다. 무엇보다 다른 분들을 서포트하는 업무를 많이 하고 있어요."Q.  EX팀에서 나의 존재는?아기 - "EX팀에서 유일한 초보자이니까요!"그래서, 앞으로 배워야 할 것도 많고 열심히 배우려고 노력하고 있답니다. 경험 많은 팀원들께서 잘 이끌어주시고 도와주셔서 열심히 따라가고 있어요.Q. 내가 생각하는 스푼에서 일하는 장점은?"업무에 대해 개개인의 의견을 말할 수 있는 기회가 참 많은 것 같아요. 모든 구성원의 의견을 다 귀담아 들어주려고 노력하는 모습도 멋지고요. 이 부분이 저는 가장 큰 장점이라고 생각해요. 수평적인 조직의 문화의 기초가 되는 부분이라고 생각하거든요"Q. 함께 일하고 싶은 사람은 어떤 사람인가요?제겐 없는 부분을 가진 사람, 차분하고 밸런스가 잡힌 사람과 일하고 싶어요.그 예로, 저희 팀 새로 들어오신 Noah가 계신데요. 면접 때가 굉장히 인상 깊었어요. 면접 때 긴장하셨을 텐데도 불구하고 질문에 대한 답변을 굉장히 차분히, 틀린 부분은 정정하시면서 대답을 해주시더라고요. 그 부분이 굉장히 매력 있고 저와는 다른 부분으로 서로 부족한 부분을 채워줄 수 있을 것 같다고 생각했어요. 팀 내에 다양한 성향과 성격의 사람들이 있으면 그런 부분이 좋을 것 같아요.척이 수집하는 신발들의 '일부분' 사진알고 싶은 Chuck의 이야기Q. 나를 한마디로 표현한다면?오나이 - "사나이의 상반되는 개념이고, 한량이되 한심하지 않은 사람을 말합니다."Q. 법을 공부하셨다고 들었습니다."네, 어릴 땐 제 꿈이 법조인이 되는 거라고 생각했고, 그래서 법학과를 나왔어요. 생각해보면 제가 법을 공부하고 고시 준비를 했던 건 법조인이 되고 싶다는 마음보다는, 법조인이 된다면 제가 얻을 수 있는 것들과 제게 돌아오는 것들이 좋다고 생각했던 것 같아요. 공부는 중학교 때 까진 정말 열심히 했던 것 같은데, 고등학교 땐 잘 안 했던 것 같아요(겸손모드..) 그 당시엔 사실 저는 공부 말고 제가 무엇을 잘하는지 모르겠더라고요. 그래서 열심히 해야 한다고 생각했던 것 같아요"Q. 신발 수집은 언제부터 시작됐나요?"어릴 때부터 신발을 좋아했던 것 같아요. 우리 세대, 제 세대엔 마이클 조던이 전성기였거든요. 그때 뭔가 트렌드였어요. 저는 운동화뿐만 아니라, 부츠도 좋아하고 모든 신발을 좋아하지만 그중 운동화가 가장 많은 것 같아요. 이유는 음.. 모르겠어요.. 그냥 좋아하는 신발을 신고 있다는 그 느낌이 좋아요. 근데 저 생각보다 운동화 몇 켤레 없어요. 한 20켤레 정도 될걸요? 더 어릴 땐 지방까지 내려가서 사고, 줄 서서 사곤 했는데 요즘은 그러진 않아요! 아! 그리고 저 모자도 수집해요. 매년 4월이 되면 모자를 꼭 하나씩 사요. 생일 쿠폰이 나오거든요. 그래서 얼마 전에 또 신상 모자 하나 샀어요"Q. 척의 인싸력은 타고난 건가요?"저요? 저 낮 좀 가리는 편인데요? (실상 전혀 그렇게 보이지 않음. 누구보다도 친근하고, 편함)단지 저는 어색한 상황을 좋아하지 않는 편이에요. 아마 그래서 모두와 편하게 지내려고 하는 게 아닐까 싶어요"Q. 원래부터 Yolo 인생을 살았나요?*Yolo (You live only once) : 미래를 위해 현재를 희생하기보다 현재를 즐기려는 사람"저는 오늘이 행복하지 않으면, 내일도 행복하지 않다고 생각해요. 제 좌우명이 오늘이 행복하면 됐다이거든요. 내가 지금 행복한가?라고 묻는 다면 그건 내가 지금 행복하지 않기에 묻는 질문이라고 생각해요. 원래부터 그랬던 건 아닌 것 같은데, 크게 아프고 나서 변한 것 같아요. 지금은 물론 의학적으로 건강하지만요. 저는 제가 완전한 Yolo족은 아닌 것 같은데.. 제가 다른 분들에겐 그래 보일 수도 있을 것 같네요!"Q. 인터뷰해보시니 어떠셨어요?"기분이 좋았어요. 누군가 저에게 관심을 가져주고, 질문을 해준다는 게 기분 좋은 일이더라고요 :)"(인터뷰에 응해주셔서 감사합니다)Chuck은, 1. 음식을 가리지 않지만, '직화' 요리만 먹지 않습니다 2. 술, 담배를 하지 않습니다.함께 식사를 하게 된다면, 센스 있게 '직화' 요리는 피하고 술과 담배는 권하지 않으면 센스만점 동료가 될 수 있을 것 같아요 :) 팀원들이 척을 한마디로 표현한다면?Go 曰: 마이쿤의 명태 코다리 명태 코다리는 사계절 내내 명태의 참맛을 느낄 수 있다고 합니다,속초 출신인 척이 마이쿤을 위해서 사계절 내내 열심히 일해주세요~May 曰: 냉철한 두뇌와 뜨거운 마음의 소유자 사고는 논리적이고 체계적이지만 행동은 정의롭고 따뜻하거나 가끔 뜨겁기도 함 ㅎㅎKai 曰: 무서운 형 - 가끔 눈살을 찌푸릴 때 화난 거 같이 보여서요..Noah 曰: 고등학교 동창 - 낯설지 않은 친근함이 매력 포인트
조회수 1138

에잇퍼센트와 함께한 2016년

2016년 4월에 에잇퍼센트에 합류해서 2016년을 에잇퍼센트와 함께 보냈다. 1년을 다 채운건 아니지만 에잇퍼센트의 성장과 발전에 개발자로서 어떤 기여를 했는지 한 번 정리해 보려고 한다. 나에게 있어서는 물론이고 에잇퍼센트에도 의미가 있을 것이다.1. 대출 개설 내역 신용평가사 공유대출자에게 대출이 실행되면 이 내역을 신용평가사의 시스템으로 공유하는 것을 개발했다. 에잇퍼센트에서 받은 대출 내역이 공유되면 타 금융권에서 대출 개설 내역을 확인할 수 있으므로 추가 대출이 쉽게 이루어지지 않을 것이다. 우리의 고객은 대출자도 있지만 투자자도 있다는 측면에서 투자자의 소중한 투자금을 안전하게 지켜내기 위한 안전 장치 중 하나가 될 수 있다.  2. 성능 개선에잇퍼센트는 빠르게 성장하고 있는 스타트업이다. 스타트업이 비즈니스적으로 빠른 실행을 하며 달리다 보면 성능의 벽에 부딪힐 때가 있는데 마침 내가 합류하고 얼마 되지 않은 시점이었다. 주로 우리의 개발 환경인 Python Django 코드를 개선하는 방향으로 진행을 했고 추후에는 사용자 브라우저단 성능 개선도 진행했다. Python Django 코드 개선에 대한 자세한 내용은 내가 쓴 블로그 포스팅에서 확인할 수 있다.3. 서버 인프라 서울 이전에잇퍼센트는 서버 인프라로 Amazon Web Services(이하 AWS)를 사용하고 있다. 서비스를 처음 시작할 때에는 AWS 도쿄 리전이 가장 가까운 곳이어서 도쿄 리전을 사용하고 있었다. 그러다가 2016년이 되어 서울 리전이 생겼고 도쿄 리전에 비해 네트워크도 빠르고 비용도 저렴해서 사용하지 않을 이유가 없었다. 그래서 도쿄 리전에서 서울 리전으로 이전하는 작업을 진행했다. 밤샘 작업을 함께 해준 개발팀원들에게 고맙다는 얘기를 다시 한번 하고 싶다.그 날의 풍경과 작업 기록, 그리고 django-storages 서울 리전 연동에 대한 글까지 남겨두었다.4. Python Django 버전 업그레이드에잇퍼센트에 합류했을 때 Python 3.4 , Django 1.8을 사용하고 있었고 Python 3.5 Django 1.9로 버전 업그레이드를 진행했다. 버전 업을 하면서 발견된 큰 문제는 없었고 Django admin 의 UI 에 flat 디자인이 적용되어 화면이 이뻐졌다. 내가 직접 한 건 아니지만 화면이 이뻐졌다고 다들 좋아해 준 기억이 난다.참고로 현재의 최신 버전은 Python 3.6 Django 1.10이다.5. 테스트 개선개발을 빠르게 하는 것도 중요하지만 안정적으로 잘 동작하는 것도 그에 못지않게 중요하다. 안정적인 서비스 개발과 운영을 위해 테스트가 중요하다. 특히나 돈을 다루는 금융회사라면 더욱 중요한 것이 테스트이기에 에잇퍼센트 개발팀에도 테스트는 매우 중요하다. 테스트 코드를 잘 작성해야 하고 테스트 코드가 실제 코드를 얼마나 커버하는지에 대한 측정도 필요하다. 에잇퍼센트는 코드를 개발해서 push 하고 pull request를 할 때마다 travis를 통해서 테스트를 수행하고 커버리지를 측정하고 있다. 커버리지 측정을 처음 시작할 때와 비교해보면 기존 대비 10% 포인트 가량 커버리지가 올라갔다. 커버리지가 떨어지면 pull request를 승인하지 않는 것을 정책으로 가져가고 있다.테스트 수행 시 커버리지 측정과 함께 PEP8 준수 확인, migration 체크, 템플릿 검증 등을 하도록 개선했다. 또한 기존에는 테스트 수행 시 sqlite3을 DB로 사용했는데 개선 후에는 실제로 사용하고 있는 PostgreSQL을 사용하도록 했다. 성능을 위해 raw SQL을 사용하는 경우가 가끔 있는데 이때 테스트가 제대로 안 되는 문제를 개선할 수 있었다.6. 개발 환경 개선유지 보수가 용이하고 효율적으로 개발하기 위한 환경을 만들기 위해 노력했다. smarturls, django-debug-toolbar, factory boy 등의 패키지를 적용했으며 로컬 서버 외에 개인별로 배포해서 테스트할 수 있는 서버 환경을 만들어서 테스트를 용이하게 했다. django 설정 분리, 모델 분리, 상수 분리 등의 리팩토링도 진행했다.7. NH핀테크 오픈플랫폼 적용 (진행 중)NH 농협 은행에서 제공하는 API를 사용해서 금융 관련 작업을 자동화하고 효율화하려고 한다. NH에서 요구하는 정보보호 및 보안 기준에 맞춰 시스템을 정비하고 만들어 나가는 중이다. 올해 상반기 안에 API 연동이 되어 에잇퍼센트에 NH핀테크 오픈플랫폼이 알맞게 녹아들어 가기를 기대해본다.8. 서비스 개선에잇퍼센트의 서비스적인 개선도 몇 가지 진행했다.- 채권 상세 페이지 개선 : 투자한 채권에 대한 지급 현황, 지난 내역을 볼 수 있게 되었다.- 로그인 상태 유지 : 기본 30분 로그인이 유지되고 30일 유지를 선택할 수 있게 되었다.- Ada 챗봇 연동 : 금융권 챗봇 중 유일하게 학습하는 Ada가 서비스와 연동하기 위한 개발을 진행했다.1~7번까지의 작업은 주로 눈에 보이지 않거나 개발팀 내부적인 개선이었고 8번은 사용자에게 바로 보이는 서비스적인 개선이었다. 위에 언급한 것 외에 코드 리뷰를 열심히 한 것이 기억에 남는다. 코드 리뷰를 통해 유지보수가 용이하고 효율적인 코드를 만들어 나가고 싶었다. 어떤 코드가 유지보수가 용이하고 효율적인지 리뷰를 통해 토론하고 배워나가는 과정이 나뿐만 아니라 개발팀 모두에게 도움이 되었으리라 본다.개인적으로는 이음에서 Ruby on Rails로 개발을 재밌게 하다가 에잇퍼센트에서의 Python Django를 사용한 개발로의 도전과 전환이었다. 새로운 언어를 접하고 배워나가는 과정 또한 개발자에게는 즐거움이 아닐까 싶다. Ruby, Python 둘 다 엄청나게 잘 하는 건 아니지만 기회가 되면 Ruby와 Python 각각의 장단점을 비교해보는 것도 재미있을 것 같다.2016년은 내가 2006년 첫 회사에 들어간 지 10년이 된 해였다. 10년 전 신입 사원 시절에는 서비스적인 개선을 주로 하고 다른 팀원에게 도움을 주기보다 내 할 일에만 충실했었다. 10년이 지난 지금 작년 한 해를 이렇게 돌아보니 서비스에 직접적인 개선도 하고 다른 팀원들이 개발을 더 잘할 수 있도록 환경을 만들고 도움을 주는 데에도 제법 역할을 했다는 생각이 든다. 나 자신이 엄청나게 변화한 건 아니지만 10년이 헛된 시간은 아니었다는 생각이 들어서 내심 뿌듯하다.2016년을 이렇게 정리하고 보니 2017년에는 더 잘 해야겠다는 생각이 든다. 에잇퍼센트 모든 구성원들과 함께 엄청나고 멋지게 성장해 보고 싶다. 화이팅!2016년 처럼 올해도 해맑게 화이팅!#8퍼센트 #에잇퍼센트 #조직문화 #기업문화 #2016년 #돌아보기 #스타트업개발자 #개발자
조회수 2444

MySQL의 Transaction Isolation Level (Lock에 관하여)

편집자 주문맥에 따라 ‘Transaction’과 ‘트랜잭션’으로 영어와 한글을 혼용함.문맥에 따라 ‘LOCK’과 ‘lock’으로 대문자와 소문자를 혼용함.OverviewMySQL DB는 일반적인 운영환경에서 뛰어난 성능을 제공합니다. 특히 적은 양의 자료가 빈번하게 교류되는 환경에서는 더욱 빛을 발하죠. 국내에서는 주로 작은 규모의 웹사이트를 구축할 때 MySQL을 사용합니다. 그런데 문제는 사이트의 규모가 커지면서부터 생긴다는 것이죠. 조금씩 느려지는 Query가 생기면 원인도 파악하고, Query를 튜닝하고, 설계도 변경하지만 MySQL의 특징적인 문제를 곧 만나게 됩니다.테이블을 복제(CREATE SELECT)하거나 다른 테이블로 옮기면(INSERT SELECT) 작업을 하는 동안 SELECT 절에 있는 테이블들이 Lock이 걸립니다. 게다가 다른 Session에서 해당 테이블을 수정(UPDATE / DELETE)하면 복제와 이동을 마칠 때까지 대기 상태로 있어야 한다는 것입니다. 이러한 문제는 시스템을 구축하고 자료가 일정량 쌓이기 전까지는 알 수 없습니다. 또한 Oracle과 같은 DB를 사용하던 사용자가, MySQL을 사용하면 이와 같은 문제가 있을 것이라고 생각하기도 어렵습니다.이러한 특징을 가진 MySQL의 Transaction Isolation Level을 알아보고자 합니다. Transaction Isolation Level 은 Transaction의 경리 수준을 말합니다. 트랜잭션 처리 시 다른 트랜잭션에서 접근해 자료를 수정하거나 볼 수 있도록 하는 수준입니다.Transaction Isolation Level의 종류와 특성Transaction Isolation Level에는 READ UNCOMMITTED, READ COMMIITED, REPEATABLE READ, SERIALIZE 네 가지 종류가 있습니다. 1)READ UNCOMMITTED1) COMMIT 되지 않은 데이터에 다른 트랜잭션에서 접근할수 있다.2) INSERT, UPDATE, DELETE 후 COMMIT 이나 ROLLBACK에 상관없이 현재의 데이터를 읽어온다.3) ROLLBACK이 될 데이터도 읽어올 수 있으므로 주의가 필요하다.4) LOCK이 발생하지 않는다.READ COMMIITED1) COMMIT 된 데이터에 다른 트랜잭션에서 접근할 수 있다.2) 구현 방식이 차이 때문에 Query를 수행한 시점의 데이터와 정확하게 일치하지 않을 수 있다.3) LOCK이 발생하지 않는다.4) MySQL에서 많은 양의 데이터를 복제하거나 이동할 때 이 LEVEL을 추천한다.REPEATABLE READ1) Default LEVEL이다.2) SELECT시 현재 시점의 스냅샷을 만들고 스냅샷을 조회한다.3) 동일 트랜잭션 내에서 일관성을 보장한다.4) record lock과 gap lock이 발생한다.5) CREATE SELECT, INSERT SELECT시 lock이 발생한다.SERIALIZE1) 가장 강력한 LEVEL이다.2) SELECT 문에 사용하는 모든 테이블에 shared lock이 발생한다.LOCK과 테이블, 어떻게 해결할 수 있을까?지금부터는 관련된 내용을 확인해보겠습니다. 우선 현재의 경리 수준부터 알아보겠습니다.mysql> SHOW VARIABLES WHERE VARIABLE_NAME='tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec) 다음으로 TEST 테이블을 만듭니다. 이때 SELECT절의 테이블을 UPDATE할 경우, 대기 상태로 빠지는 것을 확인해보겠습니다. 테이블을 만들고 상태를 확인합니다.CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 ; -- 생성시 INFORMATION_SCHEMA.PROCESSLIST 로 상태를 확인합니다. mysql> SELECT -> * -> FROM INFORMATION_SCHEMA.PROCESSLIST -> WHERE USER = 'hansj' -> AND COMMAND <> 'Sleep' -> \G *************************** 1. row *************************** ID: 11004 USER: hansj HOST: 192.168.1.150:50711 DB: test COMMAND: Query TIME: 5 STATE: Sending data INFO: CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 1 row in set (0.00 sec) 다음으로 테이블 생성 시 UPDATE를 해 대기 상태로 빠지는지 확인해보겠습니다.UPDATE test.TB_PROD_BAS SET PROD_MEMO = 'TEST' WHERE PROD_ID = 1 ; mysql> SELECT -> * -> FROM INFORMATION_SCHEMA.PROCESSLIST -> WHERE USER = 'hansj' -> AND COMMAND <> 'Sleep' -> \G *************************** 1. row *************************** ID: 11004 USER: hansj HOST: 192.168.1.150:50711 DB: test COMMAND: Query TIME: 24 STATE: Sending data INFO: CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 *************************** 2. row *************************** ID: 11006 USER: hansj HOST: 192.168.1.150:50719 DB: test COMMAND: Query TIME: 22 *****이부분 중요합니다.****** STATE: updating *****이부분 중요합니다.****** INFO: UPDATE test.TB_PROD_BAS SET PROD_MEMO = 'TEST' WHERE PROD_ID = 1 2 rows in set (0.00 sec) 위의 TIME을 보면 테이블이 생성될 때까지 대기하고, UPDATE 문의 상태가 updating 으로 표시됩니다. 하지만 이렇게 나올 경우 건수가 많으면 실제 UPDATE 중인지 대기상태인지 확인하기가 어렵습니다. LOCK이 걸린 테이블을 확인하려면 INNODB LOCK 테이블로 정확하게 알 수 있습니다. 아래 세 가지 테이블로 확인해보겠습니다. 보다 자세한 설명은 MySQL 홈페이지를 확인합니다.information_schema.INNODB_TRXLOCK을 걸고 있는 프로세스 정보information_schema.INNODB_LOCK_WAITS현재 LOCK이 걸려 대기중인 정보information_schema.INNODB_LOCKSLOCK을 건 정보위의 각 항목마다 테이블 생성 및 UPDATE 시 정보가 어떻게 나타나는지 확인해보겠습니다.1.information_schema.INNODB_TRXmysql> SELECT -> T101.TRX_ID -> ,T101.TRX_STATE -> ,T101.TRX_STARTED -> ,T101.TRX_REQUESTED_LOCK_ID -> ,T101.TRX_WAIT_STARTED -> ,T101.TRX_WEIGHT -> ,T101.TRX_MYSQL_THREAD_ID -> ,T101.TRX_ISOLATION_LEVEL -> ,SUBSTR(T101.TRX_QUERY,1,10)AS TRX_QUERY -> FROM information_schema.INNODB_TRX T101 -> ; +---------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+---------------------+------------+ | TRX_ID | TRX_STATE | TRX_STARTED | TRX_REQUESTED_LOCK_ID | TRX_WAIT_STARTED | TRX_WEIGHT | TRX_MYSQL_THREAD_ID | TRX_ISOLATION_LEVEL | TRX_QUERY | +---------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+---------------------+------------+ | 8771591 | LOCK WAIT | 2019-05-27 16:15:53 | 8771591:70031:4:306 | 2019-05-27 16:15:53 | 2 | 11006 | REPEATABLE READ | UPDATE tes | | 8771586 | RUNNING | 2019-05-27 16:15:51 | NULL | NULL | 1538969 | 11004 | REPEATABLE READ | CREATE TAB | +---------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+---------------------+------------+ 2 rows in set (0.00 sec) TRX_ID_STATE트랜잭션의 상태를 나타냅니다. 실행 중인지 LOCK WAIT 상태인지 알 수 있습니다.TRX_MYSQL_THREAD_IDPROCESSLIST 의 ID를 나타냅니다.TRX_ISOLATION_LEVELISOLATION LEVEL을 나타냅니다.따라서 위의 내용을 보면 CREATE TABLE이 실행 중인 것과, UPDATE가 LOCK WAIT인 것, 그리고 관련된 PROCESSLIST의 ID까지도 알 수 있습니다2.information_schema.INNODB_LOCK_WAITSmysql> SELECT -> * -> FROM information_schema.INNODB_LOCK_WAITS T101 -> ; +-------------------+---------------------+-----------------+---------------------+ | requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id | +-------------------+---------------------+-----------------+---------------------+ | 8771591 | 8771591:70031:4:306 | 8771586 | 8771586:70031:4:306 | +-------------------+---------------------+-----------------+---------------------+ 1 row in set (0.01 sec) requesting_trx_idLOCK WAIT 인 TRX_IDblocking_trx_idLOCK 을 건 TRX_ID현재 LOCK이 걸린 TRX_ID와 LOCK을 걸어둔 TRX_ID를 알 수 있습니다.3.information_schema.INNODB_LOCKSmysql> SELECT -> * -> FROM information_schema.INNODB_LOCKS -> ; +---------------------+-------------+-----------+-----------+----------------------+------------+------------+-----------+----------+-----------+ | lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data | +---------------------+-------------+-----------+-----------+----------------------+------------+------------+-----------+----------+-----------+ | 8771591:70031:4:306 | 8771591 | X | RECORD | `test`.`TB_PROD_BAS` | PRIMARY | 70031 | 4 | 306 | 1 | | 8771586:70031:4:306 | 8771586 | S | RECORD | `test`.`TB_PROD_BAS` | PRIMARY | 70031 | 4 | 306 | 1 | +---------------------+-------------+-----------+-----------+----------------------+------------+------------+-----------+----------+-----------+ 2 rows in set (0.01 sec) lock_trx_idLOCK 과 관련된 TRX_IDlock_modeX 쓰기, S 읽기 2)어떤 테이블이 LOCK을 걸고 있는지 알 수 있습니다.위의 내용들을 통해 REPEATABLE READ에서 CREATE SELECT시 SELECT 테이블에 LOCK이 걸려 UPDATE가 대기하게 되는 것을 알 수 있습니다. 이번에는 Transaction Isolation Level 을 READ COMMIITED로 변경하고 CREATE SELECT 및 UPDATE를 진행해보겠습니다.SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SHOW VARIABLES WHERE VARIABLE_NAME='tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | READ-COMMITTED | +---------------+-----------------+ 1 row in set (0.00 sec) UPDATE 문은 다음과 같이 수행됩니다. mysql> UPDATE test.TB_PROD_BAS -> SET PROD_MEMO = 'TEST' -> WHERE PROD_ID = 1 -> ; Query OK, 0 rows affected (0.04 sec) Rows matched: 1 Changed: 0 Warnings: 0 기존에 대기했던 것과 다르게 0.04초가 걸렸습니다.mysql> SELECT -> * -> FROM INFORMATION_SCHEMA.PROCESSLIST -> WHERE USER = 'hansj' -> AND COMMAND <> 'Sleep' -> \G *************************** 1. row *************************** ID: 11004 USER: hansj HOST: 192.168.1.150:50711 DB: test COMMAND: Query TIME: 9 STATE: Sending data INFO: CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 1 row in set (0.00 sec) -- 프로세스 정보도 CREATE TABLE 만 진행중임을 알수 있습니다. mysql> SELECT -> T101.TRX_ID -> ,T101.TRX_STATE -> ,T101.TRX_STARTED -> ,T101.TRX_REQUESTED_LOCK_ID -> ,T101.TRX_WAIT_STARTED -> ,T101.TRX_WEIGHT -> ,T101.TRX_MYSQL_THREAD_ID -> ,T101.TRX_ISOLATION_LEVEL -> ,T101.TRX_QUERY -> FROM information_schema.INNODB_TRX T101 -> ; +---------+-----------+---------------------+-----------------------+------------------+------------+---------------------+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ | TRX_ID | TRX_STATE | TRX_STARTED | TRX_REQUESTED_LOCK_ID | TRX_WAIT_STARTED | TRX_WEIGHT | TRX_MYSQL_THREAD_ID | TRX_ISOLATION_LEVEL | TRX_QUERY | +---------+-----------+---------------------+-----------------------+------------------+------------+---------------------+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ | 8771856 | RUNNING | 2019-05-27 17:17:45 | NULL | NULL | 4594347 | 11004 | READ COMMITTED | CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 | +---------+-----------+---------------------+-----------------------+------------------+------------+---------------------+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) READ COMMITTED LEVEL로 CREATE만 수행 중인 것을 알 수 있습니다.mysql> SELECT -> * -> FROM information_schema.INNODB_LOCK_WAITS T101 -> ; Empty set (0.00 sec) mysql> SELECT -> * -> FROM information_schema.INNODB_LOCKS -> ; Empty set (0.00 sec) LOCK을 걸고 걸린 것이 없어 내용도 없습니다.Conclusion지금까지 Transaction Isolation Level 을 기준으로 CREATE SELECT 시 SELECT 에 사용되는 테이블도 LOCK이 걸릴 수 있는 것을 확인했고, 그에 따른 해결 방법까지 알아봤습니다.INSERT INTO SELECT에서도 같은 현상이 나타납니다. 그렇기 때문에 운영 중인 테이블을 복제(CREATE SELECT)하거나 다른 테이블로 옮길 경우(INSERT SELECT) Transaction Isolation Level을 READ COMMITTED 변경하고 작업하기를 권장합니다.그렇지 않으면 관련된 TABLE은 LOCK이 걸리고, 관련 Query들이 대기 상태로 빠지면서 시스템 장애가 발생할지도 모릅니다.참고1)MySQL :: MySQL 5.6 Reference Manual :: 14.7.2.1 Transaction Isolation Levels2)MySQL :: MySQL 5.6 Reference Manual :: 14.7.1 InnoDB Locking글한석종 부장 | R&D 데이터팀[email protected]브랜디, 오직 예쁜 옷만
조회수 1059

Rxjava를 이용한 안드로이드 개발

Overview브랜디는 현재 2.0 기반 Android 버전입니다. Main Thread와 Sub Thread 사이의 ANR를 방지하려고 Volley, Otto Bus Library를 사용해서 백엔드 서비스(back-end Service)를 연동하고 있습니다. 이제 3.0 개발로 더 좋은 백엔드 서비스 기능을 만들려고 합니다. (기반 작업은 이미 완료했습니다.) 다만 3년 동안 브랜디 앱을 개발하면서 느꼈던 고통과 피로를 ‘제발’ 줄여보고 싶어서 브랜디 3.0에서는 Retrofit2 와 RxJava, Lambda 표현식을 사용하기로 했습니다. RxJava(Reactive programming)는 가장 추천하고 싶은 것 중 하나입니다. 우리는 함수형 리액티브(반응형) 프로그램이라는 표현으로 자주 마주치곤 하는데요. 주로 옵저버 패턴(Observer pattern)을 대체하기 위해 사용합니다. 단순히 데이터를 넘기고 마무리하는 건 옵저버 패턴으로도 충분하지만 대부분의 문제는 이벤트들을 묶어서 사용할 때 생깁니다.1) RxJava는 이벤트에 대한 조건 처리나 실패 처리, 리소스 정리에 대비해 사용합니다. 기존 방식의 명령형 리액티브 접근 방식을 사용하면 복잡함이 지속적으로 증가하는 반면, 함수형 리액티브 프로그래밍은 효율을 크게 높일 수 있습니다. 몇 가지 예제와 함께 살펴보겠습니다. Android에 직접 사용해보기새로운 프로젝트를 생성한 후, 아래와 같이 build.gradle 파일을 수정해봅시다. (JDK 1.8 설치 필수) apply plugin: 'com.android.application' android {    compileSdkVersion 26   defaultConfig {        applicationId "kr.co.brandi.myapplication"        minSdkVersion 21        targetSdkVersion 26        versionCode 1        versionName "1.0"        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"    }    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }   //추가된 부분 1      compileOptions {        sourceCompatibility JavaVersion.VERSION_1_8        targetCompatibility JavaVersion.VERSION_1_8   }  } dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar'])       //추가된 부분2    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'    implementation 'io.reactivex.rxjava2:rxjava:2.1.3'      implementation 'com.android.support:appcompat-v7:26.1.0'    implementation 'com.android.support.constraint:constraint-layout:1.0.2'    implementation 'com.android.support:design:26.1.0'    testImplementation 'junit:junit:4.12'    androidTestImplementation 'com.android.support.test:runner:1.0.1'    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' } 이제 람다 표현식과 RxJava를 사용할 준비가 되었습니다.Flowable.just("Hello World").subscribe(new Consumer() {    @Override   public void accept(String s) throws Exception {        Log.v(tag, s);   }  });   Flowable.just("Hello World !").subscribe(s -> Log.v(tag, s)); 간단한 생성자와 결과를 출력하는 부분입니다. 두 번째 subscribe는 람다 표현식으로 인터페이스를 생성하지 않더라도 첫 부분과 동일하게 결과물을 얻을 수 있습니다.2) 이제 RxJava에서 간단한 필터링으로 간편하게 데이터를 가공하는 능력을 확인해보겠습니다. 아래 코드는 기본적인 List 의 값을 출력하는 부분입니다.List valueList = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);   for (int data : valueList) {    String result = "value " + data;    Log.v(tag, result);  } Flowable.fromIterable(valueList)        .map(new Function() {            @Override            public String apply(Integer data) throws Exception {                return "value : " + data;            }        })        //.map(data -> "value : " + data)        .subscribe(data -> Log.v(tag, data)); 위의 코드에 조건을 추가해 ’짝수’만 출력하겠습니다.for (int data : valueList) {    if ((data % 2) == 0) {        String result = "value " + data;        Log.v(tag, result);    }  } Flowable.fromIterable(valueList)        //.filter(data -> {        //      return (data % 2) == 0;        //})        .filter(data -> (data % 2) == 0)        .map(data -> "value : " + data)        .subscribe(data -> Log.v(tag, data)); 위와 같이 데이터 가공은 순차적으로 진행되고, 여러 함수로 간단하게 처리할 수 있습니다. 원하는 데이터 가공을 위해 filter, map 등의 함수들을 순차적으로 이어 붙일 수도 있습니다.위에서 보여드린 RxJava는 간단한 예시이기 때문에 RxJava 의 기능을 좀 더 보여드리겠습니다.String[] data1 = {Shape.HEXAGON, Shape.OCTAGON, Shape.RECTANGLE};  String[] data2 = {Shape.TRIANGLE, Shape.DIAMOND, Shape.PENTAGON};   Flowable source =        Flowable.combineLatest(                Flowable.fromArray(data1)                        .zipWith(Flowable.interval(100L, TimeUnit.MILLISECONDS), (shape, notUsed) -> Shape.getId(shape)),                Flowable.fromArray(data2)                        .zipWith(Flowable.interval(150L, 200L, TimeUnit.MILLISECONDS), (shape, notUsed) -> Shape.getSuffix(shape)),                (id, suffix) -> id + suffix);   source.subscribe(s -> Log.d(getThreadName(), s)); CombineLatest() 함수를 이용해 두 개의 스트림을 하나로 처리하는 방법을 보여 드렸습니다. 각각의 스트림은 interval 함수를 시간 간격으로 data1과 data2 배열의 개수만큼 반복하여 처리하는 로직입니다. 서로 다른 두 스트림은 마지막 데이터를 가지고 있으며 새로운 데이터가 나올 때마다 하나의 스트림으로 출력됩니다.마블 다이어그램 3)결과Conclusion만약 RxJava를 이용하지 않고 두 개의 TimerTask를 이용해서 코딩했다면 결과는 같았을지도 모릅니다. 이제 RxJava를 알기 때문에 다시는 TimerTask를 이용해서 코딩할 일은 없을 겁니다. 알면 알수록 다양한 기능을 갖추고 있는 RxJava! 이제 브랜디 상용화 버전에 사용할 수 있게 다시 개발의 숲으로 들어가겠습니다. 그럼 이만. 1)함수나 네트워크 호출의 비동기 응답 2)Java 8 람다 표현식 자세히 살펴보기, 2018.03.09. 3)RxJava on Android 글고재성 과장 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 2516

BLSTM Tutorial

Summary:이 포스팅은 Bidirectional LSTM에 대한 기본 개념을 소개하고, tensorflow와 MNIST 데이터를 이용하여 구현해 봅니다.Bidirectional LSTM1. 개념 설명앞에서 RNN 과 LSTM 모델에 대해 소개했습니다.기본적인 LSTM 모델은 이전 시간의 step들이 다음 step에 영향을 줄 것이라는 가정을 했습니다.하지만 이후의 step 또한 앞의 step 에 영향을 줄 수 있다면 이 모델을 어떻게 적용시킬 수 있을까요?이후의 step 의 영향도 반영한 모델이 Bidirectional LSTM 모델입니다.위의 그림과 같이 BLSTM 은 두 개의 LSTM 모델을 Concatenate 하여 사용합니다.Time step 이 1부터 t 까지 있다고 가정할 때 forward lstm model 에서는 input 을 Time step 이 1 일때부터 t 까지 순차적으로 주고 학습합니다.반대로 backward lstm model 에서 input 을 T = t 일때부터 1까지 거꾸로 input 주고 학습을 하게 됩니다.time step 마다 두 모델에서 나온 2개의hidden vector은 학습된 가중치를 통해 하나의 hidden vector로 만들어지게 됩니다.2. 구현전체 코드는 Github page 를 참고해주세요.MNIST image 를 input 으로 넣었을 때 이 image 가 0 에서 9 중에 어떤 숫자인지 맞추는 BLSTM 모델을 만들어 보고자 합니다.MNIST 는 0 - 9 의 숫자 image data 이며 각 데이터는 28 x 28 의 matrix (data 는 28 x 28 길이의 array) 로 이루어져 있습니다.앞에서 봤듯이 LSTM 은 sequence 형태를 요구합니다.그래서 데이터 하나를 한 번에 넣는 것이 아니라 각 데이터의 matrix 를 row 만큼, 즉 28번의 time step 으로 나누어 넣어주게 됩니다.그래서 input_sequence 를 28 길이로 설정합니다. learning_rate = 0.001 training_epochs = 10 # 전체 데이터를 몇번 반복하여 학습 시킬 것인가 batch_size = 256 # 한 번에 받을 데이터 개수 # model # 입력되는 이미지 사이즈 28*28 input_size = 28 # input size(=input dimension)는 셀에 입력되는 리스트 길이 input_steps = 28 # input step(=sequence length)은 입력되는 리스트를 몇개의 time-step에 나누어 담을 것인가? n_hidden = 128 n_classes = 10 # classification label 개수 X = tf.placeholder(tf.float32,[None, input_steps, input_size]) Y = tf.placeholder(tf.float32,[None, n_classes]) W = tf.Variable(tf.random_normal([n_hidden * 2, n_classes])) b = tf.Variable(tf.random_normal([n_classes])) X 는 28 x 28 의 matrix 로 이루어진 데이터를 받고 Y 는 실제 class (0 - 9) 를 의미하는 length 10 의 vector 를 받습니다.그리고 각 forward lstm 모델과 backward lstm 모델에서 들어오는 weight 값을 받을 변수를 설정합니다.DropoutWrapper 는 모델에서 input 으로 주어진 data 에 대한 Overfitting 이 발생하지 않도록 만들어주는 모델입니다.각 state 를 랜덤하게 비활성화시켜서 데이터를 더 random 하게 만들어줍니다. keep_prob 변수를 통해서 dropoutWrapper 의 확률값을 조정합니다.keep_prob = tf.placeholder(tf.float32) forward lstm 과 backward lstm 에서 사용할 cell을 생성합니다# lstm cell 생성 lstm_fw_cell = tf.nn.rnn_cell.LSTMCell(num_units = n_hidden, state_is_tuple = True) lstm_fw_cell = tf.nn.rnn_cell.DropoutWrapper(lstm_fw_cell, output_keep_prob=keep_prob) lstm_bw_cell = tf.nn.rnn_cell.LSTMCell(num_units = n_hidden, state_is_tuple = True) lstm_bw_cell = tf.nn.rnn_cell.DropoutWrapper(lstm_bw_cell, output_keep_prob=keep_prob) 학습할 모델을 생성합니다outputs,_ = tf.nn.bidirectional_dynamic_rnn(lstm_fw_cell,lstm_bw_cell, X, dtype = tf.float32) 기존의 lstm 과 달리 output 이 2개의 LSTMStateTuple 로 이루어져 있습니다.각 output 에 가중치를 더해서 하나의 output 으로 만들어주는 과정이 필요합니다.여기서 가장 헷갈리는 부분이 transpose 입니다. 왜 output 에 대해서 transpose를 하는 것인지 의문이 들 수 있습니다.tf.nn.bidirectional_dynamic_rnn 문서를 보시면 output 의 default 는 [batch_size,max_time,depth] 라고 나와있습니다.각각 mini batch 의 크기 그리고 time step, hidden state 의 depth 를 의미합니다.우리는 각 데이터마다 마지막 time step 의 결과값을 output 으로 선택해야 합니다.그래야지 전체 step 이 반영된 output 을 얻을 수 있습니다.outputs_fw = tf.transpose(outputs[0], [1,0,2]) outputs_bw = tf.transpose(outputs[1], [1,0,2]) pred = tf.matmul(outputs_fw[-1],w_fw) +tf.matmul(outputs_bw[-1],w_bw) + biases matmul operation 연산 속도를 위해서 다음과 같이 하나의 output 으로 먼저 합치고 전체에 대한 가중치를 주는 것이 더 좋은 방법입니다.outputs_concat = tf.concat([outputs_fw[-1], outputs_bw[-1]], axis=1) pred = tf.matmul(outputs_concat,W) + b 이하 코드는 이전의 tutorial 과 동일합니다.

기업문화 엿볼 때, 더팀스

로그인

/