의존성 주입(Dependency Injection): 유연하고 테스트 가능한 코드 만들기

2025. 7. 2. 10:40·Backend Development
728x90

소프트웨어 개발에서 코드의 유연성과 테스트 가능성을 높이는 핵심 개념 중 하나가 바로 의존성 주입(Dependency Injection)입니다. 객체지향 프로그래밍의 SOLID 원칙 중 하나인 의존성 역전 원칙(Dependency Inversion Principle)을 실현하는 이 패턴에 대해 자세히 알아보겠습니다.


의존성 주입이란?

기본 개념

의존성 주입(Dependency Injection, DI)은 한 객체가 다른 객체를 직접 생성하지 않고, 외부에서 필요한 객체를 주입받아 사용하는 디자인 패턴입니다. 마치 요리사가 직접 재료를 구매하러 가지 않고, 누군가가 미리 준비한 재료를 받아서 요리하는 것과 같습니다.

의존성이란?

코드에서 의존성(Dependency)은 한 클래스가 다른 클래스를 사용하는 관계를 의미합니다.

// 강한 의존성 - 직접 생성
class OrderService {
  private emailService: EmailService;

  constructor() {
    this.emailService = new EmailService(); // 직접 생성 - 강한 결합
  }

  async processOrder(order: Order) {
    // 주문 처리 로직
    await this.emailService.sendConfirmation(order.email);
  }
}

위 코드에서 OrderService는 EmailService에 강하게 의존하고 있습니다. 이는 몇 가지 문제를 야기합니다.


의존성 주입이 없을 때의 문제점

1. 테스트의 어려움

// 테스트하기 어려운 코드
class UserService {
  private database: Database;

  constructor() {
    this.database = new PostgreSQLDatabase(); // 실제 DB에 연결
  }

  async getUser(id: string) {
    return await this.database.findUser(id); // 실제 DB 쿼리 실행
  }
}

// 테스트 시 문제점:
// 1. 실제 데이터베이스가 필요
// 2. 네트워크 의존성
// 3. 테스트 데이터 관리 복잡
// 4. 느린 테스트 실행

2. 유연성 부족

// 변경에 취약한 코드
class PaymentProcessor {
  private gateway: PaymentGateway;

  constructor() {
    this.gateway = new StripeGateway(); // Stripe에 고정
  }

  async processPayment(amount: number) {
    return await this.gateway.charge(amount);
  }
}

// 문제점:
// 1. 다른 결제 게이트웨이 사용 불가
// 2. 코드 수정 없이 변경 불가능
// 3. 환경별 다른 구현체 사용 어려움

3. 코드 재사용성 저하

// 재사용이 어려운 코드
class NotificationService {
  private logger: Logger;

  constructor() {
    this.logger = new FileLogger('/var/log/app.log'); // 파일 경로 고정
  }

  async sendNotification(message: string) {
    this.logger.log(`Sending: ${message}`);
    // 알림 전송 로직
  }
}

// 문제점:
// 1. 다른 로깅 방식 사용 불가
// 2. 테스트 환경에서 파일 로그 생성
// 3. 클라우드 환경에서 설정 변경 어려움

의존성 주입 구현 방법

1. 생성자 주입 (Constructor Injection)

가장 일반적이고 권장되는 방식입니다.

// 인터페이스 정의
interface EmailService {
  sendEmail(to: string, subject: string, body: string): Promise<void>;
}

interface Logger {
  log(message: string): void;
  error(message: string): void;
}

// 구현체들
class SMTPEmailService implements EmailService {
  async sendEmail(to: string, subject: string, body: string) {
    console.log(`SMTP: Sending email to ${to}`);
    // 실제 SMTP 전송 로직
  }
}

class SendGridEmailService implements EmailService {
  async sendEmail(to: string, subject: string, body: string) {
    console.log(`SendGrid: Sending email to ${to}`);
    // SendGrid API 호출 로직
  }
}

class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
  }

  error(message: string) {
    console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
  }
}

// 의존성 주입을 받는 서비스
class OrderService {
  constructor(
    private emailService: EmailService,  // 인터페이스 타입
    private logger: Logger
  ) {}

  async processOrder(order: Order) {
    try {
      this.logger.log(`Processing order ${order.id}`);

      // 주문 처리 로직
      await this.processPayment(order);
      await this.updateInventory(order);

      // 확인 이메일 전송
      await this.emailService.sendEmail(
        order.customerEmail,
        'Order Confirmation',
        `Your order ${order.id} has been processed.`
      );

      this.logger.log(`Order ${order.id} completed successfully`);
    } catch (error) {
      this.logger.error(`Order processing failed: ${error.message}`);
      throw error;
    }
  }

  private async processPayment(order: Order) {
    // 결제 처리 로직
  }

  private async updateInventory(order: Order) {
    // 재고 업데이트 로직
  }
}

// 사용 예시
const emailService = new SMTPEmailService();
const logger = new ConsoleLogger();
const orderService = new OrderService(emailService, logger);

2. 세터 주입 (Setter Injection)

class UserService {
  private database?: Database;
  private cache?: CacheService;

  setDatabase(database: Database) {
    this.database = database;
  }

  setCache(cache: CacheService) {
    this.cache = cache;
  }

  async getUser(id: string): Promise<User> {
    // 캐시 확인
    if (this.cache) {
      const cachedUser = await this.cache.get(`user:${id}`);
      if (cachedUser) return cachedUser;
    }

    // 데이터베이스에서 조회
    if (!this.database) {
      throw new Error('Database not injected');
    }

    const user = await this.database.findUser(id);

    // 캐시에 저장
    if (this.cache) {
      await this.cache.set(`user:${id}`, user, 3600); // 1시간
    }

    return user;
  }
}

// 사용
const userService = new UserService();
userService.setDatabase(new PostgreSQLDatabase());
userService.setCache(new RedisCache());

3. 인터페이스 주입 (Interface Injection)

// 주입 인터페이스 정의
interface DatabaseInjectable {
  injectDatabase(database: Database): void;
}

interface CacheInjectable {
  injectCache(cache: CacheService): void;
}

// 서비스 클래스
class ProductService implements DatabaseInjectable, CacheInjectable {
  private database?: Database;
  private cache?: CacheService;

  injectDatabase(database: Database) {
    this.database = database;
  }

  injectCache(cache: CacheService) {
    this.cache = cache;
  }

  async getProduct(id: string): Promise<Product> {
    if (!this.database) {
      throw new Error('Database not injected');
    }

    return await this.database.findProduct(id);
  }
}

의존성 주입 컨테이너

실제 프로젝트에서는 의존성 주입 컨테이너를 사용하여 객체 생성과 주입을 자동화합니다.

1. 간단한 DI 컨테이너 구현

// 서비스 식별자
const TYPES = {
  EmailService: Symbol('EmailService'),
  Logger: Symbol('Logger'),
  Database: Symbol('Database'),
  OrderService: Symbol('OrderService'),
};

// 간단한 DI 컨테이너
class DIContainer {
  private services = new Map<symbol, any>();
  private factories = new Map<symbol, () => any>();

  // 인스턴스 등록
  register<T>(token: symbol, instance: T): void {
    this.services.set(token, instance);
  }

  // 팩토리 함수 등록
  registerFactory<T>(token: symbol, factory: () => T): void {
    this.factories.set(token, factory);
  }

  // 의존성 해결
  resolve<T>(token: symbol): T {
    // 이미 생성된 인스턴스가 있는지 확인
    if (this.services.has(token)) {
      return this.services.get(token);
    }

    // 팩토리 함수로 생성
    if (this.factories.has(token)) {
      const factory = this.factories.get(token);
      const instance = factory();
      this.services.set(token, instance); // 싱글톤으로 캐시
      return instance;
    }

    throw new Error(`Service not found: ${token.toString()}`);
  }
}

// 컨테이너 설정
const container = new DIContainer();

// 서비스 등록
container.register(TYPES.EmailService, new SMTPEmailService());
container.register(TYPES.Logger, new ConsoleLogger());
container.register(TYPES.Database, new PostgreSQLDatabase());

// 팩토리로 복잡한 의존성 해결
container.registerFactory(TYPES.OrderService, () => {
  return new OrderService(
    container.resolve(TYPES.EmailService),
    container.resolve(TYPES.Logger)
  );
});

// 사용
const orderService = container.resolve<OrderService>(TYPES.OrderService);

2. TypeScript Decorator 활용

// 데코레이터 기반 DI (inversify.js 스타일)
import 'reflect-metadata';

// 데코레이터 정의
function Injectable(target: any) {
  // 클래스를 주입 가능하게 표시
  Reflect.defineMetadata('injectable', true, target);
}

function Inject(token: symbol) {
  return function (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const existingTokens = Reflect.getMetadata('inject-tokens', target) || [];
    existingTokens[parameterIndex] = token;
    Reflect.defineMetadata('inject-tokens', existingTokens, target);
  };
}

// 서비스 클래스들
@Injectable
class EmailService {
  async sendEmail(to: string, subject: string, body: string) {
    console.log(`Sending email to ${to}: ${subject}`);
  }
}

@Injectable
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

@Injectable
class OrderService {
  constructor(
    @Inject(TYPES.EmailService) private emailService: EmailService,
    @Inject(TYPES.Logger) private logger: Logger
  ) {}

  async processOrder(order: Order) {
    this.logger.log(`Processing order ${order.id}`);
    await this.emailService.sendEmail(
      order.customerEmail,
      'Order Confirmation',
      'Your order has been processed.'
    );
  }
}

테스트에서의 의존성 주입 활용

1. Mock 객체 주입

// 테스트용 Mock 구현
class MockEmailService implements EmailService {
  public sentEmails: Array<{to: string, subject: string, body: string}> = [];

  async sendEmail(to: string, subject: string, body: string) {
    this.sentEmails.push({to, subject, body});
    // 실제 이메일 전송하지 않음
  }
}

class MockLogger implements Logger {
  public logs: string[] = [];
  public errors: string[] = [];

  log(message: string) {
    this.logs.push(message);
  }

  error(message: string) {
    this.errors.push(message);
  }
}

// 테스트 코드
describe('OrderService', () => {
  let orderService: OrderService;
  let mockEmailService: MockEmailService;
  let mockLogger: MockLogger;

  beforeEach(() => {
    mockEmailService = new MockEmailService();
    mockLogger = new MockLogger();
    orderService = new OrderService(mockEmailService, mockLogger);
  });

  test('should send confirmation email after processing order', async () => {
    // Given
    const order: Order = {
      id: 'ORDER-001',
      customerEmail: 'customer@example.com',
      items: []
    };

    // When
    await orderService.processOrder(order);

    // Then
    expect(mockEmailService.sentEmails).toHaveLength(1);
    expect(mockEmailService.sentEmails[0]).toEqual({
      to: 'customer@example.com',
      subject: 'Order Confirmation',
      body: 'Your order ORDER-001 has been processed.'
    });

    expect(mockLogger.logs).toContain('Processing order ORDER-001');
    expect(mockLogger.logs).toContain('Order ORDER-001 completed successfully');
  });

  test('should log error when email sending fails', async () => {
    // Given
    const order: Order = {
      id: 'ORDER-002',
      customerEmail: 'invalid-email',
      items: []
    };

    // Mock에서 에러 발생시키기
    mockEmailService.sendEmail = jest.fn().mockRejectedValue(new Error('Email service unavailable'));

    // When & Then
    await expect(orderService.processOrder(order)).rejects.toThrow('Email service unavailable');
    expect(mockLogger.errors).toContain('Order processing failed: Email service unavailable');
  });
});

2. 테스트 더블 패턴

// Spy 패턴
class SpyEmailService implements EmailService {
  private emailCount = 0;

  async sendEmail(to: string, subject: string, body: string) {
    this.emailCount++;
    console.log(`[SPY] Email #${this.emailCount} sent to ${to}`);
  }

  getEmailCount(): number {
    return this.emailCount;
  }
}

// Stub 패턴
class StubDatabase implements Database {
  private users: User[] = [
    { id: '1', name: 'John Doe', email: 'john@example.com' },
    { id: '2', name: 'Jane Smith', email: 'jane@example.com' }
  ];

  async findUser(id: string): Promise<User | null> {
    return this.users.find(user => user.id === id) || null;
  }
}

// Fake 패턴 (메모리 기반 구현)
class FakeDatabase implements Database {
  private users = new Map<string, User>();

  async createUser(user: User): Promise<void> {
    this.users.set(user.id, user);
  }

  async findUser(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  async updateUser(id: string, updates: Partial<User>): Promise<void> {
    const user = this.users.get(id);
    if (user) {
      this.users.set(id, { ...user, ...updates });
    }
  }

  async deleteUser(id: string): Promise<void> {
    this.users.delete(id);
  }

  clear(): void {
    this.users.clear();
  }
}

실제 프레임워크에서의 의존성 주입

1. Angular의 의존성 주입

// Angular 서비스
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root' // 루트 인젝터에 등록
})
export class UserService {
  constructor(private http: HttpClient) {} // 자동 주입

  getUsers() {
    return this.http.get<User[]>('/api/users');
  }
}

// 컴포넌트에서 사용
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users">
      {{ user.name }}
    </div>
  `
})
export class UserListComponent implements OnInit {
  users: User[] = [];

  constructor(private userService: UserService) {} // 자동 주입

  ngOnInit() {
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });
  }
}

2. NestJS의 의존성 주입

// NestJS 서비스
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

// 컨트롤러
import { Controller, Get, Post, Body } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {} // 자동 주입

  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  findAll(): Cat[] {
    return this.catsService.findAll();
  }
}

// 모듈 설정
import { Module } from '@nestjs/common';

@Module({
  controllers: [CatsController],
  providers: [CatsService], // 서비스 등록
})
export class CatsModule {}

3. Spring Boot (Java) 스타일의 TypeScript 구현

// 데코레이터 기반 DI 프레임워크
class ApplicationContext {
  private static instance: ApplicationContext;
  private beans = new Map<string, any>();
  private beanDefinitions = new Map<string, any>();

  static getInstance(): ApplicationContext {
    if (!ApplicationContext.instance) {
      ApplicationContext.instance = new ApplicationContext();
    }
    return ApplicationContext.instance;
  }

  registerBean(name: string, type: any) {
    this.beanDefinitions.set(name, type);
  }

  getBean<T>(name: string): T {
    if (this.beans.has(name)) {
      return this.beans.get(name);
    }

    const BeanType = this.beanDefinitions.get(name);
    if (!BeanType) {
      throw new Error(`Bean not found: ${name}`);
    }

    // 의존성 해결하여 인스턴스 생성
    const instance = this.createInstance(BeanType);
    this.beans.set(name, instance);
    return instance;
  }

  private createInstance(BeanType: any): any {
    // 생성자 파라미터 메타데이터 읽기
    const dependencies = Reflect.getMetadata('dependencies', BeanType) || [];
    const args = dependencies.map((dep: string) => this.getBean(dep));

    return new BeanType(...args);
  }
}

// 데코레이터들
function Component(name?: string) {
  return function(target: any) {
    const beanName = name || target.name.toLowerCase();
    ApplicationContext.getInstance().registerBean(beanName, target);
  };
}

function Autowired(beanName: string) {
  return function(target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const existingDeps = Reflect.getMetadata('dependencies', target) || [];
    existingDeps[parameterIndex] = beanName;
    Reflect.defineMetadata('dependencies', existingDeps, target);
  };
}

// 사용 예시
@Component('userService')
class UserService {
  constructor(
    @Autowired('userRepository') private userRepository: UserRepository
  ) {}

  async getUser(id: string): Promise<User> {
    return await this.userRepository.findById(id);
  }
}

@Component('userRepository')
class UserRepository {
  async findById(id: string): Promise<User> {
    // 데이터베이스 조회 로직
    return new User(id, 'John Doe');
  }
}

의존성 주입의 장점과 주의사항

장점 정리

장점 설명 예시
테스트 용이성 Mock 객체 주입으로 단위 테스트 간편 실제 DB 대신 메모리 DB 사용
유연성 런타임에 다른 구현체 교체 가능 개발/운영 환경별 다른 서비스
결합도 감소 인터페이스 기반 프로그래밍 구현체 변경이 클라이언트에 영향 없음
재사용성 다양한 컨텍스트에서 같은 코드 활용 같은 서비스를 여러 컨트롤러에서 사용
단일 책임 원칙 객체 생성과 비즈니스 로직 분리 서비스는 비즈니스 로직에만 집중

주의사항

1. 순환 의존성 문제

// ❌ 순환 의존성 - 피해야 할 패턴
class UserService {
  constructor(private orderService: OrderService) {}
}

class OrderService {
  constructor(private userService: UserService) {} // 순환!
}

// ✅ 해결 방법 1: 인터페이스 분리
interface UserEvents {
  onUserUpdate(userId: string): void;
}

class UserService {
  private listeners: UserEvents[] = [];

  addListener(listener: UserEvents) {
    this.listeners.push(listener);
  }

  updateUser(userId: string) {
    // 업데이트 로직
    this.listeners.forEach(listener => listener.onUserUpdate(userId));
  }
}

class OrderService implements UserEvents {
  onUserUpdate(userId: string) {
    // 사용자 업데이트에 반응하는 로직
  }
}

// ✅ 해결 방법 2: 중간 서비스 도입
class UserOrderMediator {
  constructor(
    private userService: UserService,
    private orderService: OrderService
  ) {}

  updateUserAndOrders(userId: string) {
    this.userService.updateUser(userId);
    this.orderService.updateUserOrders(userId);
  }
}

2. 과도한 추상화

// ❌ 불필요한 추상화
interface StringFormatter {
  format(str: string): string;
}

class UpperCaseFormatter implements StringFormatter {
  format(str: string): string {
    return str.toUpperCase();
  }
}

class SimpleService {
  constructor(private formatter: StringFormatter) {} // 과도한 추상화

  formatMessage(message: string): string {
    return this.formatter.format(message);
  }
}

// ✅ 간단한 경우 직접 사용
class SimpleService {
  formatMessage(message: string): string {
    return message.toUpperCase(); // 간단한 로직은 직접 구현
  }
}

3. 성능 고려사항

// 지연 로딩(Lazy Loading) 구현
class HeavyService {
  private _expensiveResource?: ExpensiveResource;

  private get expensiveResource(): ExpensiveResource {
    if (!this._expensiveResource) {
      this._expensiveResource = new ExpensiveResource();
    }
    return this._expensiveResource;
  }

  doSomething() {
    // 필요할 때만 생성
    this.expensiveResource.performOperation();
  }
}

// 프록시 패턴을 활용한 지연 로딩
class LazyProxy<T> {
  private instance?: T;

  constructor(private factory: () => T) {}

  get(): T {
    if (!this.instance) {
      this.instance = this.factory();
    }
    return this.instance;
  }
}

// 사용
const heavyServiceProxy = new LazyProxy(() => new HeavyService());

실무 적용 가이드

프로젝트 도입 단계

1단계: 인터페이스 정의

// 먼저 핵심 서비스들의 인터페이스 정의
interface IUserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

interface IEmailService {
  sendEmail(to: string, subject: string, body: string): Promise<void>;
}

interface ILogger {
  log(message: string): void;
  error(message: string): void;
}

2단계: 구현체 작성

// 프로덕션 구현체
class DatabaseUserRepository implements IUserRepository {
  async findById(id: string): Promise<User | null> {
    // 실제 DB 조회
  }

  async save(user: User): Promise<void> {
    // 실제 DB 저장
  }
}

// 테스트 구현체
class InMemoryUserRepository implements IUserRepository {
  private users = new Map<string, User>();

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  async save(user: User): Promise<void> {
    this.users.set(user.id, user);
  }
}

3단계: 서비스 레이어 수정

// 의존성 주입 적용
class UserService {
  constructor(
    private userRepository: IUserRepository,
    private emailService: IEmailService,
    private logger: ILogger
  ) {}

  async createUser(userData: CreateUserData): Promise<User> {
    try {
      const user = new User(userData);
      await this.userRepository.save(user);

      await this.emailService.sendEmail(
        user.email,
        'Welcome!',
        'Welcome to our platform!'
      );

      this.logger.log(`User created: ${user.id}`);
      return user;
    } catch (error) {
      this.logger.error(`User creation failed: ${error.message}`);
      throw error;
    }
  }
}

4단계: 의존성 구성

// 프로덕션 환경 설정
function createProductionContainer(): DIContainer {
  const container = new DIContainer();

  container.register(TYPES.UserRepository, new DatabaseUserRepository());
  container.register(TYPES.EmailService, new SMTPEmailService());
  container.register(TYPES.Logger, new FileLogger());

  container.registerFactory(TYPES.UserService, () =>
    new UserService(
      container.resolve(TYPES.UserRepository),
      container.resolve(TYPES.EmailService),
      container.resolve(TYPES.Logger)
    )
  );

  return container;
}

// 테스트 환경 설정
function createTestContainer(): DIContainer {
  const container = new DIContainer();

  container.register(TYPES.UserRepository, new InMemoryUserRepository());
  container.register(TYPES.EmailService, new MockEmailService());
  container.register(TYPES.Logger, new ConsoleLogger());

  container.registerFactory(TYPES.UserService, () =>
    new UserService(
      container.resolve(TYPES.UserRepository),
      container.resolve(TYPES.EmailService),
      container.resolve(TYPES.Logger)
    )
  );

  return container;
}

결론

의존성 주입은 소프트웨어 개발에서 유연성, 테스트 가능성, 유지보수성을 크게 향상시키는 강력한 패턴입니다. 마치 레고 블록처럼 각 부품을 독립적으로 설계하고 필요에 따라 조립할 수 있게 해줍니다.

핵심 포인트 요약:

  • 느슨한 결합: 인터페이스를 통한 의존성 관리
  • 테스트 친화적: Mock과 Stub을 통한 쉬운 단위 테스트
  • 유연성: 런타임에 다른 구현체로 교체 가능
  • 재사용성: 같은 코드를 다양한 환경에서 활용

의존성 주입을 도입할 때는 점진적으로 적용하고, 과도한 추상화는 피하며, 팀의 코딩 규칙과 일관성을 유지하는 것이 중요합니다. 이 패턴을 잘 활용하면 더 견고하고 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다.

728x90
저작자표시 비영리 변경금지 (새창열림)

'Backend Development' 카테고리의 다른 글

CI/CD 파이프라인: 개발부터 배포까지 자동화의 모든 것  (1) 2025.07.04
네트워크 IP 할당의 모든 것: 정적 IP vs 동적 IP, 무엇을 선택해야 할까?  (1) 2025.06.27
CSRF 공격과 방어 전략: 웹 보안의 핵심 이해하기  (4) 2025.06.23
시스템 콜(System Call)이란? 운영체제와 프로그램 간의 소통 창구 완전 정복  (2) 2025.06.11
Redis의 싱글 스레드 아키텍처: 왜 빠른가?  (2) 2025.05.30
'Backend Development' 카테고리의 다른 글
  • CI/CD 파이프라인: 개발부터 배포까지 자동화의 모든 것
  • 네트워크 IP 할당의 모든 것: 정적 IP vs 동적 IP, 무엇을 선택해야 할까?
  • CSRF 공격과 방어 전략: 웹 보안의 핵심 이해하기
  • 시스템 콜(System Call)이란? 운영체제와 프로그램 간의 소통 창구 완전 정복
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (108) N
      • Frontend Development (44) N
      • Backend Development (24) N
      • Algorithm (33)
        • 백준 (11)
        • 프로그래머스 (17)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (3)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    frontend development
    tailwindcss
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
의존성 주입(Dependency Injection): 유연하고 테스트 가능한 코드 만들기
상단으로

티스토리툴바