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
반응형
728x90
반응형

UBUNTU에 Go 설치하기

  • 항상 컨테이너로 GO 프로그램을 돌렸지만, 불가피하게 로컬에 GO를 설치해 빌드하고 돌려야 할 상황이 와서 해당 내용을 기록한다.

소스 다운 받아서 설치하기

최신 버전 확인

  • 공식 사이트의 Golang 버전 확인 후 다운받기 (2024.10.21 시점 1.23.2)
  • 공식 사이트

WGET을 이용해 다운로드

  • WGET을 이용해 다운받기
    wget https://go.dev/dl/go1.23.2.linux-amd64.tar.gz

압축 해제

  • 다운 받은 압축 파일 해제
    sudo tar -C /usr/local -xzf go1.23.2.linux-amd64.tar.gz

경로 설정

vi ~/.profile

# 파일 마지막 줄 아래에 내용 추가
export PATH=$PATH:/usr/local/go/bin
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

변경 사항 적용

source ~/.profile

Go 버전 확인

go version

UBUNTU 피키지 매니저로 다운받기

패키지 인덱스 업데이트

sudo apt update

GO 설치

sudo apt-get install golang-go

Go 버전 확인

go version
728x90
반응형

'백엔드 Backend > Golang' 카테고리의 다른 글

[gRPC] Go 언어로 gRPC 사용해보기  (2) 2024.12.24
[Neo4J] 데이터 핸들링하기  (1) 2024.12.09
[Neo4j] Golang 으로 쿼리 핸들링  (0) 2024.12.06
[Neo4j] Golang으로 Neo4j 연동  (0) 2024.12.05
[GO] Goroutine이란?  (0) 2024.10.02
728x90
반응형

Golang은 뛰어난 동시성 지원이 장점이라고들 한다.

일반적으로 쓰레드를 활용하여 동시성 프로그래밍을 하지만, Golang은 고루틴 goroutine으로 가능하다.

 

고루틴 Goroutine

공시적인 설명으로는 경량 쓰레드이다. 실제 OS의 쓰레드를 사용하는 것이 아닌, golang의 런타임에서 관리되는 논리적 / 가상적 쓰레드이다.

 

사용법

go func() {

    // 대충 코드

  }()

이게 끝이다.

 

thread VS goroutine

1. 메모리 사용량

  • goroutine: 생성시 약 2KB의 스택 메모리 혹은 힙 메모리 공간만을 사용한다.
  • thread: 약 1MB 의 메모리 공간을 사용한다.
  • P.S. Guard Page: 쓰레드가 사용할 메모리 공간과 각 메모리 간의 경계 역할
  • 따라서 스레드 기반의 동시성 처리는 결국 메모리 부족 현상(OutOfMemory) 문제 발생할 수 있으므로, 스레드를 미리 생성해 재활용하는 형태가 될 수 있음.

 

2. 비용

  • goroutine: 런타임에서 논리적으로(하드웨어와 상관 없이) 생성되고 소거되기 때문에 가볍다.
  • thread: OS 에 스레드 생성을 요청해 사용하고 작업 완료 시 다시 OS에 반환해야 한다.
  • 따라서 쓰레드 생성시마다 OS에 요청해 생성하고 다시 반환하는 방식이 더 리소스 사용량이 많다.

 

3. Context Switching 비용

  • 하나의 스레드가 특정 작업 처리를 위해 사용(Blocking)된다면, 다른 쓰레드가 대신하여 처리하도록 스케쥴링 됨
  • goroutine: 3개의 레지스터(PC(Program Counter)), SP(Stack Pointer), DX)만 save/restore 작업을 함
  • thread: 스레드가 스케쥴링고 교체되는 동안 스케쥴러에서는 모든 레지스터들을 save/restore 해야함
  • 일반적으로 16개의 범용 레지스터, PC, SP, Segment 레지스터, 16개의 XMM레지스터, FP coprocessor state, 16개의 AVX 레지스터, 모든 MSR들 등 save/restore 작업을 진해해야함
  • 따라서 Context Switching 할 때 save/restore 해야하는 레지스터의 개수부터가 큰 차이가 남

 

결론

golang이 메모리 사용량과 쓰레드 생성과 작업처리에 있어서의 비용 측면에서 효율적이다.

 

마지막

goroutine의 동작 방식과 쓰레드 종류에 대해선 다음에 서술하고자 한다.

 

REFERENCE

- https://velog.io/@khsb2012/go-goroutine

 

 

728x90
반응형

+ Recent posts