์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›ก๏ธ 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 API DoS Injection Info Leak DEFENSE LAYER Security Vulnerabilities Attack Vectors and Protection Strategies

โš ๏ธ GraphQL์˜ ์ฃผ์š” ๋ณด์•ˆ ์ทจ์•ฝ์ ๋“ค

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ GraphQL์˜ ๋ณด์•ˆ ์ทจ์•ฝ์ ๋“ค์„ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด์ž! ๐Ÿ”
๊ฐ๊ฐ์˜ ์ทจ์•ฝ์ ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€, ์‹ค์ œ๋กœ ์–ด๋–ค ํ”ผํ•ด๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„์•ผ ์ œ๋Œ€๋กœ ๋ง‰์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿ’ช

1๏ธโƒฃ ๊ณผ๋„ํ•œ ์ฟผ๋ฆฌ ๋ณต์žก๋„ (Query Complexity Attack)

์ด๊ฑด GraphQL์˜ ๊ฐ€์žฅ ์น˜๋ช…์ ์ธ ์•ฝ์  ์ค‘ ํ•˜๋‚˜์•ผ. ๐Ÿ˜ฐ

GraphQL์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์›ํ•˜๋Š” ๋Œ€๋กœ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋ณต์žกํ•˜๊ณ  ๊นŠ์€ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ด๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋Ÿฐ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ธ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด:

query {
  user(id: 1) {
    posts {
      comments {
        author {
          posts {
            comments {
              author {
                posts {
                  comments {
                    # ์ด๊ฒŒ ๊ณ„์† ๋ฐ˜๋ณต๋œ๋‹ค๋ฉด...? ๐Ÿ˜ฑ
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

์ด๋Ÿฐ ์‹์œผ๋กœ ๊ด€๊ณ„๋ฅผ ํƒ€๊ณ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€๋ฉด์„œ ์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์ˆ˜์‹ญ, ์ˆ˜๋ฐฑ ๋ ˆ๋ฒจ๊นŒ์ง€ ๊ฐˆ ์ˆ˜ ์žˆ์–ด.
์„œ๋ฒ„๋Š” ์ด ์ฟผ๋ฆฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ˆ˜๋ฐฑ๋งŒ ๋ฒˆ ์กฐํšŒํ•˜๊ฒŒ ๋˜๊ณ ... ๐Ÿ’ฅ

๊ฒฐ๊ณผ? ์„œ๋ฒ„๊ฐ€ ๋ป—์–ด๋ฒ„๋ฆฌ๋Š” ๊ฑฐ์ง€! ์ „ํ˜•์ ์ธ DoS(Denial of Service) ๊ณต๊ฒฉ์ด์•ผ. ๐Ÿ”ฅ

๐Ÿšจ ์‹ค์ œ ํ”ผํ•ด ์‚ฌ๋ก€ 2019๋…„, ํ•œ ์œ ๋ช… ์†Œ์…œ ๋ฏธ๋””์–ด ํ”Œ๋žซํผ์—์„œ ์ด๋Ÿฐ ๊ณต๊ฒฉ์œผ๋กœ ์ธํ•ด 30๋ถ„๊ฐ„ ์„œ๋น„์Šค๊ฐ€ ๋งˆ๋น„๋œ ์ ์ด ์žˆ์–ด.
๋‹จ ํ•˜๋‚˜์˜ ์•…์˜์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ์ˆ˜์ฒœ ๋ช…์˜ ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ํ–ฅ์„ ์ค€ ๊ฑฐ์ง€. ๐Ÿ˜ข
๐Ÿ›ก๏ธ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•
1) Query Depth Limiting (์ฟผ๋ฆฌ ๊นŠ์ด ์ œํ•œ) ์ค‘์ฒฉ ๊นŠ์ด๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฑฐ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด ์ตœ๋Œ€ 5๋‹จ๊ณ„๊นŒ์ง€๋งŒ ํ—ˆ์šฉํ•˜๋Š” ์‹์œผ๋กœ!

๊ตฌํ˜„ ์˜ˆ์‹œ (Node.js + graphql-depth-limit):
const depthLimit = require('graphql-depth-limit');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)] // ์ตœ๋Œ€ 5๋‹จ๊ณ„๊นŒ์ง€๋งŒ ํ—ˆ์šฉ
});
2) Query Complexity Analysis (์ฟผ๋ฆฌ ๋ณต์žก๋„ ๋ถ„์„) ๊ฐ ํ•„๋“œ์— ๋ณต์žก๋„ ์ ์ˆ˜๋ฅผ ๋งค๊ธฐ๊ณ , ์ด ์ ์ˆ˜๊ฐ€ ์ž„๊ณ„๊ฐ’์„ ๋„˜์œผ๋ฉด ์ฐจ๋‹จํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ.

๊ตฌํ˜„ ์˜ˆ์‹œ:
const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => {
        console.log('Query cost:', cost);
      }
    })
  ]
});
3) Query Timeout (์ฟผ๋ฆฌ ํƒ€์ž„์•„์›ƒ) ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ๊ฐ„์„ ์ œํ•œํ•˜๋Š” ๊ฑฐ์•ผ. ์ผ์ • ์‹œ๊ฐ„ ๋‚ด์— ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด ์ž๋™์œผ๋กœ ์ค‘๋‹จ!
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);
              }
            };
          }
        };
      }
    }
  ]
});
2๏ธโƒฃ ๋ฐฐ์น˜ ๊ณต๊ฒฉ (Batching Attack)

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! ๐Ÿ’€

๐Ÿ›ก๏ธ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•
1) ๋ฐฐ์น˜ ํฌ๊ธฐ ์ œํ•œ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ฟผ๋ฆฌ ๊ฐœ์ˆ˜๋ฅผ ์ œํ•œํ•˜์ž!
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
}));
2) Rate Limiting (์†๋„ ์ œํ•œ) IP๋‹น ๋˜๋Š” ์‚ฌ์šฉ์ž๋‹น ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฑฐ์•ผ. ์ด๊ฑด GraphQL๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ชจ๋“  API์— ํ•„์ˆ˜! ๐Ÿ”
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);
3๏ธโƒฃ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๋‚จ์šฉ (Introspection Abuse)

GraphQL์˜ ๋ฉ‹์ง„ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์ธํŠธ๋กœ์ŠคํŽ™์…˜(Introspection)์ด์•ผ! ๐ŸŽญ

์ด๊ฑด API์˜ ์ „์ฒด ์Šคํ‚ค๋งˆ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ธ๋ฐ, ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ์—„์ฒญ ์œ ์šฉํ•ด.
GraphiQL์ด๋‚˜ GraphQL Playground ๊ฐ™์€ ๋„๊ตฌ๋“ค์ด ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด์„œ ์ž๋™์™„์„ฑ์„ ์ œ๊ณตํ•˜๊ฑฐ๋“ . ๐Ÿ‘

๊ทผ๋ฐ ๋ฌธ์ œ๋Š”... ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋„ ์ด๊ฒŒ ์ผœ์ ธ ์žˆ์œผ๋ฉด ๊ณต๊ฒฉ์ž๊ฐ€ API์˜ ๋ชจ๋“  ๊ตฌ์กฐ๋ฅผ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ! ๐Ÿ˜ฑ

์ด๋Ÿฐ ์ฟผ๋ฆฌ๋กœ ์ „์ฒด ์Šคํ‚ค๋งˆ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด:

query IntrospectionQuery {
  __schema {
    types {
      name
      fields {
        name
        type {
          name
        }
      }
    }
  }
}

์ด๊ฑธ๋กœ ๊ณต๊ฒฉ์ž๋Š”:
โœ… ์–ด๋–ค ํƒ€์ž…๋“ค์ด ์žˆ๋Š”์ง€
โœ… ์–ด๋–ค ํ•„๋“œ๋“ค์ด ์žˆ๋Š”์ง€
โœ… ์–ด๋–ค ์ฟผ๋ฆฌ์™€ ๋ฎคํ…Œ์ด์…˜์ด ๊ฐ€๋Šฅํ•œ์ง€
โœ… ์‹ฌ์ง€์–ด ์ˆจ๊ฒจ์ง„ ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ๊นŒ์ง€!

๋ชจ๋“  ๊ฑธ ๋‹ค ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ์ง€. ๋งˆ์น˜ ๊ฑด๋ฌผ ์„ค๊ณ„๋„๋ฅผ ๋„๋‘‘์—๊ฒŒ ์ฃผ๋Š” ๊ฒฉ์ด์•ผ! ๐Ÿ—๏ธ

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ฐœ๋ฐœ ํŽธ์˜์„ฑ ๋•Œ๋ฌธ์— ํ”„๋กœ๋•์…˜์—์„œ๋„ ์ธํŠธ๋กœ์ŠคํŽ™์…˜์„ ์ผœ๋†“๋Š” ์‹ค์ˆ˜๋ฅผ ํ•ด.
์ด๊ฑด ์ •๋ง ์œ„ํ—˜ํ•œ ์Šต๊ด€์ด์•ผ! ๐Ÿšซ
๐Ÿ›ก๏ธ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•
1) ํ”„๋กœ๋•์…˜์—์„œ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๋น„ํ™œ์„ฑํ™” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•!
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production', // ํ”„๋กœ๋•์…˜์—์„œ๋Š” OFF
  playground: process.env.NODE_ENV !== 'production'
});
2) ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ํ—ˆ์šฉ ์™„์ „ํžˆ ๋„๊ธฐ ๋ถ€๋‹ด์Šค๋Ÿฝ๋‹ค๋ฉด, ์ธ์ฆ๋œ ๊ด€๋ฆฌ์ž์—๊ฒŒ๋งŒ ํ—ˆ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด.
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');
              }
            }
          }
        };
      }
    }
  ]
});
4๏ธโƒฃ ์ธ์ ์…˜ ๊ณต๊ฒฉ (Injection Attacks)

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
  }
}

์ด๊ฑด... ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”์„ ํ†ต์งธ๋กœ ๋‚ ๋ ค๋ฒ„๋ฆฌ๋Š” ๊ฑฐ์•ผ! ๐Ÿ’ฃ

๐Ÿ›ก๏ธ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•
1) Parameterized Queries (๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ) ์ ˆ๋Œ€๋กœ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ง์ ‘ ์ฟผ๋ฆฌ์— ๋„ฃ์ง€ ๋งˆ! ํ•ญ์ƒ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด.
// โœ… ์•ˆ์ „ํ•œ ์ฝ”๋“œ
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      // ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ
      const query = 'SELECT * FROM users WHERE id = ?';
      return await db.query(query, [id]);
    }
  }
};
2) ORM ์‚ฌ์šฉ Sequelize, TypeORM, Prisma ๊ฐ™์€ ORM์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ด์ค˜!
// Prisma ์˜ˆ์‹œ
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return await context.prisma.user.findUnique({
        where: { id: parseInt(id) }
      });
    }
  }
};
3) ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ํƒ€์ž… ์ฒดํฌ GraphQL์˜ ํƒ€์ž… ์‹œ์Šคํ…œ์„ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜์ž!
// ์Šคํ‚ค๋งˆ์—์„œ ํƒ€์ž… ๋ช…์‹œ
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 }
      });
    }
  }
};
5๏ธโƒฃ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ์šฐํšŒ (Authentication & Authorization Bypass)

์ด๊ฑด ์ •๋ง ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ์•ผ! ๐Ÿ˜ฐ

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       # ๐Ÿ˜ฑ๐Ÿ˜ฑ ์ฃผ๋ฏผ๋ฒˆํ˜ธ๊นŒ์ง€!
  }
}
๐Ÿšจ ์‹ค์ œ ์‚ฌ๋ก€ 2020๋…„, ํ•œ ํ•€ํ…Œํฌ ์Šคํƒ€ํŠธ์—…์—์„œ ์ด๋Ÿฐ ์ทจ์•ฝ์ ์œผ๋กœ ์ธํ•ด ์ˆ˜์ฒœ ๋ช…์˜ ์‚ฌ์šฉ์ž ๊ธˆ์œต ์ •๋ณด๊ฐ€ ๋…ธ์ถœ๋œ ์ ์ด ์žˆ์–ด.
ํšŒ์‚ฌ๋Š” ๊ฒฐ๊ตญ ๊ฑฐ์•ก์˜ ๋ฒŒ๊ธˆ์„ ๋ฌผ๊ณ  ์‹ ๋ขฐ๋ฅผ ์žƒ์—ˆ์ง€... ๐Ÿ˜ข
๐Ÿ›ก๏ธ ๋ฐฉ์–ด ๋ฐฉ๋ฒ•
1) Field-Level Authorization (ํ•„๋“œ ๋ ˆ๋ฒจ ๊ถŒํ•œ ์ฒดํฌ) ๊ฐ ํ•„๋“œ๋งˆ๋‹ค ๊ถŒํ•œ์„ ์ฒดํฌํ•ด์•ผ ํ•ด!
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;
    }
  }
};
2) Directive๋ฅผ ํ™œ์šฉํ•œ ๊ถŒํ•œ ์ฒดํฌ ๋” ๊น”๋”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด ์ปค์Šคํ…€ Directive๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž!
// ์Šคํ‚ค๋งˆ ์ •์˜
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);
    };
  }
}
3) GraphQL Shield ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ ์ด๋ฏธ ์ž˜ ๋งŒ๋“ค์–ด์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!
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)
});

๐Ÿ” ์ข…ํ•ฉ ๋ณด์•ˆ ์ „๋žต

์ž, ์ง€๊ธˆ๊นŒ์ง€ ๊ฐœ๋ณ„ ์ทจ์•ฝ์ ๋“ค์„ ๋ดค๋Š”๋ฐ, ์ด์ œ ์ด๊ฑธ ๋‹ค ํ•ฉ์ณ์„œ ์ข…ํ•ฉ์ ์ธ ๋ณด์•ˆ ์ „๋žต์„ ์„ธ์›Œ๋ณด์ž! ๐ŸŽฏ

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์—ฌ๋Ÿฌ ๋ฐฉ์–ด ๊ธฐ๋ฒ•์„ ๋ ˆ์ด์–ด์ฒ˜๋Ÿผ ์Œ“์•„์•ผ ํ•ด. ํ•˜๋‚˜๊ฐ€ ๋šซ๋ ค๋„ ๋‹ค๋ฅธ ๊ฒŒ ๋ง‰์•„์ฃผ๋Š” ์‹์œผ๋กœ ๋ง์ด์•ผ! ๐Ÿ›ก๏ธ

๐Ÿ—๏ธ ๋‹ค์ธต ๋ฐฉ์–ด ์•„ํ‚คํ…์ฒ˜ (Defense in Depth)
Layer 1: ๋„คํŠธ์›Œํฌ ๋ ˆ๋ฒจ โ€ข WAF (Web Application Firewall) ์„ค์ •
โ€ข DDoS ๋ฐฉ์–ด ์„œ๋น„์Šค (Cloudflare, AWS Shield ๋“ฑ)
โ€ข IP ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ/๋ธ”๋ž™๋ฆฌ์ŠคํŠธ
โ€ข Rate Limiting at CDN level
Layer 2: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ โ€ข Query complexity analysis
โ€ข Query depth limiting
โ€ข Query timeout
โ€ข Batch size limiting
โ€ข Rate limiting per user/IP
Layer 3: ์ธ์ฆ/์ธ๊ฐ€ ๋ ˆ๋ฒจ โ€ข JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ
โ€ข Field-level authorization
โ€ข Role-based access control (RBAC)
โ€ข Attribute-based access control (ABAC)
Layer 4: ๋ฐ์ดํ„ฐ ๋ ˆ๋ฒจ โ€ข Input validation
โ€ข 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 ์„ค์ •
โ€ข โœ… ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ตœ์†Œํ™”
โ€ข โœ… ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

์ด ์ •๋„๋ฉด ์›ฌ๋งŒํ•œ ๊ณต๊ฒฉ์€ ๋‹ค ๋ง‰์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช

๐Ÿ” ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

๋ณด์•ˆ ์„ค์ •์„ ๋‹ค ํ–ˆ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ! ๐Ÿ™…โ€โ™‚๏ธ

์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…์ด ์ •๋ง ์ค‘์š”ํ•ด. ๊ณต๊ฒฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ฑฐ๋“ ! ๐Ÿ‘€

๐Ÿ“Š ๋ฌด์—‡์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•ด์•ผ ํ• ๊นŒ?
1. ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ โ€ข ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„
โ€ข ๋А๋ฆฐ ์ฟผ๋ฆฌ (Slow queries)
โ€ข ์ฟผ๋ฆฌ ๋ณต์žก๋„ ๋ถ„ํฌ
โ€ข ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํžˆํŠธ ์ˆ˜

๊ฐ‘์ž๊ธฐ ์‘๋‹ต ์‹œ๊ฐ„์ด ๋Š˜์–ด๋‚˜๊ฑฐ๋‚˜ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๊ณต๊ฒฉ ์‹ ํ˜ธ์ผ ์ˆ˜ ์žˆ์–ด! ๐Ÿšจ
2. ์—๋Ÿฌ ํŒจํ„ด โ€ข ์ธ์ฆ ์‹คํŒจ ํšŸ์ˆ˜
โ€ข ๊ถŒํ•œ ๊ฑฐ๋ถ€ ํšŸ์ˆ˜
โ€ข ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ ๋นˆ๋„
โ€ข ํŠน์ • IP์—์„œ์˜ ์—๋Ÿฌ ์ง‘์ค‘

๊ฐ™์€ IP์—์„œ ๊ณ„์† ์ธ์ฆ ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด? ๋ธŒ๋ฃจํŠธํฌ์Šค ๊ณต๊ฒฉ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„! ๐Ÿ”“
3. ํŠธ๋ž˜ํ”ฝ ํŒจํ„ด โ€ข ์š”์ฒญ ๋นˆ๋„
โ€ข ๋ฐฐ์น˜ ์ฟผ๋ฆฌ ์‚ฌ์šฉ ํŒจํ„ด
โ€ข ํŠน์ • ์—”๋“œํฌ์ธํŠธ ์ง‘์ค‘๋„
โ€ข ๋น„์ •์ƒ์ ์ธ ์‹œ๊ฐ„๋Œ€ ํŠธ๋ž˜ํ”ฝ

์ƒˆ๋ฒฝ 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)๋ผ๊ณ  ํ•ด! ๐ŸŽฏ

๐ŸŽฏ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ์ด๋Ÿฐ ํ…Œ์ŠคํŠธ๋Š” ๋ฐ˜๋“œ์‹œ ์ž์‹ ์˜ ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋งŒ ํ•ด์•ผ ํ•ด!
๋‚จ์˜ ์„œ๋ฒ„๋ฅผ ๊ณต๊ฒฉํ•˜๋ฉด ๋ถˆ๋ฒ•์ด์•ผ! ๐Ÿ˜ฑ
์‹œ๋‚˜๋ฆฌ์˜ค 1: Query Depth Attack ํ…Œ์ŠคํŠธ ๊นŠ์€ ์ค‘์ฒฉ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ด์„œ depth limit์ด ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž!
// ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ (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();
์‹œ๋‚˜๋ฆฌ์˜ค 2: Batch Attack ํ…Œ์ŠคํŠธ ๋Œ€๋Ÿ‰์˜ ๋ฐฐ์น˜ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ด์„œ batch limit์ด ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ!
// 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();
์‹œ๋‚˜๋ฆฌ์˜ค 3: Rate Limiting ํ…Œ์ŠคํŠธ ์งง์€ ์‹œ๊ฐ„์— ๋งŽ์€ ์š”์ฒญ์„ ๋ณด๋‚ด์„œ rate limit ํ™•์ธ!
// 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();
์‹œ๋‚˜๋ฆฌ์˜ค 4: Authorization Bypass ํ…Œ์ŠคํŠธ ๊ถŒํ•œ ์—†์ด ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ์‹œ๋„!
// 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 ๋ณด์•ˆ ๋ฌธ์ œ๋ฅผ ๊ฒช์—ˆ๊ณ , ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๐Ÿ“š

์‚ฌ๋ก€ 1: GitHub์˜ GraphQL API ๋ณด์•ˆ ๊ฐ•ํ™” ๐Ÿ™

GitHub์€ 2016๋…„์— GraphQL API๋ฅผ ๊ณต๊ฐœํ–ˆ์–ด. ์„ธ๊ณ„์—์„œ ๊ฐ€์žฅ ํฐ ์ฝ”๋“œ ์ €์žฅ์†Œ๋‹ˆ๊นŒ ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•˜๊ฒ ์ง€? ๐Ÿ”

์ดˆ๊ธฐ ๋ฌธ์ œ์ :
โ€ข ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜
โ€ข ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ์šฐํšŒ ์‹œ๋„
โ€ข ๊ณผ๋„ํ•œ ๋ฐ์ดํ„ฐ ์š”์ฒญ

GitHub์˜ ํ•ด๊ฒฐ์ฑ…:

1) Node-based Rate Limiting ์ผ๋ฐ˜์ ์ธ ์š”์ฒญ ํšŸ์ˆ˜ ์ œํ•œ์ด ์•„๋‹ˆ๋ผ, ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋…ธ๋“œ(๊ฐ์ฒด) ์ˆ˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ œํ•œํ–ˆ์–ด!

์˜ˆ๋ฅผ ๋“ค์–ด:
โ€ข ๊ฐ„๋‹จํ•œ ์ฟผ๋ฆฌ: 1 ๋…ธ๋“œ ์†Œ๋น„
โ€ข ๋ณต์žกํ•œ ์ฟผ๋ฆฌ: 100 ๋…ธ๋“œ ์†Œ๋น„
โ€ข ์‹œ๊ฐ„๋‹น ์ตœ๋Œ€ 5,000 ๋…ธ๋“œ๊นŒ์ง€ ํ—ˆ์šฉ

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋Š” ์ž๋™์œผ๋กœ ์ œํ•œ๋˜๋Š” ๊ฑฐ์ง€! ๋˜‘๋˜‘ํ•˜์ง€? ๐Ÿง 
2) ์ฟผ๋ฆฌ ๋ณต์žก๋„ ๊ณ„์‚ฐ๊ธฐ ๊ฐ ํ•„๋“œ๋งˆ๋‹ค ๋ณต์žก๋„ ์ ์ˆ˜๋ฅผ ๋งค๊ธฐ๊ณ , ์ฟผ๋ฆฌ ์‹คํ–‰ ์ „์— ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด์„œ ์ฐจ๋‹จ!
// 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๋Š” ํ˜„์žฌ ํ•˜๋ฃจ์— ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์˜ ์š”์ฒญ์„ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์–ด! ๐Ÿš€

์‚ฌ๋ก€ 2: Shopify์˜ GraphQL ๋ณด์•ˆ ์ „๋žต ๐Ÿ›๏ธ

Shopify๋Š” ์ˆ˜๋ฐฑ๋งŒ ๊ฐœ์˜ ์˜จ๋ผ์ธ ์ƒ์ ์„ ์šด์˜ํ•˜๋Š” ํ”Œ๋žซํผ์ด์•ผ. ๋ณด์•ˆ์ด ๋šซ๋ฆฌ๋ฉด ํฐ์ผ๋‚˜๊ฒ ์ง€? ๐Ÿ’ฐ

Shopify๊ฐ€ ์ง๋ฉดํ•œ ๋ฌธ์ œ:
โ€ข ์•…์˜์ ์ธ ์ƒ์  ์ฃผ์ธ์ด ๋‹ค๋ฅธ ์ƒ์  ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ์‹œ๋„
โ€ข ๋Œ€๋Ÿ‰์˜ ์ œํ’ˆ ์ •๋ณด ์Šคํฌ๋ž˜ํ•‘
โ€ข API ๋‚จ์šฉ์œผ๋กœ ์ธํ•œ ์„œ๋ฒ„ ๋ถ€ํ•˜

Shopify์˜ ํ•ด๊ฒฐ์ฑ…:

1) ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๊ถŒํ•œ ์ฒดํฌ ๋ชจ๋“  ์ฟผ๋ฆฌ์—์„œ ํ˜„์žฌ ์ƒ์  ์ปจํ…์ŠคํŠธ๋ฅผ ํ™•์ธํ•˜๊ณ , ๊ถŒํ•œ์„ ์ฒ ์ €ํžˆ ๊ฒ€์ฆ!
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;
    }
  }
};
2) ์ฟผ๋ฆฌ ๋น„์šฉ ๊ณ„์‚ฐ (Query Cost) Shopify๋Š” ๊ฐ ์ฟผ๋ฆฌ์— "๋น„์šฉ"์„ ๋งค๊ธฐ๊ณ , ์ƒ์ ๋งˆ๋‹ค ํ• ๋‹น๋œ ์˜ˆ์‚ฐ ๋‚ด์—์„œ๋งŒ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ์–ด!

โ€ข ๋ฌด๋ฃŒ ํ”Œ๋žœ: ์‹œ๊ฐ„๋‹น 1,000 ํฌ์ธํŠธ
โ€ข ๊ธฐ๋ณธ ํ”Œ๋žœ: ์‹œ๊ฐ„๋‹น 5,000 ํฌ์ธํŠธ
โ€ข ํ”„๋ฆฌ๋ฏธ์—„ ํ”Œ๋žœ: ์‹œ๊ฐ„๋‹น 20,000 ํฌ์ธํŠธ
3) ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฐ•์ œ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ๊ฐ€๋Š” ๊ฑธ ๋ง‰๊ธฐ ์œ„ํ•ด, ๋ชจ๋“  ๋ฆฌ์ŠคํŠธ ์ฟผ๋ฆฌ์— ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ฐ•์ œํ–ˆ์–ด!
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% ์ด์ƒ ์ค„์˜€๊ณ , ์„œ๋ฒ„ ๋น„์šฉ๋„ ํฌ๊ฒŒ ์ ˆ๊ฐํ–ˆ์–ด! ๐Ÿ’ฐ

์‚ฌ๋ก€ 3: ํ•œ ์Šคํƒ€ํŠธ์—…์˜ ๋ฐ์ดํ„ฐ ์œ ์ถœ ์‚ฌ๊ณ  ๐Ÿ˜ฑ

์ด๊ฑด ์‹ค์ œ๋กœ ์žˆ์—ˆ๋˜ ์ผ์ธ๋ฐ... ์ด๋ฆ„์€ ๋ฐํž ์ˆ˜ ์—†์ง€๋งŒ ๊ตํ›ˆ์ด ๋˜๋Š” ์‚ฌ๋ก€์•ผ. ๐Ÿ˜ข

์‚ฌ๊ณ  ๊ฒฝ์œ„:
2021๋…„, ํ•œ ํ•€ํ…Œํฌ ์Šคํƒ€ํŠธ์—…์ด GraphQL API๋ฅผ ์ถœ์‹œํ–ˆ์–ด.
๊ฐœ๋ฐœ ์†๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด ๋ณด์•ˆ์€ "๋‚˜์ค‘์—" ํ•˜๊ธฐ๋กœ ํ–ˆ์ง€...

๊ทธ๋Ÿฐ๋ฐ ์ถœ์‹œ 2์ฃผ ๋งŒ์— ํ•ด์ปค๊ฐ€ ์ด๋Ÿฐ ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์–ด:

query {
  users {
    id
    name
    email
    bankAccount {
      accountNumber
      balance
      transactions {
        amount
        date
        description
      }
    }
  }
}

๋ฌธ์ œ๋Š”:
โŒ ์ธ์ฆ ์ฒดํฌ๊ฐ€ ์—†์—ˆ์Œ
โŒ ํ•„๋“œ ๋ ˆ๋ฒจ ๊ถŒํ•œ ์ฒดํฌ๊ฐ€ ์—†์—ˆ์Œ
โŒ ํŽ˜์ด์ง€๋„ค์ด์…˜์ด ์—†์–ด์„œ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ์—ˆ์Œ
โŒ Rate limiting์ด ์—†์—ˆ์Œ

๊ฒฐ๊ณผ:
โ€ข 5,000๋ช… ์ด์ƒ์˜ ์‚ฌ์šฉ์ž ๊ธˆ์œต ์ •๋ณด ์œ ์ถœ ๐Ÿ’ธ
โ€ข ๊ธˆ์œต๊ฐ๋…์› ์ œ์žฌ
โ€ข ๊ฑฐ์•ก์˜ ๋ฒŒ๊ธˆ
โ€ข ์„œ๋น„์Šค ์‹ ๋ขฐ๋„ ์ถ”๋ฝ
โ€ข ๊ฒฐ๊ตญ ์„œ๋น„์Šค ์ข…๋ฃŒ... ๐Ÿ˜ญ

๋ณด์•ˆ์€ "๋‚˜์ค‘์—"๊ฐ€ ์•„๋‹ˆ๋ผ "์ฒ˜์Œ๋ถ€ํ„ฐ" ํ•ด์•ผ ํ•ด! ๐Ÿšจ

๐Ÿ’ก ๊ตํ›ˆ 1. ๊ฐœ๋ฐœ ์ดˆ๊ธฐ๋ถ€ํ„ฐ ๋ณด์•ˆ์„ ๊ณ ๋ คํ•ด์•ผ ํ•ด
2. ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „ ๋ฐ˜๋“œ์‹œ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผ ํ•ด
3. ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋Š” ํŠนํžˆ ๋” ์‹ ๊ฒฝ ์จ์•ผ ํ•ด
4. ์™ธ๋ถ€ ๋ณด์•ˆ ๊ฐ์‚ฌ๋ฅผ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ

"๋นจ๋ฆฌ ๊ฐ€๋ ค๋ฉด ํ˜ผ์ž ๊ฐ€๊ณ , ๋ฉ€๋ฆฌ ๊ฐ€๋ ค๋ฉด ํ•จ๊ป˜ ๊ฐ€๋ผ"๋Š” ๋ง์ด ์žˆ์ž–์•„?
๋ณด์•ˆ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. ์ฒ˜์Œ์—” ๋А๋ ค ๋ณด์—ฌ๋„, ์ œ๋Œ€๋กœ ํ•˜๋ฉด ๊ฒฐ๊ตญ ๋” ๋นจ๋ฆฌ, ๋” ๋ฉ€๋ฆฌ ๊ฐˆ ์ˆ˜ ์žˆ์–ด! ๐Ÿƒโ€โ™‚๏ธ

๐ŸŽ“ ๋ณด์•ˆ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ์ด์ •๋ฆฌ

์ž, ์ด์ œ ๋งˆ์ง€๋ง‰์œผ๋กœ GraphQL ๋ณด์•ˆ์˜ ํ™ฉ๊ธˆ๋ฅ ๋“ค์„ ์ •๋ฆฌํ•ด๋ณด์ž! โœจ
์ด๊ฒƒ๋งŒ ์ง€์ผœ๋„ ๋Œ€๋ถ€๋ถ„์˜ ๊ณต๊ฒฉ์€ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ›ก๏ธ

๐Ÿฅ‡ ํ•„์ˆ˜ ๋ณด์•ˆ ์กฐ์น˜ (๋ฐ˜๋“œ์‹œ ํ•ด์•ผ ํ•ด!)
1. ํ”„๋กœ๋•์…˜์—์„œ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๋น„ํ™œ์„ฑํ™” introspection: false

์ด๊ฑด ์ •๋ง ๊ธฐ๋ณธ ์ค‘์˜ ๊ธฐ๋ณธ! ๊ฐœ๋ฐœํ•  ๋•Œ๋งŒ ์ผœ๊ณ , ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ๊บผ์•ผ ํ•ด! ๐Ÿ”’
2. Query Depth & Complexity Limiting โ€ข Depth: ์ตœ๋Œ€ 5-7 ๋ ˆ๋ฒจ
โ€ข Complexity: ํ”„๋กœ์ ํŠธ์— ๋งž๊ฒŒ ์„ค์ • (๋ณดํ†ต 1000-5000)

์ด๊ฒƒ๋งŒ ํ•ด๋„ DoS ๊ณต๊ฒฉ์˜ 90%๋Š” ๋ง‰์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช
3. Rate Limiting โ€ข IP ๊ธฐ๋ฐ˜: ๋ถ„๋‹น 100-200 ์š”์ฒญ
โ€ข ์‚ฌ์šฉ์ž ๊ธฐ๋ฐ˜: ๋ถ„๋‹น 50-100 ์š”์ฒญ
โ€ข ๋ฐฐ์น˜ ํฌ๊ธฐ: ์ตœ๋Œ€ 10-20๊ฐœ

๊ณผ์œ ๋ถˆ๊ธ‰! ๋„ˆ๋ฌด ๋นก๋นกํ•˜๋ฉด ์ •์ƒ ์‚ฌ์šฉ์ž๋„ ๋ถˆํŽธํ•ด์ ธ. ์ ์ ˆํ•œ ๋ฐธ๋Ÿฐ์Šค๊ฐ€ ์ค‘์š”ํ•ด! โš–๏ธ
4. ํ•„๋“œ ๋ ˆ๋ฒจ ๊ถŒํ•œ ์ฒดํฌ ๋ฏผ๊ฐํ•œ ํ•„๋“œ๋Š” ๋ฐ˜๋“œ์‹œ ๊ฐ๊ฐ ๊ถŒํ•œ์„ ์ฒดํฌํ•ด์•ผ ํ•ด!

ํŠนํžˆ ์ด๋Ÿฐ ํ•„๋“œ๋“ค:
โ€ข ๊ฐœ์ธ์ •๋ณด (์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, ์ฃผ์†Œ)
โ€ข ๊ธˆ์œต์ •๋ณด (๊ณ„์ขŒ๋ฒˆํ˜ธ, ์นด๋“œ๋ฒˆํ˜ธ, ์ž”์•ก)
โ€ข ์ธ์ฆ์ •๋ณด (๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ, API ํ‚ค)
โ€ข ๋‚ด๋ถ€์ •๋ณด (๊ธ‰์—ฌ, ํ‰๊ฐ€, ๋‚ด๋ถ€ ๋ฉ”๋ชจ)
5. HTTPS ์‚ฌ์šฉ ๋‹น์—ฐํ•œ ๊ฑฐ์ง€๋งŒ... ์•„์ง๋„ HTTP ์“ฐ๋Š” ๊ณณ์ด ์žˆ๋”๋ผ๊ณ ? ๐Ÿ˜ฑ
Let's Encrypt๋กœ ๋ฌด๋ฃŒ SSL ์ธ์ฆ์„œ ๋ฐ›์•„์„œ ๊ผญ HTTPS ์“ฐ์ž! ๐Ÿ”
๐Ÿฅˆ ๊ถŒ์žฅ ๋ณด์•ˆ ์กฐ์น˜ (ํ•˜๋ฉด ๋” ์ข‹์•„!)
6. Query Timeout 5-10์ดˆ ์ •๋„๋กœ ์„ค์ •ํ•˜๋ฉด ์ข‹์•„. ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ฟผ๋ฆฌ๋Š” ๋ญ”๊ฐ€ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๊ฑฐ๋‹ˆ๊นŒ! โฑ๏ธ
7. ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ Sanitization ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ์ ˆ๋Œ€ ๋ฏฟ์ง€ ๋งˆ! ํ•ญ์ƒ ๊ฒ€์ฆํ•˜๊ณ  ์ •์ œํ•ด์•ผ ํ•ด.

โ€ข ํƒ€์ž… ์ฒดํฌ
โ€ข ๊ธธ์ด ์ œํ•œ
โ€ข ํŠน์ˆ˜๋ฌธ์ž ํ•„ํ„ฐ๋ง
โ€ข SQL Injection ๋ฐฉ์–ด
8. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ตœ์†Œํ™” ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋…ธ์ถœํ•˜์ง€ ๋งˆ!
๊ณต๊ฒฉ์ž์—๊ฒŒ ํžŒํŠธ๋ฅผ ์ฃผ๋Š” ๊ฑฐ์•ผ. ๐Ÿ•ต๏ธ

โŒ "User with email 'admin@company.com' not found"
โœ… "Invalid credentials"
9. ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง โ€ข ๋ชจ๋“  ์ฟผ๋ฆฌ ๋กœ๊น…
โ€ข ์—๋Ÿฌ ํŒจํ„ด ๋ถ„์„
โ€ข ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ์•Œ๋ฆผ
โ€ข ์ •๊ธฐ์ ์ธ ๋กœ๊ทธ ๊ฒ€ํ† 
10. ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ โ€ข ๋ถ„๊ธฐ๋ณ„ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
โ€ข ์˜์กด์„ฑ ์ทจ์•ฝ์  ์Šค์บ” (npm audit)
โ€ข ์ฝ”๋“œ ๋ฆฌ๋ทฐ์— ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํฌํ•จ
โ€ข ์™ธ๋ถ€ ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€ ๊ฐ์‚ฌ (์—ฐ 1ํšŒ ์ด์ƒ)
๐Ÿฅ‰ ๊ณ ๊ธ‰ ๋ณด์•ˆ ์กฐ์น˜ (์—ฌ์œ  ์žˆ์œผ๋ฉด!)
11. Query Whitelisting ๋ฏธ๋ฆฌ ์ •์˜๋œ ์ฟผ๋ฆฌ๋งŒ ํ—ˆ์šฉํ•˜๋Š” ๋ฐฉ์‹. ๊ฐ€์žฅ ์•ˆ์ „ํ•˜์ง€๋งŒ ์œ ์—ฐ์„ฑ์ด ๋–จ์–ด์ ธ.
ํŠนํžˆ ๋ฏผ๊ฐํ•œ ์„œ๋น„์Šค์— ์ ํ•ฉํ•ด! ๐Ÿ”’
12. Persisted Queries ์ฟผ๋ฆฌ๋ฅผ ํ•ด์‹œ๋กœ ์ €์žฅํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๋Š” ํ•ด์‹œ๋งŒ ๋ณด๋‚ด๋Š” ๋ฐฉ์‹.
์ž„์˜์˜ ์ฟผ๋ฆฌ ์‹คํ–‰์„ ์›์ฒœ ์ฐจ๋‹จ! ๐Ÿšซ
13. API Gateway ํ™œ์šฉ Kong, AWS API Gateway ๊ฐ™์€ ๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด:
โ€ข ์ค‘์•™ํ™”๋œ ๋ณด์•ˆ ๊ด€๋ฆฌ
โ€ข ํŠธ๋ž˜ํ”ฝ ๋ถ„์„
โ€ข DDoS ๋ฐฉ์–ด
โ€ข ์บ์‹ฑ
14. ๋ณด์•ˆ ํ—ค๋” ์„ค์ • Helmet.js ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋ณด์•ˆ ํ—ค๋”๋ฅผ ์„ค์ •ํ•˜์ž!
โ€ข 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.