Slim Framework는 꼭 필요한 기능(라우팅, 미들웨어, 의존성 주입 등)만을 제공하는 경량 PHP 마이크로 프레임워크입니다. 불필요한 코드를 줄이고 가벼운 API 시스템이나 마이크로 서비스를 개발할 때 매우 유용합니다. Composer와 결합하여 현대적이고 모듈화된 PHP 애플리케이션을 구축할 수 있습니다.
최신 PHP 개발의 핵심은 Composer입니다. Composer는 프로젝트 단위로 라이브러리(패키지)를 관리해주는 PHP 의존성 관리 도구입니다. Slim Framework 역시 Composer를 통해 설치 및 관리됩니다.
| 명령어 | 설명 |
|---|---|
| composer init | 프로젝트를 초기화하고 composer.json 파일을 생성합니다. |
| composer require slim/slim:"4.*" | Slim 프레임워크 패키지를 다운로드하고 의존성에 추가합니다. |
| composer install | composer.lock 파일을 기반으로 패키지들을 설치합니다 (배포 시). |
| composer update | 모든 패키지를 최신 버전으로 업데이트합니다. |
Composer는 `vendor/autoload.php` 파일을 생성하여 PSR-4 표준에 따른 자동 로딩을 지원합니다. 여러분의 PHP 스크립트 최상단에서 이 파일을 가장 먼저 `require` 해야 모든 라이브러리를 사용할 수 있습니다.
실제 PHP 환경에서 Slim Framework를 구동하여 /api/hello 엔드포인트를 호출했을 때의 코드를 작성하고 결과를 브라우저에서 확인해 보겠습니다.
이처럼 Slim Framework를 활용하면 불필요한 보일러플레이트 코드 없이, 꼭 필요한 라우팅 로직만으로 가벼운 API 서버를 구축할 수 있습니다. 다음 강의에서는 실제 PHP 환경에서 설치하는 과정을 상세히 진행합니다.
Slim 프레임워크를 사용하여 가장 기초적인 "Hello World" API 서버를 구축해봅니다. Composer를 통한 패키지 설치부터 라우팅 설정, 그리고 클라이언트 요청에 대한 JSON 응답 처리까지 전체 사이클을 경험하게 됩니다.
먼저 터미널을 열고 프로젝트 폴더를 생성한 후, Slim 프레임워크와 PSR-7 구현체인 Nyholm을 설치합니다.
설치가 완료되면, 프로젝트 루트 디렉토리에 index.php 파일을 생성하고 아래의 코드를 작성합니다. 이 코드는 클라이언트가 /hello/{이름} 으로 요청을 보냈을 때 JSON 형태로 응답하는 백엔드 서버입니다.
코드를 모두 작성했다면, 터미널에서 php -S localhost:8080 명령어를 실행하여 개발용 내장 웹 서버를 띄울 수 있습니다. 그 다음 브라우저에서 http://localhost:8080/hello/Developer 로 접속하여 결과를 확인해보세요.
데이터베이스 비밀번호, 외부 API 키 등 소스 코드에 절대 포함되어서는 안 되는 민감한 정보들을 별도의 .env 파일로 안전하게 분리하는 방법을 배웁니다. 보안의 가장 기초적이고 핵심적인 과정입니다.
PHP 생태계에서 환경 변수 관리를 위해 가장 널리 쓰이는 표준 패키지인 vlucas/phpdotenv를 설치합니다.
.env 파일은 절대로 Git(GitHub 등)에 커밋되어서는 안 됩니다. 소스 코드를 공유할 때는 항상 .gitignore 파일에 .env를 명시하여 실수로 업로드되는 대참사를 막아야 합니다.
루트 디렉토리에 .env 파일을 생성하고 민감한 데이터를 작성합니다. 이후 PHP 코드 상단에서 Dotenv를 초기화하면, 글로벌 변수 $_ENV를 통해 어디서든 안전하게 접근할 수 있습니다.
Slim Framework의 핵심은 PSR-7 HTTP 메시지 인터페이스를 준수한다는 점입니다. 클라이언트가 보내는 모든 데이터(Request)와 서버가 반환하는 데이터(Response)는 이 표준 객체들을 통해 일관성 있게 처리됩니다.
클라이언트가 전송한 쿼리 스트링, 폼 데이터, JSON Body 등을 읽는 방법입니다.
| 메서드 | 설명 |
|---|---|
| $request->getQueryParams() | URL 쿼리 스트링(?key=val)을 배열로 반환 |
| $request->getParsedBody() | POST로 전송된 JSON 바디나 폼 데이터를 배열/객체로 자동 파싱 |
| $request->getHeaderLine('Authorization') | 특정 HTTP 헤더 값을 문자열로 반환 |
서버가 클라이언트에게 응답할 데이터를 조립합니다. API 환경에서는 보통 배열을 JSON 문자열로 변환(json_encode)하여 Body에 기록합니다.
withStatus()나 withHeader() 메서드를 호출하면 원본 객체가 변경되는 것이 아니라 새로운 속성이 적용된 새 복사본이 반환됩니다. 체이닝(Chaining)을 통해 변경된 객체를 반드시 return 해야 정상적으로 작동합니다.
위의 코드 블록에서 클라이언트가 서버로 POST 데이터를 전송하고, 서버가 파싱 후 JSON 응답(201 Created)을 성공적으로 반환하는 과정을 확인할 수 있습니다.
Slim Framework의 라우터는 매우 강력합니다. URL의 특정 부분을 변수처럼 받아오는 동적 파라미터와, 공통된 URL 접두사나 미들웨어를 하나로 묶어주는 라우트 그룹화(Grouping) 기능을 통해 깔끔한 API 구조를 설계할 수 있습니다.
중괄호 {변수명}를 사용하면 URL 경로의 일부를 파라미터로 받을 수 있습니다. 이 값은 라우트 콜백 함수의 세 번째 인자인 $args 배열로 전달됩니다.
버전이 명시된 API나 특정 도메인(예: /api/v1/...)의 라우트들을 group() 메서드로 묶어서 관리할 수 있습니다. 이를 통해 코드 중복을 줄이고 특정 그룹에만 미들웨어를 쉽게 적용할 수 있습니다.
파라미터에 정규식을 추가하여 {id:[0-9]+} 처럼 숫자만 허용하도록 라우팅 단에서 엄격하게 제한할 수도 있습니다. 정규식에 맞지 않으면 자동으로 404 에러가 발생합니다.
위의 결과 화면에서 /api/v1/users/4028 URL로 요청을 보냈을 때, 라우터가 그룹 접두사(/api/v1)와 동적 파라미터(4028)를 모두 정확히 인식하고 파싱하는 과정을 확인할 수 있습니다.
미들웨어는 HTTP 요청이 애플리케이션의 핵심 로직(라우트 콜백)에 도달하기 전과 후에 특정 코드를 실행할 수 있게 해주는 양파 모양의 레이어(Onion Architecture)입니다. 인증(Authentication), 로깅, CORS 설정 등에 주로 사용됩니다.
Slim 미들웨어는 Request 객체와 다음 계층으로 넘겨주는 핸들러(RequestHandler)를 받습니다. "요청 가로채기 ➡️ 다음 계층으로 넘기기 ➡️ 응답 가로채기"의 순서로 진행됩니다.
인증 토큰(API Key)이 올바른지 검사하는 간단한 보안 미들웨어 예시입니다.
Slim 프레임워크에서 미들웨어를 여러 개 add() 할 때, 가장 마지막에 추가된 미들웨어가 가장 먼저 실행(LIFO 구조)된다는 점에 각별히 주의해야 합니다!
위의 코드 실행 결과에서 볼 수 있듯, Authorization 헤더에 올바른 토큰을 전송하면 미들웨어 검문을 통과하여 핵심 로직이 실행되고 정상적인 응답(200 OK)을 반환하게 됩니다.
클라이언트가 서버로 전송하는 데이터는 절대 신뢰해서는 안 됩니다. 데이터 베이스에 저장하기 전이나 핵심 로직을 수행하기 전에 올바른 형식인지 검증(Validation)하는 것은 보안과 안정성을 위해 필수적입니다. PHP 환경에서는 직관적인 API를 제공하는 RespectValidation 라이브러리를 많이 사용합니다.
가장 먼저 컴포저를 통해 라이브러리를 설치합니다. (composer require respect/validation)
설치 후에는 v::email() 처럼 다양한 검증 조건들을 메서드 체이닝으로 연결하여 직관적으로 규칙을 정의할 수 있습니다.
실무 API에서는 개별 필드를 하나씩 검증하기보다, v::attribute()나 v::key()를 활용해 전체 페이로드 구조를 한 번에 검증하고 오류를 try-catch 블록으로 잡아내는 패턴이 훨씬 깔끔하고 유지보수하기 좋습니다.
데이터가 유효하지 않을 때, 단순히 "에러 발생"이라고만 반환하지 않고 400 Bad Request 상태 코드와 함께 어떤 필드가 왜 틀렸는지 명시하는 details 배열을 제공해야 합니다. 프론트엔드는 이 구조화된 에러를 바탕으로 각 입력 폼 하단에 친절한 경고 문구를 렌더링할 수 있습니다.
위의 코드 실행 결과에서 볼 수 있듯, 잘못된 형식의 데이터를 POST 요청으로 전송하면 NestedValidationException이 발생하여, 프론트엔드가 즉시 파싱하여 사용자에게 보여줄 수 있는 상세한 에러 배열(details)과 함께 400 Bad Request가 반환됩니다.
서버에서 발생하는 에러, 데이터베이스 트랜잭션 성공/실패 여부, 보안 침해 시도 등을 추적하려면 체계적인 로깅 시스템이 필수적입니다. PHP 생태계의 표준 로깅 라이브러리인 Monolog를 Slim 프레임워크와 연동하여 파일(File)에 로그를 남기는 방법을 알아봅니다.
DI(Dependency Injection) 컨테이너에 Logger를 등록하면 앱 전역 라우트 콜백에서 $this->get('logger') 형태로 쉽게 불러와 사용할 수 있습니다.
| 주요 로그 레벨 (RFC 5424) | 사용 목적 |
|---|---|
| DEBUG | 개발 중 상세한 진행 과정 확인 및 디버깅 용도 |
| INFO | 일반적인 주요 이벤트 (유저 가입, 결제 성공 등) |
| WARNING | 시스템 중단은 아니지만 주의가 필요한 비정상적인 상태 |
| ERROR / CRITICAL | 즉각적인 조치가 필요한 심각한 런타임 에러나 DB 연결 실패 |
주입된 logger를 호출해 info(), error() 등의 메서드를 사용합니다.
메시지를 문자열 결합($msg . $id)으로 남기는 것보다, 로거의 두 번째 인자인 Context 배열에 데이터를 분리하여 넘기는 것이 강력히 권장됩니다. JSON 포맷으로 구조화되어 추후 Datadog, ElasticSearch 같은 로그 수집 도구에서 검색하기가 압도적으로 유리해집니다.
위의 코드 실행 결과 블록에서 서버의 app.log 파일 내부에 Monolog가 어떤 포맷으로 텍스트를 적재하는지 관찰해 보세요. INFO 레벨과 ERROR 레벨이 명확히 구분되며, Context 배열에 담았던 데이터들이 JSON 형식으로 깔끔하게 남은 것을 볼 수 있습니다.
API 서버가 아니라 HTML 문서를 직접 렌더링해서 클라이언트에게 내려주어야 할 때가 있습니다. PHP 백엔드 로직이 HTML 뷰 코드에 지저분하게 섞이는 것을 방지하기 위해, Slim은 Twig라는 우아하고 현대적인 템플릿 엔진을 공식적으로 지원합니다.
컴포저로 slim/twig-view 패키지를 설치한 뒤, DI 컨테이너에 Twig 인스턴스를 등록하면 라우트에서 손쉽게 가져와 사용할 수 있습니다.
라우트 콜백 내부에서 DB 데이터를 조회한 뒤, $view->render() 메서드를 통해 템플릿 파일명과 함께 변수 배열을 주입합니다.
뷰 파일(.twig)에서는 중괄호 {{ 변수명 }} 를 통해 값을 출력하고, {% for %} 등의 구문을 통해 제어 흐름을 다룹니다.
Twig의 {{ }} 출력 구문은 기본적으로 HTML 이스케이프 처리가 자동으로 적용됩니다. 악의적인 유저가 입력 폼에 <script> 태그를 넣어두었더라도, 브라우저에서 실행되지 않고 안전한 텍스트로 치환되어 렌더링되므로 XSS(Cross Site Scripting) 공격을 원천적으로 차단할 수 있습니다.
위의 코드 실행 결과 블록에서 볼 수 있듯, $userData 배열 데이터가 Twig 엔진을 거치면서 템플릿의 변수 부분과 결합되어 완벽한 HTML 페이지로 브라우저에 렌더링 됩니다.
Slim 프레임워크는 내부적으로 어떤 DI(Dependency Injection) 컨테이너든 결합할 수 있는 유연한 구조(PSR-11)를 지원합니다. 그중에서도 가장 강력하고 사용하기 쉬운 PHP-DI를 연동하여, 객체 간의 의존성을 깔끔하게 해결하고 테스트하기 쉬운 코드를 작성하는 방법을 배웁니다.
특정 클래스가 다른 클래스(예: 데이터베이스 연결 객체, 로거 등)를 내부에서 직접 생성(new)하지 않고, 외부에서 주입받는(Inject) 설계 패턴입니다. 결합도를 낮추고 단위 테스트(Unit Test) 시 Mock 객체로 교체하기 쉽게 만들어 줍니다.
| 안 좋은 예 (강한 결합) | 좋은 예 (의존성 주입) |
|---|---|
class UserController {
private $db;
public function __construct() {
// ❌ 클래스 내부에서 직접 생성
$this->db = new Database();
}
}
|
class UserController {
private $db;
// ✅ 외부(컨테이너)에서 객체를 주입받음
public function __construct(Database $db) {
$this->db = $db;
}
}
|
php-di/php-di 패키지를 설치한 후 AppFactory를 통해 앱을 생성하기 전 컨테이너를 세팅합니다. 컨테이너 저장소에 필요한 객체(예: PDO)를 미리 정의해 둡니다.
앱이 커지면 $app->get() 안에 익명 함수로 로직을 다 적는 것은 무리가 있습니다. 별도의 컨트롤러 클래스로 분리하면 PHP-DI가 생성자의 타입 힌트(Type-hint)만 보고도 필요한 객체를 알아서 찾아 주입(Autowiring)해 줍니다.
Autowiring은 매우 편리하지만, 마법처럼 동작하기 때문에 내부 흐름을 파악하기 어려울 수 있습니다. 명확한 인터페이스(Interface)를 정의하고 컨테이너 설정 파일에서 인터페이스와 구현체를 명시적으로 매핑해 주는 것이 규모가 큰 프로젝트에서는 더 좋은 설계입니다.
위의 코드 실행 결과 블록에서 볼 수 있듯, 클라이언트가 API를 요청하면 라우터가 컨트롤러를 호출하기 직전에 PHP-DI 컨테이너가 중간에 개입하여 __construct의 타입 힌트를 분석하고, 필요한 부품(PDO 객체 등)들을 컨테이너에서 꺼내어 자동으로 조립(Autowiring) 해주는 과정을 확인할 수 있습니다.
데이터베이스를 다룰 때 순수 SQL 쿼리 문자열을 작성하는 방식(PDO)은 직관적이지만 코드가 길어지고 유지보수가 어렵습니다. PHP 진영(특히 Laravel)에서 가장 강력하고 사랑받는 Eloquent ORM 패키지를 Slim 프레임워크에 독립적으로 연동하여, DB 테이블을 우아한 객체 지향(Object-Oriented) 방식으로 제어하는 기술을 배웁니다.
데이터베이스의 테이블을 프로그래밍 언어의 클래스(Class)에, 테이블의 레코드(Row)를 객체(Object) 인스턴스에 매핑하는 기술입니다. 복잡한 SQL 쿼리문을 몰라도 순수한 PHP 코드의 메서드 호출만으로 CRUD 작업을 수행할 수 있게 해줍니다.
illuminate/database 패키지를 설치한 뒤, index.php 최상단에서 DB 연결 정보 배열을 세팅합니다. Capsule Manager를 통해 정적(Static)으로 어디서든 접근할 수 있도록 setAsGlobal()을 호출하는 것이 핵심입니다.
IlluminateDatabaseEloquentModel을 상속받는 클래스만 만들어주면 끝납니다. Eloquent는 클래스명의 복수형(예: User -> users)을 자동으로 테이블 이름으로 매핑합니다.
Eloquent ORM의 모든 메서드(where, create 등)는 백그라운드에서 PDO Parameter Binding(Prepared Statements)을 사용하도록 설계되어 있습니다. 즉 User::where('email', $_POST['email']) 처럼 유저의 입력값을 바로 넣어도 악의적인 SQL 문법이 삽입(Injection)되는 것을 원천적으로 차단합니다.
위의 코드 블록 하단 콘솔 목업에서 볼 수 있듯, 우리가 작성한 직관적인 체이닝 코드(where(...)->orderBy(...))가 백그라운드에서 실제 어떤 형태의 안전한 Raw SQL 구문으로 번역되는지를 확인할 수 있습니다. 인자값(1)은 ? 로 대체되어 PDO 바인딩 처리되므로 SQL 인젝션 공격으로부터 매우 안전합니다.
모던 백엔드 개발에서 REST(REpresentational State Transfer) 원칙은 선택이 아닌 필수입니다. URI(주소)로는 조작할 자원(Resource)만을 명시하고, 그 자원에 대해 '어떤 행동(Action)'을 할 것인지는 HTTP Method(GET, POST, PUT, DELETE)로 정의하는 이 우아한 표준 설계 패턴을 Slim 프레임워크에 구현하는 방법을 알아봅니다.
과거에는 URI 자체에 동사(행위)를 포함시키는 경우가 많았습니다. (예: /createUser). REST 아키텍처에서는 주소는 명사형 자원(/users)만 명시하고, CRUD 행위는 HTTP Method에 전적으로 위임합니다.
| 행위 (CRUD) | 안 좋은 예시 (동사 포함) | RESTful 예시 (표준) |
|---|---|---|
| 목록 조회 (Read) | GET /getUsers | GET /users |
| 신규 생성 (Create) | POST /createUser | POST /users |
| 전체 수정 (Update) | POST /updateUser?id=7 | PUT /users/7 |
| 레코드 삭제 (Delete) | GET /deleteUser?id=7 | DELETE /users/7 |
Slim 프레임워크는 HTTP Method별로 직관적인 라우팅 메서드(get, post, put, delete)를 제공합니다. 또한 $app->group()을 활용하면 동일한 경로(/users)를 사용하는 연관된 API들을 깔끔하게 묶어(Grouping) 코드를 구조화할 수 있습니다.
완벽한 REST API를 구축하려면 응답 바디(JSON 데이터)뿐만 아니라, HTTP 헤더의 상태 코드도 상황에 맞게 명확히 응답해야 합니다. 데이터 생성 성공 시에는 201 Created, 데이터 삭제 완료 후 바디 내용이 비어있을 때는 204 No Content, 인증되지 않은 요청엔 401 Unauthorized를 반환하는 것이 글로벌 표준 설계입니다.
위의 터미널 목업에서는 curl을 이용해 API를 테스트한 결과를 보여줍니다. 동일한 자원 URI(/users)를 대상으로 하더라도 서로 다른 HTTP Method(POST, DELETE)를 요청했을 때 백엔드 라우터가 이를 분기하여 적절한 처리 후 알맞은 상태 코드(201 Created, 204 No Content)를 응답하는 것을 확인할 수 있습니다.
Slim 프레임워크 기반 API를 구축할 때, 매번 json_encode()를 호출하고 Content-Type 헤더를 수동으로 세팅하는 중복 작업을 줄이는 방법(커스텀 헬퍼 함수 구현)과, 프론트엔드와 원활하게 소통하기 위해 상황별로 올바른 HTTP Status Code를 매핑하는 모범 사례를 알아봅니다.
기본적인 Slim의 방식대로라면, PHP 배열을 JSON 문자열로 직렬화하고, 바디 버퍼에 쓰고, application/json 헤더를 붙이는 지루한 코드를 모든 라우트마다 반복해야 합니다.
반복을 피하기 위해 전역(Global) 헬퍼 함수를 하나 만들어두면, 응답 코드를 획기적으로 줄이고 프로젝트 전체에 일관된 API 규격을 강제할 수 있습니다.
프론트엔드 개발자와 원활하게 협업하려면, 단순히 성공(200)과 서버 에러(500)만 내려주어서는 안 됩니다. 실패 원인이나 성공의 종류에 맞게 세분화된 상태 코드를 매핑해야 합니다.
| HTTP Status | 설명 (주요 사용처) | 권장 JSON 응답 형태 (예시) |
|---|---|---|
| 200 OK | 일반적인 요청 성공 (GET 데이터 조회, PUT 성공 등) | { "data": [...] } |
| 201 Created | POST 요청으로 새로운 리소스가 성공적으로 생성됨 | { "id": 7, "message": "생성 완료" } |
| 400 Bad Request | 클라이언트가 보낸 데이터 형식이 틀림 (유효성 검사 실패) | { "error": "Invalid Data", "details": {...} } |
| 401 Unauthorized | 로그인 인증이 누락되었거나 JWT 토큰이 유효하지 않음 | { "error": "로그인이 필요합니다." } |
| 403 Forbidden | 로그인은 했으나 해당 리소스에 접근할 권한(Role)이 없음 | { "error": "권한이 없습니다." } |
| 404 Not Found | 요청한 자원(예: 없는 유저 ID)이 DB에 존재하지 않음 | { "error": "존재하지 않는 회원입니다." } |
위의 터미널 목업에서는 jsonResponse() 헬퍼 함수를 통해 API 응답이 어떻게 전달되는지 보여줍니다. 단 한 줄의 코드로 JSON 인코딩, Content-Type 헤더 세팅, 그리고 201 Created 와 같은 상태 코드 설정이 한 번에 처리되어 매우 깔끔한 백엔드 구조를 유지할 수 있습니다.
최신 RESTful API 서버는 세션(Session) 대신 상태를 저장하지 않는(Stateless) JWT 인증 방식을 주로 채택합니다. 서버에서 비밀 키(Secret Key)로 서명하여 발급한 토큰을 프론트엔드가 보관하고, 매 API 요청마다 헤더에 담아 전송하여 자신을 증명하는 인증 프로세스를 Slim 프레임워크에 구현해 봅니다.
JWT는 .(점)을 기준으로 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 나뉩니다. 누구나 Base64 디코딩으로 내용을 열어볼 수 있으므로 비밀번호 같은 민감한 정보는 절대 넣으면 안 되며, 마지막 서명(Signature) 부분이 위조 방지의 핵심 역할을 합니다.
가장 검증된 패키지인 firebase/php-jwt 를 컴포저로 설치합니다. 클라이언트가 올바른 계정 정보를 보내면, 서버는 식별자(user_id)와 만료 시간(exp)을 담아 토큰을 생성(Encode)하여 반환합니다.
프론트엔드는 발급받은 토큰을 로컬 스토리지에 보관하다가, 보안이 필요한 API를 호출할 때마다 Authorization: Bearer [토큰] 헤더에 담아 보냅니다. 서버는 이를 해독(Decode)하여 위변조 및 만료 여부를 검사합니다.
토큰의 서명(Signature)을 검증할 때 사용하는 Secret Key가 외부에 유출되면, 해커가 임의로 서명을 새로 만들 수 있으므로 시스템 보안이 완전히 붕괴됩니다. 이 키는 절대 코드 저장소(Git)에 올리지 말고 서버의 .env 환경 변수로 엄격하게 관리해야 합니다.
위의 백엔드 실행 로그 목업에서 볼 수 있듯, 정상적인 토큰이 들어오면 서명 검증 후 안전하게 유저 식별자(user_id)를 추출해 API를 제공합니다. 반면, 누군가 Payload를 변조하여 다른 권한으로 상승하려 해도, Secret Key가 없는 한 올바른 서명(Signature)을 새로 만들 수 없으므로 JWT::decode() 함수에서 즉각적으로 예외(Exception)를 발생시키고 401 Unauthorized 에러를 뱉어내며 철벽 방어를 수행합니다.
모든 웹 애플리케이션은 해커의 악의적인 공격(XSS, CSRF, SQL Injection 등)에 노출되어 있습니다. Slim 프레임워크의 CSRF Guard 미들웨어를 적용하여 폼(Form) 위조 공격을 완벽히 차단하고, PHP의 내장 함수를 활용해 사용자가 입력한 모든 데이터를 서버에 저장하기 전 안전하게 정화(Sanitization)하는 핵심 보안 계층(Security Layer) 구축 방법을 배웁니다.
게시판이나 댓글 창에 해커가 악성 자바스크립트(<script>)를 몰래 입력하여, 다른 사용자의 브라우저에서 실행되게 만드는 공격을 XSS(Cross-Site Scripting)라고 합니다. 백엔드 개발자의 가장 기본 철칙은 "클라이언트가 보낸 모든 입력을 절대 신뢰하지 않고 무조건 필터링한다"는 것입니다.
| 방어 기법 | PHP 내장 함수 및 동작 원리 |
|---|---|
| HTML 태그 무효화 | htmlspecialchars($input, ENT_QUOTES, 'UTF-8')< 기호를 < 로 변환하여, DB에 저장된 후 화면에 출력될 때 브라우저가 실행 가능한 태그가 아닌 단순 텍스트로 인식하게 만듭니다. |
| 모든 태그 강제 삭제 | strip_tags($input)문자열 내에 포함된 모든 HTML, PHP 태그를 흔적도 없이 완전히 제거해 버립니다. |
| 타입/정규식 필터링 | filter_var($input, FILTER_SANITIZE_EMAIL)이메일이나 숫자 등 기대하는 특정 포맷에 맞지 않는 불필요한 문자를 걸러냅니다. |
CSRF는 해커가 사용자의 로그인 권한(세션/쿠키)을 훔쳐, 사용자의 의도와 상관없이 몰래 송금이나 회원 탈퇴 등의 치명적인 요청을 보내도록 위조하는 해킹 기법입니다. 이를 원천 차단하기 위해 서버는 폼(Form)을 그릴 때마다 일회용 난수 보안 토큰을 발행하고, 폼이 전송될 때 이 토큰이 일치하는지 미들웨어 단에서 엄격하게 검사해야 합니다.
이전 챕터에서 배운 브라우저가 자동으로 첨부하지 않는 JWT(Bearer Token) 전용 API 서버라면 구조적으로 CSRF 공격 자체가 불가능합니다. 하지만 일반적인 세션/쿠키 기반의 인증을 혼용하거나 SSR 웹사이트를 함께 운영한다면, 이 CSRF 방어 미들웨어는 무조건 필수적으로 적용해야 합니다.
위의 백엔드 보안 로그 목업에서는 두 가지 방어 시나리오를 보여줍니다. 첫 번째는 악성 스크립트 페이로드가 들어왔을 때, htmlspecialchars()를 통해 브라우저가 실행할 수 없는 단순 텍스트 문자열(<script>)로 무력화(Sanitization)되어 안전하게 DB에 저장되는 모습입니다. 두 번째는 해커가 타 사이트에서 보안 토큰을 누락한 위조 폼을 전송했을 때, 미들웨어 단계에서 즉각적으로 라우터 진입을 차단(400 Bad Request)하는 CSRF 방어선 동작을 보여줍니다.
Slim 프레임워크가 독자적인 방식이 아닌 PHP 표준 권고안(PSR)을 완벽하게 준수하고 있다는 것은 실무에서 매우 강력한 장점입니다. HTTP 메시지를 정의하는 PSR-7과 미들웨어 파이프라인을 정의하는 PSR-15의 개념을 이해하여, 특정 프레임워크에 종속되지 않는 재사용 가능한 코드를 작성하는 방법을 배웁니다.
과거의 PHP 개발자들은 $_GET, $_POST, $_SERVER 같은 전역 변수(Superglobals)에 직접 접근하여 코드를 작성했습니다. PSR-7은 이러한 HTTP 요청(Request)과 응답(Response)을 불변(Immutable) 객체로 추상화한 PHP 커뮤니티의 표준입니다. Slim의 라우터 콜백에 주입되는 $request와 $response 객체가 바로 이 규격을 따릅니다.
| 과거의 안티 패턴 (전역 변수) | PSR-7 객체지향 방식 (Slim) |
|---|---|
$userId = $_GET['id']; |
$userId = $request->getQueryParams()['id'] ?? null; |
$token = $_SERVER['HTTP_AUTHORIZATION']; |
$token = $request->getHeaderLine('Authorization'); |
echo json_encode($data); |
$response->getBody()->write(json_encode($data)); |
PSR-7 객체는 상태가 변경되지 않습니다(Immutable). 예를 들어 $response->withStatus(404)를 호출하면 기존 객체의 상태가 변하는 것이 아니라, 상태 코드가 404로 설정된 새로운 복제본(Clone) 객체가 생성되어 반환됩니다. 따라서 반드시 $response = $response->withStatus(404); 처럼 변수에 다시 할당하거나 체이닝(Chaining)을 통해 Return 해야만 정상적으로 동작합니다.
PSR-15는 이 PSR-7 기반의 Request 객체를 받아 가공한 뒤 Response 객체를 반환하는 미들웨어(Middleware) 파이프라인의 표준 규격입니다. 커스텀 미들웨어 클래스를 작성할 때는 반드시 MiddlewareInterface를 구현(implements)해야 합니다.
과거에는 각 프레임워크(Laravel, CodeIgniter 등)마다 미들웨어를 작성하는 규칙과 Request 객체의 모양이 제각각이어서 코드를 재사용할 수 없었습니다. 하지만 PSR-15를 준수하는 미들웨어 클래스로 작성해두면, 현재 만들고 있는 Slim 앱 뿐만 아니라 Zend Expressive(Mezzio) 등 다른 PSR-15 호환 프레임워크로 시스템을 이전하더라도 코드 단 한 줄 수정 없이 미들웨어를 100% 재사용할 수 있다는 놀라운 이점이 있습니다.
PSR-15 미들웨어는 흔히 양파 껍질 구조에 비유됩니다. 위의 콘솔 실행 로그 목업에서 볼 수 있듯, Request가 껍질(미들웨어 1 -> 2)을 차례대로 뚫고 코어(라우터)로 들어갔다가, 처리가 완료된 후 Response로 감싸져서 역순(미들웨어 2 -> 1)으로 바깥 껍질로 빠져나오는 흐름이 핵심입니다. 또한 인증에 실패할 경우 코어까지 들어가지 않고 미들웨어 단계에서 즉시 튕겨 나오는(단락 평가) 모습도 확인할 수 있습니다.
안정적인 프로덕션 애플리케이션을 유지하고 지속적 통합(CI)을 구축하려면 테스트 자동화가 필수입니다. PHP 진영의 표준 테스트 프레임워크인 PHPUnit을 활용하여, Slim의 라우터 엔드포인트에 가상(Mock) 요청을 밀어넣고 그 응답(Response)이 의도한 대로 동작하는지 검증하는 방법을 알아봅니다.
먼저 컴포저를 통해 require --dev phpunit/phpunit 명령어로 개발 환경용 패키지를 설치합니다. 이후 프로젝트 루트에 phpunit.xml 설정 파일을 생성하여 테스트 대상 디렉터리와 환경 변수를 세팅합니다.
<!-- phpunit.xml 설정 예시 -->
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Slim Application Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<!-- 테스트 전용 환경 변수(DB 등) 오버라이딩 가능 -->
<env name="APP_ENV" value="testing"/>
</php>
</phpunit>
Slim 앱을 테스트할 때는 실제 서버(Apache/Nginx)를 띄우거나 Postman을 사용할 필요가 없습니다. 대신, 코드 상에서 가짜(Mock) PSR-7 Request 객체를 생성하여 Slim의 $app->handle($request) 코어 메서드에 직접 주입(Inject)하고, 그 결과로 튀어나오는 Response 객체를 검사하는 '인메모리(In-Memory) 테스트' 방식을 사용합니다.
| 주요 Assertion (검증) 메서드 | 동작 원리 및 사용처 |
|---|---|
assertSame($expected, $actual) |
HTTP 상태 코드(200, 404 등)나 헤더 값이 기대한 것과 정확히 일치(===)하는지 엄격하게 검사합니다. |
assertJson($string) |
응답 바디 문자열이 문법적으로 유효한 JSON 포맷인지 확인합니다. API 테스트에 필수입니다. |
assertStringContainsString() |
응답 바디(문자열) 내에 특정 에러 메시지나 성공 키워드가 포함되어 있는지 느슨하게 검사합니다. |
tests/UserApiTest.php 파일을 생성하고, PHPUnitFrameworkTestCase를 상속받는 테스트 클래스를 작성합니다. 모든 테스트 케이스 메서드 이름은 반드시 test로 시작해야 인식됩니다.
<?php
use PHPUnit\Framework\TestCase;
use Slim\Factory\AppFactory;
use Slim\Psr7\Factory\ServerRequestFactory;
class UserApiTest extends TestCase
{
// 테스트용 Slim App 인스턴스를 메모리에 생성하는 헬퍼 메서드
protected function getApp() {
$app = AppFactory::create();
// 실제로는 require 'routes.php'; 로 가져옵니다.
$app->get('/api/users', function ($req, $res) {
$res->getBody()->write(json_encode(['status' => 'success']));
return $res->withHeader('Content-Type', 'application/json')->withStatus(200);
});
return $app;
}
public function testGetUsersReturnsSuccess()
{
$app = $this->getApp();
// 1. Arrange (준비): 가짜(Mock) GET /api/users PSR-7 요청 객체 생성
$requestFactory = new ServerRequestFactory();
$request = $requestFactory->createServerRequest('GET', '/api/users');
// 2. Act (실행): 애플리케이션 코어에 가짜 요청을 밀어넣고 결과 응답 획득
$response = $app->handle($request);
// 3. Assert (검증): 예상된 결과와 실제 결과 비교
$this->assertSame(200, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertStringContainsString('success', $body);
}
}
터미널에서 ./vendor/bin/phpunit 명령을 실행하면, tests/ 디렉터리 내의 모든 테스트 코드를 자동으로 찾아 실행합니다. 테스트가 모두 통과하면 초록색 OK 메시지가, 단 하나라도 실패하면 빨간색 FAILURES 메시지와 실패 원인이 출력됩니다.
하단의 시뮬레이터 버튼을 눌러, 실제 개발 환경의 터미널(bash)에서 PHPUnit이 실행될 때 테스트 결과(성공 시의 쾌감과 실패 시의 에러 추적)가 콘솔에 어떻게 출력되는지 체험해 보세요.