학습/go

[golang] defer를 이용해서 깔끔하게 rollback하기

roquen4145 2024. 12. 7. 16:18

들어가기

복잡한 로직을 설계할 때는 여러 컴포넌트들이 서로 어떻게 상호작용하는지,
그리고 특정 작업을 수행하는 데 드는 비용과 그 작업이 실패했을 때 다시 원래 상태로 되돌리는 비용을 고려해야 합니다.
그래서 비용이 적게 드는 컴포넌트에 대해 먼저 작업을 시도하고,
실패했을 경우에는 빠르게 원래 상태로 되돌리는 '롤백(rollback)' 방식을 사용하기도 합니다.
이 과정에서 Golang의 defer를 사용하면 어떻게 효율적으로 개선할 수 있는지에 대해 설명합니다.

defer

golang에서 defer는 함수가 종료될 때 실행되는 코드 블록을 정의하는데 사용됩니다.

그래서 주로 리소스 해제나 파일 닫기, 뮤텍스 잠금 해제 등에 쓰입니다.

func example() {
    defer fmt.Println("이 메시지는 함수가 끝날 때 출력된다.")

    fmt.Println("이 메시지는 함수가 실행될 때 바로 출력된다.")
}

간단한 예시 코드

아래 예시를 통해 어떻게 사용하는지 알아보겠습니다.

defer를 사용하지 않은 경우

아래처럼 에러를 처리하는 부분을 defer로 사용할 수 있습니다.

func simpleWithoutDefer() error {
    err := setAComponent()
    if err != nil {
        return err
    }
    err = setBComponent()
    if err != nil {
        _ = fmt.Errorf("failed to set b, reason : %s", err)
        rollbackAComponent()
        return err
    }

    return nil
}

defer를 사용한 경우

func simpleWithDefer() (err error) {
    err = setAComponent()
    if err != nil {
        return err
    }

    defer handleError1(err)
    
    return setBComponent()
}

func handleError1(err error) {
    if err != nil {
        _ = fmt.Println("failed to set b, reason : ", err)
        rollbackAComponent()
    }
}

코드가 간단해지긴 했지만 굳이 저렇게 처리할 필요는 없어보입니다.
아래에서 더 복잡한 코드를 보겠습니다.

복잡한 예시 코드

gRPC라던가 REST API를 사용하는 경우에는 rollback이 필요한 상황에서 코드가 생각보다 복잡합니다.

단순 동작 실패 이외에 여러 가지 이유로 동작 거절이 발생할 수 있는데 그런 경우마다 롤백 코드를 넣어줘야됩니다.

defer를 사용하지 않은 경우

func complexWithoutDefer() error {
    err := setAComponent()
    if err != nil {
        return nil
    }
    resp, err := setCComponent()
    if err != nil {
        fmt.Println("failed to set c, reason : ", err)
        rollbackAComponent()
        return err
    }

    if resp.status != 200 {
        fmt.Println("failed to set c, reason : ", resp.message)
        rollbackAComponent()
        return errors.New(resp.message)
    }

    return nil
}

defer를 사용한 경우

defer를 사용하면 아래와 같이 에러 로깅과 rollback하는 부분을 따로 떼어 함수를 더 간단히 할 수 있다.

func complexWithDefer() (err error) {
    err = setAComponent()
    if err != nil {
        return err
    }

    defer handleError2(err)
    
    resp, err := setCComponent()
    if err != nil {
        return err
    }

    if resp.status != 200 {
        err = errors.New(resp.message)
    }

    return err
}

func handleError2(err error) {
    if err != nil {
        fmt.Println("failed to set c, reason : ", err)
        rollbackAComponent()
    }
}

코드를 좀 더 설명하면 return type에 err라고 미리 변수 정의가 되는 것을 볼 수 있습니다.
따라서 함수 내에서 err := 로 변수 선언을 하지 않고 바로 대입을 하는 것을 확인할 수 있습니다.


또한 defer 실행이 setAComponent 이후에 이루어지는 것을 볼 수 있는데

setAComponent가 실패하면 rollback할 필요가 없기 떄문입니다.

정리

defer를 사용하면 위와 같이 반복되는 rollback 코드 등을 간단하게 만들 수 있습니다.
rollback 뿐만 아니라 단순히 에러 로깅을 하는 경우에도 유용하기 때문에 필요에 따라 defer를 활용해보도록 해보면 좋을 것 같습니다.

반응형

'학습 > go' 카테고리의 다른 글

Taskfile을 이용하여 go lang 빌드하기  (0) 2024.11.11