지금 하는 업무가 회계의 재고자산 수불 및 원가 관련한 데이터베이스 업무인데, 단순히 데이터베이스만 만지는 게 아니라, 이 프로그램은 특이하게 회계에 관련된 결과물을 도출해내는 모든 로직이 데이터베이스 프로시저(PL/SQL)로 돌아간다. 정작 프로그램 자체에는 아무것도 없는….
아무튼 재고자산의 수불이란 입고와 출고라는 자산의 이동 방향성이 존재하고 반드시 그 재고 자산이 최종적으로 도착하는 종착이 존재한다. 예를 들자면 당사의 공장에서 생산된 후 당사의 A 지점으로 이동하거나, 혹은 당사의 A 지점에서 판매되어 최종적으로 외부 고객사에 이동하거나 하는 흐름이다. 마지막에는 원가까지 딱 맞아야 하기에 이동 과정에서 비용 및 원가의 변화 역시 제대로 감지해야 한다.
지금까지는 굉장히 복잡한 쿼리로 처리했던 것을 앞으로는 위상 정렬 알고리즘을 통해 처리되도록 로직을 뜯어 고치는 것이 현재의 과제이다. 앞서 말했듯 방향성과 종착점이 존재하는 수불의 특성 상 위상 정렬 알고리즘을 이에 적용하는 것은 적절하다고 볼 수 있다.
**a -> b
b -> c
c -> b**
a 노드의 진입차수:0, 진출차수:1
b 노드의 진입차수:2, 진출차수:1
c 노드의 진입차수:1, 진출차수:1
재고자산의 이동 루트 데이터가 아래와 같다고 하자.
* 출고지 -> 도착지
(1) a -> b
(2) c -> a
(3) d -> a
(4) d -> e
(5) e -> a
(6) e -> c
큐에 d를 넣고 d에 대한 루트는 삭제
(1) a -> b
(2) c -> a
(5) e -> a
(6) e -> c
큐에 e를 넣고 e에 대한 루트는 삭제
(1) a -> b
(2) c -> a
큐에 c를 넣고 c에 대한 루트는 삭제
(1) a -> b
* 출고지 -> 도착지
(1) a -> b
(2) c -> a
(3) d -> a
(4) d -> e
(5) e -> a
(6) e -> c
from collections import deque
# 1️⃣ 그래프 정의
# (인접 리스트를 값으로 가지는 딕셔너리)
graph = {
'a': ['b'],
'c': ['a'],
'd': ['a', 'e'],
'e': ['a', 'c'],
'b': [], # 진출 간선이 없는 노드
}
#graph = {
# 'a': ['b'],
# 'b': ['c'], # 'b' → 'c' 추가
# 'c': ['a'], # 'c' → 'a' 추가 (사이클 발생!)
# 'd': ['a', 'e'],
# 'e': ['a', 'c'],
#}
# 2️⃣ 진입 차수(In-degree) 계산
in_degree = {node: 0 for node in graph}
# 모든 노드의 진입 차수 0으로 초기화
print("*** in_degree : \n", in_degree, "\n")
# { 'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0 }
# 그래프에서 각 노드가 가리키는 노드들의 진입 차수를 계산
for nodes in graph.values(): # 그래프의 모든 값(출발 노드의 도착 노드 리스트)을 순회
for node in nodes: # 각 출발 노드에 연결된 개별 도착 노드를 순회
in_degree[node] += 1 # 도착 노드의 진입 차수 1 증가
print("*** in_degree : \n", in_degree, "\n")
# { 'a': 3, 'b': 1, 'c': 1, 'd': 0, 'e': 1 }
# 3️⃣ 진입 차수가 0인 노드를 큐에 삽입
queue = deque([node for node in in_degree if in_degree[node] == 0])
# 4️⃣ 위상 정렬 수행 (BFS)
topological_order = []
while queue:
# 큐가 빌 때까지 반복
# 큐(Queue) 는 현재 진입 차수가 0인 노드들을 저장
current = queue.popleft() # 큐에서 노드 하나 꺼내기
topological_order.append(current) # 순서에 추가
print("*** topological_order:", topological_order)
print("(", current,"의 도착지) graph[current]:", graph[current])
# 현재 노드와 연결된 간선 제거
for neighbor in graph[current]:
print("(간선감소전) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
in_degree[neighbor] -= 1 # 진입 차수 감소
print("(간선감소후) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
if in_degree[neighbor] == 0: # 진입 차수가 0이 되면 큐에 추가
queue.append(neighbor)
print("(간선0) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
print("*** queue:", queue, "\n")
# 5️⃣ 출력
print("위상 정렬 순서:", topological_order, "\n")
print("*len(topological_order):", len(topological_order), "\n*topological_order:", topological_order)
print("*len(graph):", len(graph), "\n*graph:", graph)
# 사이클이 있는 경우 감지
# (위상 정렬 결과의 노드 개수가 원래 노드 개수보다 적으면 사이클 존재)
if len(topological_order) != len(graph):
print("\n---사이클이 존재하여 위상 정렬이 불가능합니다.---")
else:
print("\n---위상 정렬 정상 수행---")
###### print result
*** in_degree :
{'a': 0, 'c': 0, 'd': 0, 'e': 0, 'b': 0}
*** in_degree :
{'a': 3, 'c': 1, 'd': 0, 'e': 1, 'b': 1}
*** topological_order: ['d']
( d 의 도착지) graph[current]: ['a', 'e']
(간선감소전) in_degree[neighbor]: 3 / neighbor: a
(간선감소후) in_degree[neighbor]: 2 / neighbor: a
(간선감소전) in_degree[neighbor]: 1 / neighbor: e
(간선감소후) in_degree[neighbor]: 0 / neighbor: e
(간선0) in_degree[neighbor]: 0 / neighbor: e
*** queue: deque(['e'])
*** topological_order: ['d', 'e']
( e 의 도착지) graph[current]: ['a', 'c']
(간선감소전) in_degree[neighbor]: 2 / neighbor: a
(간선감소후) in_degree[neighbor]: 1 / neighbor: a
(간선감소전) in_degree[neighbor]: 1 / neighbor: c
(간선감소후) in_degree[neighbor]: 0 / neighbor: c
(간선0) in_degree[neighbor]: 0 / neighbor: c
*** queue: deque(['c'])
*** topological_order: ['d', 'e', 'c']
( c 의 도착지) graph[current]: ['a']
(간선감소전) in_degree[neighbor]: 1 / neighbor: a
(간선감소후) in_degree[neighbor]: 0 / neighbor: a
(간선0) in_degree[neighbor]: 0 / neighbor: a
*** queue: deque(['a'])
*** topological_order: ['d', 'e', 'c', 'a']
( a 의 도착지) graph[current]: ['b']
(간선감소전) in_degree[neighbor]: 1 / neighbor: b
(간선감소후) in_degree[neighbor]: 0 / neighbor: b
(간선0) in_degree[neighbor]: 0 / neighbor: b
*** queue: deque(['b'])
*** topological_order: ['d', 'e', 'c', 'a', 'b']
( b 의 도착지) graph[current]: []
*** queue: deque([])
위상 정렬 순서: ['d', 'e', 'c', 'a', 'b']
*len(topological_order): 5
*topological_order: ['d', 'e', 'c', 'a', 'b']
*len(graph): 5
*graph: {'a': ['b'], 'c': ['a'], 'd': ['a', 'e'], 'e': ['a', 'c'], 'b': []}
---위상 정렬 정상 수행---
* 출고지 -> 도착지
(1) a -> b
(2) b -> c
(3) c -> d
from collections import deque
# 1️⃣ 그래프 정의
# (인접 리스트를 값으로 가지는 딕셔너리)
graph = {
'a': ['b', 'c'],
'b': ['d'],
'c': ['d'],
'd': []
}
#graph = {
# 'a': ['b'],
# 'b': ['c'], # 'b' → 'c' 추가
# 'c': ['a'], # 'c' → 'a' 추가 (사이클 발생!)
# 'd': ['a', 'e'],
# 'e': ['a', 'c'],
#}
# 2️⃣ 진입 차수(In-degree) 계산
in_degree = {node: 0 for node in graph}
# 모든 노드의 진입 차수 0으로 초기화
print("*** in_degree : \n", in_degree, "\n")
# { 'a': 0, 'b': 0, 'c': 0, 'd': 0 }
# 그래프에서 각 노드가 가리키는 노드들의 진입 차수를 계산
for nodes in graph.values(): # 그래프의 모든 값(출발 노드의 도착 노드 리스트)을 순회
for node in nodes: # 각 출발 노드에 연결된 개별 도착 노드를 순회
in_degree[node] += 1 # 도착 노드의 진입 차수 1 증가
print("*** in_degree : \n", in_degree, "\n")
# {'a': 0, 'b': 1, 'c': 1, 'd': 2}
# 3️⃣ 진입 차수가 0인 노드를 큐에 삽입
queue = deque([node for node in in_degree if in_degree[node] == 0])
# 4️⃣ 위상 정렬 수행 (BFS)
topological_order = []
while queue:
# 큐가 빌 때까지 반복
# 큐(Queue) 는 현재 진입 차수가 0인 노드들을 저장
current = queue.popleft() # 큐에서 노드 하나 꺼내기
topological_order.append(current) # 순서에 추가
print("*** topological_order:", topological_order)
print("(", current,"의 도착지) graph[current]:", graph[current])
# 현재 노드와 연결된 간선 제거
for neighbor in graph[current]:
print("(간선감소전) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
in_degree[neighbor] -= 1 # 진입 차수 감소
print("(간선감소후) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
if in_degree[neighbor] == 0: # 진입 차수가 0이 되면 큐에 추가
queue.append(neighbor)
print("(간선0) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
print("*** queue:", queue, "\n")
# 5️⃣ 출력
print("위상 정렬 순서:", topological_order, "\n")
print("*len(topological_order):", len(topological_order), "\n*topological_order:", topological_order)
print("*len(graph):", len(graph), "\n*graph:", graph)
# 사이클이 있는 경우 감지
# (위상 정렬 결과의 노드 개수가 원래 노드 개수보다 적으면 사이클 존재)
if len(topological_order) != len(graph):
print("\n---사이클이 존재하여 위상 정렬이 불가능합니다.---")
else:
print("\n---위상 정렬 정상 수행---")
###### print result
*** in_degree :
{'a': 0, 'b': 0, 'c': 0, 'd': 0}
*** in_degree :
{'a': 0, 'b': 1, 'c': 1, 'd': 2}
*** topological_order: ['a']
( a 의 도착지) graph[current]: ['b', 'c']
(간선감소전) in_degree[neighbor]: 1 / neighbor: b
(간선감소후) in_degree[neighbor]: 0 / neighbor: b
(간선0) in_degree[neighbor]: 0 / neighbor: b
(간선감소전) in_degree[neighbor]: 1 / neighbor: c
(간선감소후) in_degree[neighbor]: 0 / neighbor: c
(간선0) in_degree[neighbor]: 0 / neighbor: c
*** queue: deque(['b', 'c'])
*** topological_order: ['a', 'b']
( b 의 도착지) graph[current]: ['d']
(간선감소전) in_degree[neighbor]: 2 / neighbor: d
(간선감소후) in_degree[neighbor]: 1 / neighbor: d
*** queue: deque(['c'])
*** topological_order: ['a', 'b', 'c']
( c 의 도착지) graph[current]: ['d']
(간선감소전) in_degree[neighbor]: 1 / neighbor: d
(간선감소후) in_degree[neighbor]: 0 / neighbor: d
(간선0) in_degree[neighbor]: 0 / neighbor: d
*** queue: deque(['d'])
*** topological_order: ['a', 'b', 'c', 'd']
( d 의 도착지) graph[current]: []
*** queue: deque([])
위상 정렬 순서: ['a', 'b', 'c', 'd']
*len(topological_order): 4
*topological_order: ['a', 'b', 'c', 'd']
*len(graph): 4
*graph: {'a': ['b', 'c'], 'b': ['d'], 'c': ['d'], 'd': []}
---위상 정렬 정상 수행---
'a' → 'b' → 'c' → 'a'와 같이 순환 구조를 만들면 사이클 발생.from collections import deque
# 1️⃣ 그래프 정의
# (인접 리스트를 값으로 가지는 딕셔너리)
#graph = {
# 'a': ['b'],
# 'c': ['a'],
# 'd': ['a', 'e'],
# 'e': ['a', 'c'],
# 'b': [], # 진출 간선이 없는 노드
#}
graph = {
'a': ['b'],
'b': ['c'], # 'b' → 'c' 추가
'c': ['a'], # 'c' → 'a' 추가 (사이클 발생!)
'd': ['a', 'e'],
'e': ['a', 'c'],
}
# 2️⃣ 진입 차수(In-degree) 계산
in_degree = {node: 0 for node in graph}
# 모든 노드의 진입 차수 0으로 초기화
print("*** in_degree : \n", in_degree, "\n")
# { 'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0 }
# 그래프에서 각 노드가 가리키는 노드들의 진입 차수를 계산
for nodes in graph.values(): # 그래프의 모든 값(출발 노드의 도착 노드 리스트)을 순회
for node in nodes: # 각 출발 노드에 연결된 개별 도착 노드를 순회
in_degree[node] += 1 # 도착 노드의 진입 차수 1 증가
print("*** in_degree : \n", in_degree, "\n")
# {'a': 3, 'b': 1, 'c': 2, 'd': 0, 'e': 1}
# 3️⃣ 진입 차수가 0인 노드를 큐에 삽입
queue = deque([node for node in in_degree if in_degree[node] == 0])
# 4️⃣ 위상 정렬 수행 (BFS)
topological_order = []
while queue:
# 큐가 빌 때까지 반복
# 큐(Queue) 는 현재 진입 차수가 0인 노드들을 저장
current = queue.popleft() # 큐에서 노드 하나 꺼내기
topological_order.append(current) # 순서에 추가
print("*** topological_order:", topological_order)
print("(", current,"의 도착지) graph[current]:", graph[current])
# 현재 노드와 연결된 간선 제거
for neighbor in graph[current]:
print("(간선감소전) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
in_degree[neighbor] -= 1 # 진입 차수 감소
print("(간선감소후) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
if in_degree[neighbor] == 0: # 진입 차수가 0이 되면 큐에 추가
queue.append(neighbor)
print("(간선0) in_degree[neighbor]:", in_degree[neighbor], " / neighbor:", neighbor)
print("*** queue:", queue, "\n")
# 5️⃣ 출력
print("위상 정렬 순서:", topological_order, "\n")
print("*len(topological_order):", len(topological_order), "\n*topological_order:", topological_order)
print("*len(graph):", len(graph), "\n*graph:", graph)
# 사이클이 있는 경우 감지
# (위상 정렬 결과의 노드 개수가 원래 노드 개수보다 적으면 사이클 존재)
if len(topological_order) != len(graph):
print("\n---사이클이 존재하여 위상 정렬이 불가능합니다.---")
else:
print("\n---위상 정렬 정상 수행---")
#### print result
*** in_degree :
{'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0}
*** in_degree :
{'a': 3, 'b': 1, 'c': 2, 'd': 0, 'e': 1}
*** topological_order: ['d']
( d 의 도착지) graph[current]: ['a', 'e']
(간선감소전) in_degree[neighbor]: 3 / neighbor: a
(간선감소후) in_degree[neighbor]: 2 / neighbor: a
(간선감소전) in_degree[neighbor]: 1 / neighbor: e
(간선감소후) in_degree[neighbor]: 0 / neighbor: e
(간선0) in_degree[neighbor]: 0 / neighbor: e
*** queue: deque(['e'])
*** topological_order: ['d', 'e']
( e 의 도착지) graph[current]: ['a', 'c']
(간선감소전) in_degree[neighbor]: 2 / neighbor: a
(간선감소후) in_degree[neighbor]: 1 / neighbor: a
(간선감소전) in_degree[neighbor]: 2 / neighbor: c
(간선감소후) in_degree[neighbor]: 1 / neighbor: c
*** queue: deque([])
위상 정렬 순서: ['d', 'e']
*len(topological_order): 2
*topological_order: ['d', 'e']
*len(graph): 5
*graph: {'a': ['b'], 'b': ['c'], 'c': ['a'], 'd': ['a', 'e'], 'e': ['a', 'c']}
---사이클이 존재하여 위상 정렬이 불가능합니다.---
즉 진입차수가 0인 노드가 없는 구간이 발생하여 모든 노드를 참조하기 이전에 와일문이 종료되면 사이클 즉 순환참조가 발생했다고 보는 것.
이제 문제는 이걸 데베쿼리로 어떻게 구현하나…