Spring Data Neo4j

Spring Data Neo4j
spring data neo4jgraph db
avatar
2025.06.08
·
7 min read

본 프로젝트에 사용하기 전에 로컬로 테스트를 해보면서 모은 정보입니다.

그리고 Spring Data Neo4j 7.5.0 공식 문서를 참고하였습니다

Neo4j with Docker

docker run -p 7474:7474 -p 7687:7687 --name springboot-neo4j -d neo4j

7474포트는 HTTP API, 웹 UI 용 (http://localhost:7474/browser/)

7687포트는 Bolt프로토콜, yml 파일 설정용 이다.

Bolt는 Neo4j의 이진 프로토콜이다.

Dependency

implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'

application.yml

spring:
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: 1q2w3e4r

초기 비밀번호를 지정하려면 웹 UI(http://localhost:7474/browser/)로 접속한뒤

원하는 username : neo4j / password : neo4j 입력하면 새로운 비밀번호를 입력하게 한다.

최소 8글자를 채워야하니 국룰 비밀번호 1q2w3e4r로 해준다.

Neo4jConfig

@Configuration
public class Neo4jConfig {

    @Bean
    org.neo4j.cypherdsl.core.renderer.Configuration cypherDslConfiguration(){
        return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig()
                .withDialect(Dialect.NEO4J_5_26).build();
    }
}

JPA에 Query-DSL이 있다면 Neo4j에는 Cypher-DSL 이 존재한다.

그리고 Neo4j의 버전에 맞춰서 Cypher-DSL의 방언을 지정한다.

공식 문서에서는 이를 권장한다고 했으나

2025-06-06 기준으로 Neo4j Driver 28 버전을 사용하고 있지만 Neo4j_5_26이 최대이고 버전이 다르다고 별다른 오류가 발생하지 않고 있어 이 부분은 참고만 한다.

Object Mapping

@Node(value = "User")
@Getter
public class User {

    @Id
    @GeneratedValue(UUIDStringGenerator.class)
    private final String id = null;

    @Property(name = "email_name")
    private final String email;

    @Property(name = "provider_name")
    private final String provider;

    @Relationship(type = "REVIEWED", direction = Relationship.Direction.OUTGOING)
    private List<Reviewed> reviewed = new ArrayList<>();

    @Relationship(type = "SUBSCRIBED", direction = Relationship.Direction.OUTGOING)
    private List<Subscribed> subscribed = new ArrayList<>();

    public User(final String email, final String provider) {
        this.email = email;
        this.provider = provider;
    }
}

Neo4j에서 도메인을 생성하는 방식이 크게 두가지가 있지만 우선 어노테이션을 위주로 설명한다.

@Node (≒ @Entity)

@Node(value = "User")
@Getter
public class User {

@Node를 사용해 도메인 설계를 시작한다.

@Entity때와 동일하게 value 지정해주지 않으면 클래스 명으로 Node가 생성된다.

@Id & @GeneratedValue

@Id
@GeneratedValue(UUIDStringGenerator.class)
private final String id = null;

@Id 는 완벽히 동일하다.

@GeneratedValue가 그나마 특이한데 UUIDStringGenerator.class 는 Neo4j 에서 제공된다.

Long 기반 id 생성이 deprecated 될 예정이기 때문에 UUID 기반 String id를 사용해야 하고

UUID가 싫다면 별도 커스텀 ID 생성 유틸을 만들어서 사용해야 한다.

@Property (≒ @Column)

@Property(name = "email_name")
private final String email;

@Property(name = "provider_name")
private final String provider;

DB에 저장될 때의 이름을 지정할 수 있다.

속성에 namevalue가 존재하는 데 서로 AliasFor 관계라서 둘 중 한 개만 사용하면 된다.

@Relationship (≒ @OneToMany, @ManyToOne ...)

@Relationship(type = "REVIEWED", direction = Relationship.Direction.OUTGOING)
private List<Reviewed> reviewed = new ArrayList<>();

@Relationship(type = "SUBSCRIBED", direction = Relationship.Direction.OUTGOING)
private List<Subscribed> subscribed = new ArrayList<>();

한글로 표현한다면

사용자가(User) -> 리뷰했다(REVIEWED) -> 요금제를(Plan)

사용자 기준으로 화살표가 밖으로 향하고 있기때문에 OUTGOING으로 표현한다.

그럼 여기서 요금제를 리뷰한건데 List<Reviewed> 라고 되어있는 것이 이상하게 느껴진다.

그럼 곧바로 Reviewed를 본다.

@RelationshipProperties
@Getter
public class Reviewed {

    @RelationshipId
    private final Long id = null;

    private final int point;

    @TargetNode
    private final Plan plan;

    public Reviewed(final int point, final Plan plan) {
        this.point = point;
        this.plan = plan;
    }
}

@RelationshipProperties

@RelationshipProperties
@Getter
public class Reviewed {

그래프 DB는 Node과 Relationship으로 이루어져 있다.

각 노드사이를 이어주는 간선을 정의하는 것이 @RelationshipProperties 이다.

@RelationshipId

@RelationshipId
private final Long id = null;

사실 @RelationshipId를 까고보면 @Id 와 @GeneratedValue 로 이루어져 있다.

현재는 간선을 정의하는데는 Long 타입의 id만을 허용하고 있다.

@TargetNode

@TargetNode
private final Plan plan;

여기서 사용자가(User) -> 리뷰했다(REVIEWED) -> 요금제를(Plan) 의 요금제가 등장한다.

결국 @RelationshipProperties은 간선에 리뷰의 별점 이라는 정보를 담기위해 사용한거기 때문에 간선에 정보를 담을 필요가 없고 단순히 방향만 지정하고 싶다면 다음과 같이 지정한다.

@Relationship(type = "REVIEWED", direction = Relationship.Direction.OUTGOING)
private List<Plan> plans = new ArrayList<>();

Immutable Objects

보면서 의아한 점이 있을 수 있는데, final을 심심찮게 볼 수 있다.

공식문서에서 추천하는 사항이 몇가지가 있다.

  1. Try to stick to immutable objects 가능한 불변 객체 만들기

  2. Provide an all-args constructor 모든 인자가 포함된 생성자 제공하기

  3. Use factory methods instead of overloaded constructors to avoid @PersistenceCreator

    여러 생성자를 만들지 말고 정적 팩토리 메서드 사용하기

  4. Make sure you adhere to the constraints that allow the generated instantiator and property accessor classes to be used

    제안 사항을 충족하여 성능 향상

  5. For identifiers to be generated, still use a final field in combination with a wither method

    불변 객체 생성을 위해 wither 메서드 사용

  6. Use Lombok to avoid boilerplate code

    롬복 사용

따라서 최종 형태는 다음과 같다. 프로젝트를 진행하면서 추가되거나 변경되는 사항이 있다면 아래에 지속적으로 업데이트 할 예정이다.

@Node(value = "User")
@Getter
@With
@Builder(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(UUIDStringGenerator.class)
    private final String id;

    @Property(name = "name")
    private final String name;

    @Property(name = "email")
    private final String email;

    @Property(name = "provider")
    private final String provider;

    @Builder.Default
    @Relationship(type = "REVIEWED", direction = Relationship.Direction.OUTGOING)
    private List<Reviewed> reviewedHistory = new ArrayList<>();

    @Builder.Default
    @Relationship(type = "SUBSCRIBED", direction = Relationship.Direction.OUTGOING)
    private List<Subscribed> subscribedHistory = new ArrayList<>();

    public static User of(final String name, final String email, final String provider){
        return User.builder()
                .name(name)
                .email(email)
                .provider(provider)
                .build();
    }