데이터베이스에서 객체를 지연 로딩(lazy loading) 하기

😢 이 페이지는 다음 주소로 변경될 예정입니다.

최근 프로젝트에서 PDO를 사용해 작업하다보니 아무래도 ORM에 비해 아쉬운 점이 많아 ORM의 구현을 살펴보는 일이 잦아졌다. Giorgio Sironi의 글 Lazy loading of objects from database을 번역했다. 좀 오래된 글이긴 하지만 지연 로딩을 위해 프록시 패턴을 사용하는 방식을 설명하고 있다.

이 번역은 원 포스트의 명시와 같이 CC BY-NC-SA 3.0 US에 따른다.


데이터베이스에서 객체를 지연 로딩(lazy loading) 하기

지연 로딩(lazy loading)은 무엇인가? 객체/관계 맵핑에서는 전체 객체의 연결 관계를 메모리상에서 나타내는 방식이 관행이다. 모든 객체를 실제로 만드는 대신 환영을 만드는 방법을 이 글에서 살펴본다.

예시

PHP 애플리케이션에서 전형적인 UserGroup 객체가 있다고 생각해보자. 단지 PHP 코드 예제를 사용했을 뿐이지 Java/Hibernate 예제처럼 관계형 데이터베이스를 사용하는 언어라면 이 글의 내용은 유효할 것이다.

UserGroup은 전형적인 다대다 관계다. 사용자는 여러 그룹에 포함될 수 있고 그룹은 여러 사용자를 구성원으로 할 수 있다. 즉 데이터베이스에서 불러온 객체는 다음처럼 탐색할 수 있다.

$user = find(42); // id가 42인 사용자를 찾는다
echo $user->groups[3]->users[2]->groups[4]->name;

객체 그래프를 무한으로 탐색할 수 있는 경우는 좋은 관례가 아니다. 하지만 종종 다대다 관계에서는 이런 탐색이 필요한 경우가 있으며 단순한 API인데도 자원을 과도하게 사용하게 되는 접근법 중 하나다. 왜 자원을 과도하게 사용하는지 뒤에서 설명한다.

가장 요점인 문제는 모든 객체 그래프를 불러올 수 없다는 점인데 데이터베이스의 크기에 따라서 서버의 메모리보다 커질 수도 있고 객체로 전환하는 데 시간이 한참 걸릴 수도 있기 때문이다. 그렇다고 관계 일부만 불러올 수도 없는데 그룹과 사용자를 원하는 만큼 탐색하려면 모든 그래프가 필요하기 때문이다. 일부만 불러온 상황에서 그래프의 끝 단까지 간다면 객체가 있어야 할 위치에 null 값/null 포인터를 반환하게 되는 것은 문제가 된다.

해결책: 지연 로딩

프록시 패턴을 이 상황에 적용할 수 있다.

일반적으로 프록시는 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스이다. 프록시는 어떠한 것(이를테면 네트워크 연결, 메모리 안의 커다란 객체, 파일, 또 복제할 수 없거나 수요가 많은 리소스)과도 인터페이스의 임무를 수행할 수 있다.

첫 탐색에서는 첫 그룹의 하위 클래스인 프록시를 반환한다. 데이터 맵퍼는 추가적인 명령 없이도 해당 타입의 객체 그래프를 제공하게 된다.

var_dump($user); // User
var_dump($user->groups[3]); // Group_SomeOrmToolNameProxy
var_dump($user->groups[3] instanceof Group); // true

앞서 이야기한 것처럼 ORM은 프록시 클래스를 사용해서 원래의 클래스를 대체하는 방법으로 지연 로딩을 제공한다. 이 클래스를 위한 코드는 즉석에서 생성하며 대략 다음과 같은 형태가 된다.

class Group_SomeOrmToolNameProxy
{
    public function __construct(DataMapper $mapper, $identifier)
    {
        // 참조하는 필드를 인자 형태로 저장
    }

    private function _load()
    {
        $this->loader->load($this, $id);
    }

    public function sendMessageToAllUsers($text)
    {
        $this->_load();
        parent::sendMessageToAllUsers($text);
    }
}

새 클래스는 원래의 메소드를 대신해 호출하긴 하지만 호출하기 전에 _load() 메소드를 호출해서 객체를 사용할 수 있는 상태로 바꾼다. _load()를 호출하기 전이나 프록시 메소드를 호출하기 전에는 이 도메인 객체는 식별자 필드(id)만 내부 데이터 구조에 저장하고 있다.

이 코드를 사용할 때는 기존 Group과 같은 인터페이스를 제공하기 때문에 사용자 입장에서는 서버 자원에서 자유로운 Group 클래스를 사용한다는 점을 눈치채기도 어렵다.

무슨 뜻일까?

첫 단계의 객체는 완전히 불러오지만 두 번째 단계는 해당 객체를 불러올 수 있는 정보만 포함하는 플레이스홀더만 존재한다. 실제로 접근했을 때만 해당 필드를 데이터베이스에서 가져와 처리하게 된다.

$user = $em->find(42); // user 테이블에서 호출함
echo $user->groups[3]->name; // groups와 user_groups 테이블에서 호출함

이 패턴을 원하는 만큼 더 복잡한 환경에서도 적용할 수 있다.

  • join() 명령을 호출 객체에 정의하거나 ‘join’ 선택지를 데이터맵퍼의 메소드로 제공해서 최초 로딩에서 어느 깊이까지 객체를 불러올 것인가 지정할 수 있다. 최초에 사용자의 두 번째 단계 그래프까지 불러올 때 쿼리 한 번으로 불러오는 것이다. 물론 여전히 3번째 단계부터는 ($user->groups[3]->users[2]->role) $user를 다시 구성하지 않는 이상은 데이터베이스에 추가적인 요청을 보내 성능에 영향을 줄 것이다.
  • 지연 로딩을 켜거나 끌 수 있다. 또는 실행 과정을 기록해서 성능에 영향을 주는 지점을 찾을 수 있다.

Java의 Hibernate는 객체 프로퍼티와 관계의 지연 로딩 기능을 이 접근 방식으로 제공한다. Doctrine 1.x는 더 단순한 방식을 사용하는데 액티브 레코드를 사용하고 있고 Doctrine_Record라는 기반 클래스 상에서 모델을 구현하고 있기 때문이다.

오늘 Doctrine 2의 ORM\Proxy네임스페이스에 코드를 기여했다. 이 컴포넌트는 프록시 클래스와 객체를 기존 클래스의 메타데이터를 기반으로 생성해준다. 지연 로딩을 기존 코드 변경 없이도 바로 사용할 수 있을 것이다.

테스트 주도 개발 : Test-Driven Development by Example

😢 이 페이지는 다음 주소로 변경될 예정입니다.

예전에도 테스트주도개발에 관한 글을 인터넷에서도 한참 찾아보고 읽었었다. 글을 읽고서 TDD를 행동으로 옮겨보면 대부분 글이 구호만 잔뜩 나열했지 무슨 일을 어떻게 해야 하는지 과정을 제대로 설명하는 경우가 거의 없었다. 나도 중요하다고는 늘 이야기하지만 현장에서 제대로 사용하지 않고 있었다. 막히는 부분을 어떻게 풀어야 하는지, 어떤 방법으로 고민해야 하는지 생각만 많아지고 해결하질 못했었다. 그래서 지난 번 사온 책 중 테스트 주도 개발 (켄트 백, 인사이트, 김창준 강규영 옮김)을 가장 먼저 읽어보게 되었다.

이 책에서는 예제로 먼저 시작해 TDD가 어떤 생각의 흐름에 따라서 진행되는지 보여준다. 그 뒤로는 TDD를 하게 될 때 접할 수 있는 의문점과 그 해결책을 나열한다. 대략적으로만 알았었기 때문인지 새로웠던 부분, 기억하고 싶은 내용이 많았다.

  • TDD는 프로그램을 코드 단위로 잘게 쪼개서 실행해볼 수 있는 좋은 방법이다. 작은 단위로 내 의도대로 실행되는지 입력과 결과를 관찰한다. 단, 테스트를 먼저 작성하는 것으로 코드가 동작하지 않는 상태임을 명확하게 확인한다.
  • 빌드가 되지 않는 상태에서 빨간 불이 들어오도록 최소 코드를 작성하는 것, 이 빨간 불을 초록 불로 최대한 빠르게 바꾸기 위해 매직 넘버도 서슴치 않고 사용하는 것, 초록 불이 들어온 후에 작성한 테스트를 통해 리팩토링 하는 과정을 따른다.
  • 매직 넘버를 반환하는 메소드는 삼각측량으로 고친다. 즉 동일한 메소드를 다른 입력과 결과로 테스트를 작성했을 때 두 케이스를 모두 만족하기 위한 리팩토링을 수행한다.
  • 결과를 하드코딩 하는 것은 죄악으로 일반적으로는 죄악으로 여겨지는 일이지만 필요할 때 사용할 수 있어야 한다. 이 단계를 생각하지 않고 먼저 큰 코드를 작성해버리면 당장은 테스트의 단계를 줄일 수 있겠다. 문제는 그렇게 작성한 코드가 생각대로 동작하지 않았을 때 테스트 작성도 어려워지고 고민해야 할 단위도 커진다는 점이다. 이럴 때 다시 매직 넘버를 반환하는 수준으로 돌아올 수 있어야 하는데 생각의 단위를 가볍게 돌리는 일은 쉽지 않다. 테스트를 작성하면서 코드와 멀어진다는 생각을 하게 되는 지점이었는데 이 단순한 답이 큰 도움되었다.
  • 코딩이 안될 땐 쉬어야 한다는 이야기는 참 좋은 미덕이다.

테스트를 언제 작성하는 것이 좋을까? 테스트 대상이 되는 코드를 작성하기 직전에 작성하는 것이 좋다. (p. 210)

나중에 작성하면 항상 통과하는 무의미한 테스트를 작성하게 될 수도 있다.

시스템을 개발할 때 무슨 일부터 하는가? 완료된 시스템이 어떨 거라고 알려주는 이야기부터 작성한다. 특정 기능을 개발할 때 무슨 일부터 하는가? 기능이 완료되면 통과할 수 있는 테스트부터 작성한다. 테스트를 개발할 때 무슨 일부터 하는가? 완료될 때 통과해야 할 단언(assert)부터 작성한다. (p. 211)

TDD를 가장 간단하고 와닿게 풀어낸 문장이다.

상향식, 하향식 둘 다 TDD의 프로세스를 효과적으로 설명해 줄 수 없다. 첫째로 이와 같은 수직적 메타보는 프로그램이 시간에 따라 어떻게 변해 가는지에 대한 단순화된 시각일 뿐이다. 이보다 성장(growth)이란 단어를 보자. ‘성장’은 일종의 자기유사성을 가진 피드백 고리를 암시하는데, 이 피드백 고리에서는 환경이 프로그램에 영향을 주고 프로그램이 다시 환경에 영향을 준다. 둘째로, 만약 메타포가 어떤 방향성을 가질 필요가 있다면 (상향 혹은 하향보다는) ‘아는 것에서 모르는 것으로(known-to-unknown)’라는 방향이 유용할 것이다. ‘아는 것에서 모르는 것으로’는 우리가 어느 정도의 지식과 경험을 가지고 개발을 시작한다는 점, 개발 하는 중에 새로운 것을 배우게 될 것임을 예상한다는 점을 암시한다. 이 두 가지를 합쳐보자. 우리는 아는 것에서 모르는 것으로 성장하는 프로그램을 갖게 된다. (p. 218-219)

TDD가 어떤 순환성을 갖는지 설명하는데 이 부분 탓에 TDD를 더 어렵게 고민하게 만든다는 생각이 들었다.

첫 걸음으로 현실적인 테스트를 하나 작성한다면 상당히 많은 문제를 한번에 해결해야 하는 상황이 될 것이다. … 정말 발견하기 쉬운 입력과 출력을 사용하면 이 시간을 짧게 줄일 수 있다.(p. 219)

한번에 모든 것을 작성하려는 습관을 버려야 한다.

화이트 박스 테스트를 바라는 것은 테스팅 문제가 아니라 설계 문제다. 코드가 제대로 작동하는지를 판단하기 위한 용도로 변수를 사용하길 원한다면 언제나 설계를 향상할 수 있는 기회가 있다. 하지만 두려움 때문에 포기하고 그냥 변수를 사용하기로 결정해 버리면 이 기회를 잃게 된다. 그렇게 말하긴 했지만 정말 설계 아이디어가 떠오르지 않으면 어쩌겠는가. 그냥 변수를 검사하게 만들고 눈물을 닦은 후, 머리가 좀더 잘 돌아갈 때 다시 시도해보기 위해 적어놓고서 다음 작업을 진행할 것이다. (p. 255)

테스트를 작성하면서 테스트가 코드 내부 구현을 너무 많이 알고 있을 때 코드에 의존적인 테스트를 작성하기 마련이다. 이 책을 읽으면서 항상 느낀 점인데 테스트를 통과하면 일단 두고 넘어가도 된다는 점이다. 리팩토링에서 다시 만나면 코드를 새로 작성하거나 테스트를 새로 작성하면 된다고 계속 이야기한다.

‘관측상의 동치성’이 성립되려면 충분한 테스트를 가지고 있어야 한다. 여기에서 충분한 테스트란, 현재 가지고 있는 테스트들에 기반한 리팩토링이 추측 가능한 모든 테스트에 기반한 리팩토링과 동일한 것으로 여겨질 수 있는 상태를 말한다. (p. 292)

빨간 불에서 초록 불로 바꾸는 과정에서 관측 상의 동치성이 나타난다. 초록 불 상태에서 코드를 바꿔도 초록 불이라면 외부에서 보기에는 동일한 결과를 반환하기 때문에 문제가 없다는 것이다. 초록 불 상태로 어떤 방식으로든 빠르게 만들어내야 한다는 설명이 이 맥락에 닿아 있다.

TDD 주기(테스트/컴파일/실행/리팩토링)를 수행하기가 힘든 언어나 환경에서 작업하게 되면 단계가 커지는 경향이 있다. 각 테스트가 더 많은 부분을 포함하게 만든다. 중간 단계를 덜 거치고 리팩토링을 한다. 이렇게 하면 개발 속도가 더 빨라질까, 느려질까? (p. 315)

단기적으로는 빨라지는 것 같지만 결국엔 테스트와 거리가 먼 코드를 작성하게 된다.

패턴 복사하기 자체는 훌륭한 프로그래밍이 아니다. 이 사실은 내가 패턴에 대해 이야기 할 때면 늘 강조한다. 패턴은 언제나 반숙 상태며, 자신의 프로젝트 오븐 속에서 적응시켜야 한다. 하지만 이렇게 하기 위한 좋은 방법 중 하나는 일단 무턱대고 패턴을 복사하고 나서, 리팩토링과 테스트 우선을 섞어 사용해서 그 적응과정을 수행하는 것이다. 패턴 복사를 할 때 이렇게 하면 해당 패턴에 대해서만 집중할 수 있게 된다(한 번에 하나씩). (p. 340)

TDD와 리팩토링 과정 속에서 패턴은 자연스럽게 발견되어야 한다는 이야기와 유사하다. 패턴은 반숙이라는 표현이 와닿았다.


실천 없는 구호는 의미 없다. 이 계기로 앞으로는 TDD 하도록 노력해야겠다.

2017년 반절 회고와 반절 계획

😢 이 페이지는 다음 주소로 변경될 예정입니다.

매년 계획을 정리해서 올렸는데 올해는 반절 지나갈 동안 생각만 하고 분주하게 지냈다. 어떻게든 틈을 내서 글을 쓰면 생각도 차분해지고 일정도 정리되기 마련인데 모든 일이 다 끝나고 나서야 글을 쓸 수 있겠다는 생각이 들었다.

크고 작은 일이 많았다.

  • 이직을 했다. 그래도 큰 일이라고 잊지 않고 적어둔 덕분에 그 당시 생각을 다시 해볼 수 있었다. 정 반대 성격의 회사에서 근무하는 경험도 새롭다. 나는 어느 쪽에 더 맞는 성향일까.
  • 이직한 회사에서 예정되어 있던 프로젝트가 예산 문제로 좌초된 탓에 다른 프로젝트의 유지보수(business as usual)를 지원하고 있다. 프로세스가 상당히 세분화되어 있는 이유로 유휴시간이 엄청 많다. 그 시간을 사내에서 쓸 수 있는 부트스트랩 프로젝트를 만드는데 많이 사용하고 있지만 그래도 시간이 많이 남는다. 이렇게 버퍼가 큰 환경을 겪어보지 못해서 새롭다.
  • 번역한 책이 나왔다. 한국에 가서 책방에 놓여진 책을 보니 뿌듯하기도 하고 너무 부족한 것 같아 죄송하기도 하고 그렇다. 번역하는 분들 존경스럽다. 기회가 생긴다면 또 하고 싶지만 좀 더 계획을 제대로 세워서 할 수 있으면 좋겠다.
  • 이상한모임 앱이 나왔다. 시아님과 함께 백엔드를 만들었는데 코드도 인프라도 내가 너무 날림으로 만든 탓에 다들 고생했다. 그래도 시아님이 봐주고 테스트도 열심히 작성해주셔서 다음 사이클 걱정을 좀 덜었다. 대대적으로 뜯어 고쳐야 할 것 같다.
  • 결혼을 위해 한국을 두 번 다녀왔다. 3월 세 번째 주 주말에 먼저 다녀왔는데 그 짧은 일정에 맞춰 만나준 분들도 너무 감사하다.
  • 5월 한국 방문은 2주 조금 넘게 있다가 결혼식을 치뤘다. 먼 길까지 축하하러 와준 분들 너무 감사하고 페이스북 중계로 함께 시간을 나눠준 분들도 계셔서 너무 감사했다. 그러고 멜번으로 넘어와 민경씨와 함께 시간을 보냈다.
  • 블로그는 거의 방치했다. 신경쓸 부분이 많아지니 슬럼프 비슷한게 와버려서 퇴근 후에는 거의 개발을 하지 않았다. 대신 게임을 한다든지 넷플릭스 보면서 위 굵직한 일 사이를 메웠다. 글도 많이 안봐서 번역하는 일도 거의 없었다. 입력이 없으니 출력이 없어지는 것은 당연했다.
  • 매년 글쓰기에 대한 미련을 버리지 못하고 있다. 그런데 한번 쓰는 흐름을 잃으니 다시 찾기 너무 어렵다. 잘 쓰는 것은 아니지만 그래도 꾸준히 할 힘은 계속 있었는데. 예전 글을 읽고는 무슨 글을 이렇게 쓰냐는 둥 덧글을 잔뜩 받고 있어서 별로 쓰고 싶은 생각도 점점 줄어들고 있다. 글쓰기는 누구도 강요하지 않고 안써도 그만인데도 계속 욕심이 생긴다.
  • 영어 공부는 손도 대지 못했다. 매일 비슷한 문장만 반복하고 있는 나를 보고 있노라면 답답하게 느껴진다.

올해를 시작하며 내실을 좀 더 챙겨가는 해가 되어야겠다고 생각했었다. 전반기는 바쁠 예정이었기 때문에 계획을 거의 세우지 않았는데 지금 생각해보면 조금 빠듯하더라도 더 세세하게 계획하고 일정을 쌓았어야 하지 않았나 싶다.

남은 반년은 다음과 같은 계획을 세웠다.

  • 책을 많이 읽는다. 인터넷에도 볼 자료가 쌓여 있지만 앉아서 차분하게 읽는 일이 좀처럼 쉽지 않아서 공부하는 감각도 기를 겸, 책을 더 읽을 계획이다. 연초부터 아샬님개발자, 한 달에 책 한 권 읽기를 진행하고 있는데 여기에 소개된 책을 한국에 다녀오면서 구입해왔다. 구입한 책과 구입하고 읽지 못했던 책을 차분하게 읽으려고 한다.
  • 자격증을 취득한다. 저스틴님과 함께 MS쪽 자격증 시험을 준비하고 응시할 예정이다. 아직 “봅시다!”만 한 상태라서 진행 상황은 차후 작성하려고 한다.
  • 영어 공부를 좀 더 체계적으로 한다. 어휘와 문장력을 늘리는데 중점을 두고 공부하려고 한다. 영어책 읽는 시간도 더 늘리고 싶다.
  • 이상한모임 프로젝트도 월말에 몰아서 하지 말고 평소에 시간을 분배해서 작업한다. 몰아서 하는 습관 탓에 코드 질이 많이 떨어지는 것 같다.
  • 자격증 시험 이후에는 C# 공부를 할 생각이다. 지난 회사에서는 어떻게든 C#을 사용하는 환경이었기 때문에 실무에서 조금이나마 사용했는데 새로온 곳은 전혀 다른 분위기라서 시간을 더 만들어야 할 것 같다. 토이프로젝트도 하고 공개된 코드들도 읽는 시간을 갖고 싶다.
  • 부담없이 글을 쓸 수 있도록 짧은 글이라도 매일 쓴다. 꾸준히 쓰는 것이 중요하다 적어 놓고도 참 쉽지 않다.

남은 올 한 해 잘 보내서 연말 회고에 즐겁게 목표를 달성했다고 적고 싶다.