플라워로드 기술 블로그 : http://blog.flowerroad.ai
Notion Link : https://flyingcorp.notion.site/Android-Dependency-Inversion-feat-Clean-Architecture-4c4cc2c066e64217bf4c4c74ee78af55
Dependency Inversion? 의존성 역전??
혹자는 DIP(Dependency Inversion Principle)는 시스템의 유연성을 극대화 하기 위한 방법이라고 말하고 있습니다. 유연성을 극대화 하기 위해서 소스코드는 abstraction 에 의존해야하며 실제 구현체에는 의존 하지 않도록 시스템을 구성해야 합니다.
예를들어 Java의 경우 import구문은 interface나 abstraction class 선언만을 참조해야 한다는 말이죠.
그래서 이건 왜 하지?
위에서 말한것처럼 유연성을 극대화 하기 위한 방법중의 하나이기도 하며 제일 중요한 이유는 변동성이 큰 구현체에 대해서 의존성을 삭제 하는데에 있습니다.
예를 들어 A class가 B class를 import 후 사용하게 되면 A class는 B class를 의존하게 됩니다. 이때 B class를 수정하게 되면 이 영향은 A class도 받을 수 밖에 없습니다. 만약 A class는 B-1 class를 import해서 사용하고, B-1 class은 단지 abstraction class 혹은 interface이며 실제 구현체는 B class에 있을때에 B class를 수정하게 되더라도 abstraction 혹은 interface에 정의되어 있는 내용은 바뀌지 않기 때문에 A class는 다시 빌드 할 필요없이 그대로 사용할 수 있게 됩니다. 여기에서 B class는 B-1 class의 구현체이기 때문에 B-1에 정의되어 있는 기능은 꼭 구현을 해야만 합니다. 이말은 B class는 B-1 class를 의존하는 형태로 변경이 됩니다.
Factory Pattern
DIP를 위해서 여러 rule들이 있습니다. 이런 rule을 준수 하려면 변동성이 큰 구현체를 사용할때에 일반적인 방법으로 객체를 생성해서 사용하면 의존성이 발생하는것을 피할 수 없습니다.
따라서, 많은 객체 지향 언어(특히 Java)에서는 이런 의존성이 발생하는 것을 피하기 위해서 Factory pattern을 많이 사용합니다.
Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)
위의 그림에서 Application은 Service Interface를 이용해서 ConcreteImpl구현체를 사용합니다.
만약 일반적인 방법으로 Application에서 ConcreteImpl 구현체의 Instance를 생성하면 Application은 ConcreteImpl에 대한 의존성을 피할수 없습니다.
위의 그림과 같이 Application에서는 ServiceFactory interface에 정의 되어 있는 makeSvc를 호출하고 해당 interface의 구현체인 ServiceFactoryImpl에서 ConcreteImpl의 Instance를 생성한 후 Service type으로 반환을 하게 되면 Application에서는 ConcreteImpl의 구현체에 대한 Dependency가 사라지게 됩니다.
여기서 주요한 점은 system controlled flow는 그림의 아래쪽 방향, 붉은 라인의 위에서 아래쪽으로 진행 되지만 Dependency는 반대 방향인 붉은 라인의 아래쪽에서 위쪽으로 향하고 있습니다.
👉 붉은 라인 아래쪽의 ServiceFactoryImpl과 ConcreteImpl간의 Dependency는 피할수 없다. 따라서, 대부분 system에서는 ServiceFactoryImpl과 같은 역활을 하는 instance를 main에서 생성후 전역으로 사용한다.
예제 - Interface만을 이용한 간단한 DIP
이 예제에서는 단순히 interface만을 이용한 DIP(Dependency Inversion Principle)을 설명합니다. 이후에 위에 설명한 Factory Pattern을 이용한 예제도 함께 볼 예정입니다.
위의 그림과 같이 이 예제에서는 Socket라는 interface가 있고, Main Application에서는 두 종류의 Socket(LegacySocket, WebSocket)을 사용해서 서버와 interaction하는 부분을 보여줍니다.
Interface를 만들고 이를 구현하는 class 가 필수로 구현해야하는 method를 정의 합니다.
public interface Socket {
boolean open();
void close();
boolean send(byte[] data);
byte[] recv(int len);
}
이 interface를 implements 하는 class는 open, close, send, recv 네 가지의 method는 필수로 구현을 해야하기 때문에 Socket을 사용하는 입장의 class에서는 구현체 내부에 어떤 것이 구현되어 있는지에 대해서 신경쓰지 않아도 위의 네 method는 무조건 구현이 되어 있다고 생각하고 사용할 수 있습니다.
public class LegacySocket implements Socket{
public LegacySocket(){
}
public boolean open(){
return true;
}
public void close(){
}
public boolean send(byte[] data){
return true;
}
public byte[] recv(int len){
return new byte[10];
}
}
public class WebSocket implements Socket{
public WebSocket (){
}
public boolean open(){
return true;
}
public void close(){
}
public boolean send(byte[] data){
return true;
}
public byte[] recv(int len){
return new byte[10];
}
}
위의 LegacySocket, WebSocket는 Socket interface를 구현하는 구현체 이기 때문에 interface에 정의 되어 있는 네개의 method는 필수로 구현을 해야합니다.
마지막으로 위의 socket class를 사용하는 main application을 살펴 봅니다.
public class MainApplication {
public MainApplication(){
Socket legacySocket = new LegacySocket();
Socket WegSocket = new WebSocket();
}
}
MainApplication에서 두개의 Socket을 사용하고 있습니다. 주의깊게 봐야 하는 부분은 new로 socket을 생성할때에는 각각 legacy, Web 의 class를 사용해서 생성을 하지만 생성된 instance의 type은 동일하게 Socket으로 되어 있습니다.
이는 어떤 구현체를 생성하더라도 이들은 Socket interface에 정의 되어 있는 method가 필수로 구현되어 있고, 사용하는 MainApplication에서도 interface에 정의 되어 있는 method만 보고 해당 class들을 사용할 수 있습니다.
정리하면, Socket Interface에 의해서 LegacySocket, WebSocket는 제약사항이 발생하고(여기서는 의존성이 되는 거죠), 이 제약사항(의존성)은 Socket Interface에 정의 되어 있는 method를 필수로 구현을 해야합니다. Socket를 사용하는 Application에서는 LegacySocket, WebSocket의 구현 내용을 신경쓸 필요없이 Socket Interface에 있는 method만을 보고 사용할 수 있게 됩니다.
따라서, MainApplication은 LegacySocket, WebSocket를 의존하지 않고, 이 두 Socket구현체는 Socket Interface을 의존해서 전체적인 의존성이 역전되는 것을 볼 수 있습니다.
마치며...
의존성 역전은 프로젝트가 커지면 커질수록 필수사항이 되고 있습니다. 처음에는 굳이 이것을 할 필요가 있을까 라고 생각하지만 코드의 양이 늘어나고 기능, class가 늘어나다 보면 class간의 의존성이 엉망이 되어 어느 한곳을 수정하면 다른 한곳이 정상동작하지 않은 일이 발생하게 됩니다. 이를 DIP를 이용해서 사전에 어느정도는 control 할 수 있기 때문에 프로젝트의 크기에 따라서 거의 필수로 선택이 되어야 하는 상황이 발생하죠.
위의 간단한 DIP 예제만 살펴 보았는데 Factory Pattern에 대해서는 다음에 기회가 된다면 업데이트 하겠습니다.
참고
'개발 > android' 카테고리의 다른 글
[Android] Android Unit Test를 적용해보자! (Feat. mokito) (0) | 2022.02.25 |
---|