2017년 9월 1일 금요일

Android에서 타이틀바(TitleBar) 없애기

 안드로이드 액티비티의 기본 타이틀바가 사실 그대로 사용해도 문제가 없을 만큼 다양한 표현이 가능하거나 미세한 조정이 되지 않기 때문에 대부분 분위기에 맞지 않는 기본 타이틀바를 완전히 없애거나 다른 이미지로 교체하려는 경우가 대부분이다.

 인터넷을 검색해보면 많은 방법을 제시하지만 대부분 안드로이드가 제공하는 테마 기능의 사용을 어렵게 만드는 방법 뿐이다. 즉 AndroidManifest.xml에서 타이틀바가 없는 테마로 기본 테마를 교체하거나 코드에서 테마를 직접 지정하는 것인데 별로 좋은 방법은 아니다.

 그래서 인터넷에 떠도는 몇 가지 방법을 정리해보고 문제점과 최선의 방법을 정리해볼까 한다.  자세히 읽어보고 가장 좋은 방법이 무엇인지 각자 생각해보면 좋겠다. 참고로 나의 경우에는 마지막 방법이 가장 합리적이고 좋은 방법이라고 생각한다.

1. AndroidManifest.xml 파일에서 테마 지정

 안드로이드 프로젝트를 생성하면 기본적으로 AndroidManifest.xml 파일이 생성되고 그 안에는 <application> 태그가 하나 있으며 android:theme 속성이 선언되어있다. 기본적으로 이 속성에는 기본 생성된 style의 AppTheme가 지정되어있지만 기본 테마는 타이틀바가 표시되도록 되어있기 때문에 이 값을 "@android:style/Theme.NoTitleBar"로 바꿔서 타이틀바를 없애는 것이다.


 이 방법은 application의 전체 테마를 완전히 교체하는 것이다. 이미 개발이 어느 정도 진행 중이라면 기존에 지정되어있던 테마의 값을 알게 모르게 사용하고 있을 것이므로 아마도 개발이 어느 정도 진행된 후에 이 값을 바꾸면 앱이 죽거나 글자 색상이나 배경색 등이 완전히 바뀌어 보이는 부작용이 생긴다. 앱이 실행되지 않고 죽는 건 SDK가 버전업되면서 기본적으로 상속받는 Activity 클래스가 다른 파생 클래스로 바뀌면서 이전 버전과 달라졌기 때문이다. 예외를 발생시키는 <activity> 태그의 속성으로 android:theme="@style/Theme.AppCompat"를 지정해주면 해결된다.

 참고로 "@android:style/Theme.NoTitleBar" 테마는 타이틀바를 없애는 속성 말고는 아무것도 선언되지 않은 테마이다. 아마도 전에 있던 색상 값들이 모두 사라지면서 기본 컨트롤들의 색상이 확 바뀌었을 것이다.

  타이틀바도없고 상태바도 없는 Full Screen을 사용하는 테마는 "@android:style/Theme.NoTitleBar.Fullscreen"이므로 기억해두도록 하자.

 <application> 태그 말고 특정 <activity> 태그에 테마를 지정해서 사용할 수도 있다. 같은 속성을 <activity> 태그에 지정하면 된다. 이 경우에는 activity 레벨로 적용된다.

2. requestWindowFeature() 메소드 호출

 Activity의 onCreate() 메소드에서 requestWindowFeature(Window.FEATURE_NO_TITLE)를 호출해주는 방법이다.


 위와 같이 호출해주면 되는데 이 방법은 Activity 클래스를 상속하고 있는 경우에는 동작하지만 AppCompatActivity같은 파생 클래스를 상속하는 경우에는 동작하지 않는다. 예전 SDK의 경우에는 기본 생성되는 프로젝트 템플릿들의 액티비티가 Activity 클래스를 상속했지만 요즘 SDK가 버전업이 되면서 AppCompatActivity등 다른 Activity 파생 클래스를 상속하는 경우가 많으므로 만약 동작하지 않는다면 Activity 클래스로 바꿔서 해보면 동작할 것이다. 하지만 다른 Activity 클래스를 상속하도록 하는데는 다 이유가 있으므로 구버전만 지원할 게 아니라면 이 방법은 추천하지 않는다.

3. setTheme() 메소드 호출

 이 방법도 Activity의 onCreate() 메소드에서 setTheme(android.R.style.Theme_NoTitleBar)를 호출하는 것이다.


 위 그림과 같이 호출해주면 된다. 위의 2번 방법과 다른점은 Activity의 테마를 완전히 교체한다는 것이다. 1번에서 테마를 바꾼 것처럼 완전히 확 바뀐 색상의 Activity의 모습을 보게될 것이다.

 게다가 이 방법은 2번의 문제점도 똑같이 적용된다. Activity를 그대로 상속한 경우에는 잘 동작하는데 AppCompatActivity 등의 다른 파생 클래스를 상속한 경우에는 동작하지 않는 경우가 있다. 마찬가지로 Activity로 바꿔서 해보면 동작은 하겠지만 1번과 2번의 문제점을 동시에 가지고 있는 만큼 이 방법도 사용을 권하지 않는다.

4. 기본 테마에 style 추가 (추천)

 내가 추천하는 가장 안전하고 간단한 방법이다. 아마도 이 방법이 가장 정확한 방법일 것이다. 위의 방법들은 예전에는 먹히는 방법들이었지만 그럼에도 불구하고 사실 편법이었을 뿐이다.

 방법은 간단하다. 프로젝트 생성 시 기본적으로 생성된 style.xml 파일을 열어서 아래 그림과 같이 <item> 하나를 추가하는 것이다.


 사실 이 <item> 태그가 @android:style/Theme.NoTitleBar 테마가 가진 전부이다. 따라서 이 item 태그만 기본 style에 추가해주면 다른 모든 style은 그대로 이고 타이틀바 제거 속성만 enable 시키게 된다. 즉, 이 item을 추가한 후에도 타이틀바 외에 모든 색상, 스타일 등은 그대로 유지된다. 게다가 최신 SDK에서도 전혀 문제가 없다. 인터넷에 떠도는 수많은 정보들은 이 방법을 잘 다루지 않는다. 하지만 가장 제대로 동작하는 방법이다.


2017년 8월 19일 토요일

MySQL 기본 명령어 - 사용자 및 권한 관리

MySQL을 설치하고 나서 뭘 하려고 하면 항상 인터넷에서 검색부터하는 게 지겨워서 이것도 정리해 놓는다. MySQL 콘솔에서 실행하는 몇 가지 자주 사용하는 명령어들이다.

root 권한으로 mysql 콘솔 실행

$ mysql -u root -p

database 조회

mysql> show databases;

database 선택

mysql> use database;

현재 등록된 사용자 조회

mysql> use mysql;
mysql> select user, host from user;

사용자 추가

mysql> create user userid@localhost identified by 'password';
mysql> create user 'userid'@'%' identified by 'password';
 %는 wildcard의 의미로서 any host로부터의 접속을 허용한다는 뜻이다.

사용자 삭제

mysql> drop user 'userid';

DB에 대한 사용자 권한 부여

 모든 권한 부여
mysql> grant all privileges on dbname.table to userid@host identified by 'password';
mysql> flush privileges; 
 특정 권한 부여
mysql> grant select, insert, update on dbname.table to userid@host identified by 'password';
mysql> flush privileges; 
dbname.table 대신에 dbname.*을 지정하면 db의 모든 테이블에 대한 권한 설정.

권한 삭제

mysql> revoke all on dbname.table from userid@host;
mysql> flush privileges; 

권한 조회

mysql> show grants for userid@host;

2017년 8월 16일 수요일

Raspberry Pi 간단 명령어 정리

Raspberry Pi를 사용하면서 자주 찾게되는 명령어들을 간단히 정리하려고 한다. 자주 사용하는 명령어라고 생각되는 명령어가 생길 때마다 종종 업데이트할 계획이다.

설치 패키지 관리

# 패키지 목록 업데이트
$ sudo apt-get update

# 설치된 패키지 업그레이드 수행
$ sudo apt-get upgrade

# 사용되지 않는 패키지 삭제
$ sudo apt-get autoremove

# 다운로드된 아카이브 파일 삭제
$ sudo apt-get clean

# 다운로드된 구버전 아카이브 파일 삭제
$ sudo apt-get autoclean

# 패키지 설치
$ sudo apt-get install pkg_name1 [pkg_name2 pkg_name3 ...]

# 패키지 삭제
$ sudo apt-get remove pkg_name1 [pkg_name2 pkg_name3 ...]

# 모든 패키지 목록 보기
$ apt list

# 설치된 패키지 목록 보기
$ apt list --installed

사용자 관리

# 사용자 추가
$ sudo adduser user_id

# 그룹에 사용자 추가
$ sudo adduser user_id group_name

# 계정이 속한 그룹 조회
$ groups user_id

# 사용자 삭제 (home directory도 같이 삭제)
$ sudo deluser --remove-home user_id

# 비밀번호 변경
$ passwd

2017년 8월 14일 월요일

Raspberry Pi WiFi 설정하기

 Raspberry Pi 3에는 무선랜이 기본 탑재라서 WiFi 설정을 해 보았다. 설정 중에 겪은 몇 가지 문제점과 설정 방법을 여기서 다뤄보려고 한다.

무선랜 장치 조회

 아래와 같이 iwconfig 명령을 실행해보면 기본 탑재된 무선랜 장치는 wlan0임을 알 수 있다.
pi@raspberrypi:/home $ iwconfig
lo        no wireless extensions.

wlan0     IEEE 802.11  ESSID:"shanpark"  
          Mode:Managed  Frequency:2.447 GHz  Access Point: 64:E5:99:A3:6F:2C   
          Bit Rate=72.2 Mb/s   Tx-Power=31 dBm   
          Retry short limit:7   RTS thr:off   Fragment thr:off
          Power Management:on
          Link Quality=68/70  Signal level=-42 dBm  
          Rx invalid nwid:0  Rx invalid crypt:0  Rx invalid frag:0
          Tx excessive retries:0  Invalid misc:0   Missed beacon:0

eth0      no wireless extensions.

무선 네트워크 검색

 sudo iwlist wlan0 scan 명령을 실행하면 검색된 무선 네트워크 리스트가 여러 개 출력된다.
pi@raspberrypi:/home $ sudo iwlist wlan0 scan
wlan0     Scan completed :
          Cell 01 - Address: 64:E5:99:A3:6F:2C
                    Channel:8
                    Frequency:2.447 GHz (Channel 8)
                    Quality=70/70  Signal level=-40 dBm  
                    Encryption key:on
                    ESSID:"wlanessid"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=0000000000000000
                    Extra: Last beacon: 30ms ago
                    IE: Unknown: 00087368616E7061726B
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030108
                    IE: Unknown: 2A0100
                    IE: Unknown: 32043048606C
                    IE: Unknown: 2D1A6E181EFFFF0000000000000000000000000000
                    IE: Unknown: 3D1608050000000000000000000000000
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : PSK
                    IE: Unknown: DD180050F2020101800003A4000027A40062322F00
                    ...
 출력된 여러 값들 중에 ESSID 값을 보면 자신이 연결할 무선 네트워크가 검색되는 지 확인할 수 있다. 여러 번 시도해보고 검색이 되지 않으면 연결이 안된다는 뜻이므로 검색이 되도록 무선 공유기를 확인해보거나 환경을 다시 한 번 체크할 필요가 있다.

 여기서 별 문제가 없더라도 검색이 안되는 문제의 원인으로 두 가지를 설명하고자 한다.

 첫 번째로 raspi-config를 실행해서 Localisation Options 메뉴에서 Wi-fi Country 설정을 하지 않도록 한다. 이것을 설정하지 않아도 아무 문제가 없는데 설정을 하면 무선 네트워크 검색이 되지 않는다. 하나도 검색이 안된다면 이 문제일 가능성이 있다. 만약 이 경우라면 /etc/wpa_supplicant/wpa_supplicant.conf 파일의 맨 첫 줄에 자신이 지정한 나라의 코드 값이 보일 것이다. 첫 줄을 삭제하고 리부팅 후 다시 검색을 시도해보면 검색 결과가 출력될 것이다.

 두 번째로 특정 무선 네트워크가 검색이 안된다면 그 무선 네트워크의 channel이 12 또는 13인지 확인해보자. R Pi의 펌웨어에 문제가 있어서 channel 12 또는 13으로 설정된 무선 네트워크는 검색이 되지 않는다. 자신의 무선 네트워크가 검색이 되지 않으면 channel 값이 1 ~ 11 인지 확인해보고 아니라면 1 ~ 11 사이의 값으로 설정하도록 한다.

무선 네트워크 설정

 검색된 무선 네트워크 설정을 위한 설정값을 wpa_passphrase 명령으로 만든다. 아래 명령에서 wlanessid는 ESSID값을, password는 무선 네트워크의 패스워드를 넣고 명령을 실행하면된다.
pi@raspberrypi:/home $ wpa_passphrase wlanessid password
network={
 ssid="wlanessid"
 #psk="password"
 psk=8b784be81fb45f16ff506fde4013f50c018f832dec02810541d490aae9ab0b7a
}

 명령을 실행하고나서 화면에 출력되는 값, 즉 network={....}을 /etc/wpa_supplicant/wpa_supplicant.conf 파일의 끝에 넣어주면 설정은 끝이다. 여기서 #psk=xxxx 라인은 주석이므로 제거해도 된다. 비밀번호가 직접 보이는 건 보안상 좋지 않으므로 반드시 제거하는 걸 추천한다.

 참고로 최종 설정된 /etc/wpa_supplicant/wpa_supplicant.conf 파일의 내용을 보면 일반적으로 아래와 같다.
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
 ssid="wlanessid"
 psk=8b784be81fb45f16ff506fde4013f50c018f832dec02810541d490aae9ab0b7a
}

무선 네트워크 연결 확인

 대충 설정이 다 됐다면 속편하게 리부팅을 하고 터미널에서 ifconfig 명령을 실행해보자. 정상적으로 연결이 되었다면 wlan0 장치에 ip 주소가 설정되어있을 것이다.
pi@raspberrypi:/home $ ifconfig
eth0      ...

lo        ...

wlan0     Link encap:Ethernet  HWaddr b8:27:eb:ed:00:88  
          inet addr:192.168.0.33  Bcast:192.168.0.255  Mask:255.255.255.0
          inet6 addr: fe80::dcce:1d4e:75e0:afb/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:9236 errors:0 dropped:0 overruns:0 frame:0
          TX packets:563 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:999226 (975.8 KiB)  TX bytes:44396 (43.3 KiB)

ip 주소가 할당되었다면 정상적으로 설정된 것이다. 이제 랜선을 뽑아도 된다.



2017년 8월 13일 일요일

Raspberry Pi에 Raspbian 설치하고 초기 설정하기

 시간이 지날수록 Raspbery Pi에 Raspbian을 설치하고 초기 설정을 위해서 각종 설정파일을 편집하는 등의 작업이 갈수록 간단해지고 있어서 사실 이 포스팅은 대단히 복잡한 과정을 설명하는 건 아니다. 그저 갈수록 기억력에 의존하기가 어려워지면서 검색의 귀찮음을 줄여보고자 글을 쓸 뿐...

 그래서 간단하게 작업 순서, 각종 URL, 명령어 위주로 간단하게 기록을 남겨보려고 한다.

0. 준비

당연히 Raspberry Pi(이하 R Pi) 한 대가 있어야 겠고, Raspbian을 설치할 SD 카드가 하나 있어야 한다. SD 카드는 LITE 버전이면 4GB 이상이면 되고 Desktop 버전을 설치할 계획이라면 8GB 이상이 좋겠다. 그리고 SD 카드에 Raspbian을 설치할 PC가 한 대 필요하다. 윈도우, 맥, 리눅스 뭐든 좋다. SD 카드를 인식시킬수만 있으면 된다. Raspbian을 설치하고 최초에 한 번은 부팅 후 설정을 해줘야 하기 때문에 HDMI 지원 모니터와 USB 키보드도 있어야 한다. PC에 연결된 걸 뽑아서 R Pi에 연결해서 써도 된다.

1. Raspbian 다운로드

라즈베리파이 홈페이지에 접속해서 DOWNLOAD 메뉴로 이동하면 Raspbian 링크가 있다. 링크로 들어가면 Desktop 버전과 LITE 버전이 따로 있으므로 원하는 버전으로 다운로드하도록 한다.

2. Raspbian 설치

 위의 다운로드 페이지를 몇 줄 읽어보면 Installation guide 링크가 있다. 페이지 URL과 내용은 가끔 바뀌므로 링크를 직접 걸어놓지는 않겠다. 다운로드 페이지를 읽어보면 항상 링크가 있으므로 링크를 타고 들어가도록 하자.

 링크를 눌러서 이동해보면 "Etcher"라는 프로그램으로 다운받은 Raspbian 이미지 파일을 SD카드에 Write함으로써 설치가 끝난다. 역시 페이지 안에 Etcher를 다운로드할 수 있는 링크가 있으므로 링크를 눌러 다운로드 페이지로 이동해서 다운로드하여 설치하도록 하자. Etcher는 윈도우, 맥, 리눅스용 버전이 모두 있으므로 자신의 PC에 맞는 버전을 다운로드하여 설치하도록 하자.

 준비된 PC에 SD카드를 연결하고 Etcher를 실행해보면 별다른 설명이 필요없을 만큼 간단하게 이미지 파일을 SD카드에 Write할 수 있으므로 여기서 추가 설명은 하지않겠다. 예전엔 이 부분이 상당히 복잡한 과정이었는데 Etcher덕분에 말 할 수 없이 간단해졌다.

3. Raspberry Pi 초기 설정

 2.번에서 설치한 SD 카드를 R Pi에 꽂고 HDMI 모니터를 R Pi에 연결하고 USB 키보드를 R Pi에 연결해 준 후 R Pi에 전원을 넣으면 바로 부팅이 되고 로그인 프롬프트가 보일 것이다. 사실 추가 설정을 안해도 이 상태로 사용가능한 리눅스 머신이다.

 최초에는 디폴트로 "pi" 라는 계정이 있다. 패스워드는 "raspberry"이므로 입력해서 로그인을 해보도록 한다.

 로그인을 했으면 이제 각종 환경 설정을 할 차례다. 그냥 raspi-config라는 프로그램을 실행하면 된다. 텍스트 모드이긴 하지만 충분히 설정하기 쉬운 인터페이스를 제공하므로 원하는 설정을 하도록 한다.
$ sudo raspi-config

 설정할 수 있는 옵션이 여러가지가 있지만 반드시 해야할 것으로 몇 가지 추천하자면 아래 옵션들은 설정해 주는 게 좋겠다.
  • 비밀번호 변경
    pi 계정의 디폴트 패스워드는 누구나 알고 있으므로 바꿔줄 것을 강력히 추천한다.
  • SSH Enable
    기본으로 Disable된 상태로 설치된다. 일반적으로 모니터, 키보드없이 사용할 계획이라면 반드시 Enable시켜야 한다. "Interfacing Options" 메뉴로 들어가면 SSH를 Enable 시킬 수 있는 메뉴가 보일 것이다. Enable 시키도록 하자.
  • Locale, Timezone 설정
    이건 반드시 필요하다고 할 수는 없겠지만 사용하다보면 분명 필요한 시점이 올 것이다. "Localisation Options" 메뉴로 들어가면 설정할 수 있는 메뉴들이 보일 것이다. 적절히 설정하도록 하자. 단, "Change Wi-Fi Country" 메뉴는 건드리지 말자. 버그가 있는 지 이 값을 건드리면 무선랜 스캔이 되지 않는다. 디폴트 그대로 써도 아무 문제가 없으니 건드리지 않도록 한다.
 이제 모니터, 키보드 없이 네트워크만 연결되어있으면 어디서나 터미널로 연결해서 작업이 가능하다. PC로 이동해서 터미널을 열고 SSH 연결을 시도해보자. 이후 추가 작업은 SSH 연결을 통해서 얼마든지 가능하다.

 다시봐도 정말 간단하다. 수년 전 R Pi에 처음 뭔가를 설치하고 설정하는 작업을 할 때와는 정말 비교할 수 없을 정도로 간단해졌다. 이제는 누구나 리눅스 머신 한 대 정도 운영하는 건 껌인 세상이다.


2017년 8월 12일 토요일

Raspberry Pi에서 USB 외장하드를 Root 파일 시스템으로 사용하기

 가지고 있던 아이맥을 SSD로 업그레이드하면서 원래 있던 3.5인치 하드디스크를 외장으로 Raspberry Pi(이하 R Pi)에 연결해서 써보려고 좀 알아보니 USB로 연결된 외장 하드가 Micro SD 카드보다 더 성능이 좋다고 한다. 게다가 Root 파일 시스템으로 외장 하드를 사용하는 게 더 나은 성능을 보인다는 얘기도 있고 해서 내가 가진 R Pi 장비에 적용해 보기로 했다.

준비 작업


 일단 현재 상태는 아래와 같다.
  1. Raspbian이 설치된 Micro SD로 부팅가능한 R Pi 3. (이전 모델도 문제는 없다.)
  2. 외장으로 사용할 3.5" 1 TB Hard Disk.
  3. 외부 전원을 사용하는 3.5" 외장 하드 케이스.
 간단히 설명을 해보면 현재 사용중이던 R Pi의 Root 파일 시스템을 현재 상태 그대로 외장 하드로 옮겨서 외장 하드를 Root 파일 시스템으로 사용하도록 설정 하는 것이다. 이렇게 얘기하면 복잡한 과정이 될 것 같지만 Brennen Bearnes라는 사람이 한 번에 모든 과정을 처리하는 스크립트를 만들어 놓았으니 걱정할 건 없다.

 여기서 주의할 것은 R Pi의 USB로부터 받는 전원이 약해서 외장 하드 케이스는 반드시 외부 전원을 따로 갖추고 있는 놈으로 사용해야 한다. (그렇지 않으면 정말 쓸데없는 고생을 하게 될 수도 있다.)

 그럼 먼저 R Pi의 남는 USB 포트에 외장 하드를 연결하고 전원을 연결하여 R Pi를 부팅하자. USB에 연결된 외장 하드는 다른 디스크 장치가 연결되어 있지 않다면 /dev/sda로 잡힐 것이다. 새로 연결한 디스크 안에 파티션이 몇 개 있다면 /dev/sda1, /dev/sda2로 전에 없던 장치들이 보일 것이므로 아래 명령으로 간단히 디스크 목록을 보고 새로 잡힌 장치가 무엇인지 알아내도록 한다.

 연결 전에는 없던 새로 잡힌 장치가 무엇인지 보면 된다. 리눅스를 어느정도 사용하는 사람이면 새로 잡힌 장치가 무엇인지 여러가지 방법을 알겠지만 대부분 이 정도로 충분하다. 아래 그림은 연결된 장치가 /dev/sda이고 그 안에 /dev/sda1 파티션이 한 개 있는 상태다. 일단은 연결된 장치가 /dev/sda라고 가정하고 계속하겠다.

스크립트 실행


 이제 준비는 끝났다. 터미널로 R Pi에 연결해서 아래 명령으로 GitHub를 통해서 배포하는 스크립트를 받아서 실행하면 된다. 만약 git가 설치되지 않은 상태라면 git 먼저 설치해야 한다. 아래 명령으로 설치한다.
$ sudo apt-get install git
 git가 설치되었으면 아래 명령으로 GitHub에서 스크립트를 받아 실행하도록 한다.
$ git clone https://github.com/adafruit/Adafruit-Pi-ExternalRoot-Helper.git
$ cd Adafruit-Pi-ExternalRoot-Helper
$ sudo ./adafruit-pi-externalroot-helper -d /dev/sda
 위의 마지막 명령으로 스크립트를 실행하면 아래와 같은 메시지들이 뜨면서 화면에 뜨면서 실행 과정을 볼 수 있다. 가끔 묻는 질문에 y를 누르고 진행 시키면 된다.

 스크립트가 출력하는 메시지들을 자세히 살펴보면 어떤 작업을 수행하는 지 대충 알 수 있다. 수행하는 작업을 간단히 정리해보면 아래와 같다.

  1. apt-get으로 disk, rsync, parted 를 설치한다. 작업을 수행하는 데 필요한 프로그램들이다.
  2. 외장 하드에 새로운 파티션을 하나 생성한다. 이전에 있던 데이터는 모두 사라지고 전체 디스크를 사용하는 파티션을 하나 생성(/dev/sda1)하므로 전에 있던 중요 데이터는 모두 백업해두어야 한다.
  3. 새로 생성한 파티션을 /mnt에 마운트하고 루트 파티션의 데이터를 모두 새로 생성한 파티션으로 복사한다.
  4. /etc/fstab 파일을 편집해서 새로 생성된 파티션이 항상 마운트되도록 설정한다.
  5. /boot/cmdline.txt 파일을 편집해서 새로 생성된 파티션이 Root 파티션이 되도록 설정한다.
 스크린에 출력된 마지막 메시지를 읽어보면 reboot를 하라고 되어있다. 자동으로 리부트를 하지는 않으므로 직접 리부팅하도록 한다.

 어떤 변화가 있었는지 자세히 알고 싶다면 /etc/fstab, /boot/cmdline.txt 파일을 열어서 직접 확인해보면 된다. 매우 간단하기도 하고 한 줄 밖에 안되는 수정사항이므로 열어보는 것이 이해하는데 도움이 된다. 

복원


 만약 어떤 문제가 있어서 다시 원래 상태로 돌아가고 싶다면  /etc/fstab, /boot/cmdline.txt을 복원하면 된다. /etc/fstab 파일의 경우 이전에 사용하던 SD 카드의 파티션을 스크립트가 주석처리해 두었으므로 이것을 해제하면 되고 /boot/cmdline.txt 파일의 경우는 이전 파일을 /boot/cmdline.txt.bak 파일로 백업해두었으므로 다시 교체하면 된다. 

마무리

 현재 상태에서 Root 파티션으로 외장 하드를 이용하고 있지만 부팅은 SD 카드를 이용해야 하기 때문에 SD 카드가 없어도 된다는 것은 아니다. 따라서 SD 카드에 있던 이전 Root 파티션은 나중에 있을지 모르는  복원 작업을 위해서 그대로 남겨두던가 아니면 새롭게 포맷해서 다시 마운트하여 사용할 수 있다. 

 SD 카드는 /dev/mmcblk0 장치로 잡혀있고 파티션은 /dev/mmcblk0p1, /dev/mmcblk0p2이다. /dev/mmcblk0p1 파티션은 boot 파티션이므로 건드리면 안되고 /dev/mmcblk0p2이 이전에 사용하던 Root 파티션이므로 이것을 포맷해서 사용할 수 있다. 만일을 대비해서 포맷하지 않고 그대로 마운트해서 사용해도 문제는 없다.

 포맷 및 마운트하는 명령은 아래와 같다. (umount 철자 주의)
# Format
$ sudo mkfs.ext4 /dev/mmcblk0p2

# Mount
$ sudo mount /dev/mmcblk0p2 /mnt

# Unmount
$ sudo umount /mnt

 어차피 SD 카드는 부팅에 필요하기 때문에 항상 꽂아놓고 사용할 것이므로 fstab을 편집해서 항상 mount가 되도록 해두는 게 좋다. 아래 한 줄을 /etc/fstab 파일의 마지막에 추가하여 항상 /sdcard에 마운트가 되도록 할 수 있다.

/dev/mmcblk0p2  /sdcard         ext4    defaults        0       0

2017년 7월 18일 화요일

Spring Boot + MyBatis 프로젝트 설정하기

 Spring Boot를 이용하면 MyBatis도 매우 간단하게 연동해서 사용할 수 있다. 어떤 라이브러리(프레임워크)든지 프로젝트에 추가하려고 할 때 내가 가장 먼저 관심을 갖는 것은 가능한 최소한의 설정이 무엇인가 이다. 일단은 가장 최소한의 설정으로 기본적인 기능을 그대로 이용해보고 필요한 추가 설정이 생기면 그때그때 조금씩 수정해가는 것이 가장 깔끔한 상태를 유지하는 길이라고 생각하기 때문이다.

 Spring Boot와 MyBatis를 함께 사용하는 것은 그러한 측면으로는 정말 최소한의 설정만으로 큰 만족을 얻을 수 있다.

 일단 IntelliJ IDEA CE버전으로 기본적인 Spring Boot 프로젝트를 생성하도록 하자. (생성방법은 이전 글을 참고하기 바란다.) 최초 생성 직후의 build.gradle 파일의  dependency는 'spring-boot-starter'가 들어가 있겠지만 MyBatis를 이용하려면 아래와 같이 'mybatis-spring-boot-starter'로 교체해준다.
dependencies {
    compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
    compile('org.mariadb.jdbc:mariadb-java-client:2.0.3')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
 사실 이전 글의 생성방법에 따라 프로젝트를 생성할 때 Spring Initializr화면을 Full Version으로 전환한 후 MyBatis 체크박스에 체크를 하면 'spring-boot-starter' 대신 'mybatis-spring-boot-starter'가 dependency에 들어간다. 하지만 이렇게 기본 생성된 파일을 직접 수정해줘도 똑같다.

 추가로 JDBC 드라이버를 dependency에 추가해주어야 한다. 위의 예에서는 MariaDB의 JDBC 드라이버를 넣어주었지만 각자 자신의 DB에 맞는 드라이버를 설정해주도록 하자.

 다음으로 'src/main/resources/'에 있는 application.properties 파일에 data source를 설정해주도록 하자. (파일이 없으면 만들어준다.) data source가 설정이 되어있지 않으면 예외가 발생하면서 정상적으로 실행되지 않으므로 반드시 해주어야 한다.
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.0.33:3306/Test
spring.datasource.username=testuser
spring.datasource.password=testpass
 각 항목에 대한 설명은 굳이 하지 않아도 JDBC 유 경험자라면 다들 알만한 것들이다. 자신의 DB 생성 상황에 맞게 위 값들을 맞춰서 넣어주도록 하자. (당연히 'Test' DB와 'testuser' 사용자 계정 같은 것들은 DB에 준비가 되어 있어야 한다.)

 일단 설정 파일 수정 사항은 이게 끝이다. 이렇게 data source 설정만으로 MyBatis를 사용할 수 있으니 최소한의 설정임에 틀림이 없는 것 같다.

 이제 Java 코드에 손 댈 차례다. 먼저 Mapper로 사용할 Interface를 만들어 준다.
package com.company.dbtest.mapper;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TestMapper {
    String getTime();
}
 mapper들을 모아 놓을 package (com.company.dbtest.mapper)에 TestMapper 인터페이스를 위와 같이 선언해서 넣어놓는다.

 다음으로 프로젝트 생성 시 같이 생성된 @SpringBootApplication 클래스를 열어서 아래와 같이 수정해준다.
package com.company.dbtest;

import com.company.dbtest.mapper.TestMapper;
...

@SpringBootApplication
@MapperScan("com.company.dbtest.mapper")
public class DbtestApplication implements CommandLineRunner {

 @Autowired
 TestMapper testMapper;

 public static void main(String[] args) {
  SpringApplication.run(DbtestApplication.class, args);
 }

 @Override
 public void run(String... args) throws Exception {
  System.out.println("Time:" + testMapper.getTime());
 }
}
 수정 내용을 살펴보면 먼저 @MapperScan 어노테이션을 추가해주었다. 여기서 지정된 "com.company.dbtest.mapper" 패키지에서 자동으로 mapper를 찾아서 사용할 것이므로 정확하게 지정하도록 한다.

 이제 TestMapper 인터페이스를 @Autowired로 직접 주입받아서 바로 사용이 가능하다. 위 예에서는 주입받은 testMapper를 run()함수에서 직접 사용하고 있다.

 물론 아직 실행은 안된다. 아직 TestMapper의 getTime()에서 실행할 SQL문을 아직 정의하지 않았기 때문이다. MyBatis가 찾아서 사용할 Mapper XML 파일을 추가해 주어야 한다. 별다른 설정을 하지 않았기 때문에 MyBatis는 mapper 인터페이스와 같은 classpath에서 같은 이름의 xml파일을 찾아서 사용하려고 할 것이다. TestMapper 인터페이스와 같은 classpath(com.company.dbtest.mapper)에 TestMapper.xml 파일을 하나 만들어주고 아래와 같이 정의해 주도록 한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hansdesk.dbtest.mapper.TestMapper">
    <select id="getTime" resultType="String">
        SELECT NOW()
    </select>
</mapper>
 mapper의 자세한 정의 방법은 MyBatis의 문서를 참고하도록 하고 간단히 설명하자면 mapper 태그의 namespace에 TestMapper 인터페이스의 Full name을 지정하고 select 태그의 id로 메소드의 이름을 지정한 것이다. getTime()이 호출되면 DB에서 "SELECT NOW()"가 실행되어 시간 문자열이 반환된다.

 참고로 IntelliJ를 사용할 때의 주의사항 한 가지를 얘기해야 겠다. xml 파일이 mapper 인터페이스와 같은 패키지에 있으면 MyBatis가 같은 이름의 xml파일을 자동으로 찾아서 사용하지만 IntelliJ에서 디버깅할 때 xml파일이 'src/main/java' 폴더 아래에 있으면 찾지 못한다. 반드시 아래 그림과 같이 'src/main/resources' 폴더에 mapper package와 같은 경로를 만들어서 xml 파일을 넣어놓도록 하자. 'src/main/java' 폴더 아래 mapper 패키지에 xml파일을 넣어놓으면 찾지 못하고 예외가 발생한다. 양쪽 어디에 있건 Jar로 묶어 놓으면 같은 package에 들어가게 되지만 IntelliJ에서 디버깅할 때 만큼은 다르게 인식되는 것 같다.

2017년 7월 17일 월요일

Raspberry Pi에 MariaDB 설치/삭제하기

 Raspberry Pi에도 MySQL과 MariaDB가 모두 설치가능하다. 최신 버전은 아니고 현재 기준으로 MySQL은 5.5, MariaDB는 10.0 버전이 유효하다. (MariaDB가 더 버전이 높지만 base는 MySQL 5.5이다.) 인터넷을 찾아보면 apt-get을 이용하지 않고 직접 소스를 빌드해서 설치한다면 최신 버전도 가능한 것 같기는 한데 같이 설치해야하는 라이브러리가 많아서 쉽지는 않은 모양이다.

 apt-get을 이용하는 방법은 간단하다.
1. 우선 시스템 상태를 최신으로 업데이트하도록 하자. (반드시 필요한 건 아니다.)
$ sudo apt-get update
$ sudo apt-get upgrade
2. apt-get을 이용하여 MariaDB 설치.
$ sudo apt-get install mariadb-server
 설치는 이걸로 끝이다. 어떤 package 들이 설치 되었는 지 확인해보면 아래 그림에서 보듯이 mariadb 이름이 들어간 6개 package가 추가로 설치되어 있다. 물론 이 외에도 필요한 라이브러리들이 아직 미설치된 경우 같이 설치된다. 기본적인 서버 설정도 모두 되어있는 상태이기 때문에 바로 사용이 가능하다. 알려진대로 MariaDB는 MySQL을 대체하기 위한 제품이라 명령들이 거의 호환된다.

 삭제하는 방법도 간단하다. 아래 명령을 차례로 실행하도록 하자.
$ sudo apt-get remove mariadb-server
$ sudo apt-get autoremove
 설치 목록을 확인해보면 mariadb 이름이 들어간 package들은 모두 삭제되었을 것이다. 설치된 목록은 아래 명령으로 확인할 수 있다.
$ apt list --installed
 일단 apt-get으로 설치가 가능한 것들은 설치/삭제가 너무 간단하다. 사실 MariaDB 뿐만 아니라 다른 package들도 다 똑같다. package 이름만 정확하게 알고 있으면 된다. 사실 그래서 더욱 외워두려고 하지도 않고 나중에 또 막상 사용하려고 하면 기억이 안나서 결국 인터넷 검색을 하게된다. 사실 그래서 여기에 기록을 해두려는 의도로 포스팅을 하는 것이다.

 참고로 MySQL을 설치하는 것도 위에서 설명한 명령들의 Package명을 'mysql-server'로 바꿔주면 똑같이 설치되고 삭제된다. 그리고 한 가지 더 참고할 만한 사항을 얘기 하자면 나의 Raspberry Pi에는 MySQL이 설치되어 있었고 작업하던 Database도 있었다. 그 상태에서 MySQL을 MariaDB로 바꾸려고 먼저 설치되어있던 MySQL을 위의 명령들로 삭제하고 바로 MariaDB를 설치했는데 설치 과정 중에 이전 DB를 발견하고 그대로 사용하겠냐는 질문이 뜨길래 'Yes'로 답하고 설치를 완료하였다. 설치가 끝난 후에 접속해보니 모든 데이터가 MySQL을 사용할 때와 똑같은 상태로 남아있었다.

 MariaDB가 MySQL을 base로 만들어졌고 그 둘의 호환성에 대한 많은 얘기를 들었던 터라 어느 정도 예상은 했지만 그래도 약간은 의심이 들었었는데 직접 해보니 MySQL을 사용할 때와 정말 다른 게 없다. 더 깊이 사용해보면 뭔가 다른 점이 나올 지도 모르지만 현재로서는 사용법도 똑같고 라이센스 측면에서 자유로운 MariaDB로 갈아타는 게 괜찮은 선택이라고 생각된다.

2017년 7월 16일 일요일

Spring Boot에서 Logback 설정 파일 지정하기

 전에 IntelliJ IDEA CE 버전으로 Spring Boot 프로젝트를 생성하는 방법을 설명했었다. 그 방법으로 Spring Boot 프로젝트를 최소한의 dependency만 설정한 채로 생성을 하면 'spring-boot-starter'에 대한 dependency만 가진 채로 생성이 된다.

 이 상태로 Project 뷰에 가서 External Libraries를 열어보면 Logback과 SLF4J 라이브러리에 대한 dependency가 이미 포함되어있는 걸 알 수 있다. 즉, 추가 dependency 설정 없이 이미 Logback을 사용할 수 있는 상태라는 뜻이다. JCL, JUL, Log4J 같은 다른 Logger들을 SLF4J로 연결해주는 브릿지 라이브러리들도 모두 이미 들어가 있으니 Logback을 위한 dependency 설정은 제대로 되어있다고 볼 수 있다.

 Spring Boot에서 Logback 설정 파일은 일반적으로 logback-spring.xml 파일을 만들어서 classpath 루트에 두면 된다. 즉 'src/main/resources'에 logback-spring.xml 파일을 만들어서 넣어두면 알아서 찾아서 적용이 된다. 하지만 'src/main/resources'에 넣어두는 파일은 jar 배포시 jar안에 들어가게 되기 때문에 매번 실행 전에 로그 레벨 등의 설정을 바꿔서 적용하겠다는 의도로는 적합하지 않은 방법이다.  그렇게 하려면 로그 설정 파일은 jar파일 외부에 따로 존재하도록 해야한다.

 로그 설정 파일을 따로 만들어 두고 그 파일을 Spring Boot에서 읽어가도록 하는 방법은 application.properties 파일에 로그 설정 파일을 지정하는 것이다. 아래와 같이 logging.config 속성을 정의해주면 그 속성에 지정된 파일을 찾아서 로그 설정 파일로 이용한다.
# application.properties
logging.config=logback.xml
 로그 설정파일의 이름도 logback-spring.xml이 아닌 다른 원하는 이름으로 지정해서 사용해도 문제가 없다. 위 설정에서는 jar를 실행하는 폴더에 있는 logback.xml 파일이 설정파일로 이용될 것이다.

 위 설정으로 배포를 하게되면 반드시 logback.xml 파일이 jar를 실행하는 폴더에 같이 있어야 한다. 만약 없으면 로그 기능은 동작하지 않게 되고 설정 파일이 없다는 불평을 보게될 것이다. 이렇게 되면 배포할 때 항상 logback.xml 파일도 같이 배포해야 하는 부담이 생긴다. 왠지 설정 파일이 없으면 default로 적용되는 파일이 따로 있어서 반드시 같이 배포해야 하는 부담이 없었으면 좋겠다는 생각이 들 것이다.

 default 로그 설정 파일을 만들어서 jar에 포함시키고 외부에 파일이 있을 때는 그 파일을 지정할 수 있으면 된다.

 먼저 default로 적용될 로그 설정 파일을 만들자.  설정 파일 이름은 'logback.xml'이라고 하겠다.
1. jar에 포함시킬 default 로그 설정 파일은 'src/main/resources' 폴더에 만들어서 넣어둔다.
2. 'src/main/resources' 폴더에 'application.properties'에 아래 설정을 추가한다.
# application.properties in 'src/main/resources'
logging.config=classpath:logback.xml
 사실 별 내용은 없다. classpath에 있는 logback.xml 파일을 default로 사용하도록 설정한 것이다. 이 파일들은 jar 안에 들어있기 때문에 jar만 배포했을 때 jar 안에 있는 설정 파일들이 적용된다.

 이 상태에서 외부에 있는 설정 파일들을 적용하려면? 그냥 jar를 실행하는 폴더에 application.properties 파일을 두고 거기에 처음에 설명한 것처럼 외부 설정파일을 지정하면 된다. jar 외부에 있는 application.properties 파일이 더 우선순위가 높기 때문에 그곳에 지정한 파일이 적용된다.

2017년 7월 14일 금요일

Spring Boot의 application.properties에 대해..

 요즘 Spring Boot를 이용해서 Non-web application을 만드는 작업에 빠져서 Spring에 대해 많은 걸 배우고 공부하는 중이다. 그래서 이번에는 Spring Boot application을 위한 설정파일로 가장 간단하게 이용할 수 있는 방법인 application.properties를 이용하는 방법을 간단히 설명해 보려고 한다.

 Spring Boot를 이용할 때 대부분의 경우 설정 파일로 application.properties를 이용한다. application.properties는 기본적으로 Spring Boot가 읽어들이도록 되어있기 때문에 사실 파일만 만들어서 'src/main/resources' 폴더에 넣어주면 바로 설정파일로 이용이 가능하다. 이렇게 편한 방법이 있는데 따로 만들어서 사용할 이유가 없다.

 간단한 사용법을 보자.

# application.properties
name=Michael
import org.springframework.stereotype.*
import org.springframework.beans.factory.annotation.*

@Component
public class MyBean {

    @Value("${name}")
    private String name;

    // ...

}
 위의 MyBean 클래스의 name 멤버변수는 @Value 어노테이션을 지정한 것만으로 application.properties에 있는 name속성 값 "Michael"로 초기화가 된다.

 만약에 application.properties 파일에 name 속성이 정의되어있지 않았을 때 default값을 지정하는 방법도 있다. 아래와 같이 @Value 어노테이션에서 속성 이름 옆에 콜론(:)을 찍고 직접 지정해 주면 된다.

    @Value("${name:Michael}")
    private String name;

    @Value("${age:20}")
    private int age;
 위 예에서 지정된 default 값들은 application.properties에 값이 정의되어있지 않으면 각 변수의 타입에 맞게 변환되어 대입된다.

 사실 Spring Boot의 문서를 보면 property source를 설정하는 다양한 방법이 제공된다.

  1. Devtools global settings properties on your home directory (~/.spring-boot-devtools.properties when devtools is active).
  2. @TestPropertySource annotations on your tests.
  3. @SpringBootTest#properties annotation attribute on your tests.
  4. Command line arguments.
  5. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property)
  6. ServletConfig init parameters.
  7. ServletContext init parameters.
  8. JNDI attributes from java:comp/env.
  9. Java System properties (System.getProperties()).
  10. OS environment variables.
  11. A RandomValuePropertySource that only has properties in random.*.
  12. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants)
  13. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants)
  14. Application properties outside of your packaged jar (application.properties and YAML variants).
  15. Application properties packaged inside your jar (application.properties and YAML variants).
  16. @PropertySource annotations on your @Configuration classes.
  17. Default properties (specified using SpringApplication.setDefaultProperties).
 위의 목록은 더 높은 우선순위를 갖는 property source부터 차례대로 나열한 것이다. 여기서 소개하는 application.properties 파일을 'src/main/resources' 폴더에 넣는 방법은 위의 목록 중 15번에 나오는 방법으로 우선순위로 보면 상당히 아래쪽에 나온다. 즉 상당히 우선순위가 낮다는 말이다. 'src/main/resources' 폴더에 application.properties 파일을 넣어 놓는다는 것은 배포 시에 jar 파일에 함께 묶여서 배포가 된다는 것이고 따라서 배포 후에 설정 값을 변경하려는 의도로 사용하기에는 적합하지 않다. 그러한 용도로 사용하려면 14번에서 말하는 jar 외부의 application.properties를 사용하면 된다. 그냥 application.properties 파일을 따로 만들어서 jar를 실행하는 directory에 두면 된다. jar 안에 있는 application.properties 파일보다 우선순위가 높기 때문에 둘 다 있는 경우 jar 외부의 application.properties 파일의 값이 더 높은 우선순위를 갖는다.

 여기서 많은 사람들이 우선순위에 대해서 오해하는 점 하나를 짚고 가야겠다. 예를 들어 jar안에 같이 포함된 application.properties파일과 jar 외부의 application.properties 파일이 둘 다 존재할 때 외부의 application.properties 파일이 더 우선순위가 높기 때문에 내부의 application.properties 파일이 완전히 무시된다고 생각하기 쉽다. 하지만 그렇지 않다. 우선순위가 높은 property source의 값들은 더 낮은 property source의 값들을 덮어쓰는 것이지 낮은 우선순위의 property source를 읽어들이지 않는 다는 뜻이 아니다. 즉 둘 다 name 속성이 정의되어있다면 더 높은 우선순위의 property source에 선언된 name 속성 값이 사용된다. 하지만 age라는 속성이 더 낮은 우선순위의 property source에만 선언되어있다면 낮은 우선순위의 property에 선언되었지만 그 값이 사용된다. (더 높은 우선순위에 값이 정의되지 않았으므로)

 즉 위 목록의 모든 property source들을 아래쪽부터 윗쪽으로 차례대로 다 찾아서 읽어들이면서 이미 정의된 property들은 덮어 쓰는 개념이다. 목록을 보면 테스트 환경을 제외하고 command line argument가 가장 우선순위가 높으므로 임시로 특정 값을 override하고 싶다면 command line을 이용해서 property를 전달하면 무조건 전달된 값이 이용될 것이다.

 command line을 이용하는 방법은 아래와 같은 방법으로 전달하면 된다. ('server.port' 속성값 지정)

$ java -jar myapp.jar --server.port=9000
 위 목록에 나열된 방법은 많지만 흔히 사용하는 방법으로 이 정도만 알고 있으면 대부분의 경우 문제없이 개발을 진행할 수 있다. 개념적으로 잘 이해하고 있다면 다른 나머지 방법을 이용하는 것도 사실 시간문제일 뿐 크게 어려운 부분은 없을 것이다.

2017년 7월 4일 화요일

IntelliJ IDEA CE + Spring Boot + Gradle 프로젝트 생성하기

 IntelliJ IDEA CE(Community Edition)버전을 이용해서 Spring Boot 프로젝트를 수행하는 데 이용할 수는  없을까해서 이런 저런 시도를 해보았다. Gradle을 이용해서 프로젝트를 수행한다면 어차피 IntelliJ IDEA CE 버전도 문제가 없을 것 같았기 때문이다. 그래서 여러가지 시도를 해보고 가장 최신 버전의 Spring Boot를 이용하면서 IntelliJ스러운 프로젝트를 생성하는 방법을 여기에 기록해볼까 한다.

 최신 버전의 Spring Boot를 이용하는 Gradle 프로젝트 파일을 생성하는 건 역시 "Spring Initializr"를 이용하는 것이다. 일단 https://start.spring.io에 접속해서 프로젝트를 생성하자.

 한 화면으로 생성이 끝나는 간단한 작업이다. 각 선택 항목들도 모두 이해못할 것들은 없다. 중요한 건 빨간색 사각형 부분들이다. Gradle Project로 생성하고 있는 지 확인하도록 하자.  그리고 Group과 Artifact는 Java의 root package이름이 되므로 예제처럼 Group은 (company의) reverse domain 형태로 넣어주고 Artifact는 프로젝트 이름을 소문자로 넣어주자. 나중에 IntelliJ에서 프로젝트를 생성할 때는 "Group.Artifact"를 Group ID로 사용할 것이다.

 "Switch to the full version."링크를 누르면 dependency를 추가하는 등의 세부적인 작업을 할 수 있다. 하지만 추후에도 얼마든지 가능한 작업들 이므로 여기서는 간단한 설명을 위해 그냥 최소한의 설정으로 생성하겠다.

 "Generate Project" 버튼을 누르면 프로젝트가 zip으로 묶여서 다운로드된다. 일단 파일을 원하는 곳에 풀어두도록 하고 이제 IntelliJ를 실행해서 새로운 프로젝트를 만들자.

 "Create New Project"를 눌러서 새로운 프로젝트를 만들자.

 Gradle > Java 를 선택하고 Next를 눌러 다음으로 진행한다.

 GroupId 항목에는 Spring Initializr에 넣었던 Group과 Artifact를 그림처럼 연결해서 넣어주고 ArtifactId는 원하는 Project 이름을 넣어준다. 

 GroupId 항목은 build.gradle 파일의 group 정보로 들어가게 되고 Java의 루트 패키지가 될 것이다. ArtifactId는 프로젝트 이름으로서 그리고 Module 이름으로서 사용이 된다.

 이후는 일반적인 Gradle Java 프로젝트와 같다. 그림과 같이 선택해주고 Next를 눌러 다음으로 넘어가자.

 프로젝트 위치를 원하는 곳으로 설정하고 Finish를 눌러서 프로젝트 생성을 완료하자.

 몇 초 정도 생성작업을 하고나서 아래 그림과 같은 구조로 프로젝트가 생성된다. (왼쪽 프로젝트 뷰의 트리구조 참고)

 IntelliJ에서 프로젝트를 생성하고 나서 build.gradle 파일을 열어보면 위의 그림과 같이 간단한 내용으로 생성되어있다. "Spring Initializr"에서 생성한 프로젝트의 build.gradle과 비교해보면 빨간색으로 표시한 group 정보 말고 나머지 내용은 모두 이미 존재한다는 걸 알 수 있다. 

 대충 짐작이 갈 것이다. IntelliJ에서 생성한 build.gradle 파일에서 group 정보만 남기고 나머지는 모두 삭제하고 Spring Initializr 에서 생성한 build.gradle 파일의 내용으로 대체하도록 한다. 그리고나서 Spring Initializr가 생성한 프로젝트의 src 폴더를 IntelliJ가 생성한 프로젝트로 옮겨서 덮어쓰면 끝이다. 최종적으로 아래 그림과 같은 프로젝트가 될 것이다. 

 build.gradle 파일의 내용은 Spring Initializr가 생성한 그대로의 내용에 group 정보만 넣은 것과 같고 src 폴더를 보면 Spring Initializr가 생성한 @SpringBootApplication 클래스와 테스트 클래스가 보인다. 

 간단히 요약하자면 IntelliJ가 생성한 Gradle Java 프로젝트를 Spring Initializr가 생성한 프로젝트의 내용으로 덮어씌우는 작업이다. 단지 Group, Artifact 정보를 package 경로와 맞추기 위해서 몇 가지만 맞춰주면 되는 것이다. 

 IntelliJ IDEA CE에서 Import를 하는 방법으로 가져와 봤지만 위와 같은 구조로 생성이 되지 않는 것으로 결론을 냈다. 난 IntelliJ가 생성하는 Gradle Java 프로젝트의 기본 구조가 마음에 든다. 그리고 Spring Intializr가 생성하는 최신 build.gradle 파일도 믿음이 간다. 그래서 이런 방법을 사용하는 것이다. 

 참고로 Runnable Jar 파일을 생성하는 방법을 설명하자면 Gradle뷰에서 보이는 태스크들 중에서 'build'를 더블클릭해서 실행하는 것이다. 'build' 태스크를  build/libs/ 폴더 아래 Runnable Jar가 생성된다. build.gradle 파일에 java 플러그인이 적용되었기 때문에 'jar' 태스크가 있지만 이 'jar' 태스크는 build.gradle을 추가로 수정하지 않으면 dependency가 있는 라이브러리까지 묶어놓은 Runnable Jar를 만들지는 못하므로 반드시 'build' 태스크를 이용하도록 하자.

2017년 7월 1일 토요일

IntelliJ로 Gradle Java Project 생성부터 Runnable Jar 생성 까지

 안드로이드 개발 환경으로 Android Studio가 대세가 되다보니 이제 IntelliJ IDEA가 Java 개발에서도 많이 사용을 하게되고 더불어 Gradle도 빌드툴 분야에서 점점 더 입지를 넓혀가는 중이라 나도 Java 개발 환경으로 IntelliJ와 Gradle을 사용해 보려고 여러가지 테스트를 진행 중이다.

 그 중에서도 배포를 위한 Runnable Jar 파일을 생성하는 방법이 좀 까다로워서 여기에 그 방법을 남겨볼까 한다. 간단한 프로젝트를 하나 만들어서 Jar를 생성하는 것까지 최대한 간단하게 설명을 해보도록 하자.

 일단 IntelliJ를 실행하고 Create New Project를 선택해서 Gradle Java 프로젝트를 생성해 보자.

 Create New Project를 선택하면 아래와 같은 창이 뜨는데 왼쪽 뷰에서 Gradle을 선택하고 오른쪽 뷰에서 Java를 체크하고 Next를 눌러 다음으로 넘어가자.

그러면 Gradle 프로젝트 생성을 위한 기본 정보를 입력하는 창이 나오는데 Group ID와 Artifact ID를 적당히 원하는 값으로 넣어준다. 그림을 보면 Group ID와 Artifact ID의 일반적인 형식이 무엇인지 알 수 있을 것이다. 필수는 아니지만 적당히 맞춰서 넣어주는 게 좋겠다. Next를 눌러 다음으로 넘어가자. 

 다음으로 아래와 같은 창이 뜨면 대부분 기본적인 설정이 되어있지만 'Create directories for empty content roots automatically' 체크 박스만 추가로 더 해주도록 하자. 이렇게 해주면 기본적인 Java 소스 폴더 구조를 생성해준다. 그 구조를 사용하지 않을 계획이라면 필요없다. 'Use auto-import' 옵션은 뒤에 dependency를 추가할 때 다시 물어보기 때문에 반드시 필요하지는 않지만 여기서 체크를 해주는 게 편하다. (그림에서는 안했지만..) 나머지 옵션들은 Gradle이 권장하는 사용 패턴대로 되어있으므로 그대로 사용해도 된다. Next를 눌러 다음으로 넘어가자.

 마지막으로 프로젝트 이름과 생성 위치를 선택하고 Finish를 누르면 몇 초 정도 후에 생성이 완료된다.

 생성 완료 후에는 아래 그림의 왼쪽 프로젝트 뷰의 구조와 같은 형태가 된다. (메뉴 때문에 조금 가려졌지만 보이는 부분이 전부다)

 src -> main -> java 폴더에 우클릭해서 New > Java Class를 선택하면 클래스 생성 창이 뜨는데 적절히 클래스 이름을 입력하고 OK를 눌러 완료하면 클래스 파일 소스가 생성되어있다.

  생성된 파일을 더블클릭해서 열고 간단히 몇 줄 넣어보자. 

 실행하고 디버깅하는 방법은 일반 다른 IDE와 비슷하므로 메뉴를 좀 둘러보면 알 수 있으므로 여기서 설명은 하지 않고 바로 Runnable Jar 생성을 해보도록 하겠다.

 일반적으로 IntelliJ를 이용해서 jar를 생성할 때는 File > Project Structure... > Artifacts를 통해서 Artifact로 Jar를 하나 등록한 후 Build > Build Artifacts... 메뉴를 통해서 생성하는 방법이 많이 소개되어있다. 하지만 이 방법을 사용하면 IntelliJ의 버그때문인지 MANIFEST 파일을 생성하는 데 문제가 있고(인터넷에 많이 소개되어있으므로 자세한 설명은 생략) 그 문제를 피하더라도 dependency가 있는 외부 라이브러리를 같이 jar에 패키징하려고 하면 그 라이브러리의 MANIFEST 파일이 최종적으로 Jar에 남아서 실행이 안되는 문제가 있다.

 그러나 지금은 Gradle을 이용해서 프로젝트를 생성하였으므로 Gradle을 이용해서 Jar를 생성하면 아무 문제가 없다. Gradle 프로젝트이므로 당연히 이렇게 사용하는 게 더 맞을 것이다. 그 방법을 여기서 설명하려고 한다.

 기본적으로 Gradle 프로젝트를 생성하면 위 그림의 내용으로 build.gradle 파일이 생성되어 있다. 이 파일이 Gradle을 이용해서 빌드를 수행하기 위한 스크립트 파일이며 필요한 정보들이 최소한으로 설정되어있다. 간단히 설명하자면 'java' 플러그인이 사용되고 있고, repositories 항목의 mavenCentral()은 maven 으로부터 각종 dependency가 있는 라이브러리들을 가져오도록 설정한 것이고, dependencies 항목에 필요한 라이브러리들을 설정함으로써 maven 에서 필요한 것들을 가져오게 될 것이다.  지금은 test를 위한 컴파일 시에 junit 프레임워크가 필요하다고 설정한 것이다. (기본적으로 설정 되어있다) 나머지 버전 정보 등은 설명하지 않아도 간단한 것들이므로 보면 알 수 있을 것이다.

 위 내용은 기본적으로 생성되는 최소한의 상태다. 여기서 Jar 생성을 위해서 Manifest 정보만 아래와 같이 추가해주면 끝이다.


 아래 그림처럼 윈도우 우측의 Gradle 뷰를 열고 jar 항목을 더블클릭하면 Jar 빌드가 진행되고 최종적으로 왼쪽 프로젝트 뷰의 build -> libs 폴더 아래 jar 파일이 생성되어 있음을 확인할 수 있다. 터미널에서 직접 실행해보고 제대로 되는 지 체크해보기 바란다.

 이렇게 Gradle을 이용하는 방법은 dependency가 있는 라이브러리를 같이 jar로 묶어서 배포하려고 할 때도 이용할 수 있다. 간단하게 log4j2 라이브러리를 추가하고 같이 패키징 해보도록 하자.

 먼저 dependencies 항목에 아래 오른쪽 사각형과 같이 추가해주도록 하자. 간단히 설명하자면 compile할 때 이 라이브러리가 필요하다는 것을 Gradle 형식에 맞게 추가해 준 것이다. 참고로 두 개의 라이브러리를 등록한 것이다.

 이렇게 추가하면 auto import 옵션이 enable된 상태라면 자동으로 Gradle이 필요한 라이브러리를 maven으로부터 가져와서 왼쪽 사각형과 같이 프로젝트에 추가해준다. (정말 좋은 세상이다.) auto import가 꺼져있다면 작은 툴팁 같은 창이 우측 하단에 뜨면서 enable하겠냐고 묻는데 이 때 enable해도 된다. 가져오기가 완료되면 왼쪽 프로젝트 뷰의 External Libraries에 추가된 라이브러리가 보일 것이다.

 이제 build.gradle 파일을 아래와 같이 수정해준다.

 compile시에 필요한 파일들을 같이 패키징하기 위한 스크립트다. 디렉토리인 경우 그대로 넣고 jar인 경우 풀어서 넣는다. 많은 사람들이 소개하는 방법이지만 정확한 사용법은 Gradle과 Groovy를 제대로 사용할 줄 알아야 알 수 있을 것 같다. 앞에서 dependencies항목에 compile dependency로 jar 2개를 추가했으므로 추가된 jar 파일들의 내용물이 같이 포함되어 패키징될 것이다.

 다시 Gradle 뷰에서 jar를 더블클릭하여 jar파일 생성해보자. 생성된 jar 파일의 크기를 보면 이전에 몇 백 바이트 정도의 크기였던 jar 파일이 메가 단위로 커졌을 것이다. 생성된 Jar 파일의 내용을 보면 추가된 라이브러리의 .class 파일들이 잔뜩 들어가 있는 것을 알 수 있다. 물론 여기서는 HelloWorld 코드에서 직접 log4j 라이브러리를 사용하지는 않았지만 당연히 사용에 문제가 없다. 혹시 궁금하다면 직접 HellowWorld를 로그로 출력하는 코드를 테스트 해보기 바란다.

 참고로 jar 파일 안에 포함된 파일의 리스트를 보는 명령은 'jar -tf 파일이름.jar'이다. 압축을 풀어서 확인하고 싶다면 'jar -xvf 파일이름.jar' 명령을 실행하면 현재 폴더에 압축을 푼다.

2017년 4월 9일 일요일

Windows에 OpenSSL 설치하기

 지난 번에 macOS에 OpenSSL을 설치해 보았으니 이번에는 Windows에 설치를 해 보려고 한다. macOS는 unix계열 운영체제이기 때문에 사실 별로 어려움 없이 설치가 되었다. 하지만 윈도우는 좀 더 복잡하고 설치에 필요한 프로그램이 몇 가지 더 있다. 그래서 macOS보다 Windows가 더 정리를 해놓는 의미가 있을 것 같아서 여기서 정리를 해 놓으려고 한다. 설치 버전은 64비트 버전으로 설치할 예정이다.

1. Source Download

 물론 먼저 해야할 일은 소스를 다운로드하는 것이다. 아래 OpenSSL 사이트에 접속해서 최신 소스를 다운로드하도록 하자.
    URL: https://www.openssl.org/
현재 최신 버전은 openssl-1.1.0e.tar.gz 이다. 어차피 설치되고 나면 지워도 되는 파일이므로 원하는 폴더에 압축을 풀도록 한다.

2. Prerequisite

 Windows에서 OpenSSL을 빌드하고 설치하기 위해서는 아래 프로그램들이 필요하다.

  1. Perl
  2. OpenSSL의 빌드 환경을 Configure하기 위해서 Perl 스크립트 실행 환경이 필요하다. OpenSSL에서 추천하는 윈도우용 Perl은 ActiveState가 배포하는 ActivePerl이다. https://www.activestate.com/ActivePerl 사이트에서 다운로드할 수 있으므로 다운로드하여 설치하도록 하자. 설치 중 PATH에 perl의 경로를 등록하는 옵션이 있다. 반드시 선택해주도록 하자.
  3. Visual C++
  4. 당연히 C컴파일러가 있어야 한다. Windows에서는 가장 많이 사용할 것으로 생각되는 Visual C++컴파일러를 사용해서 빌드해 보도록 하겠다. Visual Studio 2015 무료 버전을 받아서 설치하도록 하자. 당연히 그 상위 버전이 있다면 그걸 사용하면 된다.
  5. Netwide Assembler (NASM)
  6. OpenSSL 소스에는 Assembly 코드가 포함되어있다. Windows 환경에서는 NASM을 사용해서 빌드하므로 NASM도 설치하도록 하자. http://www.nasm.us/에 접속하여 Windows용 설치파일을 받아서 설치하도록 하자. 설치 후에 NASM이 설치된 경로를 PATH에 등록해 주어야 한다.

3. Build & Install

 먼저 Visual Studio 2015를 설치했다면 시작메뉴에 'VS2015 x64 네이티브 도구 명령 프롬프트' 항목이 생성되어 있다. 이 메뉴를 관리자 권한으로 실행하도록 한다. 프롬프트가 뜨면 1번에서 압축을 푼 폴더의 루트로 이동하자. 다음은 아래 명령을 순서대로 수행하면 된다.
> perl Configure VC-WIN64A
> nmake
> nmake test
> nmake install
 위 명령 중 'make test'는 생략해도 되지만 혹시나 하는 마음이 있다면 수행해서 제대로 빌드가 되었는 지 확인해 보는 것이 좋을 것이다. 그리고 마지막 install 명령은 관리자 권한이 필요하기 때문에 처음에 명령 프롬프트를 관리자 권한으로 실행한 것이다.

 설치 경로를 특별히 지정하지 않으면 기본 경로는 C:\Program Files\OpenSSL이다. C:\Program Files\OpenSSL에 가서 확인해보면 아래와 같이 적절히 설치되어 있는 걸 확인할 수 있다.
  • C:\Program Files\OpenSSL\bin - openssl 실행 파일
  • C:\Program Files\OpenSSL\lib - openssl 라이브러리 파일
  • C:\Program Files\OpenSSL\include - openssl 헤더 파일
  • C:\Program Files\OpenSSL\html - openssl 메뉴얼

4. Etc...

 위 과정으로 설치는 끝이다. 간단히 참고할 만한 사항을 정리하자면..
  1. 헤더 파일은 C:\Program Files\OpenSSL\include 폴더에 있으므로 프로젝트의 헤더 파일 경로에 추가해주어야 한다. 그 안에 openssl 폴더가 있지만 C:\Program Files\OpenSSL\include까지만 지정해주어야 한다. 헤더 파일을 include할 때 #include <openssl/ssl.h>와 같이 지정하기 때문이다.
  2. openssl 라이브러리를 사용할 때 .lib 파일은 C:\Program Files\OpenSSL\lib 폴더에 있지만 .dll과 .pdb 파일은 C:\Program Files\OpenSSL\bin 밑에 있으므로 참고.
  3. 설치 과정 완료 후에 라이브러리 뿐 아니라 openssl.exe를 사용하려고 한다면 C:\Program Files\OpenSSL\bin 폴더의 경로를 PATH에 지정해 주는 게 좋겠다. 자동으로 등록해주진 않는다.

2017년 4월 4일 화요일

macOS에 OpenSSL 설치하기

 예전에는 macOS에 기본적으로 OpenSSL Framework가 탑재되어 있었는데 언제부턴가 Xcode를 설치하고 OpenSSL Framework를 찾아보면 이게 없다고 나온다. 정확히 기억은 나지 않지만 애플에서 공식적으로 삭제했다는 얘기도 들은 것 같고 최신 버전으로 유지가 되는 것도 아니었기 때문에 필요할 때마다 새로 설치를 해서 사용을 해왔다.  문제는 매번 설치를 하려고 할 때마다 인터넷을 찾아보면 여러 가지 설치방법이 난무하여 이게 제대로 설치한 건지 확신이 서지 않았다는 것이다.

 그래서 오늘 새로 설치를 할 일이 있어서 여기서 정리를 해 볼까 한다.

1. Source Download

 아래 OpenSSL 사이트에 접속해서 최신 소스를 다운로드하도록 하자.
    URL: https://www.openssl.org/
현재 최신 버전은 openssl-1.1.0e.tar.gz 이다. 어차피 설치되고 나면 지워도 되는 파일이므로 원하는 폴더에 압축을 풀고 그 폴더로 이동한다.

2. Build & Install

 압축을 푼 폴더의 루트에 보면 INSTALL 파일이 있는데 이 파일이 설치 방법에 대해 자세히 나와있는 파일이다. 파일을 열어보면 Unix, OpenVMS, Windows의 경우에 대해서 각각 설치 방법이 설명되어있다. macOS는 Unix 계열 운영체제이므로 Unix를 따르면 된다. 예전에는 macOS에 설치를 하기위한 부분이 조금 있었던 것 같은데 지금은 전혀 없다. 이 말은 Unix와 똑같은 방법으로 설치를 하면된다는 뜻이다. 실제로 config 단계에서 아무 옵션을 지정하지 않아도 출력되는 메시지들을 자세히 보면 모두 제대로 인식되는 것으로 보인다. 설치 방법은 터미널을 열고 아래 명령을 순서대로 수행하면 된다.
$ ./config
$ make
$ make test
$ sudo make install
 위 명령 중 'make test'는 생략해도 되지만 혹시나 하는 마음이 있다면 수행해서 제대로 빌드가 되었는 지 확인해 보는 것이 좋을 것이다. 그리고 마지막 install 명령은 super user 권한이 필요하기 때문에 sudo 를 붙여주어야 한다. INSTALL 문서에는 나와있지 않지만 macOS에서는 반드시 필요하다.

 config를 수행해서 출력 메시지들을 자세히 보면 darwin64-x86_64 버전으로 빌드가 된다는 것을 알 수 있다. 설치 경로를 특별히 지정하지 않으면 기본 경로는 /usr/local이다. /usr/local에 가서 확인해보면 아래와 같이 적절히 설치되어 있는 걸 확인할 수 있다.
  • /usr/local/bin - openssl 실행 파일
  • /usr/local/lib - openssl 라이브러리 파일
  • /usr/local/include - openssl 헤더 파일
  • /usr/local/share - openssl 문서 & 메뉴얼 파일
  • /usr/local/ssl - openssl 설정 & 인증서 경로
 /usr/local이 아닌 다른 경로에 설치하고 싶다면 ./config 명령에 아래와 같은 옵션을 주면 된다.
$ ./config --prefix=/other/path

3. Etc...

 위 과정으로 설치는 끝이다. 간단히 참고할 만한 사항을 정리하자면..
  1. 원래 macOS에 탑재된 openssl 실행파일은 /usr/bin에 있고 새로 설치된 openssl 실행파일은 /usr/local/bin에 있으므로 터미널에서 그냥 openssl을 실행하면 기존 구버전 openssl이 실행될 것이다. 새로 설치된 openssl을 실행하려면 full path를 지정하던가 사용자 환경에 맞게 쉘의 PATH설정을 바꿔서 /usr/local/bin에 있는 openssl이 실행되도록 해주어야 한다. 나같은 경우에는 운영체제의 기본 설정을 바꾸기 싫어하고 라이브러리와 헤더만 사용하기 때문에 따로 설정을 바꿔주진 않고 그대로 사용중이다.
  2. Xcode에서 openssl 라이브러리를 사용하려면 프로젝트의 빌드 세팅에서 헤더 파일 경로를 추가해 주어야 한다. 경로는 /usr/local/include 이다. 그 안에 openssl 폴더가 있지만 openssl 경로를 포함시키지 않아야 한다. 헤더파일을 include할 때 #include <openssl/ssl.h>처럼 하기 때문이다.
  3. 라이브러리 파일은 /usr/local/lib에 설치되어있고 static 라이브러리는 libssl.a, libcrypto.a 두 개 파일이다. dynamic 라이브러리는 확장자가 .a가 아니고 .dylib이므로 원하는 대로 설정해서 사용한다.

2017년 3월 31일 금요일

C++ Special Member Functions

 Special Member Function이라함은 컴파일러가 필요할 때 직접 생성해주는 멤버 함수를 말한다. 즉 아래 함수들이 Special Member Function들이다.
  • default constructor
  • destructor
  • copy constructor
  • copy assignment operator
  • move constructor
  • move assignment operator
 이 함수들은 사용자가 정의하지 않은 경우 컴파일러가 기본 구현을 제공하는 함수들이다. 하지만 항상 기본 구현이 제공되는 것은 아니고 몇 가지 조건이 만족될 때만 기본 구현을 제공한다. 이번 글에서는 그 규칙에 대해서 정리해보고자 한다.

1. 기본 생성자(default constructor)

 클래스에 생성자가 하나도 정의되어 있지 않은 경우에만 기본 생성자를 제공한다. 기본 생성자(default constructor)는 인자가 하나도 없는 생성자를 말한다. 클래스의 생성자가 단 하나도 정의되지 않은 경우에는 컴파일러가 기본 생성자를 제공하는데 제공되는 기본 생성자의 구체적인 구현은 각 멤버의 기본 생성자가 호출되는 것과 같다. 따라서 컴파일러가 생성하는 기본 생성자를 이용하려면 클래스의 모든 멤버들도 역시 기본 생성자를 제공해야 한다는 조건이 하나 더 붙는다.

2. 소멸자(destructor)

 클래스에 소멸자가 정의되어있지 않으면 컴파일러가 자동으로 기본 구현을 제공한다. 상속받은 부모 클래스의 소멸자가 virtual인 경우에는 마찬가지로 virtual로 선언된 소멸자를 제공하고 그렇지 않은 경우에는 non-virtual이다. 그리고 디폴트로 noexcept 소멸자를 제공한다.

3. 복사 생성자(copy constructor)

 클래스에 복사 생성자(copy constructor)가 정의되어 있지 않으면 컴파일러가 기본 구현을 제공한다. 하지만 제공되는 구현이 경우에 따라 다르다.
  1. 이동 생성자 또는 이동 대입 연산자가 선언되어 있으면 복사 생성자는 delete된 선언이 제공된다.
  2.  이동 생성자 또는 이동 대입 연산자가 선언되어 있고 복사 생성자가 선언되지 않았을 때는 컴파일러가 복사 생성자를 delete로 선언한다. 즉, delete된 구현을 제공하는 것이다. 이 상태에서 복사 생성자를 호출하는 코드가 어딘가에 존재한다면 컴파일러는 delete된 함수를 호출한다는 에러를 발생시킬 것이다.
  3. 소멸자 또는 복사 대입 연산자가 선언되어있으면 컴파일러가 복사 생성자를 제공하지만 이 기능은 deprecated되었다.
  4.  뭔가 말이 안되는 것 같지만 설명을 해 보자면, 이런 경우 컴파일러가 각 멤버를 복사 생성하는 구현을 제공하긴 하지만 이렇게 기본 구현을 제공하는 기능은 deprecated되었다는 것이다. 이런 저런 환경에서 개발을 하다보면 오래된 API들이 deprecated된 경우를 볼 수 있을 것이다. 호환성때문에 존재하지만 언젠가는 사라질 수 있는 API들을 그런 상태로 선언해 놓는다. 마찬가지로 이러한 컴파일러의 기능도 C++표준에서는 사용하지 말도록 권고하는 것이며 직접 구현을 제공할 것을 권고하는 것이다.
  5. 그 외에는 복사 생성자가 선언되지 않았다면 컴파일러가 기본 구현을 제공한다.

4. 복사 대입 연산자(copy assignment operator)

 클래스에 복사 대입 연산자(copy assignment operator)가 정의되어 있지 않으면 컴파일러가 기본 구현을 제공한다. 복사 생성자와 복사 대입 연산자가 서로 대칭될 뿐 같은 규칙으로 제공된다.
  1. 이동 생성자 또는 이동 대입 연산자가 선언되어 있으면 복사 대입 연산자는 delete된 선언이 제공된다.
  2.  이 규칙은 복사 생성자와 같다.
  3. 소멸자 또는 복사 생성자가 선언되어 있으면 컴파일러가 복사 대입 연산자를 제공하지만 이 기능은 deprecated되었다.
  4.  복사 생성자 대신 복사 대입 연산자가 되었을 뿐 규칙은 복사 생성자의 경우와 같다. 즉 제공은 되지만 사용을 하지 않는 게 좋다.
  5. 그 외에는 복사 대입 연산자가 선언되지 않았다면 컴파일러가 기본 구현을 제공한다.

5. 이동 생성자(move constructor)

 이동 생성자(move constructor)는 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자가 모두 선언되지 않았을 때만 기본 구현이 제공된다. 기본 구현은 각 멤버별로 이동 생성자가 호출되는 것과 같다.

6. 이동 대입 연산자(move assignment operator)

 이동 대입 연산자(move assignment operator)는 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자가 모두 선언되지 않았을 때만 기본 구현이 제공된다. 즉, 이동 생성자와 같은 규칙이다. 기본 구현은 각 멤버별로 이동 대입 연산자가 호출되는 것과 같다.


 C++98 시대의 C++ 개발자들에게는 3의 규칙이라는 것이 있어서 소멸자, 이동 생성자, 이동 대입 연산자 중에 하나라도 직접 선언했다면 나머지 두 개도 직접 선언해야 한다는 암묵적 규칙이 있었다. 3개 함수중에 하나라도 기본 제공되는 구현이 적당하지 않다면 나머지 2개도 적당하지 않을 확률이 매우 높기 때문에 생긴 규칙이다. 하지만 위 규칙에서 보듯이 암묵적일 뿐 컴파일러 강제사항은 아니다.

 C++11이 되면서 move semantics 개념이 도입되어 이동 연산이 추가되고 3의 규칙은 이동 함수들에도 확장되었다. (5의 규칙?) 하지만 과거의 코드들과 호환성을 유지해야 할 필요성 때문에 이동 함수들만 5개 함수들 중 하나라도 선언되어 있으면 자동 생성되지 않도록 결정되었고 나머지는 거의 그대로 유지가 되고 있다. 복사 함수들이 이동 함수들이 선언된 경우에는 delete로 처리되는 것도 3의 규칙의 확장선 상의 규칙이며 C++11에서 추가되었는데 이것은 이동 함수들이 선언된 경우이므로 C++98 시대의 코드에는 영향을 미치지 않기 때문이다. 3의 규칙과 달리 확장된 규칙들은 컴파일러 강제사항이기 때문에 직접 처리할 수 밖에 없다.

 그렇다면 virtual 소멸자가 필요해서 소멸자를 선언했을 뿐인데 다른 모든 special member function들의 기본 구현을 이용할 수 없게 된다면 어떨까? 이 경우 개발자는 확실히 이것이 불편하다고 느낄 것이다. 이런 경우를 위해서 기본 구현을 간단히 선언할 수 있는 방법이 있다.
class Sample {
public:
  virtual ~Sample() = default; // default destructor

  Sample(const Sample&) = default;            // default copy constructor
  Sample& operator=(const Sample&) = default; // default copy assignment

  Sample(Sample&&) = default;            // default move constructor
  Sample& operator=(Sample&&) = default; // default move assignment

 
  ...
};
함수의 구현을 제공하는 대신 선언부에 '= default'를 붙여주면 컴파일러가 제공하는 기본 구현이 그대로 적용된다. 위의 규칙 중 3번, 4번에서 deprecated된 기능을 사용하지 않고 기본 구현을 사용하는 간단한 방법으로 이렇게 default 선언을 사용하면 사용자가 직접 함수를 선언한 것이 되므로 deprecated된 구현을 사용하는게 아니며 추후에 생길 수 있는 문제를 피할 수 있다. 또한 default 구현을 이용하겠다는 뜻으로 이렇게 명시적으로 선언하는 것은 좋은 습관이다.

2017년 3월 9일 목요일

C++ deleted functions (함수의 삭제 선언)

 C++11에 새롭게 추가된 함수의 삭제(delete)에 대해서 정리해보려고 한다. 어떤 함수를 삭제(delete)시키면 그 함수에 대한 어떤 호출도 불가능하게 된다. 물론 함수를 사용하지 못하게 할 거라면 그냥 함수를 작성하지 않으면 된다. 하지만 문제가 발생하는 함수들은 컴파일러가 자동으로 생성해주는 함수들이다. C++ 컴파일러는 클래스의 생성자, 소멸자, 대입연산자 등을 필요에 의하여 자동으로 생성해주는 기능이 있다. 하지만 개발자는 일부러 그런 함수들을 사용하지 못하게 하려고 만들지 않았는데 컴파일러가 알아서 생성해 준다면 꽤 귀찮은 문제가 된다. 이럴 때 사용하는 방법이 함수를 delete시키는 것이다.

 C++11 이전에는 그런 메소드를 private으로 선언함으로써 다른 개발자들이 사용할 수 없도록 하였으나 C++11에서는 아래 예와 같이 함수를 delete시키는 방법을 제공해 준다.
class NoCopyClass {
public:
    ...
    NoCopyClass(const NoCopyClass&) = delete;
    NoCopyClass& operator=(const NoCopyClass&) = delete;
    ...
};
 복사 생성자와 복사 대입 연산자를 삭제함으로써 클래스의 객체를 복사할 수 없게하려고 한다는 것을 짐작할 수 있다.(이렇게 의도가 전달되는 것은 좋은 일이다.) 이런 생성자와 연산자들은 컴파일러가 기본 구현을 제공하는 것들이지만 개발자가 직접 삭제 함수(deleted function)로 구현한 것이기 때문에 컴파일러도 기본 구현을 제공하지 않고 다른 개발자들이 호출할 수도 없게 되는 것이다.

 참고로 함수의 삭제 선언은 public으로 선언하는 것이 관례이다. 왜냐하면 컴파일러가 함수의 삭제여부 이전에 private여부를 먼저 판단하기 때문에 삭제된 함수에 대한 정확한 메시지를 출력하도록 유도하려면 public으로 선언해야하기 때문이다.

 함수의 삭제는 private으로 선언하는 방법보다 좋은 점이 몇 가지 있는데 클래스의 멤버 함수 외에 모든 함수가 삭제 가능하다는 점이다. 이러한 점을 이용해서 "함수 인자의 암묵적인 변환을 이용한 호출"을 막을 수도 있다. 아래 예에서 isLucky()함수는 정수형을 받는 함수지만 암묵적 변환이 가능한 인자들을 넣고 호출해도 모두 호출이 된다. 하지만 호출을 막고자 하는 타입을 받는 overload 함수를 delete로 선언해버리면 암묵적 변환 전에 완벽하게 매칭되는 함수가 호출되는 것이 우선이므로 삭제된 함수를 호출하게 되고 따라서 호출 불가 판정을 하게 되는 것이다.
bool isLucky(int number);
bool isLucky(double) = delete;

if (isLucky(7)) // 호출 가능
    ...
if (isLucky('a')) // 호출 가능
    ...
if (isLucky(3.5)) // 호출 불가능. 에러!
    ...
 double 타입 인자를 받는 isLucky() 함수가 선언되지 않았으면 3.5는 int형으로 변환되어 호출이 되었을 것이다. 하지만 double 인자 버전이 delete로 선언되었기 때문에 삭제된 함수에 매칭이 되어 호출이 되지 않고 에러를 발생시킨다. 알아두면 가끔 유용하게 쓰일 것 같다.

 함수의 삭제는 템플릿의 특정 특수화(specialization) 버전을 삭제함으로써 그 함수가 생성되는 것을 막는데 이용할 수도 있다. 함수 템플릿은 컴파일러가 추론된 타입에 맞춰서 그때그때 생성하게 되는데 특정 타입에 대한 함수를 생성하지 못하도록 하려고 할 때 미리 사용을 막고자 하는 타입의 특수화 버전을 delete로 선언해버리면 컴파일러는 특수화 함수가 존재하므로 코드를 생성하지 않고 삭제된 함수이므로 호출도 하지 못하게 되는 것이다. 아래 예를 보면 어떤 상황인지 어렵지 않게 이해할 수 있다.
template<typename T>
void processPointer(T* ptr);

template<>
void processPointer<void>(void*) = delete; // void 포인터는 호출 불가!
 void 타입의 특수화 함수를 선언하고 삭제함으로써 그런 함수가 생성되는 걸 막고 당연히 호출도 불가하게 만드는 것이다.

 위와 같이 함수의 delete 선언이 주는 장점은 여러가지가 있지만 사실 가장 큰 장점은 코드의 의도를 분명히 할 수 있다는 데 있다. 특정 메소드를 사용하지 못하게 하려고 private의 선언하는 것보다 delete로 선언하는 게 더 분명하게 의도를 전달할 것이고 그런 코드를 읽게 된다면 무엇을 의도하고 함수를 삭제했는지 파악하는 것도 private으로 선언하는 것보다 훨씬 쉬울 것이다. 코드를 읽기 쉽게, 의도를 분명하게 작성하는 건 기본 중의 기본이고 항상 바람직한 일이다.

2017년 3월 6일 월요일

C++ Unscoped enumerations VS. Scoped enumerations

 C++11에 새로 추가된 것들 중에 enum class라는 것이 있다. 원래부터 있었던 enum과 유사하지만 좀 더 발전된 것이라고 할 수 있겠다. Effective Modern C++에서는 원래 있던 enum을 unscoped enum으로 새로 추가된 enum class를 scoped enum으로 구별해서 부른다. 선언 방식은 아래 예와 같다.
enum Color { black, white, red }; // unscoped enum
int white; // 에러
enum class Color { black, white, red }; // scoped enum
int white; // 문제 없음
 선언 방법에 있어서 scoped enum의 경우 "enum class"를 사용한다는 점이 이전과 달라진 점이다. 두 가지 방식을 scoped와 unscoped로 부르는 이유는 말 그대로 black, white, red의 namespace 범위가 두 경우에 대해 다르게 적용되기 때문이다. 위 예를 가지고 설명해 보자면 unscoped의 경우에는 black, white 등의 이름은 Color와 같은 namespace를 갖는다. 즉, Color라는 이름이 유효한 공간에서는 black, white 등의 이름도 모두 유효하다. 따라서 다른 변수나 함수 이름 등으로 사용할 수가 없다. 하지만 scoped의 경우에는 black, white 등의 이름이 Color 클래스 내부에서만 유효하다. 즉, Color의 중괄호 {} 안에서만 유효하므로 그 밖에서는 그 이름을 다른 변수나 함수 이름으로 사용하는 데 아무런 문제가 없다. 물론 enum값을 사용할 때 Color::black과 같이 사용해야 하는 게 불편할 수도 있지만 내가 보기엔 더 명확하고 좋은 것 같다.

 사실 이렇게 namespace가 오염(?)된다는 것만으로 scoped enum을 사용하라고 한다면 그다지 크게 공감이 가진 않는다. Effective Modern C++에서는 몇 가지 장점을 더 제시하고 있다. 그 중 한 가지가 scoped enum의 경우에는 전방 선언(forward declaration)이 가능하다는 것이다. unscoped enum의 경우에는 전방선언을 할 수 없었지만 scoped enum의 경우 이게 가능해지면서 소스 파일간의 dependency를 줄여서 헤더 파일을 약간 수정함으로써 생기는 많은 소스 코드의 재컴파일을 줄일 수 있다는 장점이 있다. C++로 정말 큰 프로젝트를 수행한다면 빌드 타임을 줄일 수 있다는 점은 큰 메리트가 될 것이다. 하지만 사실 이 점도 나에게는 그렇게 크게 공감가는 부분은 아니다. 대부분 큰 프로젝트를 수행하다보면 어느 정도 작은 크기의 모듈로 나누어지게 마련이고 그러다보면 한 번 빌드하는데 5분이상 걸리는 경우를 만나는 일도 별로 없기 때문이다. 게다가 unscoped enum도 약간의 수고를 더한다면 전방선언이 가능하기 때문에 별로 큰 장점은 아니다. 이 글을 계속 읽어보면 전방선언이 가능하다는 걸 알게 될 것이다.

 Effective Modern C++이 제시하는 장점 중에 내가 생각하는 가장 큰 장점은 타입 체크를 더 강하게 한다는 것이다. Java 같은 언어에서 enum의 타입은 class와 동등하게 엄격한 타입 검사가 적용된다. 하지만 C++에서 (unscoped) enum은 그냥 정수형처럼 사용해도 아무 제지를 받지 않는다. 심지어 float같은 상수와 연산을 해도 암묵적 변환이 자동으로 이루어진다. 물론 이게 편하다고 생각하는 개발자도 있겠지만 적어도 바람직하지 않은 것만은 사실이다. 하지만 C++11에서 추가된 scoped enum의 경우에는 정수형으로의 암묵적 변환이 허용되지 않는다. 필요하다면 개발자가 암묵적이 아닌 직접적인 캐스팅을 해주어야 문제가 없다. 이런 방식이 개발자의 실수를 줄이고 이해하기 쉬운 코드를 만드는데 훨씬 유리하다고 나는 확신한다. Java같은 최신 언어들로부터 느낀 많은 장점들이 C++에도 조금씩 조금씩 적용된다는 건 바람직한 일이 아닐 수 없다.

 많은 개발자들이 신경쓰지 않고 사용했겠지만 enum을 선언할 때 개발자가 직접 기초 타입(underlying type)을 지정할 수 있다. 아래 예에서 보듯이 unscoped와 scoped 둘 다 지정이 가능하다.
 // unscoped enum
enum Status: std::uint32_t { good = 0,
                             failed = 1,
                             incomplete = 100,
                             corrupt = 200,
                             audited = 500,
                             indeterminate = 0xFFFFFFFF
                           };

// scoped enum
enum class Status: std::uint32_t { good = 0,
                                   failed = 1,
                                   incomplete = 100,
                                   corrupt = 200,
                                   audited = 500,
                                   indeterminate = 0xFFFFFFFF
                                 };

 위의 예처럼 기초 타입을 직접 지정하는 경우에는 unscoped enum의 경우에도 전방 선언이 가능하다. 전방 선언이 허용되지 않았던 이유는 enum의 기초 타입이 지정되지 않았을 때는 컴파일러가 enum의 정의를 보고 적당한 크기의 정수형(integral) 타입을 임의로 결정하여 사용하기 때문에 기초 타입이 지정되지 않은 전방 선언 만으로는 그 크기를 알 수 없으므로 전방선언을 허용하지 않았던 것이다. 하지만 scoped enum의 경우에는 직접 지정하지 않았을 때 default로 int형을 사용하게 되어있다. 더 작거나 큰 타입을 사용하고 싶다면 직접 지정해서 사용하면 된다.

 참고로 unscoped enum이 더 편리한 경우를 들어보자면 array나 std::tuple의 인덱스로 사용할 정수형 상수(constant)를 enum으로 선언해놓고 사용할 때이다. 이 경우에는 casting이 필요없는 unscoped enum이 더 사용하기 편리하다. 아래 두 버전의 차이를 보자.
using UserInfo = std::tuple<std::string,  // name
                            std::string,  // email
                            std::size_t>; // reputation

// unscoped enum
enum UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;

...

auto val = std::get<uiEmail>(uInfo); // emil 필드의 값 얻기
// scoped enum
enum class UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;

...

auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo); // 캐스팅 필요
 굳이 추가 설명을 하지 않아도 unscoped 버전이 훨씬 가독성이 좋고 타이핑해야 할 코드도 짧다. 게다가 이런 식의 사용은 많은 개발자들이 애용하는 방법이기때문에 unscoped enum의 장점을 무시할 수도 없다.

 unscoped enum과 scoped enum을 비교했을 때 정말 치명적인 장단점이 있다고는 생각치 않는다. unscoped enum이 전혀 불편함이 없고 익숙하다면 뭐 그리 대단히 사용하지 말아야 할 이유가 있는 건 아니라는 뜻이다. 하지만 내 의견은 scoped enum을 사용하는 것이 개발자의 실수를 줄이고 코드의 의도를 더 분명히 할 수 있는 더 좋은 습관이라고 생각한다. 그래서 C++표준에 추가된 것일 테고 새로운 세대의 개발자들이 scoped enum을 사용해주길 기대하고 있을 거라 생각한다.

2017년 3월 2일 목요일

C++ type alias : 'using'

 C++11 이전의 C++ 개발자들이 자주 사용하던 using 키워드의 용도는 namespace를 계속 입력하는 게 귀찮아서 STL 사용 시 아래와 같은 선언을 코드의 앞 부분에 추가해주고 "std::"를 생략하기 위함일 것이다. (내 경우에는 그랬다)
using namespace std;
...
 C++11에서 using의 기능을 새롭게 추가하였는데 그것이 타입 별칭(Type alias)을 선언하는 것이다. 기존에 typedef라는 키워드를 이용해서 STL 등에서 제공하는 긴 iterator 타입을 간단하게 선언해서 사용해 본 경험이 많이들 있을 거라고 생각한다. using을 이용한 타입 별칭 선언은 typedef가 하는 일과 마찬가지로 기존 타입을 새로운 이름으로 선언하는 것이다. 아래 함수 포인터 타입을 선언하는 2가지 예를 보면서 어떤 게 더 보기 좋은지 한번 생각해보자.
typedef void (*FP)(int, const std::string&);  // typedef 이용
using FP = void (*)(int, const std::string&); // using 이용
 다른 건 몰라도 함수 포인터 타입을 선언할 때는 using이 훨씬 분명하게 뜻이 전달되는 것 같다. 하지만 이것만이 기존 typedef를 사용하던 습관을 버리고 using을 사용해야 할 이유라면 아마도 typedef가 익숙한 개발자에게는 별로 납득할만한 이유는 되지 못할 것 같다. Effective Modern C++책 에서는 typedef보다는 using을 사용하라고 권장하며 그 이유에 대해서도 설명하고 있는데 여기서 그 이유에 대해 정리를 해 보려고 한다.

 using을 이용한 타입 선언이 typedef와 크게 구별되는 점이 하나 있다. using의 경우에는 템플릿화가 되지만 typedef는 그렇지 못하다는 점이다. 아래 예를 보자.
template<typename T> // std::list<T, MyAlloc<T>>의 별칭은 MyAllocList<T>
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw; // client code
 C++11에서는 이렇게 using을 이용해서 간단히 T에 따라 변화하는 템플릿 타입의 별칭을 선언할 수 있지만 기존 typedef의 경우에는 템플릿화할 수 없기 때문에 이러한 목적을 이루기 위해서 아래 예와 같은 방법을 사용하였다.
template<typename T> // std::list<T, MyAlloc<T>>의 별칭은 MyAllocList<T>::type
struct MyAllocList {
  typedef std::list<T, MyAlloc<T>> type;
};

MyAllocList<Widget>::type lw; // client code
 템플릿 구조체를 하나 선언해서 그 안에서 typedef를 이용하여 별칭을 선언하는 것이다. 이렇게 함으로써 실제 타입을 사용할 때 "::type"을 덧붙인다는 소소한 불편함을 제외하면 같은 동작을 하도록 할 수 있었다. 하지만 이렇게 선언된 타입은 템플릿 안에서 사용할 때 또 다른 불편함이 발생한다. 아래 예를 보자.
template<typename T>
class Widget {
private:
  typename MyAllocList<T>::type list; // MyAllocList<T>를 멤버로 선언
  ...
};
 MyAllocList::type 타입의 멤버를 선언하는 데 그 앞에 typename이라는 키워드를 써주어야 하는 불편함이 생긴다. 컴파일러가 MyAllocList::type을 만났을 때 이게 타입 이름인지 아니면 MyAllocList의 데이터 멤버인지 알 수가 없기 때문에 typename이라고 앞에 지정함으로써 타입 이름이라고 알려주는 것이다. using을 이용한 타입 별칭 선언은 항상 타입이라는 게 확실하기때문에 아래 예에서 보듯이 typename을 따로 지정하지 않는다.

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

template<typename T>
class Widget {
private:
  MyAllocList<T> list;// "typename", "::type"이 없다.
  ...
};

 참고로 한 가지 더 정리하자면 using을 이용한 타입의 별칭 선언은 C++11에서 추가되었지만 STL의 많은 코드는 이전과의 호환성 때문에 이전 방식인 구조체 안에 type을 선언하는 방식을 그대로 유지하고 있다. 하지만 C++14에서는 이전보다 더 간단하게 사용할 수 있도록 같은 기능을 제공하는 템플릿 선언들을 추가로 제공한다. 아래 예에서 보듯이 기존 구조체 이름에 '_t'를 붙여서 같은 기능을 제공하도록 선언하였다.
std::remove_const<T>::type // C++11: const T → T
std::remove_const_t<T>     // C++14 버전

std::remove_reference<T>::type // C++11: T&/T&& → T
std::remove_reference_t<T>     // C++14 버전

std::add_lvalue_reference<T>::type // C++11: T → T&
std::add_lvalue_reference_t<T>     // C++14 버전
 위 예는 헤더파일에서 제공하는 선언들이다. 새롭게 추가된 선언들의 이름이 무엇인지 어렵지 않게 예상할 수 있을 것으로 생각된다.

2017년 2월 27일 월요일

decltype에 대하여

 Effective Modern C++에는 decltype에 대한 설명이 잘 나와있다. 책을 읽기 전까지는 언제 이런 키워드가 사용되는지 궁금할 정도로 거의 쓰이지 않을 것 같았는데 이 책을 읽고나니 나름 쓰임새가 있다는 것을 알 수 있었고 그 동작에 대해서도 깊게 이해할 수 있었던 것 같다.

 decltype은 declared type의 줄임말이다. 즉 선언된 타입을 알려주는 키워드이다. 이것도 템플릿이나 auto 키워드 처럼 컴파일타임에 유효한 것이지 런타임에 동적으로 타입을 식별해서 알려주는 것은 아니니 decltype을 이용해서 런타임에 동적으로 뭔가 하려고 했다면 생각을 바꿔야할 것이다. 최근에 나오는 많은 프로그래밍 언어들이 런타임에 동적으로 타입을 체크할 수 있는 기능들을 대부분 제공하다보니 decltype에 대해서 오해할 소지가 있기에 컴파일 타임에 정적으로 타입이 결정되어 사용된다는 것을 확실히 하고 가는 것이 좋겠다.

 먼저 간단히 아래 코드의 주석을 참고하면 decltype이 어떻게 동작할 지 알 수 있을 것이다. 사실 별로 예상과 어긋나는 동작은 없다.
const int i = 0; // decltype(i)는 const int

bool f(const Widget& w); // decltype(w)는 const Widget&
                         // decltype(f)는 bool(const Widget&)
struct Point {
  int x, y;   // decltype(Point::x)는 int
};            // decltype(Point::y)는 int

Widget w;     // decltype(w)는 Widget

if (f(w)) ... // decltype(f(w))는 bool

template<typename T> // std::vector의 단순화 버전
class vector {
public:
  ...
  T& operator[](std::size_t index);
  ...
};

vector<int> v; // decltype(v)는 vector<int>
...

if (v[0] == 0) ... // decltype(v[0])는 int&. 일반적으로 []연산자가 반환하는 타입은 참조형임
 이렇게 보니 decltype의 동작은 별로 예상과 다르지 않다. 게다가 별로 쓰일만한 곳도 없어 보인다. 일반적으로 컴파일러가 알아서 타입을 추론하도록 할 때는 auto 키워드가 쓰이기 때문에 decltype으로 타입을 알아내야 할 때가 있을까 싶기도 하다.

 책에서 말하는 decltype의 대표적인 쓰임새는 함수 템플릿의 반환값이 템플릿 인자에 의존하여 타입이 결정되지 않은 값을 반환해야할 때이다. 아래 예제를 보자. 이 예제는 일반적인 컨테이너의 []연산자와 같은 동작을 하는 함수를 구현한 것인데 값을 반환하기 전에 사용자 인증을 수행한다는 가정하에 구현된 함수이다.
template<typename Container, typename Index> // 완벽한 구현은 아니지만 뒤에 보완할 것임.
auto authAndAccess(Container& c, Index i)
  -> decltype(c[i])
{
  authenticateUser();
  return c[i];
}
 함수의 이름 앞에 auto가 지정되어 있다. 사실 이런 auto는 타입 추론과 아무 상관이 없으며 trailing return type 문법이 사용된다는 표시일 뿐이다. 이렇게 trailing return type 방식을 사용하면 함수의 인자들을 return 타입을 지정할 때에 이용할 수 있다는 장점이 있다. 위 예에서는 return 타입으로 decltype(c[i])를 사용함으로써 템플릿 인자 Container와 Index의 타입에 상관없이 return 타입을 지정할 수 있었다. 이런 식으로 decltype이 사용되는 것이다. 참고로 일반적인 컨테이너 c의 원소 타입이 T라면 함수의 return 타입은 T&이 된다.

 여기서 전에 올린 auto 타입 추론에서 설명했던 것처럼 C++14에서는 함수의 return 타입으로 auto를 지정하여 타입 추론이 되도록 할 수 있으므로 C++14에서는 아래와 같이 수정할 수 있다.
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)  // C++14. 사실 제대로된 구현은 아님.
{
  authenticateUser();
  return c[i]; // return 타입은 c[i]로부터 추론된다.
}
 이렇게 수정하면 C++14 컴파일러는 함수의 return문을 분석하여 타입 추론을 수행한다. auto 타입 추론에서 설명했듯이 이 경우 타입 추론은 템플릿 타입 추론과 같은 방식으로 이루어지며 따라서 c[i]의 referenceness가 제거되어 타입이 결정된다. 즉 c[i]의 타입은 T&지만 함수의 return 타입은 T로 추론되는 것이다. 일반적으로 []연산자는 원소의 참조형을 반환해야 하는데 위 구현은 원소의 복사본이 반환되게 된다.

 C++14에서는 이런 경우를 극복할 수 있도록 decltype(auto)를 사용하도록 하였다. 아래 예제를 보자.
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)  // C++14.
{
  authenticateUser();
  return c[i]; 
}
 decltype(auto)를 return타입으로 지정하면 반환되는 표현식을 그대로 decltype에 넘긴 것과 같은 타입으로 추론된다. 따라서 decltype(c[i])의 타입 그대로 참조형(T&)을 반환하게 되는 것이다. 이런 식의 타입 추론을 "decltype 타입 추론"이라 한다.

책에서는 따로 설명하지 않았지만 위 예제의 경우 항상 참조형을 반환해야 한다면 decltype(auto)대신 auto&를 지정해도 문제가 되지 않는다. 하지만 혹시 c[i]가 참조형이 아니라 비-참조형 값이라면 또 다시 문제가 발생하게 된다. 왜냐하면 Container의 []연산자가 특별히 비-참조형을 반환하도록 설계되어 있다면 반환되는 값은 c[i]의 복사본이 될 텐데 복사본의 참조형을 반환하는 건 위험한 일이기 때문이다. 역시 decltype(auto)가 참조형을 반환하던 비-참조형을 반환하던 상관없이 문제를 일으키지 않는 유일한 해결책이다.

decltype(auto)는 함수의 리턴 타입에서만 사용되는 것은 아니다. 아래 예제에서 처럼 일반 변수의 선언에서도 사용될 수 있다.
Widget w;
const Widget& cw = w;

auto myWidget1 = cw;           // auto 타입 추론:
                               // myWidget1은 Widget

decltype(auto) myWidget2 = cw; // decltype 타입 추론:
                               // myWidget2는 const Widget&
 참고로 책에서는 위의 authAndAccess() 예제가 rvalue에 대해서도 완전하게 동작하려면 아래 예와 같이 Universal 참조와 std::forward를 사용하여 구현해야 한다고 설명하지만 decltype(auto)에 추가적인 의미를 부여하는 것은 아니기 때문에 설명은 따로 하지 않겠다.
template<typename Container, typename Index> // final C++14 version
decltype(auto) authAndAccess(Container&& c, Index i)
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

 decltype(auto)에 대해 한 가지 더 주의해야 할 것이 있다. 대부분 decltype이 개발자의 예상대로 동작하지만 개발자의 예상을 종종 빗나가는 경우가 있다. 일반적인 변수의 이름에 decltype을 적용하면 실제 그 변수가 선언된 타입이 도출된다. 이 경우는 아무런 문제가 없다. 그러나 단순 이름이 아닌 "lvalue 표현식"을 decltype에 넘겼을 때 나오는 타입이 문제가 된다. 이 경우 lvalue 표현식이 평가되는 타입은 lvalue 참조형이다. 실제로 lvalue 표현식의 타입은 lvalue 참조형이 맞기 때문에 사실 잘못된 건 아니다. 하지만 아래 예를 보면 충분히 헷갈릴만 한 경우가 있음을 알 수 있다.
int x = 0;

decltype(auto) a = x;   // a는 int
decltype(auto) b = (x); // b은 int&. "(x)"는 lvalue 표현식이기 때문에

decltype(auto) f1()
{
  int x = 0;
  ...
  return x; // decltype(x)은 int, 따라서 f1은 int형 반환
}

decltype(auto) f2()
{
  int x = 0;
  ...
  return (x); // decltype((x))은 int&, 따라서 f2는 int& 반환
}
위 예제에서 보듯이 단순한 이름이 아닌 lvalue 표현식의 타입은 lvalue 참조형으로 평가되기 때문에 "(x)"가 lvalue 참조형으로 평가되는 것이다. 이것이 f1과 f2의 커다란 차이를 만들게 된다. 이미 짐작하고 있겠지만 f2는 지역 변수의 참조형을 반환하는 위험한 동작을 하는 잘못된 함수라는 것이다. 이 처럼 decltype(auto)를 사용할 때 주어지는 표현식은 주의깊게 작성해야 함을 기억하기 바란다.

 이렇게 C++11, C++14 버전에서 새롭게 추가된 타입 추론의 동작에 대해서 정리를 해보았다. 혹시 이전 내용이 궁금하다면 아래 링크를 참고하기 바란다.

  1. 템플릿 타입 추론
  2. auto 타입 추론

2017년 2월 23일 목요일

auto Type Deduction (auto 타입 추론)

 이번 글은 C++11에 새롭게 추가된 auto 키워드에 관해 이야기해 보려고 한다. auto라는 단어가 뜻하는 것 처럼 누군가 자동으로 무엇인가를 해줄 것이라는 걸 예상할 것이다. 그렇다. auto 키워드는 타입이 들어갈 자리에 대신 들어가서 (템플릿에서 그랬던 것처럼) 컴파일러가 주변 코드를 이용해서 타입을 대신 결정하도록 하는 키워드이다. Effective Modern C++에서도 역시 심도있게 다루고 있으며 그 내용을 기반으로 내가 이해하는 바를 여기서 기록하고자 한다.

 auto가 수행하는 작업이 곧 템플릿 인자의 타입을 결정하는 것과 거의 같기 때문에 템플릿 타입 추론에 관해서 잘 모르는 상태라면 이번 글을 읽기 전에 이전에 올린 "템플릿 타입 추론"에 관한 글을 읽고 이해해 두는 것이 좋을 것 같다.

 그럼 먼저 auto의 타입 추론과 템플릿의 타입 추론이 얼마나 같은지(사실 약간 다르다) 간단한 설명을 보도록 하자. 먼저 템플릿 함수의 전형적인 예는 아래와 같다.
template<typename T>
void f(T param);

f(expr); // expr은 임의의 표현식
 일반적인 auto 선언문은 아래와 같은 형태이다.
auto x = expr; // expr은 임의의 표현식
 컴파일러는 템플릿 함수 f()에 넘기는 인자 expr의 타입에 기초하여 T의 타입 추론을 수행한다. 이 때는 "템플릿 타입 추론" 방식으로 동작하게 되고 그 동작에 대한 설명은 여기를 참조하기 바란다. 자 그 다음 auto의 예를 보면 바로 느낌이 올 것이다. auto에서도 역시 expr의 타입에 기초하여 타입 추론을 수행하는데 그 규칙은 템플릿의 그것과 동일하다. 즉, auto가 템플릿의 T역할을 하는 것이다.

 간단히 몇 가지 비교를 해보면 이해가 더 쉬울 것이다. 아래 예를 보자.
template<typename T>
void f1(T param);

template<typename T>
void f2(T& param);

template<typename T>
void f3(const T& param);

template<typename T>
void f4(T&& param);

auto a1 = expr;        // f1(expr)과 같은 타입 추론 수행
auto& a2 = expr;       // f2(expr)과 같은 타입 추론 수행
const auto& a3 = expr; // f3(expr)과 같은 타입 추론 수행
auto&& a4 = expr;      // f4(expr)과 같은 타입 추론 수행
 위 예에서 auto와 T의 역할을 비교해보면 auto가 "템플릿 타입 추론" 방식으로 결정된다는 게 별로 이상한 일이 아님을 알 수 있을 것이다. auto의 타입 추론 방식은 심지어 배열과 함수가 지정됐을 때 조차도 템플릿과 같은 방식으로 타입을 결정한다. 아래 예를 보자.
const char name[] = "R. N. Briggs"; // name의 타입은 const char[13]
auto arr1 = name; // arr1의 타입은 const char*
auto& arr2 = name; // arr2의 타입은 const char(&)[13]

void someFunc(int, double); // someFunc는 function
                            // 타입은 void(int, double) 
auto func1 = someFunc; // func1의 타입은 void (*)(int, double)
auto& func2 = someFunc; // func2의 타입은 void (&)(int, double)
 템플릿 타입 추론에서 비-참조형과 참조형의 인자에 함수나 배열을 넘겼을 때의 동작은 auto 타입 추론에서도 그대로 적용된다. 즉 비-참조형 인자에 함수나 배열을 넘기면 포인터형으로 바뀌어 추론된다. 이 정도면 "auto 타입 추론"은 "템플릿 타입 추론"과 같다고 할 만 하다.

 하지만 auto는 템플릿 타입 추론과 한 가지 다른 점이 있는데 바로 braced initializer가 초기화값으로 주어졌을 때이다. 아래 예를 보면 auto의 타입이 예상과 다르게 결정되는 걸 볼 수 있다.
int x1 = 27;
int x2(27);
int x3 = { 27 }; // C++11, uniform initialization
int x4{ 27 }; // C++11, uniform initialization

auto a1 = 27; // 타입은 int, 값은 27
auto a2(27);  // 타입은 int, 값은 27
auto a3 = { 27 }; // 타입은 std::initializer_list<int>, 값은 {27}
auto a4{ 27 };    // 타입은 std::initializer_list<int>, 값은 {27}
 C++11에서 소개된 uniform initialization 덕분에 위의 x1, x2, x3, x4가 모두 가능한 선언이다. 하지만 똑같은 방식으로 auto 선언문으로 바꾸면 결과는 달라진다. 주석에서 볼 수 있듯이 a3, a4는 int형이 아니고 std::initializer_list<int>형으로 추론되기 때문이다. braced initializer를 이미 사용해 본 적이 있다면 컴파일러가 상황에 맞춰서 그 값들을 std::initializer_list형으로 생성한다는 걸 이미 알고 있을 것이다. auto 선언문에서도 컴파일러는 braced initializer를 std::initializer_list형으로 바꿔서 타입 추론을 수행한다. 컴파일러의 그런 동작을 이미 알고 있었다면 어느 정도 이해가 되는 상황일 것이다.

 위와 같이 auto는 braced initializer를 std::initializer_list형으로 인식하고 타입 추론을 수행하지만 템플릿에 braced initializer를 넘기면 타입 추론을 하지 못하고 컴파일에 실패한다. 이 점이 두 타입 추론의 차이점이다. 즉 아래 예에서 f()의 호출은 실패한다.
// auto 타입 추론
auto x = { 11, 23, 9 }; // x의 타입은 std::initializer_list형으로 추론됨

// 템플릿 타입 추론
template
void f(T param);

f({ 11, 23, 9 }); // error! 타입 추론을 할 수 없음.

 C++14에서는 한 가지 더 짚고 가야할 것이 있다. C++14부터는 auto를 함수의 return 타입과 람다의 인자에서도 사용이 가능한데 이 때는 braced initializer를 std::initializer_list형으로 인식하지 않고 컴파일을 실패하게 된다. 즉 아래 두 가지 경우에 대해서는 (auto 타입 추론이 아닌) "템플릿 타입 추론"을 수행한다.
auto createInitList() {
    return { 1, 2, 3 }; // error: { 1, 2, 3 }을 타입 추론할 수 없음.
}
std::vector<int> v;
...

auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
...

resetV({ 1, 2, 3 }); // error! { 1, 2, 3 }을 타입 추론할 수 없음.
 위의 두 예에서 모두 auto는 에러가 난다. "템플릿 타입 추론"과 똑같이 braced initializer를 통해서 타입 추론을 하지 못하는 것을 볼 수 있다. 이렇게 보면 auto가 타입 추론되는 방식은 변수를 선언할 때만 braced initializer를 인식(std::initializer_list)한다는 점을 제외하면 템플릿 타입 추론과 완전히 동일하다.

 마지막으로 간단히 요약해 보자면...
  1. auto 타입 추론은 템플릿 타입 추론과 거의 같지만 braced initializer를 std::initializer_list형으로 인식해서 추론을 수행한다는 점이 다르다. (템플릿 타입 추론은 braced initializer를 통해서는 타입 추론을 실패함)
  2. auto가 함수의 return 타입, 람다의 인자에 사용되었을 때는 템플릿 타입 추론과 완전히 동일하게 동작한다.(즉 braced initializer를 통한 타입 추론은 실패한다)

2017년 2월 21일 화요일

Template Type Deduction (템플릿 타입 추론)

 수많은 C++관련 도서가 있고 그 수 만큼 많은 저자들이 있지만 그 중에 내가 인상깊게 본 도서들을 꼽자면 Effective 시리즈를 꼽을 수 있겠다. 전부 다 본건 아니지만 그래도 Effective 시리즈 작가인 Scott Meyers의 책을 볼 기회가 생긴다면 기꺼이 시간을 내서 볼 용의가 있다.

 최근에 보게된 Effective Modern C++도 상당히 인상적이다. C++11부터 크게 발전해가는 C++을 제대로 사용해 보려고 여기 저기 인터넷을 뒤져가며 새로운 문법을 익히다보니 머리속이 복잡해지는 걸 느끼고 있던 참에 여러가지 복잡하게 얽힌 생각들을 대부분 깔끔하게 정리할 수 있도록 도와준 책이었다.

 그래서 정리된 것들을 내 머리속에 오래 유지할 수 있도록 몇 번에 걸쳐 포스팅을 해볼까 한다. 그 첫 번째로 C++이 수행하는 타입 추론(Type Deduction)에 관해서 정리해보자.

 C++11이 나오기 전에는 템플릿(Template)말고는 타입 추론이 수행되는 경우가 없었지만 C++11에 auto와 decltype이 추가되면서 두 가지가 더 늘었다. 조금 정확히 얘기하자면 타입 추론은 C++ 컴파일러가 개발자가 지정하지 않은 타입을 주변의 다른 코드를 이용해서 결정하는 작업이다. 따라서 컴파일러가 개발자의 의도대로 움직여주지 않으면 엉뚱한 결과를 초래하는 경우도 종종 발생한다. 사실 컴파일러가 오동작하는 건 아니고 개발자가 컴파일러의 규칙을 제대로 알지 못하기 때문에 생기는 문제이며 그 때문에 그 규칙을 여기에 정리하려고 하는 것이다.

 우선 이번 글에서는 템플릿의 타입 추론에 대해서 얘기해 보고자한다.

 일반적인 함수 템플릿은 아래와 같은 형식으로 선언될 것이다.
template<typename T>
void f(ParamType param);
 여기서 ParamType은 'const T&'같은 T를 이용하는 타입이 될 것이다.

 그리고 이 함수를 호출할 때는 아래와 같은 형태로 호출될 것이다.
f(expr);
 이 때 컴파일러는 expr의 타입에 기초하여 ParamType과 T의 타입추론을 수행한다.

ParamType이 다음의 3가지 경우에 대해서 다르게 동작하므로 개발자는 이 규칙을 정확히 이해하고 있어야 타입 추론이 엉뚱하게 되는 경우를 피할 수 있을 것이다.
  1. ParamType이 Universal 참조가 아닌 참조형(Reference)인 경우 (T&)
  2. ParamType이 Universal 참조형인 경우 (T&&)
  3. ParamType이 참조형이 아닌 경우 (T)
차례대로 하나 씩 살펴보자.

1. ParamType이 Universal 참조가 아닌 참조형(T& 또는 const T&)인 경우
 이 경우가 가장 간단한 경우로 책에서는
  1) expr의 타입에서 참조를 제거(있다면)하고
  2) 그 타입을 ParamType과 패턴 매칭시켜서 T의 타입을 결정(추론)
하면 된다.

 패턴 매칭이라는 용어를 썼지만 패턴 매칭이 어떻게 이루어지는 지에 대한 설명이 없다. 내가 아는 일반적인 패턴 매칭으로는 사실 아래 예제의 모든 경우를 만족시키지 않는다.  주석을 보면 무엇이 매칭되는 대상들인지 적어놓았으므로 자세히 보기 바란다.

template<typename T>
void f(T& param); // 여기서 param은 참조형

int x = 27;        // x의 타입은 int
const int cx = x;  // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&

f(x);  // int와 T& 매칭,       T는 int,       param은 int&
f(cx); // const int와 T& 매칭, T는 const int, param은 const int&
f(rx); // const int와 T& 매칭, T는 const int, param은 const int&
template<typename T>
void f(const T& param);

int x = 27;        // x의 타입은 int
const int cx = x;  // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&

f(x);  // int와 const T& 매칭,       T는 int, param은 const int&
f(cx); // const int와 const T& 매칭, T는 int, param은 const int&
f(rx); // const int와 const T& 매칭, T는 int, param은 const int&
 여기서 패턴 매칭이라 함은 참조 제거된 expr의 타입을 ParamType과 매칭 시키는 데 적절히 매칭된 부분을 제외하고 남아있는 타입을 T에 매칭하는 방식인가 보다.  두 번째 예제의 마지막 함수 호출 f(rx)를 예로 들면 아래와 같이 const는 매칭되므로 제거하고 남은 건 int이므로 T는 int가 된다. 즉 매칭된 건 제거하고 남은 것을 T에 매칭하면 되는 것이다.

참조 제거된 expr : const int
    ParamType : const T   &

 패턴 매칭이라는 용어때문에 좀 찜찜한 느낌을 지울 수 없다. 저자가 매칭 규칙을 정확히 설명을 해주었다면 좋았을 것 같은데 그렇지 않다. 누구나 알 수 있을 것이라 생각했는지 따로 설명을 하지 않고 있다.

 그리고 책에서는 포인터형에 대해서도 같은 방식으로 타입 추론이 일어난다고 되어있지만 추후에 인터넷에 공개된 도서의 정오표를 통해서 포인터형은 사실 뒤에 나올 3번의 경우에 속한다고 수정하였다. 내가 보기에도 맞는 말이다. 따라서 여기서는 언급하지 않도록 하겠다.

2. ParamType이 Universal 참조형(T&&)인 경우
 이 규칙은 C++11에 rvalue 참조형이 추가되면서 템플릿 타입 추론에도 영향을 미치게된 경우이다. 아래 예제와 같이 ParamType은 rvalue 참조형 같이 생겼지만 실제 전달되는 expr의 타입에 따라 다음과 같은 타입 추론이 수행된다.
  • expr의 타입이 lvalue이면 T와 ParamType은 lvalue 참조형으로 추론된다.
  • expr의 타입이 rvalue이면 1번과 같은 방식으로 추론된다.
template<typename T>
void f(T&& param); // 여기서 param은 Universal 참조형

int x = 27;        // x는 int
const int cx = x;  // cx는 const int
const int& rx = x; // rx는 const int&

f(x);  // x는 lvalue,  따라서 T는 int&,       param은 int&
f(cx); // cx는 lvalue, 따라서 T는 const int&, param은 const int&
f(rx); // cx는 lvalue, 따라서 T는 const int&, param은 const int&
f(27); // 27은 rvalue, 따라서 T는 int,        param은 int&&
 lvalue의 경우는 그냥 모두 lvalue 참조형으로 추론되므로 추가 설명이 필요 없을 것 같고 rvalue의 경우에는 1번 규칙처럼 상수 27의 타입 int와 T&&가 패턴매칭해서 T는 int가 되고 param은 int&&가 된 것이다.

3. ParamType이 참조형이 아닌 경우(T)
 이 경우에는 인자가 그냥 pass-by-value 방식으로 전달되는 것과 같다. 즉 param의 타입은 expr의 복사본의 타입이 되는 것이다. 다음 예제를 보도록 하자
template<typename T>
void f(T param); // 여기서 param은 비-참조형

int x = 27;        // x는 int
const int cx = x;  // cx는 const int
const int& rx = x; // rx는 const int&
const char* const ptr = "Fun with pointers"; // ptr은 const char* const

f(x);   // T와 param의 타입은 모두 int
f(cx);  // T와 param의 타입은 모두 int
f(rx);  // T와 param의 타입은 모두 int
f(ptr); // T와 param의 타입은 모두 const char*
 위 예제에서 "const int형인 cx를 넘겼는데 왜 T가 const int가 아니고 int일까?"라고 의문을 제기할 수 있다. 바로 전에 말한 것과 같이 ParamType이 참조형이 아닌 경우에는 값의 복사본이 전달되므로 지정된 expr의 타입이 참조형이던 const형이던 상관없이 그 복사본이 전달되고 그 복사본은 수정되지 못할 이유가 없으므로 원래 붙어있던 const성질은 사라지게 되는 것이다. 마찬가지로 volatile 같은 성질들도 모두 사라지게 된다.

 다만 ptr의 경우에 const가 그대로 있는 것이 이상해 보일 수 있지만 f(ptr)에서 복사되는 건 ptr그 자체이지 ptr이 가리키는 객체가 복사되는 게 아니기 때문에 그 객체의 const성질은 그대로 유지되어야 하고 ptr 자신의 const성질만 사라지는 게 맞다. 따라서 ptr이 가리키는 객체의 const인 왼쪽의 const는 그대로 남고 ptr자신의 const는 사라져야 하기 때문에 오른쪽의 const만 사라지게 되는 것이다.

 위의 3가지 경우 외에 생각해 보아야 할 예외 케이스가 2가지가 있다. 바로 함수 인자로 배열이 전달될 때와 함수가 전달되는 경우이다. 이런 경우를 실제 앞으로 만나게 될지는 모르겠지만 정리하는 김에 같이 해보도록 하겠다.

* 배열을 인자로 넘겼을 때
 배열을 템플릿 함수에 인자로 넘기면 타입 추론은 어떻게 될까? 아래 예제를 보자.
template<typename T>
void f(T param); // 여기서 param은 비-참조형

const char arr[] = "Array";

f(arr); // arr은 const char[6]이지만 T는 const char*
 ParamType이 비-참조형일 때는 const char[6]을 const char*로 바꿔서 타입 추론을 수행한다. 하지만 아래 예제의 경우는 좀 다르다.
template<typename T>
void f(T& param); // 여기서 param은 참조형

const char arr[] = "Array";

f(arr); // arr은 const char[6], T는 const char[6], param은 const char(&)[6]
 ParamType이 참조형일 때는 배열을 포인터로 바꾸지 않고 배열 그대로 받아들인다.

* 함수를 인자로 넘겼을 때
 크게 쓸 일은 없지만 함수를 템플릿 함수에 인자로 넘겼을 때도 배열과 비슷한 방식의 동작이 수행된다는 것도 알아두자.
void someFunc(int, double); // 함수의 타입은 void(int, double)

template<typename T>
void f1(T param); // 여기서 param은 비-참조형

template<typename T>
void f2(T& param); // 여기서 param은 참조형

f1(someFunc); // T는 void(*)(int, double, param은 void(*)(int, double)
f2(someFunc); // T는 void(int, double),   param은 void(&)(int, double)
 ParamType이 참조형일 때만 배열에서 처럼 함수타입이 유지되고 비-참조형인 경우에는 함수 포인터로 바꿔서 타입 추론을 수행한다.