PHP는 정말 배울 가치가 없을까?
주류 언어 중 PHP만큼 비판받는 언어도 없다. PHP에 대한 비판이 지나치다 못 해 인신공격까지 서슴치 않는 경우도 있다. 오늘도 가루가 되도록 까이는 PHP를 위해 해명을 곁드린 PHP의 효용 가치에 대한 내 생각을 이야기 해보려고 한다.
PHP는 다른 언어들처럼 뚜렷한 큰 목적이나 이론(사상)을 기반으로 만들어진 언어가 아니다. 초기 PHP의 약자는 Personal Home Page였다. 즉, 개인 홈페이지 만들려고 나온 언어다. 그런 언어가 이제는 웹사이트 서버사이드 프로그래밍 언어 중 점유율이 매년 늘어나 2013년에는 78.7%에 달하고 있다. 기술 수준과 완성도가 진리라 생각하는 개발자나 해커로서는 땅을 치고 통탄 할 일이 아닐 수 없을 것이다. 그들에게 PHP는 쓰레기 같은 언어고, 당연히 쓰레기로서 취급을 받아야 마땅한 일일테니까 말이다.
"오마이갓! 무슨 일이 일어난거죠?"
그분들은 이리 생각하시고 계시는 듯 하다.
1. PHP는 프로그래밍 언어로서 치명적인 기초 설계 결함이 있고, 그로 인한 한계가 너무나도 크다.
2. 보안 문제나 단점은 적기조차 버거울 정도로 많다.
3. 그런 PHP가 시장에서 널리 사용되는 것은 잘 못 된 일이다.
4. 새로 프로그래밍을 배울 때 PHP를 배우는 것은 잘 못 된 개발의 길로 들어서는 일이다.
5. PHP로 만들어진 기존 시스템도 훌륭한 다른 언어 - 아마도 그 분들이 쓰고 계신 각자의 언어로 - 다시 개발해야한다.
6. PHP를 프로그래밍 언어 시장에서 완전히 쫓아내 아무도 사용하지 않는 언어로 만들어야 한다.
PHP가 가진 문제점 자체를 부정하는 사람은 거의 없다. 대부분의 PHP 개발자도 사실 관계로서의 단점에 대한 지적은 수긍을 하는 편이다. 나 또한 그러하다. 그런데 우리는 많은 문제점과 낮은 기술 수준을 가진 제품이 오히려 시장에서 더 인정을 받고, 결국 더 오래 살아남는 경우를 종종 보아왔다. PHP는 정말로 하루라도 빨리 시장에서 사라져야만 하는 것일까? 아니면 단점과 낮은 기술 수준에도 불구하고 시장에서는 더 오래 살아남았던 것들 중 하나가 될 것일까?
개인적으로 몇 년 전까지만 해도 프로그래밍 언어를 추천 해 달라고 하면 일반인에게는 파이썬(Python)을 추천 해 주었다. 개발자를 지망하는 사람에게는 C 언어를 - 그리고 펄(Perl)도. 1999년 PHP를 처음 배울 때부터 여러 단점 때문에 짜증을 낼 때가 많았다. 아마도 이런 경험이 나로 하여금 PHP를 추천하지 않게 하였을듯 하다. 예를 들어 PHP 4의 클래스는 이걸 쓰라고 만든 것인지 의심스러울 정도였다.
그러나 PHP가 주류 언어 중 하나로 성장하면서 지적됐던 단점들이 하나씩 보완되고 있기 때문에, 여전히 남은 단점들에도 불구하고 시장에서 빨리 퇴출시켜야 된다는 주장에 무작정 동의하기 보다 몇몇 상황에서는 PHP를 배우길 추천하고 싶다.
1. 웹사이트가 일이나 취미에 보완적으로 도움이 되는 경우
프로그래밍 그 자체가 주목적이 아니라 마케팅과 같은 비기술적인 일이나 커뮤니티 같은 취미에 웹사이트가 도움이 될 것 같아 직접 만들거나 운영하려는 사람들에게는 PHP가 학습 비용 및 운영 비용에서 잇점이 있는 언어다.
PHP는 현재 PHP를 사용하지 않은 개발자들도 대부분 어느 정도는 알고 있을 정도로 - 심지어 PHP를 비판하시는 분들 조차도 - 주변의 도움을 얻기 쉬운 언어다. PHP를 모르더라도 숙련된 C/C++ 개발자라면 즉석에서 PHP 메뉴얼을 보고 답을 줄 수 있을 정도다.
또한 PHP는 웹호스팅 비용이 매우 저렴하다. PHP를 제공하지 않는 웹호스팅 업체는 거의 없을 정도고, 제공하는 서비스의 완성도도 높은 편이라 문제를 겪을 일도 적고, 발생한 문제에 대한 기술 지원도 잘 이루어진다. 웹호스팅 업체 직원도 PHP는 대부분 알고 있다. 적은 비용으로 하고자 하는 일이나 취미에 도움이 되는 웹사이트를 운영 할 수 있다.
2. PHP로 만들어진 도구를 이용해야 하는 경우
PHP가 높은 시장 점유율을 가진 것의 절반은 이런 도구들 때문이라고 생각한다. CMS 시장의 1, 2, 3위를 차지하는 워드프레스(WordPress)와 줌라(Joomla), 드루팔(Drupal) 모두 PHP로 개발되어 있다. 조그마한 소호(SOHO) 사업을 하거나, 쇼핑몰이나 커뮤니티, 소규모 미디어 사업 등을 위해 이런 도구들을 이용하는 경우 PHP를 배우는 것이 유리한 점이 많다.
각종 플러그인이나 기능들을 직접 원하는대로 수정하여 운영 할 수 있는 경우 다른 사이트에 비해 경쟁력을 갖추는데 도움이 된다.
3. 개인 또는 제한된 사용자 규모를 가진 웹페이지를 만들어야 할 경우
다른 언어(특히 C/C++) 개발자가 개인 또는 내부 직원들에게 배포 할 용도로 1 ~ 2 페이지 정도 규모의 웹페이지를 만들어야 할 경우 두 언어 모두 능숙하다는 가정하에 HTML과 로직을 마구 섞어서 사용 할 수 있는 PHP의 접근 비용이 더 적을 수 있다. 물론 이 때에도 기존에 다른 언어로 구축 된 라이브러리들을 이용해야 한다면 PHP는 선택 대상이 아니겠지만, 그렇지 않은 경우는 고려 해 볼만한 하다.
4. 이미 PHP로 구축된 시스템이 있는 경우
개인적으로 PHP를 사용하고 계속 공부하고 있는 가장 큰 이유다. 백만 라인을 가뿐히 넘는 PHP(+HTML) 소스 코드를 PHP가 단지 단점이 많고, 완성도가 떨어진다고 다른 언어로 재개발 해야 한다고 주장하려면 책상 위에 사직서를 올려놓고 이야기를 시작해야 할지도 모른다. 사업에 캐시카우인 시스템이 PHP라면 당연히 PHP의 단점을 잘 이해하고 해결(회피) 할 수 있는 방법까지 터득한 절정의 PHP 실력을 갖추도록 노력하는 것이 맞다. PHP를 버리고 다른 언어로 다시 재개발하기 보다 단점을 해결하는 수준을 넘어 안드로메다까지 끌어올린 회사를 우리는 알고 있다. 페이스북.
이제 이야기를 정리하고 마무리 하자.
PHP는 외부 라이브러리의 매개변수 순서를 자신에게 맞추지 않고, 원형 그대로를 유지 할 정도로 실용을 우선시 하는 언어다. 덕분에 체계적이거나 기술적 완성미와는 거리가 멀다. 그러므로 PHP를 선택하거나 배우려 할 때도 실용에 초점을 두는 것이 맞다. 프로그래머로서 해커로서 엔지니어로서 기술적 욕구에 대한 충족은 미안하지만 다른 언어로 채우는 것이 낫다. PHP는 분명 나아지고 있고 언젠가 그 단점들을 모두 해결해낼지 모르지만, 당분간 그게 이뤄질 가능성은 없어낮아 보인다. 몇몇 언어들처럼 PHP6가 하위 호환성을 버리고 단점들을 한 방에 정리 할 것 같진 않다는 이야기다.
한 마디로 정리하자면 이렇다.
PHP는 실용성이 중요한 상황이라면 배워 둘만한 실용적 가치 정도는 있는 언어다.
월의 몇 주차인지 계산하기
평소 크게 신경을 쓰던 문제는 아닌데 이번에 관련 작업을 하면서 이게 간단한 문제는 아니라는 것을 알게 됐다. 이 문제가 어려운 이유는 그 해의 몇 주차인지에 대해서는 ISO 표준이 있는데, 월의 몇 주차인지에 대해서는 표준이 없기 때문이다. 아마 표준이 있었으면 이미 날짜 관련 함수 중에 있었을 것이다.
본격적으로 이야기를 꺼내기 전에 주(week)에 관한 ISO 표준 중 관련 사항을 알아 보자.
1. 한 주의 시작은 월요일이다.
달력에 일요일부터 표시하고 있기 때문에 한 주 시작이 일요일 같지만 월요일이 표준이라고 한다. 즉, 한 주는 월요일로 시작 해 일요일로 끝난다.
2. 년도의 주차는 해당 주의 목요일 년도에 따라간다.
예를 들어 1월 1일이 목요일이면, 12월 31일(수), 12월 30일(화), 12월 29일(월)은 전년도에 포함되지 않고 이번 년도의 1주차에 포함된다. 반대로 1월 1일이 금요일이면 1월 1일(금), 1월 2일(토), 1월 3일(일)은 전년도 마지막 주차에 포함된다.
그럼 월의 주차를 계산하는 방법은 어떨까?
아주 다양한 계산법들이 있지만 그 중 널리 쓰이는 계산법을 몇가지 살펴보자.
1. 년의 주차 계산과 동일한 방법
즉, 목요일이 어느 달에 속하느냐를 따져 전달에 마지막 주차에 포함시키거나 이번 달 첫 주차에 포함시킨다.
2. 월요일을 기준으로 하는 방법
월요일에 어느 달에 포함되느냐에 따른다. 월요일이 31일이면 그 뒤에 나오는 1, 2, 3, 4, 5, 6일은 마지막 주차에 포함시킨다.
3. 각 달에 포함하는 방법
30일이 월요일인 경우 30일(월), 31일(화)는 전 달의 마지막 주차로 계산하고, 1일(수), 2일(목), 3일(금), 4일(토), 5일(일)은 이번 달 첫 주차로 계산하는 방법이다.
그럼 어떤 방법으로 월의 주차를 계산해야 할까? 여기서는 첫번째 년의 주차 계산과 동일한 방법으로 계산하기로 하자. 이렇게 한 이유는 다른 계산법으로 하는 경우 년의 주차로는 1 주차면서 12월의 마지막 주차로 계산되는 불일치가 나타날 수 있기 때문이다. 이런 불일치는 다른 파생적인 문제와 예외 처리가 필요 할 수 있기 때문에, 년의 주차 계산과 동일한 방법으로 월의 주차를 계산하려고 한다.
<?php
class DateTimeExtra extends DateTime {
public function weekofmonth() {
$thursday = $this->thursday();
if ( $thursday->format('n') != $this->format('n') ) {
$date = $thursday;
}
else {
$date = $this;
}
$firstday = clone $date;
$firstday->sub(new DateInterval('P'.($date->format('j') - 1).'D'));
$thursday_of_firstday = $firstday->thursday();
if ( $thursday_of_firstday->format('n') != $firstday->format('n') ) {
// 이번 달 첫번째 날을 포함한 주의 목요일의 월과 첫번째 날의 월이
// 다른 경우는 첫번째 날이 이전 달의 마지막 주에 포함되어 있다는
// 것이므로 월의 주차 기준이 되는 주를 한 주 뒤로 미룬다.
$firstday->add(new DateInterval('P1W'));
}
$month = $date->format('n');
$weekofmonth = $date->format('W') - $firstday->format('W') + 1;
return array(intval($month), intval($weekofmonth));
}
private function thursday() {
$thursday = clone $this;
$dayofweek = $this->format('N');
if ( $dayofweek < 4 ) {
$thursday->add(new DateInterval('P'.(4 - $dayofweek).'D'));
}
if ( $dayofweek > 4 ) {
$thursday->sub(new DateInterval('P'.($dayofweek - 4).'D'));
}
return $thursday;
}
}
?>
월의 주차의 계산은 이렇게 했다. 우선 특정일이 속한 주의 목요일과 특정일이 속한 월의 1일이 속한 주의 목요일을 구한다. 그리고 이 둘의 월을 비교한다. 서로 다르면 이번 달의 1일이 속한 주가 전 달의 마지막 주차에 포함된 것이므로 이번 달의 시작인 년의 주차를 한 주 미룬다. 같으면 이번 달 1일이 포함된 주차가 이번 달의 첫 주차이므로 그대로 사용한다. 이 둘의 년의 주차를 빼주면 특정일의 해당 월의 주차를 구할 수 있다.
동시성 문제를 해결하기 위한 MySQL 잠금 두가지
InnoDB를 사용하면서 동시성(Concurrency)을 고려하지 않고 개발을 하면 큰 문제가 생길 수 있다. 아래에 회원 포인트를 수정하는 간단한 로직을 살펴보자.
<?php
$dbo = new mysqli('localhost', 'bookworm', '', 'test');
$result = $dbo->query('SELECT id, name, point FROM user WHERE name = "bookworm"');
$row = $result->fetch_assoc();
$new_point = $row['point'] + 100;
// $new_point 값을 대입하지 않고 point = point + 100으로 문제를 미리 방지 할 수 있지만
// 예시를 위해 일부러 그렇게 하지 않았다.
$dbo->query("UPDATE user SET point = {$new_point} WHERE name = 'bookworm'");
?>
이 로직이 하나만 실행이 될 때는 문제가 없으나, 웹처럼 동시에 실행이 될 수 있는 환경에서는 문제가 된다.
우선 한 프로세스가 SELECT로 사용자 정보를 가져온다. 그리고 UPDATE를 실행하기 전에 다른 프로세스가 SELECT로 역시 같은 사용자의 정보를 가져온다. 그럼 둘 다 동일한 포인트를 가져오게 되고, 그 뒤에 100을 더한 값으로 처음 프로세스가 UPDATE 하고, 그 뒤에 다른 프로세스가 UPDATE 하게 됨으로서 처음 프로세스가 증가시킨 100 값이 사라지게 된다.
이를 방지하기 위해서는 InnoDB의 기본 Isolation Levels를 변경 할 수 있겠지만, 전체 쿼리 중 동시성을 고려해야 하는 경우가 많지 않다면 간단한 잠금 쿼리를 이용해서 처리 할 수 있다.
InnoDB의 잠금은 두가지로 나눌 수 있다. 하나는 LOCK IN SHARE MODE와 다른 하나는 FOR UPDATE다. 이 둘의 용도는 약간 다르기 때문에 자신의 용도에 따라 신중히 선택을 해야 한다.
LOCK IN SHARE MODE는 SELECT를 한 후에 트랜잭션이 끝날 때까지 해당 ROW 값이 변경되지 않을 것을 보장한다. 바꿔 말하면 해당 ROW를 UPDATE 하거나 DELETE 하려는 쿼리는 잠김 상태가 되어 트랜잭션이 끝날 때까지 대기하게 된다. 하지만, SELECT는 얼마든지 여러 세션이 동시에 수행하는 것이 가능하다.
기존 SELECT 쿼리문 맨 뒤에 LOCK IN SHARE MODE 문장을 추가하는 것만으로 사용이 가능하지만, 트랜잭션이 끝나기 전까지만 유효하므로 auto_commit을 꺼야 한다.
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
FOR UPDATE는 SELECT로 가져 온 데이터를 변경을 하려고 할 때 사용한다.
SELECT * FROM user WHERE id = 1 FOR UPDATE;
FOR UPDATE를 SELECT를 가져온 이후로 해당 ROW에 대해 다른 세션의 SELECT, UPDATE, DELETE 등의 쿼리가 모두 잠김 상태가 된다. 즉, FOR UPDATE를 한 세션 외에 다른 세션들은 모두 해당 ROW에 접근을 할 수 없게 되고, 모두 대기 상태가 된다. FOR UPDATE도 LOCK IN SHARE MODE처럼 트랜잭션이 끝나는 시점에서 풀린다. 처음 예로 든 사용자 포인트 증가와 같은 경우에 알맞는 잠금이다.
웹 프로그래밍에서는 쓰레드 프로그래밍처럼 세밀히 동시성을 고민 할 필요는 없지만, 웹이 기본적으로 다수 사용자가 동시에 같은 데이터에 접근을 하는 환경이므로 이로 인해 주의가 필요하다. 이 외에 이런 문제를 해결하기 위해 트리거나 저장 프로시저 등을 이용 할 수 있지만, 최근 웹서비스 초기에는 MySQL을 단순 key-value 저장소처럼 사용하다 추후 서비스 성장에 맞춰 noSQL을 도입하는 경우가 종종 보이므로, RDBMS의 기능을 너무 적극적으로 활용하지 않는 것이 낫지 않을까 싶다.