Software Design

싱글톤(Singleton)이 테스트를 막는 이유, 의존성 주입으로 풀어봤습니다

싱글톤 때문에 테스트가 안 짜여서 밤새 헤맨 적 있으신가요?

이석우iOS Developer4분 읽기
싱글톤(Singleton)이 테스트를 막는 이유, 의존성 주입으로 풀어봤습니다 대표 이미지

싱글톤 때문에 테스트가 안 짜여서 밤새 헤맨 적 있으신가요?

분명 로직은 멀쩡한데 테스트만 붙이려고 하면 자꾸 실제 DB를 때리고 실제 API를 호출하는 상황이죠.

결론부터 말씀드릴게요.

싱글톤은 객체를 코드 안에서 직접 끌어다 쓰기 때문에, 테스트할 때 가짜 객체로 바꿔치기가 안 됩니다.

반대로 의존성 주입은 필요한 객체를 밖에서 넣어주는 방식이라 테스트에서 원하는 대로 갈아 끼울 수 있습니다.

오늘은 이 둘의 차이를 실제 코드와 함께 천천히 풀어보겠습니다.

싱글톤을 의존성 주입으로 바꾸는 순간 테스트 길이 열립니다
싱글톤을 의존성 주입으로 바꾸는 순간 테스트 길이 열립니다

싱글톤은 왜 테스트를 막을까요?

싱글톤은 프로그램 전체에서 딱 하나만 존재하는 객체예요.

편하긴 합니다. 어디서든 Database.shared 한 줄이면 꺼내 쓸 수 있으니까요.

문제는 여기서 시작됩니다.

객체를 함수 안에서 직접 꺼내 쓰면, 그 함수는 진짜 데이터베이스에 딱 붙어버립니다.

테스트할 때 “진짜 말고 가짜 DB를 써”라고 말할 방법이 없어요.

// 싱글톤을 함수 안에서 직접 꺼내 씀
func saveOrder(_ order: Order) {
    // 여기서 진짜 DB에 딱 붙어버립니다
    let db = Database.shared
    db.insert(order) // 테스트에서 이걸 막을 방법이 없음
}

이 코드를 테스트하려면 진짜 DB가 켜져 있어야 합니다.

DB가 꺼져 있으면 테스트도 같이 실패하고요.

로직이 틀린 게 아닌데 인프라 때문에 빨간불이 뜨는 거죠.


의존성 주입은 어떻게 하나요?

의존성 주입은 생각보다 거창하지 않습니다.

객체를 함수 안에서 꺼내 오지 말고, 밖에서 만들어서 넣어주기만 하면 됩니다.

말 그대로 필요한 걸 “주입(injection)“하는 거예요.

// DB를 파라미터로 밖에서 받음
func saveOrder(_ order: Order, db: Database) {
    db.insert(order) // 어떤 DB인지는 부르는 쪽이 정함
}

차이가 딱 한 줄이죠?

Database.shared로 직접 꺼내던 걸, 파라미터로 받도록 바꿨을 뿐입니다.

그런데 이 작은 변화가 테스트를 완전히 바꿔놓습니다.

실제 코드에서는 진짜 DB를 넣고, 테스트에서는 가짜 DB를 넣으면 되니까요.

넣어주는 대상만 바꿨을 뿐인데, 이렇게 갈립니다
넣어주는 대상만 바꿨을 뿐인데, 이렇게 갈립니다

생성자로 받는 방식도 자주 씁니다.

class OrderService {
    private let db: Database
    // 객체를 만들 때 밖에서 받아둠
    init(db: Database) { self.db = db }
}

한 번 받아두면 클래스 안 어디서든 self.db로 쓸 수 있어서 훨씬 깔끔합니다.


테스트에서 가짜 객체로 갈아 끼우기

이제 진짜 재밌는 부분입니다.

의존성을 밖에서 받도록 바꿨으니 테스트에서는 진짜 DB 대신 가짜 DB(보통 목·Mock이라고 부릅니다)를 넣어주면 됩니다.

@Test func 주문을_저장하면_DB에_전달된다() {
    let fake = FakeDatabase() // 진짜가 아닌 가짜
    saveOrder(Order(), db: fake)
    #expect(fake.savedCount == 1) // 저장이 불렸는지만 확인
}

진짜 DB를 켤 필요가 없습니다.

네트워크도, 외부 서버도 필요 없어요. 메모리 안에서 순식간에 끝납니다.

덕분에 테스트가 빨라지고 무엇보다 안 흔들립니다.

외부 상황과 상관없이 로직만 콕 집어서 검증할 수 있으니까요.

가짜 객체를 넣으니 테스트가 순식간에 초록불로 바뀌더라고요
가짜 객체를 넣으니 테스트가 순식간에 초록불로 바뀌더라고요

싱글톤 vs 의존성 주입, 한눈에 비교

둘의 차이를 표로 정리해봤습니다.

구분 싱글톤 직접 사용 의존성 주입
객체 얻는 위치 함수 안에서 직접 꺼냄 밖에서 넣어줌
가짜 객체 교체 사실상 불가능 자유롭게 가능
테스트 속도 느림(실제 자원 필요) 빠름(메모리로 처리)
테스트 안정성 외부 상황에 흔들림 로직만 검증 가능
결합도 높음 낮음

싱글톤 자체가 무조건 나쁜 건 아닙니다.

설정값처럼 진짜로 하나만 있으면 되는 경우엔 편리하죠.

다만 “어디서든 직접 꺼내 쓰는” 습관이 문제입니다. 그게 테스트의 손발을 묶거든요.


자주 묻는 질문 (Q&A)

Q. 의존성 주입 하려면 무조건 Swinject 같은 DI 라이브러리가 필요한가요?

아닙니다. 오늘 코드처럼 파라미터나 생성자로 넘겨주는 것만으로도 이미 의존성 주입이에요. 라이브러리는 이걸 자동으로 관리해줄 뿐입니다.

Q. 파라미터가 너무 많아지면 지저분하지 않나요?

맞아요. 그래서 보통 생성자 주입으로 한 번에 받아두거나, 관련 있는 것끼리 묶습니다. 파라미터가 계속 늘어난다면, 그 클래스가 너무 많은 일을 하고 있다는 신호이기도 합니다.

진짜 대신 가짜 DB를 끼워 넣는 것, 이게 핵심입니다
진짜 대신 가짜 DB를 끼워 넣는 것, 이게 핵심입니다

테스트가 자꾸 실제 자원에 발목 잡힌다면, 오늘 이야기한 방향으로 딱 한 군데만 바꿔보세요.

객체를 직접 꺼내던 코드를, 밖에서 받도록 고치는 것.

그 작은 습관 하나가 테스트를 짤 맛 나게 만들어줍니다. 오늘도 편안한 코딩 되세요!

이어서 읽기