Differences between revisions 1 and 2
Revision 1 as of 2025-12-30 06:34:33
Size: 8457
Editor: 정수
Comment:
Revision 2 as of 2025-12-30 06:36:12
Size: 8471
Editor: 정수
Comment:
Deletions are marked like this. Additions are marked like this.
Line 42: Line 42:
당신은 코드를 **실험실**로 가져오고 싶지만, 코드가 **현실 세계**의 바닥에 시멘트로 고정되어 있다. 당신은 코드를 '''실험실'''로 가져오고 싶지만, 코드가 '''현실 세계'''의 바닥에 시멘트로 고정되어 있다.
Line 59: Line 59:
격리(Isolation)는 단순히 테스트를 위한 것이 아니다. 시스템의 **유연성**을 확보하는 것이다. 격리(Isolation)는 단순히 테스트를 위한 것이 아니다. 시스템의 '''유연성'''을 확보하는 것이다.
Line 67: Line 67:
**"계산(Logic)"****"통신(I/O)"**을 철저히 분리하라. '''"계산(Logic)"''''''"통신(I/O)"'''을 철저히 분리하라.
Line 73: Line 73:
경계(Boundary)에 있는 의존성들은 **테스트 대역(Test Double)**으로 교체하라. 경계(Boundary)에 있는 의존성들은 '''테스트 대역(Test Double)'''으로 교체하라.
Line 79: Line 79:
가능하다면 무거운 서비스 객체를 넘기지 말고, 필요한 **데이터**만 넘겨라. 가능하다면 무거운 서비스 객체를 넘기지 말고, 필요한 '''데이터'''만 넘겨라.
Line 131: Line 131:
격리의 대상은 주로 **변경 비용이 비싸거나(DB), 제어하기 힘든(Time, Network) 경계 지점**이다. 같은 모듈 내의 로직끼리는 그냥 같이 테스트하는 게 나을 때도 있다([[SocialUnitTests]]). 격리의 대상은 주로 '''변경 비용이 비싸거나(DB), 제어하기 힘든(Time, Network) 경계 지점'''이다. 같은 모듈 내의 로직끼리는 그냥 같이 테스트하는 게 나을 때도 있다([[SocialUnitTests]]).

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로 빠르게 확인하고 싶다. 하지만 코드를 실행하려고 할 때마다 무언가 방해한다.

일상적인 상황:

  • 테스트를 돌리려는데 "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

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

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

  • DesignThroughTest - 테스트를 먼저 짜려고 시도하면, 격리가 안 된 코드가 얼마나 고통스러운지 바로 알게 된다. 격리는 DesignThroughTest의 자연스러운 결과다. 원인

  • TightLoop - 코드가 격리되어 있어야 테스트가 빨라지고, 그래야 TightLoop가 가능하다. 전제 조건

  • DirectPath - 격리를 위해 불필요한 레이어를 만드는 것은 피해야 한다. 단순함을 유지하며 격리하라. 균형

  • DataAsFoundation - 로직이 데이터 중심으로 작성되면(I/O 없이), 격리는 공짜로 얻어진다. 기반

Signs of Success

  • 테스트를 실행할 때 인터넷 선을 뽑아도 불안하지 않다.
  • "이거 테스트하려면 로컬에 Redis 깔아야 해"라는 말이 사라진다.
  • 특정 버그를 재현하기 위해 복잡한 상황을 연출할 필요 없이, 테스트 코드 몇 줄로 상황을 만들 수 있다.
  • 코드가 레고 블록처럼 작고 단단하며, 어디에나 쉽게 끼워 맞춰진다.

The Ultimate Insight

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

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


CategoryPatternLanguage CategoryProgramming CategoryTDD CategoryDesign CategoryRefactoring

CleanIsolation (last edited 2025-12-30 09:00:30 by 정수)