db.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. package service
  2. import (
  3. "crawler/config"
  4. "strings"
  5. "time"
  6. "database/sql"
  7. "fmt"
  8. "gorm.io/driver/mysql"
  9. "gorm.io/gorm"
  10. )
  11. var (
  12. DB_MOVIEW DB
  13. DB_CRAWLER DB
  14. DB_PLAYR DB
  15. )
  16. type DB struct {
  17. SQLDB *sql.DB
  18. GORMDB *gorm.DB
  19. DSN string
  20. Err error
  21. }
  22. // 2025.08.08 비동기 처리를 위한 수정
  23. type logItem struct {
  24. Action int
  25. Query string
  26. Comment string
  27. }
  28. var (
  29. logChannel = make(chan logItem, 10000)
  30. flushN = 200 // 200개 쌓이면 flush
  31. flushT = 250 * time.Millisecond // 250ms마다 flush
  32. stopChannel = make(chan struct{})
  33. stoppedChannel = make(chan struct{})
  34. )
  35. func InitLogWorker(db *sql.DB) {
  36. go func() {
  37. defer close(stoppedChannel)
  38. buf := make([]logItem, 0, flushN)
  39. ticker := time.NewTicker(flushT)
  40. defer ticker.Stop()
  41. for {
  42. select {
  43. case item := <-logChannel:
  44. buf = append(buf, item)
  45. if len(buf) >= flushN {
  46. flushLogs(db, buf)
  47. buf = buf[:0]
  48. }
  49. case <-ticker.C:
  50. if len(buf) > 0 {
  51. flushLogs(db, buf)
  52. buf = buf[:0]
  53. }
  54. case <-stopChannel:
  55. for {
  56. select {
  57. case it := <-logChannel:
  58. buf = append(buf, it)
  59. default:
  60. if len(buf) > 0 {
  61. flushLogs(db, buf)
  62. }
  63. return
  64. }
  65. }
  66. }
  67. }
  68. }()
  69. }
  70. func StopLogWorker() {
  71. close(stopChannel) // stopChannel를 닫아 더 이상 logItem을 받지 않도록 함
  72. <-stoppedChannel // flush 끝날 때까지 대기
  73. }
  74. // 다중 입력 SQL 생성
  75. func flushLogs(db *sql.DB, items []logItem) {
  76. var sb strings.Builder
  77. sb.WriteString(`INSERT INTO tb_general_log (action, query, comment, created_at) VALUES `)
  78. args := make([]any, 0, len(items)*3)
  79. for i, it := range items {
  80. if i > 0 {
  81. sb.WriteByte(',')
  82. }
  83. sb.WriteString("(?, ?, ?, NOW())")
  84. args = append(args, it.Action, it.Query, it.Comment)
  85. }
  86. if _, err := db.Exec(sb.String(), args...); err != nil {
  87. fmt.Println("log flush error:", err)
  88. }
  89. }
  90. func SetConfig() config.DBAccount {
  91. var (
  92. env = config.Env
  93. db = config.DB
  94. account = config.DBAccount{}
  95. )
  96. // DB 환경설정
  97. switch env.DeveloperEnv {
  98. case config.LOCAL:
  99. account = db.Local
  100. case config.DEV:
  101. account = db.Dev
  102. default:
  103. fmt.Println("DB 설정이 잘못되었습니다.")
  104. }
  105. //fmt.Printf("\n%#v\n", account)
  106. return account
  107. }
  108. // DB 연결
  109. func SetDatabase(dbName string) DB {
  110. var (
  111. env = config.Env
  112. db = config.DB
  113. account = SetConfig()
  114. err error
  115. )
  116. if dbName != "" {
  117. account.Name = dbName
  118. }
  119. /*
  120. * sqlDB
  121. */
  122. dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s", account.User, account.Password, account.Address, account.Name)
  123. fmt.Printf("\nEnv : %s\n", env.DeveloperEnv)
  124. fmt.Printf("%s\n", dsn)
  125. SQLConnd, err := sql.Open(account.Driver, dsn)
  126. // sql.Open 은 실제 접속을 하지 않으므로 Ping 으로 확인한다.
  127. // Ping 에러를 err 에 담아, 아래에서 err 이 nil 이 아님을 보장한다. (기존: err.Error() nil 역참조 패닉)
  128. if err == nil {
  129. err = SQLConnd.Ping()
  130. }
  131. if err != nil {
  132. fmt.Printf("driver : %s\n", account.Driver)
  133. fmt.Printf("dsn : %s\n", dsn)
  134. fmt.Printf("sqlDB에 연결에 실패하였습니다.\n %v\n", err)
  135. if SQLConnd != nil {
  136. SQLConnd.Close()
  137. }
  138. return DB{}
  139. }
  140. SQLConnd.SetConnMaxLifetime(time.Duration(db.MaxLifetime) * time.Second)
  141. SQLConnd.SetConnMaxIdleTime(time.Duration(db.MaxIdleTime) * time.Second)
  142. SQLConnd.SetMaxIdleConns(db.MaxIdleConn)
  143. SQLConnd.SetMaxOpenConns(db.MaxOpenConn)
  144. /*
  145. * GormDB
  146. */
  147. GORMConnd, err := gorm.Open(mysql.New(mysql.Config{
  148. Conn: SQLConnd,
  149. }), &gorm.Config{})
  150. if err != nil {
  151. fmt.Printf("GormDB에 연결에 실패하였습니다.\n %v", err.Error())
  152. fmt.Println(err)
  153. }
  154. new := new(DB)
  155. new.SQLDB = SQLConnd
  156. new.GORMDB = GORMConnd
  157. new.DSN = dsn
  158. new.Err = err
  159. fmt.Println("Database was opened successfully !!")
  160. return *new
  161. }
  162. // DB 연결
  163. func (db *DB) Connection(dbName string) DB {
  164. return SetDatabase(dbName)
  165. }
  166. // gorm 커넥션 연결
  167. func (db *DB) GConnection(dbName string) DB {
  168. return SetDatabase(dbName)
  169. }
  170. // DB 오류 입력
  171. func (db *DB) SetErrorLog(err error, query string) {
  172. var (
  173. conn = DB_CRAWLER.SQLDB
  174. sql = `CALL SP_ERROR_LOG(?, ?, ?);`
  175. errorMessage = strings.TrimSpace(strings.Split(err.Error(), ":")[1])
  176. errorNo = strings.TrimSpace(strings.Replace(strings.Split(err.Error(), ":")[0], "Error ", "", 1))
  177. )
  178. conn.Exec(sql, errorNo, errorMessage, query)
  179. switch errorMessage {
  180. case "sql: no rows in result set":
  181. case "":
  182. default:
  183. fmt.Printf("DB error msg : %s\n", err.Error())
  184. }
  185. fmt.Printf("Exe error query : %s\n", query)
  186. }
  187. /*
  188. * DB SQL 입력
  189. * 2025.08.08 비동기 처리를 위한 수정으로 미사용
  190. */
  191. /*
  192. func (db *DB) SetGeneralLog(action int, query, comment string) {
  193. var (
  194. conn = DB_CRAWLER.SQLDB
  195. sql = `
  196. INSERT INTO tb_general_log
  197. SET
  198. action = ?,
  199. query = ?,
  200. comment = ?,
  201. created_at = NOW();
  202. `
  203. )
  204. _, err := conn.Exec(sql, action, query, comment)
  205. if err != nil {
  206. db.SetErrorLog(err, sql)
  207. }
  208. }
  209. */
  210. func (db *DB) SetGeneralLog(action int, query, comment string) {
  211. if len(query) > 4000 {
  212. query = query[:4000] + "…(truncated)" // 너무 긴 쿼리는 잘라서 저장 (행 크기/페이지 split 완화)
  213. }
  214. select {
  215. case logChannel <- logItem{action, query, comment}:
  216. default:
  217. // 큐가 가득하면 발생
  218. }
  219. }
  220. // DB 연결 종료
  221. func (db *DB) Close() {
  222. defer func() {
  223. if err := recover(); err != nil {
  224. fmt.Printf("Database recovered message: %s\n", err)
  225. }
  226. }()
  227. if DB_MOVIEW != (DB{}) {
  228. defer DB_MOVIEW.SQLDB.Close()
  229. DB_MOVIEW.SQLDB = nil
  230. DB_MOVIEW.GORMDB = nil
  231. DB_MOVIEW.DSN = ""
  232. DB_MOVIEW.Err = nil
  233. }
  234. if DB_CRAWLER != (DB{}) {
  235. defer DB_CRAWLER.SQLDB.Close()
  236. DB_CRAWLER.SQLDB = nil
  237. DB_CRAWLER.GORMDB = nil
  238. DB_CRAWLER.DSN = ""
  239. DB_CRAWLER.Err = nil
  240. }
  241. if DB_PLAYR != (DB{}) {
  242. defer DB_PLAYR.SQLDB.Close()
  243. DB_PLAYR.SQLDB = nil
  244. DB_PLAYR.GORMDB = nil
  245. DB_PLAYR.DSN = ""
  246. DB_PLAYR.Err = nil
  247. }
  248. fmt.Println("DB closed")
  249. }