Dependency 실전 가이드
Dependency의 핵심 개념과 실무 활용
학습 항목
이미지 로딩 중...
Dependency Injection 기초부터 심화까지
의존성 주입(DI)의 핵심 개념부터 고급 패턴까지 실전 예제로 학습합니다. 생성자 주입, 인터페이스 분리, IoC 컨테이너 활용법을 다룹니다.
들어가며
이 글에서는 Dependency Injection 기초부터 심화까지에 대해 상세히 알아보겠습니다. 총 12가지 주요 개념을 다루며, 각각의 개념에 대한 설명과 실제 코드 예제를 함께 제공합니다.
목차
- 기본_의존성_주입_개념
- 인터페이스_기반_주입
- 싱글톤_패턴과_DI
- 팩토리_패턴_DI
- 프로퍼티_주입_패턴
- DI_컨테이너_기본
- 자동_의존성_해결
- 스코프_관리_패턴
- 생성자_주입_vs_메서드_주입
- 순환_의존성_해결
- 모듈_기반_DI_시스템
- 고급_인터셉터_패턴
1. 기본_의존성_주입_개념
개요
클래스가 필요한 의존성을 외부에서 주입받는 기본 패턴입니다. 결합도를 낮추고 테스트를 용이하게 만듭니다.
코드 예제
class Logger {
log(message: string) { console.log(message); }
}
class UserService {
constructor(private logger: Logger) {}
createUser(name: string) {
this.logger.log(`User ${name} created`);
}
}
const service = new UserService(new Logger());
설명
UserService가 Logger를 생성자를 통해 주입받아 사용합니다. 의존성이 외부에서 관리되어 코드 재사용성이 높아집니다.
2. 인터페이스_기반_주입
개요
인터페이스를 통해 구체적인 구현체를 추상화하여 더 유연한 설계를 만듭니다.
코드 예제
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(msg: string) { console.log(msg); }
}
class FileLogger implements ILogger {
log(msg: string) { /* 파일에 저장 */ }
}
class UserService {
constructor(private logger: ILogger) {}
}
설명
ILogger 인터페이스를 사용해 ConsoleLogger나 FileLogger를 자유롭게 교체할 수 있습니다. 개방-폐쇄 원칙을 준수합니다.
3. 싱글톤_패턴과_DI
개요
싱글톤 인스턴스를 의존성으로 주입하여 전역 상태를 안전하게 관리합니다.
코드 예제
class Database {
private static instance: Database;
private constructor() {}
static getInstance(): Database {
if (!this.instance) {
this.instance = new Database();
}
return this.instance;
}
}
const db = Database.getInstance();
설명
Database 클래스는 단 하나의 인스턴스만 생성되며, getInstance()를 통해 동일한 인스턴스를 공유합니다.
4. 팩토리_패턴_DI
개요
팩토리 함수를 주입하여 객체 생성 로직을 캡슐화하고 런타임에 다양한 타입을 생성합니다.
코드 예제
interface INotification { send(msg: string): void; }
type NotificationFactory = (type: string) => INotification;
class NotificationService {
constructor(private factory: NotificationFactory) {}
notify(type: string, msg: string) {
const notification = this.factory(type);
notification.send(msg);
}
}
설명
팩토리 함수를 주입받아 필요한 타입의 알림 객체를 동적으로 생성합니다. 객체 생성 로직이 분리됩니다.
5. 프로퍼티_주입_패턴
개요
생성자가 아닌 프로퍼티를 통해 의존성을 주입하는 방식입니다. 선택적 의존성에 유용합니다.
코드 예제
class UserService {
logger?: ILogger;
setLogger(logger: ILogger) {
this.logger = logger;
}
createUser(name: string) {
this.logger?.log(`Creating ${name}`);
}
}
const service = new UserService();
service.setLogger(new ConsoleLogger());
설명
setLogger()를 통해 나중에 의존성을 주입할 수 있습니다. 순환 참조 문제를 해결할 때도 활용됩니다.
6. DI_컨테이너_기본
개요
DI 컨테이너는 의존성 등록과 해결을 자동화하는 핵심 도구입니다.
코드 예제
class DIContainer {
private services = new Map<string, any>();
register(name: string, instance: any) {
this.services.set(name, instance);
}
resolve<T>(name: string): T {
return this.services.get(name);
}
}
const container = new DIContainer();
container.register('logger', new ConsoleLogger());
설명
컨테이너에 서비스를 등록하고 필요할 때 resolve()로 가져옵니다. 의존성 관리가 중앙화됩니다.
7. 자동_의존성_해결
개요
메타데이터를 활용해 의존성을 자동으로 주입하는 고급 패턴입니다.
코드 예제
const Injectable = () => (target: any) => {
target.injectable = true;
};
@Injectable()
class ApiService {
constructor(private http: HttpClient) {}
}
function autoResolve<T>(Class: new (...args: any[]) => T): T {
// 생성자 파라미터 자동 해결
return new Class(/* auto resolved deps */);
}
설명
데코레이터를 사용해 클래스를 주입 가능하게 표시하고, 컨테이너가 자동으로 의존성을 해결합니다.
8. 스코프_관리_패턴
개요
의존성의 생명주기를 Singleton, Transient, Scoped로 관리합니다.
코드 예제
enum Scope { Singleton, Transient, Scoped }
class Container {
register(name: string, factory: () => any, scope: Scope) {
if (scope === Scope.Singleton) {
const instance = factory();
return () => instance;
}
return factory; // Transient
}
}
container.register('db', () => new Database(), Scope.Singleton);
설명
Singleton은 하나의 인스턴스를 공유하고, Transient는 매번 새로 생성합니다. 리소스 관리가 최적화됩니다.
9. 생성자_주입_vs_메서드_주입
개요
필수 의존성은 생성자로, 선택적 의존성은 메서드로 주입하는 하이브리드 패턴입니다.
코드 예제
class OrderService {
constructor(private db: Database) {} // 필수
private emailService?: EmailService;
setEmailService(service: EmailService) { // 선택
this.emailService = service;
}
createOrder() {
this.db.save(/* ... */);
this.emailService?.sendConfirmation();
}
}
설명
필수 의존성은 생성자로 강제하고, 선택적 기능은 메서드로 주입하여 유연성을 확보합니다.
10. 순환_의존성_해결
개요
Provider 패턴을 활용해 순환 참조 문제를 우아하게 해결합니다.
코드 예제
class ServiceA {
private serviceB?: ServiceB;
setServiceB(b: ServiceB) {
this.serviceB = b;
}
}
class ServiceB {
constructor(private serviceA: ServiceA) {}
}
const a = new ServiceA();
const b = new ServiceB(a);
a.setServiceB(b);
설명
한쪽은 생성자 주입, 다른 쪽은 세터 주입을 사용해 순환 참조를 끊습니다. 의존성 그래프가 명확해집니다.
11. 모듈_기반_DI_시스템
개요
관련된 의존성을 모듈로 그룹화하여 대규모 애플리케이션을 구조화합니다.
코드 예제
class AppModule {
static providers = [
{ provide: 'Logger', useClass: ConsoleLogger },
{ provide: 'Database', useClass: PostgresDB },
];
static create() {
const container = new DIContainer();
this.providers.forEach(p =>
container.register(p.provide, new p.useClass())
);
return container;
}
}
설명
모듈이 관련 서비스를 묶어 등록하고, 애플리케이션 전체 의존성을 체계적으로 관리합니다.
12. 고급_인터셉터_패턴
개요
의존성 호출을 가로채서 로깅, 캐싱, 에러 핸들링 등을 추가하는 AOP 스타일 패턴입니다.
코드 예제
function createProxy<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, prop) {
const original = obj[prop as keyof T];
if (typeof original === 'function') {
return (...args: any[]) => {
console.log(`Calling ${String(prop)}`);
return original.apply(obj, args);
};
}
return original;
}
});
}
설명
Proxy를 사용해 메서드 호출을 가로채고 추가 로직을 투명하게 실행합니다. 횡단 관심사를 분리합니다.
마치며
이번 글에서는 Dependency Injection 기초부터 심화까지에 대해 알아보았습니다. 총 12가지 개념을 다루었으며, 각각의 사용법과 예제를 살펴보았습니다.
관련 태그
#TypeScript #DependencyInjection #DesignPatterns #SOLID #IoC