처음 할 일은 단순해 보였다.
LDAP에 사용자를 몇 명 넣고, 조직 단위와 실제 그룹이 어떻게 다르게 동기화되는지 확인하면 됐다.
그런데 막상 시작하니 실험보다 실험실 준비가 더 길었다.
OpenLDAP 컨테이너를 띄운다
LDIF로 사용자와 그룹을 넣는다
ldapsearch로 원본 데이터를 확인한다
제품 API로 LDAP 연결을 만든다
동기화를 실행한다
사용자와 그룹을 다시 읽는다
실제 화면을 캡처한다
마지막으로 전부 지운다
한 번만 한다면 긴 스크립트 하나로 끝낼 수 있다.
문제는 다음 LDAP 사건이 왔을 때다.
컨테이너 이름만 바꾸고, 복사하고, 붙여 넣고, 또 비슷한 cleanup을 만든다. 그러다 어느 날 다른 사람이 만든 컨테이너를 지우거나, 데이터베이스에 직접 값을 심어놓고 제품 기능이 성공했다고 착각한다.
그 순간 깨달았다.
우리가 반복하고 있던 건 LDAP 설정이 아니었다.
안전하게 실험하는 순서였다.
좋은 모듈화는 줄 수가 아니라 판단을 옮긴다
코드를 함수로 빼면 줄 수는 줄어든다.
하지만 그게 전부라면 모듈화는 조금 세련된 복사·붙여넣기에 불과하다.
이번에 공통 모듈로 옮긴 건 이런 판단들이었다.
이 컨테이너는 정말 이번 실험이 만든 것인가?
포트가 이미 사용 중이면 시작해도 되는가?
LDAP가 준비됐다는 건 프로세스가 떴다는 뜻인가, 검색까지 된다는 뜻인가?
제품 데이터를 만들 때 API를 먼저 사용했는가?
DB를 직접 만지는 예외는 언제 허용되는가?
cleanup이 실패하면 증거와 상태를 보존해야 하는가?
이런 질문이 시나리오마다 흩어져 있으면 작성자가 매번 정답을 기억해야 한다.
공통 모듈에 들어가면 도구가 기본적으로 안전한 쪽을 선택한다.
사람의 주의력을 코드의 기본값으로 바꾼 셈이다.
첫 번째 공용 장비: OpenLDAP fixture
먼저 OpenLDAP 실험 환경을 하나의 fixture로 묶었다.
fixture는 테스트에 필요한 환경과 데이터를 준비하는 장치다. 요리로 치면 재료뿐 아니라 도마, 칼, 가스레인지까지 미리 준비해두는 세트에 가깝다.
새 fixture가 맡은 일은 이렇다.
Docker network 생성
OpenLDAP 컨테이너 실행
LDIF bootstrap
LDAP 검색 준비 상태 확인
phpLDAPadmin 실행
구조화된 ldapsearch 결과 반환
실험이 소유한 리소스만 cleanup
여기서 가장 중요한 건 실험이 소유한 리소스만이라는 부분이다.
이름이 같다고 지우면 안 된다. 컨테이너와 네트워크에 시나리오 소유권 label이 있는지 확인해야 한다.
라벨이 다르면 cleanup은 실패해야 한다.
실험실 냉장고에 내 샘플과 다른 팀 샘플이 함께 있는데, 이름이 비슷하다고 전부 버리는 청소 로봇은 유능한 로봇이 아니다.
두 번째 공용 장비: LDAP IdP API client
다음은 제품 쪽 LDAP 설정과 동기화 흐름을 묶었다.
여기서는 한 가지 원칙을 아주 강하게 잡았다.
API first.
제품 API로 LDAP 연결 생성
제품 API로 설정 변경
제품 API로 동기화 실행
공식 조회 API로 사용자·그룹·membership 확인
제품 API로 연결 삭제
“로컬 테스트인데 그냥 DB에 넣으면 더 빠르지 않나?”라는 유혹이 온다.
실제로 더 빠를 때도 있다.
하지만 DB에 원하는 결과를 직접 넣으면 우리가 검증한 건 제품의 동기화 기능이 아니다. SQL을 잘 실행했다는 사실뿐이다.
API는 단순한 HTTP 문이 아니다.
그 뒤에는 유효성 검사, 상태 전이, 연관 데이터 처리, 감사 기록 같은 제품 동작이 숨어 있다. API를 건너뛰면 테스트가 이 동작을 모두 우회할 수 있다.
그래서 fixture 준비와 실제 재현을 분리했다.
fixture 준비 제한적인 예외가 있을 수 있음
재현 대상 동작 반드시 실제 제품 경로 사용
검증 API 결과와 원본 LDAP를 교차 확인
cleanup API 우선, 불가피한 참조만 제한적으로 보정
이 차이가 테스트의 신뢰도를 결정한다.
DB fallback은 비상문이지 뒷문이 아니다
실제 cleanup에서는 재미있는 일이 벌어졌다.
사용자와 그룹은 정상적으로 정리됐지만, 이미 soft delete된 일부 참조 때문에 LDAP 연결 삭제 API가 400을 반환했다.
여기서 선택지는 두 가지였다.
첫째, “로컬이니까” DB에서 연결을 통째로 지운다.
둘째, 실패 상태를 보존하고 명시적인 허가가 있을 때만 시나리오 소유 참조를 해제한 뒤, 삭제는 다시 제품 API로 수행한다.
두 번째를 선택했다.
흐름은 이렇다.
API 삭제 시도
→ 실패하면 상태와 증거 보존
→ 격리된 폐기 가능 환경인지 확인
→ 명시적 fallback 옵션 확인
→ 이번 실험이 만든 참조만 해제
→ 제품 API로 다시 삭제
→ API로 실제 삭제 여부 확인
이건 번거로워 보이지만 중요한 차이다.
DB fallback이 제품 동작을 대신하지 않는다. 제품 API가 정상적인 마지막 문을 통과할 수 있도록 막힌 실험용 짐만 치운다.
비상문은 평소 출입구가 아니다.
평소에는 잠겨 있어야 하고, 누가 왜 열었는지 남아야 한다.
비밀번호를 가렸더니 JSON이 깨졌다
이번 작업에서 가장 재미있는 버그는 LDAP 동기화와 직접 관계가 없었다.
Docker 컨테이너 정보를 조회하면 환경변수도 함께 나온다. 그 안에는 LDAP 관리자 비밀번호가 있을 수 있다.
당연히 출력 전에 비밀번호를 가렸다.
그런데 setup이 갑자기 이런 오류로 실패했다.
Invalid control character in JSON
원인은 역설적이었다.
비밀번호를 보호하려고 만든 redactor가 JSON 문자열을 파싱하기 전에 일부를 치환하면서 따옴표 구조까지 망가뜨린 것이다.
전체 Docker inspect JSON
→ 문자열 상태에서 비밀값 마스킹
→ JSON 문법 손상
→ 파싱 실패
처음에는 마스킹 정규식을 더 정교하게 만들까 생각했다.
하지만 더 좋은 질문이 있었다.
애초에 비밀번호가 든 환경변수를 읽어야 하나?
필요 없었다.
소유권 검사에 필요한 정보는 몇 가지뿐이었다.
container ID
image
running state
ownership labels
그래서 전체 inspect 결과를 가져오지 않고 필요한 필드만 선택해 조회하도록 바꿨다.
필요한 데이터만 조회
→ 안전한 JSON 파싱
→ 구조화된 데이터에서 필요한 값만 사용
이 방식은 보안과 안정성을 동시에 개선했다.
민감정보를 잘 가리는 것보다 더 좋은 방법은 민감정보를 처음부터 가져오지 않는 것이다.
숫자로 보니 효과가 있었다
모듈화는 기분이 아니라 결과로 확인해야 한다.
시나리오 runner는 이렇게 줄었다.
이전 1,105줄
이후 871줄
감소 234줄, 약 21.2%
하지만 더 중요한 건 줄어든 234줄의 성격이다.
빠진 것은 LDAP 요구사항 설명이나 핵심 assertion이 아니었다.
- Docker lifecycle
- readiness polling
- 반복 HTTP 요청
- API 응답 검사
- LDAP 검색 결과 정규화
- 소유권 기반 cleanup
즉, 사건마다 다시 쓰면 위험한 운영 코드가 빠졌다.
시나리오에는 “무엇을 비교하고 어떤 결과를 기대하는가”가 더 선명하게 남았다.
공통화 자체도 실제 경로로 검증했다
모듈을 만들고 단위 테스트만 통과시키면 반쪽이다.
그래서 실제 흐름을 처음부터 끝까지 다시 실행했다.
OpenLDAP setup PASS
실제 ldapsearch PASS
headless 브라우저 LDAP 화면 증빙 PASS
제품 API LDAP 연결 생성·동기화 PASS
공식 API 사용자·그룹 readback PASS
결과 패키지와 secret scan PASS
API-first cleanup 의도한 400과 상태 보존 확인
명시적 제한 fallback 후 API 삭제 PASS
컨테이너·네트워크·비밀파일 제거 PASS
단위 테스트 5개와 기존 ReproKit 테스트 20개도 모두 통과했다.
특히 cleanup이 처음부터 무조건 성공하지 않았다는 점이 오히려 좋은 증거였다.
안전장치는 실패해야 할 때 실패해야 한다.
API 삭제가 막혔는데도 DB로 몰래 우회해서 초록색 PASS를 만들었다면, 그 테스트는 편리하지만 믿을 수 없다.
표준화의 진짜 효과
이번 작업 전에는 LDAP 시나리오가 많은 일을 알고 있었다.
컨테이너를 띄우는 법, 포트를 기다리는 법, API를 호출하는 법, 결과를 정리하는 법, cleanup 예외까지 전부 알고 있었다.
이제 시나리오는 자기 질문에 집중한다.
중첩된 위치의 사용자는 검색되는가?
조직 단위와 실제 그룹은 어떻게 다른가?
membership 속성은 기대한 사용자를 가리키는가?
제품 동기화 결과가 LDAP 원본과 일치하는가?
공통 모듈은 실험실 운영을 맡는다.
안전하게 준비한다
실제 제품 경로를 사용한다
증거를 구조화한다
비밀을 노출하지 않는다
소유한 것만 정리한다
이 분리가 되면 다음 LDAP 문제는 처음부터 실험실을 짓는 일이 아니다.
이미 준비된 장비를 꺼내고, 이번에 확인할 가설만 적으면 된다.
이번에 남은 다섯 가지 원칙
첫째, 반복되는 코드를 찾기 전에 반복되는 판단을 찾아라.
둘째, 제품 기능 검증에서는 API가 기본 경로다. DB는 명시적으로 통제된 예외여야 한다.
셋째, cleanup은 부록이 아니다. 실험의 마지막 검증 단계다.
넷째, 마스킹보다 수집 최소화가 강하다. 필요 없는 민감정보는 애초에 읽지 마라.
다섯째, 공통화의 효과는 줄 수가 아니라 실제 전체 경로 재실행으로 증명하라.
LDAP 하나를 재현하려다 실험실 운영 규칙까지 만들었다.
조금 일이 커진 것처럼 보이지만, 다음 사람은 훨씬 작은 코드로 더 안전한 실험을 시작할 수 있다.
좋은 자동화는 버튼을 대신 눌러주는 도구가 아니다.
실수하기 어려운 순서를 기본값으로 만들어주는 도구다.