소프트웨어 시스템의 유지보수성과 확장성은 장기적인 성공을 좌우하는 핵심 요소이다. SOLID 원칙은 로버트 C. 마틴(Robert C. Martin)이 정립한 객체 지향 설계의 다섯 가지 핵심 원칙의 앞글자만 따서 부르는 것이다.
이를 적극적으로 활용하면 소프트웨어 구조를 더욱 견고하고 유연하게 만들 수 있다.
단일 책임 원칙 (SRP: Single Responsibility Principle)
“한 클래스는 하나의 책임만 가져야 한다.”
클래스가 여러 책임을 가지게 될 경우, 하나의 변경 사항이 클래스 전체에 영향을 줄 수 있으며, 이는 유지보수에 방해가 된다.
// 단일 책임 원칙을 위반한 예시
class User {
constructor(private name: string, private email: string) {}
saveToDatabase() {
// 데이터베이스 저장
}
sendEmail(subject: string, content: string) {
// 이메일 발송
}
validate() {
// 유효성 검사
}
}
// 단일 책임 원칙을 적용한 구조
class User {
constructor(public name: string, public email: string) {}
}
class UserValidator {
validate(user: User) {
// 유효성 검사 로직
}
}
class UserRepository {
save(user: User) {
// 데이터베이스 저장 로직
}
}
class EmailService {
sendEmail(user: User, subject: string, content: string) {
// 이메일 발송 로직
}
}
개방-폐쇄 원칙 (OCP: Open-Closed Principle)
“소프트웨어 요소는 확장에는 열려 있어야 하며, 변경에는 닫혀 있어야 한다.”
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있는 구조를 지향해야 한다. 이는 추상화, 상속, 전략 패턴 등을 통해 구현할 수 있다.
// OCP를 위반한 예시
class PaymentProcessor {
process(paymentType: string, amount: number) {
if (paymentType === 'credit') {
// 신용카드 결제
} else if (paymentType === 'paypal') {
// 페이팔 결제
}
}
}
// OCP를 만족하는 설계
interface PaymentStrategy {
process(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy {
process(amount: number) {
// 신용카드 결제 처리
}
}
class PayPalPayment implements PaymentStrategy {
process(amount: number) {
// 페이팔 결제 처리
}
}
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
process(amount: number) {
this.strategy.process(amount);
}
}
리스코프 치환 원칙 (LSP: Liskov Substitution Principle)
“서브타입은 언제나 기반 타입으로 교체할 수 있어야 한다.”
상속받은 클래스가 부모 클래스의 기능을 대체할 수 없다면, 상속 구조 자체가 잘못된 설계일 가능성이 높다.
// 잘못된 상속 예시
class Rectangle {
constructor(public width: number, public height: number) {}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
area(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width;
}
setHeight(height: number) {
this.height = height;
this.width = height;
}
}
// LSP를 만족하는 인터페이스 기반 설계
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
area(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private size: number) {}
area(): number {
return this.size * this.size;
}
}
인터페이스 분리 원칙 (ISP: Interface Segregation Principle)
“클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.”
하나의 범용 인터페이스에 모든 기능을 포함하기보다는, 각 역할에 따라 인터페이스를 분리하는 것이 바람직하다.
// ISP 위반: 불필요한 메서드까지 구현해야 함
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class RobotWorker implements Worker {
work() {}
eat() { throw new Error('로봇은 먹지 않습니다'); }
sleep() { throw new Error('로봇은 자지 않습니다'); }
}
// ISP 적용: 역할 기반 인터페이스 분리
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class HumanWorker implements Workable, Eatable, Sleepable {
work() {}
eat() {}
sleep() {}
}
class RobotWorker implements Workable {
work() {}
}
의존관계 역전 원칙 (DIP: Dependency Inversion Principle)
“고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 양자는 모두 추상화에 의존해야 한다.”
구체적인 구현에 대한 의존성을 제거하고, 추상화(인터페이스)를 통해 결합도를 낮추는 것이 핵심이다.
// DIP 위반: 고수준 모듈이 직접 저수준 구현에 의존
class LightBulb {
turnOn() {}
turnOff() {}
}
class Switch {
private bulb: LightBulb;
constructor() {
this.bulb = new LightBulb();
}
operate() {
// 전구 제어
}
}
// DIP 적용: 추상화에 의존
interface Switchable {
turnOn(): void;
turnOff(): void;
}
class LightBulb implements Switchable {
turnOn() {}
turnOff() {}
}
class Fan implements Switchable {
turnOn() {}
turnOff() {}
}
class Switch {
constructor(private device: Switchable) {}
operate() {
// 장치 제어
}
}
SOLID 원칙 적용 시 기대 효과
- 유지보수성 향상: 각 구성 요소의 역할이 명확하므로, 변경의 영향 범위를 최소화할 수 있다.
- 재사용성 증가: 모듈화된 설계는 다양한 상황에 쉽게 재활용될 수 있다.
- 테스트 용이성: 단일 책임을 가지는 구성 요소는 단위 테스트 작성이 쉽다.
- 확장성 확보: 기존 로직을 변경하지 않고 새로운 기능을 추가할 수 있다.
결론
SOLID 원칙은 단순한 이론에 그치지 않는다. 프로젝트 초기에는 과도한 추상화로 인해 복잡성이 증가할 수 있으나, 경험을 쌓을수록 실용적이고 유연한 설계를 구성할 수 있게 된다. TypeScript는 인터페이스, 제네릭, 추상 클래스 등 SOLID 원칙을 적용하는 데에 적합한 기능을 갖추고 있으므로, 실제 프로젝트에서 적용해보며 설계 감각을 키워가는 것이 좋다.
SOLID는 결국 ‘좋은 소프트웨어 개발 습관’이다. 반복적으로 적용하고 개선하는 과정을 통해, 더 나은 품질의 코드를 구현할 수 있게 될 것이다.
답글 남기기