4.3 정적 멤버와 고급 클래스 패턴
정적(static) 멤버는 클래스의 인스턴스가 아닌 클래스 자체에 속하는 프로퍼티와 메서드다. new로 객체를 만들지 않아도 ClassName.method()로 직접 호출한다. 유틸리티 함수, 팩토리 메서드, 싱글톤 상태 관리처럼 "인스턴스 수준이 아니라 클래스 수준에서 존재해야 하는 것"에 사용한다.
이 장에서는 static 멤버의 기초부터 ES2022 정적 초기화 블록, 싱글톤/팩토리 패턴, Fluent Interface를 위한 this 타입, getter/setter까지 실무 패턴을 다룬다.
static 프로퍼티와 메서드
static 키워드를 붙이면 클래스 수준 멤버가 된다. 인스턴스에서는 접근할 수 없고 클래스 이름으로만 접근한다.
class MathUtils {
static readonly PI: number = 3.14159265358979;
static circleArea(radius: number): number {
return MathUtils.PI * radius ** 2;
}
static degreesToRadians(degrees: number): number {
return degrees * (MathUtils.PI / 180);
}
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
}
console.log(MathUtils.circleArea(5)); // 78.539...
console.log(MathUtils.degreesToRadians(180)); // 3.14159...
console.log(MathUtils.clamp(150, 0, 100)); // 100
// const m = new MathUtils();
// m.circleArea(5); // 오류: 정적 멤버는 인스턴스에서 접근 불가
정적 카운터 예제
정적 프로퍼티로 모든 인스턴스에 걸쳐 공유 상태를 관리한다.
class UserSession {
private static activeCount: number = 0;
private static sessions: Map<string, UserSession> = new Map();
readonly sessionId: string;
readonly userId: number;
private startTime: Date;
constructor(userId: number) {
this.userId = userId;
this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
this.startTime = new Date();
UserSession.activeCount++;
UserSession.sessions.set(this.sessionId, this);
}
end(): void {
UserSession.sessions.delete(this.sessionId);
UserSession.activeCount--;
console.log(`세션 ${this.sessionId} 종료. 남은 세션: ${UserSession.activeCount}`);
}
static getActiveCount(): number {
return UserSession.activeCount;
}
static findByUserId(userId: number): UserSession | undefined {
for (const session of UserSession.sessions.values()) {
if (session.userId === userId) return session;
}
return undefined;
}
}
const s1 = new UserSession(1001);
const s2 = new UserSession(1002);
console.log(UserSession.getActiveCount()); // 2
s1.end();
console.log(UserSession.getActiveCount()); // 1
정적 초기화 블록 (Static Initialization Block, ES2022)
복잡한 정적 초기화 로직은 static { } 블록에 작성한다. 클래스가 처음 로드될 때 한 번 실행된다. TypeScript 4.4+에서 지원한다.
class DatabaseConfig {
static readonly host: string;
static readonly port: number;
static readonly dbName: string;
static readonly connectionString: string;
static readonly isProduction: boolean;
static {
// 복잡한 초기화 로직: try/catch, 조건 분기 모두 가능
DatabaseConfig.isProduction = process.env.NODE_ENV === "production";
if (DatabaseConfig.isProduction) {
DatabaseConfig.host = process.env.DB_HOST ?? "prod-db.example.com";
DatabaseConfig.port = parseInt(process.env.DB_PORT ?? "5432", 10);
DatabaseConfig.dbName = process.env.DB_NAME ?? "prod_db";
} else {
DatabaseConfig.host = "localhost";
DatabaseConfig.port = 5432;
DatabaseConfig.dbName = "dev_db";
}
DatabaseConfig.connectionString =
`postgresql://${DatabaseConfig.host}:${DatabaseConfig.port}/${DatabaseConfig.dbName}`;
console.log(
`DB 설정 초기화 완료: ${DatabaseConfig.connectionString}`
);
}
}
// 클래스에 처음 접근할 때 static 블록 실행
console.log(DatabaseConfig.connectionString);
정적 블록은 여러 개 선언할 수 있고 선언 순서대로 실행된다.
class MultiStageInit {
static stage1: string;
static stage2: string;
static {
MultiStageInit.stage1 = "1단계 초기화";
console.log(MultiStageInit.stage1);
}
static {
MultiStageInit.stage2 = `${MultiStageInit.stage1} 완료 후 2단계`;
console.log(MultiStageInit.stage2);
}
}
싱글톤 패턴
애플리케이션 전체에서 인스턴스가 하나만 존재해야 하는 경우에 사용한다. private 생성자와 static getInstance()로 구현한다.
class Logger {
private static instance: Logger | null = null;
private logs: string[] = [];
private logLevel: "debug" | "info" | "warn" | "error" = "info";
// private 생성자: 외부에서 new Logger() 불가
private constructor() {
console.log("Logger 인스턴스 생성됨 (최초 1회)");
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
setLevel(level: "debug" | "info" | "warn" | "error"): void {
this.logLevel = level;
}
private shouldLog(level: "debug" | "info" | "warn" | "error"): boolean {
const levels = ["debug", "info", "warn", "error"];
return levels.indexOf(level) >= levels.indexOf(this.logLevel);
}
private log(level: "debug" | "info" | "warn" | "error", message: string): void {
if (!this.shouldLog(level)) return;
const entry = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
this.logs.push(entry);
console.log(entry);
}
debug(message: string): void { this.log("debug", message); }
info(message: string): void { this.log("info", message); }
warn(message: string): void { this.log("warn", message); }
error(message: string): void { this.log("error", message); }
getLogs(): string[] { return [...this.logs]; }
clearLogs(): void { this.logs = []; }
}
// 사용
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true — 동일 인스턴스
logger1.info("서버 시작");
logger2.warn("메모리 부족 경고"); // logger1과 같은 인스턴스
// new Logger(); // 오류: private 생성자
팩토리 메서드 패턴
생성자를 직접 노출하지 않고 정적 팩토리 메서드로 객체를 생성한다. 이름 있는 생성자 패턴이라고도 한다.
class Color {
private constructor(
public readonly r: number,
public readonly g: number,
public readonly b: number,
public readonly a: number = 1
) {
if ([r, g, b].some((v) => v < 0 || v > 255)) {
throw new Error("RGB 값은 0~255 범위여야 합니다.");
}
if (a < 0 || a > 1) {
throw new Error("알파 값은 0~1 범위여야 합니다.");
}
}
// 팩토리 메서드들 — 다양한 생성 방법에 의미 있는 이름 부여
static fromRGB(r: number, g: number, b: number): Color {
return new Color(r, g, b);
}
static fromHex(hex: string): Color {
const clean = hex.replace("#", "");
return new Color(
parseInt(clean.slice(0, 2), 16),
parseInt(clean.slice(2, 4), 16),
parseInt(clean.slice(4, 6), 16)
);
}
static fromRGBA(r: number, g: number, b: number, a: number): Color {
return new Color(r, g, b, a);
}
static white(): Color { return new Color(255, 255, 255); }
static black(): Color { return new Color(0, 0, 0); }
static transparent(): Color { return new Color(0, 0, 0, 0); }
toHex(): string {
const toHex = (n: number) => n.toString(16).padStart(2, "0");
return `#${toHex(this.r)}${toHex(this.g)}${toHex(this.b)}`;
}
toString(): string {
return this.a === 1
? `rgb(${this.r}, ${this.g}, ${this.b})`
: `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
}
withAlpha(a: number): Color {
return new Color(this.r, this.g, this.b, a);
}
}
const red = Color.fromRGB(255, 0, 0);
const blue = Color.fromHex("#0000FF");
const semiTransparent = Color.fromRGBA(128, 128, 128, 0.5);
console.log(red.toHex()); // #ff0000
console.log(blue.toString()); // rgb(0, 0, 255)
console.log(semiTransparent.toString()); // rgba(128, 128, 128, 0.5)
console.log(Color.white().toHex()); // #ffffff
this 타입 — Fluent Interface (메서드 체이닝)
this를 반환 타입으로 사용하면 서브클래스에서도 체이닝이 끊기지 않는다.
class QueryBuilder {
protected conditions: string[] = [];
protected selectedColumns: string[] = ["*"];
protected tableName: string = "";
protected limitValue: number | null = null;
protected offsetValue: number | null = null;
protected orderByClause: string | null = null;
from(table: string): this {
this.tableName = table;
return this;
}
select(...columns: string[]): this {
this.selectedColumns = columns;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
limit(n: number): this {
this.limitValue = n;
return this;
}
offset(n: number): this {
this.offsetValue = n;
return this;
}
orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
this.orderByClause = `${column} ${direction}`;
return this;
}
build(): string {
const cols = this.selectedColumns.join(", ");
let query = `SELECT ${cols} FROM ${this.tableName}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(" AND ")}`;
}
if (this.orderByClause) {
query += ` ORDER BY ${this.orderByClause}`;
}
if (this.limitValue !== null) {
query += ` LIMIT ${this.limitValue}`;
}
if (this.offsetValue !== null) {
query += ` OFFSET ${this.offsetValue}`;
}
return query;
}
}
// 서브클래스에서 this 타입 덕분에 체이닝 유지
class PostgresQueryBuilder extends QueryBuilder {
withNoLock(): this {
// PostgreSQL 힌트 추가
return this;
}
returning(...columns: string[]): this {
// RETURNING 절 추가
return this;
}
}
const query = new PostgresQueryBuilder()
.from("users")
.select("id", "name", "email")
.where("age > 18")
.where("status = 'active'")
.orderBy("created_at", "DESC")
.limit(20)
.offset(40)
.withNoLock() // 서브클래스 메서드
.build(); // 체이닝 끊기지 않음
console.log(query);
// SELECT id, name, email FROM users
// WHERE age > 18 AND status = 'active'
// ORDER BY created_at DESC LIMIT 20 OFFSET 40
getter/setter — 계산된 프로퍼티와 유효성 검사
get과 set 키워드로 프로퍼티처럼 사용하면서 내부에서 로직을 실행할 수 있다.
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
// getter: 계산된 프로퍼티
get celsius(): number {
return this._celsius;
}
// setter: 유효성 검사 포함
set celsius(value: number) {
if (value < -273.15) {
throw new Error("절대 영도(-273.15°C) 이하는 불가능합니다.");
}
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * (9 / 5) + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * (5 / 9);
}
get kelvin(): number {
return this._celsius + 273.15;
}
toString(): string {
return `${this._celsius}°C / ${this.fahrenheit.toFixed(1)}°F / ${this.kelvin.toFixed(2)}K`;
}
}
const temp = new Temperature(100);
console.log(temp.toString()); // 100°C / 212.0°F / 373.15K
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
console.log(temp.toString()); // 0°C / 32.0°F / 273.15K
// temp.celsius = -300; // 오류: 절대 영도 이하
실전 예제: 싱글톤 Logger, 체이닝 쿼리 빌더, 설정 관리자
설정 관리자 (Singleton + getter/setter)
type LogLevel = "debug" | "info" | "warn" | "error";
type Environment = "development" | "staging" | "production";
interface AppSettings {
env: Environment;
logLevel: LogLevel;
maxRetries: number;
timeout: number;
apiBaseUrl: string;
}
class AppConfig {
private static instance: AppConfig;
private settings: AppSettings;
private constructor() {
// 기본값 설정
this.settings = {
env: (process.env.NODE_ENV as Environment) ?? "development",
logLevel: "info",
maxRetries: 3,
timeout: 5000,
apiBaseUrl: "https://api.example.com",
};
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
get env(): Environment { return this.settings.env; }
get logLevel(): LogLevel { return this.settings.logLevel; }
set logLevel(level: LogLevel) {
this.settings.logLevel = level;
console.log(`로그 레벨 변경: ${level}`);
}
get maxRetries(): number { return this.settings.maxRetries; }
set maxRetries(value: number) {
if (value < 0 || value > 10) {
throw new Error("재시도 횟수는 0~10 사이여야 합니다.");
}
this.settings.maxRetries = value;
}
get isProduction(): boolean {
return this.settings.env === "production";
}
get apiBaseUrl(): string { return this.settings.apiBaseUrl; }
// Fluent 방식으로 여러 설정을 한 번에 변경
configure(partial: Partial<AppSettings>): this {
Object.assign(this.settings, partial);
return this;
}
snapshot(): Readonly<AppSettings> {
return { ...this.settings };
}
}
// 사용
const config = AppConfig.getInstance();
config
.configure({ timeout: 10000, maxRetries: 5 });
config.logLevel = "debug";
console.log(config.snapshot());
console.log(config.isProduction); // false (개발 환경)
고수 팁
static 멤버와 상속
정적 멤버도 서브클래스에서 접근할 수 있다. 단, this를 사용하면 서브클래스 컨텍스트를 그대로 유지한다.
class Base {
static type: string = "Base";
static create(): Base {
console.log(`Creating: ${this.type}`);
return new this(); // new this()로 현재 클래스의 인스턴스 생성
}
}
class Child extends Base {
static type: string = "Child";
}
const b = Base.create(); // Creating: Base
const c = Child.create(); // Creating: Child — 서브클래스 컨텍스트
정적 팩토리 vs 생성자
| 기준 | 생성자 | 정적 팩토리 |
|---|---|---|
| 이름 | 없음 (클래스 이름) | 의미 있는 이름 가능 |
| 캐싱 | 불가 | 가능 (싱글톤, 캐시 반환) |
| 반환 타입 | 항상 해당 클래스 | 서브타입 반환 가능 |
| 오버로딩 | 제한적 | 완전히 자유로운 시그니처 |
| 실패 처리 | 예외만 | null/undefined/Result 반환 가능 |
class Connection {
private static pool: Connection[] = [];
private constructor(private url: string) {}
// 캐시에서 반환하거나 새로 생성
static getConnection(url: string): Connection {
const cached = Connection.pool.find((c) => c.url === url);
if (cached) {
console.log("캐시된 연결 반환");
return cached;
}
const conn = new Connection(url);
Connection.pool.push(conn);
return conn;
}
// 실패 시 null 반환 (생성자는 불가)
static tryCreate(url: string): Connection | null {
try {
if (!url.startsWith("http")) return null;
return new Connection(url);
} catch {
return null;
}
}
}
정리
| 개념 | 문법 | 핵심 특징 |
|---|---|---|
| 정적 프로퍼티 | static prop: T | 클래스 수준 상태, 인스턴스 간 공유 |
| 정적 메서드 | static method() | 인스턴스 없이 호출 |
| 정적 초기화 블록 | static { } | 복잡한 초기화, try/catch 가능 |
| 싱글톤 | private constructor + static getInstance() | 인스턴스 1개 보장 |
| 팩토리 메서드 | static create() | 의미 있는 이름, 캐싱, 유연한 반환 |
| this 타입 | 반환 타입 this | 서브클래스 체이닝 유지 |
| getter | get prop() | 계산된 프로퍼티, 읽기 전용 가능 |
| setter | set prop(v) | 유효성 검사, 사이드 이펙트 |
다음 장에서는 TypeScript의 데코레이터를 배운다. 클래스, 메서드, 프로퍼티, 파라미터에 메타데이터를 주입하거나 동작을 변형하는 강력한 메타프로그래밍 도구를 NestJS, TypeORM 실무 예제와 함께 익힌다.