Hotsse Developer

GraphDB Cypher 쿼리

그래프 데이터베이스(Graph DB) Cypher 쿼리

1. Cypher 쿼리 예시

Cypher는 Neo4j에서 사용하는 선언형 쿼리 언어로, 그래프 구조를 텍스트로 직관적으로 표현할 수 있도록 설계되었습니다.

아래는 Cypher 쿼리의 기본적인 사용 예시입니다.


🔹 노드 생성

CREATE (a:Person {name: 'Alice'})
CREATE (b:Person {name: 'Bob'})
  • Person이라는 레이블을 가진 두 개의 노드를 생성
  • 각각의 노드에는 name 속성이 부여됨

🔹 관계 생성

CREATE (a)-[:FRIENDS_WITH]->(b)
  • a 노드와 b 노드를 FRIENDS_WITH라는 관계로 연결
  • 관계는 방향성을 가짐 (단방향)

관계에도 속성을 부여할 수 있습니다:

CREATE (a)-[:FRIENDS_WITH {since: 2020}]->(b)

🔹 관계 탐색

MATCH (p:Person {name: 'Alice'})-[:FRIENDS_WITH]->(friend)
RETURN friend.name
  • 이름이 “Alice”인 노드를 찾고, 해당 노드로부터 이어진 FRIENDS_WITH 관계를 따라 연결된 friend 노드를 탐색
  • 친구의 이름을 반환

🔹 노드/관계 업데이트

MATCH (p:Person {name: 'Alice'})
SET p.age = 30
  • “Alice” 노드에 age 속성을 추가 또는 업데이트

🔹 노드 삭제

MATCH (p:Person {name: 'Alice'})
DETACH DELETE p
  • 노드와 그에 연결된 모든 관계를 함께 삭제 (DETACH DELETE 사용)

🔹 전체 그래프 조회 (예시용)

MATCH (n)-[r]->(m)
RETURN n, r, m
  • 모든 노드와 관계를 조회하여 시각화할 때 사용

Cypher는 관계 중심의 질의를 매우 직관적으로 표현할 수 있는 강력한 언어입니다.
SQL과 달리 관계를 표현할 때 JOIN이 필요 없으며, 연결 구조를 자연스럽게 탐색하는 데 최적화되어 있습니다.

💡 Cypher에서 ()-[]->() 구조는 항상
(출발 노드)-[관계]->(도착 노드)를 의미합니다.


2. Neo4j 실습 환경 소개

2.1 Neo4j Desktop

  • 로컬 PC에 설치 가능한 GUI 기반 개발 툴
  • 그래프 생성, 시각화, 쿼리 실행 등 통합 환경 제공
  • 개발 및 테스트용으로 적합

2.2 Neo4j Aura

  • Neo4j에서 제공하는 공식 클라우드 서비스
  • 별도 설치 없이 웹에서 데이터베이스 생성 및 운영 가능
  • 무료 플랜 제공 (Aura Free)

2.3 Neo4j Browser

  • 웹 기반 UI (http://localhost:7474)
  • Cypher 쿼리 실행 및 그래프 시각화 기능 탑재
  • 빠른 구조 확인과 쿼리 실험에 유용

2.4 Neo4j 드라이버 (Python 예시)

from neo4j import GraphDatabase

# Neo4j에 연결
uri = "bolt://localhost:7687"
driver = GraphDatabase.driver(uri, auth=("neo4j", "password"))

# 트랜잭션으로 쿼리 실행
def get_friends(tx, name):
    query = "MATCH (p:Person {name: $name})-[:FRIENDS_WITH]->(friend) RETURN friend.name"
    result = tx.run(query, name=name)
    return [record["friend.name"] for record in result]

with driver.session() as session:
    friends = session.read_transaction(get_friends, "Alice")
    print(friends)


GraphDB 와 Neo4j 개요

그래프 데이터베이스(Graph DB)와 Neo4j 개요

1. 그래프 데이터베이스란?

그래프 데이터베이스는 노드(Node)간선(Relationship)을 통해 데이터를 표현하고 저장하는 데이터베이스입니다. 전통적인 관계형 데이터베이스(RDBMS)와 달리, 데이터 간의 연결(관계) 자체를 1차적인 구조로 다룹니다.

주요 구성 요소

  • 노드(Node): 사람, 장소, 제품 등 개별 엔티티를 나타냅니다.
  • 관계(Relationship): 노드 간의 연결을 나타내며 방향성과 속성을 가질 수 있습니다.
  • 속성(Properties): 노드나 관계에 부여된 키-값 형태의 정보입니다.
  • 레이블(Label): 노드를 분류하는 태그 역할을 하며, 쿼리 최적화에 활용됩니다.

2. 그래프 DB의 장점

  • 복잡한 관계 표현에 유리: 친구의 친구, 조직 구조, 추천 시스템 등 복잡한 연결 구조에 적합합니다.
  • 빠른 관계 탐색: JOIN 연산 없이 관계를 직접 따라가며 탐색하므로 속도가 빠릅니다.
  • 스키마 유연성: 노드마다 서로 다른 속성을 가질 수 있어 구조 변경에 유연합니다.
  • 그래프 알고리즘 내장: 페이지랭크, 커뮤니티 탐색, 최단경로 계산 등 내장 알고리즘 활용 가능

3. Neo4j란?

Neo4j는 전 세계에서 가장 널리 사용되는 오픈소스 그래프 데이터베이스입니다. Java로 구현되었으며, ACID 트랜잭션을 지원하며 신뢰성과 성능을 보장합니다.

특징

  • Cypher라는 SQL 유사 쿼리 언어 사용
  • 고성능 네이티브 그래프 처리 엔진
  • 풍부한 시각화 도구 제공
  • Neo4j Aura를 통한 클라우드 서비스 제공

4. 사용 사례

  • 추천 시스템
    • 사용자와 상품, 사용자 간의 유사성을 그래프로 표현하여 연관성이 높은 아이템을 추천
    • 예: “이 영화를 본 사용자 중, 다음 영화를 좋아한 사람은?”
  • 소셜 네트워크 분석
    • 친구 관계, 팔로우 관계, 그룹 구조 등을 탐색하여 영향력 있는 사용자나 커뮤니티 식별
  • 사기 탐지(Fraud Detection)
    • 거래 네트워크, 계좌 간의 연결 구조를 분석하여 비정상적인 행동 패턴 탐지
    • 예: 동일 IP에서 반복적으로 생성된 계좌들 간의 송금
  • 지식 그래프(Knowledge Graph)
    • 개념(엔터티) 간의 관계를 연결하여 검색 및 질의 정확도 향상
    • 예: “파리의 대통령은 누구인가?” → [파리] -[수도]-> [프랑스] -[대통령]-> [인물]
  • IT 인프라 및 네트워크 분석
    • 서버, DB, API 간의 연결 구조를 시각화 및 장애 전파 경로 분석에 활용

5. 관계형 DB와의 차이

항목 관계형 DB (RDBMS) 그래프 DB (Neo4j 등)
구조 테이블 (행/열) 기반 노드 + 관계(Edge) 기반
관계 표현 방식 외래 키, JOIN 직접적인 관계 모델링
쿼리 성능 관계 수 증가 시 느려짐 관계 수 증가 시 효과적
스키마 유연성 엄격한 스키마 유연한 속성 구조
데이터 모델링 정규화 중심 관계 중심 모델링

6. 마무리

Neo4j는 실세계의 데이터 관계를 명확하고 직관적으로 표현할 수 있는 대표적인 그래프 데이터베이스입니다.
복잡한 관계를 가진 데이터를 다루는 분야에서는 전통적인 관계형 DB보다 더 직관적이고 성능 면에서도 뛰어난 선택이 될 수 있습니다.

✅ 요약

  • 관계 중심 도메인에 적합
    복잡한 관계를 표현해야 하는 소셜 네트워크, 추천 시스템, 사기 탐지 등에 최적화되어 있습니다.

  • 쿼리 성능이 관계 수에 좌우되지 않음
    JOIN 없이 직접적인 관계를 탐색하므로 대규모 연결 데이터에 강력합니다.

  • 데이터 모델링이 직관적
    현실 세계의 개체와 관계를 그대로 반영할 수 있어 개발자와 분석가 모두에게 이해가 쉬움

  • 풍부한 생태계 및 도구
    Cypher, 시각화 도구, 클라우드 서비스(Aura) 및 다양한 언어 드라이버(Python, Java, JS 등)를 제공

  • 학습과 실험이 쉬움
    Neo4j Desktop, Neo4j Browser, 온라인 샘플 데이터 제공 등 학습을 위한 리소스가 풍부함


Neo4j와 같은 그래프 데이터베이스는 단순히 데이터를 저장하는 수준을 넘어,
데이터의 관계를 분석하고, 인사이트를 도출하며, 미래를 예측하는 데에 큰 힘을 발휘할 수 있습니다.

관계가 중요한 문제라면, RDB가 아닌 그래프 DB를 고려해 보세요.


Spring MessageSource

Spring MessageSource 란?

Spring MessageSource는 국제화(i18n)와 다국어 메시지 처리를 지원하기 위해 제공되는 Spring의 인터페이스다.
UI나 로그, 예외 메시지를 코드에 하드코딩하지 않고 메시지 번들(properties 파일)을 통해 관리할 수 있게 해준다.


주요 기능

  1. 메시지 추출
    • 코드(key) 기반으로 메시지를 가져옴
  2. 다국어 지원
    • 요청의 Locale 에 따라 다른 메시지를 반환
  3. 매개변수 포맷팅
    • 메시지 내 {0}, {1} 같은 부분에 값 삽입 가능
    • 그러나 실제 DB 연결은 현재 컨텍스트에 따라 선택된 데이터소스를 통해 이루어짐
  4. 기본 메시지 설정
    • 키가 없을 경우 기본 메시지를 설정 가능

예제 코드

아래는 글로벌 그룹사에 대한 그룹웨어 서비스를 제공하기 위해 사용자의 법인에 따라 다른 언어를 노출해야 하는 시나리오이다.

각 요청 접근 시 Interceptor 에서 사용자 토큰에서 사용자 언어를 조회하여 LocaleContextHolder 에 해당 정보를 저장한다.
(LocaleContextHolder 는 Spring 에서 제공하는 ThreadLocale 기반의 Context 저장소이다)

@Component
public class LocaleInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String lang = request.getHeader("Accept-Language"); // 또는 "X-Locale", 커스텀 헤더 가능
        Locale locale = (lang != null) ? Locale.forLanguageTag(lang) : Locale.getDefault();
        LocaleContextHolder.setLocale(locale); // ThreadLocal에 저장
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        LocaleContextHolder.clear();
    }
}

MessageSource 를 통해 다국어 메시지 데이터를 조회한다. (다국어 호출 시 별도의 Locale 을 지정하지 않으면 LocaleContextHolder 기준으로 다국어 언어를 조회한다)

@Autowired
private MessageSource messageSource;

public String getWelcomeMessage() {
    return messageSource.getMessage("greeting", new Object[]{"죠르디"});
}

각 message 에는 언어별 메시지 정보를 정의한다.

# messages_ko.properties
greeting=안녕하세요, {0}님!

# messages_en.properties
greeting=Hello, {0}!


Spring AbstractRoutingDataSource

AbstractRoutingDataSource 란?

AbstractRoutingDataSource는 Spring Framework에서 제공하는 동적 데이터소스 라우팅을 위한 추상 클래스다. 쉽게 말해, 여러 데이터소스를 등록해두고 런타임에 상황에 따라 적절한 데이터소스를 선택해주는 역할을 한다.


주요 역할

  1. 다수의 DataSource 관리
    • 하나의 애플리케이션에서 여러 DB 연결을 사용할 수 있도록 함 (예: Master/Slave 구조, 멀티 테넌시 등)
  2. 현재 컨텍스트에 맞는 DataSource 결정
    • determineCurrentLookupKey() 메서드를 오버라이딩하여, 현재 어떤 데이터소스를 사용할지 결정함
  3. 실제 라우팅 처리
    • 내부적으로는 DataSource 인터페이스를 구현하고 있어서, 마치 일반 DataSource처럼 사용할 수 있음
    • 그러나 실제 DB 연결은 현재 컨텍스트에 따라 선택된 데이터소스를 통해 이루어짐

동작 흐름 요약

  1. 여러 개의 DataSource 를 Map 형태로 등록 (targetDataSources)
  2. determineCurrentLookupKey() 메서드가 호출되어 현재 컨텍스트의 키를 반환
  3. 그 키에 해당하는 DataSource 를 찾아서 실제 쿼리 수행 시 사용

주로 사용되는 사례

  • 읽기/쓰기 분리
  • 멀티 테넌시
  • 샤딩/분산 DB 구성
  • 다중 지역 DB 연결 등

예제 코드

아래는 그룹사의 그룹웨어 서비스의 입장에서 하나의 서비스가 각 법인마다 다른 DataSource 에 연결해야 하는 시나리오이다.

각 요청 접근 시 Interceptor 에서 사용자 토큰에서 사용자의 소속 법인을 조회하여 ContextHolder 패턴을 통해 ThreadLocal 에 소속 법인 정보를 저장한다.

public class CorporationContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setCorpCode(String corpCode) {
        contextHolder.set(corpCode);
    }

    public static String getCorpCode() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }
}
@Component
public class CorporationContextInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String corpCode = request.getHeader("X-Corp-Code"); // 또는 토큰 파싱 등
        CorporationContextHolder.setCorpCode(corpCode);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        CorporationContextHolder.clear();
    }
}

DataSource 접근 시 사용자의 법인코드에 대해 동적 라우팅을 진행한다.

public class CorporationRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return CorporationContextHolder.getCorpCode(); // 현재 법인코드 반환
    }
}

각 법인에 대한 DB 정보와 CustomRoutingDataSource 설정 정보를 정의한다.

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("CORP_A", dataSourceForCorpA());
        targetDataSources.put("CORP_B", dataSourceForCorpB());

        CorporationRoutingDataSource routingDataSource = new CorporationRoutingDataSource();
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(dataSourceForCorpA()); // default 설정
        routingDataSource.afterPropertiesSet();

        return routingDataSource;
    }

    public DataSource dataSourceForCorpA() {
        return DataSourceBuilder.create()
            .url("jdbc:mysql://hostA:3306/db_corpA")
            .username("userA")
            .password("passA")
            .driverClassName("com.mysql.cj.jdbc.Driver")
            .build();
    }

    public DataSource dataSourceForCorpB() {
        return DataSourceBuilder.create()
            .url("jdbc:mysql://hostB:3306/db_corpB")
            .username("userB")
            .password("passB")
            .driverClassName("com.mysql.cj.jdbc.Driver")
            .build();
    }
}


TIL 20240403

신규로 추가되어야 하는 비즈니스 로직 및 이해관계가 복잡한 서비스를 어떻게 운영할 것인가?

  • 우선 복잡한 내용이 정말 복잡한 것인지? 오해나 기반지식 부족으로 인한 착오가 아닌지부터 재확인한다.
  • 그럼에도 불구하고 명백하게 복잡한 내용이라면, 신규 비즈니스의 사용추이를 고려하여 아예 시스템화 하지 않거나, 일부 스펙을 제외하고 개발하는 방향으로 진행한다.
  • 그럼에도 불구하고 꼭 시스템화 해야하는 비즈니스라면, 공통화를 해치는 해당 비즈니스에 대한 모듈을 격리하여 프로젝트 서비스 전체가 오염되지 않도록 한다.

격리는?

  • 아예 서비스 모듈을 분리해 버릴 수도 있고
  • 비즈니스로직 적으로 분할이 가능한 구조라면 패키지 단위로 분리해서 가져가는 것도 좋다.

제일 중요한 건 하나의 요구사항 때문에 프로젝트 전체의 운영&개발 난이도가 올라가지 않도록(오염되지 않도록) 하는 것이라고 생각한다.