Legacy PHP 서비스 Fargate로 이사하기

undefcat·2023년 1월 27일
2
post-thumbnail

PHP에 대하여

현재 저희 회사에는 영카트 기반의 서비스가 하나 있습니다. 제가 알기로는 지금으로부터 약 6년 전에 외주를 통해 만들어진 서비스입니다.

지금 회사에 합류하게 된 2022년 3월 말 기준으로, 서비스는 계속 이뤄지고 있었지만 유지보수는 거의 이뤄지지 않았다고 봐도 무방했습니다.

PHP라는 언어가 이제는 국내에서 하락세를 걷고 있어서 개발자를 구하는게 쉽지도 않고, 아시는 분들은 아시겠지만 영카트의 경우 상당히 오래전에 개발된 프레임워크이기 때문에 모던하지 않은 구조를 갖고 있습니다. 과거 PHP의 전형적인 형태로 개발됐죠. 즉, 유지보수하기가 좀 까다로운 코드 퀄리티를 갖고 있습니다.

어떻기에 유지보수하기가 힘든가를 얘기해보기 전에, PHP의 특징에 대해 간략히 짚어보겠습니다.

PHP의 특징

1. 인터프리터 언어입니다.

  • js나 python과 비슷합니다.
<?php

// hello_world.php
echo 'Hello, World!'
$ php hello_world.php
Hello, World!

2. 클래스 기반의 OOP언어의 특징을 갖고 있습니다.

  • Interface와 Class를 정의할 수 있고, Trait도 정의할 수 있습니다.
interface Hello
{
	public function world();
}

class HelloWorld implements Hello
{
	public function world()
	{
		return 'Hello, World!';
	}
}

$hello = new HelloWorld();

echo $hello->world();

3. 동적 타입 언어이지만 최근에는 정적 타입에 가까워지고 있습니다.

  • 과거에는 Scalar type(일반적으로 Primitive type이라고 지칭되는 int, float, boolean, string 등)을 제외한 array나 클래스 타입만 함수 매개변수에 타입힌트로 제한할 수 있었으나 최근 PHP는 Scalar type을 포함하여 property, return type 등도 명시할 수 있고 최신 PHP에서는 Union 타입도 지정할 수 있습니다.
<?php

function foo(Foo|Bar $input): int|float;

4. FastCGI 프로토콜을 사용합니다.

  • PHP는 항상 앞 쪽에 Apachenginx 같은 웹서버가 필요합니다. 이 웹서버가 PHP에 요청을 전달해주면, PHP는 그 결과를 응답해주는 방식입니다.

5. 공유상태가 없습니다.

  • PHP는 요청이 발생하면 스크립트를 실행합니다. 스크립트의 실행이 끝나면 그 결과물을 응답하고 종료합니다. 즉, 요청 하나 하나가 일종의 샌드박스입니다. 이는 장점이자 단점입니다.

꽤나 독특한 특징을 갖고 있습니다. 제가 느끼기엔 PHP는 버전업을 거듭할수록 Java에 가까운 모습을 했다가, 최근에는 Typescript에 가까운 느낌이 들기도 합니다.


코드퀄리티에 대하여

이런 매력적인 특징을 갖고 있음에도 PHP의 레거시 스타일은 상당히 유지보수하기 힘든 코드로 작성되어 왔습니다. 클래스 문법 자체가 2000년 초반에 도입되기도 했고, CGI 프로토콜이다보니 앞에 웹서버가 있어야 된다는 점 때문에 생긴 문제라고 생각이 드는데요.

일반적으로 웹서버가 정적 컨텐츠를 serving 할 때 URL이 디렉터리 구조를 그대로 나타내고 있습니다. PHP는 앞단에 웹서버가 필수이므로, 이로 인해 어떤 문제가 발생합니다.

만약 어떤 특정 상품페이지가 있다고 생각해봅시다. RESTFul의 경우 /products/1 와 같은 형식으로 URL이 구성되어 있을 것입니다. 그렇다면 PHP의 경우 웹루트로부터 products 디렉터리에 1이라는 스크립트가 실행돼야 한다는 뜻입니다. 또한 웹서버는 이 경로에 PHP 스크립트가 존재한다고 알아야 하며, 이를 PHP에게 처리해달라고 요청해야 합니다.

이로 인해 레거시 PHP로 된 사이트들의 경우, 끝에 .php 확장자가 있는 경우가 많습니다. 또한 RESTFul하게 URL을 구성하기가 힘들고, query string을 이용해서 products.php?id=1과 같이 URL을 구성하는 경우가 많습니다.

위처럼 모든 엔드포인트에 대해 파일을 정의하고, 그 파일에서 모든 처리를 진행합니다.

또한 이렇게 스크립트의 엔드포인트가 파일로 이루어져 있다보니, 하나의 파일에 모든 코드가 들어있는 경우가 상당히 많습니다. 즉, MVC가 아닌 경우가 많죠. 하나의 파일에 DB 커넥션, 비즈니스 로직, 뷰 코드 모든것이 존재한다는 뜻입니다.

예를 들면, 아래의 코드는 상품 리스트를 보여주는 575라인의 코드입니다.

위처럼 단 하나의 파일에서 DB쿼리, 뷰(html), DB쿼리, 뷰, <script> 태그 내의 jQuery 코드 등이 모두 모여있으며, <style> 태그가 들어가 있는 파일들도 존재합니다.

영카트는 코드가 이런식으로 이루어져 있습니다. 물론, 아주 기본적인 공통 요소들은 어느 정도 재사용 될 수 있게 빠져있긴 합니다. view의 경우 헤더, 푸터 정도가 분리되어 있고(위의 스크린샷에서 중간중간 보여지는 include 코드 등), 일종의 미들웨어 같은 역할을 하는 파일도 common.php 으로 분리되어 있습니다(물론 이 모든 것들을 엔드포인트가 되는 스크립트에서 명시적으로 가져오는 코드를 작성해야 한다는 점...).

당연하지만 모던PHP는 이런식으로 개발되지 않습니다. 다른 웹프레임워크들과 마찬가지로 모든 요청을 받는 라우터와 함께 MVC 패턴을 사용하고, 라우터에서는 URL path matching 및 path param을 컨트롤러에 전달하여 처리하도록 되어 있습니다. 아쉽게도 제가 맡은 서비스는 그렇게 안되어 있다는 점입니다.


개발 환경에 대하여

맨 처음 이 프로젝트를 인수인계 받았을 때, 코드 파일을 압축파일로 받았습니다. 네, 버전관리가 되고 있지 않았습니다. 사실 이는 어느 정도 예상했던 부분이었습니다.

그래서 인수인계 받고 가장 먼저 한 일은 다음과 같았습니다.

  1. 압축을 풀고
  2. 실서버 접속 정보를 받고
  3. 받은 파일과 동기화 후
  4. git init

당연하지만 배포환경도 없습니다. 그냥 FTP로 붙어서 코드를 업로드하기만 하면 끝납니다. PHP의 장점이 나타나는 부분이죠. 코드만 업로드하면, 바로 서버에 적용이 됩니다. 그 어떤 언어보다도 빠른 배포(?)가 가능하죠. 어떠한 재부팅 과정도 필요 없습니다.


인프라 환경에 대하여

인프라는 간결했습니다. AWS EC2 인스턴스 하나가 있고, 여기에 Apache + PHP 가 설치되어 있으며 DB는 AWS RDS로 Maria DB가 올라가 있는 구조였습니다. 딱히 설명할 게 없습니다.


문제점

제가 입사하고 나서 유지보수가 다시 이뤄지기 시작했는데요(한동안 PHP개발자가 없었음). 아무래도 개발환경부터 인프라환경까지 모든 것이 문제 투성이였습니다. 그 중에서도 가장 큰 문제점은 바로 배포의 어려움이었습니다.

배포

배포가 쉬워야 개발하기도 쉽다.

코드를 수정하거나 새로운 기능을 개발하고 운영서버에 반영하려면 FTP로 업로드 하는 방법 말고는 없었습니다. 그래서 저는 최소한 git으로 원격 repo에 있는 것을 pull하는 방식으로 반영하고 싶었습니다.

하지만 운영서버에는 git이 없었고, 또한 이 인프라환경은 저희 회사에서 관리되고 있지 않았습니다. 루트 권한으로 접속은 할 수 있었으나

  1. 서버 인프라 환경에 대한 어떠한 인수인계도 받지 못했다.
    • 서버가 껐다 켜졌을 때 어떤걸 해야 하는지, 어떻게 해야 될 지 아무도 모른다..!
  2. OS는 Amazon Linux 1 (2017.03 release) 버전이다.
  3. 월마다 최소 억단위의 매출이 지속적으로 발생하고 있다.

위와 같은 점들 때문에 서버를 건들이기가 쉽지 않았습니다.

또한 이 서비스를 담당하는 사람은 오직 저 한 명 뿐이었습니다. 입사하고 나서 코드를 어느 정도 파악하고(서비스에 대한 인수인계 역시 거의 없었습니다...) 바로 리뉴얼 작업이 예정되어 있었기 때문에, 배포환경을 바꿀 시간도 없었습니다.

그러니 배포할 때마다 항상 전쟁일 수 밖에 없었습니다. 수정된 코드, 추가된 코드, 삭제된 코드 등을 FTP로 붙어서 동기화하여 업로드해야 했고, 혹시 모를 롤백 상황에 대처하는 것도 큰 문제였습니다. 서비스 배포에 소극적일 수 밖에 없으니, 유지보수도 소극적으로 될 수 밖에 없었습니다.

그러다보니 코드퀄리티를 개선하는게 상당히 제한적일 수 밖에 없었습니다. 많은 규모의 코드 리팩토링을 했다고 하더라도, 이를 (안전하게)배포하는 일이 쉽지 않기 때문입니다.

서버 자원

제가 인수인계 받았을 때 서버의 남은 디스크 용량은 1GB 뿐이었습니다. 모든 첨부파일들은 그냥 EC2에 올라가고 있었죠. 서비스 자체가 많은 첨부파일을 요구하고 있진 않았지만, 1GB는 분명히 나중에 큰 문제가 될게 뻔했고, 그래서 나중에 용량을 추가하긴 했습니다. 다행히 하드디스크 크기를 증가시키는데에는 서버의 재부팅이 필요 없었거든요.

또한 EC2의 RAM 용량도 2GB뿐이었습니다. 이 역시 나중에 문제될 것이 뻔했습니다.


Fargate로 이사가기

결국 우려했던 문제점들이 계속 나타나기 시작했습니다. 담당자분께서 주문목록을 엑셀파로 다운로드 하시려다 실수로 지금까지 있었던 모든 주문기록에 대해 다운로드를 하시면서 서버가 죽어버리는 참사도 발생했었고, 새벽에 brute force 해킹시도에 의해 갑자기 서버가 다운되는 등... 덕분에 서버를 한 번 껐다 켜보게 됐고, apache만 띄우면 문제없이 서비스가 돌아가는 것까지 확인하게 됐죠.

이런 문제들을 근본적으로 해결하는 방법은 인프라를 저희 회사에서 직접 컨트롤할 수 있는 환경으로 옮기는 방법 말고는 없었습니다. 그래야 배포도 쉽게 할 수 있는 환경으로 구성할 수 있고, 이는 유지보수를 지금보다 훨씬 더 적극적으로 할 수 있도록 만들어 줍니다.

그래서 AWS 계정 마이그레이션을 하기로 결정했고, 그에 따라 많은 것들을 준비해야만 했습니다.

도커라이징

가장 중요한 것은 바로 도커라이징이었습니다. Fargate는 AWS ECS의 일종으로, EC2를 머신으로 쓰는것이 아닌 머신 자체는 AWS에서 제공해주고 도커 이미지만 제공하면 되는 훌륭한 서비스입니다. 이를 위해 가장 우선시 되어야 하는 작업은 도커라이징이었습니다.

Dockerfile 작성

도커라이징의 시작은 Dockerfile 작성일 것입니다. 저는 개발환경을 구성할 때 가장 먼저 하는 일 중 하나가 바로 docker compose 환경을 만드는 것이기 때문애, 이 일은 크게 어렵지 않았습니다. 다만 PHP의 경우 도커라이징할 때 고려해야되는 점이 있는데, 바로 PHP extension 문제입니다.

PHP는 기본적으로 웹서비스를 위해 탄생한 언어라서 그 자체만으로 간단한 웹서비스를 구현할 수 있지만, 추가적인 기능을 사용하기 위해선 extension을 설치해야 합니다.

예를 들어, mysql에 접속하려면 mysql extension을 설치해야 하고(mysql 클라이언트 역할), zip 파일을 다루려면 zip extension을 설치해야 합니다.

즉, PHP를 도커라이징 할 때에는 현재 운영서버에서 설치된 extension 목록을 확인하고 설치해줘야 한다는 점입니다. 이 때 https://github.com/mlocati/docker-php-extension-installer 프로젝트가 유용합니다. 이를 이용해 Dockerfile을 작성합니다.

환경변수 및 민감정보 분리하기

기존에도 개발서버, 운영서버가 분리되어 있긴 했지만 위에서도 언급했듯 버전관리가 되고 있지 않았기 때문에, 개발서버 코드와 운영서버 코드가 환경에 따라 서로 다르게 작성되어 있었습니다. 물론 이 역시 제가 인수인계를 받아 개발환경을 구성할 때 인지했던 문제였고, .env 를 이용해 환경분리를 해놨었습니다.

이 때, 민감정보들을 버전관리에서 제외하고 개발서버와 운영서버 각각 .env 파일을 다르게 작성했는데요. Fargate로 이전하면서 AWS Secret Manager를 이용하면 민감정보 역시 안전하게 분리할 수 있습니다.

무상태 서비스로 만들기

가장 큰 문제는 바로 무상태로 만드는 것이었습니다. 현재 서비스는 첨부파일은 물론, 심지어 세션관리(PHP에서는 기본적으로 세션정보를 파일로 저장합니다)까지 모두 EC2의 로컬 파일시스템을 이용하고 있었습니다.

Fargate로 이전하게 되면 서비스 내에서 사용되고 있는 모든 로컬파일시스템 관련 코드들을 수정해야 합니다. 안타깝게도(저에게 안타깝게도) 별다른 레이어 없이 파일처리 관련 코드들이 산재해 있었기 때문에, 이를 모두 수정해야만 했습니다.

로컬파일 의존성 끊기: IoC 컨테이너를 이용한 리팩토링

다행히도 PHP는 메타프로그래밍이 가능합니다. 그리고 동적타입 언어이긴 하지만 함수 매개변수에 타입힌트를 줄 수 있고, 이 정보를 런타임에 Reflection API를 통해 얻어올 수 있습니다. 이 말인 즉슨, 스프링처럼 IoC를 할 수 있다는 뜻이 됩니다. 실제로 모던 PHP 프레임워크인 Laravel은 IoC, DI를 이용해 개발을 합니다.

PHP도 의존성관리툴인 composer가 있습니다. 그리고 패키지 저장소인 packagist도 존재합니다. IoC 컨테이너 역할을 하는 패키지를 다운받아서 사용하면 손쉽게 새로운 추상 레이어를 추가하고 구현체를 환경에 따라 DI 받을 수 있을 것입니다.

PHP에는 Symfony라는 프로젝트가 존재하는데, 개발에 유용한 컴포넌트들이 여러가지 있습니다. 위에서 언급한 Laravel 역시 Symfony에 의존성을 갖고 있을 만큼 PHP 커뮤니티에서는 매우 큰 영향력을 가진 프로젝트입니다. 이런 Symfony에는 IoC 컨테이너 컴포넌트 역시 존재합니다.

IoC도 가능하니, 이제 남은 작업은 파일관련 업로드 코드를 찾고 이를 모두 수정하는 일종의 노가다만이 남아있습니다. 다행히 PHP에서 파일 업로드 코드를 찾는 방법은 꽤나 간단한데요. PHP에서는 파일을 업로드할 때 Superglobals 라는 특별한 전역변수를 반드시 사용한다는 점을 이용하는 것입니다.

모든 $_FILES 변수를 찾기만 하면, 이 코드가 바로 파일 업로드와 관련된 코드가 되는 것이죠. 이를 이용하면 다음과 같은 작업으로 쉽게 레이어를 추가할 수 있습니다.

  1. 파일관련 인터페이스를 정의한다.
  2. $_FILES 코드를 모두 찾는다.
  3. 찾은 부분을 1에서 정의한 인터페이스를 사용한 코드로 변경한다.
  4. 해당 구현체를 IoC 컨테이너로부터 가져오도록 한다.
  5. 구현체를 구현한다.

위와 같은 과정을 통해 파일업로드 하는 부분은 쉽게 변경할 수 있습니다. 다운로드하는 부분은 어쩔 수 없이 서비스에서 다운로드 기능이 있는 곳을 모두 찾아서 수정해야만 했지만요.

구현체는 로컬파일시스템 구현체와 AWS S3 구현체 두 개를 만들고, 로컬환경과 구분해서 구현체를 주입해주기만 하면 됩니다. 이 과정자체는 크게 어려운 일은 아니었지만, 상당히 귀찮고 지루한 일이었습니다. 어쨌든 파일관련 부분들을 빠짐없이 모두 찾아서 수정해야만 했으니까요.

세션파일 의존성 끊기: rediscluster로 관리하기

Fargate를 이용하면 무중단서비스를 손쉽게 구축할 수 있습니다. ECS cluster내의 service가 task를 계속 헬스체크하면서, 상태가 좋지 않은 경우 새로운 task를 띄워주는데, 만약 파일시스템 세션을 사용하게 된다면 이 순간에 모든 사용자의 세션이 사라져버릴 것입니다.

이를 막기 위해서는 당연하지만 세션을 따로 관리해야하며, 저희 회사에서는 이미 AWS ElastiCache를 사용하고 있었으므로 rediscluster를 사용할 수 있었습니다.

세션의 경우 다행히 PHP에서는 세션 드라이버를 설정파일(php.ini)로 손쉽게 수정할 수 있습니다. 이를 위해 redis php extension을 설치한 뒤 설정값만 변경해주면 됩니다.

[PHP]  
session.save_handler = rediscluster  
session.save_path = "seed[]=path.to.rediscluster:6379?timeout=10&read_timeout=10"

이렇게만 해주면 간단히 rediscluster로 세션을 적용할 수 있습니다. 이제 중간에 task가 변경되어도 세션은 항상 유지됩니다.


CI/CD 구성하기

이제 이를 손쉽게 배포할 수 있는 환경만 구성하면 됩니다. 저희 회사에서는 gitlab을 사용하고 있으므로, 이 역시 쉽게 구축할 수 있었습니다. .gitlab-ci.yml 파일만 정의하면 도커 이미지 빌드과정부터 AWS ECS로 task를 띄우는 과정까지 모두 자동화 할 수 있습니다.

  • install 스테이지에서는 composer install 을 통해 패키지를 다운받습니다. 이 때, 패키지들이 변경되는 경우는 많지 않으므로 이를 캐시해둡니다.

  • release 스테이지에서는 php-fpm 이미지를 base 이미지로 소스코드들을 모두 COPY하여 이미지를 빌드합니다. 다른 컴파일 언어들의 경우 build 라는 스테이지를 명명하여 컴파일하고 그 결과 파일을 이미지화하겠지만, PHP의 경우 인터프리터 언어이므로 스크립트 자체를 모두 포함하는 이미지를 만들어야 합니다. 이렇게 이미지를 빌드하고 이를 AWS ECR로 올리는 작업까지 진행합니다.

  • deploy 스테이지에서는 aws-cli를 이용하여 asset 파일들을 AWS S3와 sync하는 과정 및 AWS ECS로 새로운 task definition을 정의하고 service를 업데이트하는 작업을 합니다. 이 경우 서비스가 새로운 task definition을 띄우게 되므로 기존에 실행되고 있던 task는 내려가고 새로운 task가 실행되면서 무중단 상태로 서비스가 배포됩니다.

비록 아직은 test 스테이지를 구성하진 못했지만, 테스트코드가 작성은 되어있기 때문에 나중에 test 스테이지 역시 구성할 수 있을 것입니다. 참고로 PHP의 경우 PHPUnit을 이용하여 테스트코드를 작성할 수 있습니다.

이렇게해서 CI/CD까지 구성하여, Fargate로 마이그레이션 작업을 완료하였습니다.


느낀점

이번에 인프라 마이그레이션을 진행하면서 상당히 많은 점들을 느꼈는데요. 몇가지 정리해보자면

배포가 쉬워야 개발하기도 쉽다

이전에 FTP로 업로드했을 때에는 새로운 기능을 구현하는게 항상 도전이었습니다. 운영서버에 제대로 반영됐다는 것을 확신할 수 없었기 때문입니다. 이러한 배포과정의 난이도는 곧 개발의 난이도로 직결된다는 것을 이번에 확실히 깨닫게 됐습니다.

인프라가 통제돼야 개발하기도 쉽다

도커를 쓰는 가장 핵심적인 이유가 아닐까 합니다. 인프라가 통제돼야 안정감있는 개발이 가능합니다.

자동화의 힘

위에서 언급하지 않은 내용이 있는데요. 저희 인프라는 terraform으로 관리되고 있습니다. 즉, 저희 시스템은 사실상 모든 것이 코드로 관리되고 있습니다. 클라우드 플랫폼을 사용한다면 모든 것을 코드로 관리할 수 있는 시대입니다. 이는 엄청난 유연함을 보장해주며, 환경변화에 빠른 대응을 가능케 합니다. 코드로 작성될 수 있는 것들은 모두 자동화될 수 있습니다. 코드의 실행 주체가 인간이 아니기만 하면 되니까요.

자동화라는 키워드는 생각보다 훨씬 개발에 있어서 중요하다는 것을 느꼈습니다. 많은 일들을 코드로 표현하는 연습을 해야겠다는 생각이 듭니다.

이제 서비스를 확실히 통제할 수 있게 됐으니, 개선하고 싶었던 것들을 마음껏 개선할 수 있을 것 같습니다. 가장 먼저 하고 싶은 일은 아무래도 PHP의 버전을 올리는 일인 것 같습니다. 이제는 도커의 base 이미지만 변경하면, 바로 버전업을 할 수 있기 떄문이죠. 물론 기존 코드의 마이그레이션 역시 필요하겠지만, 이 역시 phpstan과 같은 코드 분석툴을 사용하면 어렵지 않게 달성할 수 있을 것 같습니다.

앞으로는 재밌게 일을 할 수 있을 것 같네요!

profile
undefined cat

2개의 댓글

comment-user-thumbnail
2023년 1월 27일

훌륭합니다.

답글 달기
comment-user-thumbnail
2023년 4월 4일

대단하십니다..!!

답글 달기