Ver3.0 ๐ก๏ธ GraphQL API ๋ณด์ ์ทจ์ฝ์ ๊ณผ ๋ฐฉ์ด ๋ฐฉ๋ฒ

๐ก๏ธ GraphQL API ๋ณด์ ์ทจ์ฝ์ ๊ณผ ๋ฐฉ์ด ๋ฐฉ๋ฒ
ํ๋ API์ ๊ฐ๋ ฅํ ๋ฌด๊ธฐ, ํ์ง๋ง ๋ฐฉ์ฌํ๋ฉด ํฐ์ฝ๋ค์น๋ค! ๐จ
๐ฏ GraphQL, ๋ ์ ์ฒด๊ฐ ๋ญ์ผ?
์ผ, ์น๊ตฌ! ์์ฆ ๊ฐ๋ฐ์๋ค ์ฌ์ด์์ GraphQL ์๊ธฐ ์ ๋ค์ด๋ดค์ด? ๐
REST API๊ฐ ํ์ฐฝ ์๋๊ฐ๋ ์์ , ํ์ด์ค๋ถ์์ "์ ์ด๊ฑฐ ์ข ๋ถํธํ๋ฐ?"๋ผ๋ฉฐ 2015๋
์ ์คํ์์ค๋ก ๊ณต๊ฐํ ๊ฒ ๋ฐ๋ก GraphQL์ด์ผ.
REST API๋ ์๋ํฌ์ธํธ๋ง๋ค ์ ํด์ง ๋ฐ์ดํฐ๋ง ์ฃผ์์? ๊ทผ๋ฐ GraphQL์ ํด๋ผ์ด์ธํธ๊ฐ ์ํ๋ ๋ฐ์ดํฐ๋ง ์์ ๊ณจ๋ผ์ ์์ฒญํ ์ ์์ด. ๋ง์น ๋ทํ์์ ๋ด๊ฐ ๋จน๊ณ ์ถ์ ๊ฒ๋ง ์ ์์ ๋ด๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ! ๐ฝ๏ธ
์๋ฅผ ๋ค์ด๋ณผ๊น? REST API์์๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ค๋ฉด /users/123์ ํธ์ถํ๋ฉด ์ด๋ฆ, ์ด๋ฉ์ผ, ์ฃผ์, ์ ํ๋ฒํธ... ๋ฑ๋ฑ ๋ชจ๋ ์ ๋ณด๊ฐ ์๋ฅด๋ฅด ์์์ ธ. ๊ทผ๋ฐ ๋๋ ์ด๋ฆ๋ง ํ์ํ๋ฐ ๋ง์ด์ผ! ๐
GraphQL์์๋ ์ด๋ ๊ฒ ์์ฒญํ๋ฉด ๋ผ:
query {
user(id: 123) {
name
}
}
๋ฑ ์ด๋ฆ๋ง ๋ฐ์์ค๋ ๊ฑฐ์ง! ๊น๋ํ์ง? โจ
๊ทธ๋ฐ๋ฐ ๋ง์ด์ผ... ์ด๋ ๊ฒ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ ๋งํผ, ๋ณด์ ์ทจ์ฝ์ ๋ ๋ง๋ง์น ์์. ์ค๋์ GraphQL์ ์ด๋์ด ๋ฉด์ ํํค์ณ๋ณด๊ณ , ์ด๋ป๊ฒ ๋ฐฉ์ดํ ์ ์๋์ง ๊ฐ์ด ์์๋ณด์! ๐ต๏ธ
โ ๏ธ GraphQL์ ์ฃผ์ ๋ณด์ ์ทจ์ฝ์ ๋ค
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก GraphQL์ ๋ณด์ ์ทจ์ฝ์ ๋ค์ ํ๋์ฉ ๋ฏ์ด๋ณด์! ๐
๊ฐ๊ฐ์ ์ทจ์ฝ์ ์ด ์ด๋ป๊ฒ ์๋ํ๋์ง, ์ค์ ๋ก ์ด๋ค ํผํด๋ฅผ ์ค ์ ์๋์ง ์์์ผ ์ ๋๋ก ๋ง์ ์ ์๊ฑฐ๋ . ๐ช
์ด๊ฑด GraphQL์ ๊ฐ์ฅ ์น๋ช
์ ์ธ ์ฝ์ ์ค ํ๋์ผ. ๐ฐ
GraphQL์ ํด๋ผ์ด์ธํธ๊ฐ ์ํ๋ ๋๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์์์? ๊ทธ๋ฐ๋ฐ ์
์์ ์ธ ์ฌ์ฉ์๊ฐ ์์ฒญ๋๊ฒ ๋ณต์กํ๊ณ ๊น์ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ด๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
์๋ฅผ ๋ค์ด ์ด๋ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ธ๋ค๊ณ ์๊ฐํด๋ด:
query {
user(id: 1) {
posts {
comments {
author {
posts {
comments {
author {
posts {
comments {
# ์ด๊ฒ ๊ณ์ ๋ฐ๋ณต๋๋ค๋ฉด...? ๐ฑ
}
}
}
}
}
}
}
}
}
}
์ด๋ฐ ์์ผ๋ก ๊ด๊ณ๋ฅผ ํ๊ณ ํ๊ณ ๋ค์ด๊ฐ๋ฉด์ ์ค์ฒฉ ๊น์ด๊ฐ ์์ญ, ์๋ฐฑ ๋ ๋ฒจ๊น์ง ๊ฐ ์ ์์ด.
์๋ฒ๋ ์ด ์ฟผ๋ฆฌ๋ฅผ ์ฒ๋ฆฌํ๋ ค๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์๋ฐฑ๋ง ๋ฒ ์กฐํํ๊ฒ ๋๊ณ ... ๐ฅ
๊ฒฐ๊ณผ? ์๋ฒ๊ฐ ๋ป์ด๋ฒ๋ฆฌ๋ ๊ฑฐ์ง! ์ ํ์ ์ธ DoS(Denial of Service) ๊ณต๊ฒฉ์ด์ผ. ๐ฅ
๋จ ํ๋์ ์ ์์ ์ธ ์ฟผ๋ฆฌ๊ฐ ์์ฒ ๋ช ์ ์ฌ์ฉ์์๊ฒ ์ํฅ์ ์ค ๊ฑฐ์ง. ๐ข
๊ตฌํ ์์ (Node.js + graphql-depth-limit):
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)] // ์ต๋ 5๋จ๊ณ๊น์ง๋ง ํ์ฉ
});
๊ตฌํ ์์:
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log('Query cost:', cost);
}
})
]
});
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
timeout: 5000 // 5์ด ์ ํ
}),
plugins: [
{
requestDidStart() {
return {
async executionDidStart() {
const timeout = setTimeout(() => {
throw new Error('Query timeout');
}, 5000);
return {
willResolveField() {
return () => clearTimeout(timeout);
}
};
}
};
}
}
]
});
GraphQL์ ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ฅผ ํ ๋ฒ์ ๋ณด๋ผ ์ ์๋ ๋ฐฐ์น ๊ธฐ๋ฅ์ ์ง์ํด. ํธ๋ฆฌํ๊ธด ํ๋ฐ... ์ด๊ฒ๋ ์
์ฉ๋ ์ ์์ด! ๐
๊ณต๊ฒฉ์๊ฐ ์ด๋ฐ ์์ผ๋ก ์์ฒ ๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ํ ๋ฒ์ ๋ณด๋ธ๋ค๋ฉด?
[
{ query: "query { user(id: 1) { name email } }" },
{ query: "query { user(id: 2) { name email } }" },
{ query: "query { user(id: 3) { name email } }" },
// ... 10,000๊ฐ ๋ฐ๋ณต
]
์๋ฒ๋ ์ด ๋ชจ๋ ์ฟผ๋ฆฌ๋ฅผ ์ฒ๋ฆฌํ๋๋ผ ๋ฆฌ์์ค๊ฐ ๊ณ ๊ฐ๋๊ณ ... ๋ ๋ค์ DoS! ๐
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
app.use('/graphql', (req, res, next) => {
if (Array.isArray(req.body)) {
if (req.body.length > 10) { // ์ต๋ 10๊ฐ๊น์ง๋ง
return res.status(413).json({
error: 'Batch size too large'
});
}
}
next();
}, graphqlHTTP({
schema,
graphiql: true
}));
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: 'Too many requests from this IP'
});
app.use('/graphql', limiter);
GraphQL์ ๋ฉ์ง ๊ธฐ๋ฅ ์ค ํ๋๊ฐ ๋ฐ๋ก ์ธํธ๋ก์คํ์
(Introspection)์ด์ผ! ๐ญ
์ด๊ฑด API์ ์ ์ฒด ์คํค๋ง๋ฅผ ์กฐํํ ์ ์๋ ๊ธฐ๋ฅ์ธ๋ฐ, ๊ฐ๋ฐํ ๋๋ ์์ฒญ ์ ์ฉํด.
GraphiQL์ด๋ GraphQL Playground ๊ฐ์ ๋๊ตฌ๋ค์ด ์ด ๊ธฐ๋ฅ์ ์ฌ์ฉํด์ ์๋์์ฑ์ ์ ๊ณตํ๊ฑฐ๋ . ๐
๊ทผ๋ฐ ๋ฌธ์ ๋... ํ๋ก๋์
ํ๊ฒฝ์์๋ ์ด๊ฒ ์ผ์ ธ ์์ผ๋ฉด ๊ณต๊ฒฉ์๊ฐ API์ ๋ชจ๋ ๊ตฌ์กฐ๋ฅผ ํ์
ํ ์ ์๋ค๋ ๊ฑฐ์ผ! ๐ฑ
์ด๋ฐ ์ฟผ๋ฆฌ๋ก ์ ์ฒด ์คํค๋ง๋ฅผ ๋ณผ ์ ์์ด:
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
์ด๊ฑธ๋ก ๊ณต๊ฒฉ์๋:
โ
์ด๋ค ํ์
๋ค์ด ์๋์ง
โ
์ด๋ค ํ๋๋ค์ด ์๋์ง
โ
์ด๋ค ์ฟผ๋ฆฌ์ ๋ฎคํ
์ด์
์ด ๊ฐ๋ฅํ์ง
โ
์ฌ์ง์ด ์จ๊ฒจ์ง ๊ด๋ฆฌ์ ๊ธฐ๋ฅ๊น์ง!
๋ชจ๋ ๊ฑธ ๋ค ์์๋ผ ์ ์์ง. ๋ง์น ๊ฑด๋ฌผ ์ค๊ณ๋๋ฅผ ๋๋์๊ฒ ์ฃผ๋ ๊ฒฉ์ด์ผ! ๐๏ธ
์ด๊ฑด ์ ๋ง ์ํํ ์ต๊ด์ด์ผ! ๐ซ
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production', // ํ๋ก๋์
์์๋ OFF
playground: process.env.NODE_ENV !== 'production'
});
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart() {
return {
didResolveOperation({ request, context }) {
if (request.operationName === 'IntrospectionQuery') {
if (!context.user || !context.user.isAdmin) {
throw new Error('Introspection disabled');
}
}
}
};
}
}
]
});
SQL Injection์ ๋ค์ด๋ดค์ง? GraphQL์์๋ ๋น์ทํ ๊ณต๊ฒฉ์ด ๊ฐ๋ฅํด! ๐
ํนํ ์ฌ์ฉ์ ์
๋ ฅ์ ์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ์ ๋ฃ์ ๋ ์ํํด.
์๋ฅผ ๋ค์ด ์ด๋ฐ ๋ฆฌ์กธ๋ฒ๊ฐ ์๋ค๊ณ ํด๋ณด์:
// โ ์ํํ ์ฝ๋!
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// ์ฌ์ฉ์ ์
๋ ฅ์ ์ง์ SQL์ ์ฝ์
const query = `SELECT * FROM users WHERE id = ${id}`;
return await db.query(query);
}
}
};
๊ณต๊ฒฉ์๊ฐ ์ด๋ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ธ๋ค๋ฉด?
query {
user(id: "1 OR 1=1") {
name
email
password
}
}
์ค์ ์คํ๋๋ SQL์:
SELECT * FROM users WHERE id = 1 OR 1=1
์ด๋ฌ๋ฉด ๋ชจ๋ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋
ธ์ถ๋๋ ๊ฑฐ์ง! ๐จ
๋ ์
๋ํ ๊ณต๊ฒฉ์๋ ์ด๋ ๊ฒ ํ ์๋ ์์ด:
query {
user(id: "1; DROP TABLE users; --") {
name
}
}
์ด๊ฑด... ์ฌ์ฉ์ ํ ์ด๋ธ์ ํต์งธ๋ก ๋ ๋ ค๋ฒ๋ฆฌ๋ ๊ฑฐ์ผ! ๐ฃ
// โ
์์ ํ ์ฝ๋
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// ๋งค๊ฐ๋ณ์ ๋ฐ์ธ๋ฉ ์ฌ์ฉ
const query = 'SELECT * FROM users WHERE id = ?';
return await db.query(query, [id]);
}
}
};
// Prisma ์์
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
return await context.prisma.user.findUnique({
where: { id: parseInt(id) }
});
}
}
};
// ์คํค๋ง์์ ํ์
๋ช
์
type Query {
user(id: Int!): User # Int ํ์
๋ง ํ์ฉ!
}
// ์ถ๊ฐ ๊ฒ์ฆ
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// ์ซ์์ธ์ง ํ์ธ
if (!Number.isInteger(id) || id < 1) {
throw new Error('Invalid user ID');
}
return await context.prisma.user.findUnique({
where: { id }
});
}
}
};
์ด๊ฑด ์ ๋ง ์ฌ๊ฐํ ๋ฌธ์ ์ผ! ๐ฐ
GraphQL์์๋ ๊ฐ ํ๋๋ง๋ค ๊ถํ์ ์ฒดํฌํด์ผ ํ๋๋ฐ, ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ด๊ฑธ ๋์น๊ฑฐ๋ .
์๋ฅผ ๋ค์ด ์ด๋ฐ ์คํค๋ง๊ฐ ์๋ค๊ณ ํด๋ณด์:
type User {
id: ID!
name: String!
email: String!
salary: Float! # ๋ฏผ๊ฐํ ์ ๋ณด!
ssn: String! # ์ฃผ๋ฏผ๋ฒํธ ๊ฐ์ ์ด๋ฏผ๊ฐ ์ ๋ณด!
}
type Query {
user(id: ID!): User
users: [User!]!
}
๋ง์ฝ user ์ฟผ๋ฆฌ์๋ง ์ธ์ฆ์ ์ฒดํฌํ๊ณ , ๊ฐ ํ๋์๋ ๊ถํ ์ฒดํฌ๊ฐ ์๋ค๋ฉด?
์ผ๋ฐ ์ฌ์ฉ์๋ ์ด๋ ๊ฒ ๋ค๋ฅธ ์ฌ๋์ ๊ธ์ฌ๋ ์ฃผ๋ฏผ๋ฒํธ๋ฅผ ๋ณผ ์ ์๊ฒ ๋ผ:
query {
user(id: "123") {
name
salary # ๐ฑ ๋จ์ ๊ธ์ฌ๋ฅผ ๋ณผ ์ ์๋ค!
ssn # ๐ฑ๐ฑ ์ฃผ๋ฏผ๋ฒํธ๊น์ง!
}
}
ํ์ฌ๋ ๊ฒฐ๊ตญ ๊ฑฐ์ก์ ๋ฒ๊ธ์ ๋ฌผ๊ณ ์ ๋ขฐ๋ฅผ ์์์ง... ๐ข
const resolvers = {
User: {
salary: (parent, args, context) => {
// ๋ณธ์ธ์ด๊ฑฐ๋ ๊ด๋ฆฌ์๋ง ๋ณผ ์ ์์
if (context.user.id !== parent.id && !context.user.isAdmin) {
throw new Error('Unauthorized');
}
return parent.salary;
},
ssn: (parent, args, context) => {
// ๋ณธ์ธ๋ง ๋ณผ ์ ์์
if (context.user.id !== parent.id) {
throw new Error('Unauthorized');
}
return parent.ssn;
}
}
};
// ์คํค๋ง ์ ์
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
ADMIN
USER
}
type User {
id: ID!
name: String!
email: String! @auth(requires: USER)
salary: Float! @auth(requires: ADMIN)
ssn: String! @auth(requires: ADMIN)
}
// Directive ๊ตฌํ
const { SchemaDirectiveVisitor } = require('graphql-tools');
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { requires } = this.args;
field.resolve = async function(...args) {
const context = args[2];
if (!context.user) {
throw new Error('Not authenticated');
}
if (requires === 'ADMIN' && !context.user.isAdmin) {
throw new Error('Not authorized');
}
return resolve.apply(this, args);
};
}
}
const { shield, rule, and, or } = require('graphql-shield');
// ๊ท์น ์ ์
const isAuthenticated = rule()(async (parent, args, context) => {
return context.user !== null;
});
const isAdmin = rule()(async (parent, args, context) => {
return context.user && context.user.isAdmin;
});
const isOwner = rule()(async (parent, args, context) => {
return context.user && context.user.id === parent.id;
});
// ๊ถํ ์ค์
const permissions = shield({
Query: {
user: isAuthenticated,
users: isAdmin
},
User: {
email: isAuthenticated,
salary: or(isOwner, isAdmin),
ssn: isOwner
}
});
// ์๋ฒ์ ์ ์ฉ
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
user: getUserFromToken(req.headers.authorization)
}),
schema: applyMiddleware(schema, permissions)
});
๐ ์ข ํฉ ๋ณด์ ์ ๋ต
์, ์ง๊ธ๊น์ง ๊ฐ๋ณ ์ทจ์ฝ์ ๋ค์ ๋ดค๋๋ฐ, ์ด์ ์ด๊ฑธ ๋ค ํฉ์ณ์ ์ข
ํฉ์ ์ธ ๋ณด์ ์ ๋ต์ ์ธ์๋ณด์! ๐ฏ
์ค์ ํ๋ก๋์
ํ๊ฒฝ์์๋ ์ฌ๋ฌ ๋ฐฉ์ด ๊ธฐ๋ฒ์ ๋ ์ด์ด์ฒ๋ผ ์์์ผ ํด. ํ๋๊ฐ ๋ซ๋ ค๋ ๋ค๋ฅธ ๊ฒ ๋ง์์ฃผ๋ ์์ผ๋ก ๋ง์ด์ผ! ๐ก๏ธ
โข DDoS ๋ฐฉ์ด ์๋น์ค (Cloudflare, AWS Shield ๋ฑ)
โข IP ํ์ดํธ๋ฆฌ์คํธ/๋ธ๋๋ฆฌ์คํธ
โข Rate Limiting at CDN level
โข Query depth limiting
โข Query timeout
โข Batch size limiting
โข Rate limiting per user/IP
โข Field-level authorization
โข Role-based access control (RBAC)
โข Attribute-based access control (ABAC)
โข Output sanitization
โข Parameterized queries
โข ORM ์ฌ์ฉ
โข ๋ฏผ๊ฐ ๋ฐ์ดํฐ ์ํธํ
ํ๋ก๋์ ์ ๋ฐฐํฌํ๊ธฐ ์ ์ ์ด ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๊ผญ ํ์ธํด๋ด! โ
| ํญ๋ชฉ | ์ค๋ช | ์ฐ์ ์์ |
|---|---|---|
| ์ธํธ๋ก์คํ์ ๋นํ์ฑํ | ํ๋ก๋์ ์์๋ ๋ฐ๋์ OFF | ๐ด ๋์ |
| Query Depth Limit | ์ต๋ 5-7 ๋ ๋ฒจ ๊ถ์ฅ | ๐ด ๋์ |
| Query Complexity | ๋ณต์ก๋ ์ ์ ์ ํ ์ค์ | ๐ด ๋์ |
| Query Timeout | 5-10์ด ๊ถ์ฅ | ๐ก ์ค๊ฐ |
| Batch Size Limit | ์ต๋ 10-20๊ฐ ๊ถ์ฅ | ๐ก ์ค๊ฐ |
| Rate Limiting | IP/์ฌ์ฉ์๋น ์ ํ ์ค์ | ๐ด ๋์ |
| Field-Level Auth | ๋ฏผ๊ฐํ ํ๋๋ ๋ฐ๋์ ์ฒดํฌ | ๐ด ๋์ |
| Input Validation | ๋ชจ๋ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ | ๐ด ๋์ |
| HTTPS ์ฌ์ฉ | SSL/TLS ์ธ์ฆ์ ์ ์ฉ | ๐ด ๋์ |
| ๋ก๊น ๋ฐ ๋ชจ๋ํฐ๋ง | ์์ฌ์ค๋ฌ์ด ํ๋ ์ถ์ | ๐ก ์ค๊ฐ |
| ์๋ฌ ๋ฉ์์ง ์ต์ํ | ์์ธํ ์๋ฌ ์ ๋ณด ๋ ธ์ถ ๊ธ์ง | ๐ก ์ค๊ฐ |
| CORS ์ค์ | ํ์ฉ๋ ๋๋ฉ์ธ๋ง ์ ๊ทผ | ๐ก ์ค๊ฐ |
์, ์ด์ ์ง๊ธ๊น์ง ๋ฐฐ์ด ๊ฑธ ๋ค ํฉ์ณ์ ์์ ํ ๋ณด์ ์ค์ ์ ๋ง๋ค์ด๋ณด์! ๐ช
์ด๊ฑด ์ค์ ๋ก ํ๋ก๋์
์์ ์ฌ์ฉํ ์ ์๋ ์์ค์ ์ฝ๋์ผ.
const { ApolloServer } = require('apollo-server-express');
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const { shield, rule } = require('graphql-shield');
// Express ์ฑ ์ค์
const app = express();
// ๋ณด์ ํค๋ ์ค์
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
}
}));
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: 'Too many requests',
standardHeaders: true,
legacyHeaders: false
});
app.use('/graphql', limiter);
// ๋ฐฐ์น ํฌ๊ธฐ ์ ํ
app.use('/graphql', (req, res, next) => {
if (Array.isArray(req.body)) {
if (req.body.length > 10) {
return res.status(413).json({
error: 'Batch size too large'
});
}
}
next();
});
// ๊ถํ ๊ท์น ์ ์
const isAuthenticated = rule()(async (parent, args, context) => {
return context.user !== null;
});
const isAdmin = rule()(async (parent, args, context) => {
return context.user && context.user.role === 'ADMIN';
});
const permissions = shield({
Query: {
users: isAdmin,
user: isAuthenticated
},
Mutation: {
createUser: isAdmin,
updateUser: isAuthenticated,
deleteUser: isAdmin
}
});
// Apollo Server ์ค์
const server = new ApolloServer({
typeDefs,
resolvers,
// ์ธํธ๋ก์คํ์
๋นํ์ฑํ (ํ๋ก๋์
)
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
// ๊ฒ์ฆ ๊ท์น
validationRules: [
depthLimit(7), // ์ต๋ ๊น์ด 7
createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log('Query cost:', cost);
}
})
],
// Context ์ค์
context: async ({ req }) => {
// ํ ํฐ์์ ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
const token = req.headers.authorization || '';
const user = await getUserFromToken(token);
return {
user,
// IP ์ฃผ์ ์ ์ฅ (๋ก๊น
์ฉ)
ip: req.ip
};
},
// ์๋ฌ ํฌ๋งทํ
(์์ธ ์ ๋ณด ์จ๊ธฐ๊ธฐ)
formatError: (error) => {
// ํ๋ก๋์
์์๋ ์์ธํ ์๋ฌ ๋ฉ์์ง ์จ๊น
if (process.env.NODE_ENV === 'production') {
// ๋ด๋ถ ์๋ฌ๋ ๋ก๊ทธ๋ง ๋จ๊ธฐ๊ณ ์ผ๋ฐ ๋ฉ์์ง ๋ฐํ
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
console.error('Internal error:', error);
return new Error('Internal server error');
}
}
return error;
},
// ํ๋ฌ๊ทธ์ธ
plugins: [
// ์ฟผ๋ฆฌ ํ์์์
{
requestDidStart() {
const startTime = Date.now();
let timeout;
return {
async executionDidStart() {
timeout = setTimeout(() => {
throw new Error('Query timeout');
}, 10000); // 10์ด ์ ํ
},
willSendResponse() {
clearTimeout(timeout);
const duration = Date.now() - startTime;
console.log(`Query took ${duration}ms`);
}
};
}
},
// ๋ก๊น
ํ๋ฌ๊ทธ์ธ
{
requestDidStart({ request, context }) {
console.log('Query started:', {
operationName: request.operationName,
user: context.user?.id,
ip: context.ip,
timestamp: new Date().toISOString()
});
return {
didEncounterErrors({ errors }) {
console.error('Query errors:', errors);
}
};
}
}
]
});
// ๋ฏธ๋ค์จ์ด ์ ์ฉ
server.applyMiddleware({
app,
path: '/graphql',
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
}
});
// ์๋ฒ ์์
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`๐ Server ready at http://localhost:${PORT}${server.graphqlPath}`);
});
์ด ์ฝ๋๋ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด ๋ชจ๋ ๋ณด์ ๊ธฐ๋ฒ์ ํฌํจํ๊ณ ์์ด! ๐
โข โ
Rate Limiting
โข โ
Query Depth & Complexity Limiting
โข โ
Query Timeout
โข โ
Batch Size Limiting
โข โ
Authentication & Authorization
โข โ
Introspection ๋นํ์ฑํ
โข โ
๋ณด์ ํค๋
โข โ
CORS ์ค์
โข โ
์๋ฌ ๋ฉ์์ง ์ต์ํ
โข โ
๋ก๊น
๋ฐ ๋ชจ๋ํฐ๋ง
์ด ์ ๋๋ฉด ์ฌ๋งํ ๊ณต๊ฒฉ์ ๋ค ๋ง์ ์ ์์ด! ๐ช
๐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
๋ณด์ ์ค์ ์ ๋ค ํ๋ค๊ณ ๋์ด ์๋์ผ! ๐
โโ๏ธ
์ง์์ ์ธ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
์ด ์ ๋ง ์ค์ํด. ๊ณต๊ฒฉ์ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ๊ณ ๋์ํ ์ ์์ด์ผ ํ๊ฑฐ๋ ! ๐
โข ๋๋ฆฐ ์ฟผ๋ฆฌ (Slow queries)
โข ์ฟผ๋ฆฌ ๋ณต์ก๋ ๋ถํฌ
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ํํธ ์
๊ฐ์๊ธฐ ์๋ต ์๊ฐ์ด ๋์ด๋๊ฑฐ๋ ๋ณต์กํ ์ฟผ๋ฆฌ๊ฐ ๋ง์์ง๋ฉด ๊ณต๊ฒฉ ์ ํธ์ผ ์ ์์ด! ๐จ
โข ๊ถํ ๊ฑฐ๋ถ ํ์
โข ํ์์์ ๋ฐ์ ๋น๋
โข ํน์ IP์์์ ์๋ฌ ์ง์ค
๊ฐ์ IP์์ ๊ณ์ ์ธ์ฆ ์คํจ๊ฐ ๋ฐ์ํ๋ค๋ฉด? ๋ธ๋ฃจํธํฌ์ค ๊ณต๊ฒฉ์ผ ๊ฐ๋ฅ์ฑ์ด ๋์! ๐
โข ๋ฐฐ์น ์ฟผ๋ฆฌ ์ฌ์ฉ ํจํด
โข ํน์ ์๋ํฌ์ธํธ ์ง์ค๋
โข ๋น์ ์์ ์ธ ์๊ฐ๋ ํธ๋ํฝ
์๋ฒฝ 3์์ ๊ฐ์๊ธฐ ํธ๋ํฝ์ด ํญ์ฆํ๋ค๋ฉด? ๋ญ๊ฐ ์ด์ํ ๊ฑฐ์ง! ๐
์ค์ ์์ ์ฌ์ฉํ ์ ์๋ ๋ก๊น ์์คํ ์ ๋ง๋ค์ด๋ณด์! ๐
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
// Winston ๋ก๊ฑฐ ์ค์
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
// ํ์ผ ๋ก๊น
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
}),
// Elasticsearch๋ก ์ ์ก (์ ํ์ฌํญ)
new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL
}
})
]
});
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ฝ์์๋ ์ถ๋ ฅ
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// ๋ก๊น
ํ๋ฌ๊ทธ์ธ
const loggingPlugin = {
requestDidStart({ request, context }) {
const startTime = Date.now();
// ์์ฒญ ์์ ๋ก๊ทธ
logger.info('GraphQL Request', {
operationName: request.operationName,
query: request.query,
variables: request.variables,
userId: context.user?.id,
ip: context.ip,
userAgent: context.userAgent,
timestamp: new Date().toISOString()
});
return {
// ์ฟผ๋ฆฌ ํ์ฑ ๋จ๊ณ
parsingDidStart() {
return (err) => {
if (err) {
logger.error('Parsing Error', {
error: err.message,
query: request.query
});
}
};
},
// ๊ฒ์ฆ ๋จ๊ณ
validationDidStart() {
return (errs) => {
if (errs && errs.length > 0) {
logger.warn('Validation Errors', {
errors: errs.map(e => e.message),
query: request.query
});
}
};
},
// ์คํ ๋จ๊ณ
executionDidStart() {
return {
// ๊ฐ ํ๋ ๋ฆฌ์กธ๋ฒ ์คํ
willResolveField({ info }) {
const fieldStartTime = Date.now();
return (error, result) => {
const duration = Date.now() - fieldStartTime;
// ๋๋ฆฐ ํ๋ ๊ฐ์ง (100ms ์ด์)
if (duration > 100) {
logger.warn('Slow Field Resolution', {
field: info.fieldName,
parentType: info.parentType.name,
duration,
path: info.path
});
}
if (error) {
logger.error('Field Resolution Error', {
field: info.fieldName,
error: error.message
});
}
};
}
};
},
// ์๋ฌ ๋ฐ์
didEncounterErrors({ errors }) {
errors.forEach(error => {
logger.error('GraphQL Error', {
message: error.message,
code: error.extensions?.code,
path: error.path,
userId: context.user?.id,
ip: context.ip
});
});
},
// ์๋ต ์ ์ก
willSendResponse({ response }) {
const duration = Date.now() - startTime;
logger.info('GraphQL Response', {
operationName: request.operationName,
duration,
hasErrors: response.errors && response.errors.length > 0,
userId: context.user?.id
});
// ๋๋ฆฐ ์ฟผ๋ฆฌ ๊ฒฝ๊ณ (1์ด ์ด์)
if (duration > 1000) {
logger.warn('Slow Query Detected', {
operationName: request.operationName,
duration,
query: request.query,
variables: request.variables
});
}
}
};
}
};
์ฌ๊ฐํ ๋ณด์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ์ฆ์ ์๋ฆผ์ ๋ฐ์์ผ ํด! ๐ฑ
Slack์ด๋ ์ด๋ฉ์ผ๋ก ์๋ฆผ์ ๋ณด๋ด๋ ์์คํ
์ ๋ง๋ค์ด๋ณด์.
const axios = require('axios');
// Slack ์นํ
URL
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
// ์๋ฆผ ์ ์ก ํจ์
async function sendAlert(severity, message, details) {
const color = {
critical: '#dc3545',
warning: '#ffc107',
info: '#17a2b8'
}[severity] || '#6c757d';
try {
await axios.post(SLACK_WEBHOOK_URL, {
attachments: [{
color,
title: `๐จ GraphQL Security Alert - ${severity.toUpperCase()}`,
text: message,
fields: Object.entries(details).map(([key, value]) => ({
title: key,
value: String(value),
short: true
})),
footer: 'GraphQL Security Monitor',
ts: Math.floor(Date.now() / 1000)
}]
});
} catch (error) {
logger.error('Failed to send alert', { error: error.message });
}
}
// ๋ณด์ ์ด๋ฒคํธ ๊ฐ์ง ๋ฏธ๋ค์จ์ด
const securityMonitor = {
requestDidStart({ request, context }) {
// ์ธ์ฆ ์คํจ ์นด์ดํฐ (๋ฉ๋ชจ๋ฆฌ ์บ์ ๋๋ Redis ์ฌ์ฉ)
const failedAttempts = new Map();
return {
didEncounterErrors({ errors }) {
errors.forEach(async (error) => {
// ์ธ์ฆ ์คํจ ๊ฐ์ง
if (error.extensions?.code === 'UNAUTHENTICATED') {
const ip = context.ip;
const count = (failedAttempts.get(ip) || 0) + 1;
failedAttempts.set(ip, count);
// 5๋ฒ ์ด์ ์คํจ ์ ์๋ฆผ
if (count >= 5) {
await sendAlert('warning', 'Multiple authentication failures detected', {
IP: ip,
Attempts: count,
'User Agent': context.userAgent,
'Last Query': request.operationName
});
}
}
// ๊ถํ ๊ฑฐ๋ถ ๊ฐ์ง
if (error.extensions?.code === 'FORBIDDEN') {
await sendAlert('warning', 'Unauthorized access attempt', {
IP: context.ip,
'User ID': context.user?.id || 'Anonymous',
Query: request.operationName,
Field: error.path?.join('.')
});
}
// ์ฟผ๋ฆฌ ๋ณต์ก๋ ์ด๊ณผ
if (error.message.includes('complexity')) {
await sendAlert('critical', 'Query complexity limit exceeded', {
IP: context.ip,
'User ID': context.user?.id || 'Anonymous',
Query: request.query
});
}
});
},
willSendResponse({ response }) {
// ๋น์ ์์ ์ผ๋ก ๋ง์ ๋ฐ์ดํฐ ๋ฐํ ๊ฐ์ง
const responseSize = JSON.stringify(response).length;
if (responseSize > 1000000) { // 1MB ์ด์
sendAlert('warning', 'Large response detected', {
Size: `${(responseSize / 1024 / 1024).toFixed(2)} MB`,
Query: request.operationName,
'User ID': context.user?.id || 'Anonymous'
});
}
}
};
}
};
๐งช ๋ณด์ ํ ์คํธ
๋ณด์ ์ค์ ์ ๋ค ํ์ผ๋ฉด ์ด์ ์ ๋๋ก ์๋ํ๋์ง ํ
์คํธํด์ผ ํด! ๐งช
์ค์ ๋ก ๊ณต๊ฒฉ์ ์๋ฎฌ๋ ์ด์
ํด๋ณด๋ฉด์ ๋ฐฉ์ด๊ฐ ์ ๋๋์ง ํ์ธํด๋ณด์. ์ด๋ฐ ๊ฑธ ์นจํฌ ํ
์คํธ(Penetration Testing)๋ผ๊ณ ํด! ๐ฏ
๋จ์ ์๋ฒ๋ฅผ ๊ณต๊ฒฉํ๋ฉด ๋ถ๋ฒ์ด์ผ! ๐ฑ
// ํ
์คํธ ์คํฌ๋ฆฝํธ (test-depth-attack.js)
const axios = require('axios');
async function testDepthAttack() {
// 10๋จ๊ณ ์ค์ฒฉ ์ฟผ๋ฆฌ ์์ฑ
let query = 'query { user(id: 1) { ';
for (let i = 0; i < 10; i++) {
query += 'posts { comments { author { ';
}
query += 'name ';
for (let i = 0; i < 10; i++) {
query += '}}}';
}
try {
const response = await axios.post('http://localhost:4000/graphql', {
query
});
console.log('โ Depth limit NOT working!');
console.log('Response:', response.data);
} catch (error) {
if (error.response?.data?.errors) {
const depthError = error.response.data.errors.find(
e => e.message.includes('depth')
);
if (depthError) {
console.log('โ
Depth limit working correctly!');
console.log('Error:', depthError.message);
}
}
}
}
testDepthAttack();
// test-batch-attack.js
async function testBatchAttack() {
// 100๊ฐ์ ์ฟผ๋ฆฌ ๋ฐฐ์น ์์ฑ
const batch = [];
for (let i = 0; i < 100; i++) {
batch.push({
query: `query { user(id: ${i}) { name email } }`
});
}
try {
const response = await axios.post('http://localhost:4000/graphql', batch);
console.log('โ Batch limit NOT working!');
} catch (error) {
if (error.response?.status === 413) {
console.log('โ
Batch limit working correctly!');
console.log('Error:', error.response.data);
}
}
}
testBatchAttack();
// test-rate-limit.js
async function testRateLimit() {
const results = {
success: 0,
blocked: 0
};
// 200๊ฐ์ ์์ฒญ์ ๋น ๋ฅด๊ฒ ์ ์ก
const promises = [];
for (let i = 0; i < 200; i++) {
promises.push(
axios.post('http://localhost:4000/graphql', {
query: '{ user(id: 1) { name } }'
})
.then(() => results.success++)
.catch(err => {
if (err.response?.status === 429) {
results.blocked++;
}
})
);
}
await Promise.all(promises);
console.log('Results:', results);
if (results.blocked > 0) {
console.log('โ
Rate limiting working!');
console.log(`Blocked ${results.blocked} out of 200 requests`);
} else {
console.log('โ Rate limiting NOT working!');
}
}
testRateLimit();
// test-authorization.js
async function testAuthorization() {
// ํ ํฐ ์์ด ์์ฒญ
try {
const response = await axios.post('http://localhost:4000/graphql', {
query: `
query {
user(id: 1) {
name
email
salary
ssn
}
}
`
});
const data = response.data.data.user;
if (data.salary !== null || data.ssn !== null) {
console.log('โ Authorization NOT working!');
console.log('Sensitive data exposed:', data);
} else {
console.log('โ
Field-level authorization working!');
}
} catch (error) {
if (error.response?.data?.errors) {
console.log('โ
Authorization working correctly!');
console.log('Errors:', error.response.data.errors);
}
}
}
testAuthorization();
๋งค๋ฒ ์๋์ผ๋ก ํ
์คํธํ๊ธฐ ๊ท์ฐฎ์ง? ์๋ํํ์! ๐
Jest๋ฅผ ์ฌ์ฉํด์ ๋ณด์ ํ
์คํธ ์ค์ํธ๋ฅผ ๋ง๋ค์ด๋ณด์.
// security.test.js
const axios = require('axios');
const API_URL = 'http://localhost:4000/graphql';
describe('GraphQL Security Tests', () => {
describe('Query Depth Limiting', () => {
test('should block queries exceeding depth limit', async () => {
let query = 'query { user(id: 1) { ';
for (let i = 0; i < 10; i++) {
query += 'posts { comments { author { ';
}
query += 'name ';
for (let i = 0; i < 10; i++) {
query += '}}}';
}
try {
await axios.post(API_URL, { query });
fail('Should have thrown an error');
} catch (error) {
expect(error.response.data.errors).toBeDefined();
expect(error.response.data.errors[0].message).toContain('depth');
}
});
});
describe('Batch Size Limiting', () => {
test('should block large batch requests', async () => {
const batch = Array(100).fill({
query: '{ user(id: 1) { name } }'
});
try {
await axios.post(API_URL, batch);
fail('Should have thrown an error');
} catch (error) {
expect(error.response.status).toBe(413);
}
});
});
describe('Rate Limiting', () => {
test('should block excessive requests', async () => {
const requests = Array(150).fill(null).map(() =>
axios.post(API_URL, {
query: '{ user(id: 1) { name } }'
}).catch(err => err.response)
);
const responses = await Promise.all(requests);
const blocked = responses.filter(r => r?.status === 429);
expect(blocked.length).toBeGreaterThan(0);
});
});
describe('Authorization', () => {
test('should block unauthorized access to sensitive fields', async () => {
const response = await axios.post(API_URL, {
query: `
query {
user(id: 1) {
salary
ssn
}
}
`
});
expect(response.data.errors).toBeDefined();
expect(response.data.errors[0].message).toContain('Unauthorized');
});
test('should allow access with valid token', async () => {
const token = 'valid-jwt-token-here';
const response = await axios.post(API_URL, {
query: '{ user(id: 1) { name email } }'
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.data.data.user).toBeDefined();
expect(response.data.errors).toBeUndefined();
});
});
describe('Introspection', () => {
test('should block introspection in production', async () => {
const response = await axios.post(API_URL, {
query: `
query {
__schema {
types {
name
}
}
}
`
});
if (process.env.NODE_ENV === 'production') {
expect(response.data.errors).toBeDefined();
}
});
});
});
์ด์ npm test ๋ช
๋ น์ด ํ๋๋ก ๋ชจ๋ ๋ณด์ ํ
์คํธ๋ฅผ ์คํํ ์ ์์ด! ๐
CI/CD ํ์ดํ๋ผ์ธ์ ์ด ํ
์คํธ๋ค์ ์ถ๊ฐํ๋ฉด, ๋ฐฐํฌ ์ ์ ์๋์ผ๋ก ๋ณด์ ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง๋ ๊ฑฐ์ง! ๐ช
๐ ์ค์ ์ฌ๋ก ์ฐ๊ตฌ
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์์? ๐
์ค์ ๋ก ์ด๋ค ํ์ฌ๋ค์ด GraphQL ๋ณด์ ๋ฌธ์ ๋ฅผ ๊ฒช์๊ณ , ์ด๋ป๊ฒ ํด๊ฒฐํ๋์ง ์์๋ณด์! ๐
GitHub์ 2016๋
์ GraphQL API๋ฅผ ๊ณต๊ฐํ์ด. ์ธ๊ณ์์ ๊ฐ์ฅ ํฐ ์ฝ๋ ์ ์ฅ์๋๊น ๋ณด์์ด ์ ๋ง ์ค์ํ๊ฒ ์ง? ๐
์ด๊ธฐ ๋ฌธ์ ์ :
โข ๋ณต์กํ ์ฟผ๋ฆฌ๋ก ์ธํ ์ฑ๋ฅ ์ ํ
โข ๋ ์ดํธ ๋ฆฌ๋ฐ ์ฐํ ์๋
โข ๊ณผ๋ํ ๋ฐ์ดํฐ ์์ฒญ
GitHub์ ํด๊ฒฐ์ฑ
:
์๋ฅผ ๋ค์ด:
โข ๊ฐ๋จํ ์ฟผ๋ฆฌ: 1 ๋ ธ๋ ์๋น
โข ๋ณต์กํ ์ฟผ๋ฆฌ: 100 ๋ ธ๋ ์๋น
โข ์๊ฐ๋น ์ต๋ 5,000 ๋ ธ๋๊น์ง ํ์ฉ
์ด๋ ๊ฒ ํ๋ฉด ๋ณต์กํ ์ฟผ๋ฆฌ๋ ์๋์ผ๋ก ์ ํ๋๋ ๊ฑฐ์ง! ๋๋ํ์ง? ๐ง
// GitHub ์คํ์ผ์ ๋ณต์ก๋ ๊ณ์ฐ
type Repository {
name: String! # ๋ณต์ก๋: 1
issues(first: Int): [Issue!]! # ๋ณต์ก๋: first * 2
pullRequests(first: Int): [PullRequest!]! # ๋ณต์ก๋: first * 3
}
// ์ด ์ฟผ๋ฆฌ์ ๋ณต์ก๋๋?
query {
repository(name: "react") {
name # 1
issues(first: 100) { # 100 * 2 = 200
title
}
pullRequests(first: 50) { # 50 * 3 = 150
title
}
}
}
// ์ด ๋ณต์ก๋: 1 + 200 + 150 = 351
๊ฒฐ๊ณผ:
GitHub API๋ ํ์ฌ ํ๋ฃจ์ ์๋ฐฑ๋ง ๊ฑด์ ์์ฒญ์ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ณ ์์ด! ๐
Shopify๋ ์๋ฐฑ๋ง ๊ฐ์ ์จ๋ผ์ธ ์์ ์ ์ด์ํ๋ ํ๋ซํผ์ด์ผ. ๋ณด์์ด ๋ซ๋ฆฌ๋ฉด ํฐ์ผ๋๊ฒ ์ง? ๐ฐ
Shopify๊ฐ ์ง๋ฉดํ ๋ฌธ์ :
โข ์
์์ ์ธ ์์ ์ฃผ์ธ์ด ๋ค๋ฅธ ์์ ๋ฐ์ดํฐ ์ ๊ทผ ์๋
โข ๋๋์ ์ ํ ์ ๋ณด ์คํฌ๋ํ
โข API ๋จ์ฉ์ผ๋ก ์ธํ ์๋ฒ ๋ถํ
Shopify์ ํด๊ฒฐ์ฑ
:
const resolvers = {
Query: {
products: async (parent, args, context) => {
// ํ์ฌ ์์ ID ํ์ธ
const shopId = context.shop.id;
// ํด๋น ์์ ์ ์ ํ๋ง ๋ฐํ
return await db.products.findMany({
where: {
shopId: shopId
}
});
}
},
Product: {
// ๊ฐ ํ๋์์๋ ๊ถํ ์ฒดํฌ
inventory: async (parent, args, context) => {
// ์์ ์ฃผ์ธ์ด๊ฑฐ๋ ์ง์๋ง ์ฌ๊ณ ์ ๋ณด ๋ณผ ์ ์์
if (!context.user.canViewInventory(parent.shopId)) {
throw new Error('Unauthorized');
}
return parent.inventory;
}
}
};
โข ๋ฌด๋ฃ ํ๋: ์๊ฐ๋น 1,000 ํฌ์ธํธ
โข ๊ธฐ๋ณธ ํ๋: ์๊ฐ๋น 5,000 ํฌ์ธํธ
โข ํ๋ฆฌ๋ฏธ์ ํ๋: ์๊ฐ๋น 20,000 ํฌ์ธํธ
type Query {
# โ ์ด๋ ๊ฒ๋ ์ ๋ผ!
# products: [Product!]!
# โ
์ด๋ ๊ฒ ํด์ผ ํด!
products(
first: Int! # ์ต๋ 250๊ฐ๊น์ง
after: String
): ProductConnection!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
}
type ProductEdge {
cursor: String!
node: Product!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
๊ฒฐ๊ณผ:
Shopify๋ ์ด๋ฐ ์ ๋ต์ผ๋ก API ๋จ์ฉ์ 99% ์ด์ ์ค์๊ณ , ์๋ฒ ๋น์ฉ๋ ํฌ๊ฒ ์ ๊ฐํ์ด! ๐ฐ
์ด๊ฑด ์ค์ ๋ก ์์๋ ์ผ์ธ๋ฐ... ์ด๋ฆ์ ๋ฐํ ์ ์์ง๋ง ๊ตํ์ด ๋๋ ์ฌ๋ก์ผ. ๐ข
์ฌ๊ณ ๊ฒฝ์:
2021๋
, ํ ํํ
ํฌ ์คํํธ์
์ด GraphQL API๋ฅผ ์ถ์ํ์ด.
๊ฐ๋ฐ ์๋๋ฅผ ๋์ด๊ธฐ ์ํด ๋ณด์์ "๋์ค์" ํ๊ธฐ๋ก ํ์ง...
๊ทธ๋ฐ๋ฐ ์ถ์ 2์ฃผ ๋ง์ ํด์ปค๊ฐ ์ด๋ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ๊ฒฌํ์ด:
query {
users {
id
name
email
bankAccount {
accountNumber
balance
transactions {
amount
date
description
}
}
}
}
๋ฌธ์ ๋:
โ ์ธ์ฆ ์ฒดํฌ๊ฐ ์์์
โ ํ๋ ๋ ๋ฒจ ๊ถํ ์ฒดํฌ๊ฐ ์์์
โ ํ์ด์ง๋ค์ด์
์ด ์์ด์ ๋ชจ๋ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ๊ฐ์ ธ๊ฐ ์ ์์์
โ Rate limiting์ด ์์์
๊ฒฐ๊ณผ:
โข 5,000๋ช
์ด์์ ์ฌ์ฉ์ ๊ธ์ต ์ ๋ณด ์ ์ถ ๐ธ
โข ๊ธ์ต๊ฐ๋
์ ์ ์ฌ
โข ๊ฑฐ์ก์ ๋ฒ๊ธ
โข ์๋น์ค ์ ๋ขฐ๋ ์ถ๋ฝ
โข ๊ฒฐ๊ตญ ์๋น์ค ์ข
๋ฃ... ๐ญ
๋ณด์์ "๋์ค์"๊ฐ ์๋๋ผ "์ฒ์๋ถํฐ" ํด์ผ ํด! ๐จ
2. ํ๋ก๋์ ๋ฐฐํฌ ์ ๋ฐ๋์ ๋ณด์ ํ ์คํธ๋ฅผ ํด์ผ ํด
3. ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ ํนํ ๋ ์ ๊ฒฝ ์จ์ผ ํด
4. ์ธ๋ถ ๋ณด์ ๊ฐ์ฌ๋ฅผ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ
"๋นจ๋ฆฌ ๊ฐ๋ ค๋ฉด ํผ์ ๊ฐ๊ณ , ๋ฉ๋ฆฌ ๊ฐ๋ ค๋ฉด ํจ๊ป ๊ฐ๋ผ"๋ ๋ง์ด ์์์?
๋ณด์๋ ๋ง์ฐฌ๊ฐ์ง์ผ. ์ฒ์์ ๋๋ ค ๋ณด์ฌ๋, ์ ๋๋ก ํ๋ฉด ๊ฒฐ๊ตญ ๋ ๋นจ๋ฆฌ, ๋ ๋ฉ๋ฆฌ ๊ฐ ์ ์์ด! ๐โโ๏ธ
๐ ๋ณด์ ๋ฒ ์คํธ ํ๋ํฐ์ค ์ด์ ๋ฆฌ
์, ์ด์ ๋ง์ง๋ง์ผ๋ก GraphQL ๋ณด์์ ํฉ๊ธ๋ฅ ๋ค์ ์ ๋ฆฌํด๋ณด์! โจ
์ด๊ฒ๋ง ์ง์ผ๋ ๋๋ถ๋ถ์ ๊ณต๊ฒฉ์ ๋ง์ ์ ์์ด! ๐ก๏ธ
์ด๊ฑด ์ ๋ง ๊ธฐ๋ณธ ์ค์ ๊ธฐ๋ณธ! ๊ฐ๋ฐํ ๋๋ง ์ผ๊ณ , ํ๋ก๋์ ์์๋ ๋ฐ๋์ ๊บผ์ผ ํด! ๐
โข Complexity: ํ๋ก์ ํธ์ ๋ง๊ฒ ์ค์ (๋ณดํต 1000-5000)
์ด๊ฒ๋ง ํด๋ DoS ๊ณต๊ฒฉ์ 90%๋ ๋ง์ ์ ์์ด! ๐ช
โข ์ฌ์ฉ์ ๊ธฐ๋ฐ: ๋ถ๋น 50-100 ์์ฒญ
โข ๋ฐฐ์น ํฌ๊ธฐ: ์ต๋ 10-20๊ฐ
๊ณผ์ ๋ถ๊ธ! ๋๋ฌด ๋นก๋นกํ๋ฉด ์ ์ ์ฌ์ฉ์๋ ๋ถํธํด์ ธ. ์ ์ ํ ๋ฐธ๋ฐ์ค๊ฐ ์ค์ํด! โ๏ธ
ํนํ ์ด๋ฐ ํ๋๋ค:
โข ๊ฐ์ธ์ ๋ณด (์ด๋ฉ์ผ, ์ ํ๋ฒํธ, ์ฃผ์)
โข ๊ธ์ต์ ๋ณด (๊ณ์ข๋ฒํธ, ์นด๋๋ฒํธ, ์์ก)
โข ์ธ์ฆ์ ๋ณด (๋น๋ฐ๋ฒํธ ํด์, API ํค)
โข ๋ด๋ถ์ ๋ณด (๊ธ์ฌ, ํ๊ฐ, ๋ด๋ถ ๋ฉ๋ชจ)
Let's Encrypt๋ก ๋ฌด๋ฃ SSL ์ธ์ฆ์ ๋ฐ์์ ๊ผญ HTTPS ์ฐ์! ๐
โข ํ์ ์ฒดํฌ
โข ๊ธธ์ด ์ ํ
โข ํน์๋ฌธ์ ํํฐ๋ง
โข SQL Injection ๋ฐฉ์ด
๊ณต๊ฒฉ์์๊ฒ ํํธ๋ฅผ ์ฃผ๋ ๊ฑฐ์ผ. ๐ต๏ธ
โ "User with email 'admin@company.com' not found"
โ "Invalid credentials"
โข ์๋ฌ ํจํด ๋ถ์
โข ์์ฌ์ค๋ฌ์ด ํ๋ ์๋ฆผ
โข ์ ๊ธฐ์ ์ธ ๋ก๊ทธ ๊ฒํ
โข ์์กด์ฑ ์ทจ์ฝ์ ์ค์บ (npm audit)
โข ์ฝ๋ ๋ฆฌ๋ทฐ์ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ ํฌํจ
โข ์ธ๋ถ ๋ณด์ ์ ๋ฌธ๊ฐ ๊ฐ์ฌ (์ฐ 1ํ ์ด์)
ํนํ ๋ฏผ๊ฐํ ์๋น์ค์ ์ ํฉํด! ๐
์์์ ์ฟผ๋ฆฌ ์คํ์ ์์ฒ ์ฐจ๋จ! ๐ซ
โข ์ค์ํ๋ ๋ณด์ ๊ด๋ฆฌ
โข ํธ๋ํฝ ๋ถ์
โข DDoS ๋ฐฉ์ด
โข ์บ์ฑ
โข Content-Security-Policy
โข X-Frame-Options
โข X-Content-Type-Options
โข Strict-Transport-Security
๋ฐฐํฌ ์ ์ ์ด ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๊ผญ ํ์ธํด๋ด! โ
| ํญ๋ชฉ | ์๋ฃ | ๋น๊ณ |
|---|---|---|
| ์ธํธ๋ก์คํ์ ๋นํ์ฑํ | โ | ํ๋ก๋์ ํ์ |
| Query Depth Limit ์ค์ | โ | 5-7 ๋ ๋ฒจ ๊ถ์ฅ |
| Query Complexity Limit ์ค์ | โ | ํ๋ก์ ํธ๋ณ ์กฐ์ |
| Query Timeout ์ค์ | โ | 5-10์ด ๊ถ์ฅ |
| Batch Size Limit ์ค์ | โ | 10-20๊ฐ ๊ถ์ฅ |
| Rate Limiting ์ค์ | โ | IP/์ฌ์ฉ์๋ณ |
| HTTPS ์ ์ฉ | โ | ํ์! |
| JWT ์ธ์ฆ ๊ตฌํ | โ | ํ ํฐ ๋ง๋ฃ ์๊ฐ ์ค์ |
| ํ๋ ๋ ๋ฒจ ๊ถํ ์ฒดํฌ | โ | ๋ฏผ๊ฐํ ํ๋ ํ์ |
| ์ ๋ ฅ ๊ฒ์ฆ ๊ตฌํ | โ | ๋ชจ๋ ์ ๋ ฅ๊ฐ |
| Parameterized Queries ์ฌ์ฉ | โ | SQL Injection ๋ฐฉ์ด |
| ์๋ฌ ๋ฉ์์ง ์ต์ํ | โ | ํ๋ก๋์ ํ๊ฒฝ |
| ๋ก๊น ์์คํ ๊ตฌ์ถ | โ | Winston, ELK ๋ฑ |
| ๋ชจ๋ํฐ๋ง ์๋ฆผ ์ค์ | โ | Slack, Email ๋ฑ |
| ๋ณด์ ํ ์คํธ ์์ฑ | โ | Jest, Mocha ๋ฑ |
| CI/CD์ ๋ณด์ ํ ์คํธ ํฌํจ | โ | ์๋ํ |
| ์์กด์ฑ ์ทจ์ฝ์ ์ค์บ | โ | npm audit, Snyk |
| CORS ์ค์ | โ | ํ์ฉ ๋๋ฉ์ธ ์ง์ |
| ๋ณด์ ํค๋ ์ค์ | โ | Helmet.js |
| ์ ๊ธฐ ๋ณด์ ๊ฐ์ฌ ๊ณํ | โ | ๋ถ๊ธฐ๋ณ/์ฐ๊ฐ |
๐ ๋ง์น๋ฉฐ
์... ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐๐๐
GraphQL์ ์ ๋ง ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ ๋๊ตฌ์ผ. ํ์ง๋ง ํฐ ํ์๋ ํฐ ์ฑ
์์ด ๋ฐ๋ฅธ๋ค๋ ๊ฑฐ, ์์ง ๋ง! ๐ท๏ธ
๋ณด์์ ํ ๋ฒ ์ค์ ํ๊ณ ๋๋๋ ๊ฒ ์๋์ผ. ์ง์์ ์ธ ๊ด์ฌ๊ณผ ์
๋ฐ์ดํธ๊ฐ ํ์ํด.
์๋ก์ด ์ทจ์ฝ์ ์ด ๋ฐ๊ฒฌ๋๋ฉด ๋น ๋ฅด๊ฒ ํจ์นํ๊ณ , ์ ๊ธฐ์ ์ผ๋ก ๋ณด์ ํ
์คํธ๋ฅผ ํด์ผ ํด. ๐
ํน์ GraphQL ๋ณด์์ ๋ํด ๋ ๋ฐฐ์ฐ๊ณ ์ถ๊ฑฐ๋, ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐ ๋์์ด ํ์ํ๋ค๋ฉด?
์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ์ ๋ฌธ๊ฐ๋ค์ ๋์์ ๋ฐ์ ์ ์์ด! ๐ก
๋ณด์ ์ปจ์คํ
๋ถํฐ ์ฝ๋ ๋ฆฌ๋ทฐ, ์ค์ ํ๋ก์ ํธ ๋ฉํ ๋ง๊น์ง ๋ค์ํ ์ฌ๋ฅ์ ๊ฑฐ๋ํ ์ ์๊ฑฐ๋ . ๐
๋ํ ์ฌ๋ฅ๋ท์ '์ง์์ธ์ ์ฒ'์๋ ์ด๋ฐ ์ ์ฉํ ๊ฐ๋ฐ ์ง์๋ค์ด ๊ฐ๋ํด!
๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ๊ฒฝํ๊ณผ ๋
ธํ์ฐ๋ ๋ฐฐ์ธ ์ ์์ผ๋ ๊ผญ ๋ค๋ฌ๋ด! ๐ณ
๊ธฐ์ตํด! ๋ณด์์ ์ ํ์ด ์๋๋ผ ํ์์ผ! ๐ก๏ธ
์ค๋ ๋ฐฐ์ด ๋ด์ฉ๋ค์ ์ค์ฒํด์, ์์ ํ๊ณ ๊ฒฌ๊ณ ํ GraphQL API๋ฅผ ๋ง๋ค์ด๋ณด์! ๐ช
๊ทธ๋ผ ์์ ํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! ํดํผ ํดํน! (๋ฌผ๋ก ํ์ดํธํ์ผ๋ก! ๐)
๋ค์์ ๋ ๋ง๋์! ๐โจ
๐ฏ ํต์ฌ ์์ฝ
GraphQL ๋ณด์์ 5๋ ์์น
1๏ธโฃ ์ฟผ๋ฆฌ ๋ณต์ก๋๋ฅผ ์ ํํ๋ผ
2๏ธโฃ ์ธ์ฆ๊ณผ ๊ถํ์ ์ฒ ์ ํ ํ๋ผ
3๏ธโฃ ์
๋ ฅ๊ฐ์ ํญ์ ๊ฒ์ฆํ๋ผ
4๏ธโฃ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
์ ๊ฒ์๋ฆฌํ์ง ๋ง๋ผ
5๏ธโฃ ์ ๊ธฐ์ ์ผ๋ก ๋ณด์ ํ
์คํธ๋ฅผ ํ๋ผ
Stay Safe, Stay Secure! ๐ก๏ธ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.