JS 월드에서 GO 월드로 이사한 후기
2025-01-0314 분 소요0
TSBOARD를 개발하면서 여러 도전들이 저를 기다리고 있었지만, 이번 백엔드 교체가 어쩌면 지금까지의 도전들 중 가장 험난하고도 다이나믹한 도전이 아니었나 생각합니다. 저의 도전기가 또 다른 누군가의 모험심을 자극시켜 줄 수 있다면 좋겠다는 생각으로 (조금 졸린 와중입니다만 ㅎㅎ) 두서 없이 이것 저것 기록의 파편들을 남겨보고자 합니다.
새로운 언어는 어떻게 배우는가
이 곳에서도 언급한 적이 있었지만, 저에게 있어 웹은 곧 PHP이고, PHP가 곧 웹개발을 뜻했습니다. 이제는 10년도 더 된 이야기 입니다만, 제가 처음 웹 프로그래밍이라는 걸 접했던 때는 제로보드4가 웹 세상을 지배하고 있던 때였고, 저는 그 때 스킨 같은 걸 만들어보면서 이것 저것 만드는 재미로 PHP4 라는 희대의 스크립트 언어를 배웠습니다. 당시에 사용했던 자바스크립트는 jQuery 의 다른 말이었기 때문에 jQuery도 필요에 의해 조금 배워서 썼던 때였네요.
당시에는 구글링이 곧 프로그래밍이었습니다. 스택오버플로우와 누군가의 블로그를 구글링으로 찾아가면서 궁금한 걸 해결해가는 과정이 저에게 있어 새로운 언어나 라이브러리를 배우는 과정이었습니다. 그러다가 직장인이 되고, 필요에 의해서 C++을 만지기 시작하면서 회사에서 제공하는 교육들을 통해 조금쯤은 더 체계적으로 새로운 언어나 프레임워크에 대해서 익혔던 것 같네요.
이번 TSBOARD의 새로운 백엔드는 고(Go) 언어로 작성되어 있습니다. 이전에 단 한 번도 써본 적이 없는 언어인데, 이번에는 어떻게 배웠을까요? 이번에는 책을 사거나 누군가의 블로그들을 구글링하거나 혹은 동영상 강의를 듣지 않았습니다. TSBOARD를 시작할 때는 그래도 패스트캠퍼스와 같은 동영상 강의 플랫폼에서 Vue 와 같은 프레임워크를 어떻게 쓰는지 등을 배웠었는데, 이번에는 아예 그런 과정도 없었습니다.
그저, ChatGPT를 옆에 켜두고, 처음부터 언어의 특징에 대해서 물어보고 제가 기존에 익혔던 언어와 유사점과 차이점을 계속 물어보면서 예제 코드를 받아보고, 제가 작성한 코드에 어떤 문제가 있는지 검토를 부탁하는 식으로 개발을 진행했습니다. 기존에 책이나 블로그 등을 통해서 학습하는 방식에 비해서, 효율성이 일단 두말 할 나위 없이 좋아졌고 이해가 더 잘되었습니다. 구글링 조차도 이제는 LLM이 대신 해주니까 새로운 언어를 배우는데 있어서 학습이 문제가 되거나 하진 않았습니다. 지금 시대에 태어났더라면 프로그래밍을 더 잘했을텐데 하는 아쉬움까지 들 정도입니다. ㅎㅎ
만약 새로운 언어를 배우는게 이전처럼 많은 시간과 노력을 요구했거나, 제가 그런 어려움을 극복할 정도의 필요를 느끼지 못했더라면 이번 백엔드 교체는 아마도 상당히 미뤄졌거나 어쩌면 불가능 했을 겁니다. 다행히도 GPT 덕분에 새로운 언어를 배워서 써야 한다는 부담은 적었고, 어쩌면 조금 만만하게(?) 생각할 수 있었던 것 같습니다. 물론 실전에 투입되고보니 결코 만만치 않았다는 점은 지나고보니 깨달은 것이지만요.
고(Go) 언어, 대체 어떤 언어인가
아직 고(Go)라는 언어를 그렇게 오랜 기간동안 사용해보진 않아서, 솔직하게 말씀드리면 잘 모르겠습니다. 겉보기에는 더할 나위 없이 심플한데다 고루틴이라는 막강한 동시성 제어까지 만능 언어처럼 느껴지기도 하는데, 일단 써보면 꼭 그렇진 않다는 걸 금방 알게 됩니다. 그러고보니 일전에 긱뉴스에서 Go vs Bun 비교(https://tsboard.dev/blog/sirini/41)하는 내용의 이 곳 블로그 글을 요약해서 공개한 적이 있었는데, 그 때 어떤 고 언어 사용자분께서 자칫 Go 언어에 대한 잘못된 편견이나 선입견을 줄 수 있다고 지적해주신 게 생각이 나네요. 어떤 마음으로 의견을 전해 주셨는지 이해가 되는 한 편, 그럼에도 어디까지나 도구로서의 언어는 목적에 맞게 평가되어야 한다는 생각이 그럼에도 들었습니다.
제가 배우고 사용해보면서 느낀 고 언어는 정말 절제된 언어라는 점이었습니다. 변수명도 길지 않고, 명시적으로 무언가를 거창하게 하지 않습니다. 반복문, 타입 정의 등은 모두 키워드가 하나이고 그 마저도 사용 방식이 정해져 있습니다. 처음 접하면 좀 이상하다 싶은 건 인터페이스 정도인데, 이제는 좀 적응이 되었습니다. 포인터 같은 건 기존에 C 계열 언어를 써보면 익숙하니까 반갑기도 했구요. 이런 단순하고 절제된 설계 덕분인지 모르겠지만 컴파일 언어 치고는 컴파일이 정말 빠릅니다. 외부 모듈 의존성 관리하는 것도 이 정도면 정말 깔끔하구요. 나무랄 부분이 별로 없습니다.
그럼에도, 감히 말씀드리자면 아쉬운 부분들이 적진 않았습니다. 일단 가장 큰 불만사항은 생각보다 쓸만한 라이브러리가 다양하지 않다는 점입니다. 이미징 라이브러리의 경우만 보더라도 일단 몇 개 뿐이고, 그 마저도 성능 측면에서 괜찮아 보이는 건 libvips 같은 외부 라이브러리를 사용하는 것 정도입니다. Go 언어가 스크립트 언어에 비한다면 충분히 의미있게 빠르겠지만, 이렇게 핵심적인 라이브러리들이 다 C 기반 라이브러리에 여전히 의존하는 점은 역설적이게도 Go 언어의 한계를 보여주는 부분이라고 생각합니다. 즉 C나 C++을 대체하는 언어는 아니라는 점입니다. 지향하는 바가 다르다는 걸 느낄 수 있었습니다.
그리고 가끔 왜 이렇게 했을까? 하는 부분들이 있는데, 2가지 정도만 언급해 보겠습니다. 첫번째는 if err ≠ nil { … } 과 같은 에러 핸들링입니다. 에러 처리는 Go 언어에서 즉시 하도록 설계되어 있고, 나중으로 미루거나 한 번에 모아서 한다 같은 개념은 없습니다. 물론 여러 개의 에러들을 핸들링하는 공통 로직이 있다면 그걸 묶어서 다시 에러 처리를 하면 되겠지만, 어쨌든 저 에러 처리 구문은 계속 등장합니다. 정말 나중에는 if err 코드를 입력하는 것 조차 짜증이 날 때가 있습니다. defer 같은 키워드도 제공해주면서 에러 처리는 왜 저렇게 하도록 하는 걸까 싶네요. 굳이 장점으로 뽑자면, 에러가 생기는 즉시 핸들링을 명시적으로 하기 때문에 문제를 빠르게 확인할 수 있다…? 잘 모르겠습니다. 일단 저렇게 해야 하니까 해야죠 뭐.
두번째는 슬라이스인데, var results []Result 이 코드와 results := make([]Result, 0) 의 차이입니다. Go 언어가 저보다 더 생소하실 분들을 위해 설명드리면, 두 코드는 기본적으로 같은 목적으로 사용됩니다. results 라는 변수는 슬라이스 변수인데, C 언어스럽게 표현해보면 일종의 배열의 포인터를 가지고 있는 변수입니다. 그래서 둘 중 어느 방식으로 변수를 선언하더라도, results = append(results, item) 처럼 배열에 값을 더 추가하거나 등의 작업을 할 수 있습니다. 여기까지만 읽으면 뭐가 문제지? 라고 생각하실 수 있는데… 문제는 저 코드들이 서로 다른 표현을 하는 이유가 있다는 점입니다.
var results []Result 같은 경우에는, 초기값이 nil 입니다. C++ 에서 말하는 nullptr 이랑 같다고 보시면 되고, 자바스크립트에서는 null 과 같다고 보시면 됩니다. 그리고 results := make([]Result, 0) 이 코드에서 results 는 nil 이 아닙니다. 빈 배열 슬라이스를 가지며 단지 크기가 0 일 뿐입니다. 좀 더 생각해보면, 초기화가 어쨌든 되어 있습니다. 응? 초기화가 안되어도, 초기화가 되어도 동작이 똑같다고?? 그렇습니다. 둘의 차이는 초기화의 차이지만, 동작은 동일합니다. 그럼 대체 뭐가 문제일까요?
문제는 아무런 값도 추가하지 않고 그냥 변수만 선언한 상황에서 값을 반환할 때 입니다. 특히 JSON 형태로 클라이언트에게 값을 전달해줘야 하는 경우를 생각해 볼 수 있습니다. 우리가 자바스크립트로 예를 들어서 서버로부터 넘어온 값을 받는다고 합시다. 이 때 results 라는 이름의 배열에 값들이 하나도 없을 경우, 우리는 무엇을 기대할 수 있을까요? 당연히 [] 입니다. 이렇게 넘겨 받아야 빈 배열이니까 results.length 같은 걸 사용 할 수 있겠죠.
results := make([]Result, 0) 으로 작성한 코드는 우리의 기대대로 동작합니다. 초기화가 되어 있고, 빈 배열 슬라이스를 가지므로 JSON으로 리턴할 때 [] 으로 리턴됩니다. 하지만 var results []Result 는 [] 을 리턴하지 않습니다. 이 코드는 null 을 리턴합니다. 초기화가 되어 있지 않기 때문에, nil 값이 JSON 형태로 변환되면서 null 이 됩니다. 그럼 results.length 같은 걸 쓸 수 있을까요? 안되죠. 추가적으로 클라이언트에서는 null 검사를 해줘야 합니다. 그냥 빈 배열로 넘어오면 되는 문제인데, 생각지도 못하게 일이 꼬일 수가 있습니다. 저처럼 Go 언어에 익숙하지 않은 사람들은 이 함정에 생각보다 쉽게 빠질 수 있어서 주의가 필요합니다.
언어 자체적으로 비슷하게 동작하지만 서로 다른 결과를 낼 수도 있는 방식을 제공하는 건 조금 의외였습니다. 그 흔한 while 문도 없고 for 문으로 통일했으면서 왜 저건 저렇게 두는 건지 의아했습니다. 물론 제가 아직 잘 몰라서 그런 것이겠지만, 처음 접하는 입장에서는 저 차이를 모르고 그냥 막 썼다가 피를 보고나니 조금 물음표가 생기게 된 것도 사실입니다.
물론 저거 말고도 있습니다만, 장점도 그에 못지 않으니 이 정도로 마무리 하겠습니다. 장점 하니까 생각나는데, 아까 말씀드렸던 defer 키워드는 그래도 쓸만합니다. 예를 들어 어떤 핸들러를 열어놓고, 해당 코드를 포함하는 함수가 리턴하기 직전에 항상 닫아야 할 경우, Go 언어는 중간에 그 많은 if err ≠ nil { … } 코드마다 핸들러의 자원을 해제하는 코드를 추가해야 합니다. 끔찍하죠. 그러나 defer 키워드가 있으면 언제 로직이 종료되고 함수를 떠나더라도 그 직전에 항상 자원 해제 코드를 실행해 줍니다.
// 대시보드용 최근 게시글 목록 가져오기 (TSBOARD admin_repo.go 발췌)
func (r *TsboardAdminRepository) GetDashboardPosts(bunch uint) []models.AdminDashboardLatestContent {
items := make([]models.AdminDashboardLatestContent, 0) // 빈 배열 슬라이스는 반드시 make로!
query := fmt.Sprintf("SELECT uid, board_uid, user_uid, title FROM %s%s ORDER BY uid DESC LIMIT ?",
configs.Env.Prefix, models.TABLE_POST)
rows, err := r.db.Query(query, bunch)
if err != nil {
return items
}
defer rows.Close() // GetDashboardPosts 함수가 종료되는 시점에 맞춰서 호출해줍니다.
for rows.Next() {
item := models.AdminDashboardLatestContent{}
var boardUid, userUid uint
err = rows.Scan(&item.Uid, &boardUid, &userUid, &item.Content)
if err != nil {
return items
}
boardId, boardType := r.FindBoardIdTypeByUid(boardUid)
item.Writer = r.FindWriterByUid(userUid)
item.Id = boardId
item.Type = boardType
items = append(items, item)
}
return items
}
defer 키워드가 없었더라면 정말 어땠을지 끔찍하네요. 이 키워드 만큼은 특별히 예제 코드로 소개해 드리고 싶어서 가져왔습니다.
JS 월드에서 GO 월드 이사 후기
어쩌다보니 글이 또 길어졌는데, 이제 마무리 할 겸 이사 후기를 말씀드리겠습니다. 이제 저에게 JS 월드는 좀 익숙해진 바이크같은 느낌입니다. 배달용 스쿠터 말고 할리 데이비슨같은 그런 멋진 바이크요. 여러 대를 굴리면 가슴이 웅장해지고, 나름 쾌적하게 서비스를 운영할 수 있습니다. 물론 규모가 커지게 되면 여러 대의 바이크를 굴려야 하는 단점이 있지만, 그럼에도 작은 서비스나 빠르게 가야 할 때는 가끔 찾게 될 것 같습니다.
반면 GO 월드는 좀 이게 맞을지 모르겠는데 11인승 승합차같은 느낌입니다. 이것 저것 필요한 걸 잔뜩 실어두고 여러 명이 탑승해서 목적지까지 갈 수 있고, 고속도로에서 나름 속도도 낼 수 있는 그런 승합차를 타는 느낌입니다. 가까운 목적지(= 상대적으로 작은 작업) 부터 비교적 먼 거리(= 상대적으로 큰 규모의 작업)까지 두루 편하게 이용할 수 있다는 느낌입니다. 저의 경우에는 멋진 할리 데이비슨 바이크도 좋지만, 성향상 11인승 승합차가 더 편하고 익숙한 것 같습니다. 새로운 언어에서 느껴지는 익숙한 승차감이라고나 할까요?
만약 새로운 백엔드 작업이 있고, 사용자의 규모면에서나 서버 자원의 풍족함 등을 감안해야 할 경우 저는 이제 두 번 고민하지 않고 Go 언어로 작업을 하게 될 것 같습니다. 앞서 여러 불만사항들을 말했지만 그럼에도 이 언어는 충분히 배우고 부딪쳐 볼 가치가 충분한 언어라고 생각합니다. TSBOARD 뿐만 아니라 다른 목적으로도 충분히 활용 가능한 언어이니만큼, 이번에 Go 언어를 처음 접해보신 분들이라면 한 번쯤 본인의 프로젝트에도 활용해 보시는 걸 추천드리고 싶습니다.
언젠가 Go 언어에 충분히 능숙해지면 그 때 또 후기 비스무리하게 글을 끄적여 볼께요. 😊
