S - 단일 책임 원칙 (Single Responsibility Principle, SRP)
“클래스는 단 하나의 책임만 가져야 한다.”는 원칙
- 한 클래스는 하나의 기능만을 수행해야 하며, 이 기능을 변경해야 할 이유도 하나여야 한다.
- 클래스의 변경 이유가 하나뿐이므로, 유지보수성이 향상된다.
- 클래스가 여러 가지 책임을 가지게 되면, 하나의 책임이 변경되었을 때 다른 책임도 영향을 받을 수 있다. 이는 코드 변경 시 예기치 않은 오류를 발생시킬 수 있고, 유지보수를 어렵게 만든다.
- 예제 코드
class Plus{
int plus(int a, int b){
return a + b;
}
int minus(int a, int b){
return a - b;
}
}
class Plus{
int plus(int a, int b){
return a + b;
}
}
Plus
클래스는 더하기 뿐만 아니라 빼기 기능도 가지고 있어 SRP를 어긴다.Plus
클래스는 더하기만 수행하여 SRP를 지킨다.O - 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
- "소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다"는 원칙
- 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 한다.
- 코드의 유지보수성과 확장성을 높여준다.
- 기존 코드의 변경을 최소화하면서 기능을 확장할 수 있기 때문에, 기존 기능에 영향을 미치지 않고 새로운 기능을 추가할 수 있다.
- 예제 코드
class A{
void print(){
System.out.print("A");
}
}
class Print{
void print(A a){
a.print();
}
}
class B{
void print(){
System.out.print("B");
}
}
interface I{
void print();
}
class A implements I{
void print(){
System.out.print("A");
}
}
class Print{
void print(I a){
a.print();
}
}
class B implements I{
void print(){
System.out.print("B");
}
}
B
를 추가하면 Print
클래스를 수정해야해 OCP를 어긴다.I
를 활용해 B
를 추가해도 기존 코드를 수정하지 않아도 되어 OCP를 지킨다.L - 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- "자식 클래스는 언제나 부모 클래스로 교체할 수 있어야 한다"는 원칙
- 자식 클래스가 부모 클래스를 대신해서 사용될 수 있어야 하며, 부모 클래스의 기능을 잘못 변경해서는 안 된다.
- 이 원칙을 어기면, 코드에서 예기치 않은 동작을 할 수 있으며, 시스템의 예측 가능성이 낮아진다.
- 자식 클래스가 부모 클래스의 계약을 어기지 않도록 하여, 다형성을 안전하게 사용할 수 있게 만든다.
- 예제 코드 1
abstract class Bird {
abstract void fly();
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("펭귄은 날 수 없음");
}
}
public class Main{
public static void main(String[] args) {
Bird penguin = new Penguin();
penguin.fly();
}
}
abstract class Bird{}
interface Fliable{
void fly();
}
class Penguin extends Bird{}
Penguin
은 날 수 없지만 Bird
를 상속받아 fly()
메서드를 강제로 구현해서 예외로 처리했다.main
에서 fly()
를 실행했지만 예상치 않은 예외가 발생할 수 있어 LSP를 어긴다.fly()
메서드를 인터페이스로 빼내서 정의한다.- 예제 코드 2
class A{
void print(){
System.out.print("A");
}
}
class B extends A{
void print(int b){
System.out.print("B" + b);
}
}
public class Main{
public static void main(String[] args) {
A b = new B();
b.print(1);
}
}
B
클래스에서 print()
를 오버라이딩하지 않고 오버로딩하여 새로운 메서드를 추가했다.A
클래스에는 print(int b)
가 존재하지 않아 실행이 안된다.A
클래스의 print()
메서드가 B
클래스에서 호출되지 않기 때문에 일관성 없는 동작이 발생해서 LSP를 어긴다.- 예제 코드 3
class A a{
void hi(){
System.out.print("A hi");
}
}
class B extends A{
@Override
void hi(){
System.out.print("B bye");
}
}
hi()
메서드는 “hi"
를 출력하도록 구현했다.hi()
메서드는 “bye”
를 출력하는 의도와는 다른 출력을 해서 LSP를 어긴다.I - 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- "하나의 커다란 인터페이스보다는 여러 개의 작은 인터페이스가 더 좋다"는 원칙
- 사용하지 않는 메서드를 포함하는 큰 인터페이스를 피하고, 클라이언트가 자신에게 필요한 메서드만을 제공하는 작은 인터페이스를 사용해야 한다.
- 유지보수성을 높이고, 응집도를 강화할 수 있다.
- 불필요한 메서드를 구현하지 않게 하여, 클라이언트가 필요하지 않은 메서드를 구현하지 않도록 할 수 있다.
- 예제 코드
interface Movable{
void run();
void fly();
}
interface Runable{
void run();
}
interface Flyable{
void fly();
}
Movable
은 fly()
메서드와 run()
메서드 두 가지 기능을 담고 있어 ISP를 위반한다.Runable
과 Flyable
로 분리해 ISP를 지켰다.D - 의존 역전 원칙 (Dependency Inversion Principle, DIP)
- "고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다"는 원칙
- 구체적인 구현이 아니라 인터페이스나 추상 클래스에 의존해야 한다는 것이다.
- 의존 관계를 역전시켜, 의존성을 쉽게 교체하거나 테스트할 수 있게 한다.
- 구체적인 클래스에 의존하지 않고 인터페이스나 추상 클래스에 의존함으로써, 시스템의 유연성과 확장성을 높인다.
- 예제 코드
class A{
void print(){
System.out.print("A");
}
}
class B{
void print(){
System.out.print("B");
}
}
class High{
A a;
High(A a){
this.a = a;
}
void print(){
a.print();
}
}
interface I{
void print();
}
class A{
void print(){
System.out.print("A");
}
}
class B{
void print(){
System.out.print("B");
}
}
class High{
I i;
High(I i){
this.i = i;
}
void print(){
i.print();
}
}
High
클래스가 구체적인 A
클래스에 의존하여 DIP를 어긴다.I
를 구현하여 High
클래스가 I
에 의존하게 만들어 DIP를 지키게 수정되었다.Share article