Blog

  • Good Job! PHPUnit-Polyfills

    코어의 유닛 테스트 코드들은 아직 완전히 최신 PHPUnit을 지원하는 건 아닌 것 같다. 대신 Yoast의 PHPUnit-Polyfills 라이브러리를 사용하여 호환성을 보장하는 것 같다. 덕분에 PHPUnit 9 최신 버전을 사용할 수도 있고, 그 덕분에 PHP 8.1 최신 버전이 사용 가능하다.

    최신 버전의 PHP와 PHPUnit을 사용하여 유닛 테스트! 이제는 정상적으로 실행된다!

    이게 된다면 앞으로 플러그인 개발시 7.4보다 더 높은 버전을 도입해서 사용할 수 있겠다! 그동안 8.0의 기능을 사용하지 못해 정체되었는데, 이제는 가능할지도.

  • 액션 스케쥴러, 함 잡솨봐

    워드프레스에는 WP-CRON이라는 스케쥴에 맞춰 특정 작업을 실행하는 API가 있다. 액션 스케쥴러는 이것에 기반해 사용하기 편리한 비동기 실행 큐 라이브러리이다. 오토매틱에서 제작하고, 우커머스나 구독 플러그인 등 다수 플러그인에서 이미 사용되고 있다.

    이번에 진행하는 프로젝트에서 이것을 사용해 볼 기회가 있었다. 간단하게 비동기 큐가 필요했고, 구조를 복잡하게 하는 것보다는 간단한 프로토타이핑으로 단순한 구성을 원했기 때문이다.

    액션 스케쥴러는 워드프레스 기반으로 개발할 때, 비동기 큐를 써야 한다면 훌륭한 선택이라고 생각한다. 하여 차후 액션 스케쥴러를 다시 써야 할 때를 위해 간단히 사용한 것을 정리한다. 공식 홈페이지에서 5분이면 간단하게 얻을 수 있는 정보의 복제 말고, 조금 애매했던 개념을 설명하는 포스트이다.


    액션이란 특정 시간에 특정 작업을 하기 위한 수단이다.


    액션의 청구라는 개념이 있다. 영어로는 대략 stake claim 정도로 표현되는데, 액션 테이블에서도 claim_id 라는 것이 발견되기도 하고, 공식 문서에서도 한 두번 정도 언급될 뿐 이것이 무엇인지 자세히 설명하지 않는다. 여기서 간단히 설명하자.

    액션은 이벤트를 예약하는 수단이다. 그러나 이것이 실행 큐에서 실제로 실행되기 위해서는, 먼저 큐가 액션의 스케쥴을 파악하여 적절한 액션을 골라 실행하기로 결정하는 작업이 먼저 필요하다. 해당 시점에 적절한 액션을 골라 실행을 결정하는 과정이 액션의 청구(claim)이며, 실행 큐가 한번에 작업하기로 결정한 액션에게는 동일한 청구 ID (claim ID)를 부여한다.


    실행 큐가 실행되면서 한 번에 액션 뭉치를 가져와 일괄적으로 작업하는 것을 배치(batch)라고 한다.

    배치에는 배치 크기 (batch size)라는 개념이 있다. 액션을 청구할 때 한번에 이 갯수만큼의 액션을 정한다. 배치 크기의 기본값은 25이다. 기본값 대로라면 실행 큐는 한 번에 최대 25개의 액션을 가져와 작업을 진행할 것이다. 이 때 25개의 작업은 동시 작업(multitasking)이 되는 것은 아니고, for-loop 을 돌면서 순차 진행된다.


    실행 큐는 WP CLI를 통해 커맨드라인으로 실행이 가능하다. 커맨드라인에서는 제한 시간의 제약기 없기 때문에 상당히 오랜 시간 동안 작업을 해도 에러만 없다면 아무 문제 없다.


    실행 큐는 기본적으로는 WP-CRON의 ‘매 분 (every minute)’ 스케쥴에 맞춰 동작한다. 그러므로 리눅스 기준 매 분마다 정확하게 실행 큐가 동작하는 것을 보장하려면 아래처럼 작업을 한다.

    1. wp-config.php에 define( 'DISABLE_WP_CRON', true ); 를 삽입.
    2. crontab에, 아래 코드 삽입.
    * * * * * wget --delete-after http://YOUR_SITE_URL/wp-cron.php >& /dev/nullCode language: JavaScript (javascript)

    참고로 훅 이름은 ‘action_scheduler_run_queue’ 이다.


    실행 큐는 훅의 콜백으로 단 1개만 실행된다. 즉, 기본값 대로라면 매 분 1개의 실행 큐가 생성되어, 25개의 액션만을 청구하여 배치를 하는 것이다.

    이 기본값들은 액션 스케쥴러 개발자들이 다양한 상황을 염두에 두고 매우 동작에 제약을 둔 설정이다. 만약 운영하는 서버가 성능이 보다 괜찮다면 큐를 좀 더 둘 수 있다. 이 때 ‘Increasing Initialisation Rate of Runners‘에서 말하는 것처럼 ‘action_scheduler_run_queue’ 훅에 맞춰 admin-ajax.php 에 원하는 갯수만큼의 실행 큐를 증가시켜 볼 수 있다. 간단한 로그를 찍는 코드로 테스트 해 봤을 때, 저렇게 만들어진 큐에서 작업이 진행되며 AJAX 요청을 보낸 쪽에서는 더 작업을 하지 않는 듯. 그러므로 N개를 추가시켰으면 N+1의 실행 큐가 아닌 N개의 실행 큐가 생성되는 모양이다.


    여러 개의 실행 큐가 admin-ajax.php 요청을 통해 실행한다면, 비동기적으로 동시 실행되는 그림이 만들어진다. 이 때 동시 배치 (concurrent_batch)라는 정수값이 중요한 역할을 한다. 이 값의 디폴트는 1이다.

    각각의 실행 큐는 각자 작업을 진행하면서, 현재 청구(claim)되어 있는 액션의 개수가 이 동시 배치를 넘는지 넘지 않는지 체크하여 초과하면, 작업을 하지 않도록 프로그래밍 되어 있다.

    디폴트인 1을 바꾸지 않으면 다음과 같은 문제점이 발생한다. 설명이 복잡하니 불릿으로 나누어 설명한다.

    • 첫번째 실행 큐가 현재 청구된 액션 수와 동시 배치 수를 비교한다. 가장 처음이므로 청구된 액션 수는 0이다.
    • 동시 배치값 1은 0보다 크므로, 첫번째 실행 큐는 배치를 시작한다.
    • 첫번째 실행 큐가 배치를 하기 위해 액션을 청구한다. 배치 크기만큼 (기본값이라면 최대 25개) 액션을 잡을 것이다.
    • 두번째 실행 큐가 동작을 시작한다. admin-ajax.php를 통해 실행되므로, 첫번째 실행 큐와는 서로 독립적인 프로세스에서 동작한다.
    • 두번째 큐와 현재 청구된 액션 수와 동시 배치 수를 비교한다. 첫번째 큐가 이미 25개의 액션을 청구한 상태이다. 엄청나게 빠른 속도로 액션들이 처리되고 있는 것이 아니라고 가정하자. 즉 아직 여전히 25개가 청구된 상태이다.
    • 동시 배치값 1은 25보다 작다. 두번째 큐는 실행을 멈춘다.
    • 즉 두번째 큐는 아무 일도 하지 않고 일을 마친다. 뭐, 세번째, 네번째 큐가 있더라도 액션 청구가 1보다 줄지 않는 한 일을 하지 않을 것이다.

    그러므로 동시 처리 수는 (총 실행 큐의 수) * (배치 사이즈) 에 근접하게 설정해야 제대로 동시성을 기대할 수 있다. 이게 가장 핵심인 부분이다.


    이 부분은 나중에 덧붙인 글이다. 동시 처리 수에 대한 개념이 부족해 보충 설명을 적는다. 위 설명은 액션이 충분히 길 때에만 적절한 설명이다. 동시 처리 수를 실행 큐 수 * 배치 사이즈에 맞추더라도 하나의 큐가 모든 작업을 하려고 들 수도 있다. 하나의 실행 큐는 자기에게 허락된 메모리량, 처리시간의 여유가 되는 만큼 최대한 액션들을 많이 가져려고 한다.

    예를 들어 보자. 총 실행 큐 수는 3개, 배치 사이즈 5개, 동시 처리 수를 15개로 잡았다고 보자. 그리고 처리해야 할 액션 수는 15개라고 가정하자. 그러면 총 실행 큐 3개가 각각 액션 5개를 맡아 처리할 것 같지만, 실제로는 그렇게 동작하지 않을 수 있다.

    만약 주어진 액션이 너무 간단해 하나의 실행 큐가 5개의 액션을 모두 처리한 다음엔, 그 실행 큐는 메모리와 실행 제한 시간이 허락된 선에서 추가적인 액션을 불러와 일을 한다. 만약 액션 처리가 너무 간단했다면 여전히 하나의 실행 큐가 모든 액션을 혼자 담당할 수도 있다. 다른 실행 큐가 비동기적으로 실행되기도 전에 말이다. 정말 너무 일이 간단하다면 하나의 실행 큐가 동시 처리 수와 무관하게 스케쥴 된 모든 액션들을 가져와 작업을 할 수도 있다!


    액션 스케쥴러는 유연성이 상당히 높다. 액션 스케쥴러는 플러그인으로도, 내 플러그인이나 테미의 서브 프로젝트로도 사용 가능하다.

    액션 스케쥴러는 원래는 wp_posts 테이블에서 액션을 관리했던 모양이다. 처음 실행하면 마이그레이션 훅을 돌리는 것이 있는데 아마 예전의 포스트 테이블의 내용을 별도 테이블로 전환하는 과정을 돌리는 것 같다. 굳이 예전으로 돌릴 필요는 없는 듯.

    작업을 돌리는 데 분 단위로 동작한다는 점이 약간 제약으로 생각될 수 있다. 그러나 정말 다량의 작업량이 있다거나 지정된 스케쥴에 동작하는 것이 우선적인 목적이라면 해당 단점이 크게 부각될 것 같지는 않다. 액션 스케쥴러는 확실히 워드프레스 환경에서 상당히 괜찮은 라이브러리임에 틀림없다고 생각한다.

  • dbDelta() 함수 똑바로 쓰기

    기본적인 것들

    $wpdb->prefix 꼭 사용하라. 보통 접두에 언더바 붙어 있으니 언더바 중복하지 말고.


    $wpdb->get_charset_collate() 메소드는 자동으로 CREATE TABLE (...) 구문 뒤에 들어갈 DEFAULT CHARACTER SET {CHARSET} COLLATE {COLLATE} 구문을 만들어 준다.


    dbDelta() 호출 전 반드시, require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); 해야 한다. 안그러면 fatal error 먹는다.

    테이블 변경

    코덱스를 참고하면 대략 다음과 같이 쓰라고 되어 있다.

    • 필드 각각 한 줄씩 나눠서 써라.
    • PRIMARY KEY 선언과 주 키 선언 사이에 2개의 스페이스로 띄워라.
    • INDEX 보다는 KEY를 써라. 최소 하나의 키를 첨부해라.
    • KEY는 반드시 다음 괄호 사이에 하나의 스페이스를 두어야 한다.
    • 필드 이름 앞뒤로 어깨점이나 백틱으로 감싸지 마라.
    • 필드 타입은 항상 소문자.
    • CREATE TABLE, UPDATE, KEY 같은 SQL 키워드는 항상 대문자.
    • 필드에 길이 파라미터를 지정할 수 있는 필드는 항상 써야 한다. 가령, int(11) 이나 bigint(20) 같은 것.

    dbDelta() 는 꽤나 복잡한 프로세스를 거쳐 SQL 문을 분석해 테이블의 변화를 감지한다. 그리고 자신이 어떤 일을 했는지를 배열로 리턴한다.

    Array
    (
    [wp_temp_table] => Created table wp_temp_table
    )

    위 예시는 wp_table_table 을 만들었을 때의 결과이다. 그리고,

    Array
    (
        [wp_temp_table.middle_name] => Added column wp_temp_table.middle_name
    )

    이 예시는 wp_temp_tablemiddle_name 이라는 필드를 추가했을 때 결과이다. 이런 식으로 필드 타입, 기본값 등등이 잘 변경되는지 확인할 수 있다.


    필드가 삭제나 인덱스 삭제는 되지 않는다. 별도로 필드를 없애는 구문을 추가해야 할 것 같다.


    코덱스에서는 INDEX 보다는 KEY 쓰리고 하지만, INDEX 가 안 되는 건 아니다. 당연히. 그리고 이미 만들어진 인덱스 변경도 잘 되지 않는다. 별도로 구문을 써야 한다. 마찬가지로 PK 선언시 두 칸을 띄워 쓰라고 하는데, 딱히 왜 그래야 하는지는 잘 모르겠다.


    복잡한 변경은 잘 인지하지 못하는 것 같다. 예를 들어 필드의 타입과 기본값까지 동시에 변경 처리하는 경우, 그 둘의 변경점을 잘 감지하지 못하는 것 같다.


    CREATE TABLE 선언시 CREATE TABLE IF NOT EXISTS 나, 별도로 테이블이 있는지 없는지 체크하는 따위의 삽질을 하지 않아도 된다. dbDelta 가 적절히 CREATE TABLE 구문과 현재 선언된 테이블의 구조를 파악해 CREATE TABLE을 그대로 사용하기도 하지만, CREATE TABLE 대신에 ALTER TABLE 를 스스로 만들어 내거나, 변경점이 없다면 아무 일도 하지 않기 때문이다.


    이런 것도 한 번 체크해 보라. dbDelta() 는 까다로운 녀석이다. SQL 구문에 변경점이 전혀 없어, 딱히 변경할 것이 없어 보인다. 그래도 진짜 아무 일도 실행하지 않는지 (즉, dbDelta()가 빈 배열을 리턴하는지) 확실히 체크해 보라. 간혹, 예를 들어, 타입을 대문자로 썼다거나 하는뭔가 실수 아닌 실수가 있다면 dbDelta()는 변경할 것이 없는데도 계속 ALTER TABLE 구문을 만드는 삽질을 하고 있을 수도 있다.


    db 버전을 설정하고 잘 관리하라. 예를 들어,

    function update_my_table() {
      $current_version = '1.1.0';
      $installed_version = get_option( 'my_db_version' );
    
      if ( $current_version !== $installed_version ) {
        global $wpdb;
    
        $sql = "CREATE TABLE .....";
    
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
        update_version( $current_version );
      }
    }Code language: PHP (php)

    같은 패턴을 쓰면 된다. 이 때 사용자는 테이블을 업데이트하기 위해 플러그인을 비활성화/활성화를 할 수 없다. 그러므로 plugins_loaded 같은 액션의 콜백에서 이 함수를 한 번 호출해야 한다.

    만약 이렇게 되면 activation callback 에서 굳이 테이블을 생성하는 작업을 중복해서 할 필요는 없는 것 같다. 아, 물론 플러그인 활성화 시 바로 초기화 작업 같은 것을 해 준다면 이야기가 달라지겠지만.

    결론

    급하게 테스트해보고 메모하듯이 글을 적었다. 내가 틀린 것을 적었을 수도 있다! 어쨌든 변경점이 생길 때 dbDelta()가 리턴하는 결과와 테이블의 구조를 실제로 잘 체크해야 한다. dbDelta()가 기능 개선이 될 수도 있으니 말이다.

    암튼 현재 dbDelta() 함수는 다른 웹 프레임워크의 마이그레이션 솔루션 (예를 들어, 장고) 만큼 테이블 변경을 세련되게 챙겨주지는 못하지만, 그래도 간단하게 테이블 변경을 찜쪄먹기에는 나름 유용한 도구로 보인다. 다만, 이 녀석이 진짜 까다로우니 조심해서 쓰도록 하자.

  • ipTime 공유기 무단 접속 IP 차단하기

    iptime공유기에 국가별 차단 기능이 생겼다고 해서.. 라는 글을 보고 공유기를 업데이트 해 보니, 진짜로 IP 차단 기능이 생긴 것을 확인하였습니다.

    그런데 UI가 조막만해서 IP를 일일이 관리하고 차단하기가 너무 어려웠어요. 오호라? UI를 잘 보니 규칙을 파일로 백업하는 기능이 있었지 뭡니까? 이것으로 .cfg라는 파일을 생성하는 것을 확인하였습니다.

    Type=firewall # Do not modify
    Version=1.0.0 # Do not modify
    lang=utf-8 # Do not modify
    
    [차단 #1]
    enable = 1
    schedule = 0000000 0000 0000
    flag = 0
    {
    	direction = outin
    	src_type = ip
    	dest_ip_address = {start}-{end}
    	protocol = none
    	policy = drop
    }
    [차단 #2]
    enable = 1
    schedule = 0000000 0000 0000
    flag = 0
    {
    	direction = outin
    	src_type = ip
    	dest_ip_address = {start}-{end}
    	protocol = none
    	policy = drop
    }

    위는 그 cfg 파일의 샘플입니다. 대략 24시간 외부에서 내부로 접속하는 특정 IP 대역을 차단하도록 설정한 예입니다. {start}와 {end}에 각각 IPv4 형태로 문자열을 넣어주면 됩니다.

    처음에는 한국 인터넷 정보 센터 (KRNIC)에 가서 모든 국가별 IP 할당 대역을 파악한 후, 특정 국가의 IP를 전부 차단시킬 생각습니다. 그러나 공유기의 성능 한계가 있어 200개까지 등록되지 않는다는 제약이 있어 이렇게는 할 수 없더군요. 예를 들어, 중국의 모든 IP 대역 등록건만 해도 8000여개에 달하니까요.

    그래서 이렇게 많은 양의 IP 대역에 대해 애쓰기보다는, 관리자 로그 파일을 보고 내 공유기에 천착하는 IP만 차단하기로 했습니다. IP 타임 관리자 로그를 보면 다음처럼 나옵니다.

    2022/03/11 09:02:22	DHCP 서버가 IP 할당함: 192.168.10.32 (MAC : XX-XX-XX-XX-XX-XX)
    2022/03/11 08:20:19	잘못된 VPN 계정 또는 암호로 접속을 시도 하였습니다(qq, XXX.XXX.XXX.XXX)

    2번째줄입니다. 어디선지는 모르지만 VPN 계정이나 관리자 로그인을 노리고 집요하게 뭔가 하는 것입니다. qq라는 id로 로그인 실패를 했고, 뒤에 원격 IP가 나옵니다.

    IP 조회를 해 보면, 진짜 아무 연고도 없는 곳입니다. 이제 로그 특정 부분을 따다 IP 부분만 수집합니다.

    우선 아래 그림처럼 공유기 관리자 로그를 복사해 파일로 만듭니다.

    지금은 깨끗이 비어 있지만, 놓아두면 로그가 많이 쌓입니다.

    파일의 로그에서 IP만 추출해 설정 파일로 만들어 주는 파이썬 스크립트를 제작했습니다. 로그 파일의 이름이 log.txt라면 이것을,

    python iptime_block_from_log.py < log.txt > rule.cfgCode language: CSS (css)

    처럼 입력합니다. 그러면 rule.cfg 파일이 만들어 집니다. 그럼 이 cfg 파일을 공유기에 올려 두면 됩니다.

    1번을 눌러 파일을 선택, 2를 눌러 공유기로 전송

    저는 이렇게 적용 후, 확실히 해당 지역에서 무단 접속이 사라진 것으로 보입니다. 효과가 나름 있는 거 같네요. 다만 공유기의 로그도 무한히 있는 것도 아닙니다. 최대 400개가 한계이니 더 다양한 IP를 차단할 수 있으려면 좀 더 로그나 IP를 쌓아두고 관리하면서 차단할 수 있어야 하겠네요. 일단 이번에는 여기까지 만들어 보고 더 나은 버전은 차후에 고민해 보겠습니다.

    덧, 이후 계속 “잘못된 VPN 계정 또는 암호로 접속을 시도 하였습니다” 메시지와 차단한 IP가 기록되네요. 이 방법으로는 접속 자체를 막지 못하는 것 같습니다.

  • M3U8 FFMPEG으로 덤프

    ffmpeg -i {URL} -c copy output.aac

    출처: Stackoverflow

  • wp-cli-secure-command

    워드프레스 보안 관련 사항을 지원하는 WP CLI 확장.

    https://github.com/igorhrcek/wp-cli-secure-command/