Skip to main content

Laravel 5.5 - 로그인(Auth) 붙히기

· 11 min read

이전 포스팅에서 데이터를 가져왔으니, 로그인을 구현해보자.

라라벨에서 제공하는 회원가입, 로그인, 비밀번호 찾기 기능을 사용하고 싶다면 artisan 명령어로 간단하게 시작할 수 있다.

$ php artisan make:auth

명령어를 실행하면 resources/views/auth 폴더에 뷰가 app/Http/Controllers/Auth에 로직이 생성되고 routes/web.php에 라우팅이 등록된다. DB도 같이 만들고 싶다면 Docs를 따라하자.

다른 테이블 사용

제공된 user 테이블을 안 쓰려면 커스터마이징이 필요하다.

설정 변경

config/auth.php 파일로 이동해 다른 모델을 등록하자.

config/auth.php
<?php
'providers' => [
'users' => [
'driver' => 'eloquent',
// 여기에 사용할 모델을 등록한다.
'model' => App\Models\Member::class,
]
]

config 안의 파일을 변경하면 config:clear를 실행해 혹시 모를 캐시를 날려주자

그리고 app/Models/Member.php로 이동해 내 모델을 라라벨 기본 인증 모듈을 사용할 수 있게 추가해야한다.

app/Models/Member.php
<?
// 라라벨 인증 사용
use Illuminate\Foundation\Auth\User as Authenticatable;
// 비밀번호 변경 메일을 위해 필요한 trait
use Illuminate\Notifications\Notifiable;

// Authenticatable 를 상속받는다.
class Member extends Authenticatable {
// Notifiable trait를 추가한다.
use Notifiable;

protected $guarded = [
'member_id', 'remember_token'
];

protected $hidden = [
'password', 'remember_token',
];
}

인증 필드 변경

Basic Auth는 기본 필드를 email로 잡고 있기 때문에 id 필드를 사용하게 변경해야한다.

app/Http/Controllers/Auth/LoginController.php
<?php
// username 메소드를 추가
public function username() {
// 반환할 필드를 선언한다.
return 'id';
}

회원가입 후 자동로그인

회원가입이 성공하면 세션을 생성해줘야한다.

app/Http/Controllers/Auth/RegisterController.php
<?php
// registered 메소드를 Override
protected function registered(Request $request, $user) {
// 세션 생성
Auth::attempt([
'id' => $request->input('id'),
'password' => $request->input('password')
]);

return response(null, 204);
}

ajax 처리

로그인

로그인을 ajax로 처리해야될 경우 커스터마이징이 필요하다.

app/Http/Controllers/Auth/LoginController.php
<?php
// authenticated 메소드를 Override
protected function authenticated(Request $request, $user) {
// 인증이 된 경우 페이지 전환이 아닌 전환될 페이지를 json으로 반환한다.
if ($request->ajax()) {
return response()->json([
'href' => url()->previous()
]);

} else {
return abort(405);
}
}

비밀번호 찾기

비밀번호 찾기를 ajax로 처리해야될 경우 커스터마이징이 필요하다.

app/Http/Controllers/Auth/ForgotPasswordController.php
<?php
public function sendResetLinkEmail(Request $request) {
$this->validateId($request);
// email이 아닌 아이디로 검색을 해 메일을 찾고 싶다면
$data = Member::where('id', $request->only('id'))
->first(['email']);

$email = isset($data->email) ? $data->email : null;

// 여기서 email이 User Model(Member Model)에 바인딩된다.
// 아래의 sendPasswordResetNotification 메소드에서 $this->email 값이다.
$response = $this->broker()->sendResetLink([
'email' => $email
]);

// 리턴 값을 변경해주자.
return response()->json([
'email' => $email
], $response == Password::RESET_LINK_SENT ? 200 : 500 );
}

routing 예외

ajax 요청으로 바꿨다면 굳이 필요없는 기본 route는 등록할 필요가 없다. (예를 들면 로그인 페이지) 먼저 routes/web.php에서 Auth::routes(); 를 지워주고 라라벨 route 파일을 열어보자.

vendor/laravel/Illuminate/Routing/Router.php
// 994 라인
public function auth()
{
// Authentication Routes...
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');

// Registration Routes...
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');

// Password Reset Routes...
$this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
$this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
$this->post('password/reset', 'Auth\ResetPasswordController@reset');
}

입맛에 맞게 web.php로 가져와 사용하자.

notification 예외

Noticifation은 사용자에게 빠르게 알림을 보낼 수 있는 기능이지만, 정해져 있는 템플릿을 사용하므로 커스터마이징이 되게 힘들다. 비밀번호 찾기시에 보낼 메일을 정해진 템플릿을 사용할 수 없다면 메소드를 수정하자.

app/Models/Member.php
<?php
// 이 메소드를 override해야한다.
// 첫 파라미터는 비밀번호 인증용 token이 들어온다.
public function sendPasswordResetNotification($token) {
Mail::to($this->email)->send(new PasswordReset($token));
}

Auth

Basic Auth를 사용하는데 건드려야 되는 곳이 많으므로 Auth 모듈만 사용하는게 정신건강에 좋다고 본다. (Bootstrap 기반의 Laravel에 딱 맞는 모양을 입은 프로젝트라면 기본 인증이 좋겠지만)

로그인

먼저 사용할 모델에 Authenticatable 클래스를 상속 받자 그리고 LoginController에서 Auth::attempt() 메소드를 실행하면 끝이다.

app/Http/Controllers/LoginController.php
public function login(Request $request) {
$password = $request->password;

if(Auth::attempt([ 'id' => $request->id, 'password' => $password ])){
return response(null, 200);

}else{
return abort(401);
}
}

not bcrypt

Auth 모듈은 기본으로 bcrypt를 사용해 비밀번호를 암호화하고 비교하는데 다른 암호화 방식을 사용해야하는 경우가 있다. bcrypt를 사용하지 않게 처리해보자.

app/Models/Member.php
// 이 메소드를 override 해야한다.
public function getAuthPassword() {
// bcrypt 비교를 하지 않기 위해 강제로 해시를 생성한다.
return Hash::make($this->password);
}

이제 Auth::attempt() 메소드에 패스워드를 넘길 때 암호화를 해주고 넘기면 된다.

N개의 세션

관리자와 회원은 같은 세션을 사용하면 안 된다. 세션을 분기해보자.

모델 생성

먼저 모델을 하나 만들고 Authenticatable 클래스를 상속받는다.

app/Models/Admin.php
<?php

use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable {
// 속성은 테이블에 맞게 알아서 정리하자
}

설정 추가

만든 모델을 Laravel Auth에서 사용한다고 등록을 해줘야한다.

config/auth.php
<?php
return [
'guards' => [
...
// 아래에 등록된 provider를 admin이란 이름의 guard로 사용
'admin' => [
'driver' => 'session',
'provider' => 'admin'
]
],
'providers' => [
...
// 방금 만든 모델을 Auth의 Provider로 등록
'admin' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class
]
],
];

passowrds 속성은 제공되는 password_resets 기능을 사용할 경우에만 추가해주면 된다.

미들웨어

관리자 세션이 인증된 사람만 관리자 페이지에 접근할 수 있어야한다. php artisan make:middleware Admin 명령어를 실행해 Admin Middleware를 만들자.

app/Http/Middleware/Admin.php
<?php
use Auth;
use Closure;

class Admin {

public function handle($request, Closure $next) {
// admin session이 있는 경우만 request를 진행한다.
if (Auth::guard('admin')->check()) {
return $next($request);
}

// 아닐경우 관리자 로그인 페이지로 넘긴다.
return redirect()->route('admin.login');
}
}

그리고 Http Kernel에 방금 만든 Admin Middleware를 라우팅에서 사용할 수 있게 등록해준다.

app/Http/Kernel.php
<?php
...
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
// admin 이름으로 Admin Middleware를 호출할 수 있게 등록
'admin' => \App\Http\Middleware\Admin::class,
];

새로운 라우팅 파일로 관리하기 위해 RouteServiceProvider에 설정을 추가한다.

app/Providers/RouteServiceProvider.php
<?php
...
public function map() {
...
$this->mapAdminRoutes();
}

protected function mapAdminRoutes() {
// /admin 으로 들어오는 요청에 대해 처리한다.
Route::prefix('admin')
// 이런식으로 한 번에 추가할 수도 있다. 하지만 except 함수를 쓸 수 없는 것 같고,
// 라우팅 페이지에서 미들웨어를 등록해주는 게 더 편했다.
// ->middleware(['web', 'admin'])
->middleware('web')
->namespace($this->namespace)
// routes/admin.php 파일을 관리자 라우팅 파일로 등록한다.
->group(base_path('routes/admin.php'));
}

마지막으로 라우팅 파일에 미들웨어를 넣어준다.

routes/admin.php
<?php
// admin middleware를 사용하고, namespace에 admin.을 추가한다.
Route::group(['middleware' => 'admin', 'as' => 'admin.'], function() {
// 이 라우팅은 route('admin.main') 으로 접근이 가능해진다.
Route::get('/', 'AdminController@index')->name('main');
});

활용

기존 Auth 메소드들에 guard만 추가해주면 쉽게 사용 가능하다.

<?php
Auth::guard('admin')->attepmt();
Auth::guard('admin')->check();
Auth::guard('admin')->user();
Auth::guard('admin')->logout();

로그아웃

Auth::logout() 메소드를 호출하면 된다.

이슈

세션 아이디

라라벨에선 로그인을 할 때 세션아이디를 새로운 값으로 엎어쳐버린다. 따라서 같은 환경에서 접근했는데, 로그인을 하면 다른 사용자가 되는 경우가 생긴다. (로그아웃시에는 그대로다.)

<?php
// 로그인 전에 세션아이디를 가져와
$old_session_id = Session::getId();
// 로그인이 성공하면 DB에서 등록된 세션값을 업데이트하자.

소셜 로그인

laravel/socialite 패키지를 설치하고 아주 쉽게 연동이 가능하다.

세팅

$ composer require laravel/socialite

기본 socialite 패키지는 facebook, twitter, linkedin, google, github or bitbucket 만 연동이 가능하므로 다른 소셜 로그인을 연동하고 싶을 경우 Socialite Providers 패키지를 사용하면 된다.

Socialite Providers 사이트에는 Naver, Kakao, BattleNet(?) 등의 소셜 연동이 가능한 패키지가 무수히 많다.

여담

JWT 인증과 같이본다면 Laravel을 사용한 실무에서 필요한 인증은 다 마무리한 셈일 듯

PHP 7.1에서 mcrypt 대체하기

· 3 min read
  • 암호화 함수인 mcrypt 가 PHP 7.1 버전부터 Deprecated 되었다.
  • 왜 사라졌는지는 링크에 자세하게 나와있다.

기존 소스

  • PHP 구버전에서는 mcryptMCRYPT_RIJNDAEL_128 알고리즘을 통해 AES128 이 구현되어 있을 것이다.

  • MCRYPT_RIJNDAEL_128AES 128동일하다.

  • 타 언어와는 조금 다른데, 호환을 위해선 pkcs5 padding 으로 변경해주는 작업이 필요하다.

  • php mcrypt function 의 기본 패딩은 zeros padding

<?php
// 이런 식이거나
$cipherText = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, pkcs5_pad($plainText), MCRYPT_MODE_CBC, $iv);

// 이런 식일 것
$td = mcrypt_module_open("rijndael-128", "", "cbc", "");
@mcrypt_generic_init($td, $key, $iv);
$cipherText = @mcrypt_generic($td, pkcs5_pad($plainText));
mcrypt_generic_deinit($td);
mcrypt_module_close($td);

function pkcs5_pad($text, $blockSize = 16) {
$pad = $blockSize - (strlen($text) % $blockSize);
return $text . str_repeat(chr($pad), $pad);
}

대안

  • PHP 5.3 부터 사용 가능한 openssl_encrypt 함수를 쓰면 된다.
  • openssl 확장 모듈이 설치되어야한다.

OPENSSL

Encrypt

<?php
$cipherText = openssl_encrypt($plainText, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
// decrypt에서 오류가 발생해 PKCS5로 패딩을 맞춰주고 보내야한다면.
$cipherText = openssl_encrypt(pkcs5_pad($plainText), 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);

Decrypt

<?php
$plainText = openssl_decrypt($cipherText, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
// unpad가 필요하다면
$plainText = pkcs5_unpad(openssl_decrypt($cipherText, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv));

function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return $text;
}
if (!strspn($text, chr($pad), strlen($text) - $pad)) {
return $text;
}
return substr($text, 0, -1 * $pad);
}

PKCS5 vs PKCS7

  • openssl_encrypt의 default padding 은 PKCS7 padding 인데, 어떻게 호환이 되는 것일까?
  • 여기서 해답을 찾았다.

The difference between the PKCS#5 and PKCS#7 padding mechanisms is the block size; PKCS#5 padding is defined for 8-byte block sizes, PKCS#7 padding would work for any block size from 1 to 255 bytes. So fundamentally PKCS#5 padding is a subset of PKCS#7 padding for 8 byte block sizes. so, data encrypted with PKCS#5 is able to decrypt with PKCS#7, but data encrypted with PKCS#7 may not be able to decrypt with PKCS#5.

triple des

  • des-ede3-cbc가 triple des 알고리즘이다.
<?php
// encrypt
openssl_encrypt($text, "des-ede3-cbc", $key, OPENSSL_RAW_DATA, $iv);

// decrypt
openssl_decrypt($cipherText, "des-ede3-cbc", $key, OPENSSL_RAW_DATA, $iv);

패키지

  • 이 모든 걸 커버하는 라이브러리를 사용하자. phpseclib/mcrypt_compat
  • phpseclib 는 laravel/passport 에서도 사용되었다.

여담

카카오페이가 PHP 5 버전만 지원해 문의해봤지만 계획이 없어 이번 기회에 모듈을 다 뜯어봤는데 MCRYPT 만 만져주면 정상적으로 결제가 되었다.

곧 이 짓을 안해도 될 듯하다. KakaoPay 종료

이러다가 다시 선형대수학 공부하겠어.

Paypal 기부 버튼 만들기

· One min read

기존에 Paypal 버튼을 만들기가 되게 복잡했는데, 최근 Paypal.Me 가 생기면서 아주 간단해졌다!

Paypal.Me에 들어가서 서비스 가입만 하면 된다. Paypal 계정이 없으면 가입 후에 계좌 인증이 필요하다

아래 소스를 추가해주자. 기존 페이팔 버튼의 소스에서 action 부분만 변경해주었다.

<!-- https://www.paypal.me/GracefulLight/3 으로 금액을 붙혀 보내면 초기 금액을 설정할 수 있다. -->
<form action="https://www.paypal.me/GracefulLight" method="get" target="_blank">
<table border="0" cellpadding="10" cellspacing="0" align="center">
<tr>
<td align="center"></td>
</tr>
<tr>
<td align="center">
<input
type="image"
src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/PP_logo_h_100x26.png"
alt="Donate"
border="0"
name="submit"
/>
</td>
</tr>
</table>
</form>

결과

image from hexo

새삼 예쁜 것 같다.

CentOS7 LEMP Stack 설치하기 (HTTP2, PHP7.1, Maria, Letsencrypt)

· 9 min read

Linux, Nginx, MariaDB, PHP

Why LEMP instead of LNMP? We go with LEMP due to the pronunciation for Nginx: Engine-X (en-juhn-ecks). Think of how in English, the article an is used instead of a for hour even though it begins with a consonant. The importance is the sound of the first letter rather than its written representation. Besides, LEMP is actually pronounceable and doesn’t sound like reciting the alphabet.

PHP

repo 를 등록하고 설치하는 방법이 있지만 php71 등의 이름으로 설정해야되서 번거롭다. yum 명령어의 --enablerepo 옵션을 사용해 php71 repository 를 가져오자.

# php repo를 가져오기
$ yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

$ vi /etc/yum.repos.d/remi-php71.repo

[remi-php71]
name=Remi\'s PHP 7.1 RPM repository for Enterprise Linux 7 - $basearch
#baseurl=http://rpms.remirepo.net/enterprise/7/php71/$basearch/
mirrorlist=http://rpms.remirepo.net/enterprise/7/php71/mirror
# 이 값을 1로 바꿔준다.
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi

$ yum -y install php php-fpm php-devel php-curl php-mcrypt php-mysql php-xmlrpc php-mbstring php-zip 등등

$ systemctl start php-fpm
$ systemctl enable php-fpm

Maria

예전 포스팅으로 대체한다. 설치방법은 똑같고 버전만 다르게 해주면 된다. 설치 후 서비스 실행만 systemctl 명령어로 해주자.

Xtrabackup

이제 mysqldump 를 좀 놓아주고 Incremental Backup 을 해주자. 이게 왜 좋은지는 공홈이나 이나 다른 블로그에 자세히 설명되어있다.

# xtrabackup 설치
$ yum -y install xtrabackup

# 증분백업을 위해 전체 백업이 한 번 필요하다.
# root가 아닌 유저일 경우 해당 DB를 백업하기 위해 PROCESS 등 여러 권한을 추가해야한다.
# (로그 메세지로 추가하라고 나온다.)
$ innobackupex --user='root' --password='암호' --databases='DB' --no-timestamp /백업위치

# 증분백업
$ innobackupex --user='root' --password='암호' --incremental --incremental-basedir='/백업위치/xtrabackup_checkpoints' --no-timestamp /증분백업위치

# 증분백업을 전체백업 위에 얹기
$ innobackupex --apply-log --redo-only --incremental-dir='/증분백업위치' /백업위치

# 증분백업 삭제
$ rm -rf /증분백업위치


# 복원
$ systemctl stop mariadb

# 데이터 폴더를 날려야한다. (만약을 대비해 백업)
$ rm -rf /var/lib/mysql/*
$ innobackupex --copy-back /백업위치

# 권한부여
$ chown -R mysql:mysql /var/lib/mysql
$ systemctl start mariadb

Nginx

HTTP2 를 적용하기 위해서는 Nginx 1.9 버전과 Openssl 1.0.2 버전 이상이 필요한데, 기본 repository 에는 이 버전이 적용되어있지 않다. 따라서 검색해보면 컴파일 설치를 해야 된다는 글이 다수다. 모든 패키지는 패키지 매니져로 관리하는게 좋다고 생각하는 나로썬 그냥 넘어갈 수 없다. Nginx 최신 repository 를 관리해주는 나이스한 곳을 찾았다.

$ yum -y install yum-utils
$ yum-config-manager --add-repo https://brouken.com/brouken.repo

# 기존 nginx repo를 사용하지 않는다는 옵션
$ yum-config-manager --save --setopt=epel.exclude=nginx*;
$ yum -y install nginx

$ systemctl start nginx
$ systemctl enable nginx

# nginx 버전 확인
$ Nginx -V
nginx version: nginx/1.13.1
built with OpenSSL 1.0.2k 26 Jan 2017

setopt 가 오류가 날 경우

ngixn.repo 를 만들어주고 연결하자.

$ vi /etc/yum.repos.d/nginx.repo

# vim /etc/yum.repos.d/nginx.repo

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=0
enabled=1

# 저장하고 설치 시작

$ yum update
$ yum -y install nginx
$ systemctl start nginx
$ systemctl enable nginx

LetsEncrypt

공짜 SSL 인 LetsEncrypt 의 이름이 certbot 으로 바뀌었다. 공홈을 참조해도 좋다. 인증을 받기위해 먼저 도메인을 따야한다.

$ yum -y install certbot

# 인증
$ certbot certonly --standalone -d example.com -d www.example.com

# 갱신 확인
$ certbot renew --dry-run

# 갱신하면서 nginx 재부팅
# 이 명령어를 적절히 cron에 넣어주자.
$ certbot renew --pre-hook="systemctl stop nginx" --post-hook="systemctl start nginx"

인증이 성공하면 /etc/letsencrypt/live/example.com/ 경로 아래에 키가 떨어질 것이다. 자세한 명령어 옵션은 Docs 참조.

nginx 연동

ssl 을 적용하면서 HTTP2 도 붙혀보자. HTTP2 에서는 bundling 보다 파일을 쪼개서 보내는게 더 효율적이라고 한다. (non-blocking 이니까)

# HTTP
server {
listen 80;
server_name example.com www.example.com;

# certbot --webroot 인증을 받기위한 설정
#location ^~ /.well-known/acme-challenge/ {
# default_type "text/plain";
# root /var/www/letsencrypt;
#}

# 80 접속시 443으로 redirect
location / {
return 301 https://$server_name$request_uri;
}
}

# HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
root /var/nginx/www/public;
index index.php index.html;

# 지저분한 보안 옵션은 추천옵션이니 넣어주자.
ssl on;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
}

location ~ /\.ht {
deny all;
}

# 캐싱할 데이터가 있다면 추가
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff)$ {
expires 1M;
add_header Cache-Control "public";
}

location ~* \.(?:css|js)$ {
expires 7d;
add_header Cache-Control "public";
}
}

해당 세팅은 Laravel5.4 용이다.

nginx.conf 에는 해당 옵션들도 추가해주자.

nginx.conf
  # CPU 물리 코어에 따라 설정한다.
# 1코어에선 1로 나머지는 auto로 설정하면 된다.
# grep ^processor /proc/cpuinfo | wc -l 로 확인 가능
worker_processes 1;

events {
# 각 worker process에서 한 번에 처리할 수 있는 최대 연결 수
# ulimit -n의 값과 같게 설정하자.
worker_connections 1024;
# I/O event 노티 방식을 epoll로 사용. (poll보다 발전한 방식)
use epoll;
# worker가 한 번에 모든 연결을 수용할 수 있도록 설정
multi_accept on;
}

http {
# access_log 제거
access_log off;
server_tokens off;

# iframe 보안 이슈 DENY
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

# PHP 서버라는 header 제거
fastcgi_hide_header X-Powered-By;
# static file 제공 최적화
sendfile on;
# TCP multiple buffer를 individual packet으로 전송하게 변경
tcp_nopush on;
# TCP에서 TCP_CORK 옵션을 활성화
# MTU에서 IP 헤더의 40-60 Byte를 뺀값과 같다는데 뭐라는건지 모르겠다.
tcp_nodelay on;
# keep alive 설정
keepalive_timeout 65;
# keepalive_requests 100000;

# gzip 압축설정
gzip on;
gzip_min_length 1000;
gzip_types application/x-javascript text/css application/javascript text/javascript text/plain text/xml application/json application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/xml font/eot font/opentype font/otf image/svg+xml image/vnd.microsoft.icon;
gzip_disable "MSIE [1-6]\.";
}

적용 후에 HTTP2 확인 확장프로그램 설치 후 이 프로그램에 파란불이 들어오면 성공이다.

파일 백업

소스는 git 으로 관리가 되는데 static 파일은 주기적으로 백업이 필요하다.

file_backup.sh
NOW_DATE=`date`
BACKUP_DATE=`date +"%Y%m%d"`
FILE_DIR=백업할 폴더 경로
BACKUP_DIR=백업된 파일 경로

# gz 압축
tar zcvf ${BACKUP_DIR}/${BACKUP_DATE}.tar.gz ${FILE_DIR}

# 3일 이상된 백업파일은 제거
find ${BACKUP_DIR}/ -mtime +3 -exec rm -f {} \;
# find ${BACKUP_DIR}/ -mtime 3 -delete

여담

다음부터 Ubuntu 쓸까 생각했는데, Nginx 최신 Repo 를 찾은게 컸다. 하지만 빨리 Docker 책 읽자. Nginx, Reverse Proxy, Redis, PHP, Jenkins 를 한 번에 해결해주겠지. 이제 DockerFile 이 잘 읽히긴 하는데, 언제쯤 갈아탈 수 있으려나

Laravel 5.5 - Eloquent ORM 사용하기

· 17 min read

Eloquent ORM을 사용해 데이터를 가져와보자. 이전 포스팅에서 이어진다. (Eloquent ORM 기능만 필요하다면 3.Model 부터 보면 된다)

DBMS에 상관 없이 테이블을 똑같이 생성하기 위한 기능이다.

생성

artisan 명령어로 간단히 생성할 수 있다. 날짜_테이블 형식의 파일이 database/migrations 밑에 생성된다. 이 파일들의 자동 생성된 이름을 절대 변경하지 말자 내부적으로 언더스코어를 explode해 class를 호출하기 때문에 건들면 고생한다.

# 모델을 생성하면서 같이 생성
# m 옵션을 추가한다.
$ php artisan make:model -m 모델명

# 마이그레이션만 생성
$ php artisan make:migration 마이그레이션명

생성 후에 up 메소드 안에 테이블을 꾸며주고 down 메소드에 테이블을 지울 때 실행할 로직을 구현한다. 컬럼 타입을 지정하는 메소드는 여기를 참조하면 된다.

<?php
public function up() {
Schema::create('table_name', function (Blueprint $table) {
$table->increments('idx');
$table->string('id', 20)->unique();
$table->string('name', 30);
$table->string('email', 100)->unique();
$table->string('password', 250);
$table->tinyInteger('tiny')->nullable();
$table->timestamps();
});
}

// 테이블에 외래키 제약조건이 걸려있을 경우 migration으로 일괄 삭제시에
// 오류가 발생하므로, 외래키 체크 옵션을 비활성화 해줘야될 수 도 있다.
public function down() {
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
Schema::dropIfExists('table_name');
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}

실행

php artisan migrate 명령어로 실행하면 된다.

  • migrate => 전체 실행
  • migrate:refresh => 다시 실행
  • migrate:rollback => 마지막 migrate 시점으로 돌림
  • migrate:reset => 제거

Tinker

이미 데이터가 들어간 테이블이 있어 migrate --path로 한 파일만을 실행해야 하는데, tinker를 사용하면 더 쉽게 해당 마이그레이션을 실행시킬 수 있다. Tinker는 커맨드로 라라벨 쉘(실행환경)으로 들어간다고 생각하면 된다.

# tinker
$ php artisan tinker

# shell로 접속된다.
Psy Shell v0.8.5 (PHP 7.0.7 ??cli) by Justin Hileman
New version is available (current: v0.8.5, latest: v0.8.8)
>>>
# blueprint 의존성을 추가해주고 (Blueprint는 up 메소드에 DI로 들어가 있기에 읽지 못한다.)
$ use Illuminate\Database\Schema\Blueprint;
# 새로 실행하려 했던 스키마를 실행시켜주기만 하면 된다.
$ Schema::create('new_table', function (Blueprint $table) {
$table->increments('idx');
$table->string('id', 20)->unique();
$table->timestamps();
});

DB에 해당 테이블이 생성된 걸 확인할 수 있다.

Seed

Seed는 테이블에 필수 데이터 또는 더미 데이터를 심어주는 과정이다. 테스트에 필요한 데이터를 넣어주는데 아주 효과적이다.

생성

php artisan make:seeder 시더명

database/seeds 아래에 파일이 생성된다. 파일을 열어보면 run() 메소드 하나가 있는데, 여기에 시드파일이 호출될 때 실행할 로직을 구현해주면 된다.

database/seeds/BoardSeed.php
<?php
public function run() {
// 모델로 생성하기
Board::create([
'title' => '공지',
'author' => '관리자',
'content' => '공지입니다'
]);
// DB 파사드로 생성하기
DB::table('board')->insert([
'title' => '공지',
'author' => '관리자',
'content' => '공지입니다'
]);
}

Faker

랜덤한 테스트용 데이터가 많이 필요하다면, 가짜(더미) 데이터를 생성해주는 라이브러리를 사용할 수 있다. 링크를 따라가보면 정말 엄청난 종류의 랜덤 데이터를 생성할 수 있음에 놀랄 것이다.

faker를 사용하려면 ModelFactory를 먼저 정의해야한다. 상품 모델을 가져와 가짜 데이터 타입을 정의해보자.

database/factories/ModelFactory.php
<?php
$factory->define(App\Models\Product::class, function(Faker\Generator $faker){
return [
// 메소드는 위의 라이브러리를 참고하자.
// 상품명을 중복되지 않게 city(도시명)으로 가져온다.
'name' => $faker->unique()->city,
// 색상 헥스코드를 가져온다.
'color' => $faker->safeColorName,
// 1000~50000원 사이의 가격을 가져온다.
'price' => $faker->numberBetween(1000, 50000),
// lorempixel의 랜덤 이미지를 가져온다.
'thumbnail' => $faker->imageUrl(200, 100),
'detail_image' => $faker->imageUrl(300, 600),
'qty' => $faker->numberBetween(1, 1000),
'status' => 1,
'owner' => $faker->name,
'sorting' => $faker->numberBetween(1, 9999)
];
});

실행

위에서 선언한 faker 모델을 seed에서 호출해보자.

database/seeds/ProductSeed.php
<?php
public function run() {
// 두번째 인자로 실행시킬 횟수를 넣어주면 된다.
factory(App\Models\Product::class, 100)->create();
}
php artisan db:seed

테이블에 상품 더미데이터가 100개 생성된 것을 확인할 수 있다.

Model

모델은 하나의 테이블의 타입을 정해 놓은 것이라 보면 된다. 데이터를 가져오거나 넣을 때 모델에 정의된 형태로 가져온다.

기본 구조

아래 구조 정도만 알아두면 된다. 더 자세한건 Eloquent Model Class를 확인해보자.

app/Models/Board.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Board extends Model {
// 임의의 생성 필드가 있다면 설정해준다. (기본값 created_at)
const CREATED_AT = 'created_dt';
// 사용하지 않을 경우 Null로 초기화해주자. (기본값 updated_at)
const UPDATED_AT = null;
// 둘 다 사용하지 않을 경우 timestamps만 꺼주면 된다. (기본값 true)
public $timestamps = false;
// PK가 auto increment가 아닐경우 false로 바꿔준다. (기본값 true)
public $incrementing = false;

// PK 값을 변경한다. (기본값 id)
protected $primaryKey = 'idx';
// 테이블 명을 변경한다. (기본값 모델명의 복수형)
protected $table = 'board';

// dump insert 및 dump update가 안되는 필드를 설정한다.
protected $guarded = [
'idx'
];

// guarded와 fillable 중 하나만 있으면 된다. (써보니 guarded가 편하더라.)
// dump insert 및 dump update가 가능한 필드를 설정한다.
//protected $fillable = [
// 'title', 'content'
//];

// 노출시키지 않을 필드가 있을 경우
protected $hidden = [
'password'
];
}

Relationship

1:1

hasOne과 belongsTo로 연결한다.

1:n

hasMany와 belongsTo로 연결한다.

n:m

belongsToMany로 연결한다. 처음에 제일 감이 안왔던 Relationship이지만 코드를 보면 이해하기가 쉽다. 주문(Order) - 주문 상품(OrderProduct) - 상품(Product) 테이블이 있다고 가정한다. 각각 시작 - 링크(피벗) - 종료 테이블이다.

<?php App/Models/Order.php

public function product(){
// 마지막으로 연결될 모델명, 링크 테이블 명, 시작->링크를 연결시킬 인덱스, 링크->종료를 연결시킬 인덱스
return $this->belongsToMany('App\Models\Product', 'order_product', 'order_id', 'product_id')
// 링크 테이블의 컬럼을 가져와야할 때
->withPivot(['price', 'quantity']);
}

이렇게 정의하면 뷰에서 order->productforeach로 돌려 product->product_id처럼 접근할 수 있고 product->pivot->price로 pivot 테이블의 데이터도 가져올 수 있다.

1:1:1

이런 관계가 있을 경우에 라라벨에서 관계메소드를 직접 지원하지는 않는다. 억지로 사용하려면, hasOne을 두 번 사용해 연결해야하는데, Select Query를 두 번 날려야 된다는 소리다. (참을 수 없다)

BelongsToThrough trait 패키지를 통해 깔끔하게 해결할 수 있다. 메소드에서 belongsToMany처럼 커스텀 외래키를 사용해야할 경우엔 5번째 파라미터로 [ 클래스 => 키 ] 형식으로 넘겨주면 된다. (여기의 소스를 참고하자)

이슈

ORM의 Join 방식

한 방 쿼리가 불가능하다. 오직 PK를 통해 Select된 데이터들을 다시 PHP 단에서 합쳐서 보여준다. 한 방이 필요할 때는 Database에서 View를 이용하거나 DB 파사드를 써야한다.

Composite Key

라라벨 Eloquent Model에서 복합키를 사용할 수는 없다. 편법으로 trait를 추가할 수 있는데 find 메소드도 overriding 해야 된다.

Timestamps

created_at과 updated_at은 timestamps 형식이라 date_format과 같은 쿼리함수 대신 whereDate 메소드로 연산을 실행해야한다. 물론 DB에 데이터가 들어가기 전 datetime으로 해당 컬럼을 변경해주면 된다. (timestamps의 유효기간은 2035년까지므로.)

Group By

GroupBy를 이용한 쿼리 사용시에 Syntax error or access violation: 1055 'table.column' isn't in GROUP BY라는 오류 메세지가 보이며 실행이 안 되는 경우가 있다. 해당 컬럼으로 GroupBy를 하지 않았는데도 발생한다.

database config의 strict 모드 중 ONLY_FULL_GROUP_BY 모드가 활성화 되어있어서 그런데 이 모드만 제외를 시켜주면 된다.

config/database.php
<?php
'connections' => [
'mysql' => [
// strict 속성을 false로 바꿔준다.
'strict' => false,
// modes 속성을 추가해 기본 strict 옵션을 다시 추가한다.
'modes' => [
'STRICT_TRANS_TABLES'
'NO_ZERO_IN_DATE'
'NO_ZERO_DATE'
'ERROR_FOR_DIVISION_BY_ZERO'
'NO_AUTO_CREATE_USER'
'NO_ENGINE_SUBSTITUTION'
]
],

변경 후 config:cache로 config 파일들을 다시 캐싱해주자.

연동

만든 모델을 선언만 하면 Eloquent Model 메소드Query Builder 메소드를 사용할 수 있다.

CRUD는 다음과 같다.

YourController.php
<?php

use App\Model;

public function test($id) {
// PK로 데이터 1개 반환
Model::find($id);
Model::where('idx', $id)->first();

// 전체 데이터 반환
Model::all();
// 조건 데이터 반환
Model::where()->get();

// Insert
$model = new Model;
$model->column = '추가 값';
$model->column2 = '추가 값2';
$model->save();

// Mass Insert
Model::create([
'column' => '추가 값',
'column2' => '추가 값2'
]);

// Bulk Insert
Model::insert([[
'column' => '추가 값',
'column2' => '추가 값2'
], [
'column' => '추가 값',
'column2' => '추가 값2'
]]);

// Update
$model = Model::find($id);
$model->column = '변경 값';
$model->column2 = '변경 값2';
$model->save();

// Mass Update
Model::where()->update([
'column' => '변경 값',
'column2' => '변경 값2'
]);

// Delete
Model::destory($id);
Model::find($id)->delete();
Model::where()->delete();
}

Collection과 Model의 차이

Model에서 쓸 수 있는 메소드와 Collection에서 쓸 수 있는 메소드가 다르다. Model Class에서 정의하는 관계 메소드들은 Model에서만 사용 가능하다.

  • Collection: Array<Model> 즉 모델의 집합이다.
  • Model: 테이블에서 하나의 행이라고 생각하자.

Paging

모델에서 paginate 메소드를 사용하면, querystring에 page 변수가 붙어 페이징이 된다. (예를들면 /boards?page=1) 라라벨의 기본 페이징은 Bootstrap의 Class를 사용한다.

<?php
// 이렇게 모델을 가지고 왔다면
Model::where()->get();

// get을 paginate로 바꿔주기만 하면 된다. (뿌려지는 list item의 기본값은 15이다.)
Model::where()->paginate(10);

// next와 prev 버튼이 없는 pagination을 구현하고 싶다면
Model::where()->simplePaginate(10);

페이징 뷰를 꾸미고 싶다면 아래 명령어를 실행해 resources/views/vendor/pagination에 view가 생기고 default.blade.php를 수정하면 된다.

php artisan vendor:publish --tag=laravel-pagination

ajax pagination

실무에선 Paging 호출을 GET보단 AJAX를 쓰는게 깔끔한데 List View와 ListItem View, Controller 세부분을 모두 변경해줘야한다. 아래 소스는 jQuery를 사용하고 있다고 가정한다. (Frontend Framework를 같이 사용하고 있다면 더 깔끔하게 처리 될 수 있을듯)

Controller

YourController.php
<?php
public function list(Request $request) {
// ajax 요청일 경우에 listitem 뷰 반환
if ($request->ajax()) {
return view('listitem', [
'data' => Model::where()->paginate(10);
]);
// get 요청일 경우에 list 뷰 반환
} else {
return view('list', [
'data' => Model::where()->paginate(10);
])
}
}

List view

list.blade.php
<div id="list">
@include('listitem')
</div>
<script>
var paging_listener = function () {
// 페이지 버튼에 click listener 등록
$('.pagination a').click(function (e) {
// a href 로 페이지가 이동하는걸 방지한다.
e.preventDefault();
get_list($(this).attr('href'));
});
};

var get_list = function (url) {
$.get(url)
.then(function(html) {
$('#list').html(html);
// 페이지가 다시 그려졌으므로 listener를 다시 등록한다.
paging_listener();
});
};

$(function () {
paging_listener();
});
</script>

ListItem view

listitem.blade.php
<ul>
@foreach($data as $item)
<li>{{ $item->name }}</li>
@endforeach
</ul>
// pagination html이 반환되어 자동으로 보여진다.
{{ $data->links() }}

multiple pagination

한 페이지에 여러 리스트가 있는 경우가 종종 있다. 먼저 paginate 메소드를 살펴보자.

<?php
/**
* Paginate the given query into a simple paginator.
*
* @param int $perPage
* @param array $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
  • perPage: 한 페이지에 표시될 Item 수
  • columns: 가져올 컬럼명의 배열
  • pageName: page 변수명 (예를들면 /boards?page=1 에서 page 변수명을 변경 가능)
  • page: 가져올 페이지

Controller

Controller에서 paginate 메소드를 활용해보자.

YourController.php
<?php
public function list(Request $request) {
if ($request->ajax()) {
// list1_page 값이 들어올 경우
if ($request->has('list1_page')) {
// list1_item 뷰에 바인딩해준다.
return view('list1_item', [
'data1' => $this->get_list1()
]);
} else if ($request->has('list2_page')) {
return view('list2_item', [
'data2' => $this->get_list2()
]);
}
}

// 메인 요청
$data1 = $this->get_list1();
$data2 = $this->get_list2();
return view('list', [
'data1' => $data1,
'data2' => $data2
]);
}

protected function get_list1() {
return Model::where()->orderBy()->paginate(5, ['*'], 'list1_page');
}

protected function get_list2() {
return Model::where()->paginate(10, ['*'], 'list2_page');
}

List view

위의 script에서 조금만 수정해주면 된다. (list item view는 위와 동일한 코드의 반복이므로 생략)

list.blade.php
<div id="list1">
@include('list1_item')
</div>
<div id="list2">
@include('list2_item')
</div>

<script>
var paging_listener = function () {
$('.pagination a').click(function (e) {
e.preventDefault();
var page = $(this).attr('href').split('?')[1];
// page.split('=')[0]은 페이지 변수명을 가져온다.
var paging_type = page.split('=')[0] === 'list1_page'
? 'list1'
: 'list2';
get_list($(this).attr('href'), paging_type);
});
};

var get_list = function (url, target) {
$.get(url)
.then(function(html) {
$(`#${target}`).html(html);
paging_listener();
});
};
</script>

여담

이제 DB에서 데이터를 가져오는 것까지 끝났다. 다음 포스팅에서는 User Login을 구현해보자.

Yarn 사용법

· 3 min read

Bower의 시대가 끝났다. 홈페이지를 들어가보면 다음과 같은 문구가 보인다.

..psst! While Bower is maintained, we recommend yarn and webpack for new front-end projects!

Yarn을 사용해보자.

2020년에는 npm 사용을 추천드립니다. 더 이상 느리지 않습니다.

NPM으로 설치할 수도 있는데 추천하는 방법은 Installer이니 다운받고 설치해주면 된다! NPM으로 설치시에는 환경변수 등록을 거쳐야한다.

설치 후 Bash에서 확인해보자.

$ yarn --version
0.24.5

사용법

npm 사용법과 아주 유사하다. 기존 NodeJS 패키지에서 yarn 명령어만을 입력하면 완벽히 호환이 되고, 새로운 프로젝트라면 yarn init 명령어를 실행하면 된다. package.json을 사용하기 때문에 그냥 명령어만 바뀌었다고 생각하면 된다. (패키지들도 npm의 것을 공유한다.)

명령어 비교

install이 add로, uninstall이 remove로, update가 upgrade로 바뀐게 사실상 끝이다. 자세한 옵션은 CLI Docs를 참조하자.

npmYarn
npm installyarn install
(N/A)yarn install --flat
(N/A)yarn install --har
(N/A)yarn install --no-lockfile
(N/A)yarn install --pure-lockfile
npm install [package](N/A)
npm install --save [package]yarn add [package]
npm install --save-dev [package]yarn add [package] [--dev/-D]
(N/A)yarn add [package] [--peer/-P]
npm install --save-optional [package]yarn add [package] [--optional/-O]
npm install --save-exact [package]yarn add [package] [--exact/-E]
(N/A)yarn add [package] [--tilde/-T]
npm install --global [package]yarn global add [package]
npm update --globalyarn global upgrade
npm rebuildyarn install --force
npm uninstall [package](N/A)
npm uninstall --save [package]yarn remove [package]
npm uninstall --save-dev [package]yarn remove [package]
npm uninstall --save-optional [package]yarn remove [package]
npm cache cleanyarn cache clean
rm -rf node_modules && npm installyarn upgrade

Global 경로

  • Windows: %LOCALAPPDATA%/Yarn/config/global

환경 변수 설정

설정을 확인한 뒤 prefix 경로를 PATH에 추가해주면 된다.

$ yarn config list
# { prefix: 'C:\\Users\\{NAME}\\npm' }

결론

bower_components 안녕 이젠 node_modules만 있겠구나

HTTP Status Code 정리

· 4 min read

API 나 AJAX 응답시에 코드로 날려주는게 표준이기도하고, 편하기도 해서 정리했다. 매번 검색해 쓰는 것도 귀찮고 포스팅마다 의미도 다르고해서.

순수하게 정보 제공만을 위한 코드

  • 102 임의의 동작이 백그라운드에서 발생하고 완료까지 시간이 걸린다고 나타낼 때 사용

200

  • 200 Success, OK 성공
  • 201 Created 새로운 리소스 생성
  • 202 Accepted 요청은 성공했으나 처리되지 않음
  • 203 Non-authoritative information 요청이 변형 프록시를 통해 라우팅 되는 경우
  • 204 No Content 요청은 성공했으나 반환되는 내용이 없음
  • 206 Partial Content 페이징된 응답을 위해 사용된다. 헤더가 전송되고 클라이언트가 허용 가능한 범위가 지정되는데 응답이 범위보다 큰 경우, 서버는 처리해야 하는 더 많은 데이터가 있음을 나타내는 이 코드를 응답한다.

300

리다이렉션

  • 301 Moved Permanently 영구적으로 리다이렉트
  • 302 Found 리다이렉트하지만 나중에 바뀔 수 있음, 사용자가 임의의 이유로 일시적인 리다이렉션 수행을 요구하는 것
  • 304 Not Modified 클라이언트에 캐시된 리소스로 요청됨
  • 307 Temporary Redirect
  • 308 Permanent Redirect 자원에 대한 영구적인 리다이렉트를 지정, HTTP 메소드가 자원을 변경하는 것을 허용하지 않는다.

400

클라이언트 오류

  • 400 Bad Request 구문적으로 잘못된 요청
  • 401 Unauthorized 인증 필요 (실제로는 Unauthenticated 의 의미)
  • 403 Forbidden 권한 부족 (실제로는 Unauthorized 의 의미)
  • 404 Not Found
  • 405 Method Not Allowed 메소드가 일치하지 않음
  • 406 Not Acceptable 헤더 또는 내용이 서버에서 받아들일 수 없는 요청
  • 407 Proxy Authentication Required 프록시 인증 필요
  • 408 Request Timeout 요청시간 초과
  • 409 Conflict 기존 리소스와 충돌
  • 410 Gone 리소스가 영원히 사라짐
  • 411 Length Required Content-Length 없음
  • 413 Requested Entity Too Large 내용이 너무 큼 (첨부파일)
  • 414 Requested URL Too Long URL 이 너무 김
  • 422 Unprocessable Entity Validation 오류
  • 429 Too Many Requests 요청 횟수 제한

500

서버 오류

  • 500 Internal Server Error 서버 오류
  • 501 Not Implemented 클라이언트가 아직 구현되지 않은 엔드포인트에 접근하는 경우
  • 502 Bad Gateway 게이트웨이 오류
  • 503 Service Unavailable 일시적인 오류 (터지거나 점검 중)

composer zlib_decode와 content-length mismatch 오류

· 3 min read

컴포저로 패키지를 다운 받은 후에 갑자기 오류가 나면서 다른 설치조차 안되는 경우가 있다. 오류 메세지는 아래 두 경우로 나타난다.

zlib_decode

json 파일 디코딩이 실패했다는 오류이다.

Failed to decode response: zlib_decode(): data error
Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info

[ErrorException]
zlib_decode(): data error

위의 링크를 따라가 보면 해결책으로 다음과 같이 제시가 되는데 ESET antivirus는 아직 국내에서 쓰는 사람을 못 봤고, IPv6을 막아도 그대로일 것이다.

  • If you are using ESET antivirus, go in "Advanced Settings" and disable "HTTP-scanner" under "web access protection"
  • If you are using IPv6, try disabling it. If that solves your issues, get in touch with your ISP or server host, the problem is not at the Packagist level but in the routing rules between you and Packagist (i.e. the internet at large). The best way to get these fixed is raise awareness to the network engineers that have the power to fix it. Take a look at the next section for IPv6 workarounds.
  • If none of the above helped, please report the error.

content-length mismatch

composer의 패키지 list를 가지고 있는 json파일이 다 안받아졌다는 내용의 오류이다.

[Composer\Downloader\TransportException]
Content-Length mismatch

해결

구글링 해보면 clear-cache를 한 뒤에 composer -vvv 명령어를 사용해 디버깅 로그를 확인해보라는 내용이 주류인데 별 도움은 안 된다. http 연결이아닌 https로 packagist에 붙어보라는 것도 해결되지 않았다.

packagist repository 변경

packagist는 유럽에 있어서 한국에서 유럽까지 갔다가 다시 돌아오는 구조라, packagist 일본 미러를 이용하면 이 문제가 해결된다.

# 이 명령어로 global config에 repos.packagist 를 추가한다.
$ composer config -g repos.packagist composer https://packagist.jp

# 설정이 되었는지 확인한다..
$ composer config -gl|grep repo

# 아래 설정이 보이면 추가가 된 것이다.
[repositories.packagist.org.type] composer
[repositories.packagist.org.url] https://packagist.jp

캐시 제거

$ composer global clear-cache
$ composer clear-cache

이제 다시 패키지를 설치하면 설치가 될 것이다.

다운로드 속도 향상

병렬 다운로드를 가능하게 하는 hirak/prestissimo 패키지를 설치하자. 288s -> 26s 가 되는 마법이 일어난다고 한다.

$ composer global require hirak/prestissimo

Laravel 5.5 - 시작하기

· 11 min read

image from hexo

1등이 된지가 14년 7월이다. 이 글은 17년 글이다. 다른 장점이 궁금하다면 다른 포스트나 stackoverflow에서 해결해줄 것 같다.

이 글을 읽어주시는 분들은 Modern PHP에 대해 이해하고 있다고 생각했기 떄문에 문법적인 것들은 설명하지 않을 것이다. 더군다나 라라벨을 도입하면서 삽질한 내용을 정리하고 있기에, 입문용은 절대 아니다. 기본적으로 아래와 같은 PHP 개념을 알고 있어야한다.

  • PSR
  • Composer
  • Class
  • Namespace
  • Closure
  • Trait
  • PDO

추가적으로 이런 걸 사용해 봤다면 더 쉬울 듯하다.

  • Monolog
  • 다른 MVC 프레임워크
  • Lodash

설치

PHP

PHP 7 이상을 설치해주고, 아래 Extension은 웬만하면 열어주는 것들이니 크게 신경쓰지 말자.

  • PHP >= 7.0.0
  • OpenSSL PHP Extension
  • PDO PHP Extension
  • Mbstring PHP Extension
  • Tokenizer PHP Extension
  • XML PHP Extension

Composer

Composer 홈페이지에서 다운받고 전역설정만 해주면 된다.

Laravel Cli

Laravel 명령어를 사용할 수 있게 composer로 전역 설치하자.

composer global require "laravel/installer"

프로젝트 생성

초기 생성

laravel new 프로젝트명

위 명령어를 실행하면, 알아서 composer 패키지까지 설치된다.

clone 했을 때

환경 설정 파일과 프로젝트 키가 없으므로 적절하게 세팅해줘야한다.

# 패키지 의존성 설치
$ composer install

# .env 파일 복사 (설정파일 생성)
# post-root-package-install scripts로 들어가 있긴한데, 복사가 안 되는 경우
$ cp .env.example .env

# 프로젝트 키 생성
$ php artisan key:generate

실행

프로젝트를 실행해보자.

php artisan serve

이제 localhost:8000으로 라라벨 프로젝트에 접근이 가능하다.

php artisan 명령어는 laravel에서 사용하는 커맨드 쉘이다. php artisan로 명령어 리스트가 나오고 php artisan help 명령어 로 해당 명령어의 옵션을 볼 수 있다.

프로젝트 구조

라라벨 한글 메뉴얼이나 공식 메뉴얼을 보면 되는데 마치 사전을 펴놓았는데 어떤 단어를 찾는지 까먹은 느낌이 들 것이다.

모든 프레임워크는 Model, View, Controller, Routing만 알면 끝이라는 것을 잠시 기억 속에서 꺼내보고 딱 3가지 경로만 기억하자.

  • Route: routes/
  • Controller: app/Http/Controllers/
  • View : resources/views/

모델은 어딨어? 라고 생각할 것 같은데 Model을 완벽하게 사용하려면 ORM을 써야하고 DB를 ORM에 맞게 정규화해야하며 이 작업은 처음 진행하기에 고통스럽다. 다음 포스트에 진행해보자.

Route

모든 웹 프레임워크의 기본은 Routing이다. routes 폴더에 기본으로 api, channels, console, web 파일이 보이는데 그 중 2가지만 알면 된다.

  • api.php: Token 인증이 필요한 라우터로 /api/{route} 로 접근이 가능하다.
  • web.php: 기본적인 Route이다. GET이 아닌 다른 메소드는 CSRF Token이 있어야만 호출이 가능하다.

Controller

Routing 요청을 처리하는 로직이 들어있는 부분이다.

View

Controller에서 처리된 데이터를 플랫폼에 보여주는 부분이다.

프로젝트 세팅

App 설정

env 파일

.env 파일에서 APP_NAME과 DB로 시작하는 설정을 바꿔준다.

app 파일

config/app.php
<?php
return [
// 시간대 세팅을 한다.
'timezone' => 'Asia/Seoul',
// 언어 설정을 한다.
// fallback_locale은 언어팩이있어야 하므로 아래에서 진행할 laravel-lang 설치 후 바꿔주자.
'locale' => 'ko',
]

database 파일

config/database.php
<?php
return [
'connections' => [
'mysql' => [
// mysql일 경우 utf8로 설정해준다 (maria는 utf8mb4)
// DB 설정과 다르게 Model 이용시 charset 및 collation이 지정되어 들어가기에 꼭 설정해줘야한다.
'charset' => 'utf8',
'collation' => 'utf8_general_ci',
// 내 테이블의 접두사가 필요하다면 여기서 설정한다.
// 예를들어 test_member, test_product 이런식의 테이블 명명규칙이라면 아래처럼 주면된다.
'prefix' => 'test_',
// engine 옵션은 my.ini에서 만져주자..
'engine' => null,
],
];

config 내의 파일과 .env파일을 건드릴 경우 laravel 환경을 다시 캐싱해주거나 아예 삭제해야 적용이 된다. php artisan config:cache 명령어 또는 php artisan config:clear 명령어를 실행해주자.

Debug Bar

정말 멋진 디버깅 툴이다. 간단히 설치 후에 .env 파일의 APP_DEBUG 옵션이 true이면 자동으로 View 하단에 생성된다. 아래와 같은 데이터를 확인할 수 있다.

  • 로그 내역
  • 뷰 데이터 바인딩 확인
  • 오류
  • 라우팅
  • 쿼리 실행
  • 메일 발송 데이터
  • 실행 시간
  • Request

설치

composer require barryvdh/laravel-debugbar

laravel-lang

기본 오류 메세지들의 localization 패키지이다.

설치

composer require caouecs/laravel-lang:~3.0

적용

vendor/caouecs/laravel-lang/src/{국가} 폴더를 resources/lang/{국가} 로 복사하고 config/app.phpfallback_locale{국가}로 바꿔주면 된다.

# 귀찮으니 git bash에서 날려봅시다.
$ cp -r ./vendor/caouecs/laravel-lang/src/ko ./resources/lang/ko

라우팅

routes/web.php
<?php
// url 변수
Route::get('/your_url/{id}', function($id) {
return $id;
});

Route::get('/your_url/{id?}', function($id = null) {
return $id;
});

컨트롤러

생성

artisan 명령어로 간단히 생성할 수 있다.

# make:controller 폴더명/컨트롤러명
$ php artisan make:controller TestController

# 리소스 메소드를 같이 생성
$ php artisan make:controller TestController --resource

연결

routes/web.php
<?php
// 컨트롤러명@메소드명 으로 바로 연결시킬 수 있다.
Route::get('/url/{id}', 'TestController@get');
Route::post('/url', 'TestContoller@post');

// CRUD가 명확한 컨트롤러인 경우 Resource를 사용하는 게 편할 수 있다.
Route::resource('tests', 'TestController');
// only 또는 except로 원하는 resource만 가져갈 수 있다.
Route::resource('tests', 'TestController', [
'only' => [
'index', 'show'
]
]);
app/Http/Controllers/TestController.php
<?php
use Illuminate\Http\Request;

class TestController extends Controller{

public function get($id){

// 뷰를 호출
return view('test.detail', [
'data' => $data
]);
}

public function post(Request $request, $id){
// Request 데이터를 제어할 수 있다.
$input = $request->all();
$name = $request->input('name');

$data = [];

// json body와 201 http code를 반환
return response()->json([
'data' => $data
], 201);
}
}

Resource

리소스 사용시라면 아래와 같은 메소드로 구성되어야한다. 게시판 컨트롤러라면 아래와 같은 구성일 것이다.

메소드경로액션라우트.메소드
GET/boards게시판 메인boards.index
GET/boards/create게시글 생성페이지boards.create
POST/boards게시글 저장boards.store
GET/boards/{id}게시글 상세페이지boards.show
GET/boards/{id}/edit게시글 수정페이지boards.edit
PUT/PATCH/boards/{id}게시글 수정로직boards.update
DELETE/boards/{id}게시글 삭제boards.destroy

블레이드 템플릿을 사용하며 홈페이지 설명이 꽤나 자세하다. 당장 알아야 할건 아래 두 개 정도 뿐이다. 나머지는 그때 그때 문서를 참조하자.

resources/views/test/detail.blade.php
// 바인딩한 데이터를 {{ }} 구문으로 바로 접근할 수 있다.
{{ $data->name }}님의 정보입니다.
resources/views/test/create.blade.php
// post로 전송시
<form method="post" url="/url">
// post로 데이터를 전송시 csrf 토큰이 필요하다.
// 이 헬퍼함수를 호출하면 자동으로 토큰 값이 들어간 input hidden 필드가 생성된다.
{{ csrf_field() }}
이름: <input type="text" name="name" vavlue="">
</form>

// 기타 메소드로 전송시
<form method="post" url="/url">
{{ crsf_field() }}
// PUT, PATCH, DELETE 와 같은 메소드로 전송시 form 태그에서는 지원을 해주지 않으므로 method spoofing이 필요하다.
// 이 헬퍼함수를 호출하면 해결된다.
{{ method_field('PUT')}}
</form>

여담

이제 라라벨을 이용해 데이터를 서버에 보내고 뷰를 구성할 수 있게 되었다. 다음 포스팅에서는 DB 연동과 ORM 모델을 사용해 데이터를 가져오고 페이징 처리를 해보자.

알고리즘

· 12 min read

객체지향 기법

  • 현실 세계의 개체를 기계의 부품처럼 하나의 객체로 만들어 기계적인 부품들을 조립하여 제품을 만들 듯 소프트웨어를 개발할 때도 객체들을 조립해 작성할 수 있도록 하는 기법

객체

  • 데이터와 데이터를 처리하는 함수를 묶어 놓은 하나의 소프트웨어 모듈
  • 데이터: 객체가 가지고 있는 정보
  • 함수: 객체가 수행하는 기능, 객체가 갖는 데이터를 처리하는 알고리즘

클래스

  • 공통된 속성과 연산을 갖는 객체의 집합
  • 객체의 일반적인 타입
  • 각각의 객체들이 갖는 속성과 연산을 정의하고 있는 틀
  • 인스턴스: 클래스에 속한 각각의 객체

메세지

  • 객체들 간에 상호작용을 하는 데 사용되는 수단
  • 객체에게 어떤 행위를 하도록 지시하는 명령 또는 요구사항

객체지향 기본 원칙

캡슐화

  • Encapsulation
  • 데이터와 데이터를 처리하는 함수를 하나로 묶는 것
  • 연관된 데이터와 함수를 함께 묶어 외부와의 경계를 만들고 필요한 인터페이스만을 밖으로 드러내는 과정

정보 은닉

  • Information Hiding
  • 다른 객체에게 자신의 정보를 숨기고 자신의 연산만을 통하여 접근을 허용하는 것
  • 캡슐화에서 가장 중요한 개념

추상화

  • Abstraction
  • 불필요한 부분을 생략하고 객체의 속성 중 가장 중요한 것에만 중점을 두어 개략하는 것
  • 모델화

상속성

  • Inheritance
  • 이미 정의된 상위 클래스의 모든 속성과 연산을 하위 클래스가 물려받는 것

다형성

  • Polymorphism
  • 메세지에 의해 객체가 연산을 수행하게 될 때 하나의 메세지에 대해 각 객체가 가지고 있는 고유한 방법으로 응답하는 것

객체지향 생명 주기

계획 및 분석 => 설계 => 구현 => 테스트 및 검증

객체지향 분석

  • OOA = Object Oriented Analysis
  • 사용자의 요구사항을 분석하여 요구된 문제와 관련된 모든 클래스, 이와 연관된 속성과 연산, 그들간의 관계 등을 정의하여 모델링하는 작업

객체지향 설계

  • OOD = Object Oriented Design
  • 객체지향 분석을 사용해 생성한 여러 가지 분석 모델을 설계 모델로 변환하는 작업
  • 시스템 설계와 객체 설계를 수행

문제 정의 => 요구 명세화 => 객체 연산자 정의 => 객체 인터페이스 결정 => 객체 구현

객체지향 구현

  • 설계 단계에서 생성된 설계 모델과 명세서를 근거로 하여 코딩하는 단계
  • 객체 기반 언어: Ada, Actor와 같이 객체의 개념만을 지원
  • 클래스 기반 언어: Clu와 같이 객체와 클래스의 개념을 지원
  • 객체 지향성 언어: 객체, 클래스, 상속의 개념을 모두 지원, Simula, Smalltalk, C++, Objective C, Java

객체지향 프로그래밍

  • OOP = Object Oriented Programming
  • 새로운 개념의 모듈 단위, 즉 객체를 중심으로 하여 프로그램을 개발하는 기법

객체지향 테스트

클래스 테스트

  • 구조적 기법에서의 단위 테스트와 같은 개념
  • 캡슐화된 클래스나 객체를 검사하는 것

통합 테스트

  • 객체 몇 개를 결합하여 하나의 시스템으로 완성시키는 과정에서의 검사
  • 스레드 기반 테스트: 시스템에 대한 하나의 입력이나 이벤트에 응답하는 데 요구되는 클래스들을 통합하는 것
  • 사용 기반 테스트: 독립 클래스를 테스트한 후 독립 클래스를 사용하는 다음 계층의 종속 클래스를 테스트

확인 테스트

  • 사용자 요구사항에 대한 만족 여부를 검사

시스템 테스트

  • 모든 요소들이 적합하게 통합되고 올바른 기능을 수행하는지 검사

아키텍처

IEEE 1471

  • ANSI / IEEE 1471-2000
  • 소프트웨어 집약적인 시스템에서 아키텍처가 표현해야 하는 요소와 내용들, 이들 간의 관계를 규정하고 있는 국제 표준
  • 표준화, 중립성, 유연성, 의사소통

저장소 구조

  • 중앙자료구조와 독립된 컴포넌트로 구성된 아키텍처
  • 큰 데이터의 이동 및 공유에 적합하며 컴포넌트 간 통신은 이뤄지지 않는다.
  • 대량의 데이터를 저장하는데 효과적이다.
  • 컴포넌트의 추가 삭제가 편리하다.
  • 중앙 집중화를 통해 데이터 관리가 용이하고 보안이 뛰어나다.

MVC 구조

  • 애플리케이션을 모델, 뷰, 컨트롤러의 세 개의 컴포넌트로 구분하는 아키텍처
  • 유저 인터페이스와 비지니스 로직들을 서로 분리하여 개발하는 방법
  • 장점
    • 동일한 모델에 대해 다양한 뷰 제공
    • 효율적인 모듈화 가능
    • 모델과 뷰의 구분으로 사용자 인터페이스에 대한 요구 사항을 적용시키는데 용이

모델

  • 애플리케이션의 핵심 기능을 포함
  • 상태 변화시 컨트롤러와 뷰에 전달

  • 정보 표시를 관리
  • 결과물 생성을 위해 모델로부터 정보를 수신

컨트롤러

  • 사용자로부터 입력을 받아 모델과 뷰에 명령을 전달
  • 모델에 명령을 전달해 상태를 변경하고 뷰에 명령을 보내 표시 방법을 변경

클라이언트/서버 구조

  • 클라이언트와 서버로 나뉘는 아키텍처
  • 클라이언트: 사용자로부터 입력을 받아 서버에 요청을 전달
  • 서버: 수신된 요청을 수행하고 데이터의 일관성을 유지
  • 새로운 서버의 추가 및 업그레이드가 용이
  • 데이터가 서버에 집중되어 데이터 관리 용이
  • 서버에 네트워크 트래픽과 데이터가 집중되 처리 비용이 급증할 수 있다.

계층 구조

  • 계층적으로 조직화가 가능한 애플리케이션에 적합한 아키텍처
  • 인접 계층 사이에서만 요청과 응답이 이뤄지며 변경 사항을 적용할 때에도 두 개의 인접 계층에만 영향을 미쳐 원활한 변경이 가능
  • OSI 7 Layer

파이프 필터 구조

  • 데이터의 흐름을 점진적으로 처리하는 시스템을 위한 아키텍처
  • 프로세싱을 위한 시스템이 각 필터에 캡슐화되어 있으며 데이터는 인접 필터 사이의 파이프를 통해 전달되는 형태

서식 문자열

  • %d: 정수형 10진수 입출력
  • %c: 문자 입출력
  • %s: 문자열 입출력
    • %-8.3s: 8칸 확보 후 문자열 왼쪽부터 문자열 3개 출력
  • %f: 소숫점을 포함하는 실수로 입출력
    • 자릿수 없으면 6자리까지 표기
    • %8.3f: 8칸 확보 후 오른쪽부터 정수 표기 후 소숫점은 3자리까지 출력

패딩 비트

  • 양수: Left, Right 모두 0
  • 음수: Left 0, Right 1

연산자 우선순위

  1. 단항
  2. 산술
  3. 시프트
  4. 관계: < <=가 == !=보다 우선
  5. 비트: &(and), ^(xor), |(or) 순서
  6. 논리: &&, || 순서
  7. 삼항연산

xor, 다른면 1 같으면 0

소수 판별

  • N을 2~(N-1)까지 차례대로 나눠 떨어지는지 확인
  • N을 2부터 차례대로 나눠 처음으로 나눠떨어질 때의 나눈 수와 N이 같으면 소수
  • N를 2부터 루트N 까지 숫자로 나눠 떨어지는지 확인

소수의 개수

  • 처음 나온 소수의 배수들은 소수가 아니다

최대공약수, 최소공배수

  • 유클리드 호제법 사용
  • 두 수 중 큰 수와 작은 수를 정한 뒤 큰 수를 작은 수로 나눠 나머지를 구한다.
  • 나머지가 0이면 그 때의 작은 수가 최대공약수
  • 두 수를 곱한 값을 최대공약수로 나눈 값이 최소공배수
  • 나머지가 0이 아니면 작은 수를 큰 수로, 나머지를 작은 수로

약수

  • N을 1부터 N까지 나눠 나머지가 0이 되는 수

소인수 분해

  • N을 2에서 루트N까지 나눠 떨어지는지 확인
  • 나눠 떨어졌다면 그 수는 소인수
  • 나눌때의 몫을 다시 N으로 놓고 2에서 루트 N까지 나눠떨어지는지 확인

10진법에서 2진법

  • 10진수를 2로 나눠 나머지를 구한 후 저장

10진법에서 N진법

  • 10진수를 N진법의 가장 가까운 누승부터 1까지 차례대로 나누는 방법