spring boot/기술 적용

Spring Boot에서 GraphQl 적용해보기

ballde 2022. 5. 31. 11:47

블로그에 쓸 기능들 정리한 깃헙입니다

https://github.com/YuSunjo/spring-boot-example

 

GitHub - YuSunjo/spring-boot-example: spring boot 간단한 예제 모음(블로그 용도) - s3이미지 업로드, nfs를 통

spring boot 간단한 예제 모음(블로그 용도) - s3이미지 업로드, nfs를 통한 이미지 업로드, FCM, Feign-Client, GraphQl - GitHub - YuSunjo/spring-boot-example: spring boot 간단한 예제 모음(블로그 용도) - s3이미지 업로

github.com

 

먼저 graphql을 사용하기에 앞서 무엇이고 왜 사용해야하는지 알아보겠습니다.

 

GraphQl이란?

GraphQL은 페이스북에서 개발한 API를 위한 쿼리 언어로 기존에 서버와 클라이언트 간 데이터 전달을 위해 많이 사용되는 Rest API의 단점들을 보완해줄 수 있는 기술입니다.

Rest Api의 단점

  • over-fetching
    • 클라이언트에서 실제로 사용되는 데이터만 불러오지 않고 사용되지 않는 데이터도 함께 불러옴으로 써 리소스의 낭비를 발생시키는 것을 의미
  • under-fetching
    • 클라이언트에서 데이터를 활용하기 위해 API를 요청할 때 EndPoint마다 Response 되는 값들이 정해져 있어서 필요한 데이터들을 모두 불러오기 위해 여러 개의 API를 요청하는 것을 의미

graphQL의 장점

# over-fetching 해결
query {
    user(id: 1) {
		# 원하는 정보만 가져온다. 
        id
        name
        age
    }
}

# under-fetching 해결
query {
    user(id: 1) {
		# 원하는 정보만 가져온다. 
        id
        name
        age
    }
    board(id: 1) {
        title,
        content
    }
}

graphQL의 단점

  • 사용되는 GraphQL을 위한 스키마를 추가 작성을 해줘야 합니다.
    • 자바로 구현되는 클래스 + 스키마 ⇒ 관리할게 많음
  • 항상 고정된 Response만 있을 경우 불필요

GraphQl 사전지식

https://nomadcoders.co/graphql-for-beginners

node js로 되어있는 강의지만 원리는 같아서 도움이 됩니다.

  • 공식문서 : https://docs.spring.io/spring-graphql/docs/1.0.0-SNAPSHOT/reference/html/
  • GraphQL을 위한 스키마를 작성할 때 Rest API에 GET, POST, PUT, DELETE가 있는 것처럼 GraphQL에는 Query(GET), Mutation(POST, PUT, DELETE)이 존재합니다.
  • resolver을 작성해야합니다.
  • spring boot에서는 어노테이션으로 Type, query, mutation을 지정할 수 있습니다.
    • 그래서 직접 스키마를 지정하는 버전, 스키마를 지정하지 않고 어노테이션으로 하는 버전 2개를 하겠습니다.
  • 밑에 예제는 jpa + querydsl로 하는데 이 설정은 따로 하지 않습니다.

dependency 추가 (gradle)

// graphql
implementation 'com.graphql-java:graphql-spring-boot-starter:5.0.2'
implementation 'com.graphql-java:graphql-java-tools:5.2.4'
// graphql gui
implementation 'io.leangen.graphql:graphql-spqr-spring-boot-starter:0.0.4'

application.yml

graphql:
  servlet:
    enabled: true
    mapping: /graphql
	# 위에 spqr 추가해주면 설정 가능
  spqr:
    gui:
      enabled: true

스키마 작성하는 예제

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User extends BaseTimeEntity {

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

    private String name;

    private String age;

user.graphqls

type User {
    id: Int!,
    name: String!,
    age: String!
}

type Query {
    getUser(id: Int!): User!
}

type Mutation {
    createUser(createUserRequest: CreateUserRequest!): User!,
}

input CreateUserRequest {
    name: String!,
    age: String!
}
  • 스키마로 인식되는 파일은 .graphqls에 해당하는 확장자 ⇒ user.graphqls, board.graphqls …
  • 컬럼명 : 타입 으로 작성
  • ! 는 null 값을 허용하는지? 체크해줍니다.
@Component
@RequiredArgsConstructor
public class UserMutation implements GraphQLMutationResolver {

    private final UserRepository userRepository;

    public UserInfoResponse createUser(CreateUserRequest request) {
        User user = userRepository.save(request.toEntity());
        return UserInfoResponse.of(user);
    }

}
@Component
@RequiredArgsConstructor
public class UserQuery implements GraphQLQueryResolver {

    private final UserRepository userRepository;

    public UserInfoResponse getUser(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new NotFoundException("존재하지 않는 유저입니다."));
        return UserInfoResponse.of(user);
    }

}
  • GraphQLQueryResolver, GraphQLMutationResolver을 통해서 resolver을 작성해줍니다.

직접 통신을 해서 결과값 받아오기 (.http 사용, gui 사용)

graphql.http (.http를 사용해서 api 통신을 할 수 있습니다.)

###
POST {{backend_api}}/graphql
Content-Type: application/graphql

mutation {
  createUser(createUserRequest: {
    name: "hello",
    age: "10"
  }
  ) {
  id
  name
  age
}
}

### 결과값
{
  "data": {
    "createUser": {
      "id": 1,
      "name": "hello",
      "age": "10"
    }
  }
}

POST {{backend_api}}/graphql
Content-Type: application/graphql

query {
    getUser(id: 1) {
        id
        name
        age
    }
}

### 결과값
{
  "data": {
    "getUser": {
      "id": 1,
      "name": "hello",
      "age": "10"
    }
  }
}

gui 사용

{{주소}}/gui에 들어가면

gui 화면이 나옵니다.

결과값

스키마 작성하지 않고 작성하는 예제

  • graphQLType으로 타입을 지정
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@GraphQLType 
public class User extends BaseTimeEntity {

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

    private String name;

    private String age;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private final List<Board> boardList = new ArrayList<>();

    public User(String name, String age) {
        this.name = name;
        this.age = age;
    }

}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@GraphQLType
public class Board {

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

    private String title;

    private String content;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    public Board(String title, String content, User user) {
        this.title = title;
        this.content = content;
        this.user = user;
    }

}
  • @GraphQlApi을 명시해주고 @GraphQLQuery, @GraphQLMutation 으로 query, mutation을 명시해줍니다.
    • CreateUserReqeust, UserInfoResponse 이런것도 따로 명시안해줘도 됩니다.
@Service
@RequiredArgsConstructor
@GraphQLApi
@Transactional
public class GraphQlBoardService {

    private final UserRepository userRepository;
    private final BoardRepository boardRepository;

    @GraphQLQuery
    public BoardInfoResponse getBoard(Long id) {
        Board board = boardRepository.findById(id)
                .orElseThrow(() -> new NotFoundException("존재하지 않는 유저입니다."));
        return BoardInfoResponse.of(board);
    }

    @GraphQLQuery
    public List<BoardInfoResponse> findAllBoard() {
        return boardRepository.findBoardAll().stream().map(BoardInfoResponse::of).collect(Collectors.toList());
//        return boardRepository.findAll().stream().map(BoardInfoResponse::of).collect(Collectors.toList());
    }

    @GraphQLMutation
    public BoardInfoResponse createBoard(CreateBoardRequest request) {
        User user = userRepository.findById(request.getUserId())
                .orElseThrow(() -> new NotFoundException("존재하지 않는 유저입니다."));
        Board board = boardRepository.save(request.toEntity(user));
        return BoardInfoResponse.of(board);
    }

}
@Service
@RequiredArgsConstructor
@GraphQLApi
@Transactional
public class GraphQLUserService {

    private final UserRepository userRepository;

    @GraphQLQuery
    public UserInfoResponse getUser(Long id) {
        User user = userRepository.findUserById(id)
                .orElseThrow(() -> new NotFoundException("존재하지 않는 유저입니다."));
        return UserInfoResponse.of(user);
    }

    @GraphQLQuery
    public List<UserInfoResponse> findAllUser() {
        return userRepository.findAllUser().stream().map(UserInfoResponse::of).collect(Collectors.toList());
    }

    @GraphQLMutation
    public UserInfoResponse createUser(CreateUserRequest request) {
        User user = userRepository.save(request.toEntity());
        return UserInfoResponse.of(user);
    }

}

gui 결과

메서드 이름은 getBoard 인데 docs는 왜 board 라고 뜨는지 아시는 분은 좀 알려주세요 ㅠㅠ

  • board 생성하기

  • user의 board들 가져오기

  • 모든 유저 불러오기