Programing/GraphQL

세기무민의 코딩일기 : Restful API 형태로 GraphQL 사용하기

세기루민 2023. 8. 6. 11:50
728x90

Restful API 형태로 GraphQL을 사용하게 된 개요

이번 포스팅의 내용은 graphqls 파일을 단일이 아닌 N개로 처리할 수 있는 방법에 대해 고민하다가 만들게 되었습니다. 

기존에는 graphql-kickstart를 사용해서 1개의 schema 파일을 이용하였는데..

여기서 N개의 스키마 파일을 재대로 읽어오지 못하는 현상을 발견하게 됩니다. 

처음에는 application.yml에서 설정해야 하는가 싶었는데 스키마 파일을 내부적으로 구분하지 못해서

이에 따라 graphqls 방식과 restfull을 혼합하여 직접 스키마 파일을 load할 수 있도록 처리했습니다.


이전 포스팅

 

세무민의 코딩일기 : Spring Boot + GraphQL ScalarType 사용하기

안녕하세요. 세기무민입니다. 이번 포스팅의 프로젝트 기반은 아래의 포스팅의 연장선이라고 보시면 될 것 같습니다. 세무민의 코딩일기 : Spring Boot + GraphQL 연결하기 안녕하세요 세기무민입니

sg-moomin.tistory.com

이전 포스팅도 graphql 관련된 내용이라서 한번 참고해보시면 좋을 것 같습니다.


코드 및 설계

디렉토리 구조

위의 그림과 같은 디렉토리 구조를 가집니다. 

컨트롤러는 분리하셔도 무관하지만 graphqls 파일의 장점 중 하나인 단일 url을 고려하면 한곳에서 관리해도 무관합니다.


코드

1. 설정 파일

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-graphql'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	annotationProcessor 'org.projectlombok:lombok'

	// GraphQL
	implementation 'com.graphql-java:graphql-spring-boot-starter:5.0.2'
	implementation 'com.graphql-java:graphql-java-tools:5.2.4'
	implementation 'com.graphql-java:graphql-java-extended-scalars:19.0'
	implementation 'org.springframework.boot:spring-boot-starter-graphql:2.7.0'
}

해당 프로젝트에서는 DB연결은 하지 않았기 때문에 DB dependencies는 없습니다.

(필요할 경우 추가하면 됩니다.)

 

appilcation.yml

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

appilcation.yml 파일은 기본적인 graphqls 셋팅과 동일하게 처리하였습니다.

 

2. 공통 코드(common)

2. 1 Controller

GraphqlApiController.java

@RestController
@RequiredArgsConstructor
public class GraphqlApiController {
    /**
     * 모든 로직의 실행 단계는 아래와 같다.
     * 대상
     * contract, cust
     *
     * 처리 순서
     * 1. Mapping Url에 따라 처리 -> PostMapping
     * 2. Mapping Url에 따른 .graphqls 스키마 파일을 load 한다. -> graphqlProvider.loadSchema
     * 3. GraphQL을 실행시킨 결과 값을 return 한다
     * **/
    private GraphQL graphQL;
    private GraphqlProvider graphqlProvider;
    private GraphqlExecute graphqlExecute;
 
    @Autowired
    public GraphqlApiController(GraphqlProvider graphqlProvider, GraphqlExecute graphqlExecute) throws IOException {
        this.graphqlProvider = graphqlProvider;
        this.graphqlExecute = graphqlExecute;
    }
 
    /**
     * contract.graphqls
     *
     * */
    @PostMapping("/contract")
    // @PostMapping("/contract")
    public ResponseEntity<Object> getContractQuery(@RequestBody String query) throws Exception {
        graphQL = graphqlProvider.loadSchema(GraphqlSchemaCode.CONTRACT_GRAPHQL.getValue());
        return ResponseEntity.ok(graphqlExecute.GraphQlExecute(graphQL, query).getData());
    }
 
    /**
     * cust.graphqls
     *
     * */
    @PostMapping("/cust")
    public ResponseEntity<Object> getCustQuery(@RequestBody String query) throws Exception {
        graphQL = graphqlProvider.loadSchema(GraphqlSchemaCode.CUST_GRAPHQL.getValue());
        return ResponseEntity.ok(graphqlExecute.GraphQlExecute(graphQL, query).getData());
    }
}

restful post 방식으로 query를 전달하면 해당 url과 일치하는 graphqsl 파일을 읽어드린 후 파일의 스키마 값을 처리하는 형태입니다.

 

2. 1 Code

GraphqlErrorCode.java

 

public enum GraphqlErrorCode{
    NullExceptionMsg("실행값이 없습니다."),
    SchemaBuildingExceptionMsg("일치하는 스키마 파일이 없습니다.");

    private String msg;

	public String getMsg(){
		return this.msg;
	}
	
	private GraphqlErrorCode(String msg) {
		this.msg = msg;
	}	
}

Exception 오류 처리를 위한 코드값입니다.

 

GraphqlSchemaCode.java

public enum GraphqlSchemaCode {  
    CUST_GRAPHQL("G1", "cust", "cust.graphqls"),
    CONTRACT_GRAPHQL("G2", "contract", "contract.graphqls");
    
    private String code;
    private String value;
    private String url;
    
    GraphqlSchemaCode(String code, String value, String url) {
        this.code = code;
        this.value = value;
        this.url = url;
    }
    
    public String getValue(){
        return this.value;
    }
    
    public String getUrl(){
        return this.url;
    } 
    
    public Resource getResource() {
        String staticUrl = "graphql/";
        return new ClassPathResource(staticUrl + this.url);
    }
    
}

저는 graphqls 파일을 추후에 공통으로 사용할 수 있을 것 같아서 코드값으로 사용하였습니다.

해당 코드에서는 graphqls 리소스 파일을 읽어오는 처리도 포함합니다.

 

2. 3 Exception

GraphqlExceuteException.java

@Component
@RequiredArgsConstructor
public class GraphqlExceuteException {
    /*
    * ExecutionResult 결과에 따른 Exception 처리
    * */
    public ExecutionResult getErrorMsg(ExecutionResult executionResult) throws Exception {
        try{
            if(executionResult != null){
                if(!executionResult.getErrors().isEmpty()) {
                    String errorMsg = executionResult.getErrors().toString();
                    throw new Exception(errorMsg);
                } else {
                    return executionResult;
                }
            } else {
                throw new Exception(GraphqlErrorCode.NullExceptionMsg.getMsg());
            }
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
     }
}

ExecutionResult 결과에 따라 Null값에 대해 Exception 처리합니다.

2. 4 Execute

GraphqlExecuteSchema.java

@Component
public class GraphqlExecuteSchema {
    private final ContractSchemaLoad contractSchema;
    private final CustSchemaLoad custSchemaLoad;
 
    public GraphqlExecuteSchema(ContractSchemaLoad contractSchema, CustSchemaLoad custSchemaLoad){
        this.contractSchema = contractSchema;
        this.custSchemaLoad = custSchemaLoad;
    }
 
    /*
    * 스키마 파일(graphqls)을 load하는 로직
    * */
    public GraphQL loadGraphqlSchema(Resource resource, String mappingUrlValue) throws IOException{
        System.out.println("resource" + resource);
        System.out.println("mappingUrlValue" + mappingUrlValue);
        
        File schemaFile = resource.getFile();
        TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(schemaFile);
        RuntimeWiring buildRuntimeWiring = this.getRuntimeWiring(mappingUrlValue);
        if(buildRuntimeWiring == null){
            throw new IOException(GraphqlErrorCode.SchemaBuildingExceptionMsg.getMsg());
        }
        GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, buildRuntimeWiring);
        return GraphQL.newGraphQL(schema).build();
    }
 
    /*
     * graphqls 파일 내 설정된 스키마들에 대해 datafetcher와 연결
     * */
    public RuntimeWiring getRuntimeWiring(String mappingUrlValue){
        if(mappingUrlValue == GraphqlSchemaCode.CUST_GRAPHQL.getValue()){
            return custSchemaLoad.buildContractSchema();
        } else if (mappingUrlValue == GraphqlSchemaCode.CONTRACT_GRAPHQL.getValue()) {
            return contractSchema.buildContractSchema();
        } else {
            return null;
        }
    }
}

스키마 파일을 Datafetcher와 연결하며 스키마 파일을 load하는 역할을 수행합니다.

 

GraphqlExecute.java

@Component
public class GraphqlExecute extends GraphqlExceuteException{
    /*
    * 전달 받은 schema(query)에 대해 실행시킨다.
    * */
    public ExecutionResult GraphQlExecute(GraphQL graphQL, String query) throws Exception {
        ExecutionResult executionResult = graphQL.execute(query);
        return getErrorMsg(executionResult);
    }
}

load가 완료된 스키마 파일에 대해 실행 처리를 합니다. 

실행이 완료되면 내가 호출 시 요청했던 quary에 대해 결과 값을 return 해줍니다.

 

3. 도메인 코드(contract/cust)

  • 코드 구조는 동일함으로 cust에 대해서만 정리합니다.
  • DB 연결을 하지 않았음으로 repository는 없습니다.

 

3. 1 datafetcher

  • 스키마 파일에 선언한 값에 대한 처리이며 현재는 테스트를 위해 임의의 값으로 선언하였습니다.

CustDataFetcher.java 

@Component
public class CustDataFetcher implements DataFetcher<Cust> {
    @Override
    public Cust get(DataFetchingEnvironment dataFetchingEnvironment) {
        Long custSeq = dataFetchingEnvironment.getArgument("custSeq");
        return new Cust(1L, 1L, "사용");
    }
}

CustListDataFetcher.java 

@Component
public class CustListDataFetcher implements DataFetcher<List<Cust>> {

    @Override
    public List<Cust> get(DataFetchingEnvironment dataFetchingEnvironment) {
        List<Cust> custList = new ArrayList<>();

        custList.add(Cust.builder()
                .custSeq(1L)
                .buyQty(1L)
                .useDist("사용").build());

        custList.add(Cust.builder()
                .custSeq(2L)
                .buyQty(2L)
                .useDist("미사용").build());

        custList.add(Cust.builder()
                .custSeq(3L)
                .buyQty(3L)
                .useDist("사용").build());

        return custList;
    }
}

 

3. 2 entity

CustListDataFetcher.java 

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

    // @Id
    private Long custSeq;
    private Long buyQty;
    private String useDist;
    public class builder {
    }

}

원래는 DB와 JPA를 사용하던 코드여서 entity 디렉토리에 생성하였고 현재 사용 용도는 DTO로 볼 수 있습니다.

 

3. 3 schemaload

CustSchemaLoad.java 

@Component
@RequiredArgsConstructor
public class CustSchemaLoad {

    private final CustDataFetcher custDataFetcher;
    private final CustListDataFetcher custListDataFetcher;


    public RuntimeWiring buildContractSchema(){
        return RuntimeWiring.newRuntimeWiring()
                .scalar(ExtendedScalars.GraphQLLong)
                .type("Query", typeWiring -> typeWiring
                        .dataFetcher("cust", custDataFetcher)
                        .dataFetcher("custList", custListDataFetcher)
                )                        
                .build();
    }
}

사실 핵심 코드라고 봐도 무관합니다.

스키마에 선언한 type(qurey/mutation 등)에 대해 Datafetcher를 연결해주는 작업을 합니다. 


결과

contract/cust 둘다 정상적으로 호출되는 것을 확인할 수 있습니다. 

이를 통해 front에서 restful 방식으로 넘겨줄 때 schema 형태로 넘겨준다면

backend에서 graphql을 사용하여 조금 더 효율적으로 처리가 가능합니다.

 

GitHub

https://github.com/sg-moomin/graphQl_backend_poc_study/tree/main/5.%20graphql_execute_backend_project/sgmoomin

위의 링크에 더 자세한 코드를 확인할 수 있습니다.

 

728x90