OCP(Open-Closed-Principal) : 개방 폐쇄 원칙 관련한 글들을 읽던 중 괜찮은 글을 봐서 퍼왔습니다.
흔히 확장성에 기반해 번거롭지만 Interface 를 만들고 구현체를 만들어 사용하는데요 이와 같은 의견을 주제로 다룬 글입니다.
인터넷에서 자료를 검색하다보면 클래스를 작성할 때 무조건적으로 인터페이스 구현을 하고 있는 것을 심심찮게 볼 수 있습니다. 예를 들어 인터페이스 이름이 "user"라면 실제 구현되는 서비스 클래스 이름은 "userImpl"가 되는 것이죠. 깃허브도 아닌 인터넷 블로그에 있는 코드들은 대부분 간단한 로직이 대부분이라 처음 스프링을 접할 때 대체 왜 쓸데없이 클래스가 많아지고 코드만 복잡해지게 의미 없는 인터페이스를 사용하는걸까 하는 의문을 많이 가졌었습니다.
스프링을 공부하면서 얻은 결론부터 말하자면 스프링(특히 DI)에서 인터페이스 사용은 당연하고 필수적인 것이 맞지만, 개방 폐쇄 원칙(OCP)에 기반한 전략 패턴을 미리 설계한 것이 아닌 그저 습관적인 사용이라면 전혀 무의미하다는 것입니다. 이는 토비의 스프링을 비롯한 여러 레퍼런스 자료들을 종합한 제 판단입니다.
먼저 인터페이스 사용이 권장되는 이유는 개방 폐쇄의 원칙(OCP)에 기반한 전략 패턴을 사용하기 위함입니다. 개방 폐쇄의 원칙이란 확장에는 열려있고 변화에는 닫혀있다는 의미인데, 이미 만들어진 서비스를 커스터마이징해본 분들은 쉽게 이해할 수 있을 것 같습니다.
예를 들어 컨트롤러에서 서비스 클래스를 하나 DI받은 뒤 사용한다고 가정해보겠습니다. 서비스 클래스를 스프링에서 제공하는 @AutoWired 어노테이션을 사용해 빈(Bean)으로 만들어줬다면 스프링은 해당 타입에 맞는 Bean 객체를 찾아서 컨트롤러 클래스에 의존주입해줍니다.
서비스 클래스가 인터페이스를 구현하고 있다면 코드 상에서 컨트롤러는 인터페이스만을 바라보고 있습니다. 실제 구현 클래스는 런타임 시점까지 알지 못하는 상태가 되는 것이죠.
컨트롤러 입장에서는 서비스 클래스의 비즈니스 로직에 대해서는 관심이 없습니다. 그냥 인터페이스에 있는 메소드를 이용해 로직을 구성할 뿐이기 때문에 서비스 클래스의 내부 로직이 어떻게 변하던 영향을 받지 않습니다. 즉, 변화에 닫혀 있습니다.
반대로 서비스 클래스는 인터페이스에서 규정된 규칙만 잘 지키면 언제든 로직을 변경할 수도 있고, 인터페이스를 구현한 새로운 클래스를 하나 만들어서 기존 클래스를 대체할 수도 있습니다. 확장에는 열려있다라고 표현합니다.
여기서 처음 제가 가졌던 의문은 수정이 필요하면 그냥 코드를 수정하면 되는 것 아닌가 하는 것이었습니다. 로직이 바뀌면 충분한 테스트를 거친 뒤에 메소드의 로직을 바꿔주면 되는데 굳이 번거롭게 클래스마다 인터페이스를 하나씩 만들어주면서까지 확장성을 유지해야할까라는 의문이었죠. 기존 대체되는 클래스는 그냥 이름만 바꿔서 따로 백업을 해두면 되는게 아닌가 했습니다. 결론적으로 반은 맞고 반은 틀린 생각이었던 것 같습니다.
전략 패턴은 다른 기능을 가진 클래스를 최대한 분리시켜 클래스 간 결합력을 약화시키는 디자인 패턴입니다. 완전히 분리시킬 수 있는 별도의 기능이라는 가정 하에 중간에 인터페이스를 하나 둬 둘을 분리시켜줍니다. 각자 분리된 기능의 클래스는 다시 추상 클래스 등으로 다시 로직을 분리해 작성될 수도 있습니다. 따라서 인터페이스의 사용은 객체지향적인 프로그래밍의 가장 기본 구조가 됩니다.
이렇게 잘 분리된 구조일 경우 나중에 시스템의 일부가 바뀔 때 최소한의 공수로 시스템을 수정할 수 있게 됩니다. 서로 영향을 받지 않도록 만들어 뒀으니 일부만 수정해주면 되기 때문이죠. 예를 들어 서비스 클래스와 DAO 클래스를 잘 분리해뒀다면 DB 종류가 바뀌더라도 DAO 클래스만 수정해주면 됩니다. 물론 서로 영향이 없도록 초반 설계가 완벽했을 때의 경우입니다.
또한 스프링과 같이 프레임워크 개념의 서비스를 제공한다거나, 고객이 커스터마이징할 수 있도록 허용하는 서비스라면 본래의 코드는 숨겨서 수정하지 못하게 해두고 사용자가 선택적으로 확장해 사용할 수 있도록 할 수도 있습니다. 스프링 시큐리티를 커스터마이징 하다보면 거의 대부분 이런 패턴으로 작성돼있는 것을 알 수 있습니다.
이 말은 즉, 전략 패턴을 위한 인터페이스 사용은 결국 설계의 영역이라는 의미입니다. 두 클래스의 기능이 의존적이라 한쪽이 바뀔 때 다른 한쪽도 바껴야하는 구조라면 굳이 인터페이스를 사용해 전략 패턴을 구사하지 않아도 된다는 것입니다. 또 굳이 향후 확장성, 변동성을 고려하지 않아도 되는 서비스라면 굳이 전략 패턴을 사용할 이유도 없습니다. 다만 강한 결합으로 이루어진 객체 구조는 객체지향적이지 않다는 측면에서 그리 권장되는 구조는 아닙니다.
따라서 단순히 인터페이스 사용이 권장되니까 모든 클래스마다 인터페이스를 하나씩 만들어서 사용한다는 것은 공부에 전혀 도움이 되지 않는 행위라는 것이 제 결론입니다. 실제로 왜 인터페이스를 사용했냐는 질문에 제대로 된 답변을 하지 못하는 분들도 여럿 봤습니다. 인터페이스를 사용해 전략 패턴을 구사했다면 기능 분리와 확정성을 고려해야하는 명확한 이유가 있어야 한다는 것이죠. 그렇지 않으면 괜한 코드 낭비, 시간 낭비이지 않을까 싶습니다. 인터페이스를 미리 완벽하게 작성해서 사용하는 것은 현실적으로 어렵기 때문에 구현체를 만들면서 역으로 인터페이스를 계속 수정하는 일이 발생할텐데, 확장에 대한 개념도 없이 이런 과정을 반복하는 것이 의미가 있을까요.
어차피 어딘가에서 실제 프로젝트에 참여한다면 해당 프로젝트 설계 기준에 따라 전략패턴을 적용하면 될 일입니다. 설계가 어려운 것이지 구현이 어려운 것은 아니니까요. 오히려 의미 없이 인터페이스 만들 시간에 디자인 패턴을 공부하는 것이 효율적이지 않나 생각해봅니다.