CleanIsolation

주니어 개발자들을 위한 패턴 언어 - 코드의 의존성을 끊어내어 자유롭게 테스트하고 변경하는 법

The Story: The Tangled Web vs. The Lego Block

두 명의 개발자, 민수와 하나가 '사용자 가입' 기능을 테스트하고 있다.

민수의 상황 (The Tangled Web): 민수는 UserServiceregister() 메서드를 테스트하려고 한다. "음, 이걸 실행하려면... 일단 DB가 켜져 있어야 하고, 이메일 서버 설정이 필요하고... 아, GlobalConfig 파일도 읽어야 하네." 테스트 코드를 짜는 데만 30줄의 설정(Setup)이 필요했다. 겨우 실행했는데 테스트가 실패했다. "왜 실패했지? 아, 사내 이메일 서버가 잠깐 점검 중이라서 타임아웃이 났어." 민수의 코드는 세상 모든 것과 보이지 않는 끈으로 연결되어 있어서, 하나만 건드려도 전체가 흔들렸다. 그는 결국 "로컬에서는 테스트 못 함"이라고 결론 내렸다.

하나의 상황 (The Lego Block): 하나는 UserService를 다르게 설계했다.

class UserService {
    constructor(userRepository, emailSender) { ... } // 필요한 걸 밖에서 받음
}

테스트할 때, 하나는 진짜 DB나 이메일 서버를 쓰지 않았다. "DB 대신 메모리에 저장하는 가짜 객체(Fake)를 넣고, 이메일 서버 대신 '보냈다고 치는' 객체(Stub)를 넣자." 하나의 테스트는 인터넷을 끊어도 돌아갔고, 0.01초 만에 끝났다. 그녀의 코드는 레고 블록처럼 어디든 끼웠다 뺐다 할 수 있었다.

Context

DesignThroughTest를 통해 인터페이스를 설계하고 있고, TightLoop로 빠르게 확인하고 싶다. 하지만 코드를 실행하려고 할 때마다 무언가 방해한다.

일상적인 상황:

당신은 코드를 **실험실**로 가져오고 싶지만, 코드가 **현실 세계**의 바닥에 시멘트로 고정되어 있다.

Problem

숨겨진 의존성(Hidden Dependencies)은 코드를 "고립된 상태"로 검증하는 것을 불가능하게 만든다.

코드가 외부 환경(DB, Network, Global State, Time)에 직접 의존하면:

Solution

테스트 대상 코드와 외부 환경 사이의 연결 고리를 "끊을 수 있는 형태(Seam)"로 만들어라.

격리(Isolation)는 단순히 테스트를 위한 것이 아니다. 시스템의 **유연성**을 확보하는 것이다.

Principle 1: Explicit Dependencies (의존성 명시화)

코드가 무엇을 필요로 하는지 숨기지 마라.

Principle 2: Separation of Concerns (Logic vs. I/O)

**"계산(Logic)"**과 **"통신(I/O)"**을 철저히 분리하라.

Principle 3: Use Test Doubles (대역 사용하기)

경계(Boundary)에 있는 의존성들은 **테스트 대역(Test Double)**으로 교체하라.

Principle 4: Pass Data, Not Objects (객체 대신 데이터)

가능하다면 무거운 서비스 객체를 넘기지 말고, 필요한 **데이터**만 넘겨라.

Real Examples

Example 1: The "Now" Problem

할인 기간인지 확인하는 로직.

Tangled (민수):

def is_discount_period():
    now = datetime.now() // 시스템 시계에 의존!
    return now.month == 12

여름에 이 코드를 테스트하려면 컴퓨터 날짜를 바꿔야 한다.

Isolated (하나):

def is_discount_period(current_time): // 시간을 데이터로 받음
    return current_time.month == 12

테스트: is_discount_period(Date("2023-12-25")) -> True. 끝.

Example 2: The Network Call

환율을 조회해서 가격을 계산하는 로직.

Tangled (민수):

class PriceCalculator:
    def convert(price):
        rate = requests.get("https://api.exchangerate.com") // 네트워크 직접 호출
        return price * rate

Isolated (하나):

class PriceCalculator:
    def __init__(self, rate_provider): // 인터페이스(역할)에 의존
        self.provider = rate_provider

    def convert(price):
        rate = self.provider.get_rate()
        return price * rate

테스트에서는 FakeRateProvider(rate=1300)을 넣어주면 된다.

Common Pitfalls

"Isolating Everything" (모든 것을 격리하기)

내부 구현 클래스끼리도 너무 과도하게 격리해서 Mock으로 도배하지 마라. 격리의 대상은 주로 **변경 비용이 비싸거나(DB), 제어하기 힘든(Time, Network) 경계 지점**이다. 같은 모듈 내의 로직끼리는 그냥 같이 테스트하는 게 나을 때도 있다(SocialUnitTests).

"Dependency Injection Framework Magic"

Spring이나 Dagger 같은 프레임워크가 DI를 해주지만, 그것 없이도 격리는 가능하다. 프레임워크에 너무 의존해서 "스프링이 없으면 테스트 못 짜는" 상태가 되지 않도록 주의하라. 순수한 자바/파이썬 코드로도 격리는 가능하다.

Connection to Other Patterns

Signs of Success

The Ultimate Insight

소프트웨어 설계의 핵심은 "무엇을 연결할까"가 아니라 "무엇을 분리할까"이다.

CleanIsolation은 당신의 코드를 세상의 혼란스러움(Side Effects)으로부터 보호하는 방화벽이다. 이 방화벽 안에서 당신의 로직은 안전하고 자유롭게 춤출 수 있다.


CategoryPatternLanguage CategoryProgramming CategoryTDD CategoryDesign CategoryRefactoring