728x90
반응형

계기

개발 진행 중, 체인 데이터가 깨지는 이슈가 발생하였으나 컨테이너의 health 체크 및 재시동에 이슈가 발생하였음
현재는 본인이 3년전에 세팅한 Docker Swarm 으로 컨테이너 오케스트레이션 중이나, 서비스 고도화로 인한 오케스트레이션해야 할 컨테이너가 다양해지고 많아짐
컨테이너 health 상태 체크 및 정상화에 대한 기능이 필요해짐
서비스 운영시의 서버 문제 발생 및 리소스 문제 대처를 위한 Auto Scaling 기능 역시 필요
이외에 무중단 배포 전략 수행을 위한 옵션 역시 필요함
이를 해결하기 위한 방안에 대하여 고민중, 회사에 제안할 내용의 초안을 작성해본다.


현 상태

  • Docker Swam을 통한 노드 컨테이너 오케스트레이션이 수행되고 있음
    • 오케스트레이션 대상이 블록체인 노드 8개 밖에 없었기에 kubernetes를 사용할 필요가 없어 보였음
  • 그러나 관리 및 오케스트레이션 해야 할 대상이 많고 다양해짐
    • 노드
    • Explorer
    • 어플리케이션들
      • 거래소
      • 지갑
    • DB
    • 게이트웨이
    • 모니터링 시스템
      • 자체 제작 Collector 및 대시보드
      • Prometheus
      • Grafana
  • Auto-Scaling, 리소스 할당에 대한 필요성 대두
  • 무중단 배포 전략 도입의 필요성 확인

제안

  • 컨테이너 오케스트레이터(Orchestrator)를 Kubernetes로 변경
    • 더욱 안정적 시스템 운영
    • Auto Scaling

구상

  • 노드 구성
    • Master
    • Worker Nodes
      • Explorer
      • Chain Nodes - 7 nodes
      • DataBase
      • Monitoring
      • Trade Application
      • Wallet Application
  • Pods 단위
    • Explorer pod
    • Blockchain Nodes pod
    • Database pod
    • Services
      • Trade Application
      • Wallet Application
      • etc...
    • Monitoring pod
      • Prometheus
      • Grafana
      • 자체 제작 Collecter, Dashboard
728x90
반응형
728x90
반응형

계기

최근 새로운 솔루션 개발 및 데모를 위한 과정에서 많은 이슈들을 경험하였다.
이를 과정에서 현 개발 환경 및 서버 환경의 분리가 필요함을 느꼈다.
때문에 회사에 새로운 서버 구성을 제안하기 위한 초안을 정리해 본다.

현 상태

* 개발 서버에서 모든 서비스의 배포와 테스트가 진행되고 있음
* 로컬 환경에서도 개발 서버의 DB를 보며 개발을 진행하고 있으며, 데모 시에 사용하는 DB 역시 개발 서버에서 수행됨
    * 개발 진행 중 테이블 구조를 바꾸는 경우도 발생하여 데모에 이슈를 야기하기도 함
* 따라서 개발서버는 새로운 기능 배포 및 테스트 용도
* QA 서버를 따로 배포하여, 운영 서버와 동일한 환경에서 데모 및 QA 수행이 필요해 보임

제안

* 운영 서버
* 개발 서버
    * 개발 수행 및 기능 테스트를 위한 서버
* QA 서버 (스테이징 서버)
    * 운영서버와 동일한 환경으로 세팅하여, 운영서버 배포 전 QA 용도로 사용
    *  QA 수행 및 데모 시의 문제를 확인함
        * DB 변동에 의한 데모 및 테스트 문제가 여러번 발생함
        * 로컬 환경에서도 개발 서버를 바라보기 때문에 더 문제가 발생
            * 따라서 DB, 서버, 클라이언트, Explorer(blockscout) 등을 배포해 테스트하는 서버 필요
728x90
반응형
728x90
반응형
  • 본 내용은 과거 번역 파일 정리 자동화 프로그램 작업에 대한 업무 일지이다.

Properties 파일 구조

  • properties 파일: <단어> = <번역된 단어> 형태로 관리됨
  • 기능에 따라 폴더가 구분되어있음. 이 폴더명을 back/<폴더명> 명으로 사용
    • 파일 위치 경로: /assets/backend/
  • ko.properties, en.properties로 파일이 나뉘어져 있다.
  • 두 파일은 같은 key를 공유하고 있기 때문에, 하나의 시트에 합쳐서 보여질 수 있게 한다.
    • key, ko, en 으로 칼럼을 잡았다.

코드

  • 번역 파일이 저장되어 있는 디렉토리 내의 파일들 조회
package read

// Import 내용들


// 디렉토리 내의 파일/폴더들 리스트 조회
func ReadDir(dir string) []os.DirEntry {
    entries, err := os.ReadDir(dir)

    if err != nil {
        log.Fatalf("Failed to Read Dir Error: %v", err)

        return nil
    }

    return entries
}

// 파일 읽기
func ReadFile(fileDir string) ([]fs.DirEntry, string) {

    fileList, readErr := os.ReadDir(fileDir)

    if readErr != nil {
        log.Printf("Can't Read json file: %v", readErr)
        return nil, fileDir
    }

    return fileList, fileDir
}
  • Property 파일 내의 데이터 처리하여 저장할 수 있게 가공
package read

// Import 내용들

type PropertyStruct struct {
    Code string
    Ko   string
    En   string
}

func ReadProperties(fileList []fs.DirEntry, pwd string) []PropertyStruct {
    var propertyDataDataList []PropertyStruct

    for _, file := range fileList {
        fileName := file.Name()

        fileNameWithoutFormat := strings.Split(fileName, ".properties")[0]

        if fileNameWithoutFormat == "example" {
            continue
        }

        fileNameLength := len(fileNameWithoutFormat)
        langCode := fileNameWithoutFormat[fileNameLength-2 : fileNameLength]

        if langCode == "en" || langCode == "ko" {
            // 파일 열기
            file, err := os.Open(pwd + "/" + fileName)
            if err != nil {
                log.Printf("[Properties] data Open Error: %v", err)
            }

            defer file.Close()
            // 스캐너로 파일 한 줄씩 읽기
            scanner := bufio.NewScanner(file)

            for scanner.Scan() {
                line := scanner.Text()

                // 빈 줄 또는 주석(#, ;) 무시
                if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
                    continue
                }
                // key=value 포맷으로 나누기
                parts := strings.SplitN(line, "=", 2)
                if len(parts) == 2 {
                    key := strings.TrimSpace(parts[0])
                    value := strings.TrimSpace(parts[1])

                    if langCode == "ko" {
                        convertedString, convTerr := utils.DecodeUnicodeEscapes(value)

                        if convTerr != nil {
                            log.Printf("Convert To Utf8 Err: %v", convTerr)
                        }
                        value = convertedString
                    }

                    isFound := false

                    for i := range propertyDataDataList {
                        data := &propertyDataDataList[i]

                        if data.Code == key {
                            isFound = true

                            switch langCode {
                            case "en":
                                data.En = value
                            case "ko":
                                data.Ko = value
                            }
                            break // 값을 찾았으므로 루프를 종
                        }
                    }

                    if !isFound {
                        propertyData := PropertyStruct{Code: key} // 새 구조체 생성
                        switch langCode {
                        case "en":
                            propertyData.En = value
                        case "ko":
                            propertyData.Ko = value
                        }
                        propertyDataDataList = append(propertyDataDataList, propertyData)
                    }

                }
            }

            if err := scanner.Err(); err != nil {
                log.Printf("Scan Error: %v", err)
            }
        }

    }

    return propertyDataDataList
}
  • 처리한 데이터들을 받아 액셀파일 생성
package write

// Import 내용들


type FileWork struct {
    file        *excelize.File
    FileName    string
    SheetLength int
}

// 칼럼 세팅
func (excelFile *FileWork)HandlePropertiesColumns () {
    excelFile.file.SetCellValue(sheetName, "A1", "Code")
    excelFile.file.SetCellValue(sheetName, "B1", "Ko")
    excelFile.file.SetCellValue(sheetName, "C1", "En")
}

// 데이터 레코드 입력
func (excelFile *FileWork)HandlePropertiesRows(dataList []read.PropertyStruct) {
    for i := 0; i <= len(dataList)-1; i += 1 {
        excelFile.file.SetCellValue(sheetName, fmt.Sprintf("A%d", i+2), dataList[i].Code)
        excelFile.file.SetCellValue(sheetName, fmt.Sprintf("B%d", i+2), dataList[i].Ko)
        excelFile.file.SetCellValue(sheetName, fmt.Sprintf("C%d", i+2), dataList[i].En)
    }
}

// 액셀 파일 저장
func (file *FileWork) SaveExcelFile() {
    savingErr := file.file.SaveAs(file.FileName)
    if savingErr != nil {
        log.Printf("Saving File Error: %v", savingErr)
    } else {
        log.Printf("Saving Success: %s", file.FileName)
    }
}

func OpenSaveExcelWithProperties(sheetName string, index int, excelFileName string, pwd string, dataList []read.PropertyStruct) {
    file := HandleFiles(sheetName, index, excelFileName, pwd, dataList)

    sheetIndex := file.HandleSheet(sheetName)

    log.Printf("Handled Sheet Index: %d", sheetIndex)

    file.file.HandlePropertiesColumns()

    file.HandlePropertiesRows(dataList)

    file.SaveExcelFile()
}
  • property 파일들이 저장되어 있는 디렉토리 내의 폴더와 파일 리스트 내에서 property 파일들만 읽어 처리하기
package libraries

// Import 내용들

// 위의 함수들 호출하여 properties 파일 처리 및 액셀 파일 생성
func CreatePropertiesTranslationData(fileName string, pwd string) {
    entries := read.ReadDir(pwd + "/assets/backend")

    log.Printf("[PROPERTIES] Entry Length: %d", len(entries))

    for i, folderData := range entries {
        folderName := folderData.Name()

        var propertyFileList []fs.DirEntry
        var proPwd string

        // 리드미 파일 혹은 예시 파일들은 제외
        if folderName == "README.md" || folderName == "example.properties" {
            continue
        }

        // 디렉토리인지 파일인지 여부 체크하여 분기처리
        if folderData.IsDir() {
            log.Printf("[PROPERTIES] Folder Name: %s", folderName)

            propertyFileList, proPwd = read.ReadFile(pwd + "/assets/common/" + folderName)

            if propertyFileList == nil {
                log.Printf("[PROPERTIES] The File is not readable: %s", proPwd)
                continue
            }
        } else {
            log.Printf("[PROPERTIES] Common File Name: %s", folderName)

            propertyFileList, proPwd = read.ReadFile(pwd + "/assets/common")

            if propertyFileList == nil {
                log.Printf("[PROPERTIES] The File is not readable: %s", proPwd)
                continue
            }

            folderName = "default"
        }

        fileDataList := read.ReadProperties(propertyFileList, proPwd)
        log.Printf("File Data List: %d", len(fileDataList))

        write.OpenSaveExcelWithProperties(folderName, i, fileName, proPwd, fileDataList)
    }
}

정리

  1. 디렉토리 내의 번역 파일 / 하위 디렉토리를 조회
  2. 번역 파일 / 하위 디렉토리 내부의 번역 파일 읽기
  3. 데이터를 파싱하여 원하는 형태로 나눠 구조체 안에서 관리
  4. 액셀 파일 생성

문제해결

  • 디렉토리 내부의 하위 디렉토리를 따로 감지하여 파일 읽는 방법
    • golang에 디렉토리 여부를 체크할 수 있는 기능이 내장되어 있음
  • 함수의 분리
    • 로직을 최소 기능 단위로 분리하여 함수 먼저 작성
    • 함수 Receiver를 이용해 클래스 내부 메서드처럼 활용할 수 있게 작성
  • 에러 발생 시 처리
    • 요구사항 상, 프로그램이 종료 되기보다 에러 발생 부분은 건너뛰고 처리
    • 로그를 확인하여 해당 부분은 매뉴얼하게 체크하는 방식을 선택함
728x90
반응형
728x90
반응형

JSON 파일 읽기

  • json 파일은 /assets/front 디렉토리 아래에 위치한다.
  • 파일은 한국어 번역 파일인 ko.json 과 en.json으로 구성됨
  • json 형식의 번역 파일은 프론트에서 다국어 처리를 위해 사용된다.
func ReadJson(fileName string) ([]byte, error) {
    // 현재 프로젝트 루트 디렉토리
    pwDir, getPwdErr := os.Getwd()

    // 에러 발생 시, 에러 리턴. 에러 발생 파일은 스킵할 수 있게 하기 위하여 리턴함
    if getPwdErr != nil {
        log.Printf("[READ_JSON] Get Pwd Error: %v", getPwdErr)
        return []byte{}, getPwdErr
    }

    file, readErr := os.ReadFile(pwDir + "/assets/front" + fileName)

    if readErr != nil {
        log.Printf("[READ_JSON] Read File Error\nFileName: %s, Error: %v", fileName, getPwdErr)
        return []byte{}, readErr
    }

    return file, nil
}

JSON 데이터 파싱

  • 파일명에 따라 한국어 / 영어로 구분
  • json 파일의 형식은 '{First: {Second: { Third: {"단어": "번역내용"}}}}' 의 형식으로 되어있다.
    • Third 에서 depth가 더 들어가는 케이스도 있음
type JsonRowStruct struct {
    First string
    Second string
    Third string
    Fourth string
}

func ParseJsonData(jsonData []byte) (map[string]interface{}, error) {
    // json 파일 데이터 매핑할 맵 변수 선언 -> 키값 아래에 데이터를 매핑시키는 방식으로 관리
    var jsonDataMap map[string]interface{}

    // json 데이터를 위에서 선언한 Map에 넣어주기
    unMarshalErr := json.Unmarshal(jsonData, &jsonDataMap)

    if unMarshalErr != nil {
        log.Printf("[PARSE_JSON] unmarshal json error: %v", unMarshalErr)
        return map[string]interface{}, unMarshalErr
    }

    return jsonDataMap, nil
}

파싱된 JSON 데이터 평탄화

  • 이러한 Depth 들을 평탄화 시켜줘야 액셀 파일에 넣기 용이함
    • 평탄화 하지 않으면 depth 별로 for 문을 돌려야 하고, depth 길이에 따른 동적 처리가 어려워 졌었음
// 액셀 파일 데이터 구조체
type FileWork struct {
    file        *excelize.File
    FileName    string
    SheetLength int
}

// 이미 한글/영어 데이터가 입력 되었을 때, 코드 값을 기준으로 데이터를 입력할 위치 탐색
func (excelData *FileWork)FindCode(sheetName string, rowIndex int, majorDepth , secondDepth, thirdDepth, fourthDepth string) bool {
    // 각 깊이별로 셀 값을 가져옴
    cellFirst, _ := excelData.file.GetCellValue(sheetName, fmt.Sprintf("A%d", rowIndex))
    cellSecond, _ := excelData.file.GetCellValue(sheetName, fmt.Sprintf("B%d", rowIndex))
    cellThird, _ := excelData.file.GetCellValue(sheetName, fmt.Sprintf("C%d", rowIndex))
    cellFourth, _ := excelData.file.GetCellValue(sheetName, fmt.Sprintf("D%d", rowIndex))

    // 데이터 코드 비교
    if cellFirst == majorDepth 
      && cellSecond == secondDepth 
      && cellThird == thirdDepth 
      && cellFourth == fourthDepth {
          return true
      }

        return false
}


// 데이터 평탄화
func (excelData *FileWork)FlattenJsonData(sheetName string, parsedData map[string]interface{}) {
    switch v := data.(type) {
    case map[string]interface{}:
        for key, value := range v {
            newPrefix := prefix

            if prefix != "" {
                newPrefix += "."
            }

            newPrefix += key

            // map value의 데이터 역시 depth가 있을 수 있기 때문에 평탄화 시킨다
            excelData.flattenKorJSON(sheetName, value)
        }
     // 문자로 들어왔으면 카테고리 구분하여 처리 - 이미 front 시트에 데이터가 들어가 있을 때
    case string:
        depths := strings.Split(prefix, ".")
        firstDepth := ""
        secondDepth := ""
        thirdDepth := ""
        fourthDepth := ""

        if len(depths) > 0 {
            firstDepth = depths[0]
        }
        if len(depths) > 1 {
            secondDepth = depths[1]
        }
        if len(depths) > 2 {
            thirdDepth = depths[2]
        }
        if len(depths) > 3 {
            fourthDepth = strings.Join(depths[3:], ".")
        }

        // 엑셀의 모든 행을 탐색하여 코드 일치 여부 확인
        rows, err := excelData.file.GetRows(sheetName)
        if err != nil {
            log.Printf("GetRows Error: %v", err)
            return
        }

        for rowIndex := range rows {
            // 첫 번째 행은 헤더이므로 건너뜀
            if rowIndex == 0 {
                continue
            }

            // 1번째 row는 칼럼 값들이므로, 2번째 줄 부터 데이터 입력(index+1)
            isCodeMatch := excelData.FindCode(sheetName, rowIndex+1, firstDepth, secondDepth, thirdDepth, fourthDepth)

            if isCodeMatch {
                // 'F' 열에 값 삽입
                excelData.file.SetCellValue(sheetName, fmt.Sprintf("E%d", rowIndex+1), v)
                break // 매칭되는 첫 번째 행에만 값을 삽입하고 루프 종료
            }
        }

    default:
        // 다른 타입은 처리하지 않음
    }
}

액셀 파일 생성 및 데이터 저장

  • 데이터를 처리, 액셀 파일 저장하기 위해 github.com/xuri/excelize/v2 외부 패키지를 사용하였다.
  • 매핑된 데이터를 평탄화하여 저장한다.
# 패키지 설치
go get -u github.com/xuri/excelize/v2

// 번역 엑셀 파일 존재 여부 체크
func (excelData *FileWork)CheckIfFileExist() bool {
    _, err := os.Stat(excelData.fileName)

    return !os.IsNotExist(err)
}

// 파일 저장
func (excelData *FileWork) SaveExcelFile() {
    savingErr := excelData.file.SaveAs(excelData.FileName)
    if savingErr != nil {
        log.Printf("[EXCEL_SAVE] Saving File Error: %v", savingErr)
    } else {
        log.Printf("[EXCEL_SAVE] Saving Success: %s", excelData.file.FileName)
    }
}

// 칼럼 설정
func (excelData *FileWork) SetJsonColumns() {
    excelData.file.SetCellValue(sheetName, "A1", "First")
    excelData.file.SetCellValue(sheetName, "B1", "Second")
    excelData.file.SetCellValue(sheetName, "C1", "Third")
    excelData.file.SetCellValue(sheetName, "D1", "Ko")
    excelData.file.SetCellValue(sheetName, "F1", "En")
}

func (excelData *FileWork)WriteJsonExcelFile(parsedData map[string]interface[}) error {
    // 번역 파일 존재 여부에 따라 액셀 파일 여는 방식이 다르므로 미리 선언
    // 프로그램 실행 시 먼저 파일을 만들고, 항상 openFile 하는 방법으로 작성하였으나 여기서는 두가지 방식을 보여주기 위해 분리해서 보여줌    
    if CheckIfFileExist(excelData.fileName) {
        file, openErr := excelize.OpenFile(excelFilePath)

        if openErr != nil {
            log.Printf("[EXCEL_WRITE] Open Excel File for JSON Error: %v", openErr)
            // 파일 오픈 실패 시, 새로 파일 생성
            excelData.file = excelize.NewFile()
        }

        excelData.file = file
    } else {
        excelData.file = excelize.NewFile()
    }

    sheetIndex, indexErr := excelData.file.GetSheetIndex("front")

    if indexErr != nil {
        log.Printf("[EXCEL_WRITE] Failed to get Sheet Index for JSON error: %v", indexErr)
        return indexErr
    }

    excelData.SetJsonColumns()

    return nil
}
728x90
반응형
728x90
반응형
  • 본 내용은 과거 번역 파일 정리 자동화 프로그램 작업에 대한 업무 일지이다.

시작

  • 회사에서 서비스에 사용되는 다국어 데이터의 검수 및 정리를 위한 액셀 파일 필요
  • 수기로 작업하던 방식에 불만을 갖게 되어, 나도 모르게 자동화 프로그램을 만들어주겠다고 선언해 버렸다
  • 기능 정리를 해 보면 아래와 같다.
    • 번역 파일을 액셀파일로 만들고, 수정 뒤에 다시 액셀파일을 번역 파일 생성해 주는 프로그램 각각을 만들어야 할 것으로 판단하였음
    • 파일 뿐만 아니라, DB의 데이터도 처리해야 해야 했음
  • 복기하는 겸, 그 내용의 일지를 옮겨 적어본다.

액셀 파일 생성 프로그램 - 번역 파일 -> 하나의 액셀 파일

  • 다국어 데이터를 사용하는 서비스는 총 3개
  • 파일들은 백엔드에서 사용하는 파일 / 프론트에서 사용하는 파일 / DB 에서 사용하는 파일 각각은 front / back / DB 폴더 안에 위치
  • 프론트에서 사용하는 다국어는 json 파일로 관리되고, 백엔드에서는 .properties 파일과 DB로 관리됨
    • properties 파일: <단어> = <번역된 단어> 형태로 관리됨
      • 기능에 따라 폴더가 구분되어있음. 이 폴더명을 back_<폴더명> 명으로 사용
      • ko.properties, en.properties로 파일이 나뉘어져 있다.
      • 두 파일은 같은 key를 공유하고 있기 때문에, 하나의 시트에 합쳐서 보여질 수 있게 한다.
      • key, ko, en 으로 칼럼을 잡았다.
    • json 파일: {"key1": {"key2": {"단어": "번역내용"}}} 형태로 관리됨
      • key2 아래에 Depth 가 최대 두개까지 더 들어갈 수 있다.
      • 파일은 한국어 ko.json과 en.json 두가지로 나뉘어지고, 두 파일을 하나의 sheet 에서 보여주게끔 진행해야 함
      • sheet명은 front_json 으로 잡음
      • 따라서 First, Second, Thir, Fourth, Ko, En 을 칼럼으로 잡았다.
    • DB: Sequential 한 id 값이 Primary Key 이고, ko 필드에 한국어 / en 영어 데이터가 저장되어 있음
      • 프로그램 실행 시, DB 에서 데이터 조회
      • 테이블 명을 db_<테이블명> 명으로 잡음
      • id, ko, en 으로 칼럼명을 잡았다.

번역 파일 재생성 - 하나의 액셀 파일 -> 기존 폴더 구조에 맞게 번역 파일로 분리

  • 액셀 파일에 저장 및 수정이 되어 전달 받았을 때, 다시 번역 파일 생성 및 데이터 업데이트/삽입 프로그램
  • json, properties 파일과 DB 내용을 다시 업데이트 해 주기
    • sheet 명의 prefix에 따라, front / back / db 로직으로 분리
    • 다시 생성한 번역 파일들은 dist 폴더 아래에 저장
    • properties 파일 - dist폴더 아래에 prefix(back)으로 디렉토리 생성 후 그 아래에 파일 저장
      • 각각 시트명에 대한 폴더 생성
      • ko / en 칼럼들은 en.properties / ko.properties 파일로 분리 시켜 저장
      • <key칼럼> = <번역 내용> 으로 데이터 생성해서 저장
    • json 파일 - dist 폴더 아래에 prefix(front)으로 디렉터리 생성 후 그 아래에 파일 저장
      • ko 칼럼은 ko.json 파일로, en 칼럼은 en.json 파일로 저장
      • First, Second, Third, Fourth 칼럼들은 각각 json의 키깂으로 사용.
    • DB 데이터 - 해당 데이블에 데이터 업데이트 / 삽입
      • id 값을 PK 로 사용하고 있기 때문에, INSERT INTO ON DUPLICATE KEY 로 쿼리
      • 실 사용 서버에 적용시키기 전 개발 서버의 DB에 적용

설계

  • 언어는 Golang으로 선택. 실행 파일을 빌드해서 넘기기에 용이하며, 여러 OS 에서 실행가능함
  • 번역 파일들은 그 폴더 채로 assets 폴더 안에 옮겨두고 프로그램 실행하면 읽고 각 폴더명에 맞게 sheet 생성 및 데이터 저장
    • 폴더 없이 가장 상위 레벨 디렉토리의 파일은 default 라는 시트명 안에 저장
  • DB 데이터들은 데이터 조회 후, 테이블명을 sheet 명으로 지정하고 id - ko - en 형식으로 데이터 저장
  • 액셀 파일들은 하나의 파일 안에 모든 데이터가 저장되어야 함
  • 처리가 완료되면, 프로그램은 종료되고 실행 파일과 같은 디렉토리에 액샐파일 생성
728x90
반응형

+ Recent posts