결혼 청첩장 페이지 개발기

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

벌써 6월이 코 앞이라 놀랐다. 그만큼 바빴던 일이 많았다. 민경 씨와 3년 반 넘는 장거리 연애 끝에 결혼하기로 작년 말 결정했다. 올해 들어서 이직도 하고, 책도 나오고, 이상한모임 앱도 만들고, 인사 겸 결혼 준비 겸 3월에도 짧게 한국을 다녀왔고, 5월은 한국에서 지내다가 결혼식을 한 후에 멜번으로 돌아왔다. 결혼 준비 과정도 글을 써서 남겨둘까 싶었지만 생각보다 직접 한 부분이 많지 않았다. 전문가들이 저렴한 비용으로 어디보다 빠르게 해결해주는 모습에서 한국에 왔음을 실감했다. 그 사이에 크게 필요 없었지만 부모님은 필요하실 것 같아 종이 청첩장도 주문했다.

청첩장을 주문하면서 예전에 규님이 올린 청첩장 개발기를 읽고 직접 만드는 것도 의미 있겠다 생각했던 게 기억이 났다. 그래서 청첩장 웹페이지는 직접 만드는 것이 좋겠다는 생각이 들어 민경 씨와 의논 후에 직접 만들게 되었다. 욕심에는 화려하게 만들어보고 싶었지만 생각보다 너무 바쁘게 지내다 결혼식 한 달 전에 가까스로 마무리 할 수 있었다.

  • 사진은 3월에 동생이 촬영해줬다. 짧은 시간이었는데 잘 찍어줘서 고마웠다.
  • 우리 이야기를 작성하는데 생각보다 시간이 많이 걸렸다. 그간 찍었던 사진을 민경 씨가 골라줘서 이야기 사이사이에 사진도 넣었다.
  • 영문 버전도 작성했다. 국문으로 먼저 작성하고 민경 씨가 번역했다.
  • html로 작성했고 페이스북 오픈그래프와 트위터 카드 메타를 추가했다.
  • 메시지 남기는 부분은 디비도 안쓰고 jQuery에 json으로 대충 저장하려고 그러다가 오히려 번잡스럽게 되어서 mysql 사용해서 제대로 자료도 저장하고 jQuery 부분도 angular 1.x 로 다 교체했다. 백엔드는 이미 서버에 있는 php를 사용했다.
  • 메시지는 간단하게 이름과 내용만 받았다.
  • 전송한 메시지는 pending 상태로 디비에 저장되며 확인이 된 메시지만 공개되도록 만들었다. 대신 제출했을
    때 안보이거나 확인 후 게시 된다는 표시가 보이면 기분이 언짢을 수(?) 있으므로 local storage에 저장해서 본인의 메시지가 바로 노출되는 것으로 보이게 작성했다. 문제가 있다면 내가 직접 거르면 되므로 따로 삭제/수정 기능은 넣지 않았다.
  • 원래는 다른 서체를 사용할 계획이었는데 본명조가 나와 바로 교체했다. Adobe CC를 구독하고 있어서 TypeKit으로 적용했다.
  • 미디어 쿼리로 여러 해상도에서도 디자이너가 고통받는 레이아웃이 나오지 않도록 조절하는 작업을 많이 했다. :lang으로 영문, 국문 스타일을 처리했는데 손이 많이 가긴 했지만 그럭저럭 괜찮은 수준까지 정리가 되었다.
  • 아이콘은 typicons을 사용했다.

이렇게 만든 청첩장은 wedding.haruair.com에서 확인할 수 있다. 많은 분들이 메시지를 남겨주셨고 제주까지 오셔서 축하해주신 분들도 계셔 너무 감사하다. (열심히 살겠습니다 😊)

제네릭 없는 PHP 인터페이스

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

PHP를 사용하면서 가장 아쉬운 부분은 인터페이스다. PHP는 인터페이스를 지원하고 있고 이 인터페이스를 활용한 타입 힌트, 의존성 주입 등 다양한 방식으로 적용 가능하다. 하지만 제네릭 타입이 존재하지 않아서 타입 컬렉션 같이 재사용하기 좋은 인터페이스를 만들 수 없다.

물론 이 문제를 해결하기 위한 패키지를 찾아보면 존재하긴 한다. 하지만 인터페이스가 아닌 클래스 구현에 의존하고 있어서 타입 검사가 로직 내부에 포함되어 있다. 간략한 구현을 보면 대략 다음과 같다. 1

<?php
class Collection
{
    protected $typeName = null;
    protected $collection = [];
    public function __construct($typeName)
    {
        $this->typeName = $typeName;
    }
    public function add($item) {
        if (! in_array($this->typeName, class_implements($item))) {
            throw new \TypeMismatchException();
        }
        $this->collection[] = $item;
    }
}

로직 내에 위치한 타입 검사는 런타임에서만 구동되어 실제로 코드가 실행되기 전까지는 문제가 있어도 찾기가 힘들다. 이런 방식의 구현에서 내부적으로는 인터페이스를 사용해서 입력을 검증하고 있지만 결국 메서드의 유무를 확인하는 덕타이핑과 큰 차이가 없어진다. 결과적으로 인터페이스가 반 쪽짜리 명세로 남아있게 된다. 주석을 잘 달아서 다소 모호한 함수 시그니처를 이해하도록 설득해야 한다.

조금 다른 부분이긴 하지만 PHP에서는 Type을 위한 타입이 존재하지 않는 대신 string으로 처리하기 때문에 위 방식조차도 깔끔하게 느껴지지 않는다. 즉, 타입::class로 반환되는 값도 타입이 아닌 문자열이며 메서드 시그니처에 적용할 수도 없다.

물론 매번 인터페이스와 클래스를 작성해서 사용하는 방법도 있겠다.

<?php
interface CollectionVehicleInterface implements CollectionInterface
{
  public function add(VehicleInterface $item);
}

class CollectionVehicle implements CollectionVehicleInterface
{
  public function add(VehicleInterface $item) {
    $this->collection[] = $item;
  }
}

의도대로 인터페이스를 통해 함수의 입력을 명확하게 정할 수 있게 되었다. 인터페이스는 명세를 명확하게 나타낸다. 다만 이런 방식으로는 모든 경우의 수에 대해 직접 작성해야 하는 수고스러움이 있다. 내부 로직은 동일한데 결국 함수 시그니처가 달라지므로 비슷한 코드를 반복해서 작성해야 한다. 이런 문제를 해결하기 위해 제네릭을 활용할 수 있다.

<?hh
namespace Haruair;

interface CollectionInterface<T>
{
  public function add(T $item) : void;
}

class Collection<T> implements CollectionInterface<T>
{
  protected array<T> $collection = [];
  public function add(T $item) : void
  {
    $this->collection[] = $item;
  }
}
?>

hack에서의 제네릭은 항상 함수 시그니처를 통해서만 사용 가능하며 명시적 선언으로 바로 사용할 수 없어 다른 언어의 제네릭과는 조금 다르다. 물론 hack은 다양한 컬랙션을 이미 제공하고 있으며 array에서도 타입을 적용할 수 있다.

요즘 제대로 된 타입 시스템이 존재하는 프로그래밍 언어를 사용하고 싶다는 생각을 정말 많이 한다. 최근 유지보수하는 프로젝트는 제대로 된 클래스 하나 없이 여러 단계에 걸친 다중 배열로 데이터를 처리하고 있다. 배열에서 사용하는 키는 전부 문자열로 관리되고 있어서 키가 존재하지 않거나 잘못된 연산을 수행하는지 판단하기 어렵다. 어느 하나 타입을 통해 자료를 확인하는 법이 없어 일일이 값을 열어보고 확인하고 있다. 물론 지금 프로젝트의 문제가 엉성한 타입에서 기인한다고 보기에는 다른 문제도 엄청 많다. 그래도 PHP에 타입이 존재하는 이상 조금 더 단단하게 사용할 수 있도록 만들었으면 이런 상황에 더 좋은 대안을 제시할 수 있지 않았을까 생각이 든다.

PHP RFC를 보면 기대되는 변경이 꽤 많이 있는데 빈번히 통과되지 않는 기능이 많아 참 아쉽다. 이 제네릭의 경우도 그 중 하나다. 기왕 인터페이스도 있는데 이런 구현도 함께 있으면 좋지 않을까. 정적 타입 언어도 아닌데 너무 많은 것을 바라는건가 싶으면서도 인터페이스도 만들었으면서 왜 이건 안만들어주나 생각도 동시에 든다. 이렇게 딱히 대안 없는 불평글은 별로 쓰고싶지 않다 😕


  1. 이 코드는 실무에서 사용하기 어렵다. 가령 class_implements는 문자열로 전달한 경우에는 해당 문자열을 사용해 클래스 또는 인터페이스를 찾으므로 객체임을 확인하는 코드가 필요하다. 

왜 클래스죠?

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

최근 아키텍처에 관한 책을 읽고 있는데 레퍼런스로 나온 글 중 하나로 Hadi Hariri의 글 Refactoring to Functional–Why Class?을 번역했다. 이 글은 함수형으로 리펙토링하기라는 코틀린 연재 중 일부라서 그다지 공정한 느낌으로 쓰여진 글은 아니지만 객체지향이라는 패러다임에서 논쟁점이 되는 여러 부분을 잘 보여주고 있어 옮겨봤다.


함수형으로 리팩토링하기 – 왜 클래스죠?

대학에서

교수: 우린 실제 세계에서 객체로 둘러쌓여 있습니다. 이 객체는 자동차, 집, 기타 등등이 될 수 있죠. 그런 이유에서 객체 지향 프로그래밍에서 클래스를 통해 실제 세계에 존재하는 객체를 연결하는 방식이 매우 쉬운 이유입니다.

2주 후

제이크: 저 이 객체와 문제가 좀 있는데요. 도와주시겠어요?
교수: 물론이죠. 객체를 만드는데 도움이 되는 여러 일반적인 방법이 있는데 요약하자면 명사를 찾아요. 그리고 동사를 찾으면 클래스에서 사용할 수 있는 메소드가 될 수 있어요. 말하는 그대로죠.
제이크: 어 말씀한 내용이 합당하네요. 감사합니다.

신입 제이크

: 제이크 씨, 당신이 작성한 클래스를 확인했습니다. 좀 크기가 큰 것 같은데요.
제이크: 죄송합니다. 어떤 부분이 문제죠?
: 음… 너무 많은 책임을 갖는 게 문제에요. 너무 많은 일을 합니다.
제이크: 그리고요?
: 잘 생각해보세요. 하나에 너무 많은 책임이 있으면 이 부분 하나가 시스템의 많은 부분과 연결되어 있다는 뜻이에요. 즉 이 클래스를 변경할 가능성도 상당히 높다는 뜻이고 그건 무언가를 고장내게 될 가능성 또한 높다는 의미죠. 거기다 단일 클래스를 1000줄이 넘도록 작성하면 물론 30줄 짜리 코드에 비해 이해하기 어려울 것이고요.
제이크: 맞는 말이네요.
: 이 코드를 작은 클래스로 나누세요. 각각의 클래스는 한 가지 일만 하고 그 클래스 홀로 쓰여야 합니다.

1년 후

메리: 제이크 씨, 방금 당신이 작성한 클래스를 확인했는데요. 그다지 행동(behavior)이 많이 들어있지 않네요.
제이크: 네, 동작이 Customer 클래스에 속하는지 Accounts 클래스에 포함해야 하는지 확신이 없어서 CustomerService라는 클래스를 별도로 만들어 거기에 넣었습니다.
메리: 네, 적당한 방법이네요. 하지만 Customer 클래스를 더 이상 클래스라고 보기 어려워졌어요. DTO에 더 가까워요.
제이크: DTO요?
메리: 네, 데이터 전달 객체(Data Transfer Object)요. 클래스와 비슷하긴 하지만 행동이 없는 경우에요.
제이크: 음, 그럼 구조체나 레코드에 가깝다는 말씀이시죠?
메리: 네, 그런 느낌이에요. 클래스를 만들 때는 행동이 있어야 해요. 그러지 않고서는 클래스라고 하기 어려워요. DTO죠.
제이크: 알겠습니다.

2년 후

메튜: 제이크 씨, 이 클래스를 봤는데 특정 구현과 결합(coupled)이 상당히 강하군요.
제이크: 네?
메튜: 음, 지금 RepositoryController 내에서 생성하고 있어요. 이 부분은 어떻게 테스트하시겠어요.
제이크: 음… 시험용 데이터베이스를 사용하면 되지 않을까요?
메튜: 아뇨. 가장 먼저 해야 하는 부분은 프로그램을 클래스가 아닌 인터페이스를 사용하도록 하는 겁니다. 이 접근 방식이 특정 구현에 매여 있지 않은 코드를 장성하는 방법이에요. 그런 후에 의존성 주입을 사용해서 특정 구현을 전달해 사용하도록 하는겁니다. 그러면 구현을 언제든지 필요할 때 변경할 수 있게 되는 거죠.
제이크: 그렇군요.
메튜: 실무에서는 IoC 컨테이너를 사용해서 다른 클래스의 인스턴스를 연결하는 것이 가능할겁니다.

3년 후

프랜시스: 제이크 씨, 이 클래스에 너무 많은 의존성을 집어넣고 있군요.
제이크: 네, 그래도 IoC 컨테이너가 다 처리할겁니다.
프랜시스: 네, 저도 알고 있습니다. 하지만 가능하다고 해서 옳은 방법이라고 말하기는 어렵네요. 이 클래스는 여러 종류의 구현체를 사용할 수 있다고 하더라도 여전히 너무 많은 다른 클래스에 의존하고 있어요. 하나에서 최대 3개로 유지하도록 해요.
제이크: 네, 알겠습니다. 감사합니다.

4년 후

안나: 제이크 씨, 이 클래스 이름은 왜 Utils인가요?
제이크: 음. 그 코드는 정말 어디에 놔야 할 지 알 수 없어서 그렇게 이름 붙였어요.
안나: 그래요. 이미 그런 코드를 위한 클래스가 있어요. RandomStuff라는 이름이에요.

맥주 마시며

제이크: 피터, 내가 생각해봤는데 말이지. 학교에서 배울 땐 객체로 생각하고 명사를 분석하라는 등 기법을 얘기했는데 말야. 그러고 나서는 이름을 잘 붙였는지, 작게 작성했는지, 단일 책임으로 작성했는지, 너무 많은 의존성을 주입하고 있는 것은 아닌지 생각해야 한단 말이야. 이제 와서는 동시성에 좋지 않다고 상태를 갖지 않는 코드를 작성해야 한다고 말하지. 처음부터 궁금했는데 이럴거면 도대체 왜 클래스를 사용하는 걸까?
피터: 헛소리 하지 마 제이크. 만약 클래스가 없다면 어디에 함수를 선언할 수 있겠어? 맥주나 한 잔 더 마실래?

다음 시간에 계속.