객체 지향 설계(Object-Oriented Design, OOD)의 품질을 평가하고 향상시키기 위한 수많은 방법론이 존재하지만, 그중에서도 SOLID 원칙은 현대 소프트웨어 공학의 가장 근본적인 초석으로 자리 잡고 있다. SOLID는 다섯 가지 핵심 설계 원칙인 단일 책임 원칙(Single Responsibility Principle), 개방-폐쇄 원칙(Open-Closed Principle), 리스코프 치환 원칙(Liskov Substitution Principle), 인터페이스 분리 원칙(Interface Segregation Principle), 그리고 의존성 역전 원칙(Dependency Inversion Principle)의 머리글자를 따서 만든 두문자어(mnemonic acronym)이다.1 이 원칙들은 단순히 개별적인 코딩 지침의 집합을 넘어, 시간이 지나도 이해하기 쉽고(understandable), 변화에 유연하며(flexible), 유지보수가 용이한(maintainable) 소프트웨어 시스템을 구축하기 위한 핵심 철학을 담고 있다.3
이 원칙들의 체계화는 소프트웨어 장인정신(Software Craftsmanship)의 저명한 주창자인 로버트 C. 마틴(Robert C. Martin, “Uncle Bob”)에 의해 2000년 그의 논문 “Design Principles and Design Patterns”에서 이루어졌다.1 마틴은 성공적인 소프트웨어는 필연적으로 진화해야 하지만, 그 과정에서 점차 복잡성이 증가하고 결국 경직성(rigidity), 취약성(fragility), 부동성(immobility), 점착성(viscosity)과 같은 특성을 띠게 되는 ‘소프트웨어 부패(software rot)’ 현상을 목격했다.1 SOLID 원칙은 바로 이러한 소프트웨어의 고질적인 문제들을 해결하고, 시간이 지나도 건강하고 적응력 있는 시스템을 유지하기 위한 처방전으로 제시되었다. ‘SOLID’라는 약어 자체는 2004년경 마이클 페더스(Michael Feathers)에 의해 처음 만들어져 널리 알려지게 되었다.3
본 보고서는 SOLID의 다섯 가지 원칙 각각에 대한 이론적 깊이를 탐구하는 것을 목표로 한다. 각 원칙의 정의와 철학적 기반을 명확히 하고, 실제 코드 예제를 통해 원칙의 위반 사례와 이를 해결하기 위한 리팩토링 과정을 상세히 분석할 것이다. 더 나아가, 이 원칙들이 어떻게 서로 유기적으로 상호작용하여 시너지 효과를 내는지, 그리고 객체 지향 패러다임을 넘어 함수형 프로그래밍과 마이크로서비스 아키텍처와 같은 현대적인 패러다임에 어떻게 적용될 수 있는지를 고찰한다. 마지막으로는 SOLID 원칙에 대한 비판적 시각을 견지하며, 과잉 공학의 위험성을 경고하고 다른 설계 원칙과의 비교를 통해 실용적인 적용을 위한 균형점을 제시하고자 한다.
보고서의 본격적인 논의에 앞서, 독자의 이해를 돕기 위해 SOLID 원칙의 핵심 개념과 목표를 요약한 표를 아래와 같이 제시한다.
| 원칙 (Principle) | 정의 (Definition) | 핵심 목표 (Core Objective) |
|---|---|---|
| Single Responsibility | 클래스는 변경되어야 할 단 하나의 이유만을 가져야 한다. | 응집성 (Cohesion), 유지보수성 (Maintainability) |
| Open-Closed | 소프트웨어 개체는 확장에 대해 열려 있고, 수정에 대해 닫혀 있어야 한다. | 확장성 (Extensibility), 안정성 (Stability) |
| Liskov Substitution | 하위 타입은 상위 타입의 객체를 대체할 수 있어야 한다. | 신뢰성 (Reliability), 다형성 (Polymorphism) |
| Interface Segregation | 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하도록 강요되어서는 안 된다. | 결합도 감소 (Decoupling), 모듈성 (Modularity) |
| Dependency Inversion | 상위 모듈은 하위 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다. | 결합도 감소 (Decoupling), 유연성 (Flexibility), 테스트 용이성 (Testability) |
이 표는 각 원칙이 고립된 규칙이 아님을 명확히 보여준다. 오히려 이들은 소프트웨어 설계의 근본적인 두 가지 목표, 즉 응집도 증가(Increasing Cohesion)와 결합도 감소(Decreasing Coupling)를 향해 유기적으로 수렴한다. 단일 책임 원칙은 모듈 내부의 응집도를 높이는 데 직접적으로 초점을 맞추는 반면, 나머지 네 원칙(OCP, LSP, ISP, DIP)은 다양한 메커니즘을 통해 모듈 간의 결합도를 낮추는 데 집중한다. 이러한 거시적인 관점을 견지할 때, 독자는 이어지는 상세 분석을 통해 각 원칙이 어떻게 전체적인 설계 품질 향상에 기여하는지에 대한 구조적인 이해를 얻을 수 있을 것이다.
단일 책임 원칙(SRP)은 “클래스는 변경되어야 할 단 하나의 이유만을 가져야 한다(A class should have one, and only one, reason to change)”는 로버트 C. 마틴의 간결한 정의로 요약된다.1 이 원칙의 핵심은 ‘책임’의 단위를 올바르게 이해하는 데 있다. 이는 단순히 클래스가 하나의 메서드만 가져야 한다는 기계적인 해석이 아니다. 여기서 ‘변경의 이유’는 소프트웨어의 특정 기능이나 비즈니스 로직을 책임지는 단일 ‘액터(actor)’ 또는 ‘컨텍스트(context)’와 밀접하게 연관된다.7 예를 들어, 재무팀과 인사팀은 서로 다른 액터이며, 이들의 요구사항 변경은 서로 다른 이유에 해당한다. 만약 하나의 클래스가 두 팀 모두의 요구사항을 처리한다면, 그 클래스는 두 가지 변경 이유를 가지게 되어 SRP를 위반하게 된다.
따라서 SRP의 근본적인 철학은 관련 있는 것들을 한데 모으고, 관련 없는 것들은 분리함으로써 높은 응집도(High Cohesion)를 달성하는 것이다.7 응집도가 높은 클래스는 그 목적이 명확하고, 이해하기 쉬우며, 변경의 파급 효과를 해당 클래스 내부로 국소화시킬 수 있다. 이는 관심사의 분리(Separation of Concerns)라는 더 넓은 소프트웨어 공학 원리와도 깊은 관련이 있다.8
SRP를 위반한 클래스는 여러 가지 부정적인 증상을 나타내며, 이는 시스템의 건강성을 심각하게 저해한다.
다중 책임 클래스와 낮은 응집도: 가장 흔한 위반 사례는 하나의 클래스가 서로 관련 없는 여러 책임을 동시에 수행하는 것이다. 예를 들어, 사용자 인증(authentication), 사용자 프로필 관리(profile management), 그리고 활동 로깅(activity logging)을 모두 처리하는 UserAccountManager 클래스를 생각해보자.9 인증 정책의 변경, 프로필 데이터 구조의 변경, 로깅 포맷의 변경은 각각 독립적인 이유로 발생하지만, 이 모든 변경이
UserAccountManager라는 단일 클래스에 집중된다. 이는 클래스의 응집도를 현저히 떨어뜨린다.
높은 결합도와 예상치 못한 부작용: 책임이 분리되지 않은 코드는 내부적으로 서로 다른 기능들이 강하게 결합(tightly coupled)될 수밖에 없다. UserAccountManager 예제에서 로깅 로직을 수정하는 과정에서 실수로 인증 로직에 영향을 미칠 수 있다. 이처럼 하나의 변경이 전혀 예상치 못한 부분에서 버그를 유발하는 부작용(side effects)의 위험이 크게 증가한다.6
테스트 및 유지보수의 어려움: 단일 책임을 가진 클래스는 그 책임에만 집중하여 단위 테스트를 작성하기 매우 용이하다.3 반면, 여러 책임을 가진 클래스는 테스트 케이스가 복잡해지고, 모든 책임의 조합을 고려해야 하므로 테스트 커버리지를 확보하기 어렵다. 또한, 새로운 개발자가 코드를 이해하고 수정하는 데 훨씬 더 많은 시간과 노력이 소요된다.4
병합 충돌(Merge Conflicts): 팀 단위 개발 환경에서 SRP 위반은 버전 관리 시스템의 병합 충돌을 빈번하게 유발한다. 예를 들어, 한 개발자는 인증 기능을 수정하고 다른 개발자는 로깅 기능을 수정하기 위해 동일한 UserAccountManager 파일을 변경했다고 가정해보자. 이 두 개발자의 작업은 개념적으로는 완전히 독립적이지만, 물리적으로는 동일한 파일을 수정했기 때문에 병합 과정에서 충돌이 발생할 확률이 매우 높다. SRP를 준수했다면 이들은 애초에 서로 다른 파일을 수정했을 것이므로 충돌 자체가 발생하지 않았을 것이다.5
SRP를 준수하기 위한 리팩토링의 핵심은 클래스가 가진 여러 책임을 식별하고, 각 책임을 독립된 클래스로 분리하는 것이다.
다음은 고객 정보(Customer entity)와 데이터베이스 저장 로직(Persistence logic)이 혼재된 클래스의 위반 사례와 해결책이다.7
위반 전 코드 (Java):
// SRP 위반: 고객 데이터와 데이터베이스 로직이 결합됨
class Customer {
private int id;
private String name;
// 고객 관련 getter/setter...
public void saveToDatabase() {
// 데이터베이스 연결 및 저장 로직
System.out.println("Saving customer " + name + " to the database.");
}
}
위 Customer 클래스는 두 가지 변경 이유를 가진다. 첫째, 고객의 속성(예: 주소 추가)이 변경될 때. 둘째, 데이터 저장 방식(예: 데이터베이스에서 파일 시스템으로 변경)이 변경될 때. 이는 명백한 SRP 위반이다.
리팩토링 후 코드 (Java):
// SRP 준수: 고객 데이터 클래스
class Customer {
private int id;
private String name;
// 고객 관련 getter/setter...
}
// SRP 준수: 고객 저장소 클래스
class CustomerRepository {
public void save(Customer customer) {
// 데이터베이스 연결 및 저장 로직
System.out.println("Saving customer " + customer.getName() + " to the database.");
}
}
리팩토링을 통해 Customer 클래스는 순수하게 고객 데이터를 표현하는 책임만을 가지게 되었고, 데이터 영속성과 관련된 책임은 CustomerRepository 클래스로 완전히 분리되었다. 이제 고객 데이터 모델의 변경은 Customer 클래스에만 영향을 미치고, 저장 방식의 변경은 CustomerRepository 클래스에만 영향을 미친다. 이로써 각 클래스는 단 하나의 변경 이유만을 가지게 되어 SRP를 준수하게 된다.
이처럼 SRP를 충실히 따르는 것은 단순히 코드를 나누는 행위를 넘어, 시스템의 변경 가능성을 예측하고 그 영향을 효과적으로 격리하는 중요한 설계 활동이다. SRP는 다른 모든 SOLID 원칙을 적용하기 위한 견고한 기반을 제공한다. 한 클래스가 여러 책임을 지게 되면, 그 클래스를 수정하지 않고 확장하는 것(OCP)은 거의 불가능해진다. 예를 들어, 보고서 생성 시 데이터 조회, 포맷팅, 출력을 모두 담당하는 ReportGenerator 클래스가 있다면, 새로운 출력 형식(예: CSV)을 추가하기 위해서는 기존 클래스의 코드를 직접 수정해야만 한다. 이는 OCP를 정면으로 위반하는 행위이다. 반면, SRP를 준수하여 책임을 DataFetcher, ReportFormatter, ReportPrinter 등으로 분리했다면, 새로운 CSVFormatter 클래스를 추가하고 이를 ReportPrinter에 주입하는 것만으로 기능 확장이 가능해진다.10 이처럼 SRP의 준수는 OCP 준수를 위한 강력한 전제 조건이 되며, 변경의 영향을 국소화시켜 시스템 전체의 안정성을 높이는 첫걸음이 된다.
개방-폐쇄 원칙(OCP)은 1988년 버트런드 마이어(Bertrand Meyer)에 의해 처음 제시된 개념으로, “소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하고(open for extension), 수정에 대해서는 닫혀 있어야 한다(closed for modification)”고 정의된다.3 이 원칙의 핵심은 시스템에 새로운 기능을 추가하거나 기존 기능의 동작을 변경해야 할 때, 이미 안정적으로 동작하고 있는 기존 코드를 수정하는 것을 최소화하거나 완전히 배제해야 한다는 것이다. 기존 코드를 수정하는 행위는 언제나 새로운 버그를 유발할 잠재적 위험을 내포하기 때문이다.4 대신, 시스템은 새로운 기능을 쉽게 ‘추가’할 수 있는 유연한 구조를 가져야 한다. OCP는 변화에 안정적인 시스템을 구축하여 장기적인 유지보수 비용을 절감하는 것을 궁극적인 목표로 한다.3
OCP를 달성하는 가장 핵심적인 기술적 메커니즘은 추상화(Abstraction)와 다형성(Polymorphism)이다.
OCP 위반의 가장 전형적인 예는 조건 분기문(예: if/else if 또는 switch)을 사용하여 타입에 따라 다른 로직을 처리하는 코드이다. 여러 종류의 도형 면적을 계산하는 AreaCalculator 클래스를 예로 들어보자.2
위반 전 코드 (Java):
class Rectangle {
public double width;
public double height;
}
class Circle {
public double radius;
}
// OCP 위반: 새로운 도형이 추가될 때마다 이 클래스를 수정해야 함
class AreaCalculator {
public double calculateArea(Object shapes) {
double totalArea = 0;
for (Object shape : shapes) {
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
totalArea += rect.width * rect.height;
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
totalArea += Math.PI * circle.radius * circle.radius;
}
// 새로운 도형 'Triangle'을 추가하려면 여기에 'else if'를 추가해야 함
}
return totalArea;
}
}
위 AreaCalculator 클래스는 새로운 도형 타입(예: 삼각형)을 지원해야 할 때마다 calculateArea 메서드 내부에 else if 블록을 추가하여 코드를 직접 수정해야 한다. 이는 “수정에 대해 닫혀 있어야 한다”는 OCP의 원칙을 명백히 위반하는 것이다.2 이러한 코드는 확장에 취약하며, 수정이 반복될수록 복잡해지고 버그 발생 가능성이 높아진다.
OCP를 준수하도록 코드를 리팩토링하기 위해, 변화하는 부분(도형별 면적 계산 로직)과 변하지 않는 부분(모든 도형의 면적을 합산하는 로직)을 분리하고, 이 둘을 추상화를 통해 연결한다. 이는 디자인 패턴 중 전략 패턴(Strategy Pattern)의 아이디어와 일치한다.
리팩토링 후 코드 (Java):
// 1. 추상화(인터페이스) 정의
interface Shape {
double area();
}
// 2. 구체적인 구현(전략) 클래스들
class Rectangle implements Shape {
public double width;
public double height;
@Override
public double area() {
return width * height;
}
}
class Circle implements Shape {
public double radius;
@Override
public double area() {
return Math.PI * radius * radius;
}
}
// OCP 준수: 새로운 도형이 추가되어도 이 클래스는 수정되지 않음
class AreaCalculator {
public double calculateArea(Shape shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.area(); // 다형성을 통해 실제 면적 계산 로직이 호출됨
}
return totalArea;
}
}
// 3. 새로운 기능 확장
class Triangle implements Shape {
public double base;
public double height;
@Override
public double area() {
return 0.5 * base * height;
}
}
리팩토링 후 AreaCalculator는 더 이상 구체적인 도형 클래스(Rectangle, Circle)를 알지 못한다. 대신 Shape라는 인터페이스에만 의존한다. 이제 새로운 Triangle 클래스를 추가하더라도, Triangle이 Shape 인터페이스를 구현하기만 하면 AreaCalculator의 코드는 단 한 줄도 수정할 필요 없이 새로운 도형을 처리할 수 있다.7 이것이 바로 ‘확장에 열려 있고, 수정에 닫혀 있는’ OCP의 이상적인 모습이다.
OCP는 단순히 if문을 사용하지 말라는 표면적인 지침이 아니다. 그 본질은 변화의 축(Axis of Change)을 예측하고, 그 축을 중심으로 추상화하라는 깊이 있는 설계 요구사항이다. 시스템의 모든 부분을 추상화하는 것은 비현실적이며 과잉 공학(over-engineering)으로 이어질 수 있다.11 따라서 숙련된 설계자는 시스템에서 어떤 부분이 변할 가능성이 높은지, 그리고 어떤 부분이 안정적으로 유지될 것인지를 식별해야 한다. 예를 들어, ‘결제 수단’은 새로운 방식(예: 간편 결제)이 계속 추가될 가능성이 높은 변화의 축이다. 반면, ‘상품 가격 계산’ 로직은 상대적으로 안정적일 수 있다. OCP의 진정한 실천은 이처럼 변화가 예상되는 ‘결제 수단’을 중심으로 PaymentStrategy와 같은 인터페이스를 설계하고, 이를 통해 시스템의 유연성을 확보하는 것이다. 결국 OCP를 효과적으로 적용하는 능력은 단순히 기술적인 코딩 스킬을 넘어, 비즈니스 요구사항의 변화 방향을 예측하고 시스템의 진화 지점을 파악하는 설계적 통찰력에 달려있다.
리스코프 치환 원칙(LSP)은 1987년 바버라 리스코프(Barbara Liskov)에 의해 처음 소개되었으며, 객체 지향 프로그래밍에서 올바른 상속 관계를 정의하는 핵심적인 기준을 제시한다.13 원칙의 공식적인 정의는 다음과 같다: “S 타입의 각 객체 o1에 대해 T 타입의 객체 o2가 있고, T 타입으로 정의된 모든 프로그램 P에서 o2를 o1으로 치환해도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다”.13 이를 더 실용적인 언어로 풀어쓰면, “하위 클래스는 언제나 자신의 상위 클래스를 대체할 수 있어야 한다”는 의미이다.3
이 원칙의 핵심은 상속이 단순히 코드 재사용을 위한 구조적(syntactic) 관계가 아니라, 행위의 일관성을 보장하는 행위적(behavioral) 관계여야 함을 강조하는 데 있다. 즉, 하위 클래스는 상위 클래스가 가진 모든 특성과 행위를 온전히 계승하고 확장해야 하며, 상위 클래스의 행위를 임의로 변경하거나 축소해서는 안 된다. 클라이언트 코드는 상위 클래스 타입의 객체를 사용하면서 특정 행위를 기대하는데, 하위 클래스 객체로 교체되었을 때 그 기대가 깨진다면 LSP를 위반한 것이다. 이러한 관점에서 LSP는 OCP를 지키기 위한 중요한 전제 조건이 된다. 하위 클래스로의 치환이 프로그램의 정확성을 깨뜨린다면, 그것은 올바른 ‘확장’이라고 볼 수 없기 때문이다.16
하위 클래스가 상위 클래스의 행위적 계약을 위반하는 경우는 다음과 같은 구체적인 조건들로 나타난다.
LSP를 설명하는 가장 고전적이고 유명한 예제는 ‘정사각형/직사각형 문제’이다. 수학적으로 정사각형은 너비와 높이가 같은 직사각형의 특수한 경우이므로, 객체 지향 모델링에서 Square 클래스를 Rectangle 클래스의 하위 클래스로 만드는 것은 직관적으로 타당해 보인다. 그러나 이는 LSP를 위반하는 대표적인 사례이다.18
Rectangle 클래스는 다음과 같은 암묵적인 불변식을 가진다: “너비(width)와 높이(height)는 서로 독립적으로 변경될 수 있다.” 클라이언트 코드는 이 불변식을 신뢰하고 Rectangle 객체를 사용한다.
LSP 위반 코드 (Java):
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 정사각형의 불변식(너비=높이)을 유지하기 위해 높이도 변경
}
@Override
public void setHeight(int height) {
this.width = height; // 너비도 함께 변경
this.height = height;
}
}
// 클라이언트 코드
public class LspTest {
public static void testArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// 클라이언트는 r의 너비가 5, 높이가 4이므로 넓이가 20일 것이라고 기대한다.
if (r.getArea()!= 20) {
throw new AssertionError("Area calculation failed!");
}
}
public static void main(String args) {
Rectangle rect = new Rectangle();
testArea(rect); // 성공
Rectangle square = new Square();
testArea(square); // AssertionError 발생!
}
}
Square 클래스는 정사각형의 속성(너비와 높이가 항상 같음)을 유지하기 위해 setWidth나 setHeight가 호출될 때 너비와 높이를 모두 같은 값으로 설정한다. 이로 인해 Rectangle의 불변식(“너비와 높이는 독립적이다”)이 깨진다. testArea 메서드는 Rectangle 타입의 객체를 인자로 받으므로, Square 객체도 전달받을 수 있어야 한다. 하지만 Square 객체를 전달하면 setHeight(4)가 호출되는 순간 너비도 4로 변경되어 최종 넓이는 16이 된다. 이는 클라이언트의 기대(20)를 위반하며, 프로그램의 정확성을 깨뜨린다.17 따라서 Square는 Rectangle을 안전하게 대체할 수 없으며, LSP를 위반한다.
이 문제를 해결하기 위해서는 행위적으로 ‘IS-A’ 관계가 성립하지 않을 때 상속을 사용하지 말아야 한다는 교훈을 얻어야 한다. 대신, 공통의 추상화에 의존하는 구조로 재설계해야 한다.
리팩토링 후 코드 (Java):
// 공통 추상화
interface Shape {
int getArea();
}
// Rectangle과 Square는 이제 형제 클래스
class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
이제 Rectangle과 Square는 상속 관계가 아닌, 공통의 Shape 인터페이스를 구현하는 독립적인 클래스가 되었다.19 이 설계에서는 더 이상 LSP 위반 문제가 발생하지 않으며, 각 클래스는 자신의 불변식을 온전히 유지할 수 있다.
LSP는 본질적으로 계약에 의한 설계(Design by Contract) 개념을 상속 관계에 적용한 것이다. 상위 클래스의 public 메서드들은 클라이언트와 맺는 일종의 ‘서비스 계약’을 정의한다. 이 계약에는 메서드 시그니처와 같은 명시적 조건뿐만 아니라, 사전조건, 사후조건, 불변식과 같은 암묵적인 행위 조건도 포함된다.3 클라이언트 코드는 이 계약을 신뢰하고 상위 클래스 타입의 객체를 사용한다. 상속은 이 계약을 재사용하고 확장하는 메커니즘이다. LSP는 하위 클래스가 이 계약을 확장할 수는 있지만(예: 더 강력한 사후조건을 제공), 계약의 근본적인 내용을 위반해서는 안 된다고 규정한다. 정사각형/직사각형 문제의 핵심은 Square가 Rectangle의 ‘너비와 높이는 독립적’이라는 암묵적 계약을 파기했다는 점이다. 이처럼 LSP 위반은 단순한 버그를 넘어 시스템의 신뢰 기반을 무너뜨리는 계약 파기 행위이며, 다형성을 통한 유연한 확장의 근간을 흔들어 결국 OCP의 실패로 직결된다.
인터페이스 분리 원칙(ISP)은 “클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강요되어서는 안 된다(No client should be forced to depend on methods it does not use)”고 명시한다.3 이 원칙은 단일 책임 원칙(SRP)이 클래스의 책임을 다루는 것과 유사하게, 인터페이스의 책임을 다룬다.23 즉, 하나의 거대하고 범용적인 인터페이스보다는, 클라이언트의 특정 요구에 맞춰 잘게 분리된 여러 개의 구체적인 인터페이스가 더 낫다는 것이다. 이러한 작고 응집도 높은 인터페이스를 역할 인터페이스(Role Interfaces)라고도 부른다.22 ISP의 목표는 시스템의 결합도를 낮추어 변경에 용이하고, 재사용성이 높으며, 이해하기 쉬운 코드를 만드는 것이다.
ISP를 위반하는 주된 원인은 ‘비대한 인터페이스(Fat Interface)’ 또는 ‘인터페이스 오염(interface pollution)’이다.25 이는 하나의 인터페이스가 서로 다른 클라이언트들이 필요로 하는 관련 없는 여러 메서드들을 한꺼번에 포함하고 있는 상황을 말한다. 비대한 인터페이스는 다음과 같은 심각한 문제들을 야기한다.
UnsupportedOperationException과 같은 예외를 던지는 무의미한 코드를 양산하게 된다. 이러한 구현은 클라이언트에게 혼란을 줄 뿐만 아니라, 리스코프 치환 원칙(LSP)을 위반할 소지가 매우 크다.27인쇄, 스캔, 팩스 기능을 모두 제공하는 복합기를 모델링하는 상황을 가정해보자. ISP를 고려하지 않으면 다음과 같은 비대한 인터페이스를 설계하기 쉽다.
위반 전 코드 (Java):
// ISP 위반: 비대한 인터페이스
interface IMultiFunctionDevice {
void print(Document d);
void scan(Document d);
void fax(Document d);
}
// 복합기는 모든 기능을 구현하므로 문제가 없어 보임
class MultiFunctionPrinter implements IMultiFunctionDevice {
@Override
public void print(Document d) { /* 인쇄 로직 */ }
@Override
public void scan(Document d) { /* 스캔 로직 */ }
@Override
public void fax(Document d) { /* 팩스 로직 */ }
}
// 하지만 단순 프린터는 스캔과 팩스 기능이 없음
class SimplePrinter implements IMultiFunctionDevice {
@Override
public void print(Document d) { /* 인쇄 로직 */ }
@Override
public void scan(Document d) {
// 불필요한 구현 강요
throw new UnsupportedOperationException("Scan not supported.");
}
@Override
public void fax(Document d) {
// 불필요한 구현 강요
throw new UnsupportedOperationException("Fax not supported.");
}
}
SimplePrinter 클래스는 오직 print 기능만 필요함에도 불구하고, IMultiFunctionDevice 인터페이스 때문에 불필요한 scan과 fax 메서드를 구현하도록 강요받는다.26 이는 ISP의 명백한 위반이며, LSP 또한 위반할 가능성을 내포한다.
이 문제를 해결하기 위해서는 비대한 인터페이스를 클라이언트의 역할에 따라 더 작고 응집도 높은 인터페이스로 분리해야 한다.
리팩토링 후 코드 (Java):
// ISP 준수: 역할 인터페이스로 분리
interface IPrinter {
void print(Document d);
}
interface IScanner {
void scan(Document d);
}
interface IFax {
void fax(Document d);
}
// SimplePrinter는 필요한 인터페이스만 구현
class SimplePrinter implements IPrinter {
@Override
public void print(Document d) { /* 인쇄 로직 */ }
}
// 복합기는 여러 역할 인터페이스를 조합하여 구현
class MultiFunctionPrinter implements IPrinter, IScanner, IFax {
@Override
public void print(Document d) { /* 인쇄 로직 */ }
@Override
public void scan(Document d) { /* 스캔 로직 */ }
@Override
public void fax(Document d) { /* 팩스 로직 */ }
}
// 클라이언트는 자신이 필요한 최소한의 인터페이스에만 의존
class PrintClient {
private final IPrinter printer;
public PrintClient(IPrinter printer) {
this.printer = printer;
}
public void doPrint(Document d) {
printer.print(d);
}
}
리팩토링을 통해 IMultiFunctionDevice는 IPrinter, IScanner, IFax라는 세 개의 역할 인터페이스로 분리되었다.22 이제 SimplePrinter는 IPrinter 인터페이스만 구현하면 되므로 더 이상 불필요한 메서드를 강요받지 않는다. MultiFunctionPrinter는 필요한 모든 인터페이스를 구현하여 자신의 다기능성을 표현한다. 또한 PrintClient와 같은 클라이언트는 이제 거대한 IMultiFunctionDevice가 아닌, 자신의 역할에 꼭 맞는 IPrinter 인터페이스에만 의존하게 되어 결합도가 현저히 낮아졌다.
ISP와 DIP(의존성 역전 원칙)는 함께 작용하여 유연하고 확장 가능한 플러그인 아키텍처(Plugin Architecture)를 가능하게 하는 핵심적인 메커니즘을 형성한다. DIP는 상위 모듈이 하위 모듈의 구체적인 구현이 아닌 ‘추상화’에 의존해야 한다고 요구하며, 이 ‘추상화’는 보통 인터페이스의 형태로 나타난다. 만약 이 인터페이스가 ISP를 위반한 ‘비대한 인터페이스’라면, 상위 모듈은 자신이 사용하지도 않는 수많은 메서드에 불필요하게 의존하게 되어 DIP가 목표로 하는 ‘느슨한 결합’의 효과가 반감된다. ISP는 바로 이 인터페이스를 역할에 따라 잘게 쪼개어, 상위 모듈이 ‘자신에게 꼭 필요한 최소한의 추상화’에만 의존하도록 보장한다. 이렇게 잘 분리된 인터페이스(ISP 준수)를 매개로 의존성을 역전(DIP 준수)시키면, 하위 모듈(구현체)은 마치 플러그인처럼 시스템에 쉽게 교체되거나 추가될 수 있다. 상위 모듈은 오직 자신이 필요로 하는 ‘역할’에만 관심을 가지기 때문이다. 결론적으로, ISP는 DIP가 최대의 효과를 발휘하기 위한 ‘인터페이스의 품질’을 보장하는 필수적인 파트너 원칙이라 할 수 있다.
의존성 역전 원칙(DIP)은 견고하고 유연한 소프트웨어 아키텍처를 구축하기 위한 핵심적인 지침으로, 다음과 같은 두 가지 명제로 구성된다.3
여기서 ‘상위 수준 모듈’은 비즈니스 정책이나 핵심 로직과 같이 시스템의 본질적인 부분을 다루는 코드를 의미하며, ‘하위 수준 모듈’은 데이터베이스 접근, 네트워크 통신, 파일 시스템 I/O와 같이 이러한 정책을 구현하기 위한 구체적인 기술이나 메커니즘을 다루는 코드를 의미한다.
전통적인 절차적 프로그래밍에서는 제어 흐름과 의존성의 방향이 일치한다. 즉, 상위 수준 모듈이 하위 수준 모듈을 직접 호출하고 의존하는 구조를 가진다. DIP는 이러한 전통적인 의존성의 흐름을 ‘역전(inversion)’시켜야 한다고 주장한다. 의존성을 역전시킨다는 것은, 상위 수준 모듈이 자신이 필요로 하는 서비스에 대한 ‘추상 인터페이스’를 정의하고 소유하며, 하위 수준 모듈은 이 인터페이스를 구현하도록 만드는 것을 의미한다. 그 결과, 의존성의 방향이 ‘하위 수준 모듈 –» 추상 인터페이스 ← 상위 수준 모듈’의 형태로 바뀌게 된다. 즉, 구체적인 세부 사항(하위 모듈)이 시스템의 핵심 정책(상위 모듈이 정의한 추상화)에 의존하게 되는 것이다. 이 원칙의 궁극적인 목표는 모듈 간의 결합도를 낮추어 시스템을 더 유연하고, 테스트하기 쉬우며, 유지보수하기 쉽게 만드는 것이다.3
DIP를 위반하는 코드는 상위 수준 모듈이 하위 수준 모듈의 구체적인 클래스를 직접 참조하고 생성하는 형태를 띤다. 예를 들어, 사용자에게 알림을 보내는 NotificationService를 생각해보자.
위반 전 코드 (Java):
// 하위 수준 모듈: 구체적인 이메일 발송 클래스
class EmailSender {
public void sendEmail(String message) {
System.out.println("Sending email: " + message);
}
}
// 상위 수준 모듈: 비즈니스 로직
// DIP 위반: 상위 모듈이 하위 모듈의 구체 클래스(EmailSender)에 직접 의존
class NotificationService {
private final EmailSender emailSender;
public NotificationService() {
this.emailSender = new EmailSender(); // 직접 생성 및 의존
}
public void sendNotification(String message) {
emailSender.sendEmail(message);
}
}
위 코드에서 NotificationService(상위 수준 모듈)는 EmailSender(하위 수준 모듈)라는 구체적인 클래스에 직접 의존하고 있다. 만약 알림 방식을 이메일에서 SMS로 변경해야 한다면, NotificationService 클래스의 내부 코드를 SmsSender를 사용하도록 직접 수정해야만 한다. 이는 OCP를 위반하는 경직된 설계이다.
DIP를 적용하기 위해서는 상위 수준 모듈과 하위 수준 모듈 사이에 추상화 계층(인터페이스)을 도입하고, 의존성의 방향을 역전시켜야 한다.
리팩토링 후 코드 (Java):
// 1. 추상화 정의 (상위 모듈이 소유)
interface IMessageSender {
void sendMessage(String message);
}
// 2. 하위 수준 모듈들은 추상화를 구현
class EmailSender implements IMessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
class SmsSender implements IMessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
// 3. 상위 수준 모듈은 추상화에만 의존
// DIP 준수: 상위 모듈이 추상 인터페이스(IMessageSender)에만 의존
class NotificationService {
private final IMessageSender messageSender;
// 의존성 주입(Dependency Injection)을 통해 구체적인 구현을 외부에서 받음
public NotificationService(IMessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendNotification(String message) {
messageSender.sendMessage(message);
}
}
// 4. 조립(Composition Root)
public class Main {
public static void main(String args) {
// 이메일로 알림을 보내고 싶을 때
IMessageSender emailSender = new EmailSender();
NotificationService emailNotification = new NotificationService(emailSender);
emailNotification.sendNotification("Hello via Email!");
// SMS로 알림을 보내고 싶을 때
IMessageSender smsSender = new SmsSender();
NotificationService smsNotification = new NotificationService(smsSender);
smsNotification.sendNotification("Hello via SMS!");
}
}
리팩토링 후 NotificationService는 더 이상 EmailSender나 SmsSender와 같은 구체적인 클래스를 알지 못한다. 대신 IMessageSender라는 추상 인터페이스에만 의존한다. 실제 사용할 구현체는 생성자를 통해 외부에서 ‘주입(inject)’받는다. 이제 NotificationService의 코드를 전혀 변경하지 않고도 알림 방식을 자유롭게 교체할 수 있게 되었다. 이는 DIP를 통해 OCP를 달성하는 전형적인 예시이다.31
DIP, 의존성 주입(Dependency Injection, DI), 제어의 역전(Inversion of Control, IoC)은 종종 혼용되지만, 실제로는 서로 다른 수준의 개념이면서 강력한 인과 관계를 형성한다. 이들의 관계를 명확히 이해하는 것은 매우 중요하다.
이들의 인과 관계는 다음과 같이 정리할 수 있다: DIP(원칙)라는 목표를 달성하기 위해 DI(패턴)라는 수단을 사용하게 되고, DI를 시스템 전반에 걸쳐 체계적으로 적용하면(특히 Spring과 같은 프레임워크를 통해) 이는 곧 IoC(패러다임)를 구현하는 것이 된다. 이 세 가지 개념을 혼동하는 것은 설계의 목표(What), 수단(How), 그리고 결과(Why)를 뒤섞는 것과 같다.
SOLID의 다섯 가지 원칙은 독립적으로 존재하는 규칙의 나열이 아니라, 하나의 유기적인 시스템처럼 서로를 보완하고 강화하며 작동한다. 이 원칙들이 개별적으로 적용될 때도 코드 품질 향상에 기여하지만, 함께 조화롭게 적용될 때 그 효과는 비선형적으로 증폭되어 견고하고 유연한 객체 지향 설계라는 공동의 목표를 달성한다.3 이들의 시너지는 마치 복잡한 화학 반응처럼, 개별 요소의 단순한 합을 넘어서는 전체적인 설계 품질의 고도화를 이끌어낸다.36
각 원칙 간의 상호 보완 관계는 다음과 같이 분석될 수 있다.
instanceof와 같은 타입 검사 코드를 추가해야 하며, 이는 결국 기존 코드의 수정을 유발하여 OCP를 깨뜨린다. 따라서 LSP가 보장되어야만, 클라이언트 코드의 수정 없이 새로운 하위 타입을 안전하게 추가하는 진정한 의미의 ‘확장’이 가능해진다.10종합적으로 볼 때, SRP와 ISP는 주로 모듈 내부의 품질, 즉 높은 응집도를 확보하는 데 중점을 둔다. 반면, OCP, LSP, DIP는 모듈 외부, 즉 모듈 간의 관계에서 낮은 결합도를 유지하는 데 중점을 둔다. 이 다섯 원칙이 함께 어우러질 때, 시스템은 내부적으로는 견고하고 외부적으로는 유연한, 이상적인 아키텍처에 가까워진다.
이러한 관점에서 SOLID 원칙 전체는 변경에 대한 체계적인 관리 전략(Strategy for Managing Change)으로 요약될 수 있다. 소프트웨어의 본질은 끊임없는 ‘변화’에 있으며, 성공적인 소프트웨어는 반드시 진화의 과정을 거친다.1 모든 변경은 비용과 위험을 수반하므로, 좋은 설계의 목표는 이 비용과 위험을 최소화하는 것이다. SOLID는 이 목표를 달성하기 위한 구체적인 전술을 제공한다.
결론적으로, SOLID는 미래에 발생할 가능성이 있는 변경을 예측하고, 그 변경이 시스템 전체에 미치는 영향을 최소화하도록 코드를 구조화하는 체계적이고 종합적인 방법론이라 할 수 있다.
SOLID 원칙은 비록 객체 지향 프로그래밍(OOP)의 맥락에서 탄생하고 발전했지만, 그 근본 철학은 특정 프로그래밍 패러다임이나 기술에 얽매이지 않는다. 복잡성을 관리하고, 응집도를 높이며, 결합도를 낮추려는 이 원칙들의 핵심 아이디어는 함수형 프로그래밍(FP)이나 마이크로서비스 아키텍처(MSA)와 같은 현대적인 패러다임에서도 여전히 유효하며, 해당 패러다임의 언어와 도구에 맞게 재해석되어 적용될 수 있다.38
함수형 프로그래밍에는 클래스, 상속, 인터페이스와 같은 OOP의 핵심 구성 요소가 없거나 다른 형태로 존재한다. 그럼에도 불구하고 SOLID의 각 원칙은 다음과 같이 의미 있는 대응 관계를 찾을 수 있다.38
SOLID 원칙은 개별 마이크로서비스의 내부 설계뿐만 아니라, 여러 서비스로 구성된 전체 아키텍처의 구조를 설계하는 데에도 중요한 지침을 제공한다.39
이처럼 SOLID 원칙이 OOP를 넘어 다양한 패러다임에 걸쳐 적용될 수 있다는 사실은, 이 원칙들이 클래스나 상속과 같은 특정 기술에 대한 지침이 아니라, 복잡성을 관리하고 변화에 대응하기 위한 보편적이고 추상적인 소프트웨어 설계 원리임을 강력하게 시사한다. SRP는 ‘응집도’, OCP는 ‘확장성’, LSP는 ‘치환 가능성’, ISP는 ‘최소 의존’, DIP는 ‘추상화 의존’이라는 더 근본적인 개념으로 환원될 수 있다. 이는 SOLID가 OOP라는 ‘구현체’를 통해 표현된 설계의 ‘인터페이스’와 같다는 것을 의미한다. 따라서 앞으로 새로운 프로그래밍 패러다임이나 아키텍처 스타일이 등장하더라도, SOLID의 근본 철학은 그에 맞는 형태로 변용되어 계속해서 유효할 가능성이 높으며, 이는 SOLID 원칙의 시대를 초월하는 가치를 증명한다.
SOLID 원칙은 의심할 여지 없이 훌륭한 소프트웨어 설계를 위한 강력한 지침이지만, 모든 상황에 무비판적으로 적용해야 하는 은탄환(silver bullet)은 아니다. 원칙의 근본적인 의도를 이해하지 못하고 기계적으로 적용할 경우, 오히려 코드의 복잡성을 증가시키고 개발 생산성을 저해하는 부작용을 낳을 수 있다. 따라서 성숙한 개발자는 원칙을 맹목적으로 추종하는 것을 넘어, 비판적인 시각을 견지하고 실용적인 균형점을 찾아야 한다.
SOLID 원칙의 남용은 여러 가지 문제점을 야기할 수 있으며, 그중 가장 대표적인 것이 과잉 공학(Over-engineering)의 위험이다.
SOLID 원칙의 실용적 가치를 더 잘 이해하기 위해, 또 다른 유명한 객체 지향 설계 원칙 모음인 GRASP(General Responsibility Assignment Software Patterns)와 비교해 볼 수 있다.
SOLID 원칙을 성공적으로 활용하기 위해서는 원칙 자체를 아는 것을 넘어, 그것을 언제, 어떻게, 그리고 어느 수준까지 적용할지 판단하는 지혜가 필요하다.
결국 SOLID 원칙의 성공적인 적용을 위한 가장 중요한 상위 원칙은 경제적 사고(Economic Thinking)이다. 모든 설계 결정은 일종의 비용-편익 분석(Cost-Benefit Analysis)에 기반해야 한다. SOLID 원칙을 적용하는 행위(예: 인터페이스 추출, 클래스 분리)는 즉각적인 ‘비용’(개발 시간 증가, 코드 라인 수 증가, 인지적 복잡성)을 발생시킨다. 이 비용을 지불함으로써 얻고자 하는 ‘편익’은 미래에 발생할 ‘유지보수 비용 감소’와 ‘확장성 향상’이다. 만약 프로젝트의 수명이 짧아 ‘미래’가 없거나 11, 시스템의 해당 부분이 변경될 가능성이 거의 없어 ‘유지보수’나 ‘확장’의 필요성이 낮다면, 이 거래는 명백한 손해다. 즉, 비용이 편익보다 크다. 반대로, 수명이 길고 변화가 잦은 대규모 시스템에서는 초기에 지불한 추상화의 비용이 미래에 발생할 수 있는 엄청난 수정 비용을 절감해주므로 매우 이득인 투자가 된다.53 따라서 “SOLID를 적용해야 하는가?”라는 질문은 궁극적으로 “이 추상화에 대한 투자가 미래에 더 큰 이익으로 돌아올 것인가?”라는 경제적 질문으로 치환되어야 한다. 이것이 바로 원칙을 맹목적으로 따르는 것과 현명하게 활용하는 것을 가르는 진정한 기준점이다.
SOLID 원칙은 지난 수십 년간 객체 지향 설계의 핵심적인 지침으로서 그 가치를 증명해왔다. 이 원칙들은 특정 프로그래밍 언어나 기술, 시대적 유행에 얽매이지 않는, 잘 구조화된 소프트웨어를 구축하기 위한 근본적인 사고의 틀을 제공한다. 코드의 응집도를 높이고 결합도를 낮춤으로써, 변화에 유연하고 테스트와 유지보수가 용이한 시스템을 만드는 데 여전히 핵심적인 역할을 수행하고 있다.1 현대의 함수형 프로그래밍이나 마이크로서비스 아키텍처와 같은 새로운 패러다임에서도 그 기본 철학이 유효하다는 사실은 SOLID 원칙의 시대를 초월하는 보편성을 입증한다.
그러나 원칙 그 자체보다 중요한 것은 그것을 사용하는 개발자의 지혜와 통찰력이다. 진정한 소프트웨어 전문가는 원칙의 정의를 암기하는 것을 넘어, 각 원칙이 추구하는 근본적인 의도(예: 변경의 격리, 계약의 준수)를 깊이 이해해야 한다. 그리고 이를 바탕으로 현재 당면한 프로젝트의 구체적인 맥락 속에서 언제, 어떻게, 그리고 어느 수준까지 원칙을 적용할지, 혹은 적용하지 않을지를 판단하는 균형 감각을 갖추어야 한다. SOLID는 모든 문제에 대한 정답을 알려주는 상세한 지도가 아니라, 더 나은 설계를 향한 여정에서 올바른 방향을 제시하는 나침반과 같다.
소프트웨어 공학의 세계는 앞으로도 계속해서 변화하고 진화할 것이다. 새로운 언어, 프레임워크, 아키텍처 스타일이 끊임없이 등장할 것이다. 하지만 복잡성을 제어하고, 변화에 효과적으로 대응하며, 지속 가능한 시스템을 만들어야 한다는 근본적인 과제는 변하지 않을 것이다. 따라서 SOLID가 담고 있는 추상화, 모듈화, 책임 분리의 철학은 앞으로도 오랫동안 소프트웨어 장인(Software Craftsman)들이 끊임없이 연마하고 추구해야 할 중요한 가치로 남을 것이다.54
| SOLID Principles in Object Oriented Design – BMC Software | Blogs, 8월 15, 2025에 액세스, https://www.bmc.com/blogs/solid-design-principles/ |
| How the SOLID Principles Guide Object-Oriented Design: Examples of Violations and Their Consequences in Large-Scale Applications | by Youngjun Kim | Medium, 8월 15, 2025에 액세스, https://medium.com/@youngjun_kim/how-the-solid-principles-guide-object-oriented-design-examples-of-violations-and-their-8bacac9dda23 |
| The Interrelation of SOLID Principles: How They Work Together | by Pushpraj Priy | Medium, 8월 15, 2025에 액세스, https://medium.com/@thepushp2/the-interrelation-of-solid-principles-how-they-work-together-72eca222db94 |
| When Using Solid Principles May Not Be Appropriate | Baeldung on Computer Science, 8월 15, 2025에 액세스, https://www.baeldung.com/cs/solid-principles-avoid |
| Challenging the Gospel of SOLID Principles | by Jeremykhoo - Medium, 8월 15, 2025에 액세스, https://medium.com/@jeremykhoois/solid-principles-sucks-b5935b1235d7 |
| Square, Rectangle and the Liskov Substitution Principle | by Alexandre Dutertre - Medium, 8월 15, 2025에 액세스, https://medium.com/@alex24dutertre/square-rectangle-and-the-liskov-substitution-principle-ee1eb8433106 |
| Interface Segregation Principle. and how to interpret it | by Vadim Samokhin - Medium, 8월 15, 2025에 액세스, https://medium.com/@wrong.about/interface-segregation-principle-bdf3f94f1d11 |
| Interface Segregation Principle (ISP) with TypeScript examples | by Oleksandr Khomyakov, 8월 15, 2025에 액세스, https://medium.com/@khomyakov/interface-segregation-principle-isp-with-typescript-examples-8f82f538a9b2 |
| Dependency Injection, Dependency Inversion, IoC | SSENSE-TECH - Medium, 8월 15, 2025에 액세스, https://medium.com/ssense-tech/dependency-injection-vs-dependency-inversion-vs-inversion-of-control-lets-set-the-record-straight-5dc818dc32d1 |
| Dependency Inversion vs Dependency Injection | by BuketSenturk - Medium, 8월 15, 2025에 액세스, https://medium.com/@buketsenturk/dependency-inversion-vs-dependency-injection-4a2dea766e6b |
| Synergistic Oxygen Reduction Catalysis by Fe Nanoparticles and Atomic Fe-Nx Sites in MOF-Derived Carbon Nanotubes | ACS Sustainable Chemistry & Engineering, 8월 15, 2025에 액세스, https://pubs.acs.org/doi/10.1021/acssuschemeng.5c05924 |
| Photon-Primed Organic Electrosynthesis Enabled by Oxidation of Photon-Induced Intermediates | Journal of the American Chemical Society, 8월 15, 2025에 액세스, https://pubs.acs.org/doi/10.1021/jacs.5c07822 |
| SOLID Microservices Design. Applying SOLID principles to… | by Adrien Nortain - Zenika, 8월 15, 2025에 액세스, https://medium.zenika.com/solid-microservices-design-dc6a4044a050 |
| SOLID principles in Functional Programming | by Maciej Kocik - Medium, 8월 15, 2025에 액세스, https://medium.com/@mkocik/solid-principles-in-functional-programming-b9b83aeddf80 |
| Applying SOLID principles to services | by David Van Couvering - Medium, 8월 15, 2025에 액세스, https://david-vancouvering.medium.com/applying-solid-principles-to-services-e56ef2382a26 |
| Microservices: Designing Effective Microservices by following SOLID Design Principles | by Saurabh Gupta | Medium, 8월 15, 2025에 액세스, https://medium.com/@saurabh.engg.it/microservices-designing-effective-microservices-by-following-solid-design-principles-a995c3a033a0 |
| SOLID and GRASP: What are these?. Object-oriented design is a paradigm… | by Harpreet Singh Kalsi | I am a dummy, enlighten me! | Medium, 8월 15, 2025에 액세스, https://medium.com/i-am-a-dummy-enlighten-me/solid-and-grasp-what-are-these-07a8acc03670 |