NOSQL/Neo4j

SpringBoot+Neo4j [2. 두 개의 노드 엔티티와 두 개의 관계를 가지는 GraphDB프로젝트]

alska95 2021. 11. 12. 18:03

목차

    SpringBoot+Neo4j A-Z [1. 하나의 노드 엔티티만을 가지는 GraphDB프로젝트]를 따라 하는 대에 무리가 없었다면, RelationShip을 가지는 엔티티의 구현 또한 식은 죽 먹기다.

     

    본 프로젝트에서는 공식 예제에서는 다루지 않는 RelationShip을 갖는 Node에 대해 다룬다.

    본 프로젝트까지 진행한다면 SpringDataNeo4J를 이용한 Node와 Relation의 CRUD를 모두 이해할 수 있을 것이다.

    https://github.com/alska95/Spring_Neo4j_Project 이 코드를 반드시 참고하며 읽어주길 바란다.

     

    이전 프로젝트에서 설명이 부족했던 RelationShip에 대해 먼저 설명하겠다.


    RelationShip 이란?

    RelationShip이란, 말 그대로 이전에 살펴보았던 노드와 노드 사이의 관계를 정의하는 수단이다. 쉽게 생각해서 RDBMS의 외래 키와 비슷한 개념이라고 생각하면 된다. 노드에 관계를 추가함으로써 그래프의 넓은 섹션에서 복잡한 쿼리를 실행하는 데 필요한 데이터에 제공합니다. 노드와 노드 사이를 특정 관계에 대하여 넘나들며 자유롭게 연산을 수행할 수 있는 것이다. RDBMS의 경우 이 관계 하나당 하나의 조인이 일어나 큰 비용이 발생할 탠데, GraphDB를 사용하면 join이 아닌 관계를 통한 연결을 통해 비용을 획기적으로 줄일 수 있다. RelationShip은 방향성(INCOMING, OUTGOING)을 가져야만 한다. 무방향 RelationShip을 가정하고 사용하려면 조회시에 무방향 조회를 하면 된다.  방향성은 한 노드와 다른 노드의 관계의 주체를 정하는데에 사용된다.

    ex)

     Match(p:Person) - [:ACTED_IN] -> (m:Movie) return p

    다음과 코드는 영화"에서"(INCOMING) 출연한 사람을 찾아 와라 라는 구문으로 관계가 어떻게 사용되는지 엿볼 수 있다.

     


    기본 프로젝트 개요

    해당 모델에 대하여 CRUD를 수행할 것이다.

    RDBMS로 표현하자면 여러 방법이 있겠지만, 다음과 같이 표현될 수 있을 것 같다.

     

    이론상 다대다 관계를 가지기 때문에 다음과 같이 표현해 봤다.


    0. 프로젝트 구조

     

     

    프로젝트가 확장됨에 따라, 다음과 같이 패키지를 분리하였다.

    추상화도 이루어지지 않았고 다소 난잡해 보이지만 어디까지나 연습 프로젝트기 때문에 이대로 진행하겠다.

     


    1. Person, Movie 노드 엔티티 클래스 정의

     

    관계를 갖기 위해서는 당연히 서로 관계를 가질 노드가 두 개 이상 필요하다.

    때문에 이전 프로젝트에서 만들어 봤던 Movie에 추가로, Person노드와 Role 관계를 추가해 보겠다.

     

    아래는 Person Entity이다. 특별히 살펴볼 부분은 없다. 다만, 나중에 조회 쿼리를 이용할 때 NoArgsConstructor을 이용해서 가져오니 이 부분은 작성해 두자.

     

    아래는 Movie엔티티이다.

    이전 프로젝트에서는 RelationShip에 대한 설명을 생략했었는데 RelationShip을 엔티티 안에 @RelationShip 어노테이션을 적어주어 RelationShip으로 등록할 수 있다.

    여기서 type과 direction을 정해주어야 한다.

    typeRelationShip의 라벨이 된다. 관계를 정의한다고 생각하면 편할것 같다.

    direction은 앞서 언급한대로 INCOMING 혹은 OUTGOING 아직 미정이거나 불확실하다면 type을 기입하지 않아도 된다.

    type을 기입하지 않으면 OUTGOING으로 설정된다.

    이렇게 설정해 주면, 해당 RelationShip을 통해 검색 등 기타 연산이 가능하게 된다.

    RelationShip의 Properties를 설정하는 법은 바로 다음에서 다룬다.




    2. RelationShip 클래스 정의

     

    그런데 살펴보면 이전 프로젝트에서는 보지 못했던 Role이 RelationShip어노테이션의 타입으로 적혀있는 것을 볼 수 있다.

     

    Role 클래스는 다음과 같다.

    Relationship에 Person을 직접 등록할 수도 있지만, Role 클래스처럼 relationship클래스를 분리하여 @RelationshipProperties를 선언하고 사용하면 좋다. 이렇게 사용하면 관계에 property까지 정의할 수 있기 때문이다.


    3. Person Repository 생성

     

    Person 노드에 대한 간단한 crud를 하기 위해 Neo4JRepository를 상속받는 인터페이스를 만든다.


    4. Movie, Person, RelationShip 생성 테스트

     

    노드 엔티티를 생성하는 메서드는 이전 프로젝트와 Neo4JRepository로 구현을 완료했다.

    만들어둔 메서드를 이용하여 아래와 같이 테스트 환경에서 노드 엔티티 생성을 시험해 보자.

    (Junit5 이용)

    모든 테스트 진행 전에, 노드 3개와 관계 2개를 db에 세팅해주는 메서드로 @BeforeEach로 어노테이션을 달아준다.

    코드와 주석을 참고하며 생성과정을 알아볼 수 있다.

     

    1. 관계를 맺을 노드 생성(혹은 이미 있는 노드 조회)

    2. 노드 두 개에 대한 관계를 생성하고 리스트에 집어넣어준다.

       (위에서는 새로운 리스트를 만들어서 집어넣었지만,

       이후에 관계를 추가할 때는 기존 리스트를 조회해서 해당 리스트에 추가해줄 것.)

    3. 관계를 가질 노드를 저장하거나 기존 노드를 불러와 update 해준다.

     

    Neo4J DB에 정상적으로 노드가 입력되었는지도 확인해 보자.

    정상적으로 노드가 입력되었음을 확인할 수 있다.




    5. 다양한 조회 쿼리 생성 (+projection)

     

    위에서 생성했던 노드와 Relationship을 가지고 테스트를 진행할 예정이다.

    테스트를 진행하기 전에 테스트에 필요한 여러 조회 쿼리들을 작성해 보자.

    A부터 F까지 6가지 방법을 나열해 봤다.

     

    A. 쿼리를 사용한 제목으로 Read 하는 메서드

    match (m:Movie) where m.title contains 'metri' return m

     

     

    B.  정규식을 사용한 메서드

    MATCH (m:Movie) where m.title =~'.*metrix.*'  RETURN m 

    Regex를 지원할 것 같아서 찾아본 결과 역시 지원하는 것 같다.

    Regex를 사용한 위 쿼리를 이용해서 또 다른 조회 메서드를 만든다.

    Regex 조회 메서드 예시는 찾을 수가 없어서 임의로 그냥 +로 쿼리를 붙여서 만들어 봤는데, 정상적으로 작동했다. 아래 테스트에서 결과를 확인해볼 것이다.

     

    C. Method명 기반 조회

    구현체를 자동으로 생성해주는 메서드까지 시험해 보겠다.

     

    D. 관계가 조건으로 붙는 조회

    match (p:Person) -[:ACTED_IN]-> (m:Movie)  return p

    어떤 Movie에서든 ACTED_IN의 관계를 갖는 모든 사람들을 가져온다.

     

     

    E. 특정 엔티티 대상관계가 조건으로 붙는 조회

    match (p:Person) -[:ACTED_IN]-> (m:Movie) where m.title contains 'met' return p

    영화 제목에 ‘met’을 포함하는 영화에서 연기를 했던 모든 사람을 가져온다.

     

     

    F. 프로젝션 사용

    조금 애먹었던 부분이다. 

    위 쿼리를 실행한 결과 계속 아래 예외가 발생했다.

    Neo4jPersistenceExceptionTranslator : 
    Don't know how to translate exception of type class 
    org.springframework.data.neo4j.core.mapping.NoRootNodeMappingException

     

    방법을 찾지 못하던 도중, 공식 문서에 부모 노드를 정해주고 그 후에 프로젝션 될 대상을 적어둔 예시를 보고 그대로 고쳐본 결과 정상적으로 작동했다. (return 뒤에 movie만 더해줌)

    위와 같이 리턴 값을 movie.tagline과 person.name으로 가져오는 임의의 리턴 값으로 가지고 싶다면, Dto 클래스를 정의해서 프로젝션 해서 가져올 수 있다. 우선 부모 노드를 리턴하고, Dto의 필드 이름과 리턴 값의 이름을 as를 사용해서 맞추어 리턴해 주자. 

     

    MovieTitleDirectorDto는 다음과 같다.

     

    아래는 Projection에 관한 공식 문서 링크이다.

    https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#projections

     


    G. 동적 쿼리

    동적 쿼리에 대해서도 QueryDSL과 같이 편리한 기능을 제공한다.

    아래는 QueryDSL과 상당히 유사한 CypherDSL에 대한 공식 문서이다.

    추후에 다시 한번 다루도록 하겠다.

    https://neo4j-contrib.github.io/cypher-dsl/current/

     

     

     

     

    매우 간단한 프로젝트이기 때문에 이 정도의 조회 쿼리만 익히면 무리 없이 원하는 결과를 모두 가져올 수 있을 것이다.





    6. Relationship을 가지는 노드 엔티티 검색 (Junit5 테스트)

     

    이제 노드들과 RelationShip 그리고 조회 메서드들이 모두 준비가 되었으니, 테스트 환경에서 실행을 해보겠다.

    아래 코드를 작성해 테스트 환경에서 DB커넥션을 얻어 오자.

     

    DB커넥션 코드와, 5번에서 작성했던 노드 생성 코드를 모두 작성했다면, 이제 조회 쿼리를 사용해 보며 결과를 확인하면 된다.

     

    테스트를 시작하기 전에 Movie엔티티에 잘못된 코드가 있어 짚고 넘어가겠다.

    Movie 엔티티를 작성할 때, 하나의 노드를 사용하는 공식 예제를 보고 무작정 필드에 final선언을 하고 사용했었다.

    그런데 에러를 보니,

    Don't know how to translate exception of type class org.springframework.data.mapping.MappingException

    해당 예외가 발생하고 있었다.

    원인을 살펴보았다.

    relationship을 포함한 노드를 조회하여 가져올 때는 noArgConstructor을 기반으로 객체를 생성하고 setter을 사용하여 field를 채우는 모양이다.

    때문에 Movie 엔티티의 final field를 private으로 수정해주고 NoArgConstructor을 추가해 주었다.

    다시 한번 실행해 보자.

    테스트는 통과했고,

    결과 값이 정상적으로 출력됨을 확인할 수 있다. 그냥 수정하고 에러에 관한 내용은 뺄까 생각했으나, 조회 방식을 익힐 수 있는 기회인 것 같아서 다루어 보았다.

     

     

     

    이제 본격적으로 테스트를 진행해 보겠다.

    위에서 다뤘던 노드와 관계들의 조회 쿼리를 날려보는 테스트들이기 때문에 코드를 따로 다루지는 않겠다.

    하지만 직접 해봐야 크게 도움이 될 것이다.

    아래는 테스트 코드 링크이다. 여기까지 진행했다면 직접 해보길 권장한다.

    https://github.com/alska95/Spring_Neo4j_Project/blob/master/src/test/java/com/example/neo4j/movie/service/CRUDTest.java




    7. Relationship을 가지는 노드 엔티티 삭제

     

    우선 삭제부터 살펴보겠다.

    이전 프로젝트에서 처럼

    match (p:Person{name:'Hwang'}) delete p

    그냥 이렇게 삭제를 시도하면 어떻게 될까??

    다음과 같은 에러가 발생한다. relationship이 설정되어 있는데 해당 엔티티가 없어지면 성립되지 않는 모양이 나오기 때문이다.

     

    때문에 엔티티를 제거할 때는 detach를 붙여 relation까지 제거를 해 주어야 한다.

    match (p:Person{name:'Hwang'}) detach delete p

    를 수행하면

    정상적으로 삭제가 되는 것을 확인할 수 있다.

    삭제 기능은 위 쿼리같이 직접 작성해도 되고, neo4jRepository에 이미 구현된 기능을 사용해도 좋다.

     

     


    8. Relationship을 가지는 노드 엔티티 업데이트

     

    노드 엔티티 업데이트는 Neo4jRepository가 제공하는 save를 이용해서, 중복된 pk값을 가지는 객체를 던져주면 수정된 값을 반영하여 merge 시켜준다.

     

    다른 방법으로는 아래와 같이 직접 쿼리를 작성하는 방법도 있겠다.



    그리고 JPA의 영속성 컨텍스트처럼 엔티티를 관리하지 않을까 하여, 변경 감지(Dirty Check) 방법으로 업데이트를 시도하여 보았으나 해당 방법으로는 업데이트가 불가능한 것 같았다.

     

    REST-API를 이용해서 노드를 업데이트하는 예제도 참고해 보자.

     

    2021.11.12 - [NOSQL/Neo4j] - SpringBoot+Neo4j A-Z [ REST-API 적용 ]

     

    SpringBoot+Neo4j A-Z [ REST-API 적용 ]

    REST-API를 이용하여 노드 업데이트를 진행해보자. 코드 참고 : https://github.com/alska95/NOSQL_Neo4j_Practice 우선 기존에 RestController을 사용하던것과 똑같이 컨트롤러와 서비스를 만들어 보자. 기본적인..

    rhsalska55.tistory.com