[Spring] GraphQL 이란? 실습해보기
오늘은 REST API와 조금 다룬 GraphQL에 대해서
기본적인 개념와 실습을 위해 프로젝트를 하나파서 만들어봤다.
내가 만든 GitHub 레포지토리를 참고하면 가장 기초적인 셋팅과
사용법을 볼 수 있으니 참고바란다.
GraphQL
GraphQL 이란?
GraphQL은 REST API의 대안으로 페이스북에서 제시한 새로운 Web API 컨셉이라고한다.
REST API 는 우리가 알듯이 다양한 HTTP Method가 서버에 존재하는 리소스와 대응되어서 동작한다.
클라이언트의 요청사항과 리소스가 잘 들어맞지 않는다면 성능 문제가 발생할 수 있다.
GraphQL 은 클라이언트 단일 요청에서 여러 하위 자원 탐색을 포함하여 원하는
데이터만 정확하게 지정할 수 있다.
이로서 얻을 수 있는 장점은 아래와 같다.
-
Overfetching
기존의 Rest Api에선 api 호출 시 필요 이상의 정보를 전달받는 문제가 있었다.
GraphQL은 client에서 필요한 데이터만을 요청하므로 overfetching이 해결 된다. -
Endpoint
Rest Api는 각 api마다 다른 endpoint가 있어서 이름을 짓거나 관련성이 있는 것들끼리 묶는 등의 관리가 어렵다.
GraphQL은 단 하나의 endpoint가 있어서 요청 주소가 매우 간단해진다.
GraphQL 적용해보기
프로젝트 개요
이제 대략적인 개념을 이해했으니 서버에서 구현하여 테스트해보자
Java 진영에서는 Spring 프레임워크를 이용하면 손쉽게 구현이 가능하다.
기존에 MVC 개발하듯 진행할 수 있다.
SpringBoot + Spring Data JPA + GraphQL을 이용한 프로젝트다.
Spring for GraphQL은 Spring Boot 2.7.0 버전 이상부터 지원한다고한다.
GraphQL을 통해 요청을 보낸다음
Spring data JPA로 임베디드 H2 데이터베이스에 접근하여
데이터를 조회하고 저장하는 테스트를 진행할 것이다.
프로젝트 셋팅
셋팅에 앞서 모든 셋팅을 다올리진 않을 것이다.
글의 가독성 측면이나 현재 GraphQL 설명에 집중하기 위해서이다.
(프로젝트 생성에 다른 정보가 필요하다면 GitHub 레포지토리를 참고하자)
프로젝트 구조
├── main
│ ├── java
│ │ └── com
│ │ └── prac
│ │ └── graphql
│ │ ├── GraphqlApplication.java
│ │ ├── domain
│ │ │ ├── controller
│ │ │ │ └── MemberController.java
│ │ │ ├── dto
│ │ │ │ └── MemberResponseDto.java
│ │ │ ├── entity
│ │ │ │ └── Member.java
│ │ │ ├── mapper
│ │ │ │ └── MemberMapper.java
│ │ │ ├── repository
│ │ │ │ └── MemberRepository.java
│ │ │ └── service
│ │ │ └── MemberService.java
│ │ └── global
│ │ └── exception
│ │ ├── BusinessLogicException.java
│ │ ├── ExceptionCode.java
│ │ └── handler
│ │ └── GraphQLExceptionHandler.java
│ └── resources
│ ├── application.yml
│ ├── data.sql
│ ├── graphql
│ │ └── schema.graphqls
│ ├── static
│ └── templates
└── test
└── java
└── com
└── prac
└── graphql
└── GraphqlApplicationTests.java
build.gradle
dependencies {
// ... 생략
implementation 'org.springframework.boot:spring-boot-starter-graphql' // <- graphql
testImplementation 'org.springframework:spring-webflux' // <- graphql
testImplementation 'org.springframework.graphql:spring-graphql-test' // <- graphql
// ... 생략
}
Spring for GraphQL
dependencies를 추가하면 옆에 주석처리해둔 3가지가 생긴다.
application.yml
spring:
graphql:
graphiql:
enabled: true # Graphql 테스트가 가능해진다. localhost:8080/graphiql 에 접속해서 가능
printer:
enabled: true # JPA에 Show-sql과 같이 Graphql 쿼리를 출력해준다.
application.properties
spring.graphql.graphiql.enabled=true
spring.graphql.graphiql.printer.enabled=true
.yml
,properties
파일 셋팅 방법이다.
나는 .yml
파일을 사용하였고 이외에 H2, Spring data JPA 관련 설정은
위에 올려둔 깃허브 레포지토리를 참고하자
코드를 보기전에 아래의 3개 어노테이션의 용도를 알고있으면
도움이되니 먼저 읽어보고 코드를 확인하자
@MutationMapping
@MutationMapping
은 Create, Update, Delete에 대응된다고 생각하면 될 것 같다.
graphql은 endpoint가 하나이므로 @MutationMapping 어노테이션만 지정해 주고 다른 설정은 필요 없습니다.
즉, @PostMapping, @PatchMapping, @DeleteMapping 등을 대신사용
@QueryMapping
@QueryMapping
는 Read에 대응된다고 생각하면 될 것 같다.
말고도 @SubscriptionMapping
이 있다고 한다.
즉, @GetMapping 을 대신 사용
@SchemaMapping
위의 두개의 어노테이션을 통용해서 쓰는듯한 느낌이다.
typeName(Query, Mutation)을 설정하여 사용이 가능하다.
또한 spring-boot-starter-graphql
를 사용하는 경우 default로 설정된
Schema 파일 위치는 classpath:graphql/**/
으로 /src/main/resources/graphql/
경로에 schema.graphqls
파일을 만들어 사용할 수 있다. (graphql가 아닌 graphqls이다)
schema.graphqls
type Member{
id: ID!
name: String!
age: Int!
}
type MemberResponseDto{
id: ID!
name: String!
age: Int!
}
# @QueryMapping 메서드 이름과 동일
# value 속성을 통해서 이름을 정해줄 수 있음
type Query{
getMember(id: ID!): Member
getMembers: [Member]
getMemberResponseDto(id: ID!): MemberResponseDto
getMembersResponseDto: [MemberResponseDto]
getMemberBySchema(id: ID!): Member
getMembersBySchema: [Member]
}
# @MutationMapping 메서드 이름과 동일
# value 속성을 통해서 이름을 정해줄 수 있음
type Mutation {
postMember(
id : Int
name : String
age : Int
): Member
postMemberByMutation(
id : Int
name : String
age : Int
): Member
}
Member.java
@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
}
MemberService.java
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member findMemberById(Long id) {
Optional<Member> findMember = memberRepository.findById(id);
return findMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
}
public List<Member> findAllMember() {
return memberRepository.findAll();
}
public Member saveMember(Member member) {
return memberRepository.save(member);
}
}
MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
}
MemberMapper.java
@Component
public class MemberMapper {
public static MemberResponseDto memberToMemberResponseDto(Member member) {
return MemberResponseDto.builder()
.id(member.getId())
.name(member.getName())
.age(member.getAge())
.build();
}
public static List<MemberResponseDto> memberListToMemberResponseDtoList(List<Member> members) {
return members.stream()
.map(member -> {
return memberToMemberResponseDto(member);
})
.collect(Collectors.toList());
}
public static Member toEntity(String name, Integer age) {
Member newMember = new Member();
newMember.setName(name);
newMember.setAge(age);
return newMember;
}
}
위에까지는 REST API와 별반 다르지 않은 계층의 코드이다.
Repository - Service 계층이고 Mapper를 이용해 맵핑을 도와주는 클래스다.
Controller 부터는 조금 달라진 모습을 볼 수 있을 것이다.
MemberController.java
/*
* EndPoint : http://localhost:8080/graphql
* Test 접속 URL : http://localhost:8080/graphiql 에 쿼리를 입력
* */
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final MemberMapper memberMapper;
/*
* @QueryMapping
* -> 주로 Read에 사용됩니다. (REST API @GetMapping과 대응)
* */
@QueryMapping
public Member getMember(@Argument Long id){
return memberService.findMemberById(id);
}
@QueryMapping
public List<Member> getMembers(){
return memberService.findAllMember();
}
@QueryMapping
public MemberResponseDto getMemberResponseDto(@Argument Long id){
Member member = memberService.findMemberById(id);
return memberMapper.memberToMemberResponseDto(member);
}
@QueryMapping
public List<MemberResponseDto> getMembersResponseDto(){
List<Member> allMember = memberService.findAllMember();
return memberMapper.memberListToMemberResponseDtoList(allMember);
}
/*
* @MutationMapping
* -> 주로 Create, Update, Delete에 사용됩니다.
* */
@MutationMapping
public Member postMember(@Argument String name,
@Argument Integer age){
Member member = memberMapper.toEntity(name, age);
return memberService.saveMember(member);
}
/*
* @SchemaMapping
* -> @QueryMapping, @MutationMapping 를 선택하여 사용할 수 있음.
* */
@SchemaMapping(typeName = "Query")
public Member getMemberBySchema(@Argument Long id) {
return memberService.findMemberById(id);
}
@SchemaMapping(typeName = "Query")
public List<Member> getMembersBySchema() {
return memberService.findAllMember();
}
@SchemaMapping(typeName = "Mutation")
public Member postMemberByMutation(@Argument String name,
@Argument Integer age){
Member member = memberMapper.toEntity(name, age);
return memberService.saveMember(member);
}
}
첫번쨰로 어노테이션들이 기존에 사용하던 Mapping어노테이션과는 다르고
@ReqeustBody와 같은 어노테이션도 사용하지 않는 모습이다.
반환타입도 schema.graphqls
에 지정되어있는 클래스들을 반환 타입으로 사용하였고
메서드 이름을 통해 Query,Mutation에 접근하는 형태를 가지고있다.
value라는 속성값을 이용하면 ex) @MutationMapping(value = “postMember”)
메서드 이름이아닌 지정한대로 변경도 가능하다.
REST API와 어노테이션과 반환형식만 다르지
크게 코드적으로 어렵게 작성해야하는 부분은 없다.
이런 기능을 제공해주는 Spring에게 감사를,,,
테스트 진행
이제 위와 같이 프로젝트 셋팅이 끝났으면
Controller에 작성한 API를 검증할 수 있다.
Spring Boot를 실행하면
위 사진과 같이 GraphQL endpoint가 표시되면 정상적으로 실행된 것이다.
로컬환경에서 API 요청시 localhost:8080/graphql
로 요청을 진행하면된다.
실제 API를 테스트하는 방법은 2가지 정도 있다.
1). Postman을 통한 요청
포스트맨으로 위에 앤드포인트로 요청하면
위와 같이 정상적으로 작동하는지 확인이 가능하다.
문법 자동완성이되지 않아 2번방법을 추천한다.
2). graphiql을 통한 요청
브라우저에 localhost:8080/graphiql
를 접속하면된다.
i
가 추가되었으니 이점을 꼭 참고하자
접속하게되면 위와 같이 쿼리를 입력하고 오른쪽에는 출력이된 화면이 표시된다.
실제로 요청 쿼리를 만들어 보냈을 경우
위에 처럼 확인이 가능한 모습을 볼 수 있다.
프론트쪽에서 원하는 데이터를 요청하는 것들만 만들어서
반환하는 특성을 확인해 볼 수 있다.
그럼 실제로 우리가 위에서 만든 Controller를 테스트하기 위해서
아래와 같은 쿼리문이 필요하다.
query{
getMember(id:1) {
id
name
age
}
getMembers {
id
name
}
getMemberResponseDto(id :1) {
id
name
age
}
getMembersResponseDto {
id
name
}
getMemberBySchema(id :1) {
id
name
age
}
getMembersBySchema {
id
name
}
}
총 6개 API를 호출하는 테스트이다.
또한 POST를 해보기 위해서
mutation를 만들어서 실제로 Database에 저장되는지도 확인해보았다.
mutation{
postMember(name:"미노이", age:18){
id
name
age
}
postMemberByMutation(name:"김채원", age:19){
id
name
age
}
}
@Argument
로 받을 매개변수 설정(name, age)하여
id,name,age를 반환받는다고 보면 될 것 같다.
이렇게 오늘은 REST API대신에 GraphQL이라는 새로운 방식을
공부해보았다. 처음에 생소해서 공식문서랑 여러 블로그를 뒤져가면서 공부했는데
직접 코드를 작성하면서 체득하는 것이 확실히 이해하는데 빠른 것 같다.
✨ 참고 블로그