본문 바로가기

개발

SOLID Principle 알아보기 - 두번째

SOLID Principle

SOLID Principle은 객체 지향 프로그래밍에서 유지보수성, 재사용성, 확장성에 중점을 두고 설계하는 원칙이다. 오늘은 SOLID 알아보기 두번째 시간으로 LSP, ISP와 DIP에 대한 내용이다. 

 

LSP (리스코프 치환 원칙 - 명확하게 subtype관계를 유지하자)

자식 클래스(인터페이스의 구현)는 언제나 자신의 부모클래스(인터페이스)로 교체될 수 있어야 합니다.

 

예시

오리는 여서 날 수 있지만.

public class Bird{
	 public void fly(){}
}
public class Duck extends Bird{}

 

타조는 새지만 날 수 없다. 따라서, fly 메서드를 사용할 수 없으므로 LSP 원칙을 위반하고 있다.

 

public class Ostrich extends Bird{}

 

새라는 부모 클래스로는 타조와 오리의 경우에는 subType관계가 명확하지 않다.

 

LSP원칙을 따르게 바꾼 버전

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}

 

기본이 되는 인터페이스(실제 구현이 되어있든 아니든)를 사용하는 모든 사용자는 반드시 인터페이스를 따라야만한다.

만약 자식클래스가 기본이 되는 인터페이스이 사용자를 혼란스럽게 한다면, if / switch 문을 쓸 수 밖에 없을 것이다. → 클라이언트 입장에서 인터페이스를 잘 활용할 수 없다.

 

ISP (인터페이스 분리 원칙 - 범용적인 Interface보단 구체적인 Interface)

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안됩니다. 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리되어야 합니다.

 

💡 사용자가 필요 없는 기능에 의존하지 않도록 인터페이스를 작게 유지하세요.

 

SRP의 인터페이스 버전 - 하나의 범용 인터페이스보다 여러 개의 구체적인 인터페이스가 낫다.

 

하나의 큰 범용 인터페이스의 문제점

클라이언트들이 호출하지도 않는 메서드에 의존하게 되며, 이러한 메서드 중 하나가 수정될 때마다 클라이언트들도 다시 컴파일되고 다시 배포되야 한다.

 

ISP위반 설계

Service가 제공하는 메소드를 클라이언트가 사용하는 기준별로 그룹핑하면 한 클라이언트에서의 변화가 다른 쪽으로 확산되지 않는다.

 

 

인터페이스와 역할

위에서는 ISP가 SRP의 인터페이스 버전이라고 했지만, 인터페이스가 여러 역할을 갖는 경우도 있다.

‘이체’라는 인터페이스는 아래와 같이 총 5가지의 역할을 가진다.

  1. 트랜잭션을 시작한다.
  2. 상대의 계좌 존재 여부를 확인한다
  3. 고객의 계좌에 원하는 금액을 출금한다
  4. 상대의 계좌에 입금한다
  5. 트랜잭션을 마친다의 절차를 거친다

응집성은 결여 되어 있지만, 특정 비즈니스 목적에 의해 하나의 처리 절차로 묶이면서 ‘이체’라는 인터페이스를 만족시킨다. 이 때 ‘이체’ 인터페이스는 이런 역할의 묶음의 Facade가 된다.

이처럼 인터페이스와 역할과의 관계는 1:1 일수도, 1대 다일 수도 있다.

 

명심해야할 점

한번 정해진 인터페이스는 바뀌면 안된다.

 

DIP (의존 역전 원칙)

상위 레벨 모듈은 하위 레벨 세부 사항에 의존해서는 안 된다. 세부사항들은 추상화된 것에 의존해야한다.

 

예시

사용자에게 이메일로 알림을 보내는 기존 코드가 있다.

 

public class Email{
    public void sendMail(){
        // send mail 
    }
}
public class Notification{
    private Email _email;
    public Notification(){
        _email = new Email();
    }
    public void promotionalNotification(){
        _email.sendMail();
    }
}

 

여기서 이메일 말고 문자로 알림을 보내야 하는 경우 상위 클래스(Notification)가 추상화에 의존하지 않고 세부사항, 구현체(Email)와 연결되어 있기 때문에 DIP를 위반하는 것입니다.

 

그렇다면 어떻게 이 문제를 해결할 수 있을까?

 

1. 인터페이스를 만든다.

public interface Messeneger{
	void sendMessage();
}

 

2. 인터페이스를 구현하는 클래스들을 만든다.

public class Email implements Messenger{
	void sendMessage(){
		// send email
	}
}
public class SMS implements Messenaer{
	void sendMessage(){
		// send SMS
	}
}

 

3. Notification 클래스를 구현체(Email, SMS)가 아닌 추상화된 것 (Messenger)에 의존하게 한다.

public class Notification{
	private Messenger _messenger;
	public Notification(Messenger messenger){
		_messenger= messenger;
	}
	public void promotionalNotification(){
		_messeager.sendMessage();
	}
}

 

이 단계에서 Messenger를 구현한 클래스의 객체들을 injection해주게 된다. 이와 관련된 것으로 DI, IOC가 있다.

 

도대체 무엇을 뒤집는 것인가?

하위 레벨 모듈이 상위 레벨 모듈에 의존하는 것이 일반적이다.

하지만 상위, 하위 레벨 모듈 모두 추상화된 것(자바에서 인터페이스)에 의존을 하게되면서,

방향성이 상위 ← 하위 에서 상위 → 추상화된 것 ← 하위로 뒤집어지는 곳이 생긴다.

 

 

Reference


https://www.nextree.co.kr/p6960/

https://zdnet.co.kr/view/?no=00000039137043

https://www.youtube.com/watch?v=TMuno5RZNeE