Ver2.5 줌라(Joomla) 웹 서비스 완전 정복: RESTful API 개발 A to Z

줌라(Joomla) 웹 서비스 완전 정복: RESTful API 개발 A to Z
너의 줌라 사이트, 이제 세상과 소통할 시간이야!
개발자 친구가 옆에서 알려주듯, 쉽고 재미있게 API의 세계로 안내할게.
들어가며: "내 웹사이트, 혹시 '외딴 섬'은 아닐까?"
안녕! 코딩하는 친구들. 오늘 좀 흥미로운 얘기를 해볼까 해.
우리가 열심히 만든 줌라(Joomla) 웹사이트, 정말 멋지지. 디자인도 짱짱하고, 콘텐츠도 알차게 채워 넣었을 거야. 그런데 말이야, 혹시 네 사이트가 거대한 인터넷 세상에서 혼자만 존재하는 '외딴 섬' 같다는 생각, 해본 적 없어?
사용자들은 브라우저를 통해서만 네 사이트에 접속하고, 데이터는 오직 그 안에서만 맴돌고...
만약 네가 만든 멋진 콘텐츠나 기능을 모바일 앱에서도 보여주고 싶다면? 다른 서비스랑 데이터를 주고받고 싶다면? 아니면, 완전히 새로운 방식의 프론트엔드(React나 Vue.js 같은 걸로!)를 입혀보고 싶다면?
이 모든 상상을 현실로 만들어주는 마법의 열쇠가 바로 **API(Application Programming Interface)**야. 그중에서도 오늘 우리가 깊게 파고들 주제는 바로 **RESTful API**지.
어렵게 들린다고? 걱정 마. "API는 개발자들만 쓰는 거 아냐?" 싶을 수도 있지만, 줌라 4 버전부터는 이게 엄청나게 쉬워졌어. 마치 잘 차려진 뷔페처럼, 우리가 쓸 수 있는 기능들이 이미 준비되어 있거든. 우리는 그냥 접시를 들고 가서 원하는 음식을 담기만 하면 돼.
이 글을 끝까지 따라오면, 너는 더 이상 웹사이트라는 '섬'에 갇혀 있지 않게 될 거야. 네 줌라 사이트는 이제 세상의 다른 서비스들과 자유롭게 대화하고 데이터를 주고받는 '허브'가 될 수 있어. 모바일 앱, 외부 서비스 연동, 심지어 IoT 기기까지! 가능성은 무한해.
자, 그럼 이제부터 줌라라는 강력한 CMS에 RESTful API라는 날개를 달아주는 여정을 함께 떠나보자고. 커피 한 잔 준비하고, 편하게 따라와 봐. 네 개발 레벨이 한 단계, 아니 몇 단계는 훌쩍 뛰어오를 테니까!
1. RESTful API, 대체 정체가 뭐야? (개발자스럽지 않게 이해하기)
자, RESTful API. 이름부터 뭔가 복잡해 보이지? 괜찮아. 최대한 쉽게 풀어줄게.
**API**는 그냥 '약속'이야. 프로그램끼리 어떻게 대화할지 정해놓은 규칙이지. 예를 들어, 네가 식당에 갔다고 상상해봐. 너(클라이언트)는 웨이터(API)에게 "김치찌개 주세요"(요청)라고 말하지. 그럼 웨이터는 주방(서버)에 가서 네 주문을 전달하고, 완성된 김치찌개(응답)를 너에게 가져다줘. 여기서 너와 웨이터 사이의 주문 방식(메뉴판 보고 주문하기, 말로 하기 등)이 바로 API야.
그럼 **REST**는 뭐냐? 'Representational State Transfer'의 약자인데... 이런 건 몰라도 돼! 중요한 건 '어떻게' 대화할지에 대한 *스타일* 중 하나라는 거야. 수많은 API 스타일 중에서 현재 웹에서 가장 인기 있고 널리 쓰이는 '대세' 스타일이라고 생각하면 편해.
REST 스타일은 몇 가지 간단한 원칙을 따라.
- 자원(Resource): 모든 건 '자원'으로 표현돼. 네 웹사이트의 '게시글', '사용자', '상품' 같은 것들이 전부 자원이야. 그리고 각 자원에는 고유한 주소(URI)가 있어. 예를 들면 `/articles/123` 처럼.
- 행위(Verb): 이 자원에 대해 뭘 할 건지는 'HTTP 메소드'라는 동사로 표현해.
- **GET**: 자원을 조회해줘. (게시글 123번 보여줘!)
- **POST**: 자원을 생성해줘. (새로운 게시글 하나 만들어줘!)
- **PUT** / **PATCH**: 자원을 수정해줘. (게시글 123번 내용 바꿔줘!)
- **DELETE**: 자원을 삭제해줘. (게시글 123번 지워줘!)
- 표현(Representation): 자원을 주고받을 때는 특정 형식으로 표현해서 줘. 요즘은 거의 대부분 **JSON(JavaScript Object Notation)**이라는 데이터 형식을 사용해. 사람이 읽기도 쉽고, 기계가 처리하기도 편해서 국룰이 됐지.
정리해볼까? RESTful API는 **"특정 주소(URI)로 표현된 자원(Resource)에 대해 HTTP 메소드(Verb)를 사용해 요청을 보내면, 서버가 처리 결과를 JSON 형태(Representation)로 응답해주는 통신 방식"** 이라고 할 수 있어. 어때, 좀 감이 와?
2. 줌라 4의 축복, 내장 웹 서비스 API 파헤치기
과거 줌라 3 시절에는 API를 구현하려면 com_api 같은 서드파티 확장을 쓰거나, 직접 처음부터 다 만들어야 했어. 정말 눈물 나는 과정이었지... 하지만 줌라 4가 등장하면서 모든 게 바뀌었어. **핵심 기능으로 웹 서비스 API가 내장**되었거든! 이건 정말 혁명적인 변화야.
줌라 4+의 내장 API는 우리가 위에서 배운 REST 원칙을 아주 충실하게 따르고 있어. 덕분에 우리는 복잡한 밑바닥 설정 없이 바로 API를 사용할 수 있게 됐지.
(1) 기본 설정: API의 문을 열어보자
자, 이제 실전이야. 네 줌라 사이트 관리자 페이지로 로그인해봐. 그리고 딱 몇 번의 클릭만으로 API를 활성화할 수 있어.
1단계: API 플러그인 활성화
먼저 API가 요청을 받을 수 있도록 문을 열어줘야 해.
관리자 메뉴에서 **시스템 > 관리 > 플러그인**으로 이동해.
검색창에 'api'라고 검색하면 `웹 서비스 - API (webservice)`라는 플러그인이 보일 거야. 이걸 활성화(체크하고 '활성화' 버튼 클릭)해줘.
2단계: 인증 플러그인 활성화
API는 아무나 막 쓰면 안 되겠지? 그래서 '인증'이라는 과정이 필요해. 줌라는 기본적으로 토큰 기반 인증을 제공해.
같은 플러그인 관리자 화면에서 `인증 - API 토큰 (token)` 플러그인을 찾아서 활성화해줘.
3단계: API 전용 사용자 생성 및 토큰 발급
이제 API를 사용할 '사용자'를 만들고, 그 사용자에게 '출입증(토큰)'을 발급해 줄 차례야.
**사용자 > 관리 > 새로 만들기**로 가서 새 사용자를 만들어. 이때 중요한 건, 이 사용자를 **Super Users 그룹**에 할당하는 거야. 그래야 모든 API 기능에 접근할 수 있거든. (물론 실제 서비스에서는 보안을 위해 최소한의 권한만 가진 그룹을 따로 만드는 게 좋아! 이건 나중에 더 자세히 다룰게.)
사용자를 저장하고 나면, 다시 해당 사용자를 클릭해서 편집 화면으로 들어가 봐. 그럼 상단에 **'API 토큰'** 탭이 새로 생겼을 거야. 클릭!
여기서 토큰을 활성화하고, 필요하다면 만료 날짜를 설정할 수 있어. 저장하면 짜잔! 아래에 긴 문자열의 **API 토큰**이 나타날 거야. 이게 바로 너의 API 출입증이야. 절대 외부에 노출하면 안 돼! 어딘가에 잘 복사해둬.
# 발급된 토큰 예시 (실제로는 훨씬 더 길어)
# Bearer 7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
이 토큰은 앞으로 API를 호출할 때마다 HTTP 헤더에 넣어서 보내야 해. "나야, 들어갈 자격 있어!"라고 증명하는 셈이지.
(2) 첫 API 호출: "똑똑, 계십니까?"
모든 준비는 끝났어. 이제 진짜로 API를 호출해서 줌라와 대화를 나눠보자. API 테스트에는 여러 도구가 있지만, 개발자들의 국민템인 **Postman**이나 간단한 명령어 도구인 **cURL**을 사용해볼게.
줌라의 기본 API 주소는 `[네 사이트 주소]/api/v1` 이야. `v1`은 API 버전 1을 의미해. 나중에 큰 변화가 생기면 `v2`, `v3`로 올라갈 수 있겠지.
자, cURL을 사용해서 사이트에 있는 모든 게시글 목록을 가져와 볼까? 터미널을 열고 아래 명령어를 입력해봐. `[네 사이트 주소]`와 `[네 API 토큰]` 부분은 네 정보로 바꿔야 해.
curl -X GET "[네 사이트 주소]/api/v1/content/articles" \
-H "Authorization: Bearer [네 API 토큰]" \
-H "Content-Type: application/json"
이 명령어의 의미를 하나씩 뜯어보자.
- `curl`: cURL을 실행하라는 명령어.
- `-X GET`: HTTP 메소드는 GET을 사용하겠다. (조회)
- `[네 사이트 주소]/api/v1/content/articles`: 요청을 보낼 주소. `content/articles`가 바로 '게시글'이라는 자원을 가리키는 주소야.
- `-H "Authorization: Bearer [네 API 토큰]"`: HTTP 헤더에 인증 정보를 추가. "이 토큰을 가진 사람이니 들여보내 줘"라는 뜻.
- `-H "Content-Type: application/json"`: 내가 보내는 요청의 본문 형식(지금은 없지만)과 받고 싶은 응답 형식이 JSON이라는 걸 알려주는 거야.
엔터를 딱! 치면, 아마 이런 식의 응답이 화면에 주르륵 나타날 거야. (네 사이트에 있는 게시글 데이터가 JSON 형태로!)
{
"data": [
{
"type": "articles",
"id": "1",
"attributes": {
"id": 1,
"asset_id": 55,
"title": "줌라 시작하기",
"alias": "joomla-getting-started",
"introtext": "<p>줌라에 오신 것을 환영합니다!</p>",
"fulltext": "",
"state": 1,
"catid": 2,
"created": "2023-10-27T10:00:00+09:00",
"created_by": 811,
"created_by_alias": "",
... // 엄청 많은 필드들
},
"links": {
"self": "https://example.com/api/v1/content/articles/1"
}
},
{
"type": "articles",
"id": "2",
... // 다음 게시글 정보
}
],
"links": {
"self": "https://example.com/api/v1/content/articles?page[offset]=0&page[limit]=50",
"first": "https://example.com/api/v1/content/articles?page[offset]=0&page[limit]=50",
"last": "https://example.com/api/v1/content/articles?page[offset]=0&page[limit]=50"
}
}
와! 성공이야! 네가 방금 코드를 통해 줌라 사이트의 데이터를 외부에서 '읽어온' 거야. 이게 바로 API의 첫걸음이자 가장 중요한 순간이야. 이제 너는 브라우저 없이도 줌라의 데이터에 접근할 수 있게 됐어.
3. 나만의 API 만들기: 커스텀 엔드포인트 개발
기본 API만으로도 할 수 있는 게 많지만, 진짜 재미는 지금부터야. 줌라가 기본으로 제공하지 않는, 나만의 기능을 가진 API를 직접 만들어 볼 거야. 이걸 **'커스텀 엔드포인트(Custom Endpoint)'**를 만든다고 해.
예를 들어, "최근 댓글 5개만 딱 가져오는 API"나 "우리 회사 제품 목록만 특별한 형식으로 보여주는 API" 같은 걸 만들 수 있겠지. 이걸 만들려면 줌라의 확장 기능, 그중에서도 '플러그인'을 개발해야 해.
겁먹지 마. 생각보다 간단해. 줌라가 API 개발을 위한 뼈대를 아주 잘 만들어 놨거든. 우리는 정해진 위치에 파일 몇 개만 만들어서 넣어주면 돼.
(1) API 플러그인 구조 훑어보기
우리는 `api-authentication` 타입의 플러그인을 만들 거야. "어? 인증 플러그인이라고? 우린 기능 만들 건데?" 싶을 수 있어. 맞아, 이름이 좀 헷갈리는데, 줌라 4 초기에는 커스텀 API 라우트를 추가하는 가장 간단한 방법이 이 플러그인을 통하는 거였어. 지금도 여전히 유효하고 직관적인 방법이야.
플러그인의 기본 폴더 구조는 이래.
/plugins
└── /api-authentication
└── /myapi // 우리 플러그인 이름
├── myapi.php // 플러그인 진입 파일
├── myapi.xml // 플러그인 설정 파일 (매니페스트)
└── /services
└── routes.php // API 라우트를 정의하는 파일! (핵심)
└── /src
└── Controller
└── CustomController.php // API 요청을 실제로 처리하는 컨트롤러
딱 이 구조만 기억하면 돼. `myapi`는 네가 원하는 이름으로 바꾸면 되고. 중요한 파일은 `myapi.xml`, `routes.php`, 그리고 우리가 만들 컨트롤러 파일이야.
(2) 실전! '오늘의 명언' API 만들기
백문이 불여일견! 간단한 '오늘의 명언'을 반환하는 API를 직접 만들어보자. 이 API는 `GET /api/v1/myapi/quote` 주소로 요청하면, 랜덤 명언을 JSON으로 던져줄 거야.
1단계: 플러그인 기본 파일 생성
먼저 네 컴퓨터에 `myapi`라는 폴더를 만들고, 그 안에 아래 파일들을 생성해줘.
`myapi.xml` (플러그인 설정 파일)
이 파일은 줌라에게 "나 이런 플러그인이야!"라고 소개하는 명함 같은 거야.
<?xml version="1.0" encoding="utf-8"?>
<extension version="4.0" type="plugin" group="api-authentication" method="upgrade">
<name>plg_api-authentication_myapi</name>
<author>Your Name</author>
<creationDate>October 2023</creationDate>
<copyright>Copyright (C) 2023 Your Name. All rights reserved.</copyright>
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
<authorEmail>your.email@example.com</authorEmail>
<authorUrl>your.website.com</authorUrl>
<version>1.0.0</version>
<description>PLG_API-AUTHENTICATION_MYAPI_XML_DESCRIPTION</description>
<files>
<filename plugin="myapi">myapi.php</filename>
<folder>services</folder>
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_api-authentication_myapi.ini</language>
<language tag="en-GB">language/en-GB/plg_api-authentication_myapi.sys.ini</language>
</languages>
</extension>
다른 건 그냥 정보고, `
`myapi.php` (플러그인 진입 파일)
이 파일은 아주 간단해. 그냥 플러그인이 존재한다는 걸 알려주는 역할만 할 거야.
<?php
/**
* @package Joomla.Plugin
* @subpackage Api-authentication.Myapi
*
* @copyright Copyright (C) 2023 Your Name. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
`defined('_JEXEC') or die;`는 줌라를 통하지 않고 이 파일에 직접 접근하는 걸 막는 보안 코드야. 줌라 개발의 기본 중의 기본!
2단계: API 라우트 정의하기
이제 가장 핵심적인 부분이야. 어떤 주소로 요청이 왔을 때, 어떤 컨트롤러를 실행시킬지 연결해주는 '라우팅' 정보를 만들 거야. `services/routes.php` 파일을 만들고 아래처럼 작성해.
`services/routes.php`
<?php
/**
* @package Joomla.Plugin
* @subpackage Api-authentication.Myapi
*
* @copyright Copyright (C) 2023 Your Name. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
use Joomla\CMS\Router\ApiRouter;
use Joomla\CMS\Factory;
defined('_JEXEC') or die;
return function (ApiRouter $router) {
$router->get(
'/v1/myapi/quote', // 우리가 만들 API 주소
new \Joomla\CMS\MVC\Controller\ApiController(
'Myapi.Custom', // 컨트롤러 이름
Factory::getApplication(),
null,
['task' => 'getQuote'] // 실행할 메소드 이름
),
['authentication' => 'optional'] // 인증 방식 (optional은 토큰 없어도 접근 가능)
);
};
코드가 좀 복잡해 보이지만, 핵심은 `$router->get(...)` 부분이야.
- 첫 번째 인자 `'/v1/myapi/quote'`: 이 주소로 GET 요청이 오면,
- 두 번째 인자 `new ...`: `Myapi.Custom` 컨트롤러의 `getQuote` 라는 메소드를 실행시켜라!
- 세 번째 인자 `['authentication' => 'optional']`: 인증은 선택 사항이다. 즉, 토큰 없이도 호출할 수 있게 해주는 설정이야. 만약 `'optional'`을 빼면 기본값인 `'required'`가 적용돼서 반드시 유효한 토큰이 있어야만 접근할 수 있어.
3단계: 요청을 처리할 컨트롤러 만들기
라우트가 "이 주소로 가면 저 컨트롤러를 실행해!"라고 알려줬으니, 이제 실제로 일을 처리할 '컨트롤러'를 만들어야지. `src/Controller/CustomController.php` 파일을 만들자.
`src/Controller/CustomController.php`
<?php
namespace Myapi\Plugin\ApiAuthentication\Myapi\Controller;
use Joomla\CMS\MVC\Controller\ApiController;
defined('_JEXEC') or die;
class CustomController extends ApiController
{
/**
* The content type of the response.
*
* @var string
*/
protected $contentType = 'application/json';
/**
* The default view for the display task.
*
* @var string
*/
protected $default_view = 'quote';
public function getQuote()
{
$quotes = [
"The only way to do great work is to love what you do. - Steve Jobs",
"Life is what happens when you're busy making other plans. - John Lennon",
"The future belongs to those who believe in the beauty of their dreams. - Eleanor Roosevelt",
"Strive not to be a success, but rather to be of value. - Albert Einstein"
];
$randomQuote = $quotes[array_rand($quotes)];
$response = [
'success' => true,
'message' => 'Here is your quote of the day!',
'data' => [
'quote' => $randomQuote
]
];
// 응답을 JSON 형태로 만들어서 반환
$this->app->getDocument()->setMimeEncoding('application/json');
$this->app->setHeader('Content-Type', $this->contentType . '; charset=utf-8');
echo json_encode($response);
return true;
}
}
자, 여기서 무슨 일이 일어나는지 보자.
- `class CustomController extends ApiController`: 줌라의 `ApiController`를 상속받아서 만들어. 그래야 API 관련 기능들을 편하게 쓸 수 있어.
- `public function getQuote()`: 이게 바로 `routes.php`에서 지정한 `getQuote` 메소드야. 이 안의 코드가 실제로 실행되지.
- 명언 배열(`$quotes`)을 만들고, `array_rand` 함수로 랜덤하게 하나를 뽑아.
- `$response` 배열에 성공 여부, 메시지, 그리고 실제 데이터(명언)를 담아. 이렇게 일관된 형식으로 응답을 주는 게 좋아.
- 마지막으로 `json_encode`를 사용해서 이 배열을 JSON 문자열로 변환한 뒤 `echo`로 출력하면 끝!
4단계: 플러그인 설치 및 테스트
이제 우리가 만든 파일들을 모두 담은 `myapi` 폴더를 zip 파일로 압축해. 이름은 `plg_api-authentication_myapi.zip` 처럼 짓는 게 국룰이야.
줌라 관리자 페이지에서 **시스템 > 설치 > 확장**으로 가서 이 zip 파일을 업로드하고 설치해.
설치가 끝나면 **시스템 > 관리 > 플러그인**으로 가서 `myapi`를 검색한 뒤, 우리가 만든 'API Authentication - myapi' 플러그인을 **활성화**하는 걸 잊지 마!
이제 모든 준비가 끝났어. 다시 cURL이나 Postman으로 우리가 만든 주소에 요청을 보내보자!
curl -X GET "[네 사이트 주소]/api/v1/myapi/quote"
이번엔 인증 헤더가 필요 없어. `optional`로 설정했으니까. 요청을 보내면...?
{
"success": true,
"message": "Here is your quote of the day!",
"data": {
"quote": "The only way to do great work is to love what you do. - Steve Jobs"
}
}
짜잔! 우리가 만든 명언 API가 멋지게 작동하는 걸 볼 수 있어. 요청할 때마다 다른 명언이 나올 거야. 축하해! 너는 방금 줌라에서 너만의 첫 커스텀 API를 성공적으로 만들어낸 거야. 이 작은 성공이 앞으로 네가 만들어갈 무한한 가능성의 시작점이 될 거야. 이런 기술을 잘 익혀두면 **재능넷** 같은 플랫폼에서 웹 개발 전문가로 활동할 때 아주 강력한 무기가 될 수 있겠지?
4. CRUD 마스터하기: 데이터 생성, 수정, 삭제 API
자, 이제 읽기(Read)는 마스터했어. 하지만 진짜 웹 서비스는 데이터를 읽기만 하진 않지. 새로운 데이터를 만들고(Create), 기존 데이터를 수정하고(Update), 필요 없는 건 지워야(Delete) 해. 이 네 가지를 합쳐서 우리는 **CRUD**라고 불러.
우리 '오늘의 명언' API를 좀 더 발전시켜 볼까? 이제 명언을 그냥 보여주는 게 아니라, 사용자가 직접 명언을 추가(POST), 수정(PATCH), 삭제(DELETE)할 수 있는 완전한 명언 관리 API로 만들어 볼 거야.
이를 위해서는 데이터를 어딘가에 저장해야겠지? 간단하게 파일에 저장하는 방식으로 구현해 볼게. (실제 서비스에서는 당연히 데이터베이스에 저장해야 하지만, 여기서는 개념 이해를 위해 단순화하자!)
(1) 데이터 저장소 준비
플러그인 폴더 안에 `data`라는 폴더를 만들고, 그 안에 `quotes.json`이라는 빈 파일을 만들어줘. 앞으로 이 파일이 우리의 작은 데이터베이스가 될 거야. 초기 내용은 이렇게 넣어두자.
`data/quotes.json`
[
{ "id": 1, "text": "The only way to do great work is to love what you do. - Steve Jobs" },
{ "id": 2, "text": "Life is what happens when you're busy making other plans. - John Lennon" }
]
(2) 라우트 확장: POST, PATCH, DELETE 추가
이제 `services/routes.php` 파일을 열어서 새로운 라우트들을 추가할 거야. `GET`만 있던 파일에 `POST`, `PATCH`, `DELETE`를 추가해서 완전한 CRUD 라우팅을 완성하자.
`services/routes.php` (수정)
<?php
// ... (파일 상단은 동일) ...
use Joomla\CMS\Router\ApiRouter;
use Joomla\CMS\Factory;
defined('_JEXEC') or die;
return function (ApiRouter $router) {
$controller = new \Joomla\CMS\MVC\Controller\ApiController(
'Myapi.Custom',
Factory::getApplication()
);
// [GET] 모든 명언 목록 가져오기
$router->get('/v1/myapi/quotes', $controller, ['task' => 'getQuotes']);
// [GET] 특정 ID의 명언 가져오기
$router->get('/v1/myapi/quotes/{id}', $controller, ['task' => 'getQuote']);
// [POST] 새 명언 추가하기
$router->post('/v1/myapi/quotes', $controller, ['task' => 'createQuote']);
// [PATCH] 특정 ID의 명언 수정하기
$router->patch('/v1/myapi/quotes/{id}', $controller, ['task' => 'updateQuote']);
// [DELETE] 특정 ID의 명언 삭제하기
$router->delete('/v1/myapi/quotes/{id}', $controller, ['task' => 'deleteQuote']);
};
와, 코드가 훨씬 깔끔해졌지? 컨트롤러 인스턴스를 한 번 만들어서 재사용하고, 각 메소드에 맞는 라우트를 연결해줬어. 여기서 `{id}`는 '변수'를 의미해. `/v1/myapi/quotes/3`으로 요청이 오면, 저 `{id}` 부분에 `3`이 담겨서 컨트롤러로 전달돼.
그리고 이번엔 인증 설정을 뺐어. 이러면 기본값인 `'required'`가 적용돼서, 이제부터 이 API들을 호출하려면 반드시 **유효한 API 토큰**이 필요해. 데이터를 조작하는 기능이니까 보안이 중요하잖아!
(3) 컨트롤러 로직 구현: CRUD의 심장
이제 `src/Controller/CustomController.php`를 대대적으로 수정할 시간이야. 라우트에서 지정한 `getQuotes`, `getQuote`, `createQuote`, `updateQuote`, `deleteQuote` 메소드를 모두 구현해야 해.
`src/Controller/CustomController.php` (대규모 업데이트)
<?php
namespace Myapi\Plugin\ApiAuthentication\Myapi\Controller;
use Joomla\CMS\MVC\Controller\ApiController;
use Joomla\CMS\Factory;
use Joomla\CMS\Response\JsonResponse; // JsonResponse를 사용하면 더 편해!
defined('_JEXEC') or die;
class CustomController extends ApiController
{
protected $contentType = 'application/json';
private $quotesFile;
public function __construct($config = [])
{
parent::__construct($config);
// 데이터 파일 경로 설정
$this->quotesFile = JPATH_PLUGINS . '/api-authentication/myapi/data/quotes.json';
}
// [Helper] 파일에서 명언 읽어오기
private function readQuotes()
{
if (!file_exists($this->quotesFile)) {
return [];
}
$json = file_get_contents($this->quotesFile);
return json_decode($json, true);
}
// [Helper] 파일에 명언 저장하기
private function writeQuotes(array $quotes)
{
file_put_contents($this->quotesFile, json_encode($quotes, JSON_PRETTY_PRINT));
}
// [GET] /quotes - 모든 명언 조회
public function getQuotes()
{
$quotes = $this->readQuotes();
$this->app->dispatch(new JsonResponse($quotes));
}
// [GET] /quotes/{id} - 특정 명언 조회
public function getQuote()
{
$id = $this->input->get('id', 0, 'int');
$quotes = $this->readQuotes();
foreach ($quotes as $quote) {
if ($quote['id'] == $id) {
$this->app->dispatch(new JsonResponse($quote));
return;
}
}
$this->app->dispatch(new JsonResponse(['error' => 'Quote not found'], 404));
}
// [POST] /quotes - 새 명언 생성
public function createQuote()
{
$input = $this->app->input->json->getArray();
$text = $input['text'] ?? '';
if (empty($text)) {
$this->app->dispatch(new JsonResponse(['error' => 'Text field is required'], 400));
return;
}
$quotes = $this->readQuotes();
$newId = count($quotes) > 0 ? max(array_column($quotes, 'id')) + 1 : 1;
$newQuote = ['id' => $newId, 'text' => $text];
$quotes[] = $newQuote;
$this->writeQuotes($quotes);
$this->app->dispatch(new JsonResponse($newQuote, 201)); // 201 Created
}
// [PATCH] /quotes/{id} - 명언 수정
public function updateQuote()
{
$id = $this->input->get('id', 0, 'int');
$input = $this->app->input->json->getArray();
$text = $input['text'] ?? '';
if (empty($text)) {
$this->app->dispatch(new JsonResponse(['error' => 'Text field is required'], 400));
return;
}
$quotes = $this->readQuotes();
$found = false;
foreach ($quotes as &$quote) {
if ($quote['id'] == $id) {
$quote['text'] = $text;
$found = true;
break;
}
}
if ($found) {
$this->writeQuotes($quotes);
$this->app->dispatch(new JsonResponse(array_values(array_filter($quotes, fn($q) => $q['id'] == $id))[0]));
} else {
$this->app->dispatch(new JsonResponse(['error' => 'Quote not found'], 404));
}
}
// [DELETE] /quotes/{id} - 명언 삭제
public function deleteQuote()
{
$id = $this->input->get('id', 0, 'int');
$quotes = $this->readQuotes();
$initialCount = count($quotes);
$quotes = array_values(array_filter($quotes, fn($q) => $q['id'] != $id));
if (count($quotes) < $initialCount) {
$this->writeQuotes($quotes);
$this->app->dispatch(new JsonResponse(null, 204)); // 204 No Content
} else {
$this->app->dispatch(new JsonResponse(['error' => 'Quote not found'], 404));
}
}
}
코드가 길어졌지만 겁먹지 마. 하나씩 보면 별거 아니야.
- `__construct()`: 생성자에서 데이터 파일 경로를 미리 설정해둬.
- `readQuotes()`, `writeQuotes()`: 파일 읽고 쓰는 로직을 헬퍼 메소드로 분리해서 코드를 깔끔하게 만들었어.
- `getQuotes()`: 모든 명언을 읽어서 `JsonResponse`로 감싸서 응답. `JsonResponse`를 쓰면 헤더 설정 같은 걸 알아서 해줘서 편해.
- `getQuote()`: URL에서 `{id}` 값을 `$this->input->get('id')`로 받아와. 해당 ID의 명언을 찾아보고, 있으면 데이터 응답, 없으면 404 Not Found 에러를 응답해.
- `createQuote()`: 요청 본문(body)에 담겨온 JSON 데이터를 `$this->app->input->json->getArray()`로 받아와. 새 ID를 만들고, 명언 배열에 추가한 뒤 파일에 저장. 성공하면 201 Created 상태 코드와 함께 생성된 데이터를 응답.
- `updateQuote()`: ID와 수정할 내용을 받아서 해당 명언을 찾아 내용을 바꾼 뒤 저장.
- `deleteQuote()`: ID를 받아서 해당 명언을 배열에서 제거하고 저장. 성공하면 내용이 없다는 의미의 204 No Content 상태 코드를 응답.
이렇게 각 상황에 맞는 **HTTP 상태 코드**를 적절히 사용해주는 게 좋은 REST API 설계의 핵심이야!
(4) CRUD API 테스트
자, 이제 플러그인을 다시 zip으로 압축해서 줌라에 업데이트(설치 화면에서 그냥 덮어쓰면 돼)하고, Postman이나 cURL로 테스트해볼 시간이야. 잊지 마, 이제부터는 모든 요청에 `Authorization` 헤더를 포함해야 해!
1. 새 명언 추가 (POST)
curl -X POST "[네 사이트 주소]/api/v1/myapi/quotes" \
-H "Authorization: Bearer [네 API 토큰]" \
-H "Content-Type: application/json" \
-d '{"text": "Simplicity is the ultimate sophistication. - Leonardo da Vinci"}'
성공하면 `201 Created` 응답과 함께 `id: 3`인 새 명언 데이터가 보일 거야.
2. 명언 목록 조회 (GET)
curl -X GET "[네 사이트 주소]/api/v1/myapi/quotes" \
-H "Authorization: Bearer [네 API 토큰]"
방금 추가한 명언까지 포함된 3개의 명언 목록이 쫙 나오겠지.
3. 명언 수정 (PATCH)
curl -X PATCH "[네 사이트 주소]/api/v1/myapi/quotes/3" \
-H "Authorization: Bearer [네 API 토큰]" \
-H "Content-Type: application/json" \
-d '{"text": "Simplicity is the key. - Leonardo da Vinci (edited)"}'
ID가 3인 명언의 내용이 바뀔 거야.
4. 명언 삭제 (DELETE)
curl -X DELETE "[네 사이트 주소]/api/v1/myapi/quotes/1" \
-H "Authorization: Bearer [네 API 토큰]"
ID가 1인 명언을 삭제해. 성공하면 `204 No Content` 응답이 오고 아무 내용도 출력되지 않을 거야.
어때? 이제 너는 줌라를 이용해서 완벽한 CRUD 기능을 갖춘 RESTful API를 만들 수 있게 된 거야. 이 원리만 이해하면, 명언이 아니라 게시글, 상품, 회원 정보 등 어떤 데이터든 자유자재로 다룰 수 있어. 데이터 저장 방식을 파일이 아닌 실제 줌라 데이터베이스(`JFactory::getDbo()`)와 연결하면, 진짜 강력한 애플리케이션을 만들 수 있는 거지.
5. 보안과 권한 제어 (ACL): 우리 집 문단속하기
지금까지 만든 API는 토큰만 있으면 누구나 명언을 추가하고, 수정하고, 삭제할 수 있었어. 하지만 실제 서비스에서는 어떨까? "관리자만 명언을 등록할 수 있게", "편집자 그룹은 수정만 가능하게", "일반 회원은 보기만 가능하게" 와 같이 세밀한 권한 제어가 필요하겠지.
이럴 때 사용하는 것이 바로 줌라의 강력한 기능인 **ACL(Access Control List)** 이야.
줌라의 모든 기능은 ACL을 통해 권한을 제어할 수 있고, 다행히도 우리가 만든 API 컨트롤러 안에서도 이 ACL을 아주 쉽게 사용할 수 있어.
컨트롤러에 권한 확인 로직 추가하기
예를 들어, 명언을 생성(`createQuote`)하는 것은 'Super Users' 그룹에 속한 사용자만 가능하도록 만들어보자. `CustomController.php`의 `createQuote` 메소드 맨 위에 아래 코드를 추가하면 돼.
// CustomController.php의 createQuote() 메소드 시작 부분
public function createQuote()
{
// 현재 API를 호출한 사용자의 정보를 가져온다.
$user = Factory::getApplication()->getIdentity();
// 사용자가 'core.create' 권한을 가지고 있는지 확인한다.
// 'com_content'는 예시. 실제로는 우리 컴포넌트의 권한을 체크해야 한다.
if (!$user->authorise('core.create', 'com_content'))
{
// 권한이 없으면 403 Forbidden 에러를 반환한다.
$this->app->dispatch(new JsonResponse(['error' => 'You are not authorized to create quotes.'], 403));
return;
}
// ... (기존의 명언 생성 로직) ...
}
핵심은 `$user->authorise('액션', '컴포넌트')` 메소드야.
- `$user = Factory::getApplication()->getIdentity();`: API 토큰을 통해 인증된 사용자의 객체를 가져와.
- `$user->authorise('core.create', 'com_content')`: 이 사용자가 `com_content`(줌라 게시글 컴포넌트)에 대해 `core.create`(생성) 권한을 가지고 있는지 확인해.
- 만약 권한이 없다면, "너는 권한이 없어!"라는 의미의 403 Forbidden 에러를 응답하고 함수를 종료해버리는 거야.
마찬가지로 수정은 `core.edit`, 삭제는 `core.delete` 권한을 체크하면 되겠지?
// updateQuote 메소드에 추가
if (!$user->authorise('core.edit', 'com_content')) { /* 403 에러 처리 */ }
// deleteQuote 메소드에 추가
if (!$user->authorise('core.delete', 'com_content')) { /* 403 에러 처리 */ }
이렇게 줌라의 ACL 시스템과 API를 연동하면, 정말 안전하고 유연한 웹 서비스를 구축할 수 있어. 보안은 아무리 강조해도 지나치지 않으니, 데이터를 다루는 API를 만들 때는 반드시 권한 제어 로직을 넣는 습관을 들이자!
마치며: 이제 당신은 '연결'의 마법사
와, 정말 긴 여정이었어. 여기까지 따라오느라 정말 고생 많았어! 하지만 이제 너는 더 이상 예전의 네가 아니야.
너는 이제 줌라 웹사이트를 '외딴 섬'이 아닌, 세상과 소통하는 '허브'로 만들 수 있는 강력한 기술을 손에 넣었어. RESTful API의 개념을 이해했고, 줌라에 내장된 API를 활성화해서 사용해봤고, 심지어 CRUD 기능을 모두 갖춘 너만의 커스텀 API를 직접 개발하고 보안까지 적용해봤지.
이게 뭘 의미할까?
- 네 줌라 사이트의 데이터를 기반으로 멋진 **모바일 앱**을 만들 수 있어.
- **React, Vue, Svelte** 같은 최신 프론트엔드 기술로 완전히 새로운 사용자 경험을 제공하는 **헤드리스(Headless) CMS**를 구축할 수 있어.
- 다른 웹 서비스(결제 시스템, CRM, 소셜 미디어 등)와 데이터를 주고받으며 **서비스를 확장**할 수 있어.
- 반복적인 작업을 자동화하는 **스크립트**를 만들 수도 있지.
가능성은 정말 무궁무진해. 오늘 배운 것은 그 가능성을 여는 첫 번째 열쇠일 뿐이야. 앞으로 이 기술을 어떻게 활용하고 발전시켜 나갈지는 전적으로 너의 상상력과 노력에 달려있어. 혹시라도 개발하다 막히는 부분이 있다면, 혼자 끙끙 앓지 말고 줌라 커뮤니티나 **재능넷** 같은 전문가 플랫폼에서 도움을 구하는 것도 좋은 방법이야.
두려워하지 말고, 계속해서 새로운 것을 시도하고 만들어봐. 코드는 세상을 바꾸는 가장 창의적인 도구 중 하나니까. 오늘 네가 배운 이 '연결'의 마법으로, 더 멋지고 새로운 가치를 만들어내길 진심으로 응원할게!
Happy Coding!
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.