1.SOLID 원칙이란?
객체지향 설계의 5대 원칙.
유지보수와 확장성 재사용성을 높이기위한 개발 지침.
1. S ( Single Responsibility Principle ) 단일 책임 원칙.
하나의 클래스는 하나의 책임만을 가져야한다.
-> 하나의 클래스가 여러 책임을 가진다면 변경 이유가 많아지고 클래스가 복잡해진다.
ex) userService = 로그인 / 회원가입 / 메일전송등 유저관련 모든 로직 처리 X
mailService, loginService등 으로 분리 O
2. O ( Open-Closed Principle ) 개방 폐쇄 원칙.
확장에는 열려있어야 하고, 변경에는 닫혀 있어야 한다.
-> 새로운 기능을 추가 시 기존 코드는 되도록 건드리지 않는다.
-> 다형성, 인터페이스 기반 설계가 핵심.
ex) 결제 방식이 카드 결제, 카카오페이가 있는 상황에서 네이버 페이가 추가되어도 기존 코드 수정X
3. L ( Liskov Substitution Principle ) 리스코프 치환 원칙
자식 클래스는 언제나 부모 타입으로 교체해도 문제 없어야한다.
-> 상속을 하되, 부모의 기능을 망가트려서는 안 된다.
ex) 부모가 속도를 올리는 기능을 담당할때, 자식이 속도를 내리는 기능을 사용한다면 규칙 위반
4. I ( Interface Segregation Principle ) 인터페이스 분리 원칙
하나의 인터페이스 보다 , 여러개의 작은 인터페이스가 낫다.
-> 클라이언트는 자신이 사용하지 않는 메서드에 의존해선 안 된다.
ex) Animal 인터페이스에 walk(),fly(),swim() 이 있을경우 걷기만 가능한 동물도 fly와 swim 강제 구현.
-> 기능별로 인터페이스를 쪼개야한다.
5. D ( Dependency Inversion Principle ) 의존 역전의 원칙
구체가 아니라 추상에 의존해야 한다.
-> 상위 모듈이 하위 모듈의 직접 구현에 의존하면 변경에 취약해진다.
-> 인터페이스 기반으로 느슨한 결합을 유지해야 한다.
ex) OrderService가 KakaoPayService를 직접 생성해선 안되고 인터페이스에 의존하여 실제 구현은 외부에서 주입해야한다.
한줄 정리
SOLID 원칙은 객체지향 시스템을 더 유연하고 유지보수하기 쉽게 만드는 5가지 지침으로, 단일 책임, 확장 용이성, 올바른 상속, 인터페이스 분리, 추상 의존을 통해 결합도를 낮추고 응집도를 높이는 원칙.
2. 해당 원칙들이 지켜지지 않을 경우
1. S 단일 책임 원칙 미적용
-> 하나의 클래스가 여러개의 책임을 가지게 되면 어디를 고쳐야 할지 파악이 어려워진다.
-> 하나를 수정했다가 다른 기능에서 예상치 못한 오류가 발생할수 있다.
-> 신규 개발자가 코드를 이해하는데 시간이 많이 소요된다.
2. O 개방 폐쇄 원칙 미적용
-> 기능을 추가하기 위해서 기존 코드를 계속 수정해야한다.
-> 배포할 때마다 기존 기능까지 리스크가 증가
-> 빠르게 변화하는 요구사항을 따라가기 어렵다.
3. L 리스코프 치환 원칙 미적용
-> 부모타입으로 믿고 사용했는데 자식이 기대와 다르게 동작하는 경우가 생긴다.
-> 다형성이 무너진다.
-> 협업중 버그가 생길 확률이 올라간다.
4. I 인터페이스 분리 원칙 미적용
-> 사용하지 않는 메서드를 강제로 구현해야 하기때문에 코드가 지저분해진다,
-> 변경시 영향받지 않아도 되는 클래스들까지 영향받는 경우가 생긴다.
5 D 의존 역전 원칙 미적용.
-> 상위 로직에서 하위 구현체를 직접 생성하게되면 변경시 상위 모듈까지 수정해야한다.
-> 유닛 테스트가 사실상 불가능하다.
-> 모듈 교체/ 확장시 전체 시스템에 영향을 준다.
즉, 결과적으로 실무에서 해당 원칙들이 지켜지지 않는다면 이러한 문제들이 발생한다.
1. 신규기능의 추가가 느려진다.
2. 작은 수정이 전체 장애로 번질수 있다.
3. 테스트 자동화가 불가능하다.
4. 신규 개발자가 코드에 적응이 매우 어렵다.
3. 해당 원칙들이 제공하는 최적화와 효율성
1. S 단일 책임 원칙 - 변경 비용 최소화와 유지보수 속도 최적화
-> 변경 이유가 1개로 제한되기 때문에 수정 범위가 작아진다.
-> 한 기능의 수정이 다른 기능에 영향을 줄 위험이 낮아진다.
-> 코드 이해속도가 빨라져 전반적인 개발 속도가 증가한다.
-> 버그 탐지 속도를 감소시킨다.
-> 디벼깅 시간을 단축시킨다.
-> 코드 리뷰 시간을 단축시킨다.
2. O 개방 폐쇄 원칙 - 확장 비용 절감 ,안정성 증가.
-> 기존 코드를 거의 건드리지 않고 새로운 기능을 추가할수 있다.
-> 배포할때 기존 기능이 깨질 가능성이 감소한다,
-> 코드 변경량이 감소한다.
-> 회귀 테스트 필요량이 줄어든다.
-> 신규 요구사항에 대한 대응이 빨라진다.
3. L 리스코프 치환 원칙 - 상속 . 다형성 최적화
-> 부모타입 기반 코드가 예측이 가능해진다.
-> 특정 구현체에 종속적으로 돌아가는 코드가 줄어든다.
-> 다형성을 제대로 활용이 가능하다.
-> 재사용성이 증가한다.
-> 런타임 버그가 감소한다.
-> 테스트 자동화 가능성이 증가한다.
4. I 인터페이스 분리 원칙 - 컴파일 . 빌드 시간 및 변경 영향 최소화
-> 거대 인터페이스가 없어 작은 모듈단위로 변경이 가능하다.
-> 불필요한 의존성이 사라져서 컴파일 시간이 단축된다.
-> 빌드 속도가 향상된다.
-> 영향범위가 최소화된다.
-> 결합도는 낮아지고 응집도는 올라가 유지보수 비용이 절감된다.
5. D 의존 역전 원칙 - 테스트. 배포 효율 극대화 / 결합도 최적화.
-> 코드가 추상에 의존하므로 구현체 교체가 쉬워진다.
-> 의존성 주입과 함께 사용하면 테스트 환경 구축이 매우 쉬워진다.
-> 테스트 비용이 급감한다.,
-> 배포 리스크가 감소한다.
-> 하위 모듈 교체 비용이 감소한다.
-> 확장성이 극대화된다.
4. 만약에 해당 질문을 면접관이 한다면 그 의도는?
1. 면접자의 객체지향 설계 역량 파악.
2. 코드 품질에 대한 인식.
3. 실제 실무에서 적용 시킬 능력.
4. 문제 해결 능력.
등을 확인하려는 의도로 생각해볼수 있다.
5. 개념 조사 도중 추가 적인 궁금즘.
1. 단일 책임 원칙 - 책임의 경계를 어떻게 판단하는가?
-> 변경 이유의 일관성으로 판단
-> 하나의 클래스가 **오직 하나의 변경 이유(change reason)**만 가진다면 책임이 하나
EX)
로그인 기능 + 이메일 전송 기능 →
- 로그인 정책 변경
- 이메일 폼 변경
두 개의 변경 이유
→ SRP 위반 → 분리 대상
회원 가입 서비스 →
- 회원 생성
- 쿠폰 발급
- 포인트 적립
다른 도메인의 규칙 → 각각 다른 책임.
2. 개방 폐쇄 원칙 - 확장이 가능하게 설계한다면 복잡도가 증가하는데 이를 통제할 방법은?
-> 확장이 자주 일어나는 부분만 추상화하고 고정된 규칙은 일부러 단순하게 유지한다.
-> 추상화는 “필요할 때만” 도입
-> 변화가 집중되는 지점을 정확히 식별해 최소 비용으로 확장성을 확보하는 것이 중요하다.
3. 리스코프 치환 원칙 - 상속이 아니라 구성이 더 적절한 상황은 어떻게 판단하는가?
-> 자식이 부모의 메서드를 재정의하려고 할 때 동작 의미가 바뀌는 경우
-> 부모의 기능 중 일부만 필요하거나, 오히려 불필요한 기능이 있을 때
-> 부모의 동작을 오버라이드해야만 원하는 기능이 되는 경우
-> 행동(Behavior)은 같아야 하는데, 구현 방식만 다른 경우
-> 런타임 동안 객체 행동을 변경해야 하는 경우
-> 도메인 모델링에서 IS-A 관계가 불안정할 때
-> 즉 상속은 ‘행동이 완전히 일치하는 경우’에만 적합하고,
그 외 대부분의 유연성이 필요한 상황에서는 구성이 추상화 유지와 재사용성 측면에서 더 안전한 선택
4. 인터페이스 분리 원칙 - 인터페이스를 분리시키는데 적정 수준은 어떻게 판단하는가?
-> 사용자(클라이언트)별 기능 필요 여부가 다르면 분리한다.
ex )
interface Worker {
void work();
void eat();
}
- RobotWorker → work()만 필요
- HumanWorker → work(), eat() 필요
RobotWorker는 eat()에 불필요하게 의존
-> 기능 변경의 흐름이 다르면 분리한다.
ex)
interface Payment {
pay();
refund();
cancelRecurring();
}
- pay()는 자주 바뀜
- refund()는 거의 고정
- cancelRecurring()은 정기 결제에서만 사용
즉, **변경 이유(Change reason)**가 서로 다르다.
-> 인터페이스 분리 후 기능 집합이 자연스러우면 적정 수준이다.
-> 인터페이스에 ‘맥락이 다른 기능’이 섞여 있다면 분리한다.
-> 구현체가 인터페이스 구현을 위해 “빈 메서드(의미 없는 구현)”을 만든다면 분리 대상이다.
5. 의존 역전 원칙 - 추상이 구체보다 높은 정책을 표현한다는게 무슨뜻인가?
“정책(Policy) = 시스템이 지켜야 하는 상위 수준의 규칙·흐름·의미”
“구체(Concrete) = 그 규칙을 수행하는 실제 구현 방법”
시스템의 중요한 정책은 “인터페이스(추상)”에서 정의되어야 하고,
“구체 클래스”는 그 정책을 따르는 도구일 뿐
ex)
interface PaymentService {
pay();
}
class CardPayment implements PaymentService { ... }
class KakaoPay implements PaymentService { ... }
class NaverPay implements PaymentService { ... }
- 정책은 인터페이스가 결정하고,
- 어떻게 결제할지는 구현체가 결정한다
추상이 정책을 표현하고, 구현은 그 정책을 따르는 구조
6. SOLID 원칙 끼리 충돌이 일어날 경우 우선순위는 어떻게 판단하는가? (EX) 책임을 분리할경우 확장성이 떨어지는경우 (S와 O 원칙 충돌.) )
“시스템 안정성 → 변경 가능성 → 복잡도 → 개발 비용” 순으로 판단
동작의 일관성(LSP·DIP)을 최우선으로 하고,
변경 가능성(SRP·OCP)을 다음으로 고려하며,
복잡도(ISP)는 그 다음에 조절
실패하지 않는 구조 → 변경에 강한 구조 → 단순한 구조 순으로 우선순위
7.SOLID 원칙을 과하게 적용할 경우 일어날수 있는 문제점은 무엇인가?( 무조건적으로 지켜야 하는가?)
-> 불필요한 추상화가 과도하게 사용된다. = >인터페이스, 추상 클래스가 끝도 없이 생성되고 모든 클래스가 인터페이스를 한 개씩 가져버리는 상황 발생.
-> 지나친 분리로 인해 관리 비용 증가 => SRP, ISP, OCP를 과하게 적용하면 클래스와 인터페이스 수가 무한히 늘어남.
-> 구조가 복잡해져 오히려 변경에 취약해진다. => 조그만 기능 추가하려고 인터페이스 2개 추가등
-> 실용성이 사라지고 이론 우선 설계가 된다. => 현실적인 요구사항보다 SOLID 지키는 행위 자체가 목적이 되어
실무에서는 절대 쓰이지 않는 복잡한 구조가 만들어짐
=> 결제가 하나뿐인데 PaymentService 인터페이스부터 만들고,
몰라도 되는 복잡한 디자인 패턴을 도입하는 것처럼 ‘미리 추상화’하는 것은 대표적 과적용
즉, SOLID는 절대적으로 지켜야 하는 규칙이 아니라,
‘변경에 강하고 안정적인 구조’를 만들기 위한 실용적 도구.
필요한 만큼만 선택적으로 적용하는 것이 가장 좋은 설계 방식
'Daily Dev Q&A' 카테고리의 다른 글
| 스프링 프레임 워크 (0) | 2025.12.22 |
|---|---|
| 트랜잭션 (0) | 2025.12.21 |
| 람다표현식 (0) | 2025.12.19 |
| 자바의 접근 제어자(Access Modifier) (0) | 2025.12.09 |
| Collections Framework (0) | 2025.12.09 |