4편. GraphQL 실무 아키텍처 설계: 도메인 구조와 책임 분리
📚 목차
1. 도메인 기반 폴더 구성 전략
2. Schema / Resolver / Service 분리
3. 모듈화된 아키텍처 적용 예시 (User 모듈)
4. 확장성 있는 초기 설계 방안
✔ 마무리 - 실무형 GraphQL 아키텍처 설계
GraphQL 서버를 처음 구축할 때는 간단한 구조로 빠르게 시작할 수 있지만, 기능이 늘어나면서 코드가 복잡해지고 유지보수가 어려워지는 경우가 많습니다.
이번 글에서는 도메인 단위로 기능을 나누고, 계층별로 책임을 분리하는 구조화 전략을 실습을 통해 직접 구현하며, 확장 가능하고 실무에 적합한 GraphQL 서버 아키텍처를 만들어 봅니다.
📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/graphql-tutorial-server)
1. 도메인 기반 폴더 구성 전략
GraphQL은 단일 엔드포인트로 다양한 데이터를 다룰 수 있는 만큼, 기능이 늘어날수록 코드 구조가 쉽게 복잡해집니다.
이를 방지하려면 기능 단위로 폴더를 분리하고, 각 기능마다 Schema, Resolver, Service 계층을 갖추는 방식이 효과적입니다.
기본 설계 원칙은 두 가지입니다.
🔸 계층 분리: Schema, Resolver, Service의 역할을 명확히 나눕니다.
🔸 기능 분리: User, Post, Comment 등 도메인 단위로 폴더를 구분합니다.
아래는 실무에서 자주 쓰이는 도메인 기반 디렉토리 구조 예시입니다.
src/
├── modules/
│ ├── post/
│ │ ├── post.schema.ts # 게시글 타입 및 쿼리 정의
│ │ ├── post.resolver.ts # 게시글 관련 요청 처리
│ │ └── post.service.ts # 게시글 비즈니스 로직
│ ├── comment/
│ │ ├── comment.schema.ts # 댓글 타입 정의
│ │ ├── comment.resolver.ts # 댓글 요청 처리
│ │ └── comment.service.ts # 댓글 비즈니스 로직
│ └── file/
│ ├── file.schema.ts # 파일 업로드 관련 타입 정의
│ ├── file.resolver.ts # 업로드 요청 처리
│ └── file.service.ts # 파일 저장 및 관리 로직
├── schema.ts # 전체 스키마 통합
├── resolvers.ts # 전체 리졸버 병합
└── index.ts # 서버 진입점 (GraphQL Yoga 실행)
이와 같이 모듈을 도메인별로 구성하면, 각 기능의 스키마 정의, 비즈니스 로직, 요청 처리 코드를 독립적으로 관리할 수 있습니다.
그 결과, 기능 확장 시 기존 코드에 영향을 주지 않고 새로운 모듈을 추가할 수 있으며, 팀 협업 시에도 충돌을 최소화할 수 있습니다.
✔️ 구조 선택 시 고려해야 할 3가지 체크리스트
1. 프로젝트 규모와 팀 구성
▸ 소규모 프로젝트나 1~2인 개발에서는 단순 구조로 시작해도 되지만, 팀 단위 개발에서는 모듈화를 반드시 고려해야 합니다.
▸ 기능별 담당자가 명확한 경우, 도메인 단위 구조가 코드 소유권 관리에 유리합니다.
2. 기능 확장 가능성
▸ 현재는 User, Post만 필요하더라도, 향후 Comment, Like, Notification 등 확장이 예상된다면 초기에 모듈화 패턴을 적용하는 것이 좋습니다.
▸ 모듈화는 기능 추가 시 기존 코드 수정 범위를 최소화해 기능 간 결합도를 낮춥니다.
3. 테스트 및 배포 전략
▸ Service 계층 분리를 통해 GraphQL 서버 없이도 독립적인 단위 테스트가 가능하므로, CI/CD와 연계가 용이합니다.
▸ 배포 시 특정 기능만 롤백하거나 기능 플래그를 적용해야 할 경우, 모듈 단위로 격리된 구조가 안정성을 높입니다.
2. Schema / Resolver / Service 분리
GraphQL 서버를 설계할 때 가장 중요한 원칙 중 하나는 Schema, Resolver, Service 세 가지 계층을 명확히 나누는 것입니다.
각 계층이 담당하는 역할을 구분하면 코드의 가독성과 유지보수성이 크게 향상되며, 테스트와 확장에도 유리합니다.
🔸 Schema – API 구조 정의 계층
▸ GraphQL SDL(Schema Definition Language)을 사용해 타입, 쿼리, 뮤테이션, 서브스크립션 등의 구조를 정의합니다.
▸ 클라이언트가 어떤 데이터를 요청할 수 있으며, 응답이 어떤 형태인지 명확히 보여줍니다.
▸ 예: User, Post 타입 정의, users, createUser 쿼리/뮤테이션 정의.
🔸 Resolver – 요청 처리 계층
▸ 클라이언트로부터 들어온 쿼리(Query)와 뮤테이션(Mutation)에 대한 실제 실행 함수를 작성하는 계층입니다.
▸ 비즈니스 로직은 직접 작성하지 않고, Service 계층에 위임하여 처리합니다.
▸ 예: users 요청이 오면 userService.getAll()을 호출하고 결과를 반환.
🔸 Service – 비즈니스 로직 및 데이터 처리 계층
▸ 데이터베이스 쿼리, 외부 API 호출, 핵심 비즈니스 로직을 수행합니다.
▸ GraphQL 서버와 완전히 분리할 수 있으므로, 단위 테스트 작성이 용이합니다.
▸ 예: Prisma ORM을 이용한 DB 접근, 캐시 조회, 외부 API 연동.

이러한 계층 분리는 단일 책임 원칙(Single Responsibility Principle, SRP)을 적용한 구조로, 각 파일이 하나의 역할만 담당하도록 구성됩니다.
이를 통해 다음과 같은 이점을 얻을 수 있습니다:
1. 유지보수 용이성
▸ 스키마 변경은 schema.ts, 로직 변경은 service.ts에서만 작업 가능.
2. 테스트 효율성
▸ GraphQL 서버 없이도 Service 계층만 독립 테스트 가능.
3. 확장성 확보
▸ 새로운 기능(Post, Comment 등) 추가 시 동일한 구조를 복제하여 적용 가능.
4. 의존성 최소화
▸ 데이터 접근 방식(DB → API 변경 등)을 바꿔도 Resolver 로직에 영향이 적음.
✔️ 구조 예시
src/
└── modules/
└── user/
├── user.schema.ts # API 구조 정의
├── user.resolver.ts # 요청 처리
└── user.service.ts # 비즈니스 로직
이와 같이 Schema → Resolver → Service 계층을 분리하면, 코드의 책임이 명확해지고 팀 협업 시 역할 구분이 뚜렷해집니다.
또한, 테스트 환경에서도 각 계층을 독립적으로 다룰 수 있어, 서비스 확장성과 안정성을 모두 확보할 수 있습니다.
3. 모듈화된 아키텍처 적용 예시 (User 모듈)
앞서 살펴본 도메인 기반 구조와 Schema / Resolver / Service 계층 분리 원칙을 실제 코드로 구현해 보겠습니다.
예시는 User 기능을 기준으로 작성하며, 동일한 패턴을 다른 도메인(Post, Comment 등)에도 그대로 적용할 수 있습니다.
✔️ 디렉토리 구조
src/
└── modules/
└── user/
├── user.schema.ts # GraphQL 타입 및 쿼리/뮤테이션 정의
├── user.resolver.ts # 요청 처리 로직
└── user.service.ts # 비즈니스 로직 및 데이터 처리
├── schema.ts # 전체 스키마 병합
├── resolvers.ts # 전체 리졸버 병합
└── index.ts # 서버 실행 진입점
🔸1) user.schema.ts – API 구조 정의
// src/modules/user/user.schema.ts
export const userTypeDefs = /* GraphQL */ `
type User {
id: Int!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: Int!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
▸ User 타입과 관련된 모든 쿼리(Query)와 뮤테이션(Mutation)**을 정의합니다.
▸ 이 스키마는 이후 schema.ts에서 다른 모듈의 스키마와 함께 병합됩니다.
🔸2) user.service.ts – 비즈니스 로직 계층
// src/modules/user/user.service.ts
const users = [
{ id: 1, name: '홍길동', email: 'hong@example.com' },
{ id: 2, name: '김철수', email: 'kim@example.com' },
];
let nextId = 3;
export const userService = {
getAll: () => users,
getById: (id: number) => users.find((u) => u.id === id) || null,
create: (name: string, email: string) => {
const newUser = { id: nextId++, name, email };
users.push(newUser);
return newUser;
},
};
▸ 현재는 Mock 데이터를 사용하지만, 실제 환경에서는 Prisma ORM 또는 DB 쿼리로 교체할 수 있습니다.
▸ Service 계층은 Resolver와 분리되어 있어, GraphQL 서버 없이도 단위 테스트 가능.
🔸3) user.resolver.ts – 요청 처리 계층
// src/modules/user/user.resolver.ts
import { userService } from './user.service';
export const userResolvers = {
Query: {
users: () => userService.getAll(),
user: (_: unknown, args: { id: number }) => userService.getById(args.id),
},
Mutation: {
createUser: (_: unknown, args: { name: string; email: string }) =>
userService.create(args.name, args.email),
},
};
▸ 요청을 받아 필요한 데이터를 Service 계층에 위임합니다.
▸ 이 구조 덕분에 Resolver는 요청 매핑 역할만 수행하므로 코드가 간결해집니다.
🔸4) schema.ts – 스키마 병합
// src/schema.ts
import { userTypeDefs } from './modules/user/user.schema';
export const typeDefs = /* GraphQL */ `
${userTypeDefs}
`;
▸ 모든 모듈의 스키마를 하나의 typeDefs로 병합하는 진입점입니다.
🔸5) resolvers.ts – 리졸버 병합
// src/resolvers.ts
import { userResolvers } from './modules/user/user.resolver';
export const resolvers = {
Query: {
...userResolvers.Query,
},
Mutation: {
...userResolvers.Mutation,
},
};
▸ 각 모듈의 리졸버를 통합하여 최종 스키마에 적용합니다.
🔸6) index.ts – 서버 실행
// src/index.ts
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'http';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers,
}),
});
const server = createServer(yoga);
server.listen(4000, () => {
console.log('🚀 Server ready at http://localhost:4000/graphql');
});
▸ graphql-yoga v5 방식으로 서버를 구동합니다.
▸ HTTP 서버와 통합하여 배포 환경에서도 활용 가능합니다.
✔️ 테스트 예시
아래는 서버가 제대로 작동하는지 확인할 수 있는 GraphQL 쿼리입니다.
# 전체 사용자 목록 조회
query {
users {
id
name
}
}
# 단일 사용자 조회
query {
user(id: 1) {
name
email
}
}
# 사용자 추가
mutation {
createUser(name: "이영희", email: "lee@example.com") {
id
name
}
}

▸ GraphQL Playground 또는 Apollo Studio에서 위 쿼리를 실행하면 결과를 바로 확인할 수 있습니다.
이 예시는 User 모듈만 구현했지만, 같은 방식으로 Post, Comment, File 업로드 등 다른 도메인도 쉽게 확장할 수 있습니다.
이처럼 모듈화 + 계층 분리 패턴을 초기에 적용하면, 기능이 늘어나도 코드 복잡도가 폭발적으로 증가하지 않고, 확장성과 유지보수성을 안정적으로 확보할 수 있습니다.
4. 확장성 있는 초기 설계 방안
GraphQL 서버를 장기적으로 안정적으로 운영하려면, 기능 확장과 팀 협업을 고려한 구조 설계가 필요합니다.
아래 세 가지 원칙을 초기부터 적용하면 유지보수성과 확장성을 모두 확보할 수 있습니다.
1) 일관된 모듈 구조 유지
▸ 모든 도메인은 schema.ts, resolver.ts, service.ts 형태를 갖추도록 규칙화합니다.
▸ 코드 스타일과 네이밍 규칙을 ESLint/Prettier로 통일하여 협업 시 혼란을 줄입니다.
2) 변경에 강한 계층 설계
▸ DB나 외부 API 접근 로직은 반드시 Service 계층에 위치시켜, 스키마나 리졸버 코드의 변경을 최소화합니다.
▸이렇게 하면 데이터 소스가 변경돼도 핵심 비즈니스 로직만 수정하면 됩니다.
3) 기능 추가와 테스트 용이성 확보
▸ 새로운 기능(Post, Comment 등)은 기존 모듈 구조를 그대로 복제해 적용합니다.
▸ Service 계층이 독립적으로 테스트 가능하도록 설계해, CI/CD 파이프라인에서 빠른 검증이 가능합니다.
✔ 마무리 - 실무형 GraphQL 아키텍처 설계
이번 글에서는 GraphQL 서버를 도메인 기반 폴더 구조와 Schema / Resolver / Service 계층 분리 원칙에 따라 설계하고, 이를 모듈화 패턴으로 구현하는 방법을 살펴보았습니다.
핵심은 단순히 코드 파일을 나누는 것이 아니라, 각 계층과 모듈이 명확한 책임을 갖도록 구조화하는 것입니다. 이를 통해 다음과 같은 실무적 이점을 확보할 수 있습니다.
🔸 유지보수 용이성: 변경 범위가 명확하여 코드 수정 시 리스크 최소화
🔸 기능 확장성: 새로운 도메인 추가 시 기존 구조를 그대로 복제해 빠르게 확장 가능
🔸 협업 효율성: 팀 내 역할과 코드 소유권이 명확해져 충돌과 혼란 감소
🔸 테스트와 배포 안정성: Service 계층 테스트로 문제를 조기 발견하고, 모듈 단위 롤백이 가능
실무에서는 기능이 몇 개 되지 않는 초기 단계라도, 이번 장에서 다룬 구조를 적용해 두는 것이 장기적인 개발 효율을 높이는 핵심 전략입니다.
이 구조는 단순히 “예쁘게” 코드를 나누는 것이 아니라, 프로젝트의 성장 속도와 품질을 동시에 지키는 안전장치이기 때문입니다.
결국, 잘 설계된 GraphQL 아키텍처는 단기 성과뿐 아니라 장기적인 서비스 안정성과 팀 생산성까지 보장합니다.
📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/graphql-tutorial-server)
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'3.SW개발 > GraphQL 배우기' 카테고리의 다른 글
| 6편. GraphQL Scalar 타입 완전 정복 (0) | 2025.11.30 |
|---|---|
| 5편. GraphQL 스키마와 리졸버 구조 이해 (0) | 2025.11.30 |
| 3편. GraphQL Hello Query 실습: 첫 서버 구축하기 (0) | 2025.11.27 |
| 2편. GraphQL 서버 개발 환경 구축 (Node.js + TS) (0) | 2025.11.25 |
| 1편. GraphQL이 필요한 이유: REST와의 차이 완벽 이해 (0) | 2025.11.25 |