커스텀 뷰의 XML 속성 파싱 라이브러리, AttributeParser 소개

분명히 블로그 조회수 1만 찍으면 mvvm에 관해 올린다고 몇일 전 언급한 적이 있지만…

도입

최근에 앱을 개발하면서 자주 커스텀 뷰를 많이 쓰게 된다.

보통 앱들 보면 타이틀 바 라던가 그런 공통적 요소가 많이 있는데, 한 번만 잘 설계해두면 xml의 속성만을 바꾸는 것으로 쉽게 되니 빠르게 개발하기엔 좋다.

그러나… XML의 속성이 10개 이상 넘어갈 경우에는 무심코 머리를 짚게 된다.

val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CombinedButton) ?: return
textPrimary = typedArray.getString(R.styleable.CombinedButton_textPrimary)
textSecondary = typedArray.getString(R.styleable.CombinedButton_textSecondary)
textPrimaryColor = typedArray.getColor(R.styleable.CombinedButton_textPrimaryColor, Color.BLACK)
textSecondaryColor = typedArray.getColor(R.styleable.CombinedButton_textSecondaryColor, Color.BLACK)
textPrimarySize = typedArray.getDimension(R.styleable.CombinedButton_textPrimarySize, resources.getDimensionPixelSize(R.dimen.combined_text_view_default_size).toFloat())
textSecondarySize = typedArray.getDimension(R.styleable.CombinedButton_textSecondarySize, resources.getDimensionPixelSize(R.dimen.combined_text_view_default_size).toFloat())
textExtraSpace = typedArray.getInt(R.styleable.CombinedButton_textExtraSpace, 1)
fontPrimaryText = typedArray.getString(R.styleable.CombinedButton_fontPrimaryText)
fontSecondaryText = typedArray.getString(R.styleable.CombinedButton_fontSecondaryText)
textPrimaryStyle = typedArray.getInt(R.styleable.CombinedButton_textPrimaryStyle, 0)
textSecondaryStyle = typedArray.getInt(R.styleable.CombinedButton_textSecondaryStyle, 0)

아마 1월 31일 쯔음에 Reflection 을 이용해서 비슷한 걸 만든 기억이 있지만, 오히려 기존에 했던 것과 그렇게 차이가 나지 않아 안 쓰게 되었다.

그러면, 좀 더 자동화되는 라이브러리를 만들어서 혼자 잘 쓰자! 라고 생각해서 심야 영화를 본 뒤 카페에서 공부하면서 작업한 결과, 오늘 소개한 AttributeParser 가 완성되게 되었다.

이론 기술

처음으로 Reflection 기반의 Annotation Processing 가 아닌, Annotation Processor 를 통한 Annotation Processing 기반으로 작성되었다. 가볍게 쓰고 + private 같은거를 싹다 무시하고 개발하기에는 Reflection가 최고이지만 각각 장단점이 있길 마련이다.

florent37의 Dagger-Auto-inject 를 포크해서 기능 추가할 때 살짝 Annotation Processor 를 공부한 적은 있지만, 처음부터 설계하고 작성하기는 처음이다.

간단히 Annotation Processor 에 대해 설명하자면, 컴파일 시간에 대상 어노테이션을 긁어 모아 클래스 파일을 생성하고, 그걸 사용하는 개념이다. Dagger, DataBinding, ButterKnife, Glide, PermissionDispatcher 등 이미 많은 라이브러리에서 사용되고 있고, 런타임 시간이 아닌 컴파일 시간에 실행되 오버헤드가 없고, 런타임에 포함되는 코드가 적거나 없고, 실제로 생성된 코드를 보며 디버깅 하기 쉽다는 등 장점이 많다.

그러면 왜 지금까지 Reflection만을 사용했는가.. 하면, annotation processor 는 컴파일 시간때 클래스 파일을 생성하므로 JavaPoet 같은라이브러리를 사용하여 자바 코드를 직접 작성해야 한다. 보통 코드를 짤 때 IDE의 지원을 받아서 실제로 키보드로 치는 코드 양은 적으나 annotation processor 를 위해 작성하는 코드는 IDE의 지원을 일절 받을 수 없는 맨땅의 코드라 처음 입문하기가 상당히 어렵다. 시간도 많이 들어가기도 하고. 그에 대비해 Reflection 는 상대적으로 빠른 시간 안에 기능을 구현할 수 있어 Reflection 을 적극적으로 사용하게 된 것이다.

그러다가 실제로 작성해보고 싶기도 했고, 자동화의 끝판왕은 자동 코드 파일 생성이라고 굳게 믿고 있기 때문에 Annotation Processor로 구현하기로 마음을 먹은 것이다.

설계

1월 31일날 만든 AttrParser 의 단점을 먼저 살펴보자.

맨 위의 TypeArray.getString() 을 반복하지 않아도 된다는 장점은 있지만…

  1. 쓸데없이 annotation 을 하나로 통합해서 type를 계속 적어줘야 한다.
  2. 같은 이유로 default value를 설정할 때, boolean 이나 float 를 int형으로 바꿔서 하고 있어 매우 헷갈린다
  3. Index를 수동으로 적어줘야 한다.

라는 단점이 있다. 개인적으로 느낀거지만 이전과 다를 바가 없다고 느꼈다.

그래서, 이번 AttributeParser 는 아래 사항을 주안점으로 삼았다.

  1. Annotation Processor 를 이용한 코드 파일 생성
  2. 각 type에 따른 Annotation 분리
  3. 자동 로그 출력 기능 (타입, 변수명, 실제 값까지)

Annotation Processor를 사용하기 위해서는 총 2개의 모듈이 필요하다. 하나는 annotation 이나 기타 클래스를 담은 일반 라이브러리와 annotation 모듈을 의존하는 compiler 모듈이다. 그리고 데모 앱에서는 이 두개 모듈을 각각 implementation, annotationProcessor (또는 kapt) 로 구성할 필요가 있다.

어노테이션들은 다음같이 구성했다.

package pyxis.uzuki.live.attribute.parser.annotation

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class AttrInt(val value: String = "", val defValue: Int = 0)

총 5줄 정도의 아주 간단한 어노테이션이다.

대충 잡은 설계로는, CustomView 어노테이션으로 커스텀 뷰 단위를 나누고, 그 안에서 Attr 어노테이션의 위치와 필드명, 타입을 찾는다. 그리고 서로 매핑하여 데이터를 가공하고,  CustomView 어노테이션이 붙은 클래스를 기반으로 하는 커스텀 클래스를 만든다. 그 안에서 각각의 값을 가지고 있기 위한 field들과 적용하기 위한 메서드들이 생성될 것이다.

컴파일러 모듈은 조금 더 많은데, 각각 어노테이션을 매핑하기 위한 모델이나 홀더, 그리고 유틸과 상수값들을 정의하고, AttributeParserProcessor 란 클래스에서 이 클래스들을 사용하여 생성하는 역할을 한다.

AttributeParserProcessor 에서는 처음으로 지정된 어노테이션이 어느 위치에 있는지 전부 찾아서 매핑한 다음, CustomView 어노테이션의 갯수만큼 반복을 돌려서 안에 있는 Attr 어노테이션 등을 파싱하는 것이다.

Reflection 과 다르게 Annotation Processor 는 런타임 코드에 바로 주입될 수 없으므로 파싱한 결과는 우선 생성된 클래스의 private static 필드로서 가지고 있고, 사용자가 apply 메서드 등을 부르면 해당 값에 설정하는 역할을 할 것이다.

이런 과정을 거쳐서 생성된 클래스가 바로 밑의 클래스이다.

package pyxis.uzuki.live.attribute.parser;

import android.content.res.TypedArray;
import android.util.AttributeSet;
import java.lang.String;
import pyxis.uzuki.live.attribute.parser.demo.R;
import pyxis.uzuki.live.attribute.parser.demo.StyleView;

public class StyleViewAttributes {
  private static boolean booleanTest;

  private static int colorTest;

  private static float dimensionTest;

  private static int intTest;

  private static float floatTest;

  private static int resourceTest;

  private static String stringTest;

  private R r;

  public static void apply(StyleView styleView, AttributeSet set) {
    apply(styleView, styleView.getContext().obtainStyledAttributes(set, R.styleable.StyleView));
  }

  public static void apply(StyleView styleView, TypedArray array) {
    bindAttributes(array);

    styleView.booleanTest = booleanTest;
    styleView.colorTest = colorTest;
    styleView.dimensionTest = dimensionTest;
    styleView.intTest = intTest;
    styleView.floatTest = floatTest;
    styleView.resourceTest = resourceTest;
    styleView.stringTest = stringTest;
  }

  public static void printVariables() {
    android.util.Log.d("StyleView", "==================== StyleView ====================" + 
    "\nboolean booleanTest = " + booleanTest +  
    "\nint colorTest = " + colorTest +  
    "\nfloat dimensionTest = " + dimensionTest +  
    "\nint intTest = " + intTest +  
    "\nfloat floatTest = " + floatTest +  
    "\nint resourceTest = " + resourceTest +  
    "\njava.lang.String stringTest = " + stringTest +  
    "\n====================================================");
  }

  private static void bindAttributes(TypedArray array) {
    if (array == null) return;

    booleanTest = array.getBoolean(R.styleable.StyleView_booleanTest, false);
    colorTest = array.getColor(R.styleable.StyleView_colorTest, 0);
    dimensionTest = array.getDimension(R.styleable.StyleView_dimensionTest, 0.0f);
    intTest = array.getInt(R.styleable.StyleView_intTest, 0);
    floatTest = array.getFloat(R.styleable.StyleView_floatTest, 0.0f);
    resourceTest = array.getResourceId(R.styleable.StyleView_resourceTest, 0);
    stringTest = array.getString(R.styleable.StyleView_stringTest);

    array.recycle();
  }
}

물론, 사용자가 위의 클래스를 신경 쓸 필요는 전혀 없다. 단순히 apply 에 필요한 파라미터만 주입해주면 값이 설정되는 구조이다.

마무리

평소에 그나마 공부를 해왔던 덕분에, 실제로 만드는 시간은 약 7시간 정도 걸린 것 같다.

실제로 써보니 Reflection 보다 강력한 부분도 있었고, 좀 안 좋은 부분도 있었지만 설계만 잘 하면 오래토록 쓸 수 있을 것 같다.

마지막으로, 오늘 만든 라이브러리는 당연하겠지만 Github에 업로드 되어있다.

WindSekirun/AttributeParser

bintray 에 업로드도 요청했으니, 아마 오늘 밤쯤에는 올라가지 않을까 싶다.

제로부터 시작하는 NextCloud 설치하기 on VPS

NextCloudDropbox 와 비슷하게 파일 호스팅 서비스를 제공하는 솔루션인데, 개인 서버에도 설치할 수 있다.

오늘 퇴근하고 나서 약 5시간 동안 삽질 끝에 설치에 성공했는데, 겸사겸사 정리도 해보려고 한다.

환경

OS는 Ubuntu 16.04 LTS 를 사용할 것이며, 아래 조건을 만족해야 한다. (예제에서는 16.10을 사용한다.)

  1. SSH 접속 가능한 상태
  2. 터미널에 두려움을 가지지 않는 용기
  3. 설명이 없어도 알아서 구글링하는 용기

목적

제목대로, 제로부터 NextCloud 구축까지 시행할 것이다.

즉, 아무것도 없는 상태에서 Nginx, PHP-FPM, MariaDB, Let’s Encrypt SSL 연결, NextCloud 까지 전부 설정을 마칠 것이다.

작업 예상시간은 1시간 이하이다.

참고로, [ ] 로 감싸진 것의 경우 자기가 원하는 정보로 교체하면 된다.

0. VPS 구매하기

VPS 는 웹호스팅과 서버호스팅의 중간 형태로, 하나의 물리 서버를 여러 개의 가상 서버로 나누어 사용하면서 각각의 가상 서버를 독립적으로 운영할 수 있게 하는 것이다.

여기서는 Vultr 에 있는 UzukiLive 서버에서 진행한다. 옵션은 아래와 같다.

NextCloud를 어느정도까지 사용할 것에 따라 다르지만, 보통 2 CPU 에 16GB 이상이 150명까지 환경에 적당하다고 한다.

Deployment Recommendation 에 어느정도 나와있으니 자기의 목적에 맞는 VPS 를 찾아보자.

나는 적어도 혼자나 많아봤자 5~6명일거라서, 기존에 $5 옵션 이었던 1 CPU 1GB 에서 1 CPU 2GB 로 증설했다.

1. 루트 유저 추가하기

초기 서버 설정으로 관리용 계정을 root 이외에 하나 더 만드는 것이다.

마음에 드는 터미널로 접속하자.

ssh root@***.***.***.***

비밀번호는 Vultr 기준 인스턴스 메인에 있다.

adduser [pyxis]

유저를 추가하는 명령어이다.

그러면 비밀번호와 정보를 물어볼텐데, 적당히 채워주고 생성을 마친다.

usermod -aG sudo [pyxis]

그리고 방금 추가한 유저에게 root 권한을 부여한다.

이렇게 해서 루트 유저 추가가 끝났고, 만든 유저로 로그인하자.

ssh pyxis@***.***.***.***

이런 메세지가 표시되면 성공한 것이다.

2. Nginx

Nginx Web Server 로, Apache 를 쓸 수도 있었지만 개인적으로는 Nginx 가 좀 더 친숙했으므로 Nginx 를 사용하려고 한다.

sudo apt-get -y install nginx

위 명령어로 Nginx 를 설치한다.

한 가지 팁으로, sudo 를 일일히 치기 귀찮다면 sudo -i로 root 모드로 들어간 상태에서 진행할 수 있다.

설치를 다 한다음, sudo nano /etc/nginx/nginx.conf를 입력해서 에디터를 연다.

user www-data;
worker_processes 8;
pid /run/nginx.pid;
 
events {
    worker_connections 768;
    # multi_accept on;                                                                                                                                                                                                                   
}
 
http {
 
    ##                                                                                                                                                                                                                                   
    # Basic Settings                                                                                                                                                                                                                     
    ##                                                                                                                                                                                                                                   
 
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;                                                                                                                                                                                                                 
 
    # server_names_hash_bucket_size 64;                                                                                                                                                                                                  
    # server_name_in_redirect off;                                                                                                                                                                                                       
 
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

worker_processes 값과 server_tokens 값을 변경하는데, 각각 설명은 다음과 같다.

  • worker_processes: 하나의 worker 가 동시에 실행할 수 있는 Thread 수이다.
  • server_tokens: 서버 버전 정보 표시 금지

참고로 nano 의 사용법을 몰라도 이 세개만 기억하면 된다.

  • 방향키: 이동, 백스페이스: 지우기
  • control + K : 라인 지우기
  • control + X : 저장

변경한 후, Nginx 의 서비스를 재시작해주자.

sudo systemctl restart nginx.service

3. NextCloud 다운로드

현재 최신버전은 12.0.3 이다.

cd /var/www
sudo wget https://download.nextcloud.com/server/releases/nextcloud-12.0.3.zip
unzip nextcloud-12.0.3.zip
rm -rf nextcloud-12.0.3.zip

이런 구조로 되면 된다.

4. NextCloud 폴더에 권한 설정

sudo adduser nextcloud
sudo chown -R nextcloud:www-data /var/www/nextcloud
sudo chmod -R o-rwx /var/www/nextcloud

nextcloud 유저를 만든 다음, 해당 유저 및 www-data 그룹을 방금 압축을 푼 /var/www/nextcloud 에 권한을 설정한다.

5. PHP-FPM 설정

5.6 에 비해 7.0에서 좀 더 php의 속도가 증가했으므로 7.0을 설치하기를 추천한다.

sudo apt-get -y install php-cli php-json php-curl php-imap php-gd php-mysql php-xml php-zip php-intl php-mcrypt php-imagick php-mbstring
sudo apt-get install -y php-fpm

설치가 다 되면 /etc/php/7.0/fpm/pool.d 폴더에 nextcloud 전용 설정 파일을 만든다.

[nextcloud]
listen = /var/run/nextcloud.sock
 
listen.owner = nextcloud
listen.group = www-data
 
user = nextcloud
group = www-data
 
pm = ondemand
pm.max_children = 30
pm.process_idle_timeout = 60s
pm.max_requests = 500
 
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

다 만들었으면 php 서비스를 재시작하자.

sudo systemctl restart php7.0-fpm.service

6. MariaDB 설치

sudo apt-get install -y mariadb-server mariadb-client

MySQL 를 사용해도 상관없다.

$ sudo mysql_secure_installation
 
Set root password? [Y/n] Y
Remove anonymous users? [Y/n] Y
Disallow root login remotely? [Y/n] Y
Remove test database and access to it? [Y/n] Y
Reload privilege tables now? [Y/n] Y

7. Nextcloud 데이터베이스 설정

sudo mysql -u root -p
MariaDB> CREATE DATABASE nextcloud;
Query OK, 1 row affected (0.00 sec)

MariaDB> CREATE USER "nextcloud"@"localhost";
Query OK, 0 rows affected (0.00 sec)
 
MariaDB> SET password FOR "nextcloud"@"localhost" = password('[PASSWORD]');
Query OK, 0 rows affected (0.00 sec)
 
MariaDB> GRANT ALL PRIVILEGES ON nextcloud.* TO "nextcloud"@"localhost" IDENTIFIED BY "[PASSWORD]";
Query OK, 0 rows affected (0.00 sec)
 
MariaDB> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
 
MariaDB> EXIT
Bye

PASSWORD 는 마지막 NextCloud 설정할 때 필요하니 잘 기억해두자.

8. Nginx 설정 파일 만들기

사실 하나하나 추가해야 되지만, 여기서는 완전한 파일만 기재한다.

/etc/nginx/sites-available 폴더에 nextcloud 란 이름으로 파일을 만든다.

upstream php-handler {

    server unix:/var/run/nextcloud.sock;
}

server {

    listen 80;
    listen [::]:80;
    server_name [cloud.uzuki.live];

    root /var/www/nextcloud/;

    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Robots-Tag none;
    add_header X-Download-Options noopen;
    add_header X-Permitted-Cross-Domain-Policies none;
    add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains;';

    location = /robots.txt {

        allow all;
        log_not_found off;
        access_log off;
    }

    location = /.well-known/carddav {

        return 301 $scheme://$host/remote.php/dav;
    }

    location = /.well-known/caldav {

        return 301 $scheme://$host/remote.php/dav;
    }

    client_max_body_size 512M;
    fastcgi_buffers 64 4K;

    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    location / {

        rewrite ^ /index.php$uri;
    }

    location ~ ^/.well-known/acme-challenge/* {

        allow all;
    }

    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {

        deny all;
    }

    location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {

        deny all;
    }

    location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {

        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param modHeadersAvailable true;
        fastcgi_param front_controller_active true;
        fastcgi_pass php-handler;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
    }

    location ~ ^/(?:updater|ocs-provider)(?:$|/) {

        try_files $uri/ =404;
        index index.php;
    }

    location ~* \.(?:css|js|woff|svg|gif)$ {

        try_files $uri /index.php$uri$is_args$args;
        add_header Cache-Control "public, max-age=7200";
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Robots-Tag none;
        add_header X-Download-Options noopen;
        add_header X-Permitted-Cross-Domain-Policies none;

        access_log off;
    }

    location ~* \.(?:png|html|ttf|ico|jpg|jpeg)$ {

        try_files $uri /index.php$uri$is_args$args;
        access_log off;
    }
}
sudo ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled/nextcloud
nginx -t

심볼릭 링크를 생성하고 설정 파일이 문법에 맞는지 확인한다.

이와 같이 나온다면 재시작을 해주자.

sudo systemctl restart nginx.service
sudo systemctl restart php7.0-fpm.service

9. Let’s Encrypt로 SSL 인증받기

이 작업을 위해서는 도메인이 필요하다. NextCloud 는 SSL 이하 사용을 권장하므로 Self-Signing 라도 해야되지만 SSL 인증서를 구매하거나 적어도 Let’s Encrypt 로 인증을 거쳐야 한다.

sudo apt-get install -y software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install -y certbot
sudo certbot certonly --webroot -w /var/www/nextcloud --agree-tos --email [pyxis@uzuki.live] -d [cloud.uzuki.live] --rsa-key-size 4096

인증이 완료되었다고 나오면 dhparams 키도 추가로 생성해주자.

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
sudo chmod 600 /etc/ssl/certs/dhparam.pem

참고로 꽤나 오래 걸리므로 커피 한잔 마시고 오는 것이 좋다.

10. SSL 설정 반영하기

/etc/nginx/sites-available/nextcloud 를 수정한다.

Let’s Encrypt 로 받은 pem 키와 위에서 생성한 dhparam 키를 각각 넣어준다.

upstream php-handler {

    server unix:/var/run/nextcloud.sock;
}

server {

    listen 80;
    listen [::]:80;
    server_name [cloud.uzuki.live];
    return 301 https://$server_name$request_uri;
}

server {

    listen 443 ssl;
    listen [::]:443 ssl;
    server_name [cloud.uzuki.live];

    root /var/www/nextcloud/;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/[cloud.uzuki.live]/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/[cloud.uzuki.live]/privkey.pem;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 1440m;
    ssl_buffer_size 8k;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !kECDH !DSS !MD5 !EXP !PSK !SRP !CAMELLIA !SEED';
    ssl_prefer_server_ciphers on;

    ssl_trusted_certificate /etc/letsencrypt/live/cloud.uzuki.live/chain.pem;
    ssl_stapling on;
    ssl_stapling_verify on;

    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Robots-Tag none;
    add_header X-Download-Options noopen;
    add_header X-Permitted-Cross-Domain-Policies none;
    add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains;';

    location = /robots.txt {

        allow all;
        log_not_found off;
        access_log off;
    }

    location = /.well-known/carddav {

        return 301 $scheme://$host/remote.php/dav;
    }

    location = /.well-known/caldav {

        return 301 $scheme://$host/remote.php/dav;
    }

    client_max_body_size 512M;
    fastcgi_buffers 64 4K;

    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    location / {

        rewrite ^ /index.php$uri;
    }

    location ~ ^/.well-known/acme-challenge/* {

        allow all;
    }

    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {

        deny all;
    }

    location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {

        deny all;
    }

    location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\.php(?:$|/) {

        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param HTTPS on;
        #Avoid sending the security headers twice
        fastcgi_param modHeadersAvailable true;
        fastcgi_param front_controller_active true;
        fastcgi_pass php-handler;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
    }

    location ~ ^/(?:updater|ocs-provider)(?:$|/) {

        try_files $uri/ =404;
        index index.php;
    }

    location ~* \.(?:css|js|woff|svg|gif)$ {

        try_files $uri /index.php$uri$is_args$args;
        add_header Cache-Control "public, max-age=7200";
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Robots-Tag none;
        add_header X-Download-Options noopen;
        add_header X-Permitted-Cross-Domain-Policies none;
        # Optional: Don't log access to assets
        access_log off;
    }

    location ~* \.(?:png|html|ttf|ico|jpg|jpeg)$ {

        try_files $uri /index.php$uri$is_args$args;
        access_log off;
    }
}
sudo systemctl reload nginx.service

위 설정들은 SSL Test를 A+ 로 통과할 수 있는 설정이나 필요한 조건에 따라 더 추가할 수 있다.

11. 인증서 자동 갱신

Let’s Encrypt 는 무료인 대신 90일 마다 갱신을 해줘야 한다. crontab 로 자동으로 갱신하도록 설정해준다.

crontab -e
42 23 * * 1 /usr/bin/certbot renew >> /var/log/le-renew.log

12. http2

기존 http 1.1 을 개선한 http의 새로운 프로토콜로, 여러 방법을 사용하여 지연 시간을 감소시킨 프로토콜이다.

위 11번의 설정 파일에서 16 ~ 17줄 부분에 교체만 해주면 된다.

listen 443 ssl http2;
listen [::]:443 ssl http2;

13. 타임아웃 설정

1. /etc/php/7.0/fpm/pool.d/nextcloud.conf 맨 밑 request_terminate_timeout = 300 추가

2. /etc/nginx/sites-available/nextcloud 의 105번째 줄 밑에 fastcgi_read_timeout 300; 추가

14. 접속

마지막으로 server_name 로 설정한 주소로 들어가보면 설정 마법사가 나온다.

이미지 출처: https://www.pcextreme.nl/community/d/153-set-up-your-own-cloud-storage-within-minutes-using-nextcloud-and-aurora-s3

맨 위 두개 필드에는 아이디와 비번을, 그 다음에는 파일을 보관할 장소를 적는다.

마지막 4개 필드에는 7번에서 설정한 데이터베이스 정보를 넣는데, 각각 유저 이름, 비밀번호, 데이터베이스 이름, 주소를 넣는다.

만일 finish setup 에서 넘어가지 않다면 맨 밑 localhost 를 localhost:3306 으로 변경하면 되는 것 같다.

15. 끝

총 15단계에 걸쳐서 설치가 끝났다.

추가적으로 OPCache, redis 를 설치하면 더 속도가 상승되지만 그건 나중에 언급해도 괜찮을 것 같다.

 

Migrate CircleCI 1.0 to CircleCI 2.0 (CI/CD for Android)

 

CircleCI 2.0이 나온지 아마 3개월 정도 지났는데,  슬슬 1.0에서 올려야 될 타이밍인 것 같아서 CircleCI 1.0 에서 2.0으로 올리는 방법을 정리하려고 한다

1.0과 2.0의 차이점

기존 CircleCI 1.0에 비해 2.0은 이미 빌드되어 있는 Docker Image를 사용한다. 그와 동시에 캐시 기능도 확실히 제공하기 때문에 1.0에 비해 더 빠른 속도를 제공한다.

왼쪽이 1.0, 오른쪽이 2.0인데 확실히 봐도 속도가 빨라졌음을 알 수 있다.

마이그레이션 방법

기존 circle.yml 삭제

general:
    artifacts:
        - /home/ubuntu/RichUtilsKt/app/build/outputs/apk/

machine:
    environment:
        ANDROID_HOME: /usr/local/android-sdk-linux
        GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"'

dependencies:
    pre:
        - echo y | android update sdk --no-ui --all --filter tools,platform-tools,android-26
        - echo y | android update sdk --no-ui --all --filter build-tools-26.0.2
    override:
        - ANDROID_HOME=/usr/local/android-sdk-linux

compile:
    override:
       - (./gradlew dependencies):
                  timeout: 360

test:
    override:
        - (./gradlew test):
            timeout: 360

단 기존 환경 기준으로 API 26을 사용했다는 것 만큼은 기억해두고 지우자.

.circleci/config.yml 파일 생성

2.0에서 부터는 설정 파일이 .circleci 란 폴더 안에 config.yml 로서 존재하게 되었다.

version: 2
jobs:
  build:
    working_directory: ~/RichUtilsKt
    docker:
      - image: circleci/android:api-26-alpha
    environment:
      JVM_OPTS: -Xmx3200m
    steps:
      - checkout
      - run :
          name: display directory
          command: ls -la
      - restore_cache:
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Download Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars-{{ checksum "build.gradle" }}-{{ checksum  "app/build.gradle" }}
      - run:
          name: Run Tests
          command: ./gradlew lint test
      - store_artifacts:
          path: app/build/reports
          destination: reports
      - store_test_results:
          path: richutils/build/test-results

조금 길은데, 하나씩 살펴보자.

  • version: 2.0 이므로 2를 적는다.
  • jobs: 시행할 작업을 나열할 곳이다.
  • working_directory: 작업이 실행될 장소. 보통 프로젝트의 루트 폴더명이다.
  • docker image: 이미 빌드되어 있는 안드로이드 이미지를 적어넣는다. Docker Hub에 따르면 api-23 부터 api-26 까지 있다. 만일 7.0이 대상이라면 api-25 를 적어넣으면 된다.
  • environment: 자바 힙 최대 용량을 3200mb로 설정한다.
  • steps: run, restore_cache, checkout, save_cache 등 문서상에 정의된 내용을 나열하는 곳이다.
  • checkout: git checkout. 더 이상 자세한 설명은 생략한다.
  • run : shell commend 를 실행하는 명령어이다. name 로는 CircleCI 페이지에 표시될 제목, commend 에는 실제 명령어를 작성한다.
  • restore_cache: 주어진 key 로부터 캐시를 불러오는 것이다. 여기서는 ./gradlew androidDependencies 까지를 캐시로 저장한다.
  • save_cahce: 주어진 경로를 캐시하는 기능이다.
  • store_artifacts: 빌드 리포트를 올리는 기능이다.
  • store_test_results: 테스트 결과 리포트를 올리는 기능이다.

여기서 주의할 점은 checksum, store_artifacts, store_test_results 경로를 제대로 설정해야 하는데, 대소문자 구별하는 듯 하다. 실제 파일이 있는 경로를 가리키도록 설정하면 된다.

마지막으로 저장하고 커밋하면 CircleCI 에 빌드 트리거가 걸리고 빌드가 시작된다.

정리

private repo 같은 경우 1x container, 오픈소스 저장소의 경우 4x container 가 지원되는 것 같으니, 앞으로 나오는 왠만한 저장소에 부착하려고 한다.

가끔 정신 나간 상태로 코딩하다 보면 CircleCI 쪽에서 실패 메일이 오게 되면 그때서야 정신을 차리는 일이 다반사이기 때문이다(…