Ver3.0 ๐ก๏ธ GraphQL API ๋ณด์, ์ด์ ์ ๋๋ก ์์๋ณด์!

๐ก๏ธ GraphQL API ๋ณด์, ์ด์ ์ ๋๋ก ์์๋ณด์!
์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ๋ฐฐ์ฐ๋ GraphQL ๋ณด์ Best Practices
์๋
! ๐ ์์ฆ GraphQL ์ฐ๋ ๊ฐ๋ฐ์๋ค ์ ๋ง ๋ง์์ก์ง? REST API์ over-fetching, under-fetching ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด์ฃผ๋ GraphQL์ ์ ๋ง ๋งค๋ ฅ์ ์ธ ๊ธฐ์ ์ด์ผ. ๊ทผ๋ฐ ๋ง์ด์ผ... ๊ฐ๋ ฅํ ๋งํผ ๋ณด์ ์ํ๋ ํจ๊ป ๋ฐ๋ผ์จ๋ค๋ ๊ฑฐ ์๊ณ ์์ด? ๐ฐ
์ค๋์ GraphQL API๋ฅผ ์์ ํ๊ฒ ์ด์ํ๊ธฐ ์ํ ํต์ฌ ๋ณด์ ์ ๋ต๋ค์ ์น๊ตฌํํ
์ค๋ช
ํ๋ฏ์ด ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ํ์ด๋ณผ๊ฒ. ์ฌ๋ฅ๋ท(https://www.jaenung.net)์์ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๋ ์ ๋ฌธ๊ฐ๋ค๋ ๊ผญ ์์์ผ ํ ๋ด์ฉ์ด๋๊น ์ง์คํด์ค! ๐ฏ
๐ค 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);
};
}
}
์ฌ๋ฅ๋ท์์ ๋ณด์ ๊ด๋ จ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๋ ์ ๋ฌธ๊ฐ๋ค๋ ์ด๋ฐ ํจํด์ ๋ง์ด ์ถ์ฒํด. ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ด ๋๊ณ ํ ์คํธํ๊ธฐ๋ ์ฝ๊ฑฐ๋ ! ๐
โก 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 |
| ์ํฐํ๋ผ์ด์ฆ | ๋ฌด์ ํ | ๋ฌด์ ํ | ๋ฌด์ ํ |
์ด๋ฐ ์์ผ๋ก ๋น์ฆ๋์ค ๋ชจ๋ธ๊ณผ ์ฐ๊ณํด์ ์ค๊ณํ๋ฉด ๋ณด์๋ ์ฑ๊ธฐ๊ณ ์์ตํ๋ ํ ์ ์์ด! ๐ฐ
๐ก๏ธ 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์ด๋ ๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ ์๋ฐ์ด ๋ ์ ์๊ฑฐ๋ . ๋ก๊ทธ ์์ง ์ ์ ๋ง์คํน ์ฒ๋ฆฌ๋ฅผ ํ๋ ๊ฒ ์ข์. ๐ญ
๐งช 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. ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ ์ผ์ ์๋ฆฝํ๊ธฐ
์์ ํ ์ฝ๋ฉ ๋์ธ์! ๐๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.