솔리드 원칙은 소프트웨어 디자인의 다섯 가지 기본 원칙
이 원칙들은 소프트웨어를 더 견고하고 유연하며 유지보수가 쉽도록 만드는 데 도움이 되는 지침을 제공
▶ 단일 책임 원칙 (Single Responsibility Principle - SRP)
클래스는 단 하나의 책임만 가져야 함
이유와 장점
- 유지보수성 향상
- 각 클래스나 모듈이 하나의 책임만을 갖도록 설계하면 코드의 유지보수가 쉬워짐
- 특정 기능을 변경해야 할 때 해당 기능에 대한 코드만 이해하고 수정할 수 있음
- 코드의 가독성 증가
- 각 클래스 또는 모듈이 특정 책임에 집중하면 코드가 명확하고 가독성이 향상
- 다른 개발자들도 해당 코드를 이해하기 쉽게 되어 협업이 용이
- 재사용성 증가
- 단일 책임을 갖는 모듈은 다른 부분에서 쉽게 재사용
- 특정 기능이 변경되어도 해당 모듈만 수정하면 되므로 다른 부분에 영향을 미치지 않음
- 테스트 용이성
- 작은 단위의 책임을 가진 모듈들은 테스트하기 쉬움
- 한 모듈의 기능을 테스트할 때 다른 책임에 영향을 받지 않아 테스트 코드 작성이 간단해짐
예시
나쁜 예시
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
validateCredentials() {
// 사용자 자격 증명을 검증하는 코드
}
saveToDatabase() {
// 데이터베이스에 사용자 정보를 저장하는 코드
}
}
User 클래스는 사용자 객체를 표현할 뿐만 아니라, 자격 증명을 검증하고 데이터베이스에 저장하는 두 가지 역할을 가지고 있음
좋은 예시
class UserValidator {
validateCredentials(user) {
// 사용자 자격 증명을 검증하는 코드
}
}
class UserManager {
saveToDatabase(user) {
// 데이터베이스에 사용자 정보를 저장하는 코드
}
}
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
User 클래스는 단순히 사용자 정보를 가지고 있으며, UserValidator 클래스는 사용자 자격 증명을 검증하고, UserManager 클래스는 사용자 정보를 데이터베이스에 저장하는 역할을 각각 담당함
이렇게 하면 각 클래스가 하나의 책임만을 갖게 되어 코드의 유지보수와 확장성이 향상됨
▶ 개방/폐쇄 원칙 (Open/Closed Principle - OCP)
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 함
이유와 장점
- 확장성 향상
- 개방/폐쇄 원칙을 지키면 기존의 코드를 수정하지 않고도 새로운 기능이나 모듈을 추가할 수 있음
- 코드를 확장할 때는 기존 코드를 수정하지 않고 새로운 코드를 추가함으로써 시스템을 확장할 수 있음
- 유지보수성 향상
- 기능의 변경이나 확장이 필요한 경우 해당 기능을 제공하는 클래스를 수정하지 않아도 됨
- 새로운 기능을 추가하거나 기존 기능을 변경할 때 다른 부분에 영향을 덜 주면서 작업할 수 있음
- 코드 안정성
- 기존 코드를 변경하지 않고 새로운 코드를 추가함으로써 기존 기능이나 모듈에 문제가 발생할 확률을 낮춤
따라서 시스템의 안정성을 유지할 수 있습니다.
- 기존 코드를 변경하지 않고 새로운 코드를 추가함으로써 기존 기능이나 모듈에 문제가 발생할 확률을 낮춤
예시
나쁜예시
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
}
class AreaCalculator {
calculateArea(rectangle) {
return rectangle.width * rectangle.height;
}
}
class Circle {
constructor(radius) {
this.radius = radius;
}
}
// 새로운 도형이 추가될 때마다 AreaCalculator를 수정해야 함
AreaCalculator 클래스는 Rectangle과 Circle의 넓이를 계산하도록 구현되어 있습니다. 하지만, 새로운 도형이 추가될 때마다 AreaCalculator를 수정해야 하는 문제가 있음
좋은 예시
class Shape {
area() {
throw new Error("Subclasses must implement the area method");
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return 3.14 * this.radius ** 2;
}
}
// 새로운 도형이 추가되어도 AreaCalculator를 수정할 필요가 없음
개선된 예시에서 AreaCalculator 클래스 대신 Shape 클래스가 도입됨
Shape 클래스는 넓이를 계산하는 area 메서드를 정의하고, 이를 구현하는 각 도형 클래스는 Shape 클래스를 상속받음
이렇게 하면 새로운 도형이 추가될 때 AreaCalculator를 수정할 필요 없이, 간단히 새로운 도형 클래스를 추가하면 됨
▶ 리스코프 치환 원칙 (Liskov Substitution Principle - LSP)
자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 함
즉, 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스를 사용해도 기능이 변하지 않아야 합니다.
이유와 장점
- 다형성 강화
- 리스코프 치환 원칙을 지키면 서브타입은 언제나 기본 타입으로 대체될 수 있음
- 이는 코드에서 다형성을 강화하고, 서브타입을 사용하는 부분에서 기대한 동작을 항상 보장
- 모듈성 향상
- 서브타입을 기본 타입으로 대체할 수 있기 때문에, 코드의 모듈성이 향상
- 새로운 서브타입이 추가되거나 변경되더라도 해당 서브타입을 사용하는 부분에서는 영향을 최소화할 수 있음
- 코드 예측 가능성
- 리스코프 치환 원칙을 지키면 서브타입을 사용하는 코드에서 예측 가능성이 증가
- 즉, 코드를 이해하고 유지보수하기 쉬워짐
예시
나쁜 예시
class Bird {
fly() {
console.log("날다");
}
}
class Ostrich extends Bird {
// 날지 못함
}
Ostrich 클래스가 Bird 클래스를 상속하고 있습니다. 그러나 Ostrich 클래스에서는 fly 메서드를 구현하지 않아 날지 못하는 새를 나타냅니다. 이는 리스코프 치환 원칙을 위반하는 것
좋은 예시
class Bird {
fly() {
// 날다
}
}
class Sparrow extends Bird {
// 날다
}
class Ostrich extends Bird {
// 날지 못해도 구현
}
Ostrich 클래스도 fly 메서드를 구현하게 되었음. 이렇게 함으로써 Ostrich 클래스를 Bird 클래스의 서브타입으로서 자유롭게 사용할 수 있게 되며, 리스코프 치환 원칙을 준수하게 됩니다
▶ 인터페이스 분리 원칙 (Interface Segregation Principle - ISP)
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않도록 하며, 한 클래스는 자신이 사용하지 않는 인터페이스에 의존해서는 안 됨. 즉, 큰 덩어리의 인터페이스보다는 작은 여러 개의 인터페이스가 낫다는 말
이유와 장점
- 클라이언트 코드에 불필요한 의존성 제거
- 클라이언트 코드가 자신이 사용하지 않는 메서드에 의존하지 않도록 하는 것은 인터페이스 분리 원칙의 핵심
이를 통해 클라이언트 코드는 필요한 인터페이스에만 의존하게 되어 불필요한 의존성이 제거됩니다.
- 클라이언트 코드가 자신이 사용하지 않는 메서드에 의존하지 않도록 하는 것은 인터페이스 분리 원칙의 핵심
- 클래스의 응집성 향상
- 클래스가 자신과 무관한 메서드를 구현하거나 인터페이스를 포함할 필요가 없기 때문에 클래스의 응집성(cohesion)이 향상
- 각 클래스는 자신의 주요 기능과 관련된 메서드만을 포함하게 되어 코드가 더 명확해짐
- 클래스의 재사용성 증가
- 인터페이스 분리를 통해 작은 규모의 인터페이스로 나누면, 이 인터페이스를 구현하는 클래스는 필요한 기능만 구현하게 되므로 다른 클래스에서도 쉽게 재사용
예시
나쁜 예시
class Worker {
work() {
// 일하다
}
eat() {
// 식사하다
}
}
// 사용자는 일할 때만 필요한데, 일하지 않는 동안에도 eat() 메서드에 의존
const user = new Worker();
user.work();
Worker 클래스는 work와 eat 두 가지 메서드를 포함하고 있음
하지만 사용자는 일할 때만 work 메서드가 필요하므로, eat 메서드에는 의존하지 않아야 함
좋은 예시
class Workable {
work() {
// 일하다
}
}
class Eatable {
eat() {
// 식사하다
}
}
class Worker {
constructor(workable) {
this.workable = workable;
}
work() {
this.workable.work();
}
}
// 사용자는 일할 때만 필요한 workable에만 의존
const workableUser = new Worker(new Workable());
workableUser.work();
Worker 클래스는 Workable 인터페이스에만 의존하도록 되어 있음
사용자는 일할 때만 필요한 Workable 객체를 주입받아 사용하며, Eatable에는 의존하지 않음
이렇게 함으로써 인터페이스 분리 원칙을 지킴
▶ 의존성 역전 원칙 (Dependency Inversion Principle - DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 함
즉, 추상화된 것은 구체적인 것에 의존하면 안 되며, 구체적인 것이 추상화된 것에 의존해야 함
이유와 장점
- 결합도 감소
- 의존성 역전 원칙을 지키면 고수준 모듈은 저수준 모듈에 의존하지 않고, 양쪽 모두 추상화에 의존하게 됨
이렇게 함으로써 모듈 간의 결합도가 감소하고, 한 모듈을 변경할 때 다른 모듈에 미치는 영향이 줄어듬
- 의존성 역전 원칙을 지키면 고수준 모듈은 저수준 모듈에 의존하지 않고, 양쪽 모두 추상화에 의존하게 됨
- 유연성 향상
- 의존성 역전을 통해 추상화를 통해 상위 수준 모듈이 하위 수준 모듈에 대한 세부 구현을 몰라도 되므로, 새로운 구현을 쉽게 추가하거나 기존 구현을 변경할 수 있음
- 테스트 용이성
- 의존성을 주입하는 방식을 사용하면 모듈의 의존성을 쉽게 대체
- 유닛 테스트 작성 시에 모의 객체(Mock Objects) 등을 이용하여 의존성을 주입하여 테스트하기 용이하게 만듬
예시
나쁜 예시
class LightBulb {
turnOn() {
console.log("전구를 켭니다.");
}
turnOff() {
console.log("전구를 끕니다.");
}
}
class Switch {
constructor() {
this.bulb = new LightBulb();
}
operate() {
if (this.isOn) {
this.bulb.turnOff();
this.isOn = false;
} else {
this.bulb.turnOn();
this.isOn = true;
}
}
}
const switchButton = new Switch();
switchButton.operate(); // 전구를 켭니다.
Switch 클래스가 LightBulb 클래스에 직접 의존하고 있음
좋은 예시
class Switchable {
turnOn() {
throw new Error("구현되지 않은 메서드: turnOn");
}
turnOff() {
throw new Error("구현되지 않은 메서드: turnOff");
}
}
class LightBulb extends Switchable {
turnOn() {
console.log("전구를 켭니다.");
}
turnOff() {
console.log("전구를 끕니다.");
}
}
class Switch {
constructor(device) {
this.device = device;
}
operate() {
if (this.isOn) {
this.device.turnOff();
this.isOn = false;
} else {
this.device.turnOn();
this.isOn = true;
}
}
}
const bulb = new LightBulb();
const switchButton = new Switch(bulb);
switchButton.operate(); // 전구를 켭니다.
Switch 클래스는 Switchable 인터페이스에 의존하고 있음
Switchable 인터페이스를 구현한 클래스(예: LightBulb)를 주입받아 사용하므로, Switch 클래스는 구체적인 구현이 아니라 추상적인 인터페이스에 의존함. 이렇게 하면 코드의 확장성과 유연성이 향상됨
솔리드 원칙을 따르면 코드의 유지보수성, 재사용성, 테스트 용이성 등이 향상되어 좀 더 견고하고 효율적인 소프트웨어를 개발할 수 있음
'CS > 프로그래밍' 카테고리의 다른 글
디자인 패턴 - 싱글톤 패턴 (3) | 2024.02.28 |
---|---|
[CS Study] 디자인 패턴 (0) | 2024.02.04 |
고차 컴포넌트 (HOC, High Order Component) (0) | 2024.01.20 |
CSS vs CSS-in-CSS vs CSS-in-JS (1) | 2024.01.13 |
CSS in JS (1) | 2024.01.13 |