IT

GoLang 과 Javascript 를 이용해서 데이터베이스를 실시간 트리거(감시?) 하기

번개애비 2021. 9. 29. 22:29

보통 MySQL 트리거의 경우, SQL문으로 어떤 쿼리를 실행할 수 있지만,

실제 애플리케이션단에서 최신화 데이터를 감지하여 따로 조치하는 경우에서는 MySQL에서 제공하는 트리거를 활용하기 난감하다.

결국 Javascript Websocket을 통해 Go 프로그램과 통신을 하고, Go 가 실시간으로 데이터베이스의 변화를 감지하여 반환하는 형태로 구현하였다. (참고로 가이드코드임으로 실서비스 적용시 코드튜닝이 필요함.)

간혹, SetTimeOut을 이용하여 Ajax로 구현된 경우가 있는데 서버 부하나 속도측면에서 상당히 불리함으로 이러한 실시간 대응에서는 가급적 WebSocket을 사용하는것을 권장한다.

 

아래는 실행화면 :=

대략 5초단위로 최신화 데이터 여부를 반환해준다.

 

 


통신은 다음과 같이 이뤄진다.

 

1. 클라이언트가 소켓을 연결함과 동시에 테이블명을 서버로 전달한다.

2. 서버는 해당 클라이언트와 연결된 이후 테이블명을 전달받아 소켓연결 시점을 기준으로 최신화 데이터를 해당 테이블에서 검색한다.

3. 최신화 데이터일경우 1 을 반환하고 오류 혹은 SQL 구문오류시 0을 반환한다.


 

 

 

먼저 Javascript 소스코드이다.

함수형태로 되어 있고, 최신화데이터를 수신받으면 다른 행동을 할 수 있도록 콜백도 선언이 가능하다.

소스내 __WSS_SERVER__ 는 소켓서버의 주소로 넣어줘야하며, tablenm에는 기본값을 넣어두었다.

(사용전 데이터베이스에 맞춰 수정필요)

var dbtrigger = function(tablenm = null, success_callback = null){
	var autoreboot = true;
	if(tablenm == null){
		tablenm = 'sid';
	}
	let websocket = new WebSocket(__WSS_SERVER__+'/dbtrigger');
	websocket.onopen = function(evt) {
		websocket.send(tablenm);
	};
	websocket.onmessage = function(evt) {
		if(evt.data){
			if(evt.data == 1){
				autoreboot = false;
				websocket.close();
				if(success_callback != null){
					success_callback();
				}
			}
			if(evt.data == 0){
				autoreboot = false;
				console.log('DB Trigger ERROR');
				websocket.close();
			}
		}
	};
	websocket.onclose = function(evt) {
		if(autoreboot){
			console.log('reconnect dbtrigger');
			dbtrigger(tablenm, success_callback);
		}
	};
	websocket.onerror = function(evt) {
		websocket.close();
	};
};

 

 

Javascript 함수 사용예)

//order테이블을 감시하고 최신화데이터가 존재시 order_search()함수를 실행한다.
dbtrigger('orders',function(){
	order_search();
});

 

데이터베이스의 테이블명에 클라이언트단에서 노출되는 치명적인 단점이 존재하지만, 약간의 암/복호화만 한다면 실서비스에서도 충분히 적용이 가능할것으로 보인다.

 

 

 

다음 GoLang 소스이다 :=

중간중간에 log를 주석을 달아두었음으로 주석을 제거하여 진행을 확인할 수 있다.

소스내부의 쿼리문의 경우 데이터베이스 구성에 따라 수정해서 사용해야한다.

package main

import (
	"fmt"
	"log"
	"time"
	"net/http"
	"github.com/gorilla/websocket"
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)

//데이터베이스 설정
var DB, err = sql.Open("mysql", "서버정보")

func main() {
	
	//소켓 핸들링함수 선언
	http.Handle("/", http.FileServer(http.Dir("static")))
	http.HandleFunc("/dbtrigger", socketHandler)
	
	//서버포트설정 (실제로는 proxy를 통해 443 포트를 사용함)
	port := "5000"
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

//버퍼
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return  true
	},
}

// /dbtrigger 연결시
func socketHandler(w http.ResponseWriter, r *http.Request) {
	
	//커넥션정보
	conn, err := upgrader.Upgrade(w, r, nil)
	defer conn.Close()
	if err != nil {
		//log.Printf("[ERROR]upgrader: %v", err)
		return
	}
	
	

	for {
		
		//메시지 읽기
		messageType, p, err := conn.ReadMessage()
		if err != nil {
			//log.Printf("[ERROR]ReadMessage: %v", err)
			return
		}
		if p != nil{
			//메시지가 도착할경우 테이블이름 받아오기
			tablenm := string(p)
			//log.Printf("Trigging tablenm : %s", tablenm)
			
			//현재시간 측정
			now := time.Now().Format("2006010215040599999")
			//log.Printf("This data : %s", now)
			
			//현재시간 이후로 발생된 최신 데이터 찾기
			for{
				//5초마다 DB조회
				time.Sleep(5 * time.Second)
				
				//sid 테이블에서 요청한 테이블명중에 현재시간 이후 최신 데이터 찾기
				var sid int
				sqlRaw := fmt.Sprintf("SELECT sid FROM sid WHERE sid > %s AND TABLES = '%s' AND LOCKCOLUMN = 0 ORDER BY sid DESC LIMIT 1", now, tablenm)
				rows, err := DB.Query(sqlRaw)
				if err != nil {
					//실패
					var str string = "0"
					var bytes []byte
					bytes = []byte(str)
					if err := conn.WriteMessage(messageType, bytes); err != nil {
						//log.Printf("conn.WriteMessage: %v", err)
						return
					}
				}
				defer rows.Close()
				for rows.Next() {
					err := rows.Scan(&sid)
					if err != nil {
						//실패
						var str string = "0"
						var bytes []byte
						bytes = []byte(str)
						if err := conn.WriteMessage(messageType, bytes); err != nil {
							//log.Printf("conn.WriteMessage: %v", err)
							return
						}
					}
					//데이터가 존재할 경우
					if sid != 0 {
						//log.Println("find new data :",sid)
						
						//성공
						var str string = "1"
						var bytes []byte
						bytes = []byte(str)
						if err := conn.WriteMessage(messageType, bytes); err != nil {
							//log.Printf("conn.WriteMessage: %v", err)
							return
						}
						
					}
					
				}
				
			}
			
			
		}
		
	}
}

 

 

 

Go 소스에서는 포트번호가 5000번 포트를 사용하고 있는데, 필자의 경우 앞단 Nginx 서버에서 SSL과 Proxy로 처리하여 443포트를 사용하여 불필요한 포트낭비를 최소화하고 차후 부하분산에 쉽게 대응할 수 있도록 조치해두었다. 

아래는 Nginx 가상호스트 셋팅의 예이다. (SSL은 서버환경에 맞게 다시 설정필요함)

upstream ws {
    server 127.0.0.1:5000;
}

server {
    server_name  도메인;
    location / {
      proxy_pass http://ws;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header Host $host;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/도메인/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/도메인/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    add_header Strict-Transport-Security "max-age=31536000" always;

}

 

 

근래에 들어서, 특히 대규모서비스로 갈수록 Vanilla JS로 리팩토링하는 프로젝트가 많아지고 있기도 하고, 점차 XHR 통신보다는 Socket으로 변화되고 있는 추세이다.

뭔가 하드코어하면서 프레임웍 없이 바닥부터 짜는 느낌이라 원시인이 된 것 같은 느낌인데, 돌이켜보면 결국 쉬운길은 없었다.

개발자들이 편의를 위해 만들어진 프레임워크를 사용하는것도 좋지만 결국엔 본질을 잘 이해하고 활용할 수 있어야만 살아남는것 같다.

(물론 단기간 내 빠르게 처내야하는 프로젝트는 논외)