라이브러리

spring-boot-graphql 설정하기

탄생 2021. 3. 25. 13:50

graphql이란?

Graphql은 페이스북에서 만든 쿼리 언어입니다.
웹 클라이언트가 데이터를 서버로 부터 효율적으로 가져오는 것이 목적입니다.
클라이언트 시스템에서 작성하고 호출합니다.

 

rest API 와 비교

REST APIURL, METHOD등을 조합하기 때문에 다양한 Endpoint존재 합니다.
반면, graphql은 단 하나의 Endpoint존재 합니다.
또한, graphql API에서는 불러오는 데이터의 종류를 쿼리 조합을 통해서 결정 합니다.
예를 들면, REST API에서는 각 Endpoint마다 데이터베이스 SQL 쿼리가 달라지는 반면, graphql APIgraphql 스키마의 타입마다 데이터베이스 SQL 쿼리가 달라집니다.

예제 GIT 주소


graphql 설정

1. dependencies 설정

- build.gradle

    testImplementation group: 'com.h2database', name: 'h2', version: '1.4.200' // 예제 database로 활용
    
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.4.3'
    implementation group: 'com.graphql-java-kickstart', name: 'graphql-spring-boot-starter', version: '11.0.0'
    implementation group: 'com.graphql-java-kickstart', name: 'graphql-java-tools', version: '11.0.0'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

 

2. GraphQL servlet 설정

spring boot 설정 추가(application.yml or application.properties)

spring:
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
    driverClassName: org.h2.Driver
    hikari:
      maximum-pool-size: 24
  h2:
    console:
      enabled: true
  jpa:
    generate-ddl: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect
        format_sql: true
    show-sql: true
graphql:
  servlet:
    enabled: true
    mapping: /graphql
    corsEnabled: false
    cors:
      allowed-origins: http://localhost:8080
      allowed-methods: GET, HEAD, POST, PATCH
    exception-handlers-enabled: true
    context-setting: PER_REQUEST_WITH_INSTRUMENTATION
    async-mode-enabled: true
  tools:
    schema-location-pattern: "**/*.graphqls"
    introspection-enabled: true

 

3. 도메인, 레포지토리, 데이터 만들기

MemberRole의 도메인과 해당 데이터를 채워주는 data를 만듭니다.

 

- domain/Member.class

@Builder
@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String loginId;

    private String password;

    private String name;

    @OneToMany
    @JoinColumn(name = "memberId", referencedColumnName = "id", insertable = false, updatable = false)
    private List<Role> role;
}

- domain/Member.class

@Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private int memberId;

    private String role;

- repository/MemberRepository

@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {
}

- repository/RoleRepository

@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
    Role findByMemberId(int memberId);
}

- resources/data.sql

INSERT INTO member (id, login_id, password, name) VALUES
  (1, 'member', 'password', '멤버'),
  (2, 'account', 'password', '계정'),
  (3, 'user', 'password', '사용자');

INSERT INTO role (id, member_id, role) VALUES
  (1, 1, 'ROLE_ADMIN'),
  (2, 1, 'ROLE_MEMBER'),
  (3, 2, 'ROLE_MEMBER'),
  (4, 3, 'ROLE_MANAGER');

 

4. dto 만들기

파라미터와 응답값으로 활용할 자료 구조를 만들어 줍니다.
from 메소드를 통해 domaindtoconvert해 줍니다(실무에서는 ObjectMapper등을 활용하시면 됩니다.)

 

- dto/MemberDto.class

@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberDto {

    private Integer id;

    private String loginId;

    private String password;

    private String name;

    private List<RoleDto> roles;

    public static List<MemberDto> from(Collection<Member> entities) {
        return entities.stream().map(MemberDto::from).collect(Collectors.toList());
    }

    public static MemberDto from(Member entity) {
        return MemberDto.builder()
                        .id(entity.getId())
                        .loginId(entity.getLoginId())
                        .password(entity.getPassword())
                        .name(entity.getName())
                        .roles(RoleDto.from(entity.getRole()))
                        .build();
    }
}

- dto/RoleDto.class

@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleDto {
    private Integer id;

    private Integer memberId;

    private String role;

    public static List<RoleDto> from(Collection<Role> entities) {
        if(entities == null) {
            return null;
        }
        return entities.stream().map(RoleDto::from).collect(Collectors.toList());
    }

    public static RoleDto from(Role entity) {
        return RoleDto.builder()
                        .id(entity.getId())
                        .memberId(entity.getMemberId())
                        .role(entity.getRole())
                        .build();
    }
}

 

5. 스키마와 Resolver 작성

1~4번은 일반적인 프로젝트의 설정 부분이라고 생각하시면 되고 이번 설정이 graphql의 주요 설정이라고 보시면 됩니다.
graphqlcontroller 가 없고 스키마를 통해 Resolver에 요청하여 그에 대한 응답을 줍니다.

5-1. 스키마 작성

스키마 파일은 *.graphqls라는 확장자로 저장해야 합니다. 클래스패스(classpath) 어느 곳에나 존재할 수 있으며,
여러개의 스키마 파일을 작성하여서 개발자가 원하는 만큼 모듈화 할 수 있습니다.
한가지 요구사항은 루트 쿼리(Root Query)와 루트 뮤테이션(Root Mutation)에 대한 정의는 하나만 존재해야 합니다.
(이것은 자바나 스프링 부트의 요구사항이 아니라, GraphQL 스키마의 요구사항 입니다.)

Spring Boot GraphQL Starter는 스키마 파일을 자동으로 탐색하고, 이를 GraphQL을 담당하는 스프링 빈(bean)에게 명시된 스키마 구조를 주입시켜 줍니다.

 

GraphQL SchemaPermalink
GraphQL Schema에는 앞에서 설명했던 데이터 타입을 지정하는 것 뿐만 아니라, 몇 가지 지켜야 할 규칙들이 있습니다.

- Schema 확장자 : 모든 GraphQL Schema 의 확장자는 *.graphqls 로 설정되어야 합니다.
- Query : 읽기 연산을 정의함
- Mutation : 쓰기 연산을 정의함
- type : 응답값을 정의함
- input : 파라미터값을 정의함
- type, input 의 혼용은 불가능 함 : 2개의 값이 정확히 동일해도 각각 정의 내려야 함.
- 타입명 뒤에 ! : null 값을 허용하지 않겠다는 의미임
- 루트 쿼리(Root Query)와 루트 뮤테이션(Root Mutation)에 대한 정의는 하나만 존재해야 합니다.

- resources/members.graphqls

type Member {
    id: Int!
    login_id: String!
    password: String!
    name: String
    roles: [Role]
}

type Role {
    id: Int!
    member_id: Int!
    role: String
}

input MemberParam {
    loginId: String!
    password: String!
    name: String
}

# 루트 쿼리 (Root Query)
type Query {
    getMember(id: Int!) : Member!
}

# 루트 뮤테이션 (Root Mutation)
type Mutation {
    createMember(memberParam: MemberParam) : Member!
    deleteMember(id: Int!) : Boolean!
}

 

5-2. Resolver 작성

스키마에서 작성한 함수(파라미터)Resolver에서 작성한 함수(파라미터)와 매핑되어 실행이 됩니다.

 

- graphql/MemberMutation.class

@Component
@RequiredArgsConstructor
@Transactional
public class MemberQuery implements GraphQLQueryResolver {

    private final MemberRepository memberRepository;

    public MemberDto getMember(int id) {
        Member member = memberRepository.findById(id)
                                        .orElse(null);
        return MemberDto.from(member);
    }
}

- graphql/MemberMutation.class

@Component
@RequiredArgsConstructor
@Transactional
public class MemberMutation implements GraphQLMutationResolver {

    private final MemberRepository memberRepository;

    private final RoleRepository roleRepository;

    public MemberDto createMember(MemberDto memberDto) {
        Member member = memberRepository.save(Member.builder()
                                                    .loginId(memberDto.getLoginId())
                                                    .name(memberDto.getName())
                                                    .password(memberDto.getPassword())
                                                    .build());
        return MemberDto.from(member);
    }

    public Boolean deleteMember(int id){
        Optional<Member> optionalMember = memberRepository.findById(id);
        Role role = roleRepository.findByMemberId(id);
        if(optionalMember.isPresent()) {
            roleRepository.delete(role);
            memberRepository.delete(optionalMember.get());
        }
        return true;
    }
}

 

6. 호출

6-1. curl 호출

- query 호출

curl -X POST -H "Content-Type:application/json" --data "{\"query\" : \"query { getMember(id: 1) {id login_id}}\"}" http://localhost:8080/graphql

- mutation 호출

curl -X POST -H "Content-Type:application/json" --data "{\"query\" : \"mutation { createMember(memberParam: {loginId:\\\"testid\\\" password:\\\"pw\\\" name:\\\"name\\\"}) {id login_id}}\"}" http://localhost:8080/graphql

6-2. API 호출

postman이나 api tester와 같은 툴을 이용한 호출

 

- query 호출

[POST] http://localhost:8080/graphql
body : {"query" : "query { getMember(id: 1) {id login_id roles {id role}}}"}

- mutation 호출

[POST] http://localhost:8080/graphql
body :  {"query" : "mutation { createMember(memberParam: {loginId:\"testid\" password:\"pw\" name:\"이름\"}) {id login_id}}"}

[POST] http://localhost:8080/graphql
body :  {"query" : "mutation { deleteMember(id: 2)}"}

 


 참고 사이트