One to Many 조인에서 Many 쪽 최종 레코드만 조회하기

today 2018-11-20 face Posted by appkr turned_in Work & Play forum 0

고객 목록에 고객별 최근 주문 1건에 대한 요약 정보를 보여주세요.

라는 요구사항이 있습니다.

“고객” 객체와 “주문” 객체간의 관계는 다음과 같습니다.

+------------+           0..* +------------+
|  Customer  | <>-----------> |   Order    |
+------------+                +------------+

애플리케이션 레이어에서 구현한다면, ORM을 통해 구한 Collection<Customer>을 순회하면서, Customer객체의 멤버 필드인 Collection<Order>를 대상으로 최근 Order 객체만 필터링했거나, 적절한 순서로 정렬하여 뽑아 쓰기 쉽도록 했을겁니다. 요런 느낌으로요(검증 안된 Pseudo Code 입니다).

// Service Layer
public function listCustomers()
{
    return $customers->map(function (Customer $customer) {
        $sorted = $customer->orders->sortByDesc('id')->values();
        $customer->setRelation('orders', $sorted);
        return $customer;
    });
}

// Controller/View 등
foreach ($customers as $customer) {
    $lastOrder = $customer->orders->first();
    $lastOrder->order_number; // 최근 주문 번호   
}

문제점들이 눈에 띕니다.

  • 시간복잡도는 O(m x n)입니다.
  • CPU와 메모리를 혹사시킵니다.
  • Customer.orders: Collection<Order>를 미리 로드하지 않았다면, N + 1 문제가 발생합니다.
  • 이 외에도 제가 보지 못한 문제점들이 더 있을 겁니다…

읽기 전용 쿼리이므로 ORM을 쓰지 않아도 됩니다. 싸고, 빠르고, 안전하게 SQL만으로 뽑아내는 방법을 찾아봤습니다.

PHP 객체의 복제 특성

today 2018-11-04 face Posted by appkr turned_in Learn & Think forum 0

PHP 객체를 다른 변수에 할당(대입)하면, 객체 자체가 메모리 복제되어 새로운 변수에 할당되는 것이 아니라, 원본 객체가 담긴 메모리 번지만 참조됩니다(Like Pointer in C language).

  • $foo$foo2Foo 클래스의 인스턴스가 담긴, 같은 메모리 번지를 가리킵니다.
  • $foo3$foo 인스턴스로 부터 복제했으므로, 복제 시점의 $foo의 상태를 그대로 가져오지만, 서로 다른 객체입니다.
  • $foo4Foo 클래스의 새로운 인스턴스이므로, 당연히 $foo, $foo2, $foo3와 다른 객체입니다.

SQL Injection 방어

today 2018-10-20 face Posted by appkr turned_in Learn & Think forum 0

Exploit of a mom 그림출처: Exploits of a Mom

Cut#1 (학교) 여기 학교인데요. 전화 드린 이유는, 학교에 컴퓨터 문제가 좀 있어서요.
Cut#2 (엄마) 우리 애가 사고쳤나요?
     (학교) 예... 일종의...
Cut#3 (학교) 그런데, 정말로 아들 이름을 "Robert'); DROP TABLE students;--" 로 지으신게 맞나요?
      (엄마) 예 맞아요. 그래서 집에서는 우리 애를 "리틀 바비 테이블"이라고 불러요.
Cut#4 (학교) ;;; 기뻐하실 지 모르겠지만, 학생 테이블의 레코드 전체를 잃어 버렸어요.
      (엄마) 적어도 이번 사건 덕분에 DB에 입력할 값을 잘 필터링 해야 한다는 사실 정도는 배우셨겠군요~
• • •

프레임웍을 쓰면 쉽게 안전성을 확보할 수 있는데요. 그럼에도 불구하고, 아래 코드의 queryStoresByParams() 함수처럼 Raw 쿼리를 써야 할 때가 있죠. 이 때 SQL Injection을 당하지 않도록 주의해야 합니다.

<?php // 회사 코드에서 일부 발췌

class StoreRetriever
{
    // 라라벨 프레임웍이 제공하는 엘로퀀트 ORM과 쿼리 빌더를 이용하는 경우
    // PDO와 Prepared Statement를 이용하므로 SQL Injection은 자동 방어됨
    public function retrieveStoresByParams(StoreSearchParamDto $dto, array $eagerLoads = [])
    {
        $builder = !empty($eagerLoads)
            ? Store::query()->with($eagerLoads)->select('stores.*')
            : Store::query()->select('stores.*');
        $this->applySearchParams($builder, $dto);
        $this->applyOrderBy($builder, $dto);

        return $builder->paginate($dto->getSize(), ['*'], 'page', $dto->getPage());
    }

    // Raw 쿼리를 쓸 때 사용자로부터 받은 문자열을 직접 쿼리에 끼워 넣으면 SQL Injection에 무방비 상태가 됨
    // e.g. $query[] = "and stores.created_at >= {$from}";
    //
    // 여기서 Raw 쿼리를 왜 썼는가? "성능". 소위 말하는 "Query Model"
    // PHP7 with Xdebug on Docker, 테이블 조인 5개 & 12,000 레코드 조회시 API 응답 시간
    // 	 - retrieveStoresByParams(): around N sec 
    //   - queryStoresByParams(): under N/10 sec
    public function queryStoresByParams(StoreSearchParamDto $dto, array $columns = ['stores.*'])
    {
        $columnString = implode(',', $columns);
        $query[] = "select {$columnString} from stores";
        $bindings = [];

        $query[] = "where 1 = 1";

        $from = $dto->getFrom();
        if ($from !== null) {
            $query[] = "and stores.created_at >= :from";
            $bindings['from'] = $from;
        }

        // ...

        return \DB::select(implode(' ', $query), $bindings);
    }

    private function applySearchParams(Builder $builder, StoreSearchParamDto $dto)
    {
        $from = $dto->getFrom();
        if ($from !== null) {
            $builder->where('stores.created_at', '>=', $from);
        }
    
        // ...
    }
    
    private function applyOrderBy(Builder $builder, StoreSearchParamDto $dto)
    {
        foreach ($dto->getOrderBy() as $order) {
            $builder->orderBy($order['sortKey'], $order['sortDirection']);
        }
    }
}

Linux "top" cheatsheet

today 2018-10-13 face Posted by appkr turned_in Cheatsheet forum 0

보통 개발용 컴퓨터에서는 htop처럼 좀 더 편리한 프로세스 모니터 도구를 사용하지만, 운영 서버에는 서비스에 불필요한 바이너리는 설치하지 않는게 좋죠. 해서, 특히 서비스 장애와 같이 급박한 상황에 운영 서버에 SSH in 해서 top을 이용하려면 바보가 된 느낌을 종종 받습니다.

이 포스트에서는 바닐라 리눅스 환경에서 사용 빈도가 높은 top 사용법만 정리해봤습니다. 우분투 16.04를 사용했는데요, CentOS도 같을 거라 생각합니다 CentOS는 조금 다릅니다, 댓글로 남깁니다.

$ cat /etc/issue
# Ubuntu 16.04.3 LTS \n \l

$ top

전체 요약

  • Help
    • h: 도움말
  • Display
    • Z: 칼라 설정
    • t: CPU 통계 뷰 토글
    • m: 메모리 통계 뷰 토글
    • 0: Zero value(0) 뷰 토글
    • b: 하이라이트 토글
    • x: 활성 필드 표시 토글
    • y: 활성 프로세스 토글
    • c: 커맨드 상세 표시 토글
    • V: 프로세스간 부모/자식 관계 표시
  • Action
    • W: 설정 저장
    • k: 프로세스 중단하기
  • Navigation & Sorting (default: desc)
    • <, >: 활성 필드 네비게이션
    • N: PID 필드를 활성으로 변경
    • M: %MEM 필드를 활성으로 변경
    • P: %CPU 필드를 활성으로 변경
    • T: TIME+ 필드를 활성으로 변경
  • Search & Filter
    • L: 검색
    • o: 필터, e.g. COMMAND=apache, !COMMAND=apache, %MEM>0.1, ..
    • u: 사용자 이름으로 필터
    • ctrl + O: 현재 적용된 필터
    • =: 필터 해제
keyboard_arrow_up