SOLID 원칙 적용
SOLID 원칙은 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 말합니다.
타입스크립트에서 이 원칙들을 적용하면 더 유지보수가 쉽고, 유연하며, 확장 가능한 소프트웨어를 만들 수 있습니다.
단일 책임 원칙
단일 책임 원칙 (Single Responsibility Principle)은 하나의 클래스나 모듈이 하나의 책임만을 가져야 한다고 말합니다.
타입스크립트 예시
// 나쁜 예
class User {
constructor(public name: string, public email: string) {}
saveToDatabase() { /* ... */ }
sendEmail() { /* ... */ }
}
// 좋은 예
class User {
constructor(public name: string, public email: string) {}
}
class UserRepository {
saveUser(user: User) { /* ... */ }
}
class EmailService {
sendEmail(user: User, message: string) { /* ... */ }
}
타입스크립트의 인터페이스를 사용하면 이 원칙을 더 잘 지킬 수 있습니다.
interface UserData {
name: string;
email: string;
}
interface UserRepository {
save(user: UserData): Promise<void>;
}
interface EmailService {
send(to: string, message: string): Promise<void>;
}
개방-폐쇄 원칙
개방-폐쇄 원칙 (Open-Closed Principle)에서는 소프트웨어 엔티티는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 합니다.
타입스크립트 예시
abstract class Shape {
abstract area(): number;
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
area(): number {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
function calculateTotalArea(shapes: Shape[]): number {
return shapes.reduce((total, shape) => total + shape.area(), 0);
}
여기서 calculateTotalArea
함수는 새로운 Shape
타입이 추가되어도 수정할 필요가 없습니다.
리스코프 치환 원칙
리스코프 치환 원칙 (Liskov Substitution Principle), 하위 타입은 상위 타입을 대체할 수 있어야 합니다.
interface Bird {
fly(): void;
}
class Sparrow implements Bird {
fly() {
console.log("Sparrow flying");
}
}
class Ostrich implements Bird {
fly() {
throw new Error("Ostriches can't fly");
}
}
// 이 경우 Ostrich는 LSP를 위반합니다.
// 더 나은 설계:
interface FlyingBird {
fly(): void;
}
interface RunningBird {
run(): void;
}
class Sparrow implements FlyingBird {
fly() {
console.log("Sparrow flying");
}
}
class Ostrich implements RunningBird {
run() {
console.log("Ostrich running");
}
}
인터페이스 분리 원칙
인터페이스 분리 원칙 (Interface Segregation Principle), 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하도록 강제되어서는 안 됩니다.
// 나쁜 예
interface Worker {
work(): void;
eat(): void;
}
// 좋은 예
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class Human implements Workable, Eatable {
work() { /* ... */ }
eat() { /* ... */ }
}
class Robot implements Workable {
work() { /* ... */ }
}
의존관계 역전 원칙
의존관계 역전 원칙 (Dependency Inversion Principle), 고수준 모듈은 저수준 모듈에 의존해서는 안 되며 둘 다 추상화에 의존해야 합니다.
// 나쁜 예
class EmailNotifier {
sendEmail(message: string) { /* ... */ }
}
class NotificationService {
private emailNotifier = new EmailNotifier();
notify(message: string) {
this.emailNotifier.sendEmail(message);
}
}
// 좋은 예
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(message: string) { /* ... */ }
}
class SMSNotifier implements Notifier {
send(message: string) { /* ... */ }
}
class NotificationService {
constructor(private notifier: Notifier) {}
notify(message: string) {
this.notifier.send(message);
}
}
타입스크립트의 이점
타입스크립트의 정적 타입 시스템은 SOLID 원칙 적용에 큰 도움을 줍니다.
- 인터페이스와 추상 클래스를 통한 명확한 계약 정의
- 제네릭을 통한 재사용 가능한 컴포넌트 생성
- 컴파일 시간에 원칙 위반 감지 가능
제네릭을 활용한 SRP와 ISP 강화
interface Repository<T> {
find(id: string): Promise<T>;
save(item: T): Promise<void>;
}
class UserRepository implements Repository<User> {
// 구현...
}
class ProductRepository implements Repository<Product> {
// 구현...
}
이 접근 방식은 각 리포지토리가 단일 책임을 가지면서도, 공통 인터페이스를 통해 일관성을 유지합니다.
균형 잡힌 SOLID 적용
SOLID 원칙을 과도하게 적용하면 불필요한 복잡성이 증가할 수 있습니다. 다음 지침을 고려하세요.
- 실제 필요성에 기반한 추상화 도입
- 현재와 가까운 미래의 요구사항만 고려
- 팀의 기술 수준과 프로젝트 규모에 맞는 설계 복잡도 유지
레거시 코드 리팩토링 전략
- 점진적 접근 : 한 번에 하나의 원칙에 집중
- 단위 테스트 작성 : 리팩토링 전 동작을 보장
- 인터페이스 도입 : 기존 클래스를 인터페이스로 추상화
- 의존성 주입 도입 : 하드코딩된 의존성을 제거
- 책임 분리 : 큰 클래스를 작은 클래스들로 분할
Best Practices와 주의사항
- 인터페이스 우선 : 구체적인 구현보다 인터페이스에 의존
- 의존성 주입 활용 : 생성자 주입을 통한 느슨한 결합 구현
- 작은 인터페이스 선호 : 큰 인터페이스보다 작고 집중된 인터페이스 사용
- 상속보다 합성 : 상속 대신 합성을 통한 유연한 설계
- 단위 테스트 작성 : SOLID 원칙 준수 여부를 테스트로 검증
- 문서화 : 복잡한 추상화의 의도와 사용법 문서화
- 과도한 엔지니어링 주의 : 실제 필요 이상의 추상화 지양
- 지속적인 리팩토링 : 코드 품질 유지를 위한 정기적인 리팩토링 수행
타입스크립트의 강력한 타입 시스템은 이러한 원칙들을 더 효과적으로 구현하고 강제할 수 있게 해줍니다.
특히, 인터페이스와 추상 클래스를 활용한 개방-폐쇄 원칙과 리스코프 치환 원칙의 구현은 타입스크립트에서 매우 자연스럽게 이루어집니다.
타입 체크를 통해 상위 타입을 하위 타입으로 안전하게 대체할 수 있는지 컴파일 시간에 확인할 수 있기 때문입니다.
제네릭의 사용은 단일 책임 원칙과 인터페이스 분리 원칙을 강화하는 데 큰 도움이 됩니다.
제네릭을 통해 타입에 구애받지 않는 재사용 가능한 컴포넌트를 만들 수 있으며, 이는 코드의 중복을 줄이고 응집도를 높이는 데 기여합니다.
의존관계 역전 원칙의 구현에 있어서도 타입스크립트의 인터페이스와 의존성 주입 패턴을 결합하면 모듈 간의 결합도를 크게 낮출 수 있습니다.