쉬어가는 시간

인터넷 서핑을 하다보면 다음과 같은 문제를 본 적이 있을 것이다.

https://blog.naver.com/adol718/50144639020

내용은 대충 이렇다.

보석방에 70만원짜리 보석을 사러 온 신사가 있는데 100만원 짜리 위조수표를 사용해서 30만원을 거슬러 달라 한다.

돈이 없던 보석방 주인이 돈을 환전하기 위해 빵집 주인에게 돈을 빌리게 된다.

그런데 신사가 사용한 수표는 가짜수표였다.

이때, 보석방 주인이 손해를 본 금액은 얼마인가?

위 내용이 문제이다.

인터넷 댓글을 보다보면 130, 160, 70, 100 등등 수많은 답이 존재했는데 이 문제를 AgensGraph를 이용하여 문제를 해결해보고자 한다.

이런 거래 자체의 흐름을 파악하기 위해선 머리로 상상하는 것이 아니라 하나씩 관계를 그려보는 것이 필요하다고 생각한다.

다음과 같이 그래프 모델링을 진행한다.

  • 참고로 AgensGraph는 Vertex, Edge가 존재하는데 Vertex는 하나의 entity에 존재하는 row와 같다고 보면 된다. (완전 같지는 않다. 이해를 돕고자 설명한 것이다.)

Edge는 이러한 Vertex들 사이의 관계를 표현할 때 사용한다. 주로 동사를 표현한다.

하여 (Vertex)-[Edge]->(Vertex) 라는 관계가 성립된다.

잘 모르겠으면 agensgraph quick guide를 참고하기 바란다. http://bitnine.net/documentations/quick-guide-1-3.html

문제 상황설명

  1. 신사가 보석방에게 70만원짜리 보석을 사러왔는데 수표 100만원을 지불했다.
  2. 보석방 주인은 돈을 거슬러주기 위해 현금 100만원을 빵집주인에게 빌린다.
  3. 돌아온 보석방 주인은 신사에게 30만원을 거슬러준다.
  4. 알고보니 수표는 위조수표였고 빵집주인은 보석방 주인에게 빌린돈을 받는다.
  5. 이때 보석방 주인은 손해를 얼마나 봤는가?

1. 그래프 모델링에 앞서 Vertex, Edge label을 만들어야 한다.

위 문제에서 나타난 동사와 명사를 구분하여 label을 생성한다.
 - Vertex label 종류 : person (entity는 사람이고, 그 안에 name이 gentleman, jeweler, baker로 나뉠 수 있다.)
 - Edge label 종류 : give (주다), borrow(빌리다), repay(갚다)

Alt text

2. 이 문제를 해결하고자 그래프 데이터를 순서대로 입력한다.

그래프 데이터 입력

CREATE GRAPH quiz;
CREATE vlabel person;
CREATE elabel give;
CREATE elabel borrow;
CREATE elabel repay;

CREATE (:person {name:'gentleman'});
CREATE (:person {name:'jeweler'});
CREATE (:person {name:'baker'});

1) 신사가 보석방에게 70만원짜리 보석을 사러왔는데 수표 100만원을 지불했다. (위조수표기 때문에 가치는 0이다.)

MATCH (a:person) WHERE a.name = 'gentleman'
MATCH (b:person) WHERE b.name = 'jeweler'
CREATE (a)-[r:give {value:0}]->(b);

2) 보석방 주인은 돈을 거슬러주기 위해 현금 100만원을 빵집주인에게 빌린다.

  • 이 과정에서 두 개의 edge가 생성된다.
  • 보석방 주인이 위조수표(가치 0원)를 빵집 주인에게 전달하는 과정
    MATCH (a:person) WHERE a.name = 'jeweler'
    MATCH (b:person) WHERE b.name = 'baker'
    CREATE (a)-[r:give {value:0}]->(b);
    
  • 보석방 주인이 빵집 주인에게 100만원을 빌리는 과정
    MATCH (a:person) WHERE a.name = 'jeweler'
    MATCH (b:person) WHERE b.name = 'baker'
    CREATE (a)-[r:borrow {value:100}]->(b);
    

3) 돌아온 보석방 주인은 신사에게 보석과 잔돈 30만원을 준다.

-- 보석방 주인이 신사에게 보석을 주는 과정
MATCH (a:person) WHERE a.name = 'jeweler'
MATCH (b:person) WHERE b.name = 'gentleman'
CREATE (a)-[r:give {value:70}]->(b);
-- 보석방 주인이 신사에게 잔돈을 주는 과정 (같은 edge는 하나만 만들 수 있기 때문에 value를 update 한다.)
MATCH (a:person)-[r:give]->(b:person)
WHERE a.name = 'jeweler'
AND b.name = 'gentleman'
SET r.value = r.value + 30;

4) 수표가 위조수표로 판명됐고, 보석방 주인이 빵집주인에게 빌린 돈을 갚는 과정

MATCH (a:person) WHERE a.name = 'jeweler'
MATCH (b:person) WHERE b.name = 'baker'
CREATE (a)-[r:repay {value:100}]->(b);

위의 그래프 데이터를 만드는 과정은 스크린샷과 같다. Alt text

이런 오묘한 과정들이 섞이다 보니 혼동이 오는 것인데 여기서 문제는 보석방 주인이 손해를 봤다는 것이 문제이다.

그렇다면 이런 관점에서 edge는 행위가 두 가지 이다.

첫째, 주는 행위 (give, repay)

둘째, 받는 행위 (borrow)

일단 보석방 주인이 주는 행위라면 보석방 주인은 손해일 것이고, 받는 행위이면 무언가 얻었기 때문에 이득을 취하는 행위일 것이다. query language 중 CASE WHEN 을 이용하여 조건을 나뉘어본다.

-- edge의 label이 borrow인 경우는 value가 양수, 나머지  label인 경우는 음수로 value를 구하고 모든 value를 sum 하는 query
SELECT sum(t.value::int) 
FROM 
 (MATCH (a)-[r]->(b)
  RETURN CASE WHEN (label(r) = 'borrow') THEN r.value ELSE -r.value END AS value ) t;

Alt text

그렇다. 답은 -100이 된다. 결국 보석방 주인은 100만원을 손해봤다는 결론이 나온다.

AgensGraph에 아쉬운점은 RETURN type이 JSONB로 무조건 넘어오기 때문에 graph query에 sum이나 aggregate 함수가 전혀 먹히질 않는다. 이는 빨리 고쳐줬으면 좋겠는데… 모르겠다. 우선 해결책은 다행인게 Hybrid DB(GDB+RDB)이기 때문에 RDB영역 SQL을 사용할 수 있다. (즉, inline view 형식으로 graph 데이터를 조회하고 SELECT - FROM 을 이용하여 조회할 수 있다.) Graph 영역에서도 aggregate 함수를 제공해주면 아래와 같이 동작할텐데 현재 버전에선 되지 않는다. 언젠간 해결되겠지….

MATCH (a)-[r]->(b)
RETURN sum(CASE WHEN (label(r) = 'borrow') THEN r.value ELSE -r.value END) AS value;

==> 

SELECT sum(t.value::int) 
FROM 
 (MATCH (a)-[r]->(b)
  RETURN CASE WHEN (label(r) = 'borrow') THEN r.value ELSE -r.value END AS value ) t;