본문으로 건너뛰기

Laravel Query Logging, 쿼리 로그

· 약 1분

Laravel DebugBar 를 이용하는게 편하지만 dump 나 json 리턴시에 DebugBar 가 보이지 않으므로 직접 찍어줘야하는 경우가 많다.

<?php
# DB 파사드를 추가한다.
use DB;
...

public function your_func(Request $request) {
// 로그를 enable 시키고
DB::enableQueryLog();
// 쿼리를 여기에 실행한다.
Member::where('조건', '값')->get();
Product::find(1);

// 쿼리 로그를 찍는다.
$queryLogs = DB::getQueryLog();
dump($queryLogs);
}

결과

배열에 query, bindings (preparedStatement 를 위한 것), time 이 상세하게 나온다.

Laravel 5.5 - Debugbar와 BrowserSync의 충돌 해결

· 약 1분

Laravel Mix로 browserSync 옵션을 활성화 시에 Debugbar가 생기지 않는 오류가 발생할 경우 (스크립트 단에서 JSON parse 오류가 발생한다) 다음과 같이 설정해주면 된다.

webpack.mix.js
mix.browserSync({
proxy: {
// artisan serve시의 주소
target: "localhost:8000",
reqHeaders: function () {
// host를 직접 지정해준다.
return {
host: "localhost:3000",
};
},
},
});

Cannot start container iptables failed

· 약 1분

Container를 다시 올릴 때 다음과 같은 오류로 올라가지 않는 경우가 있다. docker kill이 아닌 stop, rm으로 container를 지웠을 때 뭔가 충돌이 나는 것 같다.

Error response from daemon: Cannot start container aca936f2822fce32235e627ff539c58b74b2f4e66cfa701de47ce609e2590d13: iptables failed: iptables -t nat -A DOCKER -p tcp -d 0/0 --dport 50000 -j DNAT --to-destination 172.17.0.10:50000 ! -i docker0: iptables: No chain/target/match by that name.
(exit status 1)

해결

이 중 마음에 드는 방법으로 해결하면 된다.

서비스 restart

docker 서비스를 restart 한다.

iptables rule 추가

오류 메세지에 필요한 rule을 추가한다.

$ iptables -t filter -N DOCKER
$ iptables -t nat -N DOCKER

AWS CodeCommit 사용하기

· 약 5분

AWS 에서 제공하는 Cloud Git Repository 인 CodeCommit 으로 소스코드를 관리해보자.

공식 문서가 아주 잘 되어있다. 문서를 보고 시작해도 된다.

IAM 유저 생성

IAM 에서 유저를 만든 뒤에 AWSCodeCommitFullAccess 권한을 추가한다.

키 파일 업로드

ssh 키를 생성하고 Public Key 파일(id_rsa.pub)을 해당 유저의 Security credentials 메뉴에서 업로드한다. 키가 등록되면 SSH key ID가 보이는데 메모해 놓자.

config

사용할 유저의 ssh config 를 설정해줘야한다. User 에 들어가는 값은 위에서 적어놓은 SSH key ID 값이다.

$ vi ~/.ssh/config

# 아래 내용을 맨 위에 넣어주자.
Host git-codecommit.*.amazonaws.com
User EXAMPLEEXAMPLEEXAMPLE
IdentityFile ~/.ssh/id_rsa

# 저장한 뒤 권한을 바꿔준다.
chmod 600 ~/.ssh/config

테스트

설정이 완료된 후에 서울리젼으로 ssh 연결을 시도해보자.

$ ssh git-codecommit.ap-northeast-2.amazonaws.com

# 다음과 같은 메세지가 리턴되면 성공이다.
You have successfully authenticated over SSH. You can use Git to interact with AWS CodeCommit. Interactive shells are not supported.Connection to git-codecommit.
ap-northeast-2.amazonaws.com closed by remote host.
Connection to git-codecommit.ap-northeast-2.amazonaws.com closed.

연동

이제 AWS CodeCommit 과 내 소스를 연결시켜보자.

# 이미 존재하는 프로젝트의 경우
$ git remote remove origin
$ git remote add origin ssh://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/레파지토리명

# 처음 시작하는 경우
$ git clone ssh://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/레파지토리명

# 권한이 없거나 ssh 계정을 물어볼 때
# origin의 path 앞에 SSH key ID를 추가하자
$ git remote add origin ssh://EXAMPLEEXAMPLEEXAMPLE@git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/레파지토리명

git pull origin master시에 정상적으로 가져오는 걸 확인할 수 있다.

ssh 연동이 잘 되지 않을시

메뉴얼대로 따라해도 연동이 안될 때는 당황하지 말고 IAM > Users > Security credentials에서 HTTPS Git credentials for AWS CodeCommit를 만들어주고 CodeCommit Repository 연결을 http로 하면 된다.

Webhook

Github, Bitbucket 와 달리 Webhook 설정하는 법이 조금은 복잡하다. (GCP 의 Pub/Sub 와 비슷한 느낌)

CodeDeploy 서비스를 사용하면 쉬워질 것 같은데, 그럼 CodePipeline 도 쓰고 싶을 것 같고 CI 세팅을 해야되고 다음 기회에

Lambda

람다는 웹(URL)으로 호출할 수 있는 Javascript function 이다.

설정

먼저 Lambda > 함수 > 함수생성 > 새로 작성 메뉴에서 webhook 이란 이름의 함수를 생성한다. 트리거 구성 메뉴에서 CodeCommit 을 선택하고 입력 폼을 잘 채워주자.

image from hexo

기존 브랜치로 푸시, master 브랜치를 선택했다. 사용자 지정데이터에는 webhook 을 걸 URL 경로를 넣어준다. (예: https://yourdomain.com/webhook)

소스

이미 세팅된 함수를 사용하자.

"use strict";

const url = require("url");
const https = require("https");

exports.handler = (event, context, callback) => {
const webhook_url = event.Records[0].customData;

if (!webhook_url) {
const error = new Error("Web-hook URL not provided as custom data.");
callback(error);
} else {
console.log("POST web-hook to " + webhook_url);
const options = url.parse(webhook_url);
options["method"] = "POST";

const req = https.request(options, (res) => {
let body = "";
console.log("Status:", res.statusCode);
console.log("Headers:", JSON.stringify(res.headers));
res.setEncoding("utf8");
res.on("data", (chunk) => (body += chunk));
res.on("end", () => {
console.log("Successfully triggered web-hook.");
// If we know it's JSON, parse it.
if (res.headers["content-type"] === "application/json") {
body = JSON.parse(body);
}
callback(null, body);
});
});

req.on("error", callback);
req.end();
}
};

https 커넥션이 아닌 경우 http 모듈을 사용해서 request 를 보내면 될 것 같다.

등록 후엔 CodeCommit > 트리거 > webhook > 트리거 테스트를 진행하면 정상적으로 호출이 된다.

로그

호출 로그는 CloudWatch > 로그에서 생성한 Lambda 명으로 확인할 수 있다.

로그 스트림이 설정되지 않았을 경우엔 Lambda > 구성 > 기존 역할에 표시된 역할(Rule)이 해당 Lambda 함수를 잡고 있는지 확인해야한다. IAM > Rules > 해당 룰 > Permissions에서 Show policy를 누르면 Resource 속성에서 확인할 수 있고 다르다면 제대로 연결시켜주면 된다.

여담

AWS Korea week in review에 소개되었다.

Laravel 5.5 - Log Permission 문제

· 약 4분

웹 서버의 유저로 로그 파일이 생성되어야 하는데, 어느 순간부터 root:root 권한을 달고 daliy log 가 생성되는 경우가 있다.

여러가지 경우의 수가 있는데, 맞는 조건을 찾아서 Permission 오류가 발생하지 않게 처리해보자.

storage 에는 쓰기권한이 있어야한다.

$ chcon -R -t httpd_sys_rw_content_t storage

WebServer user

웹 서버의 유저가 다르게 설정 되어있을 때 권한이 바뀔 수 있다. 서버 설정을 열어서 유저가 제대로 설정되어 있는지 확인해보자.

nginx.conf
user nginx;

php-fpm user

php-fpm 에서 설정하는 user 와 group 이 다르게 설정 되어 있을 때 권한이 바뀔 수 있다. php-fpm 설정을 열어 유저가 제대로 설정되어 있는지 확인해보자.

php-fpm.d/www.conf
user = nginx
group = nginx

listen.owner = nginx
listen.group = nginx

log rotate

로그 파일이 너무 커지는 걸 막기위해 log rotate 설정이 되어있다면 권한이 바뀔 수 있다. logrotate 가 cron 에 물려 있는지 설정을 확인해보자.

/etc/logroate.d/*
$ pwd
/etc/logroate.d

$ vi nginx
$ vi php-fpm

cron 사용시

crontab 의 경우 root 유저로 실행이 되면 cron 에서 Laravel 을 호출할 때 log 가 root 권한으로 생성될 수 있다.

로그 분기

log 파일을 생성하는 프로세스별로 분기해서 해결할 수 있다. Stackoverflow 참조

bootstrap/app.php
$app->configureMonologUsing(function(Monolog\Logger $monolog) {
$filename = storage_path('logs/laravel-'.php_sapi_name().'.log');
$handler = new Monolog\Handler\RotatingFileHandler($filename);
$monolog->pushHandler($handler);
});

설정을 추가해 놓으면 logs 폴더 하위에 다음과 같이 로그가 분기되어 생성된다.

storage/logs
$ ls -al .
-rw-r--r-- 1 nginx nginx 718 Aug 18 10:56 laravel-fpm-fcgi-2017-08-18.log

cron 은 root 에서 실행되나 user shell 에서 Laravel 프로세스를 실행하는 경우 root 에 의해 log 가 생성되었다면 다음과 같이 permission 을 변경해서 생성해야한다. Post 참조

bootstrap/app.php
$app->configureMonologUsing(function(Monolog\Logger $monolog) {
$filename = storage_path('/logs/laravel-' . php_sapi_name() . '.log');
// 5번째 파라미터로 666 권한을 넘긴다.
$handler = new Monolog\Handler\RotatingFileHandler($filename, 0, \Monolog\Logger::DEBUG, true, 0666);
$monolog->pushHandler($handler);
});

RotatingFileHandler 의 Parameter 는 여기를 참조하자. 666 으로 생성 시엔 굳이 php_sapi_name()을 사용하지 않아도 된다. (rw 권한이 모두에게 있으니까)

setfacl

다른 해결 방법으로는 ACL 을 수정해 logs 폴더 자체를 해당 user:group 이 편집할 수 있게 처리하면 된다.

$ pwd
/public_html/storage/logs

$ setfacl -d -m u:nginx:rwx .

$ getfacl .
default:user:nginx:rwx

# 삭제
$ setfacl -d -x u:nginx .

여담

Laravel 프로젝트 시작시 bootstrap/app.php 안에 로그를 분기 로직을 넣고 개발하는 게 좋아보인다.

Docker로 LEMP Stack 구축하기

· 약 2분

이 포스트 전에 웹서버 세팅을 하나씩 설치해서 띄워보는 걸 권장하고 Docker, SSH Login, LetsEncrypt, sed 명령어의 사용법을 알고 있어야 한다. 구성할 서버 스택은 다음과 같다.

  • Docker
  • Docker-compose
  • Host 에 사용될 Linux (Centos7)
  • Alpain Linux
  • Nginx ^1.13
  • MariaDB ^10.2
  • PHP ^7.1
  • Laravel =5.4
  • LetsEncrypt
  • HTTP2
  • Redis

이전 포스트를 참조하자.

Container 쇼핑

Docker Hub에서 마음에 드는 Container 를 사용해도 되지만, 생각처럼 돌아가는 Container 는 다음과 같았다.

Laradock을 안 썼죠?

  1. Laradock 에서 caddy 를 사용하지 않고 nginx 와 certbot 만을 이용해 http2 환경을 구성하는 예제가 없었다.
  2. 그래도 시도해봤으나 certbot 인증시에 DocumentRoot 를 잡지 못하는 현상을 삽질로 매꿀 시간이 없었다.
  3. Git repo 를 Clone 받아서 Docker-compose 로 Container 를 구동하기 때문에 추후 ECS 에 적용할 수가 없는 구조였다.
  4. 직접 구축해보고 싶었다.

세팅

nginx-php-fpm

Laravel 용 및 튜닝을 위해 Docker hub 의 이미지 대신 Git repo 의 이미지를 Clone 해서 세팅을 해보자.

내용 추가 중..

AWS ECS 부수기

· 약 6분

ECS 는 서울 리젼이 아직 없어서 그런가, 구글링해도 사용할만한 데이터가 너무 적었다. (기초 설명은 잘 되있다. 하지만 Hello World Application 을 올리려고 ECS 를 쓰는 건 아니니까..)

Cluster

Amazon ECS 클러스터는 작업을 배치할 수 있는 컨테이너 인스턴스의 논리적 그룹화입니다. 이 공식 설명을 보고 Cluster 에 대한 감을 잡기가 쉽지 않았다.

  • 간단히하면 Docker Container 를 올리는 EC2 Instance 이다.
  • K8S 의 그 클러스터이다.

Task

  • Task 는 작업이라고 번역되며, 하나의 Task Definition JSON 은 하나의 Docker-compose YAML 이라고 보자.

Container Definition

  • 배포되는 각 컨테이너의 정의
  • 파드의 개념

Task Definition

  • 컨테이너의 집합인 Task의 정의
  • 파드의 개념

Service

Amazon ECS 는 단일 ECS 클러스터에서 작업 정의에 지정된 수("원하는 개수")의 인스턴스를 동시에 실행 및 관리할 수 있게 해줍니다. 어떤 이유로 작업이 실패 또는 중지되는 경우 Amazon ECS 서비스 스케줄러가 작업 정의의 다른 인스턴스를 시작하여 이를 대체하고 서비스의 원하는 작업 수를 유지합니다.

  • Task 를 자동으로 관리할 수 있게 하는 기능, LB 나 Auto Scaling 모두 여기서 적용이 된다.
  • 롤링 업데이트 시에 배포 및 오토 스케일링을 담당한다.
  • K8S의 서비스, 디플로이먼트, 레플리카세트의 역할

Repository

AWS Docker 레지스트리 서비스로 AWS Private DockerHub 라고 보자.

네트워크 모드

모드내용
default네트워크 모드 기본값으로 bridge와 같음
awsvpcAWS에서만 제공되는 네트워크 모드 (ENI가 Task 자체 VPC의 IP 주소할당)
bridgeDockerContainer를 호스트와 같은 네트워크에 배치해 라우팅 없이 컨테이너 접근 가능
hostTask가 배치되는 호스트의 네트워크를 공유하는 모드 (Fargate 사용 불가)
noneTask에 속한 컨테이너의 외부 접근이 불가능하고 포트 매핑 사용불가

볼륨 생성

-v 또는 --volume 으로 Host 의 폴더를 Mount 하는 기능이 꼭 필요한데, 설정 창에선 찾기가 너무 힘들었다. ECS 에서는 작업 정의 생성 시에 하단에 볼륨 추가 를 꼭 먼저 클릭해 볼륨부터 추가해야한다.

image from hexo 이름엔 --name 옵션 사용하듯이 닉네임을 넣고 소스 경로엔 Host directory 경로를 넣자.

추가가 되면 컨테이너 추가 시에 탑재 지점 메뉴의 소스 볼륨 select box 에서 선택할 수 있다.

image from hexo

이 짓을 하는 것보단 공식 문서의 Task Definition JSON Parameter 를 보고 JSON 으로 때려박는게 편하다.

예시 JSON 은 아래와 같다. (3306 과 3307 을 열고 Host 의 Data 폴더를 Mount 하는 기본 구성의 MariaDB Image)

mariadb
{
"requiresAttributes": [
{
"value": null,
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.21",
"targetId": null,
"targetType": null
}
],
"taskDefinitionArn": "your task definition arn",
"networkMode": "bridge",
"status": "ACTIVE",
"revision": 3,
"taskRoleArn": null,
"containerDefinitions": [
{
"volumesFrom": [],
"memory": null,
"extraHosts": null,
"dnsServers": null,
"disableNetworking": null,
"dnsSearchDomains": null,
"portMappings": [
{
"hostPort": 3306,
"containerPort": 3306,
"protocol": "tcp"
},
{
"hostPort": 3307,
"containerPort": 3307,
"protocol": "tcp"
}
],
"hostname": null,
"essential": true,
"entryPoint": null,
"mountPoints": [
{
"containerPath": "/var/lib/mysql",
"sourceVolume": "dbdata",
"readOnly": null
}
],
"name": "maria",
"ulimits": null,
"dockerSecurityOptions": null,
"environment": [
{
"name": "MYSQL_DATABASE",
"value": "db"
},
{
"name": "MYSQL_PASSWORD",
"value": "db_pw"
},
{
"name": "MYSQL_ROOT_PASSWORD",
"value": "root_pw"
},
{
"name": "MYSQL_USER",
"value": "db_user"
}
],
"links": null,
"workingDirectory": null,
"readonlyRootFilesystem": false,
"image": "mariadb:latest",
"command": [
"mysqld",
"--character-set-server=utf8",
"--collation-server=utf8_general_ci"
],
"user": null,
"dockerLabels": null,
"logConfiguration": null,
"cpu": 0,
"privileged": null,
"memoryReservation": 500
}
],
"placementConstraints": [],
"volumes": [
{
"host": {
"sourcePath": "/ecs/dbdata"
},
"name": "dbdata"
}
],
"family": "mariadb"
}

고민

Container 를 Task 별로 생성해야하는데, 그럼 Task JSON 에서 link 옵션을 연결할 수가 없다. 이 경우엔 어떻게 Task Definition 을 짜야되나? EC2 에 접근해서 매번 link 를 생성해서 다시 올려야되나?

이 부분을 해결하기 위해선 ecs-task-kite를 사용하거나 VPC 를 구성해 수동으로 연결해 주는 방법 밖에 없다.

쉬운 방법으로 가자면 DB 는 (모든 컨테이너가 하나의 데이터를 바라봐야하는) RDSElastiCache처럼 AWS 의 서비스 사용하고 VPC 를 구성해 Backend, Frontend 단의 서버만 ECS Task 를 만들어서 가변적으로 돌리는 게 좋아보인다.

물론 동기화를 할 수도 있는데... 삽질할 시간에 더 잘 나온 포스팅을 기다려본다.

여담

그냥 모니터링 컨테이너 하나 더 띄우고, HAProxy 컨테이너 올리고 EC2 에 다 때려박고 싶다.

AWS Korea week in review에 소개되었다.

일본 우편번호(주소) 검색 API

· 약 1분

다음 우편번호 검색 API 처럼 일본 우편번호 검색도 간단히 구현할 수 있다. 여기에서 내용을 확인할 수 있지만, 더 쉽게 써보자.

<input type="number" id="zip" />
<button type="button" onClick="search_addr();">住所検索</button>

<input type="text" id="address" />
<!-- 스크립트를 로드 -->
<script src="//api.zipaddress.net/sdk/zipaddr.min.js" async></script>
<script>
var searchAddr = function () {
var $zip = $("#zip");
var zip = $zip.val();

// 일본 우편번호는 7자리로 고정되어있다.
// sample 6800001
if (zip && zip.length === 7) {
ZA.request(zip, function (data, err) {
var $address = $("#address");
if (err) {
$address.val("");
$zip.focus();
return alert(data.message);
}
$address.val(data.fullAddress);
$address.focus();
});
} else {
alert("郵便番号に誤りがあります。");
$zip.focus();
}
};
</script>

여담

주소지명으로 검색하는 API 는 찾아볼 수 없었다. 역시 주소검색은 다음

Centos7 Timezone 변경하기

· 약 1분
# 기존 설정 백업
$ mv /etc/localtime /etc/localtime.bak

# 타임존 연결
$ ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

# 백업을 안하고 바로 연결시
$ ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime

timedatectl

timedatectl 명령어를 사용해 쉽게 바꿀 수 있다.

# 한줄로 깔끔하게
$ timedatectl set-timezone Asia/Seoul

여담

시간날 때 세팅용 쉘을 만들어야겠다.

Laravel 5.5 - 다형성 관계

· 약 6분

댓글 테이블이 있고 이 댓글은 여러 테이블에서 사용된다고 치자. 그럼 댓글 테이블에 type과 type_id를 가져가야할 것이다. 이 때 사용할 수 있는 관계가 다형성 관계(릴레이션)인데, 공식 문서의 설명이 조금은 부족하다고 느꼈다. 파헤쳐보자.

morphTo는 type과 type_id를 가진, 여러 테이블로 연결되어야할 테이블에서 사용하는 릴레이션 메소드이다. 공식 문서에는 데이터를 가져온 뒤 릴레이션을 연결하는 예시만 있고, Eager 로딩 (With 구문을 사용하는 방법) 후 specific한 필드를 사용하게 변경하는 경우에 대한 정보는 없다.

기본 문법

YourModel.php
<?php

class YourModel extends Model {
...

// 다형성 관계를 가질 함수를 data로 정의했다
public function data() {
return $this->morphTo();
}
}

이렇게 정의시에 YourModel::with('data')->get() 으로 호출하면 불러와져야되지만, 필드명, 모델명이 정확하지 않으면 쿼리 호출조차 되지 않는다. (심지어 에러도 발생하지 않는다)

필드명 정의

먼저 morphTo의 소스코드를 를 살펴보자.

morphTo
<?php
/**
* Define a polymorphic, inverse one-to-one or many relationship.
*
* @param string $name
* @param string $type
* @param string $id
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function morphTo($name = null, $type = null, $id = null)

name, type, id를 파라미터로 받는다. 그럼 파라미터를 넘겨보자.

YourModel.php
<?php

class YourModel extends Model {
...

public function data() {
// morphTo의 paremeter로 null, 타입필드명, 타입인덱스 필드명을 넘긴다.
return $this->morphTo(null, 'type', 'type_idx');
}
}

여기서 name엔 도대체 뭘 넣어야 되는거야? 라고 의문이 생길 수가 있다. 함수 내에서 name 변수는 $this->getMorphs(Str::snake($name), $type, $id); 에만 딱 한 번 사용된다.

getMorphs 함수를 따라가보자.

getMorphs
<?php
protected function getMorphs($name, $type, $id) {
// $type과 $id가 명시되면 그 값을 먼저 반환한다.
return [$type ?: $name.'_type', $id ?: $name.'_id'];
}

주석처럼 typeid가 명시되면 name값은 사용되지 않는 쓰레기 값이 되어버린다. 따라서 null로 넘겨주면 된다.

타입-모델 바인딩

타입과 인덱스를 명시하면 드디어 오류메세지가 노출된다. 내가 정의한 type명을 가진 Class가 없다 라는 내용인데, 이제 타입과 모델을 연결시켜보자.

이 때 사용할 수 있는 메소드가 공식 문서에서 조금 스크롤을 내리면 있는 Custom Polymorphic Types에 잘 설명되어 있다.

하지만 등록하는 부분에 대한 설명이 **You may register the morphMap in the boot function of your AppServiceProvider or create a separate service provider if you wish.**라고 되어있다. 즉 AppServiceProvider에 넣던지 Service Provider로 생성이다.

한 모델에만 쓸 건데 전체에 등록을 할 필요가 없으니, 사용할 모델에 기능을 넣어보자.

YourModel.php
// Relation을 사용해야한다.
use Illuminate\Database\Eloquent\Relations\Relation;

<?php
class YourModel extends Model {
// 이 메소드는 모델이 initialize될 때 실행된다.
protected static function boot() {
parent::boot();

// 여기에 타입 별로 모델을 바인딩한다.
Relation::morphMap([
// type이 product일 경우 id는 product_id를 가리킨다.
'product' => 'App\Models\Product',
// type이 order일 경우 id는 order_id를 가리킨다.
'order' => 'App\Models\Order'
]);
}

public function data() {
// morphTo의 paremeter로 null, 타입필드명, 타입인덱스 필드명을 넘긴다.
return $this->morphTo(null, 'type', 'type_idx');
}
}

완벽해졌다. 이제 오류 없이 실행되는 것을 확인할 수 있다.

morphOne

문서 상에는 설명 되지 않은 morphOne 이란 메소드도 있다. morphMany는 관계가 설정된 값을 배열로 반환하지만 morphOne은 하나의 데이터로 반환한다. (hasOne과 hasMany처럼)

구조는 다음과 같다.

<?php
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)

morphToMany

morphedByMany

여담

다대다 다형성 관계 메소드 (morphToMany, morphedByMany)의 경우는 나중에 사용하게 되면 정리해야겠다. Relation 메소드들은 문서를 대충 훑고 API Docs를 직접 까보는게, 효율적인 것 같다.