Spring for GraphQL 시작하기 (2)

semin·2024년 3월 2일
post-thumbnail

1편에 이어서 연관관계를 가지는 객체를 활용한 GraphQL 구현 방법을 알아보자.

연관관계 객체를 활용한 기능 구현

이번에는 Album 객체를 생성하여 1편에서 생성한 Music 객체와 일대다 관계를 만들어 준 뒤 이를 통해 QueryMutation 을 구현할 것이다.

Album 스키마 추가

Album 객체와 input 그리고 간단한 QueryMutation 을 추가해 주었다.

type Album {
    id: ID!
    name: String!
    releaseDate: DateTime!
    musicList: [Music!]!
}

input AlbumInput {
    name: String!
    releaseDate: DateTime!
    musicList: [MusicInput!]
}

type Query {
    album(id: ID!): Album!
}

type Mutation {
    createAlbum(albumInput: AlbumInput!) : Album
}

Album 객체의 musicList 필드를 보면 schema 에서 객체간 필드 연관관계를 어떻게 정의하는지 확인할 수 있다.

Album Entity 생성

Music 과 일대다 관계를 매핑하여 Album 엔티티를 생성해주고 적절한 cascade 옵션을 부여했다.

@Entity
public class Album {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private ZonedDateTime releaseDate;

    @OneToMany(mappedBy = "album", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Music> musicList;
}

public void addMusic(Music music) {
    musicList.add(music);
    music.setAlbum(this);
}

Music 에도 연관관계 필드를 추가해준다.

@Entity(name = "music")
public class Music {
	
    ...

    @Setter
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "album_id")
    private Album album;
    
}

DTO 구현

Music 을 구현할 때와 마찬가지로, InputResponse DTO 도 적절하게 구현해주었다.

AlbumInput.java

@Builder
public record AlbumInput(
        long id,
        String name,
        ZonedDateTime releaseDate,
        List<MusicInput> musicList
) {
    public Album toEntity() {
        Album album = Album.builder()
                .name(name)
                .releaseDate(releaseDate)
                .build();

        List<Music> musicList = this.musicList.stream().map(MusicInput::toEntity).toList();
        musicList.forEach(album::addMusic);

        return album;
    }
}

AlbumResponse.java

@Builder
public record AlbumResponse(
        long id,
        String name,
        ZonedDateTime releaseDate

) {
    public static AlbumResponse of(Album album) {
        return AlbumResponse.builder()
                .id(album.getId())
                .name(album.getName())
                .releaseDate(album.getReleaseDate())
                .build();
    }
}

스키마에서는 Album 타입에 musicList 필드를 정의했지만, AlbumResponse 에는 해당 필드를 정의하지 않았다. album 쿼리에서 musicList 필드 데이터를 불러오는 방법을 알아보자.

@SchemaMapping 활용하기

@SchemaMapping 는 스키마에 정의된 필드에 대한 핸들러 메서드를 정의하는 어노테이션이다. 이를 이용해 AlbummusicList 필드 데이터를 불러올 수 있다.

@SchemaMapping(typeName = "Album", field = "musicList")
public List<MusicResponse> getMusicList(AlbumResponse albumResponse, @Argument Long musicId) {
	return musicService.getMusicList(albumResponse, musicId);
}

어노테이션 속성으로, typeNamefield 를 지정할 수 있다. 만약 지정하지 않으면 typeName 은 인자로 전달된 클래스의 이름으로, field 는 메소드 이름에 의해 결정된다.

현재 스키마에서는 Album 으로 정의한 객체를 AlbumResponse 라는 클래스를 이용해 반환하고 있으며, 메소드 이름도 구체적으로 네이밍 하고 싶었기 때문에 두 개의 속성을 모두 따로 작성해주었다.

이제 Album 을 활용하여 MutationQuery 를 실행해보자.

Mutation 실행

다음과 같은 mutation 을 실행하여 Music 을 포함한 Album 데이터를 생성한다.

mutation

mutation createAlbum ($album: AlbumInput) {
		createAlbum(albumInput: $album) {
		  id
		}
}

# Variables
{
  "album": {
      "name": "UNFORGIVEN",
      "releaseDate": "2023-05-01T15:56:55.209386+09:00",
    	"musicList": [
      	{
          "name": "UNFORGIVEN",
          "releaseDate": "2023-05-01T15:56:55.209386+09:00",
          "genre": "DANCE"
        },
        {
          "name": "ANTIFRAGILE",
          "releaseDate": "2023-05-01T15:56:55.209386+09:00",
          "genre": "DANCE"
        }
      ]
  }
}

Query 실행

이번에는 앨범과 앨범에 수록된 음악까지 데이터를 한번에 조회하는 쿼리를 사용해보자.

쿼리

query getAlbum {
    album(id: 1) {
        id
        name
        releaseDate
        musicList {
            id
            name
            releaseDate
        }
    }
}

결과

{
    "data": {
        "album": {
            "id": "1",
            "name": "UNFORGIVEN",
            "releaseDate": "2023-05-01T15:56:55.209386+09:00",
            "musicList": [
                {
                    "id": "1",
                    "name": "UNFORGIVEN",
                    "releaseDate": "2023-05-01T15:56:55.209386+09:00"
                },
                {
                    "id": "2",
                    "name": "ANTIFRAGILE",
                    "releaseDate": "2023-05-01T15:56:55.209386+09:00"
                }
            ]
        }
    }
}

성공적으로 데이터를 저장하고, 조회하는 것에 성공하였다.

Field에 Argument 전달하기

위 기능에서는 앨범에 수록된 모든 음악 목록을 불러왔다. 서비스에서는 페이지네이션이나 필터링을 요구할 수도 있고, 단일 필드에 대해 정보 가공을 원할 수도 있다.

이러한 요구사항을 만족하기 위해 필드에 인자를 전달하는 방법을 알아보자.
우선 스키마의 musicList 필드에 인자를 추가한다.

type Album {
    id: ID!
    name: String!
    releaseDate: DateTime!
    musicList(musicId : ID): [Music!]!
}

실제로 이런 요구사항은 없겠지만, 예시를 위해 musicId 를 인자로 전달하는 상황을 가정했다.

@SchemaMapping 메서드에도 동일한 변수 명으로 인자를 추가해준다.

    @SchemaMapping(typeName = "Album", field = "musicList")
    public List<MusicResponse> musicList(AlbumResponse albumResponse, @Argument Long musicId) {
        return musicService.getMusicList(albumResponse, musicId);
    }

이제 musicList 필드에 인자를 전달했을 때, 인자가 전달되어 필터링이 되는지 확인해보자.

쿼리

query getAlbum {
    album(id: 1) {
        id
        name
        releaseDate
        musicList(musicId: 2) {
            id
            name
            releaseDate
        }
    }
}

결과

{
    "data": {
        "album": {
            "id": "1",
            "name": "UNFORGIVEN",
            "releaseDate": "2023-05-01T15:56:55.209386+09:00",
            "musicList": [
                {
                    "id": "2",
                    "name": "ANTIFRAGILE",
                    "releaseDate": "2023-05-01T15:56:55.209386+09:00"
                }
            ]
        }
    }
}

정상적으로 인자가 전달되어 필터링이 된 것을 확인할 수 있다.

객체 필드 뿐만 아니라 스칼라 타입 필드에도 인자를 전달할 수 있다.
DateTime 필드에 timezone 을 지정하는 인자를 정의하는 상황을 가정해보자.

type Music {
    id: ID!
    name: String!
    releaseDate(timezone: String): DateTime
    genre: Genre
}

예시에서는 임시 기능 구현을 위해 String 타입을 전달하였지만, 인자로 enum 이나 객체 등 다양한 타입을 전달할 수 있다.

필드의 인자도 @ShemaMapping 을 이용해 전달받을 수 있다.

@SchemaMapping(typeName = "Music", field = "releaseDate")
public ZonedDateTime changeTimezone(MusicResponse musicResponse, @Argument String timezone) {
	if(timezone != null) {
	return musicResponse.releaseDate().withZoneSameInstant(ZoneId.of(timezone));
	}
    
	return musicResponse.releaseDate();
}

필드에도 인자를 잘 전달할 수 있는지 테스트 해보자.

쿼리

{
    album(id: "1") {
        id
        name
        releaseDate
        musicList(musicId: "1") {
            id
            name
            releaseDate(timezone: "America/Los_Angeles")
            genre
        }
    }
}

응답

{
    "data": {
        "album": {
            "id": "1",
            "name": "UNFORGIVEN",
            "releaseDate": "2023-05-01T15:56:55.209386+09:00",
            "musicList": [
                {
                    "id": "1",
                    "name": "UNFORGIVEN",
                    "releaseDate": "2023-04-30T23:56:55.209386-07:00",
                    "genre": "DANCE"
                }
            ]
        }
    }
}

musicListreleaseDate 필드의 Timezone이 변경된 것을 확인할 수 있다.

마무리하며

Spring for GraphQL 을 학습하며 Query, Muation, Schema 문법 및 다양한 Type 들에 대해 알아보았다.

포스팅에서 다루지 않았지만 GraphQL에서는 이외에도 다양한 타입과 기능을 지원한다. 그러한 내용은 앞으로 기능을 고도화 해가며 필요한 내용을 공부하고, 적용할 예정이다.

다음은 마이크로서비스 아키텍처를 적용하기 위해 멀티 모듈 프로젝트로 전환하고 공통으로 사용될 모듈을 분리하는 과정을 작성해보겠다.

참고자료

GraphQL

https://graphql.org/learn/

Srping for GraphQL

https://docs.spring.io/spring-graphql/reference/controllers.html
https://stackoverflow.com/questions/73647712/how-to-handle-nested-field-with-arguments-in-spring-for-graphql-controller

profile
블로그 이전 -> https://choicco.tistory.com/

0개의 댓글