๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
SonamuContext๋Š” API ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘์— ํ•„์š”ํ•œ ์ •๋ณด๋“ค์„ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.

Context ๊ฐœ์š”

Request ์ •๋ณด

HTTP ์š”์ฒญ ๋ฐ์ดํ„ฐํ—ค๋”, ๋ฐ”๋””, ์ฟผ๋ฆฌ

Reply ๊ฐ์ฒด

HTTP ์‘๋‹ต ์ œ์–ด์ƒํƒœ ์ฝ”๋“œ, ํ—ค๋”

User ์ •๋ณด

์ธ์ฆ๋œ ์‚ฌ์šฉ์žID, ๊ถŒํ•œ, ์—ญํ• 

ํ™•์žฅ ๊ฐ€๋Šฅ

์ปค์Šคํ…€ ํ•„๋“œ ์ถ”๊ฐ€ํ”„๋กœ์ ํŠธ๋ณ„ ๋ฐ์ดํ„ฐ

SonamuContext ๊ตฌ์กฐ

๊ธฐ๋ณธ ๊ตฌ์กฐ

interface SonamuContext {
  // HTTP Request ๊ฐ์ฒด
  request: FastifyRequest;
  
  // HTTP Reply ๊ฐ์ฒด
  reply: FastifyReply;
  
  // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด (์˜ต์…˜)
  user?: {
    id: number;
    email: string;
    username: string;
    role: string;
    // ํ”„๋กœ์ ํŠธ๋ณ„ ์ถ”๊ฐ€ ํ•„๋“œ
  };
  
  // ์ปค์Šคํ…€ ํ•„๋“œ (ํ”„๋กœ์ ํŠธ๋ณ„๋กœ ํ™•์žฅ)
  [key: string]: any;
}
Sonamu๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ Fastify๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, request์™€ reply๋Š” Fastify์˜ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.

Context ๊ตฌ์„ฑ ์š”์†Œ

1. Request

HTTP ์š”์ฒญ๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(): Promise<User[]> {
    const context = Sonamu.getContext();
    
    // Request ์ •๋ณด ์ ‘๊ทผ
    const headers = context.request.headers;
    const query = context.request.query;
    const body = context.request.body;
    const params = context.request.params;
    const ip = context.request.ip;
    const hostname = context.request.hostname;
    const url = context.request.url;
    const method = context.request.method;
    
    console.log("Request from IP:", ip);
    console.log("User-Agent:", headers["user-agent"]);
    
    // ...
  }
}
์ฃผ์š” ์†์„ฑ:
  • headers: HTTP ํ—ค๋” (๊ฐ์ฒด)
  • query: URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ (๊ฐ์ฒด)
  • body: ์š”์ฒญ ๋ฐ”๋”” (POST/PUT ๋“ฑ)
  • params: URL ๊ฒฝ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ
  • ip: ํด๋ผ์ด์–ธํŠธ IP ์ฃผ์†Œ
  • hostname: ํ˜ธ์ŠคํŠธ๋ช…
  • url: ์š”์ฒญ URL
  • method: HTTP ๋ฉ”์„œ๋“œ

2. Reply

HTTP ์‘๋‹ต์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class UserModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: CreateUserParams): Promise<{ userId: number }> {
    const context = Sonamu.getContext();
    
    // ์‘๋‹ต ํ—ค๋” ์„ค์ •
    context.reply.header("X-Custom-Header", "value");
    
    // ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ ์„ค์ •
    context.reply.status(201); // Created
    
    // ์ฟ ํ‚ค ์„ค์ •
    context.reply.cookie("session_id", "abc123", {
      httpOnly: true,
      secure: true,
      maxAge: 3600000, // 1์‹œ๊ฐ„
    });
    
    // User ์ƒ์„ฑ
    const wdb = this.getPuri("w");
    const [userId] = await wdb
      .table("users")
      .insert(params)
      .returning({ id: "id" });
    
    return { userId: userId.id };
  }
}
์ฃผ์š” ๋ฉ”์„œ๋“œ:
  • status(code): ์ƒํƒœ ์ฝ”๋“œ ์„ค์ •
  • header(name, value): ์‘๋‹ต ํ—ค๋” ์„ค์ •
  • cookie(name, value, options): ์ฟ ํ‚ค ์„ค์ •
  • redirect(url): ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  • send(data): ์‘๋‹ต ์ „์†ก (์ผ๋ฐ˜์ ์œผ๋กœ ์ž๋™)

3. User

์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
interface ContextUser {
  id: number;
  email: string;
  username: string;
  role: "admin" | "manager" | "normal";
  // ํ”„๋กœ์ ํŠธ๋ณ„ ์ถ”๊ฐ€ ํ•„๋“œ
}

class PostModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: PostSaveParams): Promise<{ postId: number }> {
    const context = Sonamu.getContext();
    
    // ์ธ์ฆ ํ™•์ธ
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    // ๊ถŒํ•œ ํ™•์ธ
    if (context.user.role !== "admin" && context.user.role !== "manager") {
      throw new Error("Insufficient permissions");
    }
    
    const wdb = this.getPuri("w");
    
    // ํ˜„์žฌ ์‚ฌ์šฉ์ž๋ฅผ ์ž‘์„ฑ์ž๋กœ ์„ค์ •
    const [post] = await wdb
      .table("posts")
      .insert({
        ...params,
        user_id: context.user.id,
        author: context.user.username,
      })
      .returning({ id: "id" });
    
    return { postId: post.id };
  }
}
context.user๋Š” ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด์— ์˜ํ•ด ์„ค์ •๋ฉ๋‹ˆ๋‹ค. ์ธ์ฆ์ด ํ•„์š”ํ•œ API์—์„œ๋Š” ํ•ญ์ƒ context.user์˜ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์‹ค์ „ ์‚ฌ์šฉ ์˜ˆ์ œ

์ธ์ฆ ํ™•์ธ

class UserModel extends BaseModelClass {
  @api({ httpMethod: "PUT" })
  async updateProfile(params: ProfileParams): Promise<void> {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      context.reply.status(401);
      throw new Error("Unauthorized");
    }
    
    const wdb = this.getPuri("w");
    
    await wdb
      .table("users")
      .where("id", context.user.id)
      .update({
        bio: params.bio,
        avatar_url: params.avatarUrl,
      });
  }
}

๊ถŒํ•œ ํ™•์ธ

class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    const context = Sonamu.getContext();
    
    // ์ธ์ฆ ํ™•์ธ
    if (!context.user) {
      context.reply.status(401);
      throw new Error("Unauthorized");
    }
    
    // ๊ถŒํ•œ ํ™•์ธ
    if (context.user.role !== "admin") {
      context.reply.status(403);
      throw new Error("Forbidden: Admin only");
    }
    
    // ์ž๊ธฐ ์ž์‹ ์€ ์‚ญ์ œ ๋ถˆ๊ฐ€
    if (context.user.id === userId) {
      context.reply.status(400);
      throw new Error("Cannot delete yourself");
    }
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}

๋กœ๊น…

class OrderModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: OrderParams): Promise<{ orderId: number }> {
    const context = Sonamu.getContext();
    
    // ์š”์ฒญ ๋กœ๊น…
    console.log({
      timestamp: new Date(),
      userId: context.user?.id,
      ip: context.request.ip,
      userAgent: context.request.headers["user-agent"],
      endpoint: context.request.url,
      method: context.request.method,
    });
    
    // ์ฃผ๋ฌธ ์ƒ์„ฑ
    const wdb = this.getPuri("w");
    const [order] = await wdb
      .table("orders")
      .insert({
        ...params,
        user_id: context.user!.id,
        ip_address: context.request.ip,
      })
      .returning({ id: "id" });
    
    return { orderId: order.id };
  }
}

์ปค์Šคํ…€ ํ—ค๋”

class FileModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async download(fileId: number): Promise<Buffer> {
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const file = await rdb
      .table("files")
      .where("id", fileId)
      .first();
    
    if (!file) {
      throw new Error("File not found");
    }
    
    // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ํ—ค๋” ์„ค์ •
    context.reply.header("Content-Type", file.mime_type);
    context.reply.header(
      "Content-Disposition",
      `attachment; filename="${file.filename}"`
    );
    context.reply.header("Content-Length", file.size.toString());
    
    // ํŒŒ์ผ ๋‚ด์šฉ ๋ฐ˜ํ™˜
    return Buffer.from(file.content);
  }
}

์ฟ ํ‚ค ์ฒ˜๋ฆฌ

class AuthModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async login(params: LoginParams): Promise<{ token: string }> {
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    // ์‚ฌ์šฉ์ž ์ธ์ฆ
    const user = await rdb
      .table("users")
      .where("email", params.email)
      .first();
    
    if (!user || user.password !== params.password) {
      context.reply.status(401);
      throw new Error("Invalid credentials");
    }
    
    // JWT ํ† ํฐ ์ƒ์„ฑ
    const jwt = require("jsonwebtoken");
    const token = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: "24h" }
    );
    
    // ์ฟ ํ‚ค์— ํ† ํฐ ์ €์žฅ
    context.reply.cookie("auth_token", token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      maxAge: 86400000, // 24์‹œ๊ฐ„
      sameSite: "strict",
    });
    
    return { token };
  }
  
  @api({ httpMethod: "POST" })
  async logout(): Promise<{ message: string }> {
    const context = Sonamu.getContext();
    
    // ์ฟ ํ‚ค ์‚ญ์ œ
    context.reply.clearCookie("auth_token");
    
    return { message: "Logged out successfully" };
  }
}

IP ๊ธฐ๋ฐ˜ ์ œํ•œ

class ApiModel extends BaseModelClass {
  private readonly ALLOWED_IPS = [
    "127.0.0.1",
    "192.168.1.0/24",
    // ...
  ];
  
  @api({ httpMethod: "POST" })
  async adminAction(params: AdminParams): Promise<void> {
    const context = Sonamu.getContext();
    
    // IP ํ™•์ธ
    const clientIp = context.request.ip;
    
    if (!this.isAllowedIp(clientIp)) {
      context.reply.status(403);
      throw new Error("Access denied from this IP");
    }
    
    // ๊ด€๋ฆฌ์ž ์ž‘์—… ์ˆ˜ํ–‰
    // ...
  }
  
  private isAllowedIp(ip: string): boolean {
    // IP ์ฒดํฌ ๋กœ์ง
    return this.ALLOWED_IPS.includes(ip);
  }
}

Rate Limiting ์ •๋ณด

class ApiModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async list(): Promise<any[]> {
    const context = Sonamu.getContext();
    
    // Rate limit ์ •๋ณด๋ฅผ ์‘๋‹ต ํ—ค๋”์— ์ถ”๊ฐ€
    context.reply.header("X-RateLimit-Limit", "100");
    context.reply.header("X-RateLimit-Remaining", "95");
    context.reply.header("X-RateLimit-Reset", Date.now() + 3600000);
    
    const rdb = this.getPuri("r");
    return rdb.table("items").select("*");
  }
}

Context ์‚ฌ์šฉ ํŒจํ„ด

์ฃผ์˜์‚ฌํ•ญ

Context ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. API ๋ฉ”์„œ๋“œ ๋‚ด์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  2. context.user๋Š” ์ธ์ฆ ํ›„์—๋งŒ ์กด์žฌ
  3. ๋น„๋™๊ธฐ ์ž‘์—… ์ค‘์—๋„ ๋™์ผํ•œ Context ์œ ์ง€
  4. Context ์ˆ˜์ •์€ ์‹ ์ค‘ํ•˜๊ฒŒ
  5. ์‘๋‹ต ํ—ค๋”๋Š” ๋ฐ์ดํ„ฐ ์ „์†ก ์ „์— ์„ค์ •

ํ”ํ•œ ์‹ค์ˆ˜

// โŒ ์ž˜๋ชป๋จ: Context ์—†์ด user ์ ‘๊ทผ ์‹œ๋„
class UserModel extends BaseModelClass {
  private currentUserId?: number;
  
  @api({ httpMethod: "POST" })
  async create(params: UserParams): Promise<void> {
    // Context๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ์•Š์Œ
    if (this.currentUserId) { // โ† ์ž‘๋™ ์•ˆ ํ•จ
      // ...
    }
  }
}

// โŒ ์ž˜๋ชป๋จ: user ์กด์žฌ ํ™•์ธ ์—†์ด ์‚ฌ์šฉ
class PostModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: PostParams): Promise<void> {
    const context = Sonamu.getContext();
    
    // context.user๊ฐ€ undefined์ผ ์ˆ˜ ์žˆ์Œ
    const userId = context.user.id; // โ† ์—๋Ÿฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
  }
}

// โœ… ์˜ฌ๋ฐ”๋ฆ„: Context ๊ฐ€์ ธ์˜ค๊ณ  user ํ™•์ธ
class PostModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: PostParams): Promise<void> {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    const userId = context.user.id; // โ† ์•ˆ์ „
    // ...
  }
}

๋‹ค์Œ ๋‹จ๊ณ„