EP11. Supabase Edge Functions Architecture
서비스가 성장하면서 코드가 많아지더라도 코드의 복잡도를 낮춰 유지보수 가능한 서비스가 될 수 있도록 구조를 설계합니다.
디자인패턴, 아키텍처는 취향이며 정답은 없습니다. 더 옳다고 판단되는 설계가 있다면 다른 설계로 적용하셔도 됩니다.
강의에서는 ‘레이어드 아키텍처’를 적용합니다.
추후 Push Notification기능 구현을 위해 Push Token을 관리하는 API를 설계해보면서 어떻게 구조를 가져가야 하는지 살펴봅니다.
1. 레이어드 아키텍처(Layered Architecture)
1.
Router: 요청을 수신하고 적절한 컨트롤러로 라우팅합니다.
2.
Controller: 사용자 입력을 처리하고 서비스 계층을 호출하여 비즈니스 로직을 실행합니다.
3.
Service: 비즈니스 로직을 구현합니다. 이 계층에서는 주로 애플리케이션의 핵심 기능이 구현됩니다.
4.
Repository: 데이터베이스와의 상호작용을 처리합니다. 데이터 저장소와 관련된 작업을 캡슐화합니다.
2. 디렉토리 구조
FCM Token은 유저 정보의 하나이므로 유저 정보를 다루는 Router, Controller, Service, Repository를 추가합니다.
3. 코드 구현
하위 Repository 부터 상위 Controller, 그리고 index 순으로 코드를 구현합니다.
1.
api/repositories/userRepository.ts
export class UserRepository {
// userID를 이용해 해당 유저의 FCM 토큰 목록을 반환하는 함수
async getFCMTokensByUserID(userID: string): Promise<string[]> {
// TODO: 다음 Supabase Database 강의에서 구현
// FCM 토큰 배열 반환
return ["token_1", "token_2", "token_3", "token_4"];
}
// userID와 fcmToken을 인풋으로 받아서 DB에 추가하는 함수
async addFCMToken(userID: string, fcmToken: string): Promise<boolean> {
// TODO: 다음 Supabase Database 강의에서 구현
return true;
}
// userID와 fcmToken을 인풋으로 받아서 DB에서 삭제하는 함수
async deleteFCMToken(userID: string, fcmToken: string): Promise<boolean> {
// TODO: 다음 Supabase Database 강의에서 구현
return true;
}
}
TypeScript
복사
Database 레벨에 접근해 데이터를 CRUD 처리합니다. 아직 Supabase Database는 다루지 않았으므로 mock data를 반환하도록 합니다.
2.
api/services/userService.ts
import { UserRepository } from "./../repositories/userRepository.ts";
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
// userID를 이용해 해당 유저의 FCM 토큰 목록을 반환하는 함수
async getFCMTokensByUserID(userID: string): Promise<string[]> {
return await this.userRepository.getFCMTokensByUserID(userID);
}
// userID와 fcmToken을 인풋으로 받아서 DB에 추가하는 함수
async addFCMToken(userID: string, fcmToken: string): Promise<boolean> {
return await this.userRepository.addFCMToken(userID, fcmToken);
}
// userID와 fcmToken을 인풋으로 받아서 DB에서 삭제하는 함수
async deleteFCMToken(userID: string, fcmToken: string): Promise<boolean> {
return await this.userRepository.deleteFCMToken(userID, fcmToken);
}
}
TypeScript
복사
•
Repository를 소유하며 Repository를 이용해 데이터를 CRUD 합니다.
•
Service는 다수개의 Repository를 가질 수 있고 여러 데이터들을 연산, 가공합니다. 현재 예제의 경우 복잡한 경우가 아니라 Repository와 1:1로만 매칭되어 있습니다.
3.
api/controllers/userController.ts
import { Context } from "https://deno.land/x/hono/mod.ts";
import { UserService } from "../services/userService.ts";
// UserController 클래스는 FCM 토큰을 관리하기 위한 컨트롤러입니다.
export class UserController {
private userService: UserService;
// 생성자에서는 UserService 인스턴스를 초기화합니다.
constructor() {
this.userService = new UserService();
}
// postFCMTokenV1 메서드는 FCM 토큰을 추가하는 API 엔드포인트입니다.
async postFCMTokenV1(c: Context) {
// 요청에서 fcmToken을 추출합니다.
const { fcmToken } = await c.req.json();
// fcmToken이 없는 경우 에러 메시지를 반환합니다.
if (!fcmToken) {
return c.json("Missing userID or fcmToken");
}
// UserService를 통해 FCM 토큰을 추가합니다.
const result = await this.userService.addFCMToken("mock_user_id", fcmToken);
// 결과에 따라 적절한 메시지를 반환합니다.
return c.json(
result ? "FCM token added successfully" : "Failed to add FCM token",
);
}
// getFCMTokensV1 메서드는 특정 사용자의 FCM 토큰들을 가져오는 API 엔드포인트입니다.
async getFCMTokensV1(c: Context) {
// UserService를 통해 사용자 ID에 해당하는 FCM 토큰들을 가져옵니다.
const tokens = await this.userService.getFCMTokensByUserID("mock_user_id");
// 토큰 목록을 JSON 형식으로 반환합니다.
return c.json(tokens);
}
// deleteFCMTokenV1 메서드는 FCM 토큰을 삭제하는 API 엔드포인트입니다.
async deleteFCMTokenV1(c: Context) {
// 요청에서 fcmToken을 추출합니다.
const { fcmToken } = await c.req.json();
// fcmToken이 없는 경우 에러 메시지를 반환합니다.
if (!fcmToken) {
return c.json("Missing userID or fcmToken");
}
// UserService를 통해 FCM 토큰을 삭제합니다.
const result = await this.userService.deleteFCMToken(
"mock_user_id",
fcmToken,
);
// 결과에 따라 적절한 메시지를 반환합니다.
return c.json(
result ? "FCM token deleted successfully" : "Failed to delete FCM token",
);
}
}
TypeScript
복사
4.
api/routers/userRouter.ts
import { UserController } from "./../controllers/userController.ts";
import { Hono } from "https://deno.land/x/hono/mod.ts";
// Hono 인스턴스를 생성하여 라우터를 설정합니다.
const userRouter = new Hono();
// UserController 인스턴스를 생성합니다.
const userController = new UserController();
// POST 요청을 처리하여 FCM 토큰을 추가하는 엔드포인트를 정의합니다.
userRouter.post(
"/v1/fcmToken",
(c) => userController.postFCMTokenV1(c),
);
// GET 요청을 처리하여 특정 사용자의 FCM 토큰 목록을 가져오는 엔드포인트를 정의합니다.
userRouter.get(
"/v1/fcmToken",
(c) => userController.getFCMTokensV1(c),
);
// DELETE 요청을 처리하여 FCM 토큰을 삭제하는 엔드포인트를 정의합니다.
userRouter.delete(
"/v1/fcmToken",
(c) => userController.deleteFCMTokenV1(c),
);
// userRouter를 기본으로 내보냅니다.
export default userRouter;
TypeScript
복사
•
API 버전 관리를 위해 Path에 버전을 추가하고, Controller의 함수에도 버저닝. 1:1 매칭
5.
index.ts
import { Hono } from "https://deno.land/x/hono/mod.ts";
import userRouter from "./routers/userRouter.ts";
// Hono 인스턴스를 생성하여 애플리케이션을 설정합니다.
const app = new Hono();
// 기본 경로를 "/api"로 설정하고, "/users" 경로에 대해 userRouter를 설정합니다.
app.basePath("/api")
.route("/users", userRouter);
// 애플리케이션을 시작하여 요청을 수신합니다.
Deno.serve(app.fetch);
TypeScript
복사
•
{supabase base url}/{api}/{route}/{userRouter 하위 path}
4. 로컬 테스트
1.
GET
2.
POST
3.
DELETE
5. Public 배포
bash deploy.sh
TypeScript
복사
public 배포 후 public API endpoint 테스트도 진행합니다.