최근 원격지원 서비스를 개발과 동시에 FRP Github Contributor가 된 경험을 작성하고자 한다.

FRP


1. FRP 적용 이유

우리 회사의 서비스 제품 중 일부는 데모 클라이언트 역할을 하는 서버들이 포함되어 있으며, 이러한 서버들은 외부 노출을 위해 Ngrok을 사용하고 있다.

NGROK 이란?
로컬에서 실행 중인 서버를 인터넷에서 접근할 수 있도록 안전한 터널을 제공하는 툴이다.
설정이 간단하다는 장점이 있지만, 속도와 기능이 유료버전에 비해 제한적이라는 단점이 존재한다.

 

LiDAR와 함께 패키지 형태로 제공되는 서비스는 대부분 클라이언트의 폐쇄망에서 사용된다. 하지만, 긴급 유지보수나 이슈 확인을 위해 NAT(Network Address Translation)를 우회해야 할 경우, 기존에는 RustDeskNgrok 같은 외부 도구를 활용했다.

 

그러나 이 환경은 여러 문제점을 가지고 있었다:

RustDesk: 원격 제어 툴로는 부족한 성능과 안정성.

Ngrok: 속도와 기능이 제한적.

터미널 환경: 원활하지 않은 연결로 인해 이슈 확인이 비효율적.

 

개선 필요성

이러한 문제를 해결하기 위해:

1. 외부 소프트웨어 의존도를 낮추고,

2. 더 나은 성능과 커스터마이징 옵션(소스코드와 동일한 언어)을 제공하며,

3. 원격 지원 및 터널링 기능을 하나로 통합한 설루션이 필요했다.

 

이에 따라, 우리는 FRP(Fast Reverse Proxy)를 적용하기로 결정했고, 아래와 같은 형태로 중계서버를 두고 중계 서버에서는 내부 네트워크망을 연결할 수 있는 방식의 형태를 이용해서 내-외부망의 통신을 가져갔다.

 

기존 구조에서 중계 서버는 FRP로 대체되었으며, 제공되는 데이터 수집 서버에 FRP 클라이언트를 설치하여 클라이언트 요청에 따라 중계 서버와 연결/해제가 가능한 형태로 설계했으며, 아래 그림과 같다.

1. 중계 서버 (FRP):
• 외부망과 내부망을 연결하는 브릿지 역할.
• 데이터 수집 서버의 클라이언트 요청을 처리하여 중계 서버와 연결하거나 해제할 수 있는 동적 환경 제공.

2. 내부망:
• 데이터 수집 서버는 FRP 클라이언트를 통해 중계 서버와 연결.
• 내부망에는 엔지 서버 및 LiDAR 센서가 위치하며, 데이터를 실시간으로 처리 및 제공.

3. AWS Private 환경:
• AWS 환경에 ALB(Application Load Balancer)와 Lambda를 활용하여 자동화된 데이터 흐름 관리.
• Lambda는 요청 처리를 통해 CloudWatch와 연동하여 로그 및 알림을 관리.
4. Slack Notification:
• FRP의 연결 상태나 요청 이벤트는 Slack 알림을 통해 실시간으로 전달.
• 운영자가 원격 연결 상태를 즉시 확인할 수 있도록 설계.

 

기존 RustDesk, Ngrok의 역할은 Frp가 대체하고 해당 연결의 Notification을 Slack을 통해 알람을 전달하는 구조를 생각했다.
- Lambda 함수의 관리를 위한 Chalice, LoadBalancer는 다음 포스팅에 다루고 FRP 위주로 작성해보고자 한다.


 

2. FRP 적용방법

기존 서버 환경은 Docker Container 환경으로 구축되어 있기 때문에 FRP에서 제공하는 Server, Client를 이용했다.

우선 FRPS의 설정부터 확인해 보면 다음과 같다.

bindPort = 7000

webServer.addr = "0.0.0.0"
webServer.port = 9500
webServer.user = "admin"
webServer.password = "admin"

transport.tcpMux = true
tcpmuxHTTPConnectPort = 1337

log.to = "console"
log.level = "trace"
log.maxDays = 3

 

1. bindPort:
• 중계 서버에서 외부망(public)으로부터 접근 가능한 포트를 설정.
• 해당 포트는 방화벽 또는 라우터에서 포트 포워딩이 설정되어 있어야 외부에서 접근이 가능.

2. transport.tcpMux:
• TCP Multiplexing 기능을 활성화하여 SSH 터널링과 같은 다중 연결을 효율적으로 처리할 수 있도록 설정.
• tcpmuxHTTPConnectPort: Multiplexing에 사용할 포트를 지정합니다. 이 포트는 SSH 터널링에 매우 중요하다.

3. webServer.addr & webServer.port:
• FRPS 관리 페이지(Web UI) 접근 주소와 포트 설정.
• 사용자 인증을 위해 webServer.user와 webServer.password를 지정.

4. log 설정:
• 로그 출력 대상과 수준(trace, debug, info 등)을 지정.
• log.maxDays는 로그 보관 기간을 설정.

 

회사 개발 서버에서 포트 포워딩을 통해 bindPort(7000)가 외부망에서 접근 가능하도록 라우터를 설정했다.

 

다음 FRPC의 설정을 확인해보면 다음과 같다.

user = "frp_test"

serverAddr = "외부 공개된 중계서버 IP"
serverPort = 설정된 포트 번호

log.to = "console"
log.level = "debug"

# Set admin address for control frpc's action by http api such as reload
webServer.addr = "127.0.0.1"
webServer.port = 9500
webServer.user = "admin"
webServer.password = "admin"

[[proxies]]
name = "ssh"
type = "tcpmux"
multiplexer = "httpconnect"
localPort = 22
localIP = "127.0.0.1"
customDomains = ["abc.com"]

[[proxies]]
name = "a"
type = "http"
localPort = 11111
customDomains = ["abc.com"]
locations = ["/api/v1/a"]

[[proxies]]
name = "b"
type = "http"
localPort = 22222
customDomains = ["abc.com"]
locations = ["/api/v1/b"]

[[proxies]]
name = "c"
type = "http"
localPort = 33333
customDomains = ["abc.com"]

 

1. serverAddr & serverPort:
• FRPS(Server)의 공인 IP와 bindPort를 설정합니다.

2. proxies:
• 중계 서버에서 전달할 요청의 규칙을 정의합니다.
• SSH 터널링: type = "tcpmux"를 사용하여 특정 포트(localPort 22)를 지정.
• HTTP 프록시: type = "http"를 사용하여 경로(/api/v1/a, /api/v1/b)에 맞는 요청을 특정 포트로 전달.

3. customDomains:
• 요청을 받을 도메인 이름을 정의합니다.
• abc.com이라는 도메인에 대해 특정 경로 요청을 설정한 포트로 전달하도록 구성했습니다.

 

그 외에는 location에 맞는 라우팅이 되는 것이 마치 프록시 설정과 상당히 비슷하다. 

 

FRP는 중계서버에서 리버스 프록시를 해주고 있는 것이다. 

예를 들어, 클라이언트가 abc.com/api/v1/a에 접속하면:

FRPS는 해당 요청을 FRPC로 전달한다.

FRPC는 로컬 IP(127.0.0.1)의 포트 11111로 요청을 라우팅 한다.

 

이 방식은 마치 리버스 프록시 설정과 유사하며, 클라이언트의 요청을 내부망으로 안전하게 전달할 수 있다.

 

SSH 터널링의 경우 proxy socat을 활용하여 로컬 -> 중계서버 -> 클라이언트서버로 갈 수 있도록 아래와 같은 커맨드를 활용했다.

ssh -o 'ProxyCommand socat - Proxy:127.0.0.1:abc.com:22,proxyport=1337'  xxxx@중계서버.com

1. ProxyCommand란?
ProxyCommand는 SSH 클라이언트 옵션으로, SSH 연결을 설정하기 전에 특정 명령어를 실행하도록 지정한다.
이를 통해 SSH가 직접 대상 서버에 연결하지 않고, 프록시(Proxy)나 다른 중계 프로그램을 경유하도록 설정할 수 있다.

2. socat - Proxy:란?
socat은 소켓 통신을 다루는 강력한 유틸리티입니다. 다양한 프로토콜(예: TCP, UDP, HTTP)을 사용하여 데이터를 전송하거나, 소켓 연결을 프록시 서버를 통해 중계하는 데 사용됩니다.

 

1337은 기존 중계서버에서 tcpmux의 openport임을 기억하자.


3. Customize

클라이언트 환경 최적화

대부분의 클라이언트는 교통량이 많은 교차로에서 소형 PC에 설치되어 작동하고 있다.

제한된 디스크 용량과 메모리를 고려해, 최적화 가능한 부분을 식별하고 개선 작업을 진행했다.

 

TinyFRP 적용

TinyFRP라는 경량화된 FRP 클라이언트를 도입하여 기존 20MB였던 클라이언트 크기를 약 6MB로 축소(95% 용량 절감).

용량 절감이 가능했던 이유:

필요 없는 Web Admin 페이지 제거.

JSON 및 TOML 파싱 기능 제거.

 

의존성 관리 문제와 해결

FRP 클라이언트를 라이브러리 화하는 과정에서 go mod 의존성 관리 문제가 발생했다:

1. FRP가 사용하는 samber.io 라이브러리의 버전업으로 인해 코드 충돌이 발생.

2. go.mod의 replace를 통해 문제를 해결할 수 있었으나, 자동화된 배포 과정에서 스크립트 관리 문제가 발생할 가능성이 있다고 판단.

 

PR(Pull Request)로 해결

장기적인 안정성을 위해 FRP 라이브러리에 Pull Request를 작성하여 수정 사항을 기여했다.

수정 사항이 프로젝트의 Main 브랜치에 Merge 되었을 때, 개발자로서 가장 기분 좋았던 순간 중 하나였다.


4. 결과

비용 절감

1. 기존에 사용하던 Ngrok을 제거하여 매월 31달러의 비용 절감을 달성.

2. 모든 클라이언트 구역에 FRP가 적용되면, 월 300달러 이상의 비용 절감이 예상된다.

 

안정성 및 효율성 향상

기존 터미널 환경에서 발생했던 끊김 현상이 크게 개선되어, 운영 환경의 스트레스를 줄였습니다.

VOC 처리와 모니터링 환경이 Slack 알림 연동으로 더욱 효율적으로 변화.

'개발일지' 카테고리의 다른 글

10월 개발일지  (1) 2023.11.01
7월개발~ 8월초  (0) 2023.08.16
6월 개발  (1) 2023.07.09
5월 개발  (0) 2023.06.11

오랜만에 작성하는 개발일지이다.  9월은 지난번 개발한 투표/설문 조사의 성능개선을 했고, 10월은 새로운 프로젝트에 들어갔다.
9월에 마무리된 개발건이 배포가 밀려 10월까지 밀리면서 엄청 바쁜 10월 한 달을 보낸 것 같다. 

10월 시작하는 프로젝트는 완전 백지상태에서 시작하는 작업이다 보니 새로운 경험을 생각보다 많이 했는데 해당 부분에 대해 기록으로 남기고자 한다.


유저인증

유저의 인증 경우 토큰방식을 선택했는데, 이 토큰을 jwt가 아닌 암호화 알고리즘을 이용해서 작성하게 되었다.

지난번 작성한 포스팅이 이 암호화 및 해쉬에 대해 작성을 했다.

 

아무튼 로그인에 대해 암호화는 지난번 포스팅에 작성한 것처럼 작성하였다. 

 

토큰의 만료기한 또한 디비에서 관리하게 되면서 매번 만료기한을 업데이트시켜주어야 했다. 

미들웨어를 사용해서 해당 비즈니스 문제를 해결했다.

미들웨어란 요청,응답이 서비스 라우팅에 닿기 전 또는 닿은 후 응답을 각각 가로챌 수 있는데 가로채서 토큰의 유무를 검증하고 토큰의 파싱값을 컨택스트에 넣어주는 방법으로 작성했다.

 

여기서 컨택스트란 에코-프레임워크를 비롯해 고에서는 http 요청을 하나의 고 루틴으로 해결한다. 이렇게 고 루틴으로 할당되면 해당 고 루틴을 이용해 request와 response를 처리한다. 
이에 따라 각 레이어를 넘나들면서 context 를 들고 이동하게 되는데 이는 처음 루틴을 생성했을 때부터 데이터들을 가지고 레이어들을 넘나 든다. 그 외에도 context를 이용해 deadline, cancel 등 고 루틴의 핸들링을 쉽게 다루는 방법이 존재하니 검색해서 확인하자.

 

func Authorization(key,tokenURL string) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.handlerFunc {
		return func(c echo.Context) error {
			headerToken := c.Requset().Header.Get("Authorization")
			if 특정 URL 을 가지고 있다면? {
				URL 요청 토큰연장
			}
			if tkn,err := 토큰 parse; err != nil {
				return http.StatusUnauthorized 요청
			}else{
				c.Set("token",tkn)
				return next(c)
			}
		}
	}
}

미들웨어를 반환해 주는 함수를 작성해 준다면 미들웨어의 적용이 가능하다.
작성된 미들웨어는 아래와 같이 적용가능하다.

func run() error {
	e := echo.New()
	e.Use(atask_middleware.Authorization("key","tokenURL"))
	routing(e)
	return e.Start("포트")	
}

이렇게 작성된 에코는 라우팅되어 있는 URL 함수가 실행되기 전 미들웨어를 먼저 타게 된다.


Request, Response dump

입출 하는 모든 Request, Response에 대해서 로그를 남기고자 했다.

위에서 언급한 미들웨어의 특성 중 Request Response 모든 시점에 캐치가 가능하다고 작성했다. 

해당 특성을 이용하면 로그에 대해 손쉽게 작성할 수 있다.

 

type dumpResponseWriter struct {
	io.Writer
	http.ResponseWriter
}

func (w *dumpResponseWriter) Write(b []byte) (int, error) {
	return w.Writer.Write(b)
}


func LoggerDump() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// request 요청
			reqBody := []byte{}
			if c.Request().Body != nil { // Read
				reqBody, _ = io.ReadAll(c.Request().Body)
			}
			c.Request().Body = io.NopCloser(bytes.NewBuffer(reqBody))
			

			// response 요청
			resBody := new(bytes.Buffer)
			mw := io.MultiWriter(c.Response().Writer, resBody)
			writer := &dumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
			c.Response().Writer = writer

			if err := next(c); err != nil {
				return err
			}

			dump(c, reqBody, resBody.Bytes())
			return nil
		}
	}
}

 

Request부터 확인해 보면 바디를 읽어서 선언한 reqBody에 읽어온 데이터값을 할당하고, 
다시 c.Request(). Body에 값을 reqBody로부터 읽어온 값을 할당해 준다.

왜 이런 2번의 작업이 필요하냐면 c.Reqeust(). Body는 일회성 스트림이다.

다시 말해 한번 읽으면 다시 읽을 수 없다는 것을 의미하기 때문에 다시 할당해 주는 부분이 필요하다.

(그렇지 않다면 실제 라우팅의 함수에서 리퀘스트 바인딩에 어떠한 값도 가져올수없다...)

 

Response의 경우 response writer를 위에 작성한 커스텀타입의 Writer로 덮어주는 방법이다. 
multiWriter를 이용해 바이트 버퍼와, response 응답값 모두에 응답값을 작성하게 된다.

{"level":"info","time":"2023-10-31T14:53:26+09:00","caller":"hsad-poc/atask_common/atask_middleware/logger.go:76","message":"
	[REQUEST][METHOD:POST][URI:/user/api/v1.0/login][USER:] body : {"email":"","password":""}
	[RESPONSE][STATUS:200][Latency:42.674µs] body: {"code":200,"data":{"access_token":"","email":"","grade":"","member_id":,"username":""},"msg":"SUCCESS"}"}

결과적으로 위와 같은 로그라인을 작성했다.

 

문제발생

다음날 출근해 보니 총 4개의 프로세스가 떠있어야 했는데 특정 프로세스 한 개가 죽어있었다... 
로그를 확인해 보니 1개의 프로세스에서는 이미지 업로드가 포함되어 있어 멀티파트 폼 데이터를 사용하게 되는데... 
해당 이미지의 바이트 코드를 전부 로그에 작성해 버리는 것이 아닌가.... 

func parseMultiPartForm(c echo.Context) []byte {
	if err := c.Request().ParseMultipartForm(maxMemory); err != nil {
		log.Error().Err(err).Msgf("fail to parsing multi part form data")

	}
	m := make(map[string]interface{})
	for key, value := range c.Request().Form {
		if key != "file" {
			m[key] = value
		}
	}
	data, err := json.Marshal(m)
	if err != nil {
		log.Error().Err(err).Msgf("fail to marshal data %+v", data)
		data = []byte{}
	}
	return data
}

멀티파트를 포함한 요청인경우 분기를 나누고 위와 같은 방법을 적용했다. 
에코에서 parse를 지원해 줘서 다행이지.. 아니었으면 한 삽 펐을 것으로 보인다.
map을 이용해 file을 제외한 모든 경우를 담아 json으로 엔코딩 해서 바이트로  반환해 주었다.


zerolog

지난 프로젝트에서 logurs 라이브러리를 이용해 로그를 작성했다. 

하나 해당 로그라이브러리로 인해 tps측정에 문제가 되는 것을 확인하고, 이번 신규프로젝트에서는 zerolog를 적용하기로 했다.

 

middleware 쪽을 담당하고 있다 보니 로그도 자연스레 나의 파트가 되어버렸다.

 

목적 

1. 콘솔 로그 작성

2. 파일 로그 작성(일별로)

 

1차시도 Hook을 이용한 방법  (실패)

zerolog 홈페이지에 Hook에 대한설명이 짤막하게 남아있는데, 해당 hook을 이용하면 매번 로그가 호출되어 콘솔에 적힐 때마다.
파일에 남긴다 라는 생각을 했다.

type FileWriter struct {
	log  chan byte
	cur  time.Time
	file *os.File
}

func (f FileWriter) Run(e *zerolog.Event, level zerolog.Level, message string) {
	// 날짜 비교 해서 날짜가 지났으면 카피 후 새파일 생성  
	// 파일 열고 없으면 만들고
	// 파일 에 message를 작성한다.
}

이렇게 작성했더니 이게 웬걸 message 에는 내가 원하는 정보들이 담겨있지 않았다.... 
어디서 로그가호출 되었고, 타임라인이 어떤지 는 없고 단순 로그에 넘긴 메세지만 저 message 함수인자 값으로 넘어온다...

하루종일 zerolog hook에 대해서 찾아봤으나 원하는 정보를 찾기 힘들었다. 

 

2차시도 Multiwriter를 이용한 방법

zerolog document를 처음부터 정독했다. 거의 막바지에 Multiple Log Output이라는 섹션이 있는데 이를 이용하면 로그를 여러 방면으로 남길 수 있게 된다. 
제공하는 함수로는 MultiLevelWriter이고, io.Writer 인터페이스만 만족하면 된다. 

바로 인터페이스 구현체를 작성했다.

func New(path, fileName string, logLevel zerolog.Level) zerolog.Logger {
	console := zerolog.ConsoleWriter{Out: os.Stderr}
	multi := zerolog.MultiLevelWriter(console, NewFileWriter(path, fileName))
	zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
		return file + ":" + strconv.Itoa(line)
	}
	return zerolog.New(multi).Level(logLevel).With().Timestamp().Caller().Logger()
}

콘솔은 콘솔라인에 남기기 위한 writer, NewFileWriter는 file에 남기기 위한 writer 가 되겠다.

CallerMarshalFunc 같은 경우는 로그의 호출이 무슨 파일, 몇 번째 줄에서 발생되어 어떤 형식으로 작성하겠는가? 를 설정해 주는 부분이다.

 

type FileWriter struct{
	log chan []byte
	file *os.File
	cur time.Time
	path string
}
func prettier(s string) []byte {
	// \문자제거, 라인분할
}
func (f *FileWriter) Write(p []byte) (n int,err error) {
	// log <- 채널로 메세지 전송
}
func (f *FileWriter) WriteLog(msg []byte) (int,error){
	// 인터페이스 구현함수
	return f.file.Write(msg)
}
func(f *FileWriter) IsRotate(now time.Time) bool {
	// 마지막 파일 수정일 기준으로 하루가 지났는가?
}
func(f *FileWriter) RotateFile(now time.Time)error{
	// 현재 파일의 이름을 변경
	// 현재 시간 변경
	// 현재 파일 새로운 파일로 변경
}

func NewFileWriter(path, name string) *FileWriter{
	// 파일 셋팅
	// 시간 세팅
	go func() {
		for v := range 로그채널 {
			//v 값을 writeLog 함수 호출
		}
	}
}

우선 타입을 살펴보면  log의 값을 받기 위해 채널을 설정했다. 버퍼링 채널을 위해 1024를 설정했다.

비동기 채널링을 설정한 이유는 로그의 중복된 값 혹은 로그의 씹힘을(동시성)문제를 방지하기 위해 채널링을 생성했다. 

아무래도 채널을 구성하게되면 큐의 자료형태로 들어가게 되고 1024의 로그만큼 계속 비동기적으로 로그를 작성하게 된다고 생각되어 채널을 선택했다.

지난번 고의 동시성 프로그래밍 책에서도 뮤텍스보다는 채널을이용해서 작성할 것을 권장했는데 이렇게 적용하게 될 줄을 몰랐다..

 

또한 파일의 OS 값을 받아오면 해당 파일의 마지막 업데이트 값을 받아올 수 있다.... 

해당 사실을 몰라 마지막 로그의 인덱스로부터 1024바이트씩 읽어 날짜 값을 가져와 파싱 해서 비교하는 로직을 작성했는데...

엄청난 삽질이 아닐 수가 없다. 

 

해당 로그의 인터페이스를 구현 하면서 지난번 읽었던 UltimateGo 에서 왜 인터페이스의 함수의 변수 사용에 대해 강조했는지 조금이나마 이해가 간다.

함수의 변수를 인터페이스로 받으니깐 다형성의 적용이 무궁무진하다...

다만 이런생각이 든다. 고였으니깐.. 인터페이스의 구현이 생각보다 쉬웠의깐

나만의 커스텀 함수를 작성해서 인터페이스의 구현체를 작성하지 않았을까? 라는 생각이 든다.

과연 스프링 부트였다면  이 인터페이스를 구현해서 커스텀하게 작성했을까? 라는 의문이 드는 부분이었다... 

검색을 통해 내가 구현하고자 하는 라이브러리에 대해 찾고 해당 라이브러리를 추가해서 사용하지 않을까 싶다...

 

3. 일별 로그 기록 -> 용량별 로그기록

Git Action, Kubernetes, istio 등의 DevOps를 팀장님이 설정하고 나니, 클라우드워치(aws 인프라를 사용한다.) 또한 적용하게 되는 것이 아닌가. 이러다 보니, 쿠버네티스의 각 파즈 별로 들어가서 로그를 확인할 일이 줄어들고, 이에 따라 로그의 작성을 일단위가 아닌 용량단위 그리고 백업은 대략 3~5개 파일로 구성하자는 의견이 팀회의를 통해 나왔고... 

 

해당 로직을 구성하기 위해 lumberjack 이라는 라이브러리를 사용했다. 

lumberjack 라이브러리는 로그를 기록하고자 하는 폴더의 로그 파일을 감시하고, 로그의 용량을 판단해 새로운 로그 파일을 작성할지 아닐지에 대해 구현해 놓은 간단한 라이브러리였다.

지난번 일별구현은 왜 커스텀하게 하고 이거는 라이브러리를 사용했는가에 대해 의문이 생길 수 있다. 

지난번 일별 로그기록을 할 때 당연히 해당 라이브러리의 기능을 확인했으나, 팀에서 추구하는 바와 달라 포기했던 기억이 있어 팀의 비즈니스 로직이 변경되자마자 lumberjack 라이브러리를 적용했다.

 


 

이렇게 미들웨어부터 msa의 공통 로그까지 작성해 봤다. 물론 api 핸들러를 작성한 것은 당연한 것이고...

처음 시작할 때 말했다 것처럼 이번 프로젝트의 초기 뼈대를 잡는데 정말 많은 시간을 할애했다.

 

내가 분명 UltimateGo, EffectiveGo에서 본 naming convention, package convention 과는 많이 달랐다 그러나 팀이 추구하는 방향에 따라 상당 부분 달라질 수 있다.

이러한 자율성이 고가 가진 힘이자, 위험한 부분이라고 생각한다.

 

프로젝트 시작과 동시에 이러한 컨벤션을 작성하지 않은 것이 참 후회된다.

사수님과 내가 작성한 코드를 보면 코드의 통일성이 없이 각 프로세스 별로 각자의 주관이 상당히 뚜렷하다.

DVT (Testing)가 성공적으로 종료되었지 만 개인적으로 이번 프로젝트는 10점 만점에 6점짜리 프로젝트인 것 같다.

 

지난번 투표/설문조사를 통해 가독성에 대해 상당히 많은 코드리뷰를 받았었다... 그렇기 때문에 ultimateGo, EffectiveGo, GoogleGO CodeConvention, BankSalad Go BLog, Buzvill 블로그 등 상당히 많은 블로그를 보며 고의 표준 레이아웃, 패키지 컨벤션 등 을 확인했지만, 오히려 해당 컨벤션을 지키면서 작성했던 것이 팀의 코드 히스토리상 어긋난다는 이유로 전부 변경 했어야 했다. 

항상 코드리뷰를 보면 서비스의 로직에 대한 부분보다 이러한 컨벤션에 대한 리뷰가 많았다...

내가 보고 배운 표준 컨벤션과는 다른 것을 적용하다 보니, 이러한 컨벤션에 대한 정의를 미리 내려야 하지 않을까 싶다.

 

이렇게 제공되는 자율성이 고가 가진 장점이자 단점이라고 생각한다.

 

이러한 부분을 수정하면서 느낀 것은 왜 코드컨벤션이 필요한가? 에 대한 명확한 이유를 납득했다.

'개발일지' 카테고리의 다른 글

FRP 적용  (0) 2024.12.31
7월개발~ 8월초  (0) 2023.08.16
6월 개발  (1) 2023.07.09
5월 개발  (0) 2023.06.11

7월 개발은 지난달에 개발하고 완료된 상태가 배포도 안 된 상태에서 다시 새로운 기능개발 인 투표/설문 개발 을하게되었다.

새로운 신규개발과 더불어 지난달 개발된 항목에 대해 지속적인 API 지원요청 이 있어 로그확인등 하는데 생각보다 작업하는 일정이 늘어졌다.

 

1. https 오류 지원 

현재 개발의 부모페이지 즉 프런트 부모페이지는 외부협력사에서 담당하고 있다. 해당 개발사에서 는 특정 테스트 서버환경을 제공해주지 않고 단순 우리의 API를 호출하면서 "안된다"라는 메일을 받으면 정말 당황스럽다. 

지난 6월 리버스 프락시 를 적용해서 openssl -x509 를 사설 인증서를 만들어 프록시 서버를 오픈하여 포트를 공유하였다. 이에 대한 피드백은 지지난주 금요일 퇴근 5시 전에 단순 안된다 라는 메일 이 왔고, 이를 해결하기 위해 let's encrypt를 활용해 3개월짜리 무료 ca 인증을 받을 수 있다. 

 

[certbot 인증과정]

lets encrypt 사이트를 접속하게 되면, certbot acme 클라이언트를 이용해 발급받는 방법을 추천하는데 이 방법을 이용해서 인증서 발급을 받았다. 

acme 란 웹보안인증서를 자동으로 발급, 갱신을 위한 프로토콜이다. certbot 웹 페이지를 실제 접근하게 되면 정말 설명이 잘되어 있다.

설명서를 읽기 위해 우선 리눅스의 버전을 알아야 한다. 

 

확실히 리눅스 쪽에서 작업이 어색하다 보니 자주 사용하는 명령어인 scp mv cp ltr grep 등의 명령어만 사용할 줄 알지 다른 거는 정말 모른다. 

리눅스 버전확인 : cat /etc/os-release

centos - 7을 활용하고 있다. 해당 버전을 certbot에 기입을 하게 되면 설치하는 방법에 대해 설명해 준다.

1번 스텝 : ssh를 이용해 서버에 접속한다 

2번 스텝 : snapd를 설치한다.(https://snapcraft.io/docs/installing-snap-on-centos)

리눅스 에는 os에 맞는 버전관리 툴? 이 존재한다 우분투는 apt centos는 yum 등이 있는데 이를 통합해서 관리하는 snapd라는 툴이 새로 나온 것이다. 당연히 우리 서버에는 존재하지 않기에 설치해주어야 했다.

3번 스텝: certbot을 설치한다.

sudo snap install --classic certbot

4번 스탭 : certbot 명령어를 활성화시켜준다.

sudo ln -s /snap/bin/certbot /usr/bin/certbot

ln 링크의 약자로 파일 또는 디렉터리 연결을 할 때 사용하는 것이다.  -s와 조합하여 사용하는 것으로 

/snap/bin/cerbot을 /usr/bin/certbot으로 심벌링 링크 즉 원본파일을 가리키는 링크를 생성하는 것이다. 

이에 따라 /usr/bin 은 환경변수에 포함되어 있기에 리눅스 내에서 certbot 커맨드를 사용할 수 있는 것이다.

 

5번 스탭: certbot을 이용해 어떻게 인증받을지 정하는 것인데 2가지 선택지가 있다.
실제로 해당 웹페이지에 가면 더 다양한 방법을 알려준다. (https://eff-certbot.readthedocs.io/en/stable/using.html)

- 웹서버가 현재 실행 중이 아니라면? 

sudo certbot certonly --standalone

- 웹서버가 계속 실행되는 것이 유지되어야 한다면?

sudo certbot certonly --webroot

 

 

1번의 경우는 80번 포트를 바인딩해서 도메인 인증 저을 한다고 한다. 따라서 웹서버가 진행 중이라면 모두 정지해주어야 한다.

실제로 회사에서는 80번 포트를 운영하는 것 이 nginx 가 있어서 해당 경우는 사용할 수 없었다.

 

2번의 경우는 계속 인증에 실패했다.

2번의 인증 동작방식 중 하나인 /. well-known/acme-challenge 패스에 접근이 되어야 하는데 그것이 불가능해서 발생되는 오류였다.

팀장님께서는 휴가 중이시고 이 리버스 프락시 서버를 만들게 된 계기도 nginx 설정을 만지지 말라고 하셔서 생긴 것이기 때문에 nginx에 추가적인 설정으로 해당 주소를 바인딩하기로 결정했으나 잉? 이게 무슨 일인가 nginx는 서버에서 돌아가고 있지 않은 게 아닌가... 

http {
        include mime.types;
        server {
                listen 80;
                listen [::]:80;
                location ^~/.well-known/acme-challenge/{
                        default_type "text/plain";
                        root /var/www/letsencrypt;
                }
        }
}

바로 설정 추가해서 돌려주었다. 잉 웬걸 서버 실행에 자꾸 실패한다. 확인해 보니. apache httpd 서버가 돌아가고 있는 게 아닌가?

httpd라는 웹서버가 있다는 것도 이때 처음 알게 되었는데 검색을 해보니 nginx와 같은 웹서버 역할을 해준다. 그렇다면 config를 해줄 수 있는 무언가가 있다고 생각해 찾아보니 

/etc/httpd/conf/httpd.conf라는 파일이 존재한다. 해당 폴더에 웹루트에 대해 적혀있고 해당 루트를 기준으로 인증에 필요한 폴더 패스 f를 생성해 주었다.

띠용 계속 실패한다. 이사님에게 여쭤보니 우리의 도메인은 route53을 이용하고 있다고 하신다. route53 의설정을 찾아보니 (https://certbot-dns-route53.readthedocs.io/en/stable/)

aws의 키파일들이 /. aws/config 에 존재해야 한다. 이사님도 계시지 않고, 팀장님도 휴가 중이셔서 다른 방법을 찾아봐야 할 것 같다. 

 

httpd 서버를 통해 특정 포트 혹은 실행 중인 포트와 연동되는 것이 없어 httpd를 일시중지 시키고 1번의 경우를 이용해 certbot을 인증을 받게 되었다. 만약 중지되어서 lsof -i :80에 아무것도 나오면 안 되는 것이니 이중체크 하기 바란다.

 

인증을 받게 되면 /etc/letsencrypt/live/도메인으로 cert.pem, chain.pem, fullchai.pem, privkey.pem READM를 파일들을 준다. 

리버스 프락시에 제공된 cert.pem과 privkey.pem을 이용해 서버를 재기동하고 확인하니 정상적으로 동작하고 있다. 

이렇게 완료되는 줄 알았으나. 다시 받게 된 "안된다 메일"과 함께 온 에러메시지

curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it.
To learn more about this situation and how to fix it, please visit the web page mentioned above.

너무 답답해서 전화를 해서 확인을 해보니 다른 컴퓨터에서는 정상접근이 가능하나 해당 작업하는 서버에서는 curl 요청 시 위와 같은 에러가 발생한다는 거였다...

문제의 원인은 저 cert.pem 이 문제였다. 해당. pem 은 자체적인 서버인증 만을 가지고 있는 것이다. 다시 말해 웹스에서는 정상접근이 가능하다 그러나 curl을 이용해서 서버의 인증서를 이용해 요청을 하게 될 때 몇몇 서버에서는 해당. pem을 인증할 수 없는 문제가 발생된다. 

README에 작성되어 있는데 한 번이라도 봤더라면 fullchain.pem을 적용했을 텐데 참 아쉽다... 

이에 따라 fullchain을 적용해서 아래 적용되었는지 확인을 하게 되면 아래와 같이 chain 이 형성된다.

openssl s_client -connect "리버스 프록시 주소"


Certificate chain
 0 s:/CN=im.plea.kr
   i:/C=US/O=Let's Encrypt/CN=R3
 1 s:/C=US/O=Let's Encrypt/CN=R3
   i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
 2 s:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3

 

이후 더 이상의 메일은 없었다고 한다. 

 

2. Log 파일 읽기

 

특정 API 호출 시 데이터가 없다는 메일을 수신했다. 제발 이런 메일을 보내면 언제 주로 테스트를 했고 어떻게 했는지에 대해 첨부 좀 해주었으면 좋겠다. 모든 걸 다 확인해야 하기 때문에 생각보다 시간이 오래 걸린다. 

scp로 메일 받기 전날짜의 로그를 로컬로 가져와 sublime Text를 이용해 검색했다.

특정 IP 기준으로 확인을 해보니 진짜 데이터가 안 내려간다. 로직상 에서 데이터가 안 내려가는 경우는 jwt 토큰 인증이 안된경우만 안내려가는 것인데 무엇이 문제인가 로그를 위아래 확인해 보니 세상에 jwt 토큰을 헤더에 잘못 넣어주고 있었다.

우리는 A라고 API 명세서 상에 적어주었으나, B라고 작성해 요청을 하는 것이 아닌가? 이거는 다시 말해서 저 외부협력사는 6월 중순부터 토큰을 넣어서 api 요청을 해본 적이 없다는 소리 아니겠는가?

이런 협력사와 일을 하는 게 너무 스트레스받는다.

 

3.  투표/설문조사 기능 개발

 

투표/설문조사라는 새로운 기능개발 건이 들어왔다. 기존에 없는 신규 기능이기에 처음부터 작성해야 해서 정말 재밌었다. 

ERD 설계, API 명세서 작성 등 4일 동안 팀장님한테 와장창 깨졌다. 

ERD와 API 명세서를 작성하는데 왜 동시성에 대한 문제와 쿼리에 대해 고민하는지 사적지식을 그만 탐구하라고 하셨다.

 

저런 거를 고민 안 하고 어떻게 ERD, API 명세서를 작성하는지 이해가 안 간다. 동시성을 고민해야 했던 이유는 라이브 방송에서 진행되는 투표/설문조사 의 경우 어떻게 통계 테이블에 데이터를 밀어 넣어주고 해당 고 루틴 관리에 대해 생각보다 많은 고민을 했고,

이렇게 ERD를 작성했을 때 어떤 걸 기준으로 조회가 많이 발생할 것 같은데? 복합 PK 가 좋을까? 이런 고민은 필요한 것이 아닌가에 대해 팀장님과 의논을 했고 팀장님이 원하던 것은 완벽한 ERD, API 가 아닌 빠르게 먼저 기초 틀을 잡길 원하셨던건데 나는 다른 방향을 가고 있었다고 하셨다. 단순 자전거 수리점에서 체인만 갈면되는것을 , 이것저것 핸들 안장 바퀴 등을 다손보는 작업을 하고 있어 팀장님 입장에서는 답답하셨다고 한다.

 

테이블의 설계와 pk fk 전략은 기존 데이터 베이스와 동일하게 가져갔다. fk는 없이 id 칼럼을 추가하고 인덱스를 걸어주기로 했다.

지난 프로젝트에서 이렇게 설정했던 이유는 fk에 따른 조인 시 오버헤드 연산 그리고 msa 가 적용되어 있어 id 하나의 값을 가지고 레디스, mysql  한 번에 조회하는 이점 등이 있어 이렇게 설정했었다. 이에 대해 추후 어떻게 하면 이런 방식이 좋을 수 있는지에 대해 작성해 보겠다. 

그외 테이블 설계의 특별한것은 없었던것 같다.

 

ERD,API 명세서 작성이 완료된 이후 코드작성을 하는데 너무 재밌었다. 새로운 신규기능에 대해 개발 을 하는 것이 너무 새로웠다. 물론 프로젝트 구조상 api 핸들러는 템플릿 메서드 패턴이 적용되어 필요한 인터페이스를 구현하기만 하면 되지만 이런 api 가 아닌 라이브 채팅 서버와 채팅 매니저 관리 쪽은 고 루틴과 채널을 이용해야 했다.

이번에 배운 동시성 프로그래밍 의 규칙에 맞춰 잘 작성했다고 생각한다. 라이브 채팅 서버 혹은 vod에서 투표가 진행된다면 해당 결과를 통계테이블에 옮겨주는 작업이다. 물론 배치를 통해서 작성하면 손쉽게 작성할 수 있겠지만 투표진행이 없음에도 배치가 도는 비효율적인 코드를 작성하기 싫어 고 루틴과 채널을 활용해서 작성해 보았다.

 

캡슐화를 통해 고 루틴을 생성하는 함수에서 채널을 생성해 해당 채널을 반환하는 함수를 작성했다. 이에 따라 단방향 채널의 반환값이 결정되고, 함수의 호출 부는 해당 단방향 함수에서 값을 읽어오기만 하면 된다.

이렇게 작성된 코드는 파이프라인 의 패턴으로 확장하기도 쉬울뿐더러 단방향 채널의 반환값이 주는 매력에 빠졌다.

이에 따라 함수 호출자는 동시성에 대한 고려 없이 그냥 함수를 호출만 하면 되고, 함수 의 내부로직에서 동시성의 관리만 해주면 된다.

또한 해당 고 루틴 들은 각자 투표의 아이디 값을 기준으로 map을 통해 관리되고 map에서 삭제되는 순간에는 mutex lock을 걸어 동시성 이슈를 제거하고자 했다. 이렇게 하다 보니 차라리 투표의 핸들러? 를 만들어서 구조체 하나에서 관리하는 것이 더 좋다고 생각해 apiHandler의 구조체에 추가해 주었다.

최근 동시성 프로그래밍 책을 읽고 막 책에서 소개해주는 복잡하고, 어려운 패턴을 적용한 것이 아닌 동시성 프로그래밍에서 강조하는 컨택스트의 관리를 왜 함수 내부에서 관리하는 것이 좋은지 코드를 작성하면서 알게 되었다. 정말 코드의 작성이 간결하고 로직의 구현이 쉬웠다.

 

그러나 한 가지 고민되는 부분은 통계 테이블로 옮기는 과정이 upsert이다.

나의 설계에 따르면 투표가 진행 중이라면 1초에 한 번씩 고 루틴 신호가 발생해 upsert를 진행하는데, mysql 유후 커넥션 풀이 10개 필요시 20개까지 늘어나게 된다.

그렇다면 20개 모두 사용 중이라면 해당 선전됨 고 루틴은 하나의 upsert 가 진행되는 동안 나머지는 모두 대기상태에 빠져 생각보다 성능이 나쁘지 않을까 생각된다.

하지만 우리의 투표시스템이 막 100~200 개처럼 엄청 크다고 생각하지는 않는다. 만약 실제로 이렇게 진행되는 투표가 많이 존재한다면 차라리 배치 프로세스를 도입하는 것이 훨씬 이득인 것처럼 보인다. 

 

또한 실시간 채팅 의 경우 투표의 시작 메시지에 대해 브로드 캐스팅이 필요하다 현재 투표가 진행되었다는 브로드캐스팅이 필요하다.

우리의 서버는 채팅 매니저- 나츠 - 채팅 이 존재하는데 나츠는 고로 만들어진 메시지큐 이다. 어드민 에서 발생되는 메세지 들은 고 루틴 의버퍼링 채널을 통해 5~10개씩 나츠로 밀어 넣어주고 나츠에서는 이를 브로드캐스팅으로 채팅 접속자에게 나눠주는 구조로 되어있다.

 

이에 우리 채팅매니저 쪽에서는 밀어 넣어주는 메시지에 대해서 시그널에 대해 정의가 되어있는데 기존 것을 활용하기 에 적합한 곳이 없어 투표라는 시그널을 뚫어주었다. 

추후 나츠를 이용한 채팅을 간략하게 구현하는 것을 포스팅해보겠다.

 

확실히 CRUD 쪽에 있어서는 4~5월 프로젝트할 때보다 상당히 많은 시간이 줄어든 것 같다. orm에 익숙해지고 어떤 쿼리가 나갈지 알고 있으니 코드작성에 막힘없이 작성했으며, 사실 테스트 코드는 작성하지 않았다. 테스트 코드라기보다는 그냥 저 채팅 시뮬레이터 같은 것을 만들어서 신호에 따라 쿼리가 나가고 데이터 업데이트가 잘 되는지에 대해서 만 시뮬레이션을 돌렸다.

 

이번 개발 역시 시간이 부족했다. 여름휴가 기간이다 보니 다들 휴가를 가고, 휴가 복귀 후에 테스트를 원하시니... 적어도 8월 초에는 마무리를 지어야 하지 않겠는가?... 

실제 개발한 API는 17개 이고 현재 진행 중인 고 루틴에 대해 확인하는 테스트용 api까지 총 18개를 작성했다. 3개를 제외한 나머지는 단순 CRUD 여서 코드가 많았지 생각보다 고민시간 없이 수월하게 작성했으며 3개는 라이브와 고 루틴의 생성과 삭제에 연관되어 있다 보니 생각보다 고민을 많이 하면서 작성했다.

 

 

 

 

 

'개발일지' 카테고리의 다른 글

FRP 적용  (0) 2024.12.31
10월 개발일지  (1) 2023.11.01
6월 개발  (1) 2023.07.09
5월 개발  (0) 2023.06.11

지난 한 개의 프로젝트 종료 이후 새로운 프로젝트의 담당이 되었다. 증권방송 관련된 개발 건이었는데, 생각보다 개발의 소요가 크지 않았다.

 

스페이스 리뷰커밋 기준으로 작성한다.

 

1. 스웨거 수정 

이번 프로젝트는 지난프로젝트 와 달리 스웨거가 존재한다. API 문서 가 따로 존재하지 않고 이렇게 스웨거로 있다 보니 정말 편하다. 지난 프로젝트는 생각보다 읽어야 할 문서 가 많고 버전관리가 잘되지 않고 있어서 직접 코드를 확인해서 검증하는 2번씩 봐야 했다면, 스웨거가 있어 정말 비즈니스에 대해 빠르게 파악이 가능했다. 다만 몇몇 스웨거는 작동되지 않고 있어서 수정이 필요했는데.

 

예전 개인프로젝트 간에 작성했던 스웨거 와는 너무 달랐다. 어노테이션을 이용해 클래스 필드 위에 작성을 했었는데.  여기 적혀있는것은 swager.html , rapidoc.js, 그다음 호출에 대한 request, response의 json 데이터이다. 간략하게 보여주자면

요런 식으로 json 파일에 원하는 주소와 스키마 그리고 값을 입력해 주는 방식인데 생각보다 괜찮다. 당연히 이렇게 swagger.html을 서버와 같이 말아 올려 야하기 때문에 static에 넣어서 사용하는 게 생각보다 좋은 거 같다. 다만 저 json 파일에서 수정할 때 생각보다 헷갈린다. 

 

2. 버전업 API 

보통 신규 api 가 아닌 기존 api 를 수정할 때 이렇게 버전업 전략을 사용한다고 한다. 기존 api를 사용하던 곳에서 생길 문제를 이렇게 예방한다고 하는데 이런 부분에 있어 생각을 해보지 않아 api/개발/필요로 바로 개발했다가 위와 같은 설명을 듣고 v1.2 이런 식으로 추가해서 변경했다.

전체적인 로직이 변경되지 않는이상 대체적으로 파라미터를 하나 추가하는 편인데, 쿼리 자체가 변경되어야 한다고 판단해서 위와 같이 버전업을 선택했다.

- 테스트 코드 

확실히 이번에는 시간적인 여유가 많이 있어 TDD 도 적용했다. 아무래도 기존 프로젝트 에 testify 같은 라이브러리의 접목이 과하다고 생각되어 그냥 일반적으로 interface로 모킹을 적용해 작성했다.

Go에서 지향하는 테이블 주도 테스트이다 아래와 같다.

위와 같은 방식으로 functionName 을 집어넣어 내가 테스트하고자 하는 함수에 대해 분기를 나눠주기 위해 iota를 적용해 작성했다. 자바로 치면 enum이라고 생각하면 편하겠다.

우측 사진에 보이는것처럼 테스트하고자 하는 서버스의 목 데이터를 넣어서 테스트해주면 된다.

고에서는 test 코드 안에서 서로 임포트가 불가능하다 따라서 이렇게 mock 폴더를 만들어서 관리를 해주었다. 

LiveListInjectMock 은 인터페이스이다. 

그래서 Success와 Fail 케이스는 각각 저 인터페이스를 구현하고 
해당 서비스에서는 LiveListInject 인터페이스를 주입받아 호출을 하게 된다면? 성공 실패 원하는 주입을 통해 테스트를 효과적으로 할 수 있다.

그래서 첫 번째 위의 사진에 첨부된 코드 중 이렇게 주입이 가능해지는 것이다.

&mock.MockLiveService{LiveListInjectMock: mock.LiveListMockFail{}},

 

물론 이렇게 테스트 가 가능한 이유는 기존 구현된 모든 서비스 들은 추상화되어있기에 이런 식의 코드 구현이 가능하다.(이래서 코드 설계 디자인이 너무 중요한 것 같다.)

 

확실히 디자인패턴 의 공부가 이런 쪽에 있어서는 정말 도움이 많이 되는 것 같다.

 

3. 검색 API 파람 추가

기존 검색에 추가적인 파라미터가 필요했다. 예를 들어 1,2 로만 가능했던 검색이 이제는 3이라는 옵션도 추가되어야 했고 3은 전혀 다른 쿼리가 나가야 했다. 

이에 따라 enum을 이용해 swith 문으로 해결했다. 

swich case 3: 새로운 로직 default : 기존로직 이렇게 작성된다면 기존 코드 의 사이드 이펙트 없이 수정이 가능하나 기존함수를 수정해야 하기 때문에 쿼리에 대한테스트가 필수적이다.

 

-gorm prealod 기능

gorm 은 eagar 기능 인 프리로드를 지원해 준다. in query로 한방에 날려주는데 생각보다 성능이 좋다.

다만 이번 에는 preload를 서브쿼리를 적용해서 사용했어야 했는데 생각보다 코드가 깔끔하다.

생각보다 코드가 명확해서 읽는 사람도 편하다 다만 서브쿼리의 경우 위에 보이는 것처럼 스트링으로 처리하기 때문에 오타가 나면 생각보다 찾기 어려우니 주의하자.

메인 쿼리는 index_merge, eq_ref 

프리로드 쿼리 는 range, eq_ref, index

 

메인쿼리의 index_merge는 검색해 보니 생각보다 성능이 좋은 편은 아니라고 한다. 테이블에 설정된 여러 인덱스를 태워야 하기 때문이라는데 디비 설계상 중복된 데이터가 메인쿼리에서 발생할 수 없다고 판단했다. 테이블에 존재하는 인덱스 칼럼들은 연관관계없는 테이블에 대해 참조가 필요했기에 인덱스가 걸려있어, 중복데이터 발생이 없다고 판단하고 넘어갔다. 

 

*추가적으로 개발건에 대해 기존코드 수정은 위와 같이 코멘트를 남기려고 노력한다. 비즈니스 가 변경되고 ,특정한이유에 있어 추가되는 코드들은 상식적으로 맞지않고, 처음보는 사람에게 읽기 어려운 코드를 제공한다고 생각한다. 비즈니스로직 의 히스토리를 모르고 있다면 코드독해에 상당한 어려움이 있고 실제로 지난 프로젝트에서 상당한 어려움을 겪어 위와같이 커멘트를 작성하고자 한다.

 

-테스트 코드

테스트 코드는 단순 쿼리 확인을 위해 dryrun 옵션을 세션에 추가해 작성했다. 

dryrun 은 실행되는 쿼리의 쿼리 스트링만을 뽑아낼 수 있어며 아래 보이는 result처럼 어떤 쿼리가 발생되는지에 대해 쿼리로 뽑아서 확인할 수 있다. 

다만 문제점으로는 preload로 실행되는 서브쿼리와 인쿼리는 제공되지 않아 추가적인 쿼리 확인은 다른 테스트 방식이 요구된다.

따라서 메인쿼리의 확인을 위해 위와 같이 작성했고, 프리로드 쿼리의 확인을 위해 직접적인 디비호출을 실행했다.

 

4. CORS 에러

문제점 : 우리는 ovp 플랫폼을 사용한다. ovp 플랫폼에서 제공되는 url을 토큰과 같이 프런트로 내려주게 되고 프런트는 이 url 경로를 통해 리소스를 요청한다. 문제는 url 경로를 통해서 요청되는 것은 redirect 요청이고 그 플랫폼에서는 redirect 요청에 따른 진짜 리소스를 제공하게 된다.

여기서 리다이렉트 하는 구간에서 CORS 에러가 발생하게 된다.

ovp 플랫폼에서 제공하는 플레이어를 이용한다면 cors 에러는 발생하지 않는다. 왜냐하면, hls로 요청하고(http live streaming protocall) 그걸 임베디드로 띄워서 사용하면 되기 때문에 문제가 없다.

그러나 pip 모드 문제점이 있어 videojs 라이브러리를 프런트 팀에서 선택하게 되고, 프런트에서 요청하고, 데이터를 받아와야 하는 상황에 처한 것이다.

소통의 중요함을 너무 크게 느꼈고, 에러의 문제점이 어디에서 도출되는지 에 대한 명확한 고민 없이 코드 확인부터 하다 보니 너무 많은 시간을 이 에러 핸들링에 소모했다.

1. cors 에러가 발생된다고 해서, 어떤 요청 어디에서 발생했는지에 대한 자료요청을 하지 않고, 서버의 cors 설정을 확인한 점

2. ovp 플랫폼과 연락해보지 않은 점

3. cors 발생의 헤더 나 리스폰스 값을 정확하게 한인 해보지 못한 점

4. 프런트와 의 소통미스 hls, m3u8 등 정확한 의미를 인지하지 못한 점

 

1,2,3,4번의 혼합으로 정말 크게 뺑 ~~~~ 돌아갔다.

결국 해당 ovp플랫폼에서는 우리와 같이 사용하는 케이스에 대해 인지하지 못했으며, 확인해 보겠다고 했으며 우리는 서버에서 요청해서 리다이렉트 URL을 파람에 추가해서 내려주기로 했다. 

당장 7월 6일까지 테스트 서버 오픈이기에 위와 같은 방법으로 임시 해결책을 적용하였다. 이러한 방법이  해결책이기에 명확한 해결방법이 제공되었으면 좋겠다... 아니면 무엇을 놓쳤는지 정말 모르겠다.

 

5. SPA Static으로 말아 올리기

CORS와 같이 나를 오래 괴롭힌 문제였다. 문제의 요지는 아래와 같다.

spa이다 보니 routing 이 당연히 server에 열린 routing 과는 다르게 존재한다. 

static 폴더와 경로의 마찰이 생길 것을 생각해, 새로운 프리픽스를 적용해서 관리하고 싶었다. 

우리는 echo 프레임 워크를 사용한다. echo에서 제공해 주는 미들웨어 하나를 적용해서 작성했다.

정말 단순하다. 이렇게만 작성해 주면 된다. staticConfig 구조체의 설명 중 HTML5의 핵심설명만 보자면 
// Enable HTML5 mode by forwarding all not-found requests to root so that
// SPA (single-page application) can handle the routing.
// Optional. Default value false.

오우 이렇게 지원을 다해주고 있다. 미친것 같다.

Root 같은 경우는 시작점에 대한 패스이다 이런 거는 config 파일을 통해 관리되고 있어 위와 같이 처리했다.

 

이렇게 말아 올렸는데 이게 무슨 일인가... 시작페이지에서 차례대로 들어간다면 정말 잘 들어가진다. 그러나 새로고침, 특정 주소입력에 대해 계속 메인 페이지로 돌아가는 게 아닌가... 

해결해 보고자 e.Static으로 file 자체를 라우팅에 리턴해보기도 하고 여러 가지 삽질이란 삽질을 하루 온종일 했다.

결국 팀장님 께도움을 요청했고 1분 만에 찾아내셨다 ㅎ 

요청되는 js 파일은 정상적으로 된다. 이건 백엔드 문제가 아니다.라고 하셨고 몇 번 더 확인해 보니. 토큰의 인증값이 사라져서 메인페이지로 리다이렉트 되는 게 아닌가? ㅎㅎ.......

 

CORS 도 그렇고 이것도 그렇고 개발자 도구를 좀 더 면밀하게 살펴보았다면 문제 해결에 있어 상당한 시간이 줄어들었다고 판단된다.

 

6. script 작성

매번 프런트의 푸시 이후 배포를 수정해 주어하는 불편함이 있었다. 테스트 서버에 대해 ci/cd 가 설정되어 있지 않아 매번 linux로 파일을 밀어 넣어야 했는데, 매번 배포 후 부탁하시는 모습이 불편해 보여 스크립트 하나작성해서 보내드렸더니 너무 좋아하신다.

 

스크립트 라고 해봐야 vue 빌드하고 scp로 서버에 날리고, 빌드된 폴더를 삭제하는 꼴랑 몇 줄 안 되는데..

#!/bin/sh

echo 빌드 폴더 사내서버 패치 
echo 빌드 Vue 폴더 
npm run build

sleep 1

scp "리눅스 주소 ㅎㅎ"

sleep 1
echo Dist 빌드폴더 삭제
rm -rf dist

 echo 빌드 폴더 사내서버 패치 완료

bash 스크립트 작성자에게 정말 초보처럼 보이고 이상해보일진 모르지만 누군가에게 도움이 되는 코드를 작성해서 생각보다 기분이 좋았다.

 

 

총평: 이번 프로젝트에서는 코드의 개발보다 프런트와 의 의사소통, 데이터 세팅 테스트 서버 구성 등 다양한 경험을 해볼 수 있는 시간이 되었던 것 같다. 나의 부족한 부분 특히 디버깅의 미숙한 부분이 생각보다 많이 느껴져서 부끄러웠으며, 테스트 코드 작성은 생각보다 즐거운 시간을 보낸 것 같다. 항상 이런 여유로운 시간에 따라 프로젝트를 하고 싶지만 이건 꿈같은 바람이지 않을까 한다. 테스트 코드 통과를 마지막으로 글을 마치고자 한다.

'개발일지' 카테고리의 다른 글

FRP 적용  (0) 2024.12.31
10월 개발일지  (1) 2023.11.01
7월개발~ 8월초  (0) 2023.08.16
5월 개발  (0) 2023.06.11

회사에서 지정된 API 설계 및 배치 프로세스를 작성한 회고록을 작성하고자 한다.

1. 배치 프로세스 

- 배치 프로세스 는 cms 서버에서 돌아간다. cms 서버에서 메모리 할당량이 넉넉해서 cms 서버에서 구축하게 되었고 하나의 고 루틴을 할당하고 ticker를 설정해 매 1분마다 신호를 보내 고 루틴이 실행될 수 있도록 작성하였다.

- 배치의 특성상 프로세스 내에 설정값들을 yaml 파일에 따로 뺴서 작성하고자 했으나 table에 설정하여  설정값에 대해 유연성을 두었다.

- 컨피그 에서 설정한 키값 "delete_cron"을 이용해 값을 가져오고 조회가 실패하고, record not found 에러가 생긴다면 테이블에 인서트를 수행하게 작성하였으나. (이 코드는 추후 버그가 발생할 수 있는 코드가 될 수 있다고 리뷰를 받게 되어 삭제했다.)

- 테이블 구성으로는

  • pk 값 "delete_cron 을 설정해 주고", Is_Use라는 필드를 넣어 사용되는 필드인지에 대해 추가를 했으나 실제 코드상에서 검증하는 부분이 없어 리뷰에서 검증하는 부분에 추가적인 코드작성 요청을 받았었다.
  • code_name을 넣어 이 컨피그 값이 어떤 역할을 하는지에 대해 명확하게 작성하기 위해 작성하였으며
  • code_value는 크론 값 자체를 스트링으로 넣어 배치 가 언제 돌아가는지에 대한 설정값을 추가하였다.
    (크론 [분 / 시간 / 일 / 월 / 요일 ] 총 5자리,  이며  각 자리는 정해진 범위가 존재하며 알맞게 사용되어야 한다. https://en.wikipedia.org/wiki/Cron)
  • 마지막 실행시간
  • Msg 마지막 실행 에서 몇 개의 라인이 삭제되었는지에 대해 히스토리를 남기기 위해 추가했다.

- 크론 의 값을 가져와 cron 라이브러리 의 cron.Schedlue 타입을 가져와 크론 값 parse를 수행한다.

- parse 에서 전해진 Job과 마지막 실행시간을 비교해서 마지막 실행시간 다음 job 배치 의 실행 시간 비교해서 함수 실행 여부를 판별한다.

- 실행 여부가 결정되면 정해진 배치 임무에 따라 배치 삭제를 진행하게 되고 배치 삭제에 대한 결과 로우와 마지막 수행 시간을 각각 업데이트해주면 된다.

 

예전 스프링배치 를 이용해 배치 프로세스를 작성했는데, 그때와는 사뭇 다른 배치 프로세스를 고 루틴을 이용해 이렇게 쉽게 작성하였다. 

물론 배치 인서트 작업이 배로 어렵다는것은 안다 추가적인 필드가 필요하고 실패지점에 대한 관리와, 다음 배치실행 시 실패지점으로 부터 시작하는 등의 추가적인 로직 작업이 필요하지만, 삭제 작업을 위와 같이 고 루틴을 이용해 손쉽게 작성할 수 있다는 점이 매력적인 것 같다.

 

2. API 설게

 

- 콘서트 의 특정 공연에 대해 관리 및 수정을 해야 하는 API를 개발해야 했다. 이에 따라 추가적인 테이블 설계를 하였는데 회사의 모든 테이블 은 연관관계 가 없다. 다시 말해 db 상의 연관관계가 존재하지 않으며, 코드상에 연관관계를 설정해 사용하고 있다. 

이러한 특성에 따라 복합키 의 사용이 생각보다 많이 있는데 어떤 테이블 에는 복합키 에 설정된 값과 중복해서 인덱스를 다시 설정해서 사용하는 게 아닌가? 복합키 설정에 있어서 순서가 매우 중요하다. 

예를 들어 "a,b,c,d" 를 이용해 복합키를 설정하게 된다면 b, b and c , b and d , c and d와 같이  조회를 한다면? 모두 all type으로 전체 로우를 조회하게 된다.

대신 a 를 포함한 where 조건절을 사용하게 된다면 range 혹은 const 타입으로 인덱스를 활용해 조회를 수행하게 된다.

MYSQL에서 복합키 설정을 첫 번째 열을 기준으로 인덱스가 생성되는 특징이 있는데 이를 고려하지 않고 테이블 설계, 쿼리를 작성하다 보니 나중에 실행계획서 작성할 때 인덱스 활용이 없는 경우가 발생되었고, 추가적인 인덱스 설정을 추가하게 되었다.

 

- 핵사고 날 아키텍처 가 적용된 프로젝트에 적응하기 가 너무 힘들었다. 각종 어댑터 가 존재해, 외부 시스템과 상호작용하게 된다. 도메인의 메인 비즈니스 로직이 외부요소에 의존하지 않도록 분리되어 있는데 이렇게 되다 보니 , 어느 패키지에서 어떤 역할을 할지 어떤 계층이 될지에 대해 고민하는 게 생각보다 많은 시간이 할애되었다.
spring-mvc 패턴, 레이어드 아키텍처에 적응되어 있는 나에게, 템플릿 패턴, 퍼사드 패턴 등등 디자인 패턴이 범벅되어버린 프로젝트 구조에 있어 코드를 해석하는 게 너무 힘들었다. 

인터페이스를 이용해 웬만한 호출들은 전부 추상화되어 있기 때문에 언제는 미들웨어의 인터페이스가 언제는 레포지토리의 인터페이스가  발생하기 에 어느 부분을 수정을 해야 하는지 어디에 추가하는지 이게 이벤트 발행은 가능한지에 대한 의문이 너무 많이 들었다.

 

모든 프로젝트가 동일한 패키지 구조와 코드 플로우를 가져가기에 하나의 프로세스 만 이해하게 되면 나머지는 그래도 이해하기 쉬웠다. 

메인 고 루틴 과 리퀘스트 발생에 따른 고루틴 생성과 서비스 핸들링, 카프카 설정에 따른 어드민 생성 호출 시 이벤트 발행과 같은 코드의 호출, 이벤트 컨슘에 따른 핸들링을 모두 작성하다 보니 고려해야 할 사항이 많았으며 

msa 가 적용되어 프로세스 가 각 서비스 별로 분리되어 있다. 이에 따라 공통으로 사용하는 부분을 common 패키지로  관리되고 이에 따라 common 패키지 구조체 설정이 생각보다 어려웠다. 

멀티모듈과 유사하게 common 패키지는 다른 모듈? 프로세스 컴파일 간에 같이 포함되게 된다. 따라서 가볍게 유지하는 것이 상식적으로 맞지 않겠는가? 이에 따라 고 의특성상 메서드 작성을 어디에 해야 할지에 너무 큰 고민이 되었다. toDomain? 이 된다면? 바뀌는 코드에서 가야 하는 코드에 대한  구조체에 작성하면 된다. 그런데 이 toDomain 이 다른 프로세스, 모듈에서 사용되는가? 그것은 또 아니다. 이렇게 되다 보니 common 패키지에 무엇을 작성해야 하는지에 대한 혼동이 온다. 

그 외에도 특정 이벤트 혹은 인터페이스 타입에 맞게 변경해야 하는 경우가 있다면? 팀 내부적인 특정 컨벤션이 존재하지 않다 보니, 중구난방으로 코드를 작성한 게 없지 않아 존재한다.

사수님 : "중복으로 사용하게 된다면 common에 작성하세요". 

중복으로 사용되지 않지만 함수의 의미상 common 에 위치해야 하는데 그게 아니기에 타입을 따로 받아서 새로운 타입을 만들어서 사용하게 되었는데. 참 아이러니하다.

여기서 고의 강타입 언어의 단점을 너무나도 크게 느껴졌다. db에서 가져오는 타입, 도메인으로 변경하는 타입, 이벤트 발행하는 타입에 대한 모든 변경 코드가 필요하게 되고 이에 따른 코드 작성이 어마어마하게 많다. (물론 코파일럿이 대부분 도와주었다.) 그렇다 보니 이벤트 발행에 항상 동일한 to 함수가 사용되는 것이 아니고 이에 따른 서로 다른 변환함수가 필요하게 된다. 

작성할 코드가 많고, 재사용 가능한 코드에 대해 고민을 많이 하게 되다 보니 생각보다 많은 시간을 소모했다.

 

- go 메서드 의 receiver method, value method 즉 포인터, 일반 함수라고 생각하면 된다. go uber 가이드에서 제공하는 방법으로는 하나의 방법으로 통일할 것을 권장하지만 아무래도 프로젝트에 참여한 사람이 생각보다 많다 보니 아무래도 혼합된 방법으로 구조체의 함수를 작성하게 되고 이에 따라 고 랜드에서 경고가 있는데 하나의 함수선언 방식으로 통일해 달라는 경고이다. 

이에 관해 사수 님께 여쭤보고, 이번 개발건에 대해서는 전부 receiver로 작성하게 되었다. 

 

- [에러처리]

dvt, cvt, qa 팀 의 모든 테스트를 통과했고 상용배포까지 마무리가 되었지만 갑자기 날아온 문의사항, 데이터가 삭제된다는 문의였다.

어드민 사용자 입장에서 api 호출에 대해 에러응답이 발생된다면 마치 데이터가 삭제된 것처럼 나오는데 그 현상인데 데이터가 삭제된다는 문의에 식은땀이 흘렀다. 팀장님 빠르게 로그를 검색하고, 에러의 원인을 파악하셔서 손쉽게 해결되었지만. 문제의 발생은 내가 작성한 코드에서 생겼다.  db 조회에 대한 err를 받고 그다음 로직에서 err에 대한 핸들링 없이, 코드 작성이 되어있다 보니 memory invalid exception으로 패닉으로 떨어진다.

비즈니스 로직에서 특정 칼럼 값은 항상 이벤트 발생으로 방송센터에서 넣어주는 값으로 알고 코딩을 해서 위와 같은 결과가 발생했지만, 에러에 대한 핸들링을 하지 않아서 크게 혼났다.

 

- [테스트 코드]

tdd를 적용할 시간조차 없었다. 단순 함수를 작성하고 이게 내가 원하는 쿼리가 발생하는지 에 대한 검증 정도만 있었지 tdd의 t 자조차 꺼낼 수가 없었다.. 그러다 보니 테스트 코드를 작성했어도 단순 확인하는 용도이기에 커밋을 전혀 하지 않았다.

프로젝트 종료 이후 세미나 간에 tdd 주제를 맡아 발표를 하게 되었고, tdd를 과연 저 프로젝트하는 동안 적용이 가능한가에 대한 나의 답은 NO이다.

저런 짧은 시간과, CDR 문서를 비롯한 각종 문서 지옥에서 tdd와 cdr 문서 작성 등  과 같은 중복된 일을 해야 하는가? 에 대한 의문이 생긴다. 

내가 작성하는 신규 api, function, module 은 테스트 할 수 있다. 그 코드에 대한 사이드 이펙트에 대해 어느 정도 예상을 하고 코드 작성을 하게 되니깐, 그런데 내가 작성한 것이 아닌 코드에 대해 테스트 코드를 작성하는 것은 정말 개발자 역량에 따른 천차만별이라고 생각한다. 

물론 코드를 보고 어떤 의도로 작성이 되었는지 어떤 문제점이 생길지에 대해 코드에 대한 테스트 코드는 작성할 수 있다. 고작 API 개발이니깐 그런데 이런 API 가 아닌 시스템 비즈니스 로직을 검증해야 한다면?????
또한 API 개발 간에도 문의사항으로 지속적인 로직 수정이 발생되었고, 이에 따른 테스트 코드 관리는 내가 너무 벅찬 부분이라고 생각되었다.

 

-[gorm]

정말 많은 삽질을 했다. transaction 관리를 db 호출할 때마다 개별적으로 해주어야 한다. 프로젝트 특성상 10개의 db 풀이 상주되고 있으며 상황에 따라 최대 20개까지 늘어날 수 있다. 

mysql 기본 격리 수준인 Repeatable Read에 따라 하나의 트랜잭션과 다른 트랜잭션 에서의 조회 결과는 다를 수가 있다. 따라서 nested transaction을 구성할 때 생각보다 조심해서 작성되어야 한다. 이 부분은 추후 포스팅에서 자세하게 다뤄보겠다.

 

 

 

 

'개발일지' 카테고리의 다른 글

FRP 적용  (0) 2024.12.31
10월 개발일지  (1) 2023.11.01
7월개발~ 8월초  (0) 2023.08.16
6월 개발  (1) 2023.07.09

+ Recent posts