[TIL]
Graphviz
Please join the Graphviz forum to ask questions and discuss Graphviz. What is Graphviz? Graphviz is open source graph visualization software. Graph visualization is a way of representing structural information as diagrams of abstract graphs and networks. I
graphviz.org
( Graphviz 의 공식 사이트 )
구현 목표)
Graphviz 라는 노드들을 연결하여 그래프를 그려주는 툴을 기반으로 사용자의 앱 이동 경로를 시각화하기로 하였다.
Graphviz 는 공식적으로는 java 를 지원하지 않지만 감사하게도(?) 자바를 통해 객체 형태로 노드들을 관리하고 시각화할 수 있게끔 오픈소스로 Graphviz 를 변형해둔 깃허브 주소를 발견해서 해당 소스를 토대로 개발을 진행하였다.
⬇️ Graphviz in JAVA 오픈소스 Github ⬇️
GitHub - nidi3/graphviz-java: Use graphviz with pure java
Use graphviz with pure java. Contribute to nidi3/graphviz-java development by creating an account on GitHub.
github.com
구현 과정)
우선, 결과부터 얘기해보자면 아래에 기술하는 첫번째의 방법으로는 구현을 실패하였고 차선책으로 사용한 두번째 구현 방법으로 구현에 성공하였다.
1️⃣ 순수 자바코드로 구현 시도
해당 오픈소스 사용 방법으로는 해당 오픈소스 의존성을 추가하고 자바 코드로 노드를 시각화하려면 아래와 같이 가변형 그래프객체(그래프에 노드를 추가하는 형식이 아니라 한번에 그래프를 생성하는 경우는 MutableGraph 객체를 사용하지 않아도 된다)를 생성하고 가변형 노드객체를 선언하여 추가하는 형식으로 이용하면 된다.
<dependency>
<groupId>guru.nidi</groupId>
<artifactId>graphviz-java</artifactId>
<version>0.18.1</version>
</dependency>
( 의존성 추가 )
import guru.nidi.graphviz.attribute.Color;
import guru.nidi.graphviz.engine.Format;
import guru.nidi.graphviz.engine.Graphviz;
import guru.nidi.graphviz.model.MutableGraph;
import guru.nidi.graphviz.model.MutableNode;
import static guru.nidi.graphviz.model.Factory.*;
import java.io.File;
import java.io.IOException;
public class GraphExample {
public static void main(String[] args) {
try {
// MutableGraph 객체 생성
MutableGraph g = mutGraph("example1").setDirected(true);
// 개별 노드 생성
MutableNode nodeA = mutNode("a").add(Color.RED);
MutableNode nodeB = mutNode("b");
// 노드를 그래프에 추가
g.add(nodeA);
g.add(nodeB);
// 노드 간 링크 추가
nodeA.addLink(nodeB);
// 그래프를 PNG로 렌더링 후 파일로 저장
Graphviz.fromGraph(g).width(200).render(Format.PNG).toFile(new File("example/ex1m.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
( JAVA 코드로 객체화하여 그래프를 생성하는 방법 )
여기서 각 객체를 선언 및 사용하는 부분에서는 Graphviz 모듈을 사용하지 않지만 .render() 메소드 부분에서는 직접적으로 Graphviz 의 모듈 중 하나인 dot 명령어를 사용하므로 Graphviz 를 프로젝트가 돌아갈 운영체제에 설치한다.
⬇️ MAC, Linux Graphviz 설치 방법 ⬇️
# ⬇️ MAC 설치 ⬇️
brew install graphviz
# ⬇️ Linux 설치 (Debian 계열 - Ubuntu) ⬇️
sudo apt install graphviz
# ⬇️ Linux 설치 (Redhat 계열) ⬇️
sudo dnf install graphviz
그리고 코드를 실행시켜주기만 하면 되는데 ( dot is not defined ) 오류가 이클립스의 콘솔에서 발생하였고 Graphviz 를 설치했음에도 이클립스에서 프로젝트를 실행할 경우 내장된 dot 명령어를 찾지 못해 환경변수 설정까지 진행을 시켜봤지만 동일한 오류가 계속 발생하였다.
( 이클립스 프로그램 내의 환경변수 설정도 지정해줬지만 동일하게 dot 명령어를 찾지 못했다. )
혹시 몰라 터미널에서 dot -V 명령어를 통해 dot 명령어가 정상적으로 실행되는지 확인해보았지만 터미널에서는 정상작동하는 것을 볼 수 있었다.
그래서 다음 방법을 찾아보다가 아래와 같은 방법으로 구현을 진행하게 되었다.
2️⃣ 자바에서 shell 명령어를 사용하여 구현
public class GraphExample {
public static void main(String[] args) {
ArrayList<String> routerTypeList = new ArrayList<>();
ArrayList<String> currentPathList = new ArrayList<>();
String command = "export PATH=\"$PATH:/usr/local/bin\"; echo 'digraph {";
try {
for (int i = 0; i < 앱이동경로MapList.size(); i++) {
routerTypeList.add(앱이동경로MapList.routerType);
// 쉘에서는 경로를 나타내는 / 를 사용하면 오류를 뱉기 때문에
// 이동경로(ex. /home) 과 같은 \ 를 추가하여 이스케이프 문자로 처리함
if(앱이동경로MapList.currentPath.charAt(0) == '\\') {
currentPathList.add('\"' + 앱이동경로MapList.currentPath + '\"');
}else {
currentPathList.add('\"' + 앱이동경로MapList.currentPath + '\"');
}
}
for(int i=0; i<currentPathList.size(); i++) {
if(i==0) {
// 앱 내 이동경로가 1개인 경우 경로를 추가하지 않고 node 하나만 출력하기 위해
// 커맨드에 해당 노드 경로만 추가
if(currentPathList.size() == 1) {
command += currentPathList.get(0);
}
continue;
}else {
// 만약 커맨드 내에 노드 -> 노드(ex. a -> b) 이동이 이미 중복 선언되어 있다면
// 중복 이동 그래프를 그리는 것을 방지하기 위해 커맨드를 추가하지 않음
// 이 부분을 제거하면 모든 중복 경로이동의 화살표가 표현됨
if(!command.contains(currentPathList.get(i-1) + "->" + currentPathList.get(i) + " ")) {
command += (currentPathList.get(i-1) + "->" + currentPathList.get(i) + " ");
}
}
}
// svg 파일을 생성할때 사용할 랜덤한 uuid 를 생성
// API 요청은 비동기로 작동하기 때문에 중복 요청의 경우의 수를 생각
String uuid = UUID.randomUUID().toString();
// sh shell 로 전송할 커맨드를 작성
// -Tsvg 옵션으로 svg 파일로 그래프를 생성
// -Nshape=rect 로 모든 노드를 사각형으로 표현
// -o {파일명} 을 통해 파일의 이름 선언
command += "}' | dot -Tsvg -Nshape=rect -o " + uuid + ".svg;";
// 명령어를 실행할 ProcessBuilder 객체를 생성
ProcessBuilder builder = new ProcessBuilder();
String homeDirectory = System.getProperty("user.dir");
log.debug("##### homeDirectory ::: {}", homeDirectory);
File graphDir = new File(homeDirectory, "graph");
if(!graphDir.exists()) {
if(!graphDir.mkdir()) {
log.error("##### fail to mkdir ::: {}", graphDir.getAbsolutePath());
}
}
builder.directory(graphDir);
// sh 쉘로 커맨드 실행
builder.command("sh", "-c", command);
Process process = builder.start();
// 에러 발생 시 커스텀된 StreamGobbler 를 사용하여 sh shell 내의 에러를 읽어
// 콘솔에 출력함
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), System.err::println);
Executors.newSingleThreadExecutor().submit(errorGobbler);
log.debug("##### success make graph ::: {}/{}.svg", graphDir.getAbsolutePath(), uuid);
int exitCode = process.waitFor();
assert exitCode == 0;
// 생성된 파일을 읽어와 Base64 로 인코딩하여 문자열로 변환한 뒤 응답
File graphFile = new File(graphDir, uuid + ".svg");
if(graphFile.exists()) {
try {
byte[] graphContent = Files.readAllBytes(graphFile.toPath());
String encodedGraph = Base64.getEncoder().encodeToString(graphContent);
message = encodedGraph;
}catch(IOException e) {
message = "";
e.printStackTrace();
}
}else {
message = "";
}
// 생성된 svg 파일을 삭제 및 삭제되지 않은 경우 log 출력
if(!graphFile.delete()) {
log.debug("##### fail to delete graphFile ::: {}", graphFile.getAbsolutePath());
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
message = e.getMessage();
} finally {
}
return returnJson(message);
}
}
( 그래프 생성 코드 )
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;
public class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;
public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumer);
}
}
( 쉘 내 에러를 받아올 StreamGobbler 커스텀 )
⬇️ 결과물 ⬇️

'TIL' 카테고리의 다른 글
[TIL] 2024.01.07 - Thymeleaf 를 사용해서 백오피스 구현해보기 1 ( 로그인 구현 ) (1) | 2025.01.07 |
---|---|
[TIL] 2025.01.03 - 서버 실행 시 관리자 Default 관리자 계정 생성하기 (0) | 2025.01.03 |
[TIL] 2024.12.30 - JDBC 드라이버 표준 시간대 설정 (0) | 2024.12.30 |
[TIL] 2024.12.26 - AuthenticationManager stackOverFlow 문제 해결(feat. 커밋로그) (1) | 2024.12.26 |
[TIL] 2024.12.25 - Nginx 를 사용해 React 세부 페이지 서빙하기 (2) | 2024.12.25 |