DEVBLOG 소개

devblog.selfhow.com ?

데브 블로그는 국내 개발자분들의 블로그를 한번에 모아서 보여주는 서비스입니다.

devblog.selfhow.com 을 만든 이유

특별한 의미는 없습니다.
그냥 만들어 보고 싶었어요…

사실은 더 멋진 사이트가 있어요.

여기에 소개하는 건 좀 제살 깎아먹기처럼 보이지만, 멋진건 멋진거니까요.

AWESOME DEVBLOG

https://awesome-devblog.now.sh/
대시보드 형식으로 한번에 보실 수 있고, 카테고리도 나누어져 있어요.

daily devblog

http://daily-devblog.com
하루에 한 번씩 글을 모두 모아서 이메일로 배달해 줘요.

개발기는 아래에서 읽어보실 수 있어요.

셀프 렌즈

http://lens.selfhow.com
제살 깎아먹기의 정점. 제 사이트입니다 (…)
소개는 셀프렌즈 소개 에서 보실 수 있습니다.

데브 블로그

다만 http://devblog.selfhow.com 은 워드프레스로 만들어놔서, 뭔가 글이 갱신되면 빠르게 캐치할 수 있다는 장점이 있습니다.
저는 개인적으로 슬랙 메신저로 rss 를 받아볼 수 있는 rss-slack 을 추천합니다.
근무시간에 끊임없이 남의 블로그를 볼 수 있는 쾌감을 느끼실 수 있죠.

어떻게 만들었을까?

사실 이 이야기를 쓰고 싶어서 서비스를 개발했습니다.

개발 언어 선정

php로 만들까, java로 만들까, python으로 할까, c#으로 할까를 약 3분간 고민했습니다.
결론부터 말씀드리면 php 로 만들었어요.

사실 java나 c#은 굳이 이런걸 만드는데 이렇게 귀찮은 언어를 사용해야 하나.. 라는 점이 눈에 딱 들어와서 2.5초만에 탈락했습니다.
python과 php 가 경합하고 있었죠.

python

장점

  • 훨씬 더 깔끔하게 코드를 짤 수 있다.
  • 나중에 유지보수 하기도 편하다.

단점

  • 굴리려면 서버가 필요하다. => 가상서버를 굴리고 있으니 상관없다.
  • 서버 인프라를 직접 내가 관리해야 한다. => 이건 좀 귀찮다.
  • RDBMS를 써야 하는데, 가상서버가 워낙 저렴해서 툭하면 mysql이 죽는다.. => 관리를 하고 있기는 한데 자다가 일어나서 mysql을 재기동하고 싶지는 않다.
  • 실제로 보여지는 부분은 워드프레스로 만들 꺼라서, python / PHP 2가지 환경을 모두 관리해야 한다.

php

장점

  • 어차피 웹호스팅에 올릴 꺼라서 인프라는 내가 관리 안한다.

단점

  • 그 외 나머지.

그렇지만 저는 인프라의 귀차니즘이 다른 것보다 더 크게 다가왔기 때문에 그냥 PHP로 하기로 했습니다.

데이터 출처 획득

github의 awesome-devblog 에서 가져왔습니다.
많은 파일 중 db.yml 을 사용해서 데이터 출처를 획득합니다.
db.yml 은 확장자에서 유추하다시피 yaml 파일입니다.
PHP에서 yaml 파일을 사용하기 위해서는 파싱이 필요하죠.

YAML 파싱

yaml_parse – 실패

PHP에는 YAML 을 파싱하는 기본 구문이 있습니다.
바로 yaml_parse 인데요.
안타깝게도 PECL 을 필요로 합니다.
웹호스팅에서 PECL 을 지원해 줄 리가 없어서 포기하고 다른 라이브러리를 찾아봅니다.

spyc

곧바로 구글링 결과 spyc라는 라이브러리를 찾습니다.
순수 php로 만들어진 yml 파서입니다.

function get_yml_db(){  
	$url = "https://raw.githubusercontent.com/sarojaba/awesome-devblog/master/db.yml";  
	$data = curl_get_content($url);  
	$result = Spyc::YAMLLoad($data);  
	return $result;  
}  

이런 식으로 데이터를 가져옵니다.

curl_get_contents

curl_get_content는 제가 개인적으로 만든 함수입니다.
대부분의 웹 호스팅은 다른 서버에 접속해서 정보를 가져오는 file_get_contents를 보안상의 이유로 막아두었기 때문에 (열어놓으면 다른 호스팅 사용자의 파일을 그대로 볼 수 있으니까요..) curl로 래핑해서 만들었습니다.
동작은 file_get_contents 와 동일하게 인터넷 어딘가에 있는 웹페이지를 긁어옵니다.

소스코드는 selfhow gist – curl_get_content 에서 확인 가능합니다.

yaml 로드

$result = Spyc::YAMLLoad($data);  

한줄이면 됩니다.
yaml 을 PHP 배열 형식으로 파싱해 줍니다.

DB 구조 설계

function createtable(){  
	$db = createdb();  
	$db->createtable("awesome_list", array('name', 'last_collect_date'));  
	$db->createtable("awesome_posts", array('awesome_list_id', 'title', 'link', 'content', 'summary', 'pubdate_ymd','blog_name','blog_link', 'insert_date'));  
}  

이런 구조로 만듭니다.

awesome_list

awesome_list 는 각 개발자 블로그 목록입니다.
name을 키로 가지고 있고, 마지막 수집일을 항목으로 가집니다.
다만 db.yml 파일을 실제로 보시면 아시겠지만, 개별 블로그마다 여러가지 항목이 있을수도 있고 없을수도 있습니다.
누군가는 profile, blog 만 가지고 있고, 다른 누군가는 rss, twitter 등이 있는 형식이죠.

awesome_attrs 테이블 만듬 – 삭제.

처음에 설계할 때는 RDBMS 의 원칙에 충실하게 이런 식으로 만들었었어요.
awesome_attrs 테이블을 만들고, awesome_list_id, attr_key, attr_value 컬럼을 추가했습니다.
그런데 이렇게 하다 보니 문제가 생겼어요.
개발자 블로그는 고정이 아니라 주기적으로 갱신을 해야 하는데, 한번 전체 갱신을 할때마다 awesome_attrs 테이블의 CRUD 가 너무 많이 일어납니다.
2018.10.10 현재 yml DB가 1000개를 조금 넘어가는데, 여기서 개별 attr마다 insert or update를 하려니 CRUD 가 한번의 요청 당 4천번 이상 일어나는 거에요.
그래서 다른 방법을 강구합니다.

awesome_list 테이블의 확장

awesome_list 테이블에 하나의 attrs 가 있을 때마다 컬럼을 추가합니다.
즉 처음에 awesome_list 테이블에는 name과 last_collect_date 항목만 있었는데, 나중에는 blog, facebook, twitter, linkedin .. 등의 컬럼이 생기는 형태입니다.


function set_columns($yml_db){
	$db = createdb();

	// attr에 해당하는 컬럼들 추가.
	// 이미 DB 컬럼에 존재하는 컬럼 목록 추출.
	$exist_columns = array();
	$tbl_columns = $db->show_columns('awesome_list');
	foreach ($tbl_columns as $row) {
		$field = $row['Field'];		
		array_push($exist_columns, $field);
	}
	
	$add_columns = array();
	foreach ($yml_db as $lst) {
		foreach ($lst as $attr_key => $attr_value) {

			// 이미 있는 컬럼이 아니고, 새롭게 ymldb에서 추가된 컬럼이 아니라면
			if (
				in_array($attr_key, $exist_columns) == false
				&& in_array($attr_key, $add_columns) == false
			){	
				array_push($add_columns, $attr_key);
			}
		}
	}

	if (count($add_columns) > 0 ){
		$db->addcolumn('awesome_list', $add_columns);	
	}
}

awesome_posts

awesome_posts 는 개발자 블로그의 글 모음입니다.
awesome_list 를 부모 키로 가지고, 제목, 링크, 컨텐츠, 요약, 발행일, 블로그 이름, 블로그 링크, 데이터를 넣은 날짜 등을 가지고 있네요.
이 테이블의 스키마는 변경되지 않습니다.

개발자 블로그 목록 갱신


function update_yml_db(){
	$yml_db = get_yml_db();
	$db = createdb();
	set_columns($yml_db);

	foreach ($yml_db as $lst) {
		// 이름이 없으면 식별자가 없어서 pass
		if (array_key_exists("name", $lst) == false){			
			continue;
		}
		$name = $lst['name'];
		$db->insert_or_update('awesome_list', $lst , "name = ?", array($name));
	}
}
  • yml을 읽은 다음 $yml_db = get_yml_db();
  • 컬럼을 확장하고 set_columns($yml_db);
  • name을 키로 개별 블로그를 갱신합니다.

개별 글 갱신

RSS 리더 등을 만들 때 가장 어려운 건, 기술적으로 구현하는 부분이 아니라 수집하는 시간을 줄이는 거에요.
예를 들어서 awesome-devblog 에 약 1,000개의 블로그가 있다고 하면, 1분에 블로그 하나의 갱신 여부를 체크한다고 해도 1시간에 60개의 블로그밖에 수집할 수 없죠.
이래서야 14시간이 지나서야 블로그 글을 한 번 수집할 수밖에 없게 됩니다.
실시간성이 너무 떨어지네요.
nodejs, python, java, c# 등 다른 언어를 사용한다면 async 나 multithread 등을 사용해서 시간을 확 줄일 수 있는데, 아쉽게도 php는 멀티쓰레드가 잘 지원되지 않습니다.
only web을 위한 언어라서, only web에서는 멀티쓰레드를 쓸 일이 별로 없거든요.

그렇다고 계속 수집기를 돌린다면 분명히 웹 호스팅 업체에서 리소스 과다 사용을 이유로 저를 쫓아낼 것이 분명합니다(…)

한번에 10개씩만 갱신하자

그래서 한번에 10개씩만 갱신하자라는 방침을 세웁니다.
가장 수집한 지 오래된 항목 10개를 가져온 다음, 그 10개에 대해서만 rss를 읽는 거죠.

$query = "select * from awesome_list where rss is not null order by last_collect_date asc limit 10";  

마지막 수집일 갱신


$db->insert_or_update(
	'awesome_list', 
	array('last_collect_date'=>date('YmdHis')), 
	"awesome_list_id = ?",
	array($awesome_list_id)
);

마지막 수집일을 먼저 갱신하고 다른 프로세스를 시작합니다.
이렇게 하는 이유는 수집하려는 블로그가 수집하려는 시점에 정상 작동한다는 보장이 없기 때문입니다.
일시적으로 서버가 다운되었거나, 속도가 너무 느려서 사용할 수 없는 수준일 경우가 있어요.
만약에 수집을 끝내고 나서 수집일을 갱신하게 되면, 블로그 글을 수집하다가 말고 php의 request time out때문에 수집이 정상 작동을 안할 수도 있습니다.

  • 정상 프로세스 : 블로그 rss 데이터 가져옴 => 내 DB에 갱신 => 수집일 갱신
  • 비정상 프로세스 : 블로그 rss 데이터 가져옴. 너무 오래 걸림 => 내 DB에 갱신 전에 request time out에 걸림

이렇게 될 수도 있습니다.
이렇게 되면 다음번에 수집 시기가 다가왔을 때 “오래 걸리는 블로그 먼저” 다시 수집을 시작하기 때문에 다른 블로그 글 수집 전체에 영향을 끼칠 수도 있습니다.

그래서 비정상작동을 하는 블로그도 일단 수집되었다고 마킹을 해 두고 실패하면 어쩔 수 없지.. 라고 판단하기로 했어요.

rss 수집

$rss_url = $row['rss'];  
$rss_result = rss::read_rss($rss_url);  
$rss_items = $rss_result['items'];  
$rss_items = array_reverse($rss_items);  

rss 수집기는 simplepie 를 사용합니다.
특정 rss url을 던지면 알아서 rss를 가져와 줍니다.

rss 의 특성상 최신 글이 0번이 되기 때문에, 배열을 뒤집어서 오래된 글부터 처리해요.

이미 처리한 글은 패스


if ($db->simpleselect2exist('awesome_posts', 'link', $link)){
	continue;
}

단순하게 개별 글의 링크가 포스트 테이블에 이미 있다면 이미 처리한 글로 보고 추가적인 작업을 하지 않습니다.

미리보기 준비


$stripcontent = strip_tags($content);
if (mb_strlen($stripcontent) > 140){
	$stripcontent = mb_substr($stripcontent, 0, 140);
}

rss로 수집한 본문 글을 html 빼고 순수 텍스트만 가져옵니다.
그렇다고 전문을 다 보여주면 저작권 위반 이기 때문에 앞에 140글자만 잘라오지요.

개별 아이템 db에 입력


$bindval = array(
	'title'=>$title,
	'link'=>$link,
	'content'=>$stripcontent,
	'summary' => $summary,
	'pubdate_ymd'=>$pubdate_ymd,
	'blog_name'=>$blog_name,
	'blog_link'=>$blog_link,
	'insert_date'=>date('YmdHis')
);

$db->insert('awesome_posts', $bindval);

db에 입력했어요 🙂

워드프레스로 글 쓰기

XMLRPC


$remote = new xmlrpc();
$remote->newPost("awesome-devblog",
	"워드프레스ID","워드프레스비밀번호",
	"워드프레스주소.com/xmlrpc.php",
	$write_title,
	$write_content,
	'',
	true,
	$tags,
	$pubdate
);

워드프레스는 xmlrpc 를 이용해서 원격 글쓰기가 가능합니다.
이렇게 개별로 만들어진 글을 워드프레스로 보냅니다.
그래서 최종적으로는 데브 블로그 로 글이 써 집니다.

$pubdate

유심히 보시면 $pubdate 라는 변수를 보실 수 있습니다. 글 쓴 날짜를 지정하는 값이에요.
데브 블로그는 원 블로그가 글을 쓴 날 ($pubdate) 을 기준으로 글을 작성합니다.
따라서 가장 최근에 올라오는 글이 무조건 조금 전에 수집되었다는 보장같은 건 없습니다.

주기적으로 실행하기

제가 일일이 수집해라 .. 라고 버튼을 누르고 있을 수는 없으니 cron을 사용해서 주기적인 작업을 하기로 해요.
물론 제 가상서버를 이용해서 수집할 수도 있습니다만,
cron-job.org 에 가보면 온라인으로 cron을 서비스하는 곳이 있습니다.
물론 무료죠 🙂

여기에 cron-job을 등록해 놓고 쓰기로 합니다.

http header auth


if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) && $user == '내아이디' && $pw == '내비번'){
	update_yml_db();
}

http header auth 라는 건 일반 웹페이지로 로그인을 하는 게 아니라, http header에 로그인 아이디와 비밀번호를 담아서 보내는 걸 말합니다.
다행히도 cron-job.org는 http header auth 를 지원하기 때문에 여기에서 인증을 하기로 해요.
그렇지 않으면 아무나 url만 알면 수집기를 돌릴 수 있기 때문에 마음이 무척 불안하고 콩닥콩닥하거든요.

블로그 목록 갱신


function run(){
	auth::check_auth();
	$method = $_GET['method'] ? $_GET['method'] : null;
	if ($method == null){
		exit();
	}

	if ($method == 'yml_db'){
		update_yml_db();
	}
	elseif ($method == 'rss_list') {
		get_rss_list();		
	}
}

/?method=yml_db 라는 형식으로 요청이 들어오면 블로그 목록을 갱신해요.
저는 하루에 한번만 블로그 목록을 갱신하기로 했어요.
블로그가 매 시간마다 업데이트 될 것 같지는 않았거든요.

개별 글 갱신

/?method=rss_list 라는 형식으로 요청이 들어오면 개별 글을 갱신해요.
5분에 한번씩 실행되게 설정해 뒀어요.
하나 처리하는데 얼마나 걸릴 지 몰라서 테스트를 해 보았는데요.
한번도 처리하지 않은 한번에 한 블로그 처리하는데 평균 15초 정도 걸리더라고요.
10개를 처리하는데 2.5분 정도 걸린다는 뜻인데, 여유 시간을 두기로 했어요.
그래서 5분에 10개씩 블로그를 처리합니다.

cron-job 로그 확인

cron-job에는 놀랍게도 무료인데도 불구하고 run history까지 볼 수 있어요.
하지만 매 번 fail로 뜨네요.
이유는 cron-job은 한번에 30초씩까지만 request time out을 유지하는데, 저는 한번 실행할 때 2.5분이 걸리거든요.
fail 횟수가 너무 누적되면 자동으로 cron-job이 꺼져요.
cron-job.org에서 the cronjob will be disabled because of too many failures 를 체크해 두면 cron-job이 자동으로 꺼질 때 이메일이 와요.
그럼 다시 들어가셔 켜주시면 되요.

만드는데 걸린 시간

뭘 만들지 조사하고, 기술을 선택하고, 설계하고, 다 만들어서 서비스를 시작할 때까지 약 6시간 정도 걸린 것 같습니다.
업무 시간 피해서 하느라고 시간이 좀 걸렸네요.

~소개글 쓰느라 시간이 더 걸렸다는 건 비밀입니다. ~

업데이트 내역

2018.10.11.

처음 워드프레스로 구축할 때는 서브 디렉토리를 개별 도메인으로 변경하는 프록시 서버 방식으로 네트워크 세팅을 해 두었었어요.
이 방식은 웹 호스팅이 용량 / 트래픽 / 리소스 등 오버가 나지 않는 한 무제한으로 사이트를 늘릴 수 있다는 장점이 있죠.
하지만 아쉽게도! 너무 느립니다.
국내 서버에서 국내 서버로 프록시하는데도 페이지 로딩에 거의 5초 가까이 걸리네요.
그래서 제가 관리하는 다른 워드프레스 사이트의 멀티사이트로 이전했습니다.
아무도 접속 안했지만 이 때문에 일시적으로 접속이 원활하지 않았습니다. 아쉽네요.

맺음말

잘 이용해 주시면 저야 감사하죠.