우리가 테스트를 하는 이유. 근데 이제 Golang을 곁들인

우리가 테스트를 하는 이유. 근데 이제 Golang을 곁들인

안녕하세요, 금융쇼핑 PA Server Engineer 김한수입니다. 이 글에서는 “우리는 왜 테스트를 하는지?”에 대한 질문에 좀 더 깊숙이 들어가 보고 실제 Golang에서 테스트를 도와주는 도구들에 관해서 이야기해보려고 합니다.



테스트의 목적

우리가 엔지니어링에서 테스트하는 목적은 무엇일까요? 회귀 버그 방지, 코드 품질 향상 등 테스트 자동화가 주는 가치에 대해서는 널리 퍼져있지만, 그 사고 과정과 본질에 대해서 다루는 이야기는 많지 않아 아쉬운 것 같습니다. 그래서 이 글에서는 조심스럽게 그 본질에 대해서 이야기해 보려고 해요.



테스트의 본질

테스트의 본질을 이해하려면 먼저 “테스트는 어떻게 탄생했을까?”에 대한 답을 해봐야 할 것 같습니다.

이건 우리가 살아가는 세상의 원리와 어느 정도 맞닿아 있는데요, 이 세상의 대부분 도구가 탄생하는 과정은 어떤 문제를 발견하는 걸 시작으로, 그 문제를 해결하기 위해서 해결 방안(Solution)을 도출한 뒤에 그것을 만족하는 구체적인 방법(=도구)을 찾는 과정을 거치게 됩니다.


1_01

여기서 “테스트한다”는 해결 방안(Solution)에 속하고 구체적인 방법에는 여러 테스트 방법론이 해당한다고 생각됩니다. 그렇다면 좀 더 근본적으로 들어가서 테스트라는 해결 방안은 어떤 문제를 해결하기 위해 도출되었을까요?



우리는 무의식중에 테스트하고 있다

테스트가 해결하려는 문제를 알아보기 위해 한 가지 예시를 들어보겠습니다.

여러분 중에 대부분은 아마도 ‘내가 이때 출발하면 목적지에 늦지 않게 도착할 수 있을까?’라는 질문이 생긴 경험이 있을 겁니다. 그리고 이 질문을 해소하기 위해 미리 길 찾기 기능을 사용해 보곤 합니다. 우리는 이처럼 불확실함을 해소하기 위해 미리 Test 해보는 과정을 자연스럽게 경험합니다.

이걸 좀 더 근본적으로 바라보면, 인간은 불확실성에서 오는 불안감을 해결하고 싶어 하는 경향이 있는 걸 알 수 있습니다. 그래서 데이터를 모으고 미래를 예측(Test)함으로써 불안함을 완화하는 모습을 보입니다. 하지만 안타깝게도 이 과정을 거친다고 해서 우리의 의도대로 세상이 100% 돌아가는 건 아닙니다. 다만, 그 확률을 100%에 수렴하게 만드는 것에 의미가 있다고 생각합니다.

위에 살펴본 예시처럼 우리는 무의식적으로 데이터를 모으고 테스트를 통해 검증하는 피드백 순환 고리(Feedback Loop)를 만들고 있습니다. 이건 개인의 삶뿐만 아니라 비즈니스에서도 꽤 중요하고 중간에 길을 잃더라도 다시 푯대를 향해 나아갈 수 있게 도와주는 유용한 시스템입니다.


1_01

엔지니어링에서 테스트의 목적

그렇다면 엔지니어링에서 테스트는 어떤 확률을 높이기 위해 활용되는 걸까요?

먼저, 기술 관점에서 테스트의 목적을 떠올려보면 자연스럽게 회귀 버그 방지, 비즈니스 요구사항 만족이 떠오르는 것 같습니다. 그리고 메이커의 관점에서 생각해보면 결국 ‘비즈니스 리스크를 줄이기 위해서’라는 결론으로 이어진다고 생각합니다. 이것에 대해서는 아래에서 좀 더 이야기해 보려고 합니다.



회귀 버그 방지하기

소프트웨어 개발 중 신규 개발 또는 리팩토링을 통한 코드 수정 시에 회귀 버그(Regression Bug)가 발생할 수 있습니다. 회귀 버그는 ‘정상적으로 동작하던 소프트웨어가 의도치 않게 오작동하는 것’을 의미하고, 이는 유저 경험을 해치거나 비즈니스 리스크로 적용될 수 있습니다.

실제 상황을 한 번 상상해 보죠. 만약 리팩토링 후 버그가 발생했는데, 그 기능이 1분에 몇백만 원의 매출을 만드는 중요한 기능이고 자동화된 테스트가 없어서 이 문제를 실제 버그가 발생한 지 5분 뒤에 발견했다면 어땠을까요? 5분이라는 짧은 시간이지만 길게 느껴지고 정말 아찔한 순간일 겁니다. 장애를 해소한 후에 팀원의 격려와 위로를 받겠지만, 개인적으로는 다시는 겪고 싶지 않은 상황일 거예요.

그렇다면 반대로 ‘그런 중요한 기능을 왜 건드려?’라고 생각할 수도 있을 것 같습니다. 하지만, 소프트웨어를 적절한 시기에 리팩토링하지 않는다면 개발 속도가 점점 느려지고 누적되어 결국 비즈니스 속도가 느려지게 될 겁니다. 그래서 자동화된 테스트 코드를 통해 소프트웨어를 안정적으로 리팩토링할 수 있는 유연성을 확보하는 것은 중요하다고 생각됩니다.

즉, 회귀 버그를 방지하는 것의 목적은 결국 비즈니스 리스크를 줄이는 것과 연결되어 있습니다.



비즈니스 요구사항 만족하기

비즈니스 요구사항 만족은 말 그대로 소프트웨어가 요구사항에 맞게 의도대로 동작하는지 검증하는 것입니다.

예를 들어, 배포까지 1일 정도 남은 QA 과정에서 실제 비즈니스 요구사항과 소프트웨어의 동작이 일치하지 않는 이슈를 발견하는 상황을 상상해 봅시다. 이 경우 배포 시점을 늦춰야 할 수도 있고 누군가가 야근을 해서 대응하는 경우도 생길 수 있습니다. 하지만 이건 팀의 관점에서 모두가 소중한 선수이기 때문에 좋은 상황은 아니라고 생각됩니다.

반면에 개발 과정에서 이 비즈니스 요구사항을 테스트 코드로 작성해 두고 사전에 검증했다면 어땠을까요?

현실적으로 미래를 완벽히 알 수 없고 모든 케이스를 방지하는 건 어렵겠지만, 적어도 사전에 정의한 요구사항은 더 빠른 시점에 검증할 수 있었을 겁니다.

결국, 이렇게 서비스 기획부터 배포까지의 과정이 늦어지는 것도 비즈니스의 속도가 늦어지고 리스크에 해당한다고 생각됩니다. 그래서 이 리스크를 방지하기 위해 우리에게는 피드백 순환 고리가 필요하고 자동화된 테스트는 개발 과정에서 피드백 순환 고리로 동작하게 됩니다.



테스트 도구 알아보기 With Golang

지금까지 엔지니어링에서 테스트의 목적은 ‘비즈니스를 위해서’라는 이야기를 쭉 풀어봤습니다. 이 기반 지식에서 이제는 실질적으로 Go에서 어떤 테스트 도구들이 있는지 살펴보겠습니다.



Golang 기본 테스트 도구들

Go 언어가 가진 강력함은 ‘단순함’에서 나온다고 생각됩니다. 어려운 말을 단순하고 쉽게 하는 것이 어렵듯이 단순함은 때때로 강력합니다.

이 단순함을 최대한 유지하면서 최소한의 핵심 라이브러리를 곁들이면 생산성을 크게 높일 수 있습니다.




Table Driven Test (테이블 주도 테스트)

**Table Driven Test (테이블 주도 테스트)**는 ****Go Wiki에도 권장하는 테스트 패턴으로, 테이블 구조로 테스트 케이스를 추가해 중복 코드를 줄이고 새로운 케이스를 추가하기 쉽습니다.

다만, 개인적인 생각으로는 Table Driven Test는 단순한 함수에서는 유용하지만 실제 의존성이 다양한 비즈니스 로직에서는 사용하기 어려운 것 같습니다. 뱅크샐러드에서는 의존성이 다양한 복잡한 테스트의 경우 다른 stretchr/testify/suite package를 사용하는데요, 이건 좀 더 아래에서 이야기해 보겠습니다.

import "testing"

func Add(a, b int) int {
	return a + b
}

func TestAdd(t *testing.T) {
	tests := []struct {
		name     string
		a, b     int
		expected int
	}{
		{name: "양수_더하기", a: 1, b: 2, expected: 3},
		{name: "음수_더하기", a: -1, b: -1, expected: -2},
		{name: "양수와_음수_혼합", a: -1, b: 1},
		// 추가할 케이스가 있다면 단순히 여기 추가
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := Add(tt.a, tt.b)
			if result != tt.expected {
				t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
			}
		})
	}
}


TIP) subtest name에 space(공백) 대신 underscore(_) 사용하기

Go에서 *testing.T의 Run() 함수를 호출하면 subtest로 취급되고 내부적으로는 별도의 고루틴 안에서 동작하게 됩니다. (이하 subtest라고 표현하겠습니다.)

소소한 팁으로 subtest name에 space(공백) 대신 underscore(_)를 추가하면 IDE에서 해당 테스트 코드를 검색해 찾기 쉬워집니다.

실제로, subtest name에 언더바가 아닌 space를 넣고 go test를 실행하면 아래처럼 space가 underscore로 변환되어 output이 출력됩니다. 그래서 space 대신 underscore를 사용하면 복사해서 찾기 쉬워집니다.

=== RUN   TestAdd
=== RUN   TestAdd/양수_더하기
--- PASS: TestAdd/양수_더하기 (0.00s)
=== RUN   TestAdd/음수_더하기
--- PASS: TestAdd/음수_더하기 (0.00s)
=== RUN   TestAdd/양수와_음수_혼합
--- PASS: TestAdd/양수와_음수_혼합 (0.00s)
--- PASS: TestAdd (0.00s)
PASS


assertion 코드에서 반복적인 비교 제거 및 가독성 향상

stretchr/testify 라이브러리에서 assert, require package를 사용하면 assertion 코드 작성 시 생산성을 높일 수 있습니다. 각 package에 대한 설명은 다음과 같습니다.

기능 assert 패키지 require 패키지
실패 시 동작 t.Error() 호출 t.FailNow() 호출
실행 흐름 에러 로그를 남기고 계속 실행함 에러 로그를 남기고 즉시 중단함
주요 용도 값 비교, 독립적인 상태 검증 필수 전제 조건(Precondition) 검증, 에러 체크

더 자세한 내용을 예시 코드를 보면서 살펴보겠습니다. 아래는 기본 testing package만 사용해서 작성한 테스트 코드입니다. if문이 여러 개 붙어서 코드가 길어지는 모습을 확인할 수 있습니다.

import (
	"testing"
)

func TestCreateUser(t *testing.T) {
	user, err := CreateUser("김뱅샐")
	if err != nil {
		t.Fatalf("failed to create user: %v", err)
	}
	if user.Role != "member" {
		t.Errorf("expected role 'member', got '%s'", user.Role)
	}
	if user.Name != "김뱅샐" {
		t.Errorf("expected name '김뱅샐', got '%s'", user.Name)
	}
}

아래는 stretchr/testify를 사용한 코드입니다. 기존 코드와 비교하면 if문이 사라지고 훨씬 간결해진 걸 확인해 볼 수 있습니다.

import (
	"testing"
	
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCreateUser(t *testing.T) {
	user, err := CreateUser("김뱅샐")
	
	// 에러가 발생하면 user는 nil이므로 아래 실행될 코드에서 nil pointer 방지를 위해 require 사용
	require.NoError(t, err)
	
	// 마지막 파라미터로 string을 입력해 주석을 추가할 수 있음.
	assert.Equal(t, "member", user.Role, "역할은 member 여야 함")
	assert.Equal(t, "김뱅샐", user.Name)
}

또한, 아래처럼 struct끼리 비교도 간편하게 가능합니다.

import (
	"testing"
	
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCreateUser(t *testing.T) {
	// given
	userName := "김뱅샐"

	// when
	user, err := CreateUser(userName)
	
	// then
	require.NoError(t, err)
	expectedUser := &User{
		// 입력한 유저 이름, Role은 member가 반환되어야 함
		Name: userName,
		Role: "member",
	}
	assert.Equal(t, expectedUser, user)
}	

추가로, struct Equal 검증에 실패하면 아래와 같이 output으로 diff를 보여줘서 이를 기반으로 디버깅할 수 있습니다.

Error:      	Not equal: 
            	expected: &user.User{Name:"김뱅샐", Role:"admin"}
            	actual  : &user.User{Name:"김뱅샐", Role:"member"}
            	
            	Diff:
            	--- Expected
            	+++ Actual
            	@@ -2,3 +2,3 @@
            	  Name: (string) (len=9) "김뱅샐",
            	- Role: (string) (len=5) "admin"
            	+ Role: (string) (len=6) "member"
            	 })
Test:       	TestCreateUser



test fail 시 깔끔한 diff 출력

stretchr/testify 로 Equal 검증 실패 시 출력되는 diff output에 단점이 일부 존재합니다.

예를 들면 time.Time을 필드로 사용할 때 testify 는 이를 struct로 보고 내부 필드까지 모두 비교합니다. 그래서 Equal 검증에 실패한 경우 아래와 같이 조금 어려운 test output을 볼 수 있습니다.

Error:      	Not equal: 
            	expected: &user.User{Name:"김뱅샐", Role:"member", RegisteredAt:time.Date(2025, time.December, 1, 1, 36, 24, 845727000, time.Local)}
            	actual  : &user.User{Name:"김뱅샐", Role:"member", RegisteredAt:time.Date(2024, time.June, 10, 12, 0, 0, 0, time.UTC)}
            	
            	Diff:
            	--- Expected
            	+++ Actual
            	@@ -4,13 +4,5 @@
            	  RegisteredAt: (time.Time) {
            	-  wall: (uint64) 13998096397663520024,
            	-  ext: (int64) 1393209,
            	-  loc: (*time.Location)({
            	-   name: (string) "",
            	-   zone: ([]time.zone) <nil>,
            	-   tx: ([]time.zoneTrans) <nil>,
            	-   extend: (string) "",
            	-   cacheStart: (int64) 0,
            	-   cacheEnd: (int64) 0,
            	-   cacheZone: (*time.zone)(<nil>)
            	-  })
            	+  wall: (uint64) 0,
            	+  ext: (int64) 63853617600,
            	+  loc: (*time.Location)(<nil>)
            	  }
Test:       	TestCreateUser

test output의 가독성이 안 좋으면, 테스트를 고치는 시간이 오래 걸릴 수 있고 이는 생산성 저하로 이어진다고 생각됩니다. 그래서 이를 완화하기 위해서 아래처럼 google/go-cmp/cmp 를 사용해 기존 코드보다 diff를 깔끔하게 출력할 수 있습니다.

func TestCreateUser(t *testing.T) {
	// given
	var (
		userName     = "김뱅샐"
		registeredAt = time.Date(2024, 6, 10, 12, 0, 0, 0, time.UTC)
	)

	// when
	user, err := CreateUser(userName, registeredAt)

	// then
	require.NoError(t, err)
	expectedUser := &User{
		Name:         "김뱅샐",
		Role:         "member",
		RegisteredAt: time.Now(),
	}
	// 깔끔한 diff 출력을 위해 cmp.Diff() 사용
	if diff := cmp.Diff(expectedUser, user); diff != "" {
		assert.Failf(t, "expectedUser not equal to user (-expected +user):\n", diff)
	}
}

/* 이전 코드보다 가독성이 좋아진 OUTPUT

// ...
  	Error:      	expectedUser not equal to user (-expected +user):
  	Test:       	TestCreateUser
  	Messages:   	  &user.User{
  	            	  	Name:         "김뱅샐",
  	            	  	Role:         "member",
  	            	- 	RegisteredAt: s"2025-12-01 01:39:56.351955 +0900 KST m=+0.001214001",
  	            	+ 	RegisteredAt: s"2024-06-10 12:00:00 +0000 UTC",
  	            	  }
--- FAIL: TestCreateUser (0.00s)

*/



Custom Comparer

google/go-cmp/cmp를 사용할 때 cmp.Option interface를 통해 Custom Comparer를 정의할 수 있습니다. 이는 주로 기본 cmp.Diff()만으로 비교하기 어려운 type에 사용합니다.

예를 들면 decimal의 경우 decimal이 표현하는 scale까지 Equal로 비교하는 경우가 있는데, 이 경우 1000000.001000000.0001000000이라는 같은 값으로 보고 싶지만, Equal 비교 시 ‘서로 다르다’는 결과를 반환할 수 있습니다. 그래서 아래와 같이 Custom Comparer를 정의하면 이를 의도한 대로 비교할 수 있습니다.

import (
  "testing"
  
	"github.com/ericlagergren/decimal"
	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/assert"
)

// DecimalComparer cmp.Diff 옵션으로 사용할 수 있는 decimal.Big Comparer를 반환합니다.
// [usecase]
// assert.Fail(t, "expected not equal to actual (-expected +actual)", diff)
func DecimalComparer() cmp.Option {
	return cmp.Comparer(func(a, b *decimal.Big) bool {
		if a == nil || b == nil {
			return a == b
		}

		return a.Cmp(b) == 0
	})
}

func TestCreateUserAccount(t *testing.T) {
	// given
	var (
		accountName = "Grace"
		balance     = decimal.New(1000000, 2) // 10,000.00
	)

	// when
	account, err := CreateUserAccount(accountName, balance)

	// then
	require.NoError(t, err)
	expectedAccount := &UserAccount{
		Name:    "Grace",
		// decimal이 표현하는 scale은 다르지만 value는 같음
		Balance: decimal.New(10000000, 3), // 10,000.000
	}
	
	// assert.Equal(t, expectedAccount, account) -> fail
	
	// DecimalComparer를 Diff() 옵션으로 추가
	if diff := cmp.Diff(expectedAccount, account, DecimalComparer()); diff != "" {
		assert.Fail(t, "expected not equal to actual (-expected +actual)", diff)
	}

}


struct style test로 테스트 유지보수성 올리기

Table Driven Test는 단순함이 장점이지만 실무에서는 다양한 의존성과 mock 코드가 늘어나면 사용하기 어려워집니다. 그래서 단순한 유틸 함수의 경우 Table Driven Test를 주로 활용하고 비즈니스 로직 테스트와 같이 복잡하고 여러 mocking이 필요한 로직은 stretchr/testify/suite package를 사용해 유지보수성을 챙길 수 있습니다.

stretchr/testify/suite은 아래와 같이 struct 기반의 테스트 구조를 제공하며, suite.Suite를 임베딩한 struct 안에 다양한 의존성을 정의해둘 수 있습니다. 그리고 SetupTest(), SetupSubTest(), TearDownTest(), TearDownSubTest() 등 interface method를 통해 테스트 라이프사이클을 제어할 수 있습니다.

이런 구조를 통해 의존성 라이프 사이클 제어는 suite.Suite에서 담당하고 각 테스트에서는 의존성 선언을 따로 하지 않고 테스트 로직에 집중할 수 있습니다.

import (
	"github.com/stretchr/testify/suite"
	"github.com/golang/mock/gomock"
	// ...
)

type CreditUserServiceTestSuite struct {
	suite.Suite
	// 여기에 의존성 정의
	ctrl *gomock.Controller // fyi; gomock을 mocking에 사용
	userService *user_app.MockUserService
	creditUserRepository *repository.MockCreditUserRepository
}

func (s *KCBUserServiceTestSuite) SetupTest() {
  // Test 실행 시마다 실행할 로직. (의존성 초기화 등)
	s.ctrl = gomock.NewController(s.T())
  s.userService = user_app.NewMockUserService(s.ctrl)
}

func (s *KCBUserServiceTestSuite) TearDownTest() {
	// Test 종료 시마다 실행할 로직. (의존성 제거 등)
	s.ctrl.Finish()
}

실제 비즈니스 로직 테스트에서 사용한다면 아래와 같은 모습이 됩니다. 아래 코드를 통해서 실제로 의존성 라이프사이클 관리는 suite.Suite에서 담당하고 각 Test는 의존성 선언 코드 없이 테스트 코드에 집중할 수 있는 걸 볼 수 있습니다.

import (
	/* ... */
	"github.com/golang/mock/gomock" 
	"github.com/stretchr/testify/suite"
	/* ... */
)

type TermsServiceTestSuite struct {
	suite.Suite
	ctrl *gomock.Controller

	termsCli *client.MockTermsClient
}

func (s *TermsServiceTestSuite) SetupTest() {
	s.ctrl = gomock.NewController(s.T())

	s.termsCli = client.NewMockTermsClient(s.ctrl)
}

func (s *TermsServiceTestSuite) TearDownTest() {
	s.ctrl.Finish()
}

func (s *TermsServiceTestSuite) newService() *termsService {
	return &termsService{
		termsCli: s.termsCli,
	}
}

func TestTermsServiceTestSuite(t *testing.T) {
	suite.Run(t, new(TermsServiceTestSuite))
}

func (s *TermsServiceTestSuite) TestListUserTermsConsentStatusesByTermsTypes() {
	var (
		now = time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC)
	)
	
	s.Run("여러_약관_유형을_조회할_때_모두_동의한_경우_해당_수만큼_약관_상태를_반환", func() {
		// given
		ctx := context.Background()
		var (
			userID         int64 = 123
			termsTypeEnums       = []termspb.TermsType{/* ... */}
		)

		s.termsCli.EXPECT().
		  FindUserTermsConsentStatusesByTermsTypes(/* ... */).
			Return(
			  // 모두 동의한 상태면 request에 입력한 약관 유형 수와 동일한 수의 결과 반환
  			/* ... */
  		}, nil)

		// when
		termsService := s.newService()
		result, err := termsService.ListUserTermsConsentStatusesByTermsTypes(ctx, userID, termsTypeEnums)

		// then
		s.Require().NoError(err)
		s.Equal(termsTypeEnum, result.TermsTypeEnum, "반환된 약관 유형이 요청한 약관 유형과 동일해야 함")
	})
	
	// 케이스 추가 시 아래에 s.Run()과 함께 테스트 코드 추가
}

// 다른 Test 함수에서도 TermsServiceTestSuite에 정의한 의존성 사용 가능
func (s *TermsServiceTestSuite) TestGetUserTermsConsentStatusByTermsType() {
	var (
		now = time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC)
	)
	
	s.Run("단일_약관_유형을_조회할_때_동의한_경우_약관_상태를_반환", func() {
		// given
		ctx := context.Background()
		var (
			userID        int64 = 123
			termsTypeEnum       = /* ... */
		)

		s.termsCli.EXPECT().
		  FindOneUserTermsConsentStatusByTermsType(/* ... */).
		  Return(/* ... */, nil)

		// when
		termsService := s.newService()
		result, err := termsService.GetUserTermsConsentStatusByTermsType(ctx, userID, termsTypeEnum)

		// then
		s.Require().NoError(err)
		s.NotNil(result)
		s.Equal(termsTypeEnum, result.TermsTypeEnum, "반환된 약관 유형이 요청한 약관 유형과 동일해야 함")
	})
}



글을 마무리하며

이 글에서는 테스트란 피드백 순환 고리(Feedback Loop)를 만들어 ‘불확실성’을 대비하고, 더 나아가 소프트웨어의 품질을 안정적으로 유지하면서 비즈니스 리스크를 최소화하기 위한 것이라는 관점을 표현하려고 했습니다. 그래서 테스트의 본질부터 이야기를 시작했습니다.

그리고 글의 뒷부분에서는 실질적으로 Go 언어에서 생산성을 높이는 Table Driven Test, stretchr/testify의 assertion 유틸과 struct style test, 그리고 가독성 높은 diff를 위한 google/go-cmp까지 구체적인 도구들을 살펴보았는데요, 코드와 도구에 중점을 두었지만 여기서 다룬 도구 외에도 테스트에 도움을 주는 패턴 등 다양한 도구가 존재합니다. 그리고 그 도구가 탄생한 관점을 들여다보고 인사이트를 얻는 것이 중요하다고 이야기하고 싶습니다.

추가로, 이 글에서 자세히 설명하진 않았지만 작성된 테스트 코드에 Given, When, Then 구조를 일부 사용했습니다. 이 구조는 테스트의 ‘의도’를 명확하게 표현할 수 있게 도와줍니다. 이 구조는 장기적인 가독성과 유지보수성을 높이는 데 긍정적이라고 생각되고 명확하게 구조화된 테스트는 단순한 검증 도구를 넘어, 코드가 아닌 ‘비즈니스 요구사항(스펙)’ 자체를 설명하는 훌륭한 문서가 되어준다고 생각합니다. (이 관점에 대해서는 다른 글로 이야기하고 싶어서 이 글에서는 자세하게 다루지 않았습니다)

그리고 이런 관점이 모이면, 엔지니어로서 시스템 안정성 확보와 비즈니스의 속도를 지키는 일이 단순히 기술적인 숙련도 이상의 의미를 가지게 되는 것 같습니다. 이는 우리가 해결하고자 하는 근본적인 비즈니스 문제를 깊이 이해하고 개발에만 치중하는 것이 아닌 비즈니스에도 집중하는 ‘메이커’의 태도와도 연결된다고 생각합니다.

여러분은 이 글을 읽으면서 어떤 생각이 드셨나요? 나중에 기회가 된다면 함께 나누는 시간이 있길 기대합니다. 🙂




NEXT WAVE - 뱅크샐러드 대규모 채용합니다!

현재 뱅크샐러드에서는 대규모 채용 프로모션을 진행하고 있습니다!

저희 팀에서도 데이터와 함께 고객분들의 삶을 이롭게 하는 다양한 과제를 앞두고 있고 견고한 기술을 바탕으로 더 안전하고 빠른 비즈니스 여정, NEXT WAVE를 함께 만들어갈 분들을 환영합니다. 🙌

문을 두드리다 보면 열릴 것임을 믿으며 기대하고, 앞으로 합류하게 되실 분들과 함께 만들어갈 멋진 임팩트를 기대합니다. ✨





보다 빠르게 뱅크샐러드에 도달하는 방법 🚀

지원하기

Featured Posts

post preview
post preview
post preview
post preview
post preview

Related Posts