Notice
Recent Posts
Recent Comments
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- 하버드
- ActiveX
- 개발
- Eclipse
- 코틀린
- hcj
- Android
- css
- gradle
- 안드로이드
- 구글
- html
- kotlin
- js
- 안드로이드 개발
- 자바
- GraphQL
- build.gradle
- springboot
- 안철수
- 보안
- java
- linux
- C++
- Android 4.1
- 탐지기법
- 노개북
- JavaScript
- 리눅스
Archives
- Today
- Total
꿈소년의 개발 이야기
[스프링 부트 개발자 온보딩 가이드 스터디] Chapter 07 GraphQL 기반 마이크로블로그 API 서버 개발 본문
Do it 스터디!/스프링 부트 개발자 온보딩 가이드 스터디!
[스프링 부트 개발자 온보딩 가이드 스터디] Chapter 07 GraphQL 기반 마이크로블로그 API 서버 개발
fogthegreat 2026. 4. 5. 21:27반응형
DAY 9
🔖 오늘 읽은 범위 :
🌱공부 내용: Chapter 07 GraphQL 기반 마이크로블로그 API 서버 개발
👢쪽수: p.291 - p.344
- 목표: GraphQL 기반 Minilog API 재구현
- 기능 요구사항
- GraphQL Query 기능
- GraphQL Mutation 기능
- 구현 요구사항
- 엔드포인트는 /graphql 을 사용한다.
- 해당 엔드포인트를 통한 GraphQL 연산은 인증을 거친 요청에 한해 허용한다.
GraphQL 이해하기
GraphQL | A query language for your API
GraphQL
- API 쿼리 언어
- 클라이언트가 원하는 데이터 구조를 직접 정의할 수 있어 유연하고 효율적임.
- 중요한 포인트는 QL(Query Language).
- 클라이언트가 필요한 데이터 형태를 선언적으로 정의하고 서버가 구조에 맞게 정확히 일치하는 데이터를 반환한다.
GraphQL 핵심 개념
- 선언적 데이터 요청
- 클라이언트가 “필요한 데이터와 그 구조를 명시적으로 정의”할 수 있다.
- 서버는 요청된 필드만 반환하기 때문에 불필요한 데이터 전송이 없다.
- 단일 엔드포인트
- 모든 요청이 단일 엔드포인트를 통해 처리 된다.
- 필요한 데이터를 한번에 가져온다.
- 타입 시스템
- 엄격한 타입 시스템 제공.
- API 데이터 구조를 명확하게 정의함.
- 오버 패칭이 없다.
- 언더 패칭은 단일 요청으로 해결 가능하다.
GraphQL 구성 요소
스키마
- 클라이언트가 요청할 수 있는 데이터 구조 정의.
쿼리
- 데이터를 조회하는 요청.
- 필요한 데이터와 구조를 클라이언트가 명시적을 요청할 수 있다.
뮤테이션
- 데이터 수정 또는 추가하는 요청.
- 새로운 데이터 생성, 기존 데이터 업데이트도 포함.
서브스크립션
- 실시간 데이터 업데이트 요청.
- 웹 소켓을 활용하기 때문에 GraphQL 은 실시간 데이터 업데이트가 가능하다.
- 웹 소켓 연결을 통해 서버와 클라이언트 간의 지속적인 연결을 유지하여, 서버에서 발생하는 이벤트를 실시간으로 클라이언트에게 전달할 수 있다. 클라이언트는 데이터 변경 사항을 즉시 수신할 수 있다.
리졸버
요청에 대한 실제 데이터를 제공하는 함수.
데이터를 다양한 소스에서 가져올 수 있다. 데이터베이스, REST API 엔드포인트, 다른 GraphQL 서버에서도 데이터를 가져올 수 있다.
스프링 부트3에서 GraphQL 리졸버는 일반적으로 @QueryMapping, @MutationMapping 과 같은 애노테이션을 사용하여 정의한다.
예시
// 1. Database 에서 가져오기 @RestController @RequestMapping("/graphql") public class UserResolver { @QueryMapping public User getUser(@Argument Long id) { return userRepository.findById(id).orElseThrow(); } } // 2. 다른 Rest API 엔드포인트에서 데이터 가져오기 @RestController @RequestMapping("/graphql") public class UserResolver { private final RestTemplate restTemplate; public UserResolver(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @QueryMapping public User getUser(@Argument Long id) { String url = "https://api.example.com/users/" + id; return restTemplate.getForObject(url, User.class); } } // 3. 다른 GraphQL 서버에서 데이터 가져오기 @RestController @RequestMapping("/graphql") public class UserResolver { private final GraphQLClient graphQLClient; public UserResolver(GraphQLClient graphQLClient) { this.graphQLClient = graphQLClient; } @QueryMapping public User getUser(@Argument Long id) { String query = "{ user(id: " + id + ") { name email } }"; return graphQLClient.execute(query, User.class); } }
GraphQL 동작 구조
- 서버 → 클라이언트 요청 처리 단계
- 클라이언트 요청 수신
- 클라이언트는 단일 엔드포인트로 쿼리 또는 뮤테이션 요청 보낸다.
- 스키마와 요청 매핑
- 서버는 요청 내용을 스키마에 정의한 타입 및 필드와 매핑한다.
- 리졸버 호출
- 매핑된 필드에 따라 리졸버를 호출하고 필요한 데이터를 조회 또는 연산을 수행한다.
- 응답 생성 및 반환.
- 리졸버 결과를 기반으로 응답을 구성한 후 클라이언트에게 반환한다.
- 리졸버에서 반환 한 결과들을 가지고, GraphQL 서버(Spring Boot 3)에서 최종 응답을 구성하여 전달한다.
- 클라이언트 요청 수신
GraphQL 스키마
- 클라이언트가 요청할 수 있는 데이터 타입과 구조 정의
타입(Type)
- 데이터 표현 기본 단위.
- String, Int, Boolean 같은 기본 타입
- 사용자 정의 타입.
| GraphQL | Java |
|---|---|
| Int | Integer |
| Float | Double |
| String | String |
| Boolean | Boolean |
| ID | String or Long |
쿼리 타입
- 데이터 조회 가능한 필드 정의.
뮤테이션 타입
- 데이터 추가, 수정, 삭제 할 경우 사용하는 필드 정의.
필드
- 각 타입의 속성.
- 특정 데이터에 대한 접근 방법 정의.
쿼리
- 데이터 조회.
- 요청할 필드를 명시적으로 정의.
- 필요한 데이터만 가져올 수 있게 불필요한 데이터 제외 가능.
{
user {
name
age
}
}
뮤테이션
- 데이터 추가, 수정, 삭제 할때 사용한다.
- REST 의 POST, PUT, DELETE 요청.
mutation {
addMessage(content: "Hello, GraphQL") {
id
content
}
}
에러 처리
- data 필드와 errors 필드가 포함 된 응답 반환.
- 에러 처리 흐름
- 클라이언트가 잘못된 쿼리를 서버에 전송한다.
- 서버가 스키마와 쿼리를 비교하여, 유효성을 검사하는 과정에서 오류 감지한다.
- 서버는 오류 메시지를 포함한 응답을 생성하여 클라이언트에게 반환한다.
{
"errors": [
{
"message": "Field 'unknownField' not found in type 'Query'",
"locations": [ { "line": 1, "column": 2 } ]
}
]
}
GraphQL 에서 고려해야 할 문제
- 복잡한 서버 설정
- 설정이 REST API 보다 복잡하다.
- 데이터 로더와 같은 최적화 도구가 추가로 필요할 수 있다.
- 캐싱 문제
- REST HTTP 캐싱(headers, status code 등)을 활용하기 어렵다.
- 클라이언트가 별도의 캐싱 전략을 구현해야 할 수도 있다.
- 스키마 설계의 중요성
- 스키마가 API 설계의 중심이다. 따라서 설계 단계에서 신중하게 계획할 필요가 있다.
프로젝트
// application.properties
# GraphiQL을 사용하는 경우, 엔드포인트 설정
spring.graphql.graphiql.enabled=true // GraphQL 오퍼레이션 실행 여부. 상용 배포에는 false 해야 함.
spring.graphql.graphiql.path=graphiql스키마 정의
지정 경로
<Project 경로>/src/main/resources/graphql/schema.graphqls스칼라 타입 확장
- Long, DateTime 같은 것을 정의하여 다양한 데이터 유형 지원.
쿼리, 뮤테이션 오퍼레이션 정의
Input Type 활용
- 뮤테이션 인 경우, Input Type 을 활용해 코드 확장성과 가독성 향상.
응답 타입 정의
스칼라 정의
Scalar : 기본 데이터 유형
지원되지 않는 데이터 유형은 확장 시켜서 스칼라를 직접 정의한다.
scalar Long scalar DateTime @specifiedBy(url:"https://tools.ietf.org/html/rfc3339")실제 매핑 처리 로직은
GraphQLConfig에서 구현한다.
쿼리 정의
type Query {
getArticles(userId: Long!): [ArticleResponse]
getArticle(articleId: Long!): ArticleResponse
getFeedList(followerId: Long!): [ArticleResponse]
getFollowList(followerId: Long!): [FollowResponse]
getUsers: [UserResponse]
getUserById(userId: Long!): UserResponse
}
뮤테이션 정의
type Mutation {
createArticle(input: CreateArticleInput!): ArticleResponse
updateArticle(input: UpdateArticleInput!): ArticleResponse
deleteArticle(articleId: Long!): Boolean
follow(followeeId: Long!): FollowResponse
unfollow(followeeId: Long!): Boolean
createUser(input: CreateUserInput!): UserResponse
updateUser(input: UpdateUserInput!): UserResponse
deleteUser(userId: Long!): Boolean
}
뮤테이션 Input Type 정의
input CreateArticleInput {
content: String!
}
응답 타입 정의
type ArticleResponse {
articleId: Long
content: String
authorId: Long
authorName: String
createdAt: DateTime
}
GraphQLConfig - 스칼라 타입 매핑
// @Configuration 을 통해서 스프링 부트가 애플리케이션 실행 시, 이 설정 클래스를 자동 인식한다.
// 여기서 RuntimeWiringConfigurer 빈이 자동 등록되고, GraphQL 실행 시점에 해당 설정이 자동 적용된다.
@Configuration
public class GraphQLConfig {
// RuntimeWiringConfigurer: graphql 실행 환경(Runtime Wiring)을 구성하는 인터페이스.
// GraphQL 엔진에 등록할 수 있다.
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder ->
wiringBuilder
.scalar(ExtendedScalars.DateTime) // DateTime 타입을 RFC3339 표준 형식으로 처리 지원.
.scalar(ExtendedScalars.GraphQLLong); // 64bit 정수 처리.
}
}
전역 GraphQL 예외 처리
@Component
public class MinilogGraphQLExceptionResolver extends DataFetcherExceptionResolverAdapter {
private static final Logger logger =
LoggerFactory.getLogger(MinilogGraphQLExceptionResolver.class);
@Override
protected @Nullable GraphQLError resolveToSingleError(
Throwable exception, DataFetchingEnvironment env) {
logger.error(
"Error occurred while fetching data at {}: {}",
env.getExecutionStepInfo().getPath(),
exception.getMessage(),
exception);
GraphQLError error;
var handlerParameterPath = env.getExecutionStepInfo().getPath();
if (exception instanceof ArticleNotFoundException
|| exception instanceof UserNotFoundException) {
error =
GraphqlErrorBuilder.newError()
.message(exception.getMessage())
.errorType(ErrorType.NOT_FOUND)
.path(handlerParameterPath)
.build();
} else if (exception instanceof NotAuthorizedException) {
error =
GraphqlErrorBuilder.newError()
.message(exception.getMessage())
.errorType(ErrorType.UNAUTHORIZED)
.path(handlerParameterPath)
.build();
} else if (exception instanceof IllegalArgumentException) {
error =
GraphqlErrorBuilder.newError()
.message(exception.getMessage())
.errorType(ErrorType.BAD_REQUEST)
.path(handlerParameterPath)
.build();
} else {
error =
GraphqlErrorBuilder.newError()
.message("Internal Server Error")
.errorType(ErrorType.INTERNAL_ERROR)
.path(handlerParameterPath)
.build();
}
return error;
}
}
GraphQL 표준 오류 타입은 아래와 같다. 보통 이 기준으로 분류가 된다.
public enum ErrorType implements ErrorClassification { /** * {@link graphql.schema.DataFetcher} cannot or will not fetch the data value due to * something that is perceived to be a client error. */ BAD_REQUEST, /** * {@link graphql.schema.DataFetcher} did not fetch the data value due to a lack of * valid authentication credentials. */ UNAUTHORIZED, /** * {@link graphql.schema.DataFetcher} refuses to authorize the fetching of the data * value. */ FORBIDDEN, /** * {@link graphql.schema.DataFetcher} did not find a data value or is not willing to * disclose that one exists. */ NOT_FOUND, /** * {@link graphql.schema.DataFetcher} encountered an unexpected condition that * prevented it from fetching the data value. */ INTERNAL_ERROR }
GraphQL 적용 된 API 구조
graph LR
GraphQLResolver(GraphQL Resolver) --> Service(Service)
Service --> Repository(Repository)
Repository --> Database[(Database)]
API 테스트
-
- 스웨거로 로그인해서 JWT 발급.
-
JWT 등록 후 쿼리, 뮤테이션 API 진행.
{ "Authorization": "Bearer JWT_토큰값" }
GraphQL 도입은 언제?
- 도입 기준은?
- 클라이언트가 각각 다른 화면이나 컴포넌트가 다양한 데이터 조합을 필요로 할 때.
- 이때 클라이언트는 하나가 아니라 여러 곳이다. 모바일, 웹, 외부 오픈 API 등등.
- 여러 서비스 특히 마이크로 서비스나 여러 DB 등일 경우에 데이터를 조합해서 한꺼번에 내려줘야 할 때.
- REST API 의 경우,
- API 구조가 단순한 경우.
- 각 화면에 필요한 데이터가 정해져 있는 경우.
- GraphQL 에 대한 경험이 적거나 없어서 학습이 아직 필요한 경우.
- REST 한계점.
- 오버 패칭(Over-Fetching; 불필요한 데이터까지 가져오는 문제)
- 언더 패칭(Under-Fetching; 원하는 데이터를 한번에 못 받아서 여러 번 호출해야 하는 문제)
- GraphQL 한계점.
- 설계와 운영의 복잡성
- N+1 쿼리 문제
- 권한/인증 체크의 어려움. ⇒ 특정한 테이블 접근 권한을 제어할 수 있나?
- 단일 엔드포인트로 인한 요청 패턴과 데이터 조합의 다양성으로 인해 모니터링과 트러블 슈팅의 어려움.
- 스키마의 작은 변화에도 민감한 서비스 영향도와 그로 인한 배포 및 운영 관리의 복잡성 문제.
- 일반적으로 REST 로 시작하고, 서비스와 데이터 구조가 복잡해질 때, 일부 API 를 GraphQL 로 점진적 전환 방식을 채택한다.
- GraphQL API 를 경험한 적이 있는데, 실제 작업해보니 그 당시 백엔드 개발자들의 고민을 느껴보게 된다.
반응형
'Do it 스터디! > 스프링 부트 개발자 온보딩 가이드 스터디!' 카테고리의 다른 글
| 스프링 부트 개발자 온보딩 가이드 스터디 후기 (0) | 2026.04.05 |
|---|---|
| [스프링 부트 개발자 온보딩 가이드 스터디] Chapter 08 도커를 이용한 애플리케이션 패키징 및 배포 (0) | 2026.04.05 |
| [스프링 부트 개발자 온보딩 가이드 스터디] 구글 OAuth2 로그인 기능과 JWT, 스프링 시큐리티 구현 (0) | 2026.04.05 |
| [스프링 부트 개발자 온보딩 가이드 스터디] Chapter 06 Minilog에 인증 기능 추가하기 (1) | 2026.04.05 |
| [스프링 부트 개발자 온보딩 가이드 스터디] 복잡한 실제 비즈니스 요구사항과 JPA 코드 구조 잡기 (0) | 2026.04.03 |
