TypeScript 클래스 설계 완전 정복: constructor vs 필드 초기화 차이와 활용법
2025. 5. 22. 14:11
728x90

글을 쓰게 된 계기

최근 TypeScript 기반의 인증 서비스를 리팩토링하면서, 서비스 클래스 내부에서 값을 어떻게 초기화할지 고민할 일이 많았습니다. constructor를 활용해 외부 값을 주입할지, 아니면 내부에서 직접 고정값을 넣을지에 따라 테스트 코드 작성의 난이도와 유연성이 크게 달라졌기 때문입니다.

이 경험을 바탕으로, constructor와 필드 직접 초기화의 차이를 명확히 비교하고 각각의 적절한 사용처를 정리해두면 좋겠다는 생각에 이 글을 작성하게 되었습니다.


클래스를 설계할 때 생성자(constructor)를 사용할지, 아니면 필드에 직접 값을 초기화할지에 따라 코드의 유연성과 테스트 가능성에 큰 차이가 발생합니다. 본 글에서는 두 방식의 차이를 예제와 함께 명확히 비교해보겠습니다.


1. 비교 대상 코드

① 생성자(constructor) 사용 예

class UserService {
  constructor(private readonly name: string) {
    // 생성자 내부에서는 다음과 같은 작업이 가능합니다:
    // - 주입받은 값을 this에 할당
    // - 초기 로직 실행
    // - 유효성 검사 수행
    console.log('UserService가 생성되었습니다.');
  }

  greet() {
    console.log(`Hello, ${this.name}`);
  }
}

② constructor 없이 필드 초기화

class UserService {
  name = 'default'; // 직접 필드 초기화

  greet() {
    console.log(`Hello, ${this.name}`);
  }
}

2. 차이점 정리

항목  constructor 사용 필드 직접 초기화
객체 생성 시 인자 주입 new UserService('John') 등으로 가능 외부에서 인자 전달 불가 (고정값 사용)
동적 초기화 가능 여부 가능 불가능 (정적인 값만)
DI 및 테스트 적합성 높음 (Mock 객체 주입 가능) 낮음 (강결합 구조)
필드 접근 방식 생성자 내부에서 this.xxx 초기화 클래스 내부에서 직접 선언 및 초기화
접근 제어자 활용 private, public, readonly 등 자유롭게 사용 가능하나 초기화가 분리됨

3. 실제 사용 예제 비교

① 생성자 기반

const user1 = new UserService('Alice');
user1.greet(); // Hello, Alice

const user2 = new UserService('Bob');
user2.greet(); // Hello, Bob

② 필드 직접 초기화 기반

const user1 = new UserService();
user1.greet(); // Hello, default

const user2 = new UserService();
user2.name = 'Bob'; // public이어야 가능
user2.greet(); // Hello, Bob

4. 실제 차이가 중요한 이유

✔️ 의존성 주입(DI) 및 테스트

class RealUserRepo {}
class MockUserRepo {}

class UserService {
  constructor(private repo: UserRepo) {}
}

// 테스트 시
new UserService(new MockUserRepo()); // 가능

vs.

class UserService {
  private repo = new RealUserRepo(); // 강하게 결합됨
}

→ 테스트 및 확장성에 불리함


✔️ 정적 vs 동적 초기화

  • constructor 방식: 객체 생성 시점에 동적으로 값 주입 가능
  • 필드 직접 초기화: 코드에 하드코딩된 값만 사용 가능 (정적)

5. 각 방식이 자주 사용되는 상황

 생성자(constructor) 방식이 자주 쓰이는 경우

  • 외부로부터 의존성을 주입받아야 하는 경우 (DI 패턴)
  • 서비스 클래스, 레포지토리, 유즈케이스 등에서 객체 간 결합도를 낮추기 위해
  • 테스트 시 Mock 객체를 주입해야 하는 경우
  • 설정값을 외부에서 받아야 하는 설정 클래스
  • 사용자 입력 기반 동적 값 처리 등

예시:

class AuthService {
  constructor(private readonly userRepo: UserRepository) {}
}

 필드 직접 초기화 방식이 자주 쓰이는 경우

  • 고정된 설정 값이나 내부 상수로만 동작하는 클래스
  • 단순한 유틸 클래스, 헬퍼 함수 모음 등
  • 기본 상태가 정해져 있어 외부 값 주입이 불필요한 경우

예시:

class DateFormatter {
  format = 'YYYY-MM-DD';

  formatDate(date: Date) {
    // ...format logic
  }
}

6. 결론: 어떤 상황에서 어떤 방식을 선택할까?

상황 추천 방식
외부에서 값을 주입받아야 할 때 constructor 사용
고정된 값만 필요할 때 필드에 직접 초기화
테스트 및 확장 가능한 구조가 필요할 때 무조건 constructor 방식 권장

유연하고 테스트 가능한 구조를 갖춘 서비스를 설계하려면 의존성 주입(DI)을 고려한 생성자 기반 설계를 사용하는 것이 바람직합니다. 단순하고 하드코딩된 값만 필요한 경우에는 필드 초기화도 괜찮지만, 실제 서비스 코드에서는 생성자 패턴을 표준으로 삼는 것이 유지보수성과 확장성 측면에서 유리합니다.

728x90
JeongPark
JeongPark