Cannot resolve DataBindingComponent after migrate to AndroidX

문제

Databinding + Kotlin을 사용하는 Android Studio 버전을 3.2로 올리고 ‘Migrate to AndroidX’ 를 실행하면 DataBindingComponent 객체를 찾을 수 없다면서 아래와 같은 로그가 뜬다.

error: cannot generate view binders java.lang.NullPointerException
  	at android.databinding.tool.store.SetterStore.getMatchingMultiAttributeSetters(SetterStore.java:615)
  	at android.databinding.tool.store.SetterStore.getMultiAttributeSetterCalls(SetterStore.java:502)
  	at android.databinding.tool.BindingTarget.resolveMultiSetters(BindingTarget.java:220)
  	at android.databinding.tool.LayoutBinder.<init>(LayoutBinder.java:257)
  	at android.databinding.tool.DataBinder.<init>(DataBinder.java:58)
  	at android.databinding.tool.CompilerChef.ensureDataBinder(CompilerChef.java:114)
  	at android.databinding.tool.CompilerChef.sealModels(CompilerChef.java:348)
  	at android.databinding.annotationprocessor.ProcessExpressions.writeResourceBundle(ProcessExpressions.java:233)
  	at android.databinding.annotationprocessor.ProcessExpressions.onHandleStep(ProcessExpressions.java:128)
  	at android.databinding.annotationprocessor.ProcessDataBinding$ProcessingStep.runStep(ProcessDataBinding.java:212)
  	at android.databinding.annotationprocessor.ProcessDataBinding$ProcessingStep.access$000(ProcessDataBinding.java:197)
  	at android.databinding.annotationprocessor.ProcessDataBinding.doProcess(ProcessDataBinding.java:98)
  	at android.databinding.annotationprocessor.ProcessDataBinding.process(ProcessDataBinding.java:73)
  	at org.jetbrains.kotlin.kapt3.base.ProcessorWrapper.process(annotationProcessing.kt:99)

해당 DataBindingComponent 객체는 프로젝트에 Databinding이 활성화 되있고, @BindingAdapter 어노테이션을 사용할 경우에 생성되는 클래스이다.
이 문제는 AndroidX 를 적용한 프로젝트 A와 A가 의존성을 가지는 라이브러리 프로젝트에서 AndroidX를 사용하지 않아 발생하는 문제로, 쉽게 말해 패키지 충돌이라 보면 된다.

실제로 AndroidX를 사용하지 않을 경우 DataBindingComponent 의 패키지는 android.databinding.DataBindingComponent 이고, AndroidX를 사용할 경우에는 androidx.databinding.DataBindingComponent가 된다.

해결 방법

Studio 에서 Project View를 ‘Project’에 맞춘 다음, ‘External Libraries’ 항목을 클릭하면 해당 프로젝트가 의존하고 있는 모든 라이브러리 프로젝트가 나온다. 이 라이브러리 중 Databinding를 활성한 라이브러리를 찾고, 그 중 AndroidX를 사용하지 않는 라이브러리를 찾으면 된다.

이 방법이 번거로운 경우에는, 해당 모듈의 build.gradle 에서 하나씩 지워보면서 확인해도 된다.

본인 같은 경우에는 두 개의 라이브러리가 이 대상이 되었는데, 하나는 프로젝트의 코어 프로젝트인 BaseApp 와 다른 하나는 WindSekirun/BindAdapters 에서 발생하고 있었다.
따라서 두 개의 라이브러리 코드에서 AndroidX로 마이그레이션하고, 마이그레이션한 버전을 다시 해당 프로젝트에 적용시키니 적용이 되었다.

커스텀 뷰의 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 를 설치하면 더 속도가 상승되지만 그건 나중에 언급해도 괜찮을 것 같다.