인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

수강평 13

평균평점 4.9

100% 수강 후 작성

대학교에서 네트워크 전공을 수강하고 이번 HTTP 강의를 듣게 되었는데, 전공을 수강하였을 때는 각각의 기능들이 어떻게 동작하고 어떤 개념들이 있는지에 대해서는 자세하게 배웠으나 통합적으로 엮어나가는 과정이 부족하였습니다. 이 강의를 통해 그 부족한 부분이 채워지는 것 같아서 되게 만족하였습니다. 그리고 이전 스프링 핵심 원리 강의를 들으면서 막연히 문장 의미만 알고 있었던 SOLID에 중요성을 알게 되어서 너무 만족스러웠습니다. 사실 이거는 이전 수강평에 작성을 하여야했는데 이미 작성을 해서 여기에 추가로 적게 되었네요 그리고 혹시 제가 인터페이스와 SOLID를 맞게 이해하였는지 확인해주시면 감사하겠습니다. 인터페이스가 무엇인지 설명하기 위해서는 앞서 언급한 SOLID가 무엇인지 정확하게 언급하고 넘어가야 한다고 생각한다. ​ SRP : 단일 책임 원칙(Single Responsibility Principle) 한 클래스는 하나의 책임만 가져야 한다. ​ 만약 해당 원칙을 따르지 않고 설계한 프로그램에서 한 클래스의 특정 기능에 문제가 생겼을 경우, 해당 기능을 수정하기 위해서 그와 연관된 다른 클래스들을 찾아내고 함께 수정을 해주어야 한다. 자바가 가진 특징 중 하나인 모듈화 프로그래밍 기법을 온전히 이용하고 있다고 보기 힘들다고 생각한다. 여기서 모듈화가 무엇인지에 대해서 알아보자 위키백과에서는 모듈을 다음과 같이 설명하고 있다. 모듈(module)은 역사적으로 프로그래밍이라는 관점에서는 기본적으로 본체에 대한 독립된 하위 단위라는 필연적인 개념의 큰 틀을 따르고 있지만 본체와 모듈 간에 가지고 있었던 문제들을 해결해 나가는 과정에서 발전하였다. 모듈에 가장 큰 영향을 미쳤던 클래스 그리고 라이브러리가 향상됨에 따라 점차 발전하였다. 이러한 지속 가능성은 이것의 가장 큰 장점 중 하나이다. 초기에는 분리된 독립성의 모듈로 도입되었으나 점차로 객체화, 캡슐화, 모듈화 프로그래밍 기법 등 여러 기능들이 추가되면서 점차적으로 영역이 나뉘어가고 있다. 그러나 이로 인하여 모듈성을 제대로 반영하지 못하고 있다는 비난을 받을 수도 있다. 한편 이러한 비난은 모듈 시스템, 모듈 프로그래밍이 갖는 현재의 한계를 인식하고 보다 안정적으로 발전하기 위해 효율적인 방향을 추구하는데 기여할 수 있다. 여기서 본체는 하드웨어적인 운영체계일 수도 있고 규모 있는 소프트웨어 프로그램의 본체일 수도 있다. ​ 그렇다면 모듈화란 무엇일까? 모듈화는 시스템을 상호 연결된 모듈로 분해하는 것을 의미한다. ​ 다시 본론으로 넘어가서 단일 책임 원칙을 따르지 않을 경우 모듈화 프로그래밍 기법을 이용하고 있지 않다는 이유를 모듈을 레고로 비유하여 설명해보려한다. 레고의 경우 하나 하나가 독립적으로 되어있어 서로 다른 레고를 조합하여 하나의 물체를 만들 수가 있게된다. 그리고 이 물체를 다른 물체로 변경하려 하였을 때는 레고를 분해하고 다시 조립을 하면 된다. 하지만 레고가 열에 녹아서 끈끈하게 붙어버리게 되었다면 레고를 분해하기가 상당히 곤란해질 것이다. 나는 이 경우를 단일 책임 원칙을 따르지 않는 경우라 생각한다. 즉 단일 책임 원칙을 따르고 설계할 경우에는 클래스 혹은 모듈을 독립적으로 분해할 수 있고, 그것이 문제가 생겼을 경우에는 레고 블럭 조각을 교체하듯이 쉽게 수정할 수도 있을 것이다. 하지만 그렇지 않을경우 어느 한 클래스의 문제가 생겼을 때 그와 연관된 다른 클래스들도 코드를 수정하게 되고, 클래스가 독립적으로 이루어지지 않아 서로 다른 클래스를 조합하기도 힘들어지게 될 것이다. ​ 하지만 이것을 한 번 다르게도 생각해보고자 한다. 과연 책임은 어디까지를 말하는가? 즉, 책임을 한 번 분명하게 생각해보고자 한다. OrderServiceImpl라는 클래스가 있고, 할인 정책(고정 할인, 비율 할을 반영한 클래스가 있고 코드를 다음과 같이 짰다라고 가정해보자 ​ public class OrderServiceImpl implements OrderService { private final Discountpolicy discountPolicy = new RateDiscountPoilicy(); ​ 여러 기능들... } ​ 이럴경우 나는 하나의 책임에서 하나란 여기서 new RateDiscountPolicy()처럼 구체화를 지정해주는 것과 OrderServiceImpl의 속해있는 여러 기능들 이 두가지 개념을 분리해야 한다 생각한다 즉 위 코드는 하나의 책임을 갖고 있는 것이 아닌 두 개의 책임을 갖고 있는 것이라 보며, 구체화를 지정해주는 클래스를 따로 설정을 해주어야 한다 생각한다. [여기서 나온 개념이 관심사 분리라는 개념이 아닐까 생각해보기도 한다.] ​ 이 두 가지 관점이 SRP가 내포하는 진정한 의미가 아닐까 라고 생각한다. ​ OCP : 개방-폐쇄 원칙 (Open/Close Principle) 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다. ​ 여기서는 역할과 구현이라는 표현을 이용해보고자 한다. 역할이란 인터페이스를 의미하고, 구현이란 인터페이스를 구현한 클래스를 의미한다. 그렇다면 확장은 무엇이고, 변경은 과연 무엇일까? 확장이란 어떠한 역할을 구현한 구현체 즉 클래스를 여러 개 만들 수 있는 것이고, 변경이란 이 역할 즉 인터페이스를 이용하는 클라이언트에 영향이 생겨서는 안되는 것을 의미한다. 예를들어, 운전자가 있고 역할을 담당하는 자동차 모듈이 있다. 그리고 구현을 담당하는 구현체인 K5, 소나타, 테슬라 같은 차들이 있다고 가정해보자 여기서 자동차 모듈의 역할을 충실히 따르기만 한다면 제네시스를 구현체로 추가할 수도 혹은 아우디 차량을 추가할 수도 있다. 이것은 모두 종류는 다르지만 자동차라고 볼 수 있을 것이다. 하지만 이것을 운전하는 운전자는 K5에서 소나타로 차를 바꿔도 혹은 다른 차로 바꾸어도 운전을 하는 행위에 영향이 생겨서는 안될 것이다. 하지만 '변경에는 닫혀 있다'라는 의미를 다르게도 생각해 볼 수 있다. ​ SRP에서 언급한 내용을 이용해 보고자 한다. 구체화를 지정해주는 클래스를 구성 영역으로 하고, OrderServiceImpl처럼 이러한 기능만을 수행하는 클래스들을 모아놓은 곳을 사용 영역이라 해보자. 변경이란 구성 영역에 해당하는 코드들이 수정되어도 사용 영역에 해당하는 코드들은 수정되지 않아야 한다라고도 볼 수 있다 생각한다. 또한 '확장에는 열려 있다'는 위 내용을 이용하자면 Discountpolicy라는 할인 정책을 반영한 인터페이스가 있고 이를 구현한 RateDiscountPoilicy라는 비율 할인 정책을 반영한 클래스가 있다라고 가정할 때, Discountpolicy라는 할인 정책을 반영한 인터페이스를 구현한 FixDiscountPolicy라는 고정 할인 정책을 반영한 클래스를 추가할 수 있다는 것이다. ​ 이 두 가지 관점이 OCP가 내포하는 진정한 의미가 아닐까 라고 생각한다. LSP : 리스코프 치환 원칙 (Liskov Subsitution Principle) 프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다. ​ 이 개념은 자바의 다형성에 대해 설명하면서 믿음 즉 신뢰에 대한 의미도 내포하고 있다라고 생각한다. ​ 우선 하위 타입의 인스턴스로 바꿀 수 있어야한다는 것에 대해 이야기해보고자 한다. 이것을 이야기 하기전에 다형성에 대해서 우선 이야기해보고자 한다. 객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다. 이를 좀 더 구체적으로 말하자면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다. 이 구체적으로 말한 부분을 예시를 들어서 자세히 살펴보자. ​ 나는 LSP 원칙을 잘 적용한 대표적인 사례는 자바의 컬렉션 프레임워크(Collection Framework)라고 생각한다. 컬렉션 프레임워크에서는 컬렉션데이터 그룹을 크게 3가지 타입이 존재한다고 인식하고 각 컬렉션을 다루는데 필요한 기능을 가진 3개의 인터페이스인 List, Set, Map을 정의하였다. 그리고 인터페이스 List와 Set의 공통된 부분을 다시 뽑아서 새로운 인터페이스인 Collection을 추가로 정의하였다. 만약 Collection이라는 인터페이스 타입으로 변수를 선언하여 할당할 경우 변수에 ArrayList 자료형을 담아 사용하다, 중간에 전혀 다른 HashSet 자료형으로 바꿔도 add()라는 메서드는 동작을 보장받는다. 이것이 하위 타입의 인스턴스로 바꿀 수 있어야한다는 의미이며, 다형성이다. ​ 이번에는 프로그램의 정확성을 깨뜨리지 않는다는 부분에 대해 이야기해보고자 한다. 앞서 얘기한 것으로도 이미 이 부분을 이해할 수 있으나 다른 시각으로도 봐보려 한다. 만약 우리가 컬렉션 프레임워크처럼 자바에서 미리 제공된 것들을 이용하지 않고 직접적으로 클래스 혹은 인터페이스를 만들고 그것을 상속 혹은 구현하는 클래스를 만들었을 때 우리는 다른 누군가가 혹은 나 자신이 그것을 추후 이용할 것이라고 생각하면서 코드를 작성하여야 한다. 하지만 내가 만든 코드를 다른 누군가가 이용할 때 믿음과 신뢰가 없다면 어떠할까? 그 코드가 옳바르게 작성되어져 있는지 계속 확인하여 하며 이것은 시간이 많이 소모되는 작업이다. 즉, 우리는 이러한 신뢰와 믿음을 바탕으로 클래스들을 이용하고 있다. 이러한 신뢰와 믿음을 주기위해서는 인터페이스가 혹은 클래스가 의도한대로 코드를 작성하여 정확성을 깨뜨리지 않아야하며, 사용하는이도 코드를 작성한 자가 위의 원칙을 따라 작성하였다는 것을 믿고 사용하여야 한다고 생각한다. ISP : 인터페이스 분리 원칙 (Interface Segregation Principle) 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 범용적인 인터페이스 보다는 클라이언트가 실제로 사용하는 인터페이스를 만들어야 한다. ​ 나는 이것을 SRP라는 개념을 인터페이스에서 다시 한 번더 강조한 것이 아닐까라고 생각한다. 만약 인터페이스의 추상 메서드들을 범용적으로 구현하였다면, 그 인터페이스를 상속받는 클래스는 자신이 사용하지 않는 인터페이스의 추상 메서드들 또한 반드시 구현하여야만 한다. 또한 인터페이스의 추상 메서드가 변경되게 된다면 그것을 구현한 여러 클래스들에서도 수정이 필요하게 된다. 이것은 코드의 복잡성을 낳게 되고, 또한 수정을 하기 어려워지게되어 시간과 비용이 증가되게 된다. 따라서, 클라이언트의 목적과 용도에 적합하도록 인터페이스를 잘 설계하여야 한다라고 생각한다. ​ 인터페이스 분리 원칙은 단일 책임 원칙과 상당히 유사한 개념을 갖고 있다고 생각한다. 여기서 SRP인 단일 책임 원칙은 클래스의 개념에서 단일 책임을 강조하고, ISP인 인터페이스 분리 원칙은 인터페이스의 개념에서 단일 책임을 강조한다고 생각한다. DIP : 의존관계 역전 원칙 (Dependency Inversion Principle) 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존관계 주입은 이 원칙을 따르는 방법 중 하나 ​ 우선 의존이란 과연 무엇일까에 대해서 얘기를 해보고자 한다. 통상, 프로그램 구조를 설계할 때는, 코드 뭉치가 제공하는 기능을 기준으로 클래스로 나누는 경우가 잦다. 예를 들어, 문서 작성 프로그램에는 다양한 확장자(.pdf, .hwp, .docx 등)로 파일을 저장할 수 있도록 기능을 제공하는 클래스가 존재할 것이다. 프로그래밍 언어의 라이브러리 관리 툴에는 HTTP 요청을 보내는 클래스, HTTP 응답으로 전송받은 파일을 버퍼에 담아 다시 합하는 클래스 등이 존재할 것이다. 코드가 제공하는 기능(이하 서비스)를 기준으로 클래스를 나눌 때, 외부에 기능을 제공하는 클래스를 서비스 제공자 Service provider , 그 기능을 사용하는 클래스를 서비스 사용자 Service user 로 나눌 수 있다. 이 관계를 서비스 사용자는 서비스 제공자에게 의존한다고 얘기한다. 단순하게 의존성 혹은 의존관계로 표현하기도 한다. ​ 그렇다면 이제는 왜 추상화에 의존해야하는지, 구체화에는 왜 의존하면 안되는지에 대해서 이야기해보고자 한다. 만약 우리가 짠 코드가 구체화에도 의존하게 된다면 구체화를 다른 구체화로 변경할 시에 그것을 의존하는 클래스 또한 코드의 내용이 변경되어야 한다. 여기서 객체 지향이란 무엇인가에?에 대해 적었던 내용을 덧붙이려한다. 만약 우리가 A클래스라는 코드를 작성하였다고 한다. 이것이 추상화에만 의존하였을 경우에는 추상화를 구현한 구체화가 변경되더라도 A클래스의 코드는 변경되지 않는다. 하지만 A클래스가 추상화 뿐만 아니라 구체화에도 의존하게 된다면 구체화를 변경하였을 때 A클래스의 코드 또한 변경이 된다. 이것을 방지하기 위해 추상화에만 의존하도록 코드를 짠다면 어떻게 될까? 추상화를 구현한 객체가 생성되지 않아 NullPoiterException이 생기게 될 것이다. 이 문제점을 해결하기 위해 나온것이 의존관계 주입(Dependency Injection)이라 생각한다. 의존관계 주입이란 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의미한다. 즉, 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있게되며, 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있게된다. 여기서 '외부에서 실제 구현 객체를 생성' 이 부분을 공연에 빗대어 표현하고자 한다. 공연을 생각해보면 남배우와 여배우가 있고 공연의 남배우, 여배우를 뽑고 할당할 감독이 있다. 이것을 프로그램에서도 적용을 하는 것이다. 어느 한 클래스에서 직접적으로 구현체를 선언하는 것이 아닌 의존관계 주입에 맞춰서 구체화를 지정해주는 클래스를 따로 설정해 주는 것이다. 이것이 앞에서 언급한 관심사 분리이며 감독에 해당하는 영역을 구성 영역, 배우에 해당하는 영역을 사용 영역으로 두는 것이다. 이렇게 할 경우 배우는 상대 배우에 행동에만 주의하면 되지 그 배우가 누구인지에 대해서는 알지 않아도 되며, 배우를 지정해주는 감독도 따로 있기에 배우가 할당되지 않는 경우도 없게 된다. 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 하며, 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다. 이제 인터페이스에 대해서 언급하려 한다 앞서 인터페이스를 껍데기라 표현하였다. 지금도 인터페이스를 껍데기라 생각하는 것에는 동의한다. 하지만 SOLID 또한 인터페이스를 설명하고 있다고 생각하고 궁극적으로 객체란 무엇인가, 다형성이란 무엇인가에 대한 의미를 내포하고 있다라고 생각한다. 즉 인터페이스란 좋은 객체 지향을 설계하기 위한 훌륭한 도구이며, 자바의 특징 중 다형성을 가장 뚜렷하게 나타내는 것이 아닌가라고 생각한다.

김영한님의 프로필 이미지
김영한
지식공유자

박주형님 열심히 들어주셔서 감사합니다. 이미 정답에 가까운 고민을 많이 하셨네요. 더 깊이있게 학습하기 위해 한가지 추천해드리자면 로버트 마틴의 클린 코더(coder) 책을 한번 정독해보시길 추천드립니다. 응원합니다!

김영한

·

41강

·

33,239명 수강

김영한

·

41강

·

33,239명 수강