CleanIsolation
주니어 개발자들을 위한 패턴 언어 - 코드의 의존성을 끊어내어 자유롭게 테스트하고 변경하는 법
Contents
The Story 1: The Tangled Web vs. The Lego Block (Programming)
두 명의 개발자, 민수와 하나가 '사용자 가입' 기능을 테스트하고 있다.
민수의 상황 (The Tangled Web): 민수는 UserService의 register() 메서드를 테스트하려고 한다. "음, 이걸 실행하려면... 일단 DB가 켜져 있어야 하고, 이메일 서버 설정이 필요하고... 아, GlobalConfig 파일도 읽어야 하네." 테스트 코드를 짜는 데만 30줄의 설정(Setup)이 필요했다. 겨우 실행했는데 테스트가 실패했다. "왜 실패했지? 아, 사내 이메일 서버가 잠깐 점검 중이라서 타임아웃이 났어." 민수의 코드는 세상 모든 것과 보이지 않는 끈으로 연결되어 있어서, 하나만 건드려도 전체가 흔들렸다.
하나의 상황 (The Lego Block): 하나는 UserService를 다르게 설계했다. 필요한 저장소나 이메일 발송 기능을 생성자를 통해 밖에서 받도록 만들었다. 테스트할 때, 하나는 진짜 DB나 이메일 서버를 쓰지 않았다. "DB 대신 메모리에 저장하는 가짜 객체(Fake)를 넣고, 이메일 서버 대신 '보냈다고 치는' 객체(Stub)를 넣자." 하나의 테스트는 인터넷을 끊어도 돌아갔고, 0.01초 만에 끝났다. 그녀의 코드는 레고 블록처럼 어디든 끼웠다 뺐다 할 수 있었다.
The Story 2: The Socket and The Plug (Ordinary Life)
민수와 하나는 새로 산 '공기청정기'를 거실에 배치하고 있다.
민수의 연결 (The Hardwired): 민수는 집안의 모든 가전제품이 벽 안의 전선에 직접 납땜(Soldering)되어 있어야 안전하다고 믿는다. "선이 빠질 염려도 없고 전기 전도율도 좋잖아." 하지만 공기청정기를 안방으로 옮기고 싶을 때마다 민수는 벽을 뜯고 전선을 잘라낸 뒤, 안방 벽을 다시 뜯어 연결해야 했다. 가전제품 하나를 옮기는 일이 집 전체를 수리하는 대공사가 되었다.
하나의 연결 (The Plug-in): 하나는 가전제품과 벽 사이에 '소켓(Socket)'과 '플러그(Plug)'라는 표준 규격을 사용한다. "플러그만 뽑으면 어디든 옮길 수 있고, 고장 나면 그 제품만 바꾸면 돼." 하나에게 공기청정기를 옮기는 일은 1초면 충분했다. 심지어 전기가 들어오지 않는 캠핑장에서도 보조 배터리라는 '대역'에 꽂아 공기청정기를 테스트해 볼 수 있었다. 분리할 수 있는 능력이 곧 자유와 유연성임을 하나는 알고 있었다.
Context
DesignThroughTest를 통해 인터페이스를 설계하고 있고, TightLoop로 빠르게 확인하고 싶다. 하지만 코드를 실행하려고 할 때마다 무언가 방해한다.
일상적인 상황:
- 테스트를 돌리려는데 "DB 연결 실패" 에러가 뜬다.
- 특정 날짜에만 발생하는 버그를 잡으려는데, 시스템 날짜를 바꿀 수가 없다.
- 함수 하나를 테스트하고 싶은데, 그 함수가 속한 클래스가 너무 무거운 객체들을 생성한다.
- 테스트 결과가 실행할 때마다 다르다(Flaky Test).
당신은 코드를 실험실로 가져오고 싶지만, 코드가 현실 세계의 바닥에 시멘트로 고정되어 있다.
Problem
숨겨진 의존성(Hidden Dependencies)은 코드를 "고립된 상태"로 검증하는 것을 불가능하게 만든다.
코드가 외부 환경(DB, Network, Global State, Time)에 직접 의존하면:
느리다: I/O가 개입되면 피드백 루프가 밀리초(ms)에서 초(s) 단위로 느려진다.
불안정하다: 외부 요인(네트워크 장애 등)으로 테스트가 실패하면, 내 로직이 틀린 건지 환경이 문제인지 알 수 없다.
재사용 불가: 다른 환경에서 이 코드를 쓰고 싶어도, 딸려오는 의존성 때문에 가져갈 수 없다.
Solution
테스트 대상 코드와 외부 환경 사이의 연결 고리를 "끊을 수 있는 형태(Seam)"로 만들어라.
격리(Isolation)는 단순히 테스트를 위한 것이 아니다. 시스템의 유연성을 확보하는 것이다.
Principle 1: Explicit Dependencies (의존성 명시화)
코드가 무엇을 필요로 하는지 숨기지 마라.
Bad: 함수 내부에서 Database.getInstance()를 호출하거나 new EmailService()를 직접 한다. (숨겨진 의존성)
Good: 필요한 객체를 인자(Argument)나 생성자(Constructor)로 받는다. "나는 이게 필요해요"라고 소리치게 하라.
Principle 2: Separation of Concerns (Logic vs. I/O)
"계산(Logic)"과 "통신(I/O)"을 철저히 분리하라.
- 비즈니스 로직은 순수한 데이터만 다루게 하라(Pure Function).
- I/O는 시스템의 가장 가장자리(Boundary)로 밀어내라.
- 이렇게 하면 로직은 100% 격리된 상태로 테스트할 수 있다.
Principle 3: Use Test Doubles (대역 사용하기)
경계(Boundary)에 있는 의존성들은 테스트 대역(Test Double)으로 교체하라.
Stub: "DB에서 데이터 3개를 리턴해줘" (미리 준비된 답을 줌)
Mock: "이메일 전송 함수가 정확히 1번 호출되었는지 확인해줘" (행동을 검증)
Fake: "메모리 리스트에 저장해줘" (가짜로 동작하는 구현체)
Principle 4: Pass Data, Not Objects (객체 대신 데이터)
가능하다면 무거운 서비스 객체를 넘기지 말고, 필요한 데이터만 넘겨라.
calculateDiscount(User user) 대신 calculateDiscount(int userGrade)가 훨씬 격리하기 쉽다.
Real Examples
Example 1: The "Now" Problem
할인 기간인지 확인하는 로직에서 시간을 직접 시스템에서 가져오는 대신 인자로 받음으로써, 어떤 날짜의 시나리오든 1초 만에 테스트할 수 있게 함.
Example 2: The Network Call
환율을 조회하는 기능을 인터페이스 뒤로 숨기고, 테스트에서는 고정된 환율을 리턴하는 가짜 객체를 주입하여 네트워크 없이도 가격 계산 로직을 검증함.
Common Pitfalls
"Isolating Everything" (모든 것을 격리하기)
내부 구현 클래스끼리도 너무 과도하게 격리해서 Mock으로 도배하지 마십시오. 격리의 대상은 주로 변경 비용이 비싸거나(DB), 제어하기 힘든(Time, Network) 경계 지점입니다.
"Dependency Injection Framework Magic"
스프링과 같은 프레임워크가 DI를 해주지만, 그것 없이도 순수한 코드로 격리는 가능해야 합니다. 프레임워크에 너무 의존하여 "스프링이 없으면 테스트 못 짜는" 상태가 되지 않도록 주의하십시오.
Connection to Other Patterns
DesignThroughTest - 테스트를 먼저 짜려고 시도하면, 격리가 안 된 코드가 얼마나 고통스러운지 바로 알게 됩니다. 격리는 DesignThroughTest의 자연스러운 결과입니다. 원인
TightLoop - 코드가 격리되어 있어야 테스트가 빨라지고, 그래야 TightLoop가 가능합니다. 전제 조건
DirectPath - 격리를 위해 불필요한 레이어를 만드는 것은 피해야 합니다. 단순함을 유지하며 격리하십시오. 균형
DataAsFoundation - 로직이 데이터 중심으로 작성되면(I/O 없이), 격리는 공짜로 얻어집니다. 기반
Signs of Success
- 테스트를 실행할 때 인터넷 선을 뽑아도 불안하지 않다.
- "이거 테스트하려면 로컬에 Redis 깔아야 해"라는 말이 사라진다.
- 특정 버그를 재현하기 위해 복잡한 상황을 연출할 필요 없이, 테스트 코드 몇 줄로 상황을 만들 수 있다.
- 코드가 레고 블록처럼 작고 단단하며, 어디에나 쉽게 끼워 맞춰진다.
The Ultimate Insight
소프트웨어 설계의 핵심은 "무엇을 연결할까"가 아니라 "무엇을 분리할까"입니다.
CleanIsolation은 당신의 코드를 세상의 혼란스러움(Side Effects)으로부터 보호하는 방화벽입니다. 이 방화벽 안에서 당신의 로직은 안전하고 자유롭게 숨 쉴 수 있습니다.
CategoryPatternLanguage CategoryProgramming CategoryTDD CategoryDesign CategoryRefactoring
