2024. 5. 13. 23:47ㆍ컴퓨터 공학
* 몇 개월 전, 회사에서 문제 해결을 위해서 리팩터링이 있었고, 그 과정에서 에러가 발생했었는데, 그 경험을 공유하고자 이 글을 작성합니다.
저에게 하루 약 120만 건 이상의 요청이 들어오는 서버에 관한 태스크가 주어졌습니다.
서버에 새로운 파이프라인을 추가했고, 이에 대해서 개발환경에서 모든 테스트를 마치고, 실제 운영환경에 배포했습니다.
무사 배포를 기원했지만, 배포하자마자 서버는 죽었다 살아나기를 반복했고, 이 때문에 바로 서버를 롤백했습니다.
저는 매우 의아했습니다. 에러가 발생한 지점의 코드를 저는 고친 것이 없었고, 해당 지점에서 원래 에러가 발생하고 있지 않았기 때문이죠.
그렇게 한 시간 동안 원인을 찾기 위해 로그를 열심히 찾았고, 결국 문제의 원인을 찾아냅니다.
문제 발생의 원인
로그를 보던 중, 이런 로그를 보게 됩니다.
{0xc00000c030, 0x0}
중괄호로 된 것은 struct 같은데,
왜 하나는 주소값이 있고, 하나는 주소값이 없는 것인가...
마침 에러가 발생한 곳은, 모두 인터페이스 nil check를 거친 곳이어서 더 수상했습니다.
그러던 중, 특이한 사실을 알아냈습니다.
바로 인터페이스의 nil이 진짜 nil이 아닐 수도 있다는 사실을요.
package main
import (
"fmt"
)
type A struct {
ID string
}
func makeInt() interface{} {
var a *A
return a
}
func main() {
var a = makeInt()
if a == nil {
fmt.Println("a is nil")
} else {
fmt.Println("a is not nil")
}
}
여기서 어떤 것이 출력될까요?
당연하게 저는 `a is nil`이 출력될 것으로 예상했지만
결과는 `a is not nil`이 출력되었습니다.
왜 그럴까요?
이건 위의 로그에서도 알 수 있는데요,

interface는 내부적으로 type과 value를 가지고 있습니다.
그리고 value만 nil이라면 nil check에 실패하게 되는 것이죠.
에러가 난 코드를 간단하게 재현해 보자면,
package main
import (
"fmt"
)
type A struct {
ID string
}
func makeInt() interface{} {
var a *A
return a
}
func main() {
var a = makeInt()
var ok bool
var b *A
if b, ok = a.(*A); ok {
fmt.Println("ok")
return
}
fmt.Println(b.ID)
}
이렇게 type 변환을 시도했고, nil이라면 실패했을 type 변환에 성공한 것입니다.
이렇게 interface가 nillable 하게 되면, 이런 문제를 야기할 수 있습니다.
그렇기 때문에 반드시 inteface는 nil이 되어서는 안 됩니다.
라이브러리 선택과 제거의 중요성
저는 서버를 수정하는 과정에서 chebyrash/promise라는 라이브러리를 사용하고 있는 코드를 보고,
일반 go 루틴을 사용하는 것과 차이가 없다고 느껴 이를 제거했습니다.
GitHub - chebyrash/promise: Promise / Future library for Go
Promise / Future library for Go. Contribute to chebyrash/promise development by creating an account on GitHub.
github.com
그리고 이것이 문제를 드러나게 했습니다.
promise의 내부 코드를 살펴보면 다음과 같은 코드가 있습니다.
func (p *Promise[T]) handlePanic() {
err := recover()
if err == nil {
return
}
switch v := err.(type) {
case error:
p.reject(v)
default:
p.reject(fmt.Errorf("%+v", v))
}
}
promise 실행 중, 에러가 나면 recover()를 통해서 panic을 잡아주는 로직이 존재했던 것입니다.
더구나 아무런 로그를 남기지 않으니 알 도리가 없었죠.
이를 통해 문제가 발생한 이유를 알아냈습니다.
- 원래 코드에 문제가 있어서 panic을 발생하고 있었으며,
- promise의 recover 때문에 에러가 발생하지 않고 있었고,
- 로그를 남기지 않았기 때문에 문제가 발생하는 줄도 몰랐던 것입니다.
라이브러리를 제거할 때, 내부에 있는 구현부까지 확인했어야 했는데,
제 불찰로 인해 문제가 발생했습니다.
교훈
이 문제를 통해서 얻은 교훈은 다음과 같습니다.
- 언어의 특성을 잘 이해하고 코드를 작성해야한다.
- nillable한 인터페이스를 만들지 말자.
- 로그를 잘 찍자.
'컴퓨터 공학' 카테고리의 다른 글
멱등성 있는 이벤트 발행하기 (3) | 2025.03.12 |
---|---|
(디자인 패턴 복습 시리즈) 어댑터 패턴(Adapter pattern) (0) | 2023.02.04 |
(Python) FastAPI MVC(1) (0) | 2023.01.20 |
웹 서버 vs 웹 애플리케이션 서버(Web Server vs WAS) (0) | 2023.01.02 |