Neo4j AuraDB에서 무료 Graph Database 인스턴스를 만들어 보세요!
이번 포스팅에서는 새로운 `REPEATABLE ELEMENTS` 매치 모드를 사용해서, 이전에는 불가능했던 **쿼리**를 해결하는 방법을 보여드릴게요. 예를 들어, **Node** 중 하나에 들어오고 나가는 **Relationship**이 하나만 있는 원형 패턴을 찾는 것 같은 **쿼리** 말이죠.
이 **Graph**를 한번 살펴볼까요?

여기서 `a`에서 `e`로 가는 방법이 몇 가지나 될까요? (**Relationship**의 방향을 따라가면서요!) 제 생각에 여러분 대부분은 두 가지라고 답할 거예요. `b`를 통해서 가거나, 아니면 `c`와 `d`를 거쳐서 우회하는 경우죠.
Cypher로 한번 시도해 볼게요.
MATCH p=(a {name: "a"})-->+(b {name: "e"}) RETURN p
결과가 어떨 것 같나요? 당연히 여러분이 맞았어요! 다음 두 가지 결과를 얻을 수 있어요.
(a)-[:LINK]->(b)-[:LINK]->(e)
(a)-[:LINK]->(b)-[:LINK]->(c)-[:LINK]->(d)-[:LINK]->(b)-[:LINK]->(e)

좋아요. 그런데 왜 이게 완벽한 해결책이 아닐까요?
(a)-[:LINK]->(b)-[:LINK]->(c)-[:LINK]->(d)-[:LINK]->(b)-[:LINK]->(c)-[:LINK]->(d)-[:LINK]->(b)-[:LINK]->(e)
아니면 이건 어때요?
(a)-[:LINK]->(b)-[:LINK]->(c)-[:LINK]->(d)-[:LINK]->(b)-[:LINK]->(c)-[:LINK]->(d)-[:LINK]->(b)-[:LINK]->(c)-[:LINK]->(d)-[:LINK]->(b)-[:LINK]->(e)

왜냐하면 같은 **Relationship**을 두 번 횡단할 수 없기 때문이에요. 우리는 딱 한 번의 루프만 수행할 수 있어요. `c`와 `d` 말이죠. 그런데 왜 그럴까요? 우리는 같은 `b` **Node**를 두 번 방문할 수 있었는데, 왜 **Relationship**은 안 되는 걸까요?
Cypher에서는 전통적으로 **Graph**를 탐색할 때 **Node**를 다시 방문하는 것은 허용되었지만, **Relationship**은 다시 방문할 수 없었어요. 이걸 `DIFFERENT RELATIONSHIPS` 매치 모드라고 불러요. 하지만 이게 지원되는 유일한 모드였기 때문에, 굳이 이걸 깊게 생각할 필요가 없었죠.
`DIFFERENT RELATIONSHIPS`가 기본값인 데에는 몇 가지 이유가 있어요. 우리 인간이 기대하는 것과 비슷하기도 하고 (위의 예시를 생각해보세요!), 무한 루프가 발생할 가능성이 적고, 계산 비용도 더 저렴하거든요.
하지만 새로운 Cypher 25 버전, 즉 Neo4j의 최신 릴리스인 2025.06.0에는 `REPEATABLE ELEMENTS`라는 새로운 매치 모드가 추가되어서, 이제 **Relationship**도 다시 방문할 수 있게 되었어요! `DIFFERENT RELATIONSHIPS`는 여전히 기본값이지만, `MATCH` 다음에 `REPEATABLE ELEMENTS`라는 키워드를 추가할 수 있어요.
MATCH REPEATABLE ELEMENTS <PATH PATTERN>, <PATH PATTERN>...
그런데, 도대체 왜 이런 걸 하고 싶을까요? 언제 유용할까요? 다양한 사용 사례를 한번 살펴볼게요.
데이터 세트
이걸 위해서, 전 세계 모든 공항과 공항 연결 방식이 담긴 데이터 세트가 필요했어요. 정규 항공편으로 해당 노선을 운항하는 항공사가 하나 이상 있다면, 하나의 공항이 다른 공항과 연결된 것으로 간주하는 거죠. 두 공항 사이에는 기껏해야 하나의 연결만 있을 거고, 방향은 신경 쓰지 않기로 했어요. (항공사가 한 방향으로 비행하면, 다른 방향으로도 비행한다고 가정하는 거예요. 항상 그런 건 아니지만, 적어도 대부분의 경우엔 단순화할 수 있거든요.)
저는 Jonty Wareing인데, 대부분의 공항 및 노선 데이터 세트를 GitHub에서 위에서 설명한 형식으로 Neo4j 데이터베이스로 직접 가져오는 Cypher 쿼리를 작성했어요. 이 쿼리는 AuraDB Free에서도 작동하도록 설계되었답니다:
MATCH (n) DETACH DELETE n;
CREATE INDEX iata IF NOT EXISTS FOR (a:Airport) ON (a.iata);
CALL apoc.load.json("https://raw.githubusercontent.com/Jonty/airline-route-data/refs/heads/main/airline_routes.json", null, {failOnError:false}) YIELD value
UNWIND keys(value) AS iata
WITH value[iata] AS iata
CALL(iata) {
MERGE (airport:Airport {iata: iata['iata']})
SET airport.location = point({latitude: toFloat(iata['latitude']), longitude: toFloat(iata['longitude'])})
WITH iata, airport
CALL(iata, airport) {
UNWIND keys(iata) AS prop
FILTER prop <> 'routes' AND prop <> 'iata' AND prop <> 'latitude' AND prop <> 'longitude'
SET airport[prop] = iata[prop]
}
CALL(iata, airport) {
UNWIND iata['routes'] AS route
UNWIND route['carriers'] AS carrier
WITH route, collect(carrier['name']) AS carriers
MERGE (destination:Airport {iata: route['iata']})
MERGE (airport)-[c:CONNECTION]-(destination)
ON CREATE
SET c += { km: route['km'], carriers: carriers }
}
} IN TRANSACTIONS OF 1000 ROWS;
참고로
FILTER는 위 쿼리에서 사용된 키워드인데요. Cypher 25의 또 다른 새로운 기능이랍니다.WITH다음에WHERE를 사용하는 것과 같아요.
Aura를 사용하고 있다면 쿼리 도구에서 바로 실행할 수 있어요. 시각화를 위해 도구 모음에서 탐색 도구로 전환하면 되는데요. 설정이 4,000개 이상의 nodes를 허용하는지 확인하고, 모든 nodes를 가져오는 쿼리를 실행해 보세요.
MATCH (n) RETURN n
이제 으로 바꿔볼까요? 화면 오른쪽 하단에 있는 선택기에서 (아직 설정되어 있지 않다면) 를 좌표에 사용할 속성으로 설정하면 돼요. 전체 그래프를 볼 수 있도록 축소하고, 그래프를 보려면 슬라이더를 당겨 보세요. X 와 Y 축을 조정하면 모든 공항에서 친숙한 모양이 보이기 시작할 거예요.

자, 이제 Command+A 또는 Ctrl+A를 눌러 모든 nodes를 선택해 주세요. nodes 위에서 마우스 오른쪽 버튼을 클릭하고 확장 > 모두를 선택하면 모든 경로도 볼 수 있답니다.
이것으로 끝이에요. 이제 REPEATABLE ELEMENTS의 사용 사례에 필요한 데이터가 준비되었네요!
사용 사례
다음은 위의 공항 데이터를 사용하는 REPEATABLE ELEMENTS의 몇 가지 다양한 사용 사례랍니다.
이사피외르뒤르(Ísafjörður)의 고립
저는 스웨덴 말뫼에 살고 있어요. 여기에는 "Sturup"(IATA 코드 MMX)라는 별명을 가진 말뫼 공항이라는 공항이 있죠. 훨씬 더 큰 덴마크 코펜하겐 공항에 가는 것이 더 빠르고 쉽기 때문에 이 공항에서 비행기를 타는 일은 거의 없어요. 하지만 이 사용 사례에서는 말뫼 공항에서 비행기를 타고 싶다고 가정해 볼게요 (강풍으로 인해 코펜하겐으로 가는 다리가 폐쇄되었을 수도 있겠죠!).
이제 저는 지금 당장 어딘가로 날아가고 싶은 충동을 느껴요 (이 날 다리가 폐쇄되어 타이밍이 좋지 않네요!). 그냥 비행기를 타고 어디론가 가고 싶은 게 아니라, 적어도 세 번은 비행기를 타고 가고 싶어요. 하지만 보안 검색대에서 보내고 싶은 시간에는 제한이 있으므로 비행 횟수는 5회를 넘지 않아야 해요. 말뫼에서 시작한다면 얼마나 많은 가능성이 있을까요? 다음을 한번 살펴볼까요?
MATCH (mmx:Airport {iata: "MMX"})
MATCH p=(mmx)(()-[:CONNECTION]-()){3,5}(mmx)
RETURN p
이를 통해 우리는 181,630가지의 다양한 여행 옵션을 선택할 수 있고, 서쪽 시카고, 북쪽 트롬쇠, 동쪽 도쿄, 남쪽 아디스아바바에서 전 세계 공항의 6.4%(242개)에 도달할 수 있답니다.
제겐 아이슬란드 북서부의 이사피외르뒤르(Ísafjörður)에 사는 친구가 있는데(아, 물론 실제로 그렇진 않지만 그렇다고 쳐봐요!) 트롬쇠에서 제가 겪었던 놀라운 경험에 대해 듣고 자기도 똑같이 해보고 싶어해요. 자, 이제 Ísafjörður(IATA 코드 IFJ)에 대한 쿼리를 업데이트해볼까요?
MATCH (ifj:Airport {iata: "IFJ"})
MATCH p=(ifj)(()-[:CONNECTION]-()){3,5}(ifj)
RETURN p
결과가 0이에요! 왜 그럴까요? Icelandair에서 이 공항을 오가는 항공편을 운항하고 있는데 말이죠. 그 이유는 레이캬비크로 가는 연결편이 단 하나뿐이고, 출국 항공편에서 이걸 사용해야 하기 때문에 다시 돌아올 때 이 연결편을 사용할 수 없기 때문이에요(같은 relationship을 두 번 횡단할 수 없다는 점을 기억하세요!).
해결책은 새로운 REPEATABLE ELEMENTS를 사용하는 거예요. 연결을 다시 방문할 수 있도록 해주죠. 이렇게요!
MATCH (ifj:Airport {iata: "IFJ"})
MATCH REPEATABLE ELEMENTS p=(ifj)(()-[:CONNECTION]-()){3,5}(ifj)
RETURN p
이제 결과를 얻었네요. 아쉽게도 제 친구는 경기가 3번밖에 안 되어서 아이슬란드를 떠나지 못해요. 왜냐면 레이캬비크가 국내 공항이기 때문에 다른 노선이 두 개뿐이거든요(국제 공항은 레이캬비크에서 30마일 떨어진 케플라비크!). 따라서 네 단계를 거치면 Akureyri 또는 Egilsstadir에 도착하고 돌아오게 돼요(아니면 Reykjavik에 갔다가 두 번 돌아오거나). 다섯 번째 홉을 사용하면 Ísafjörður로 돌아오지 않아요. 하지만 적어도 REPEATABLE ELEMENTS 덕분에 Ísafjörður의 고립을 완전히 해결하진 못하더라도 쿼리는 해결할 수 있게 되었어요.
나는 500마일을 날고 싶다
이번에는 The Proclaimers의 "I'm Gonna Be(500 Miles)"의 정신으로 공항 그래프를 사용해서 또 다른 사용 사례를 살펴볼게요. 500마일을 걷는 건 너무 힘들 것 같으니, 대신 비행기를 타보자구요!
마일리지 추적하시는 분 계신가요? 우리가 절대 잃고 싶지 않은 항공사 자격을 유지하는 데 500마일이 부족하다고 상상해 보세요. 그럼 이전 섹션과 마찬가지로 500 상태 포인트를 획득하는 것을 목표로 한번 둘러볼까요? 거리와 티켓 가격 사이에는 약간의 상관관계가 있다고 가정하고(실제로는 좀 약하지만!), 우리는 검소하니까 거리를 최대한 낮게 유지하면서도 500 이상을 유지하려고 하는 거죠.
이번에는 항공편 상태 때문에 Scandinavian Airlines만 이용하고 싶어요(당분간 항공사 제휴는 무시할게요). 이 항공사의 세 가지 주요 허브는 코펜하겐, 스톡홀름, 오슬로에요. 그래서 이번에는 다리에 바람이 잘 통해서 Malmö 대신 코펜하겐으로 넘어갈 수 있기를 바라봅니다.
연결 시의 거리는 킬로미터(km) 단위이고, 500마일은 805km와 같아요. 우리의 질문은 코펜하겐에서 출발해서 2~6번의 점프를 하고 다시 코펜하겐에 도착하는 여정이에요. 목표는 가능한 한 805km에 가깝게 유지하는 거죠(물론 넘어야 하고요!). 한도를 6개로 설정한 이유는 150km 미만의 비행편이 거의 없기 때문이에요.
MATCH (cph:Airport {iata: "CPH"})
MATCH p=(cph)(()-[c:CONNECTION]-() WHERE "Scandinavian Airlines" IN c.carriers){2,6}(cph)
WITH p, reduce(d = 0, c IN relationships(p) | d + c.km) AS distance
WHERE distance >= 805
ORDER BY distance LIMIT 1
RETURN p, distance
우리가 받은 제안은 코펜하겐에서 오슬로까지(516km), 오슬로에서 오르후스까지(433km), 다시 코펜하겐까지(147km) 비행하는 여정이에요. 총 거리는 1,096km(681마일)네요.
음, 꽤 많이 초과됐네요. 제가 돈이 엄청 많다고 생각하는 걸까요? 좀 더 최적의 변형을 찾아봐야겠어요. 다시 한번 REPEATABLE ELEMENTS 구조를 활용해볼게요. 우리는 오슬로까지 왕복하는 시간이 더 짧으면서도 여전히 500마일 이상이 될 거라는 걸 알고 있으니, 연결 재사용을 허용하면 확실히 도움이 될 거예요. 다음은 REPEATABLE ELEMENTS를 사용한 쿼리에요(548km보다 짧은 항공편만 고려하도록 필터를 추가했어요. 이미 그 두 배에 달하는 솔루션이 있으니까요!).
MATCH (cph:Airport {iata: "CPH"})
MATCH REPEATABLE ELEMENTS p=(cph)(()-[c:CONNECTION]-() WHERE c.km < 548 AND "Scandinavian Airlines" IN c.carriers){2,6}(cph)
WITH p, reduce(d = 0, c IN relationships(p) | d + c.km) AS distance
WHERE distance >= 805
ORDER BY distance LIMIT 1
RETURN p, distance
같은 공항 사이를 두 번 이상 비행하는 게 허용되면 더 짧은 옵션이 제공되기도 해요. Torp는 코펜하겐에서 420km 떨어진 오슬로 남쪽 노르웨이 해안 공항인데, 그래서 총 여행은 840km(522마일)로 끝났어요. 덕분에 항공편 상태를 1년 더 확보하게 되었죠.
두 번째로 좋은 대안은 오르후스를 세 번 왕복하는 거였는데, 그건 너무 단조로웠을 것 같아서 Torp가 있어서 다행이었어요.
상상의 친구를 방문하다
Cypher에는 "keyword"라고 불리는 키워드가 있는데, SHORTEST 이걸 통해 그래프에서 최단 경로(홉의 무게가 아닌 홉 수로)를 찾을 수 있어요. 이건 이제 REPEATABLE ELEMENTS를 지원한답니다.
잠깐만요, 최단 경로의 경우 관계를 반복할 필요가 전혀 없다고 말할 수도 있겠네요! 음, 두 지점 사이의 절대 최단 경로를 원한다면 그렇지 않죠. 하지만 홉 제한이 더 낮거나 경로에 대한 다른 특별한 규칙이 있는 경우 그럴 수도 있어요. 또 다른 예를 살펴볼까요?
저는 코펜하겐에서 댈러스로 비행기를 타고 친구들(이번에는 진짜 친구들!)을 방문하고 싶고, 가능한 한 적은 비행 시간으로 여행하고 싶어요. 이렇게 할 수 있겠죠.
MATCH p = ALL SHORTEST (:Airport {iata:"CPH"})--+(:Airport {iata:"DFW"})
WITH p, reduce(d = 0, c IN relationships(p) | d + c.km) AS distance
ORDER BY distance LIMIT 1
RETURN p
첫 번째 명령문은 (최소 비행 횟수에서) 최단 경로를 찾는 명령문이에요. 우리는 ALL SHORTEST (대신 SHORTEST 1)를 사용해서 단 하나의 경로가 아닌 모든 최단 경로를 반환해요. 그런 다음 총 거리를 기준으로 순서를 지정하고 거리 기준으로 가장 짧은 것을 선택하는 거죠. 총 거리가 더 짧고 홉 수가 더 많은 항공편이 여전히 있을 수 있지만, 우리는 항공편 환승 횟수를 줄이는 걸 목표로 하고 있고, 이걸 통해 시카고에서 한 번의 환승 옵션을 제공해 줘요.
여기에 또 다른 조건을 추가해 볼게요. 이사피외르뒤르(Ísafjörður)에 있는 제 상상의 친구를 기억하시나요? 지구의 곡률 때문에 북유럽에서 북미로 가는 항공편은 대개 아이슬란드 상공을 비행한다는 걸 알고 있으니, 그를 방문하는 것도 괜찮겠죠? 다음과 같은 쿼리가 될 거예요.
MATCH p = ALL SHORTEST (:Airport {iata:"CPH"})--+(:Airport {iata:"IFJ"})--+(:Airport {iata: "DFW"})
WITH p, reduce(d = 0, c IN relationships(p) | d + c.km) AS distance
ORDER BY distance LIMIT 1
RETURN p
하지만 우리는 어떤 결과도 얻지 못했고, 그 이유를 이미 알고 있어요. 이사피외르뒤르(Ísafjörður)에 가려면 여행을 계속할 때 재사용할 경로를 사용해야 해요. 그래서 다시, 우리는 다음으로 전환해야 해요. REPEATABLE ELEMENTS 문제를 해결하려면 다음 단계를 따르세요.
MATCH REPEATABLE ELEMENTS p = ALL SHORTEST (:Airport {iata:"CPH"})--{,10}(:Airport {iata:"IFJ"})--{,10}(:Airport {iata: "DFW"})
WITH p, reduce(d = 0, c IN relationships(p) | d + c.km) AS distance
ORDER BY distance LIMIT 1
RETURN p;
+는 {,10}으로 대체돼요. 이는 다음을 사용할 때 무한 수량자를 가질 수 없기 때문이에요.
REPEATABLE ELEMENTS. 조금만 생각해 보면 이해될 거예요.
이를 통해 우리는 케플라비크(레이캬비크 국제 공항)를 통과하는 9홉 경로를 얻게 되지만, 레이캬비크 국내 공항을 거쳐 이사피요르뒤르까지 이동하려면 아쿠레이리까지 비행기를 타고 가야 해요. 그런 다음 케플라비크(Keflavík)로 다시 같은 길을 횡단해야 하며, 거기에서 시카고를 거쳐 댈러스로 계속 이동해야 하죠.
이걸 보고 내린 결론은 케플라비크와 레이캬비크 사이 50km 정도는 택시를 타는 게 더 나았을 거라는 거예요. 그렇게 하면 9번의 비행 중 4번의 비행과 총 1,066km의 비행 시간을 절약할 수 있거든요. 하지만 택시에서는 공짜 땅콩을 얻을 수 없잖아요!
요약
여기서 살펴본 예를 보면 REPEATABLE ELEMENTS는 동일한 관계를 양방향으로 탐색하려는 경우에만 유용하다는 걸 알 수 있어요. 하지만 우리는 같은 방향으로 관계를 재검토하고 싶을 수도 있죠. 이 기사의 첫 번째 예제 그래프로 돌아가서 최단 경로를 원한다고 말하면 a to e, 그러나 6번 이상의 점프에서는 다음을 사용하지 않으면 결과를 얻을 수 없어요. REPEATABLE ELEMENTS:
MATCH REPEATABLE ELEMENTS p = SHORTEST 1 (a {name: "a"})-->{6,10}(b {name: "e"})
RETURN p
이는 두 개의 루프를 통해 결과를 제공해요. c and d.
Neo4j 및 Cypher의 대부분의 전통적인 사용에서는 다른 관계의 기본 일치 모드가 아마도 여러분이 원하는 방식일 거예요. 하지만 앞서 살펴봤듯이 관계를 여러 번 탐색할 수 있다면 유익한 경우가 분명히 있겠죠?
다음에 대해 더 자세히 읽어볼 수 있어요. 매치 모드(Match Modes) 와 반복 가능한 요소(Repeatable Elements)는 Cypher 문서에서 확인해보세요. 궁금한 사항은 게시판에 올려주시면 됩니다. Neo4j 커뮤니티 포럼에 질문해주세요.
- Cypher
- GQL
- Relationships
에이치시스템즈의 LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.
'GraphRAG' 카테고리의 다른 글
| 텍스트 임베딩 검색, 아주 간단하지만 효과적인 개선 방법 (0) | 2026.05.20 |
|---|---|
| AWS 환경에서 그래프 기술로 GenAI의 난제들을 해결하다 (0) | 2026.05.20 |
| GraphRAG Python 패키지로 그래프 기반 RAG 애플리케이션을 위한 하이브리드 검색 구현하기 (0) | 2026.05.20 |
| How a Neo4j semantic layer makes your Text-to-SQL agent smarter and cheaper (0) | 2026.05.20 |
| GraphTalk Pharma & Life Sciences 2025 되짚어보기 (0) | 2026.05.20 |
