
GraphQL은 페이스북이 2015년에 오픈 소스로 공개한 API 접근 방식의 일종이다.
sql과 같은 쿼리 기반 기술이지만, sql과는 달리 웹 클라이언트가 데이터를 서버로 부터 효율적으로 가져오기 위해 만들어졌다.
클라이언트가 서버에게 어떤 데이터가 필요한지 명시함으로서 불필요한 데이터를 받지 못하는 사태를 방지할 수 있다.
쿼리는 서버에 어떤 데이터가 필요한지, 또한 그 데이터에서 어떤 부분들을 가져올지 결정한다.
쉽게 말하자면, 정해진 함수를 호출하여 얻게 될 반환값에서 필요한 부분만 빼내 가져오는 것이다.
CRUD의 'R'에 해당한다고도 할 수 있다.
구성 요소
Query Name (쿼리 명칭)
쿼리의 이름은 프로그램 함수명과 같이 어떤 쿼리인지 식별하는 역할을 한다.Variable (인자)
프로그램 매개변수와 같이 데이터를 가져오는 데 필요한 값을 명시하는 역할을 한다.
콜론(:)을 기준으로 좌측에 $를 앞에 두어 명시한 값이 '변수명'이 되고, 우측에 있는 값이 '데이터 타입'이 된다.
특정 객체를 인자로 전달할 경우, 별도의 VARIABLES 기입란에 객체 구성 요소들을 명시한다.Field (필드값)
쿼리를 통해 실제로 가져올 값들을 명시한 부분이다.
경우에 따라 Variable에 명시한 값을 필드 스키마의 인자값으로 대입할 수도 있다.
예시
query ExampleQuery($queryVariable:String){ fieldSchema(fieldVar:$queryVariable){ resultParts2 resultParts4 resultParts5 } }
쿼리가 데이터를 '가져오는' 역할을 담당했다면, 뮤테이션은 데이터를 '수정하는' 역할을 담당한다.
서버에 데이터를 새로 기입하거나 수정하는 등 서버의 데이터를 조작하는 행위에 사용된다.
CRUD의 'CUD'에 해당한다고도 할 수 있다.
구성 요소
Mutation Name (뮤테이션 명칭)
쿼리 명칭과 마찬가지로 어떤 뮤테이션인지 식별하는 역할을 한다.Variable (인자)
이 또한 쿼리와 마찬가지로 데이터 조작에 필요한 값을 명시하는 역할을 한다.Field (필드값)
뮤테이션을 통해 데이터를 수정할 동작을 명시하고, 그에 따른 반환값을 필드 스키마의 필드값으로 명시할 수도 있다.
예시
mutation ExampleMutation($mutationVariable:MutationData){ serverDataEdit(editData:$mutationVariable){ mutationResult } }예시 - VARIABLES
{ "mutationVariable":{ "identificationrNumber": 123456, "identificationName": "exampleName", "date": "2024-05-06", "identificationImageName": "example_image.jpg", "tags": [ tag1, tag2, tag3 ] } }
쿼리를 통해 데이터 요청 시 사용할 field들을 정의한 것을 type이라 한다.
콜론을 기준으로 좌측에 변수명, 우측에 자료형을 넣는다.
자료형에 !가 붙을 경우 해당 필드는 non-nullable, 즉 null을 허용하지 않게 된다.
배열도 사용 가능한데, 앞서 말한 !가 어디에 붙느냐에 따라 그 성격이 달라진다.
배열 내에 !가 있을 경우, 배열의 요소들은 모두 null을 허용하지 않는다.
배열 외에 !가 있을 경우, 배열은 null하지 않고 0개 이상의 값을 가져야 한다.
예시
type Skill{ skillID: ID! skillName: String! skillNumber: Int! skillValue: Float isMagic: Boolean user: [String]! #user 배열은 비어서는 안되는 상태 keywords: [String!] #keywords 배열 내 요소들은 null을 허용하지 않는 상태 }
생성될 객체 전체를 전달하는 데 사용된다.
type과 같이 데이터 요청 시 사용할 field들을 정의한 것이지만, query나 mutation의 인자 용도로 쓰인다.
예시
input SkillInput{ skillID: ID! skillName: String! skillNumber: Int! skillValue: Float! isMagic: Boolean! user: [String] keywords: [String]! }
build.gradle
dependencies { implementation 'org.springframework.boot:spring-boot-starter-graphql' ... }
GraphQL의 스키마를 정의하는 파일명.graphqls 파일을 /resource/graphql 폴더에 생성한다.
해당 파일에 사용할 type 및 input을 정의하고, Query와 Mutation 스키마 또한 정의한다.
exmaple.graphqls
input SkillInput{ skillID: ID! skillName: String! skillNumber: Int! skillValue: Float! isMagic: Boolean! user: [String] keywords: [String]! } input SearchOption{ skillIdList: [Int] skillNumbers: [Int] isMagic: Boolean! keywords: [String] } type Skill{ skillID: ID! skillName: String! skillNumber: Int! skillValue: Float isMagic: Boolean user: [String]! keywords: [String!] } type Query { searchSkill(searchOption: SearchOption):[Skill] } type Mutation { addSkillData(skillInput: SkillInput!): String }
graphql을 사용할 경우, 기존 REST방식의 어노테이션을 사용하지 않는다.
대신 springframework.graphql의 @MutationMapping, @QueryMapping을 사용한다.
사용할 쿼리 및 뮤테이션의 이름과 메서드의 이름이 일치해야 하며,
매개변수에 @Argument(name = 쿼리문 지정 매개변수명)를 명시하여 메서드에 받은 매개변수가 해당 쿼리문의 인자임을 명시해야 한다.
exampleController
@Controller public class AdminManagementController { @Autowired DataService dataService; @MutationMapping public String addMemberData(@Argument(name = "skillInput") SkillInputDTO skillInput) return dataService.saveSkillData(skillInput); } @QueryMapping public SkillDTO searchSkill(@Argument(name = "searchOption") SearchOptionDTO searchOption){ return dataService.getData(searchOption); } }
그런데 위와 같이 쿼리의 input을 DTO를 통해서 처리할 경우, DTO클래스에 @setter를 명시해야 한다.
GraphQL 라이브러리가 해당 필드에 값을 설정하는 방식으로 동작하기 때문이다.
일반적으로 GraphQL 라이브러리는 매개변수로 전달된 객체의 필드에 값을 설정하기 위해 Java Reflection을 사용한다.
Reflection은 클래스의 필드와 메서드에 동적으로 접근할 수 있는 기능을 제공하는데, 이를 사용하여 private 필드에 직접 접근하려면 해당 필드에 접근 권한을 부여해야 하므로, setter를 통해 해당 필드에 접근하는 것이다.
@Getter @Setter @NoArgsConstructor public class SearchOptionDTO { private List<Integer> skillIdList; private List<Integer> skillNumbers; private Boolean isMagic; private List<String> keywords; public SearchOptionDTO(List<Integer> skillIdList, List<Integer> skillNumbers, Boolean isMagic, List<String> keywords) { this.skillIdList=skillIdList; this.skillNumbers=skillNumbers; this.isMagic=isMagic; this.keywords=keywords; } }
기존 REST방식은 url이나 메서드 등을 조합하기 때문에 다양한 Endpoint가 존재하게 된다.
그러나 GraphQL은 /graphql 엔드포인트만 사용하고, 쿼리 조합을 바꿔 통신하는 방식이라 백엔드-프론트엔드 간 부담이 덜해진다.
개인적으로 이 부분이 최대 장점이라 생각한다.
요청 데이터에서 필요한 부분만 가져가다 보니, HTTP 요청 횟수와 응답 사이즈를 조절할 수 있게 되었다.
또한 REST방식은 데이터끼리 같은 변수 형식을 사용하더라도 응답에 따른 데이터 조합이 달라지면 별도의 DTO를 명시해야 했는데,
GraphQL은 쿼리를 사용하므로 운용할 DTO의 종류를 간략화할 수 있게 되었다.
당장 앞서 보여준 예시에서도 searchSkill을 통해 Skill 데이터를 가져오게 만들었는데,
만약 필요한 Skill 데이터가 Skill의 이름과 그 ID 값만이라면, REST방식에서는 skillID와 skillName으로 이루어진 별도의 DTO클래스와 컨트롤러를 작성해야 했을 수도 있었다.
그러나 GraphQL환경에서는 기존 searchSkill 쿼리 필드에 skillID와 skillName를 명시하는 것으로 값을 가져올 수 있어 전반적인 관리를 간략화할 수 있다.
부분 호출 예시
query IWannaSkillFullData($searchOption:SearchOption){ searchSkill(fieldVar:$searchOption) } query IWannaSkillIdAndName($searchOption:SearchOption){ searchSkill(fieldVar:$searchOption){ skillID skillName } }
graphql에서 기본적으로 지원하는 데이터 자료형은 ID, String, Int, Float, Boolean 5개 뿐이다.
즉 Double과 같은 실수나 MultiPartFile과 같은 파일을 전달할 수 없다.
불편하지만, 기본 제공 자료형 외의 자료형을 사용하려면 사용자가 별도로 구현해야 한다.
예를 들어 Date자료형을 사용하려면 graphql-java-extended-scalars 라이브러리를 이용하는 식이다.
MultiPartFile의 경우, AWS S3와 같은 외부 저장소를 사용한다면 '미리 서명된 URL'을 통해
서버를 거치지 않고 직접 전달하는 방식으로 저장소에 전달하는 방법도 있다.
예시 - java class
@Configuration public class GraphQLScalarConfig { @Bean public RuntimeWiringConfigurer runtimeWiringConfigurer() { return wiringBuilder -> wiringBuilder.scalar(ExtendedScalars.Date); } }예시 - graphqls
scalar Date
https://www.graphql-java.com/tutorials/getting-started-with-spring-boot
https://www.bangseongbeom.com/graphql-downsides-alternatives.html
https://velog.io/@jiwon0813/%EC%8A%A4%ED%94%84%EB%A7%81-graphql-DTO%EC%97%90-%EA%B0%92%EC%9D%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%93%A4%EC%96%B4%EC%98%A4%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C-%ED%95%B4%EA%B2%B0%EB%B2%95