์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›ก๏ธ GraphQL API ๋ณด์•ˆ, ์ด์ œ ์ œ๋Œ€๋กœ ์•Œ์•„๋ณด์ž!

๐Ÿ›ก๏ธ GraphQL API ๋ณด์•ˆ, ์ด์ œ ์ œ๋Œ€๋กœ ์•Œ์•„๋ณด์ž!

์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ๋ฐฐ์šฐ๋Š” GraphQL ๋ณด์•ˆ Best Practices

์•ˆ๋…•! ๐Ÿ‘‹ ์š”์ฆ˜ GraphQL ์“ฐ๋Š” ๊ฐœ๋ฐœ์ž๋“ค ์ •๋ง ๋งŽ์•„์กŒ์ง€? REST API์˜ over-fetching, under-fetching ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด์ฃผ๋Š” GraphQL์€ ์ •๋ง ๋งค๋ ฅ์ ์ธ ๊ธฐ์ˆ ์ด์•ผ. ๊ทผ๋ฐ ๋ง์ด์•ผ... ๊ฐ•๋ ฅํ•œ ๋งŒํผ ๋ณด์•ˆ ์œ„ํ—˜๋„ ํ•จ๊ป˜ ๋”ฐ๋ผ์˜จ๋‹ค๋Š” ๊ฑฐ ์•Œ๊ณ  ์žˆ์–ด? ๐Ÿ˜ฐ

์˜ค๋Š˜์€ GraphQL API๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์šด์˜ํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ๋ณด์•ˆ ์ „๋žต๋“ค์„ ์นœ๊ตฌํ•œํ…Œ ์„ค๋ช…ํ•˜๋“ฏ์ด ์‰ฝ๊ณ  ์žฌ๋ฏธ์žˆ๊ฒŒ ํ’€์–ด๋ณผ๊ฒŒ. ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค๋„ ๊ผญ ์•Œ์•„์•ผ ํ•  ๋‚ด์šฉ์ด๋‹ˆ๊นŒ ์ง‘์ค‘ํ•ด์ค˜! ๐ŸŽฏ

GraphQL ๋ณด์•ˆ ์ธ์ฆ ๊ถŒํ•œ ๊ฒ€์ฆ ์ œํ•œ 4๊ฐ€์ง€ ํ•ต์‹ฌ ๋ณด์•ˆ ์˜์—ญ

๐Ÿค” GraphQL์ด ์™œ ๋ณด์•ˆ์— ์ทจ์•ฝํ• ๊นŒ?

๋จผ์ € GraphQL์˜ ํŠน์„ฑ๋ถ€ํ„ฐ ์ดํ•ดํ•ด์•ผ ํ•ด. REST API๋Š” ์—”๋“œํฌ์ธํŠธ๋งˆ๋‹ค ์ •ํ•ด์ง„ ๋ฐ์ดํ„ฐ๋งŒ ์ฃผ๊ณ ๋ฐ›์ž–์•„? ๊ทผ๋ฐ GraphQL์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์ฟผ๋ฆฌ๋กœ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์–ด. ์ด๊ฒŒ ์žฅ์ ์ด๋ฉด์„œ ๋™์‹œ์— ๋ณด์•ˆ ๋ฆฌ์Šคํฌ๊ฐ€ ๋˜๋Š” ๊ฑฐ์•ผ. ๐Ÿ˜…

๐ŸŽฏ GraphQL์˜ ์ฃผ์š” ๋ณด์•ˆ ์ทจ์•ฝ์ 

1. ๊ณผ๋„ํ•œ ์ฟผ๋ฆฌ ๋ณต์žก๋„ ๐Ÿ’ฃ
์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ๋ณต์žกํ•œ ์ค‘์ฒฉ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ด์„œ ์„œ๋ฒ„๋ฅผ ๋งˆ๋น„์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž โ†’ ๊ฒŒ์‹œ๊ธ€ โ†’ ๋Œ“๊ธ€ โ†’ ์ž‘์„ฑ์ž โ†’ ๊ฒŒ์‹œ๊ธ€... ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฌดํ•œ ๋ฐ˜๋ณต๋˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ง€.

2. Introspection ๋…ธ์ถœ ๐Ÿ”
GraphQL์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์Šคํ‚ค๋งˆ ์ „์ฒด๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋Š” introspection ๊ธฐ๋Šฅ์ด ์ผœ์ ธ ์žˆ์–ด. ๊ณต๊ฒฉ์ž๊ฐ€ API ๊ตฌ์กฐ๋ฅผ ์™„์ „ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด์•ผ.

3. ์ธ์ฆ/๊ถŒํ•œ ๋ถ€์žฌ ๐Ÿšช
ํ•„๋“œ ๋ ˆ๋ฒจ์—์„œ ์„ธ๋ฐ€ํ•œ ๊ถŒํ•œ ์ œ์–ด๋ฅผ ํ•˜์ง€ ์•Š์œผ๋ฉด, ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด.

4. ๋ฐฐ์น˜ ๊ณต๊ฒฉ ๐ŸŽฒ
์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ํ•œ ๋ฒˆ์— ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ๋ฐฐ์น˜ ๊ธฐ๋Šฅ์„ ์•…์šฉํ•ด์„œ Rate Limiting์„ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์–ด.

โš ๏ธ ์‹ค์ œ ์‚ฌ๋ก€

2019๋…„, ํ•œ ์œ ๋ช… ์†Œ์…œ ๋ฏธ๋””์–ด ํ”Œ๋žซํผ์—์„œ GraphQL API์˜ ๊นŠ์ด ์ œํ•œ์ด ์—†์–ด์„œ ๊ณต๊ฒฉ์ž๊ฐ€ ์ค‘์ฒฉ ์ฟผ๋ฆฌ๋กœ ์„œ๋ฒ„๋ฅผ ๋‹ค์šด์‹œํ‚จ ์‚ฌ๊ฑด์ด ์žˆ์—ˆ์–ด. ๋‹จ ํ•˜๋‚˜์˜ ์š”์ฒญ์œผ๋กœ ์ˆ˜๋ฐฑ๋งŒ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋๋‹ค๊ณ  ํ•ด. ๐Ÿ˜ฑ

๐Ÿ” 1. ์ธ์ฆ(Authentication) ์ œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ

์ธ์ฆ์€ "๋„ˆ ๋ˆ„๊ตฌ์•ผ?"๋ฅผ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์ด์•ผ. GraphQL์—์„œ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ HTTP ํ—ค๋”๋ฅผ ํ†ตํ•ด ํ† ํฐ์„ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์„ ๋งŽ์ด ์จ. JWT(JSON Web Token)๊ฐ€ ๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ด์ง€! ๐ŸŽซ

โœ… JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ Best Practices

โ€ข Access Token + Refresh Token ์ „๋žต
Access Token์€ ์งง์€ ์œ ํšจ๊ธฐ๊ฐ„(15๋ถ„~1์‹œ๊ฐ„), Refresh Token์€ ๊ธด ์œ ํšจ๊ธฐ๊ฐ„(1์ฃผ~1๊ฐœ์›”)์œผ๋กœ ์„ค์ •ํ•ด. Access Token์ด ๋งŒ๋ฃŒ๋˜๋ฉด Refresh Token์œผ๋กœ ์ƒˆ๋กœ ๋ฐœ๊ธ‰๋ฐ›๋Š” ๊ตฌ์กฐ์•ผ.

โ€ข HTTP-Only ์ฟ ํ‚ค ์‚ฌ์šฉ
ํ† ํฐ์„ localStorage์— ์ €์žฅํ•˜๋ฉด XSS ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•ด. HTTP-Only ์ฟ ํ‚ค์— ์ €์žฅํ•˜๋ฉด JavaScript๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์–ด์„œ ํ›จ์”ฌ ์•ˆ์ „ํ•ด. ๐Ÿช

โ€ข ํ† ํฐ ํŽ˜์ด๋กœ๋“œ ์ตœ์†Œํ™”
JWT์—๋Š” ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ๋„ฃ์ง€ ๋งˆ! ์‚ฌ์šฉ์ž ID, ์—ญํ•  ์ •๋„๋งŒ ํฌํ•จํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ์„œ๋ฒ„์—์„œ ์กฐํšŒํ•ด.

โ€ข ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ฒ€์ฆ
ํ† ํฐ ๊ฒ€์ฆ ์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•ด์•ผ ํ•ด. none ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ’ก ์‹ค์ „ ํŒ

GraphQL Context์— ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด์•„๋‘๋ฉด ๋ชจ๋“  ๋ฆฌ์กธ๋ฒ„์—์„œ ์‰ฝ๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด. Apollo Server ๊ธฐ์ค€์œผ๋กœ ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    const token = req.headers.authorization || '';
    const user = await verifyToken(token);
    return { user };
  }
});

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐ ๋ฆฌ์กธ๋ฒ„์—์„œ context.user๋กœ ์ธ์ฆ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ„๋‹จํ•˜์ง€? ๐Ÿ˜Š

๐ŸŽญ 2. ๊ถŒํ•œ ๋ถ€์—ฌ(Authorization) ์„ธ๋ฐ€ํ•˜๊ฒŒ ์„ค์ •ํ•˜๊ธฐ

์ธ์ฆ์ด "๋ˆ„๊ตฌ์ธ์ง€" ํ™•์ธํ•˜๋Š” ๊ฑฐ๋ผ๋ฉด, ๊ถŒํ•œ ๋ถ€์—ฌ๋Š” "๋ฌด์—‡์„ ํ•  ์ˆ˜ ์žˆ๋Š”์ง€" ๊ฒฐ์ •ํ•˜๋Š” ๊ฑฐ์•ผ. GraphQL์—์„œ๋Š” ํ•„๋“œ ๋ ˆ๋ฒจ, ํƒ€์ž… ๋ ˆ๋ฒจ, ์ฟผ๋ฆฌ ๋ ˆ๋ฒจ์—์„œ ๋ชจ๋‘ ๊ถŒํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•ด์•ผ ํ•ด! ๐ŸŽฏ

๐Ÿ›ก๏ธ ๋‹ค์ธต ๊ถŒํ•œ ์ œ์–ด ์ „๋žต

1. ์Šคํ‚ค๋งˆ ๋ ˆ๋ฒจ ๊ถŒํ•œ
ํŠน์ • ํƒ€์ž…์ด๋‚˜ ํ•„๋“œ ์ž์ฒด๋ฅผ ์—ญํ• ์— ๋”ฐ๋ผ ์ˆจ๊ธธ ์ˆ˜ ์žˆ์–ด. ๊ด€๋ฆฌ์ž๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋Š” ํ•„๋“œ๋ฅผ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ์Šคํ‚ค๋งˆ์—์„œ๋Š” ์•„์˜ˆ ์ œ๊ฑฐํ•˜๋Š” ๊ฑฐ์ง€.

2. ๋ฆฌ์กธ๋ฒ„ ๋ ˆ๋ฒจ ๊ถŒํ•œ
๊ฐ ๋ฆฌ์กธ๋ฒ„์—์„œ ๊ถŒํ•œ์„ ์ฒดํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ๊ฐ€์žฅ ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์–ด.

3. ๋””๋ ‰ํ‹ฐ๋ธŒ ๊ธฐ๋ฐ˜ ๊ถŒํ•œ
@auth, @hasRole ๊ฐ™์€ ์ปค์Šคํ…€ ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์„ ์–ธ์ ์œผ๋กœ ๊ถŒํ•œ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด. ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์ง€๋Š” ์žฅ์ ์ด ์žˆ์ง€! โœจ

๋””๋ ‰ํ‹ฐ๋ธŒ ๋ฐฉ์‹์ด ๊ฐ€์žฅ ๊น”๋”ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์ข‹์•„. ์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ:

type User {
  id: ID!
  email: String!
  name: String!
  salary: Float @hasRole(requires: ADMIN)
  ssn: String @hasRole(requires: ADMIN)
}

type Query {
  users: [User!]! @auth
  adminStats: Stats! @hasRole(requires: ADMIN)
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด salary์™€ ssn ํ•„๋“œ๋Š” ๊ด€๋ฆฌ์ž๋งŒ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด. ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•˜๋ฉด ํ•ด๋‹น ํ•„๋“œ๋Š” null์ด ๋˜๊ฑฐ๋‚˜ ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€. ๐Ÿšซ

โœจ ๊ถŒํ•œ ์ฒดํฌ ๊ตฌํ˜„ ์˜ˆ์‹œ

๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ์ด๋Ÿฐ ๋А๋‚Œ์ด์•ผ:

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    field.resolve = async function (...args) {
      const context = args[2];
      
      if (!context.user) {
        throw new AuthenticationError('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค');
      }
      
      const requiredRole = this.args.requires;
      if (requiredRole && !context.user.roles.includes(requiredRole)) {
        throw new ForbiddenError('๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค');
      }
      
      return resolve.apply(this, args);
    };
  }
}

์žฌ๋Šฅ๋„ท์—์„œ ๋ณด์•ˆ ๊ด€๋ จ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค๋„ ์ด๋Ÿฐ ํŒจํ„ด์„ ๋งŽ์ด ์ถ”์ฒœํ•ด. ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋†’๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋„ ์‰ฝ๊ฑฐ๋“ ! ๐Ÿ‘

์‚ฌ์šฉ์ž ์š”์ฒญ Query + Token ์ธ์ฆ ๊ฒ€์ฆ JWT ํ™•์ธ ๊ถŒํ•œ ๊ฒ€์ฆ Role ์ฒดํฌ ํ† ํฐ ์ „๋‹ฌ ์ธ์ฆ ์„ฑ๊ณต์‹œ โœ“ ์ ‘๊ทผ ํ—ˆ์šฉ โœ— ์ ‘๊ทผ ๊ฑฐ๋ถ€

โšก 3. ์ฟผ๋ฆฌ ๋ณต์žก๋„ ์ œํ•œํ•˜๊ธฐ

์ด๊ฒŒ GraphQL ๋ณด์•ˆ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋ฒ„๋ฅผ ๋‹ค์šด์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•์ด ๋ฐ”๋กœ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋‚ด๋Š” ๊ฑฐ๊ฑฐ๋“ . ๐Ÿ˜ˆ

๐ŸŽฏ ์ฟผ๋ฆฌ ๋ณต์žก๋„ ์ œ์–ด ๋ฐฉ๋ฒ•

1. ๊นŠ์ด(Depth) ์ œํ•œ
์ค‘์ฒฉ ์ฟผ๋ฆฌ์˜ ๊นŠ์ด๋ฅผ ์ œํ•œํ•ด. ๋ณดํ†ต 5~7 ๋ ˆ๋ฒจ ์ •๋„๊ฐ€ ์ ๋‹นํ•ด. ์ด๊ฒƒ๋งŒ์œผ๋กœ๋„ ๋Œ€๋ถ€๋ถ„์˜ ์•…์˜์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด.

2. ๋ณต์žก๋„(Complexity) ์ ์ˆ˜
๊ฐ ํ•„๋“œ์— ๋น„์šฉ ์ ์ˆ˜๋ฅผ ๋งค๊ธฐ๊ณ , ์ „์ฒด ์ฟผ๋ฆฌ์˜ ์ ์ˆ˜ ํ•ฉ๊ณ„๋ฅผ ์ œํ•œํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹จ์ˆœ ํ•„๋“œ๋Š” 1์ , ๋ฆฌ์ŠคํŠธ๋Š” 10์ , ๊ด€๊ณ„ ์กฐํšŒ๋Š” 5์  ์ด๋Ÿฐ ์‹์œผ๋กœ.

3. ๋ฐฐ์—ด ํฌ๊ธฐ ์ œํ•œ
๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•„๋“œ์—๋Š” ๋ฐ˜๋“œ์‹œ first, limit ๊ฐ™์€ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ธ์ž๋ฅผ ๊ฐ•์ œํ•ด์•ผ ํ•ด. ๋ฌด์ œํ•œ ์กฐํšŒ๋Š” ์ ˆ๋Œ€ ๊ธˆ๋ฌผ! ๐Ÿšซ

4. ํƒ€์ž„์•„์›ƒ ์„ค์ •
์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ๊ฐ„์— ์ œํ•œ์„ ๋‘ฌ. 5์ดˆ ์ด์ƒ ๊ฑธ๋ฆฌ๋Š” ์ฟผ๋ฆฌ๋Š” ์ž๋™์œผ๋กœ ์ทจ์†Œ๋˜๋„๋ก ์„ค์ •ํ•˜๋Š” ๊ฑฐ์ง€.

์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” graphql-depth-limit์ด๋‚˜ graphql-validation-complexity ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋ฉด ๋ผ:

import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(7), // ์ตœ๋Œ€ 7๋‹จ๊ณ„ ๊นŠ์ด
    createComplexityLimitRule(1000, {
      onCost: (cost) => {
        console.log('์ฟผ๋ฆฌ ๋ณต์žก๋„:', cost);
      }
    })
  ]
});

๐Ÿ’ก ๋ณต์žก๋„ ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ˆ์‹œ

๊ฐ ํ•„๋“œ์˜ ๋ณต์žก๋„๋ฅผ ์ด๋ ‡๊ฒŒ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด:

type Query {
  user(id: ID!): User @complexity(value: 1)
  users(first: Int!): [User!]! @complexity(multipliers: ["first"])
  posts(first: Int!): [Post!]! @complexity(multipliers: ["first"], value: 2)
}

type User {
  id: ID! @complexity(value: 0)
  name: String! @complexity(value: 1)
  posts(first: Int!): [Post!]! @complexity(multipliers: ["first"], value: 5)
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด users(first: 100)์€ 100์ , ๊ฑฐ๊ธฐ์— ๊ฐ ์‚ฌ์šฉ์ž์˜ posts(first: 50)์„ ์กฐํšŒํ•˜๋ฉด 100 ร— 50 ร— 5 = 25,000์ ์ด ๋˜๋Š” ๊ฑฐ์•ผ. ์ œํ•œ์„ 1000์ ์œผ๋กœ ์„ค์ •ํ–ˆ๋‹ค๋ฉด ์ด ์ฟผ๋ฆฌ๋Š” ๊ฑฐ๋ถ€๋˜๊ฒ ์ง€? ๐Ÿ›‘

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

๋ณต์žก๋„ ์ œํ•œ์„ ๋„ˆ๋ฌด ๋นก๋นกํ•˜๊ฒŒ ์„ค์ •ํ•˜๋ฉด ์ •์ƒ์ ์ธ ์‚ฌ์šฉ์ž๋„ ๋ถˆํŽธํ•ด์งˆ ์ˆ˜ ์žˆ์–ด. ์‹ค์ œ ์‚ฌ์šฉ ํŒจํ„ด์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋ฉด์„œ ์ ์ ˆํ•œ ๊ฐ’์„ ์ฐพ์•„์•ผ ํ•ด. ์ฒ˜์Œ์—๋Š” ์—ฌ์œ ์žˆ๊ฒŒ ์„ค์ •ํ•˜๊ณ  ์ ์ฐจ ์กฐ์ด๋Š” ๊ฒŒ ์ข‹์•„! ๐Ÿ“Š

๐Ÿ” 4. Introspection ์ œ์–ดํ•˜๊ธฐ

GraphQL์˜ introspection์€ ์ •๋ง ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์ด์•ผ. ๊ฐœ๋ฐœํ•  ๋•Œ GraphQL Playground๋‚˜ GraphiQL์—์„œ ์ž๋™์™„์„ฑ์ด ๋˜๋Š” ๊ฒŒ ๋‹ค ์ด ๊ธฐ๋Šฅ ๋•๋ถ„์ด๊ฑฐ๋“ . ๊ทผ๋ฐ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š”? ๊ณต๊ฒฉ์ž์—๊ฒŒ API ๊ตฌ์กฐ๋ฅผ ๋‹ค ์•Œ๋ ค์ฃผ๋Š” ๊ผด์ด ๋˜๋Š” ๊ฑฐ์ง€! ๐Ÿ˜ฑ

๐ŸŽฏ Introspection ๋ณด์•ˆ ์ „๋žต

1. ํ”„๋กœ๋•์…˜์—์„œ ์™„์ „ํžˆ ๋น„ํ™œ์„ฑํ™”
๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•์ด์•ผ. ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์—์„œ๋งŒ ์ผœ๊ณ  ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋„๋Š” ๊ฑฐ์ง€.

2. ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ํ—ˆ์šฉ
๋‚ด๋ถ€ ๊ฐœ๋ฐœ์ž๋‚˜ ํŠน์ • ์—ญํ• ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋งŒ introspection์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ œํ•œํ•ด.

3. Rate Limiting ์ ์šฉ
์™„์ „ํžˆ ๋„๊ธฐ ์–ด๋ ต๋‹ค๋ฉด introspection ์ฟผ๋ฆฌ์— ์—„๊ฒฉํ•œ Rate Limiting์„ ์ ์šฉํ•ด. ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋Š” ํ•˜๋ฃจ์— ๋ช‡ ๋ฒˆ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฑฐ์•ผ.

4. ๋ถ€๋ถ„์  ๋…ธ์ถœ
๋ฏผ๊ฐํ•œ ํƒ€์ž…์ด๋‚˜ ํ•„๋“œ๋งŒ ์ˆจ๊ธฐ๊ณ  ๋‚˜๋จธ์ง€๋Š” ๊ณต๊ฐœํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด. ํ•˜์ง€๋งŒ ์ด๊ฑด ๊ด€๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ์–ด.

Apollo Server์—์„œ introspection์„ ๋„๋Š” ๊ฑด ์ •๋ง ๊ฐ„๋‹จํ•ด:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  playground: process.env.NODE_ENV !== 'production'
});

ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์ œ์–ดํ•˜๋ฉด ๋ฐฐํฌ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์ผœ์ง€๊ณ  ๊บผ์ง€๋‹ˆ๊นŒ ํŽธํ•˜์ง€! ๐ŸŽ›๏ธ

โœจ ์กฐ๊ฑด๋ถ€ Introspection ๊ตฌํ˜„

์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ introspection์„ ํ—ˆ์šฉํ•˜๋ ค๋ฉด ์ด๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: true,
  plugins: [{
    requestDidStart() {
      return {
        didResolveOperation({ request, context }) {
          if (request.operationName === 'IntrospectionQuery') {
            if (!context.user || !context.user.roles.includes('DEVELOPER')) {
              throw new ForbiddenError('Introspection์€ ๊ฐœ๋ฐœ์ž๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค');
            }
          }
        }
      };
    }
  }]
});

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐœ๋ฐœ์ž ์—ญํ• ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋งŒ ์Šคํ‚ค๋งˆ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด. ์œ ์—ฐํ•˜๋ฉด์„œ๋„ ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•์ด์ง€! ๐Ÿ˜Š

๐Ÿšฆ 5. Rate Limiting๊ณผ Throttling

์•„๋ฌด๋ฆฌ ์ฟผ๋ฆฌ ๋ณต์žก๋„๋ฅผ ์ œํ•œํ•ด๋„, ๊ณต๊ฒฉ์ž๊ฐ€ ๋‹จ์ˆœํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ดˆ๋‹น ์ˆ˜์ฒœ ๋ฒˆ์”ฉ ๋ณด๋‚ด๋ฉด? ์„œ๋ฒ„๋Š” ๊ฒฐ๊ตญ ๋‹ค์šด๋˜๊ณ  ๋ง์•„. ๊ทธ๋ž˜์„œ Rate Limiting์ด ํ•„์š”ํ•œ ๊ฑฐ์•ผ! โฑ๏ธ

๐ŸŽฏ Rate Limiting ์ „๋žต

1. IP ๊ธฐ๋ฐ˜ ์ œํ•œ
๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ•์ด์•ผ. ๊ฐ™์€ IP์—์„œ ์˜ค๋Š” ์š”์ฒญ์„ ์ œํ•œํ•˜๋Š” ๊ฑฐ์ง€. ํ•˜์ง€๋งŒ ํ”„๋ก์‹œ๋‚˜ VPN์„ ์“ฐ๋ฉด ์šฐํšŒํ•  ์ˆ˜ ์žˆ์–ด.

2. ์‚ฌ์šฉ์ž ๊ธฐ๋ฐ˜ ์ œํ•œ
์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋ณ„๋กœ ์ œํ•œ์„ ๋‘๋Š” ๊ฒŒ ๋” ํšจ๊ณผ์ ์ด์•ผ. ์‚ฌ์šฉ์ž ID๋‚˜ API ํ‚ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ œํ•œํ•˜๋Š” ๊ฑฐ์ง€.

3. ๋น„์šฉ ๊ธฐ๋ฐ˜ ์ œํ•œ
์ฟผ๋ฆฌ ๋ณต์žก๋„ ์ ์ˆ˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ œํ•œํ•ด. ๋‹จ์ˆœํ•œ ์ฟผ๋ฆฌ๋Š” ๋งŽ์ด ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ณ , ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋Š” ์ ๊ฒŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฑฐ์•ผ.

4. ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ
๊ณ ์ •๋œ ์‹œ๊ฐ„ ์ฐฝ์ด ์•„๋‹ˆ๋ผ ์ด๋™ํ•˜๋Š” ์‹œ๊ฐ„ ์ฐฝ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ •๊ตํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•ด. ๐ŸชŸ

์‹ค์ „์—์„œ๋Š” Redis๋ฅผ ํ™œ์šฉํ•œ Rate Limiting์„ ๋งŽ์ด ์จ. graphql-rate-limit ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋ฉด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:

import { createRateLimitDirective } from 'graphql-rate-limit';

const rateLimitDirective = createRateLimitDirective({
  identifyContext: (ctx) => ctx.user.id
});

const typeDefs = gql`
  directive @rateLimit(
    max: Int
    window: String
    message: String
  ) on FIELD_DEFINITION

  type Query {
    users: [User!]! @rateLimit(max: 100, window: "1m")
    expensiveQuery: Result! @rateLimit(max: 5, window: "1h")
  }
`;

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด users ์ฟผ๋ฆฌ๋Š” ๋ถ„๋‹น 100๋ฒˆ, expensiveQuery๋Š” ์‹œ๊ฐ„๋‹น 5๋ฒˆ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์–ด. ๊ฐ„๋‹จํ•˜์ง€? ๐ŸŽฏ

๐Ÿ’ก ๊ณ„์ธต๋ณ„ Rate Limiting

์‚ฌ์šฉ์ž ๋“ฑ๊ธ‰์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ œํ•œ์„ ์ ์šฉํ•  ์ˆ˜๋„ ์žˆ์–ด:

์‚ฌ์šฉ์ž ๋“ฑ๊ธ‰ ๋ถ„๋‹น ์š”์ฒญ ์ˆ˜ ์‹œ๊ฐ„๋‹น ๋ณต์žก๋„ ๋™์‹œ ์—ฐ๊ฒฐ
๋ฌด๋ฃŒ ์‚ฌ์šฉ์ž 60 10,000 5
๊ธฐ๋ณธ ์œ ๋ฃŒ 300 50,000 20
ํ”„๋ฆฌ๋ฏธ์—„ 1,000 200,000 100
์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๋ฌด์ œํ•œ ๋ฌด์ œํ•œ ๋ฌด์ œํ•œ

์ด๋Ÿฐ ์‹์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋ชจ๋ธ๊ณผ ์—ฐ๊ณ„ํ•ด์„œ ์„ค๊ณ„ํ•˜๋ฉด ๋ณด์•ˆ๋„ ์ฑ™๊ธฐ๊ณ  ์ˆ˜์ตํ™”๋„ ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ฐ

Rate Limiting ๋™์ž‘ ์›๋ฆฌ ์š”์ฒญ 1 ์š”์ฒญ 2 ์š”์ฒญ 3 ์š”์ฒญ 4 ์š”์ฒญ 5 0์ดˆ 20์ดˆ 40์ดˆ 60์ดˆ 80์ดˆ ํ—ˆ์šฉ ๊ตฌ๊ฐ„ (๋ถ„๋‹น 3ํšŒ ์ œํ•œ) ์ฐจ๋‹จ ๊ตฌ๊ฐ„

๐Ÿ›ก๏ธ 6. ์ž…๋ ฅ ๊ฒ€์ฆ๊ณผ Sanitization

์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋‚ด๋Š” ๋ชจ๋“  ์ž…๋ ฅ์€ ์˜์‹ฌํ•ด์•ผ ํ•ด! SQL Injection, NoSQL Injection, XSS ๊ฐ™์€ ๊ณต๊ฒฉ์€ ๋Œ€๋ถ€๋ถ„ ์ž…๋ ฅ ๊ฒ€์ฆ ๋ถ€์กฑ์—์„œ ์‹œ์ž‘๋˜๊ฑฐ๋“ . GraphQL๋„ ์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ์•ผ. ๐Ÿ”’

๐ŸŽฏ ์ž…๋ ฅ ๊ฒ€์ฆ Best Practices

1. ํƒ€์ž… ์‹œ์Šคํ…œ ํ™œ์šฉ
GraphQL์˜ ๊ฐ•๋ ฅํ•œ ํƒ€์ž… ์‹œ์Šคํ…œ์„ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•ด. String๋ณด๋‹ค๋Š” Email, URL, DateTime ๊ฐ™์€ ์ปค์Šคํ…€ ์Šค์นผ๋ผ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ๊ฑฐ์•ผ.

2. ์ž…๋ ฅ ๊ธธ์ด ์ œํ•œ
๋ฌธ์ž์—ด ํ•„๋“œ์—๋Š” ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ด. ๋ฌด์ œํ•œ ์ž…๋ ฅ์€ ๋ฒ„ํผ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๋‚˜ DoS ๊ณต๊ฒฉ์˜ ์›์ธ์ด ๋  ์ˆ˜ ์žˆ์–ด.

3. ์ •๊ทœ์‹ ๊ฒ€์ฆ
์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, URL ๊ฐ™์€ ํ˜•์‹์ด ์ •ํ•ด์ง„ ๋ฐ์ดํ„ฐ๋Š” ์ •๊ทœ์‹์œผ๋กœ ๊ฒ€์ฆํ•ด. ํ•˜์ง€๋งŒ ๋„ˆ๋ฌด ๋ณต์žกํ•œ ์ •๊ทœ์‹์€ ReDoS ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜! โš ๏ธ

4. ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹
ํ—ˆ์šฉํ•  ๊ฐ’์„ ๋ช…์‹œํ•˜๋Š” ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹์ด ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ๋ณด๋‹ค ์•ˆ์ „ํ•ด. Enum ํƒ€์ž…์„ ์ ๊ทน ํ™œ์šฉํ•˜์ž!

5. HTML/SQL ์ด์Šค์ผ€์ดํ•‘
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•˜๊ฑฐ๋‚˜ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ์ด์Šค์ผ€์ดํ•‘ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์•ผ ํ•ด.

์ปค์Šคํ…€ ์Šค์นผ๋ผ ํƒ€์ž…์„ ๋งŒ๋“ค์–ด์„œ ์ž…๋ ฅ ๊ฒ€์ฆ์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์–ด:

import { GraphQLScalarType, Kind } from 'graphql';

const EmailType = new GraphQLScalarType({
  name: 'Email',
  description: '์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ',
  serialize(value) {
    return value;
  },
  parseValue(value) {
    if (!isValidEmail(value)) {
      throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค');
    }
    return value.toLowerCase().trim();
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      if (!isValidEmail(ast.value)) {
        throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค');
      }
      return ast.value.toLowerCase().trim();
    }
    return null;
  }
});

function isValidEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email) && email.length <= 254;
}

์ด์ œ ์Šคํ‚ค๋งˆ์—์„œ Email ํƒ€์ž…์„ ์“ฐ๋ฉด ์ž๋™์œผ๋กœ ๊ฒ€์ฆ์ด ๋˜๋Š” ๊ฑฐ์•ผ! ๐Ÿ˜Ž

โœจ ์ž…๋ ฅ ๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ

validator.js๋‚˜ joi ๊ฐ™์€ ๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋ฉด ๋” ํŽธํ•ด:

import Joi from 'joi';

const userInputSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(100).required(),
  age: Joi.number().integer().min(0).max(150),
  website: Joi.string().uri().optional()
});

// ๋ฆฌ์กธ๋ฒ„์—์„œ ์‚ฌ์šฉ
const createUser = async (parent, args, context) => {
  const { error, value } = userInputSchema.validate(args.input);
  
  if (error) {
    throw new UserInputError('์ž…๋ ฅ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค', {
      validationErrors: error.details
    });
  }
  
  // ๊ฒ€์ฆ๋œ ๋ฐ์ดํ„ฐ๋กœ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  return createUserInDB(value);
};

โš ๏ธ ์ฃผ์˜: ReDoS ๊ณต๊ฒฉ

๋ณต์žกํ•œ ์ •๊ทœ์‹์€ Regular Expression Denial of Service(ReDoS) ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด (a+)+ ๊ฐ™์€ ํŒจํ„ด์€ ์ž…๋ ฅ์ด ๊ธธ์–ด์งˆ์ˆ˜๋ก ์ง€์ˆ˜์ ์œผ๋กœ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ์ฆ๊ฐ€ํ•ด. ๊ฒ€์ฆ๋œ ์ •๊ทœ์‹ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•˜๋Š” ๊ฒŒ ์ข‹์•„! โฐ

๐Ÿ” 7. HTTPS์™€ ๋ณด์•ˆ ํ—ค๋” ์„ค์ •

GraphQL API๋Š” ๋ฐ˜๋“œ์‹œ HTTPS๋กœ ์„œ๋น„์Šคํ•ด์•ผ ํ•ด! HTTP๋กœ ํ†ต์‹ ํ•˜๋ฉด ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM)์— ๋…ธ์ถœ๋˜๊ฑฐ๋“ . ํ† ํฐ์ด๋‚˜ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ํ‰๋ฌธ์œผ๋กœ ์ „์†ก๋˜๋Š” ๊ฑด ์ •๋ง ์œ„ํ—˜ํ•ด. ๐Ÿ˜ฑ

๐ŸŽฏ ํ•„์ˆ˜ ๋ณด์•ˆ ํ—ค๋”

1. Strict-Transport-Security (HSTS)
๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ•ญ์ƒ HTTPS๋กœ๋งŒ ์ ‘์†ํ•˜๋„๋ก ๊ฐ•์ œํ•ด. max-age=31536000; includeSubDomains; preload ์ด๋ ‡๊ฒŒ ์„ค์ •ํ•˜๋ฉด 1๋…„ ๋™์•ˆ HTTPS๋งŒ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ผ.

2. Content-Security-Policy (CSP)
XSS ๊ณต๊ฒฉ์„ ๋ฐฉ์–ดํ•˜๋Š” ํ•ต์‹ฌ ํ—ค๋”์•ผ. GraphQL API์—์„œ๋Š” default-src 'none'์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

3. X-Content-Type-Options
nosniff๋กœ ์„ค์ •ํ•ด์„œ MIME ํƒ€์ž… ์Šค๋‹ˆํ•‘์„ ๋ฐฉ์ง€ํ•ด์•ผ ํ•ด.

4. X-Frame-Options
DENY๋กœ ์„ค์ •ํ•ด์„œ ํด๋ฆญ์žฌํ‚น ๊ณต๊ฒฉ์„ ๋ง‰์•„.

5. Referrer-Policy
no-referrer๋‚˜ strict-origin-when-cross-origin์œผ๋กœ ์„ค์ •ํ•ด์„œ ์ •๋ณด ์œ ์ถœ์„ ๋ฐฉ์ง€ํ•ด.

Express ๊ธฐ๋ฐ˜ ์„œ๋ฒ„๋ผ๋ฉด helmet ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์“ฐ๋ฉด ํ•œ ๋ฒˆ์— ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด:

import helmet from 'helmet';
import express from 'express';

const app = express();

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'none'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'"],
      imgSrc: ["'self'"],
      connectSrc: ["'self'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  frameguard: {
    action: 'deny'
  },
  referrerPolicy: {
    policy: 'no-referrer'
  }
}));

๐Ÿ’ก CORS ์„ค์ •๋„ ์ค‘์š”ํ•ด!

GraphQL API๋ฅผ ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ˜ธ์ถœํ•œ๋‹ค๋ฉด CORS ์„ค์ •๋„ ์‹ ๊ฒฝ ์จ์•ผ ํ•ด:

import cors from 'cors';

const corsOptions = {
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['https://yourdomain.com'],
  credentials: true,
  maxAge: 86400, // 24์‹œ๊ฐ„
  methods: ['POST', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
};

app.use(cors(corsOptions));

GraphQL์€ ๋ณดํ†ต POST ๋ฉ”์„œ๋“œ๋งŒ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ GET์„ ํ—ˆ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์–ด. ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ์—ด์–ด๋‘๋Š” ๊ฒŒ ๋ณด์•ˆ์˜ ๊ธฐ๋ณธ์ด์•ผ! ๐Ÿ”’

๐Ÿ“Š 8. ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง

๋ณด์•ˆ ์‚ฌ๊ณ ๋Š” ์˜ˆ๋ฐฉ์ด ์ตœ์„ ์ด์ง€๋งŒ, ๋งŒ์•ฝ ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด ๋น ๋ฅด๊ฒŒ ๊ฐ์ง€ํ•˜๊ณ  ๋Œ€์‘ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ๊ทธ๋ž˜์„œ ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ํ•„์ˆ˜์•ผ! ๐Ÿ”

๐ŸŽฏ ๋กœ๊น…ํ•ด์•ผ ํ•  ๊ฒƒ๋“ค

1. ์ธ์ฆ/๊ถŒํ•œ ์‹คํŒจ
๋กœ๊ทธ์ธ ์‹คํŒจ, ๊ถŒํ•œ ์—†๋Š” ์ ‘๊ทผ ์‹œ๋„๋Š” ๋ชจ๋‘ ๊ธฐ๋กํ•ด์•ผ ํ•ด. ํŠน์ • IP๋‚˜ ์‚ฌ์šฉ์ž์˜ ๋ฐ˜๋ณต์ ์ธ ์‹คํŒจ๋Š” ๊ณต๊ฒฉ ์‹œ๋„์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„.

2. ์ฟผ๋ฆฌ ๋ณต์žก๋„ ์ดˆ๊ณผ
๋ณต์žก๋„ ์ œํ•œ์„ ์ดˆ๊ณผํ•œ ์ฟผ๋ฆฌ๋Š” ๋กœ๊ทธ๋กœ ๋‚จ๊ฒจ์„œ ํŒจํ„ด์„ ๋ถ„์„ํ•ด์•ผ ํ•ด. ์ •์ƒ ์‚ฌ์šฉ์ž์˜ ์š”๊ตฌ์‚ฌํ•ญ์ธ์ง€, ๊ณต๊ฒฉ ์‹œ๋„์ธ์ง€ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .

3. Rate Limit ๋„๋‹ฌ
์ œํ•œ์— ๊ฑธ๋ฆฐ ์š”์ฒญ๋“ค์„ ์ถ”์ ํ•˜๋ฉด ๋ด‡์ด๋‚˜ ์Šคํฌ๋ž˜ํผ๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ์–ด.

4. ๋А๋ฆฐ ์ฟผ๋ฆฌ
์‹คํ–‰ ์‹œ๊ฐ„์ด ๊ธด ์ฟผ๋ฆฌ๋Š” ์„ฑ๋Šฅ ๋ฌธ์ œ๋‚˜ ๋น„ํšจ์œจ์ ์ธ ์ฟผ๋ฆฌ ํŒจํ„ด์„ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ์–ด. ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ์ฐพ๋Š” ๋ฐ ๋„์›€์ด ๋ผ.

5. ์—๋Ÿฌ์™€ ์˜ˆ์™ธ
๋ชจ๋“  ์—๋Ÿฌ๋Š” ๋กœ๊ทธ๋กœ ๋‚จ๊ฒจ์•ผ ํ•˜์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ๋Š” ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด ์•ˆ ๋ผ! ๊ณต๊ฒฉ์ž์—๊ฒŒ ์‹œ์Šคํ…œ ์ •๋ณด๋ฅผ ์•Œ๋ ค์ฃผ๋Š” ๊ผด์ด ๋˜๊ฑฐ๋“ . ๐Ÿ˜ฐ

Apollo Server์—์„œ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ๋กœ๊น…์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:

const loggingPlugin = {
  requestDidStart(requestContext) {
    const start = Date.now();
    
    return {
      didEncounterErrors(ctx) {
        console.error('GraphQL ์—๋Ÿฌ ๋ฐœ์ƒ:', {
          query: ctx.request.query,
          variables: ctx.request.variables,
          errors: ctx.errors,
          user: ctx.context.user?.id,
          ip: ctx.request.http?.headers.get('x-forwarded-for')
        });
      },
      
      willSendResponse(ctx) {
        const duration = Date.now() - start;
        
        if (duration > 1000) {
          console.warn('๋А๋ฆฐ ์ฟผ๋ฆฌ ๊ฐ์ง€:', {
            duration,
            query: ctx.request.query,
            user: ctx.context.user?.id
          });
        }
        
        console.info('์š”์ฒญ ์™„๋ฃŒ:', {
          duration,
          operationName: ctx.request.operationName,
          user: ctx.context.user?.id
        });
      }
    };
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [loggingPlugin]
});

โœจ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ „๋ฌธ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ๋ฅผ ์“ฐ๋Š” ๊ฒŒ ์ข‹์•„:

๋„๊ตฌ ํŠน์ง• ์ถ”์ฒœ ์šฉ๋„
Apollo Studio GraphQL ์ „์šฉ, ์ฟผ๋ฆฌ ๋ถ„์„ GraphQL ์„ฑ๋Šฅ ์ตœ์ ํ™”
Datadog ์ข…ํ•ฉ APM, ์•Œ๋ฆผ ๊ธฐ๋Šฅ ์ „์ฒด ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง
Sentry ์—๋Ÿฌ ์ถ”์ , ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ์—๋Ÿฌ ๋””๋ฒ„๊น…
ELK Stack ๋กœ๊ทธ ์ˆ˜์ง‘/๋ถ„์„/์‹œ๊ฐํ™” ๋Œ€์šฉ๋Ÿ‰ ๋กœ๊ทธ ์ฒ˜๋ฆฌ

์žฌ๋Šฅ๋„ท์—์„œ ๋ณด์•ˆ ์ปจ์„คํŒ…์„ ์ œ๊ณตํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค๋„ ์ด๋Ÿฐ ๋„๊ตฌ๋“ค์„ ํ™œ์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์˜ ์‹œ์Šคํ…œ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์–ด! ๐Ÿ’ช

โš ๏ธ ๊ฐœ์ธ์ •๋ณด ๋กœ๊น… ์ฃผ์˜

๋กœ๊ทธ์— ๋น„๋ฐ€๋ฒˆํ˜ธ, ํ† ํฐ, ์‹ ์šฉ์นด๋“œ ๋ฒˆํ˜ธ ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•ด! GDPR์ด๋‚˜ ๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ๋ฒ• ์œ„๋ฐ˜์ด ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๋กœ๊ทธ ์ˆ˜์ง‘ ์ „์— ๋งˆ์Šคํ‚น ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ๐ŸŽญ

๋ณด์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ ์‹ค์‹œ๊ฐ„ ์š”์ฒญ 1,247 ์š”์ฒญ/๋ถ„ ์„ฑ๊ณต๋ฅ  99.8% ์ง€๋‚œ 1์‹œ๊ฐ„ ์ฐจ๋‹จ๋œ ์š”์ฒญ 23 ์ง€๋‚œ 1์‹œ๊ฐ„ ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ 142ms ์ตœ๊ทผ ๋ณด์•ˆ ์ด๋ฒคํŠธ โ€ข Rate limit ์ดˆ๊ณผ (IP: 192.168.1.100) โ€ข ๊ถŒํ•œ ์—†๋Š” ์ ‘๊ทผ ์‹œ๋„ (User: guest_123) โ€ข ๋ณต์žก๋„ ์ œํ•œ ์ดˆ๊ณผ (Query: deepNested)

๐Ÿงช 9. ๋ณด์•ˆ ํ…Œ์ŠคํŠธ์™€ ๊ฐ์‚ฌ

์•„๋ฌด๋ฆฌ ๋ณด์•ˆ ์žฅ์น˜๋ฅผ ๋งŽ์ด ์„ค์น˜ํ•ด๋„, ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์œผ๋ฉด ์†Œ์šฉ์—†์–ด! ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ์™€ ๊ฐ์‚ฌ๊ฐ€ ํ•„์ˆ˜์•ผ. ๐Ÿ”ฌ

๐ŸŽฏ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1. ์นจํˆฌ ํ…Œ์ŠคํŠธ (Penetration Testing)
์‹ค์ œ ๊ณต๊ฒฉ์ž์ฒ˜๋Ÿผ ์‹œ์Šคํ…œ์„ ๊ณต๊ฒฉํ•ด๋ณด๋Š” ๊ฑฐ์•ผ. ์ทจ์•ฝ์ ์„ ์ฐพ์•„๋‚ด๊ณ  ์ˆ˜์ •ํ•˜๋Š” ๊ฒŒ ๋ชฉ์ ์ด์ง€. ์ „๋ฌธ ๋ณด์•ˆ ์—…์ฒด์— ์˜๋ขฐํ•˜๊ฑฐ๋‚˜, ํ™”์ดํŠธํ–‡ ํ•ด์ปค๋ฅผ ๊ณ ์šฉํ•  ์ˆ˜๋„ ์žˆ์–ด.

2. ์ž๋™ํ™”๋œ ๋ณด์•ˆ ์Šค์บ”
OWASP ZAP, Burp Suite ๊ฐ™์€ ๋„๊ตฌ๋กœ ์ž๋™์œผ๋กœ ์ทจ์•ฝ์ ์„ ์Šค์บ”ํ•  ์ˆ˜ ์žˆ์–ด. CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ†ตํ•ฉํ•˜๋ฉด ๋” ์ข‹์•„! ๐Ÿค–

3. ์˜์กด์„ฑ ์ทจ์•ฝ์  ๊ฒ€์‚ฌ
์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์•Œ๋ ค์ง„ ์ทจ์•ฝ์ ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ด. npm audit์ด๋‚˜ Snyk ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ์จ.

4. ์ฝ”๋“œ ๋ฆฌ๋ทฐ
๋™๋ฃŒ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ณด์•ˆ ๊ด€์ ์—์„œ ์ฝ”๋“œ๋ฅผ ๋ฆฌ๋ทฐํ•˜๋Š” ๊ฑฐ์•ผ. ๋„ค ๋ˆˆ์œผ๋กœ ๋ชป ๋ณธ ๋ฌธ์ œ๋ฅผ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์–ด.

5. ๋ฒ„๊ทธ ๋ฐ”์šดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ
์™ธ๋ถ€ ๋ณด์•ˆ ์—ฐ๊ตฌ์ž๋“ค์—๊ฒŒ ์ทจ์•ฝ์ ์„ ์ฐพ์œผ๋ฉด ๋ณด์ƒ์„ ์ฃผ๋Š” ํ”„๋กœ๊ทธ๋žจ์ด์•ผ. HackerOne, Bugcrowd ๊ฐ™์€ ํ”Œ๋žซํผ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

GraphQL ํŠนํ™” ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋“ค๋„ ์žˆ์–ด:

โœจ GraphQL ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ

๋„๊ตฌ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
InQL Introspection ๋ถ„์„, ์ทจ์•ฝ์  ํƒ์ง€ Burp Suite ํ™•์žฅ
GraphQL Cop ๋ณด์•ˆ ์„ค์ • ๊ฒ€์ฆ CLI ๋„๊ตฌ
BatchQL ๋ฐฐ์น˜ ์ฟผ๋ฆฌ ๊ณต๊ฒฉ ํ…Œ์ŠคํŠธ Python ์Šคํฌ๋ฆฝํŠธ
GraphQLmap ์ž๋™ํ™”๋œ ์ทจ์•ฝ์  ์Šค์บ” CLI ๋„๊ตฌ

์˜ˆ๋ฅผ ๋“ค์–ด GraphQL Cop์œผ๋กœ ๋ณด์•ˆ ์„ค์ •์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฑด ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•ด:

$ npm install -g graphql-cop
$ graphql-cop -t https://your-api.com/graphql

[โœ“] Introspection: Disabled
[โœ—] Depth Limiting: Not Configured
[โœ“] Query Complexity: Enabled (max: 1000)
[โœ—] Rate Limiting: Not Detected
[โœ“] Field Suggestions: Disabled
[โœ—] Batch Queries: Allowed (potential DoS risk)

๋ณด์•ˆ ์ ์ˆ˜: 60/100

์ด๋Ÿฐ ๋„๊ตฌ๋“ค์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋Œ๋ ค์„œ ๋ณด์•ˆ ์ƒํƒœ๋ฅผ ์ฒดํฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„! ๐Ÿ“Š

๐Ÿ’ก ๋ณด์•ˆ ๊ฐ์‚ฌ ์ฃผ๊ธฐ

โ€ข ์ผ์ผ: ์ž๋™ํ™”๋œ ๋ณด์•ˆ ์Šค์บ”, ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง
โ€ข ์ฃผ๊ฐ„: ์˜์กด์„ฑ ์ทจ์•ฝ์  ๊ฒ€์‚ฌ, ๋ณด์•ˆ ์ด๋ฒคํŠธ ๋ฆฌ๋ทฐ
โ€ข ์›”๊ฐ„: ์ˆ˜๋™ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ, ์„ค์ • ๊ฒ€ํ† 
โ€ข ๋ถ„๊ธฐ: ์นจํˆฌ ํ…Œ์ŠคํŠธ, ์ „์ฒด ๋ณด์•ˆ ๊ฐ์‚ฌ
โ€ข ์—ฐ๊ฐ„: ์™ธ๋ถ€ ๋ณด์•ˆ ์ปจ์„คํŒ…, ์ธ์ฆ ๊ฐฑ์‹ 

๐Ÿš€ 10. ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์‹œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

์ž, ์ด์ œ ์‹ค์ „์ด์•ผ! ํ”„๋กœ๋•์…˜์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•ด์•ผ ํ•  ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ. ํ•˜๋‚˜๋ผ๋„ ๋น ๋œจ๋ฆฌ๋ฉด ํฐ์ผ ๋‚  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ ๊ผผ๊ผผํžˆ ์ฒดํฌํ•ด! โœ…

๐ŸŽฏ ํ”„๋กœ๋•์…˜ ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โœ… ์ธ์ฆ/๊ถŒํ•œ
โ–ก JWT ์‹œํฌ๋ฆฟ ํ‚ค๊ฐ€ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋‚˜์š”?
โ–ก ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„์ด ์ ์ ˆํ•˜๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ๋ชจ๋“  ๋ฏผ๊ฐํ•œ ์ฟผ๋ฆฌ/๋ฎคํ…Œ์ด์…˜์— ์ธ์ฆ์ด ํ•„์š”ํ•œ๊ฐ€์š”?
โ–ก ํ•„๋“œ ๋ ˆ๋ฒจ ๊ถŒํ•œ ์ œ์–ด๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‚˜์š”?

โœ… ์ฟผ๋ฆฌ ์ œํ•œ
โ–ก ์ฟผ๋ฆฌ ๊นŠ์ด ์ œํ•œ์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”? (๊ถŒ์žฅ: 5-7)
โ–ก ์ฟผ๋ฆฌ ๋ณต์žก๋„ ์ œํ•œ์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ๋ฐฐ์—ด ํฌ๊ธฐ ์ œํ•œ(ํŽ˜์ด์ง€๋„ค์ด์…˜)์ด ๊ฐ•์ œ๋˜๋‚˜์š”?
โ–ก ์ฟผ๋ฆฌ ํƒ€์ž„์•„์›ƒ์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?

โœ… Rate Limiting
โ–ก IP ๊ธฐ๋ฐ˜ Rate Limiting์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ์‚ฌ์šฉ์ž ๊ธฐ๋ฐ˜ Rate Limiting์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ๋น„์šฉ ๊ธฐ๋ฐ˜ Rate Limiting์ด ๊ณ ๋ ค๋˜์—ˆ๋‚˜์š”?

โœ… ์ž…๋ ฅ ๊ฒ€์ฆ
โ–ก ๋ชจ๋“  ์ž…๋ ฅ์— ํƒ€์ž… ๊ฒ€์ฆ์ด ์ ์šฉ๋˜๋‚˜์š”?
โ–ก ๋ฌธ์ž์—ด ๊ธธ์ด ์ œํ•œ์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ์ปค์Šคํ…€ ์Šค์นผ๋ผ ํƒ€์ž…์œผ๋กœ ํ˜•์‹ ๊ฒ€์ฆ์„ ํ•˜๋‚˜์š”?
โ–ก SQL/NoSQL Injection ๋ฐฉ์–ด๊ฐ€ ๋˜์–ด ์žˆ๋‚˜์š”?

โœ… ์ •๋ณด ๋…ธ์ถœ ๋ฐฉ์ง€
โ–ก Introspection์ด ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก GraphQL Playground๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š๋‚˜์š”?
โ–ก ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋‚˜์š”?

โœ… ๋„คํŠธ์›Œํฌ ๋ณด์•ˆ
โ–ก HTTPS๊ฐ€ ๊ฐ•์ œ๋˜๋‚˜์š”?
โ–ก HSTS ํ—ค๋”๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก CORS๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก CSP ํ—ค๋”๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?

โœ… ๋กœ๊น…/๋ชจ๋‹ˆํ„ฐ๋ง
โ–ก ๋ณด์•ˆ ์ด๋ฒคํŠธ๊ฐ€ ๋กœ๊น…๋˜๋‚˜์š”?
โ–ก ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ์•Œ๋ฆผ ์‹œ์Šคํ…œ์ด ๊ตฌ์ถ•๋˜์–ด ์žˆ๋‚˜์š”?
โ–ก ๋กœ๊ทธ์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š๋‚˜์š”?

โœ… ์˜์กด์„ฑ ๊ด€๋ฆฌ
โ–ก ๋ชจ๋“  ํŒจํ‚ค์ง€๊ฐ€ ์ตœ์‹  ๋ฒ„์ „์ธ๊ฐ€์š”?
โ–ก ์•Œ๋ ค์ง„ ์ทจ์•ฝ์ ์ด ์žˆ๋Š” ํŒจํ‚ค์ง€๊ฐ€ ์—†๋‚˜์š”?
โ–ก ์˜์กด์„ฑ ์ž๋™ ์—…๋ฐ์ดํŠธ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋‚˜์š”?

โš ๏ธ ๋ฐฐํฌ ์ „ ๋งˆ์ง€๋ง‰ ํ™•์ธ

ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ „์—๋Š” ๋ฐ˜๋“œ์‹œ ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์—์„œ ๋จผ์ € ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด! ์‹ค์ œ ํ”„๋กœ๋•์…˜ ๋ฐ์ดํ„ฐ์˜ ๋ณต์‚ฌ๋ณธ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋ฉด ๋” ์ข‹์•„. ๊ทธ๋ฆฌ๊ณ  ๋กค๋ฐฑ ๊ณ„ํš๋„ ๋ฏธ๋ฆฌ ์ค€๋น„ํ•ด๋‘ฌ์•ผ ํ•ด. ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ๋น ๋ฅด๊ฒŒ ์ด์ „ ๋ฒ„์ „์œผ๋กœ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ฑฐ๋“ ! ๐Ÿ”„

๐Ÿ“š ์‹ค์ „ ์˜ˆ์ œ: ์•ˆ์ „ํ•œ GraphQL ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ์•ˆ์ „ํ•œ GraphQL ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ์ „์ฒด ์ฝ”๋“œ๋ฅผ ํ•œ๋ฒˆ ๋ณผ๊นŒ? ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๋ณด์•ˆ ๊ธฐ๋ฒ•์„ ์ ์šฉํ•œ ์˜ˆ์ œ์•ผ! ๐ŸŽ“

// server.js
import { ApolloServer } from 'apollo-server-express';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
import { createRateLimitDirective } from 'graphql-rate-limit';
import jwt from 'jsonwebtoken';

// ํƒ€์ž… ์ •์˜
const typeDefs = gql`
  directive @rateLimit(
    max: Int
    window: String
  ) on FIELD_DEFINITION
  
  directive @auth on FIELD_DEFINITION | OBJECT
  
  directive @hasRole(requires: Role!) on FIELD_DEFINITION
  
  enum Role {
    USER
    ADMIN
  }
  
  type User {
    id: ID!
    email: String!
    name: String!
    role: Role!
    posts: [Post!]! @rateLimit(max: 100, window: "1m")
  }
  
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }
  
  type Query {
    me: User @auth
    user(id: ID!): User @auth
    users(first: Int = 10): [User!]! @auth @hasRole(requires: ADMIN)
    post(id: ID!): Post
    posts(first: Int = 10): [Post!]!
  }
  
  type Mutation {
    login(email: String!, password: String!): AuthPayload!
    createPost(title: String!, content: String!): Post! @auth
    deletePost(id: ID!): Boolean! @auth
  }
  
  type AuthPayload {
    token: String!
    user: User!
  }
`;

// ๋ฆฌ์กธ๋ฒ„
const resolvers = {
  Query: {
    me: (parent, args, context) => {
      if (!context.user) {
        throw new AuthenticationError('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค');
      }
      return getUserById(context.user.id);
    },
    
    user: (parent, { id }, context) => {
      if (!context.user) {
        throw new AuthenticationError('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค');
      }
      return getUserById(id);
    },
    
    users: (parent, { first }, context) => {
      if (!context.user || context.user.role !== 'ADMIN') {
        throw new ForbiddenError('๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค');
      }
      return getUsers(first);
    },
    
    posts: (parent, { first }) => {
      return getPosts(first);
    }
  },
  
  Mutation: {
    login: async (parent, { email, password }) => {
      const user = await authenticateUser(email, password);
      if (!user) {
        throw new AuthenticationError('์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค');
      }
      
      const token = jwt.sign(
        { userId: user.id, role: user.role },
        process.env.JWT_SECRET,
        { expiresIn: '1h' }
      );
      
      return { token, user };
    },
    
    createPost: (parent, { title, content }, context) => {
      if (!context.user) {
        throw new AuthenticationError('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค');
      }
      
      return createPost({
        title,
        content,
        authorId: context.user.id
      });
    }
  }
};

// Express ์•ฑ ์„ค์ •
const app = express();

// ๋ณด์•ˆ ํ—ค๋”
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'none'"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true
  }
}));

// CORS
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(','),
  credentials: true
}));

// Rate Limiting ๋””๋ ‰ํ‹ฐ๋ธŒ
const rateLimitDirective = createRateLimitDirective({
  identifyContext: (ctx) => ctx.user?.id || ctx.ip
});

// Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives: {
    rateLimit: rateLimitDirective
  },
  
  // Context ์„ค์ •
  context: async ({ req }) => {
    const token = req.headers.authorization?.replace('Bearer ', '');
    let user = null;
    
    if (token) {
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        user = await getUserById(decoded.userId);
      } catch (err) {
        console.error('ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ:', err.message);
      }
    }
    
    return {
      user,
      ip: req.ip
    };
  },
  
  // ๊ฒ€์ฆ ๊ทœ์น™
  validationRules: [
    depthLimit(7),
    createComplexityLimitRule(1000)
  ],
  
  // ํ”„๋กœ๋•์…˜ ์„ค์ •
  introspection: process.env.NODE_ENV !== 'production',
  playground: process.env.NODE_ENV !== 'production',
  debug: process.env.NODE_ENV !== 'production',
  
  // ์—๋Ÿฌ ํฌ๋งทํŒ…
  formatError: (err) => {
    // ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ƒ์„ธํ•œ ์—๋Ÿฌ ์ •๋ณด ์ˆจ๊ธฐ๊ธฐ
    if (process.env.NODE_ENV === 'production') {
      console.error(err);
      return new Error('์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค');
    }
    return err;
  },
  
  // ํ”Œ๋Ÿฌ๊ทธ์ธ
  plugins: [
    {
      requestDidStart() {
        const start = Date.now();
        
        return {
          didEncounterErrors(ctx) {
            console.error('GraphQL ์—๋Ÿฌ:', {
              query: ctx.request.query,
              user: ctx.context.user?.id,
              errors: ctx.errors.map(e => e.message)
            });
          },
          
          willSendResponse(ctx) {
            const duration = Date.now() - start;
            
            if (duration > 1000) {
              console.warn('๋А๋ฆฐ ์ฟผ๋ฆฌ:', {
                duration,
                operation: ctx.request.operationName
              });
            }
          }
        };
      }
    }
  ]
});

// ์„œ๋ฒ„ ์‹œ์ž‘
server.applyMiddleware({ app, path: '/graphql' });

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
  console.log(`๐Ÿš€ ์„œ๋ฒ„๊ฐ€ https://localhost:${PORT}${server.graphqlPath}์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค`);
});

์ด ์ฝ”๋“œ๋Š” ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๋ณด์•ˆ ๊ธฐ๋ฒ•์„ ์ ์šฉํ•œ ์™„์ „ํ•œ ์˜ˆ์ œ์•ผ. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์ด ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด ์•ˆ์ „ํ•œ GraphQL API๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ๋А๋ผ ์ˆ˜๊ณ ํ–ˆ์–ด! ๐Ÿ‘ GraphQL API ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•˜๊ณ  ๋ณต์žกํ•œ ์ฃผ์ œ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ ์šฉํ•˜๋ฉด ์ถฉ๋ถ„ํžˆ ์•ˆ์ „ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.

ํ•ต์‹ฌ์„ ๋‹ค์‹œ ์ •๋ฆฌํ•˜๋ฉด:

๐ŸŽฏ GraphQL ๋ณด์•ˆ ํ•ต์‹ฌ ์š”์•ฝ

1. ์ธ์ฆ๊ณผ ๊ถŒํ•œ ๐Ÿ”
JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ, ํ•„๋“œ ๋ ˆ๋ฒจ ๊ถŒํ•œ ์ œ์–ด, ๋””๋ ‰ํ‹ฐ๋ธŒ ํ™œ์šฉ

2. ์ฟผ๋ฆฌ ์ œํ•œ โšก
๊นŠ์ด ์ œํ•œ, ๋ณต์žก๋„ ์ œํ•œ, ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฐ•์ œ, ํƒ€์ž„์•„์›ƒ

3. Rate Limiting ๐Ÿšฆ
IP/์‚ฌ์šฉ์ž/๋น„์šฉ ๊ธฐ๋ฐ˜ ์ œํ•œ, ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ

4. ์ž…๋ ฅ ๊ฒ€์ฆ ๐Ÿ›ก๏ธ
ํƒ€์ž… ์‹œ์Šคํ…œ, ์ปค์Šคํ…€ ์Šค์นผ๋ผ, ๊ธธ์ด ์ œํ•œ, ์ด์Šค์ผ€์ดํ•‘

5. ์ •๋ณด ๋ณดํ˜ธ ๐Ÿ”
Introspection ์ œ์–ด, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ•„ํ„ฐ๋ง, HTTPS

6. ๋ชจ๋‹ˆํ„ฐ๋ง ๐Ÿ“Š
๋กœ๊น…, ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง, ์•Œ๋ฆผ, ์ •๊ธฐ ๊ฐ์‚ฌ

๋ณด์•ˆ์€ ํ•œ ๋ฒˆ ์„ค์ •ํ•˜๊ณ  ๋๋‚˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ์ง€์†์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ , ์ƒˆ๋กœ์šด ์œ„ํ˜‘์— ๋Œ€์‘ํ•˜๊ณ , ์‹œ์Šคํ…œ์„ ๊ฐœ์„ ํ•ด์•ผ ํ•ด. ๋งˆ์น˜ ๊ฑด๊ฐ• ๊ด€๋ฆฌ์ฒ˜๋Ÿผ ๋ง์ด์•ผ! ๐Ÿƒโ€โ™‚๏ธ

์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ๋Š” GraphQL ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€๋“ค์ด ์ปจ์„คํŒ… ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์–ด. ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ์„ ํƒ์ด์•ผ. ๋ณด์•ˆ์€ ํˆฌ์žํ•  ๊ฐ€์น˜๊ฐ€ ์ถฉ๋ถ„ํžˆ ์žˆ๋Š” ์˜์—ญ์ด๊ฑฐ๋“ ! ๐Ÿ’Ž

์ด ๊ธ€์ด ๋„ˆ์˜ GraphQL API๋ฅผ ๋” ์•ˆ์ „ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ๋ฌผ์–ด๋ด! ํ•จ๊ป˜ ์•ˆ์ „ํ•œ ์›น์„ ๋งŒ๋“ค์–ด๊ฐ€์ž! ๐ŸŒโœจ

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

1. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜ ๋ณด์•ˆ ์ƒํƒœ๋ฅผ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ์ ๊ฒ€ํ•ด๋ณด๊ธฐ
2. ๊ฐ€์žฅ ์ทจ์•ฝํ•œ ๋ถ€๋ถ„๋ถ€ํ„ฐ ํ•˜๋‚˜์”ฉ ๊ฐœ์„ ํ•˜๊ธฐ
3. ์ž๋™ํ™”๋œ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ๋ฅผ CI/CD์— ํ†ตํ•ฉํ•˜๊ธฐ
4. ํŒ€์›๋“ค๊ณผ ๋ณด์•ˆ ๊ฐ€์ด๋“œ๋ผ์ธ ๊ณต์œ ํ•˜๊ธฐ
5. ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ ์ผ์ • ์ˆ˜๋ฆฝํ•˜๊ธฐ

์•ˆ์ „ํ•œ ์ฝ”๋”ฉ ๋˜์„ธ์š”! ๐Ÿš€๐Ÿ”’

๐Ÿ” โšก ๐Ÿš€ ์•ˆ์ „ํ•œ ๋น ๋ฅธ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ GraphQL API๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

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