← 모든 글
DevOps

테스트를 돌리려면 사내 서버가 살아 있어야 했다

설정 서버가 응답하지 않으면 테스트도 부팅되지 않았고, 리포지토리 테스트는 데이터베이스에 이미 들어 있는 값을 쓰고 있었다. 설정을 걷어내고 테스트를 떼어낸 과정을 적었다.

테스트를 돌리려면 애플리케이션이 떠야 하고, 애플리케이션은 부팅하기 전에 사내 설정 서버에서 값을 받아야 했다. 서버가 응답하지 않으면 테스트가 실패하는 것이 아니라 시작조차 되지 않는다.

평소에는 걸리지 않았다. 서버는 대체로 살아 있고, 살아 있는 동안에는 이 연결이 보이지 않는다. 그래도 테스트가 사내 네트워크 안에서만 도는 상태라는 것은 그것과 별개의 사실이었다. 세션 작업으로 테스트를 자주 돌리게 되면서 이 전제가 눈에 들어왔다.

설정을 바깥에서 받아 오고 있었다

부팅 순서를 따라가니 애플리케이션이 뜨기 전에 먼저 읽는 파일이 있었다. 그 파일에는 설정 서버 주소와 이 서비스의 설정 이름만 들어 있었다.

spring.cloud.config.uri    사내 설정 서버 주소
spring.cloud.config.name   이 서비스의 설정 이름

나머지는 전부 서버에서 받았다. 데이터베이스 접속 정보, 캐시 주소, 토큰 서명 키까지 거기 있었다. 운영에서는 합리적인 구조다. 값을 한곳에서 관리하고 배포 없이 바꿀 수 있다.

문제는 테스트도 같은 경로를 탄다는 것이었다. 테스트는 애플리케이션을 띄우고 시작하므로, 부팅이 설정 서버에 기대면 테스트도 기댄다. 서버가 없으면 테스트가 아니라 부팅이 실패했다.

flowchart TD
  A["테스트 실행"] --> B["애플리케이션 부팅"]
  B --> C{"설정을 어디서 받나"}
  C -->|이전| D["사내 설정 서버 조회"]
  D --> E["받아 온 주소로 데이터베이스 연결"]
  E --> F["이미 들어 있는 데이터에 의존"]
  C -->|이후| G["테스트 프로파일의 값 사용"]
  G --> H["테스트가 필요한 데이터를 직접 생성"]

테스트가 어느 데이터베이스를 쓰는지 몰랐다

더 봤더니 문제가 하나 더 있었다. 테스트 지원 클래스에 내장 데이터베이스로 바꾸지 말라는 설정이 붙어 있었다.

그러면 테스트는 설정에 적힌 데이터베이스를 그대로 쓴다. 그 설정은 설정 서버에서 왔으니, 테스트가 어디에 붙는지는 그때 서버가 뭘 내려 줬는지에 달려 있었다. 테스트 코드만 읽어서는 알 수 없는 상태였다.

테스트가 남의 데이터를 쓰고 있었다

리포지토리 테스트를 하나씩 열어 보니 데이터를 만드는 코드가 없는 것들이 있었다. 은행 목록을 조회하는 테스트가 그랬다. 조회하고 결과를 검증하는데, 그 데이터를 넣은 자리가 어디에도 없었다.

이미 들어 있으니 통과하고 있었던 것이다. 누가 언제 넣었는지 모르는 값에 테스트가 기대고 있었고, 그 값이 지워지면 코드를 고치지 않아도 테스트가 깨진다. 반대로 값이 틀려도 테스트는 통과한다.

이건 앞의 두 문제와 뿌리가 같았다. 테스트가 자기 바깥에 있는 것을 전제하고 있었다. 설정 서버, 데이터베이스, 그 안에 든 데이터까지 셋 다 바깥이었다.

설정 서버가 실제로 주던 값을 세어 봤다

걷어내기 전에 무엇을 잃는지부터 봤다. 서버에서 받던 값을 항목으로 적으니 스무 개가 안 됐고, 그중 배포 없이 바꿔야 할 만큼 자주 변하는 값은 없었다.

운영에서 얻는 이점이 크지 않은데 로컬과 테스트에서 치르는 비용은 매일 발생하고 있었다. 그래서 서버를 걷어내고 값을 애플리케이션 설정에 되돌리기로 했다.

값 대신 이름을 적었다

되돌린다고 값을 파일에 그대로 쓸 수는 없었다. 접속 정보와 서명 키가 저장소에 들어가는 것은 다른 문제를 만든다.

설정 파일에는 환경변수 이름만 적고 실제 값은 실행 환경에서 넣게 했다. 로컬에서 자주 쓰는 항목에는 기본값을 함께 뒀다.

datasource.url        DB_HOST · DB_PORT · DB_NAME 을 환경변수로 (로컬 기본값 있음)
cache.host            REDIS_HOST 를 환경변수로 (로컬 기본값 있음)
token.secret          JWT_SECRET 을 환경변수로 (기본값 없음)

기본값을 붙일지 말지는 항목마다 갈랐다. 없으면 로컬에서 매번 환경변수를 채워야 하고, 붙이면 실수로 기본값인 채 뜨는 일이 생긴다. 접속 주소처럼 틀리면 바로 실패하는 것에는 붙였고, 서명 키처럼 틀린 채로도 뜨는 것에는 붙이지 않았다.

훑는 김에 캐시 주소가 파일에 그대로 박혀 있는 것도 같이 뺐다. 설정 서버를 쓰면서도 그 항목만 하드코딩돼 있었는데, 나눠서 관리하니 어느 쪽이 진짜인지 알기 어려웠다.

테스트에 자기 설정을 줬다

테스트용 설정 파일을 따로 만들고, 테스트 지원 클래스에 그 프로파일을 쓰라고 지정했다. 이제 테스트는 운영 설정을 물려받지 않는다.

항목이전이후
설정 출처사내 설정 서버프로파일별 설정 파일
부팅 전제서버가 살아 있어야 함없음
테스트 데이터베이스설정이 가리키는 곳테스트 프로파일이 정한 곳
테스트 데이터이미 들어 있다고 가정테스트가 직접 생성

데이터를 전제하던 테스트에는 실행 전에 필요한 값을 만드는 코드를 붙였다. 은행 목록처럼 고정된 것도 스무 개 남짓 직접 넣게 했다. 코드가 길어졌지만 그 테스트가 무엇을 전제하는지 파일 안에서 다 보이게 됐다.

최후 처리가 원인을 가리고 있었다

바꾸는 동안 전역 예외 처리에서 최후 처리를 주석으로 막아 두고 작업했다. 어떤 예외든 정해진 형태로 감싸서 내보내는 그 처리 때문에, 테스트가 실패해도 원래 예외가 아니라 감싼 응답만 보였기 때문이다.

감싸는 것을 걷어내니 부팅 실패의 진짜 원인이 그대로 나왔다. 응답 형태를 지키려고 넣은 장치가 원인을 가리고 있었던 셈이다.

관측 경로를 열면서 범위를 정했다

설정을 정리한 김에 지표 수집 경로를 열었다. 상태와 지표를 내보내는 엔드포인트를 켜고, 수집기가 읽을 수 있게 보안 설정에서 그 경로만 인증 없이 통과시켰다.

여기서 정할 것은 무엇을 여느냐였다. 상태 확인과 지표만 열고 나머지는 닫았다. 환경 변수나 빈 목록처럼 내부 구성을 그대로 보여 주는 것들이 같은 묶음에 들어 있어서, 묶음째로 열면 필요 없는 것까지 나간다.

지운 것이 아니라 주석으로 남겼다

설정 서버 의존성은 지우지 않고 주석으로 막아 뒀다. 되돌릴 일이 생길지 판단이 서지 않아서였는데, 지금 보면 어정쩡한 선택이다. 주석은 몇 달만 지나면 왜 남겼는지 아무도 모르는 줄이 된다.

테스트가 바깥을 전제하지 않게 된 것은 이번 범위까지다. 실행 환경에서 환경변수를 어떻게 넣을지는 배포 쪽에서 정해야 하는 일이라 여기서는 이름만 정하고 끝냈다.

테스트가 왜 안 도냐고 물으면 대개 테스트 코드를 먼저 본다. 이번 것은 테스트가 아니라 테스트가 서 있는 바닥의 문제였다. 바닥은 잘 돌 때 보이지 않고, 잘 돌지 않을 때는 이미 다른 일을 하고 있어서 원인을 찾을 여유가 없다.