객체지향 프로그래밍과 SOLID

절차적 프로그래밍, 전통적인 설계 원리

 

 객체지향 프로그래밍 이전에 프로시저를 이용한 절차적 프로그래밍(procedural programming)이라는 방법이 많이 사용되었다. 이것은 프로시저(=함수)를 이용하여 절차적으로 프로그래밍을 하는 것으로, 메인함수뿐 아니라 여러 절차(기능)을 함수로 만들어 진행시키는 방식을 이야기한다. 순서도를 생각하면 쉽다. 

 

 

간단한 알고리즘 순서도

 

필요한 기능들을 함수로 만들어놓고, 함수를 통해서 흐름을 나타내는 설계방식 하에서도 여러가지 설계 원리가 많이 나타났다. 

 

단순성(simplicitiy): 유지보수를 위해서 가장 중요한 특성, 소프트웨어 유지보수는 비용이 매우 많이 드는 작업이기 때문에, 유지보수를 위해서는 단순하고 이해하기 쉬운 설계가 중요하다.
효율성(efficiency): 단순성과 비슷한 맥락을 가진다. 처리시간(시간복잡도)과 기억공간(공간복잡도)상에서 들어가는 비용을 이야기한다.
단계적 분해(stepwise refinement): 문제를 상위 개념부터 구체적인 단계로 하향식으로 분할하는 기법(divide and conquer). 프로그램을 서버와 클라이언트로 분할하거나, 시스템을 여러 서브시스템으로, 서브시스템은 여러 패키지로 분할하는 형태를 말한다.
추상화(abstraction): 주어진 문제에서 특정한 목적에 관련된 필수 정보만 추출하여 강조하고, 세부사항(구현)은 생략함으로서 복잡성을 줄이고 절차적인 동작 관점으로 정의하는 방법. 
모듈화(modularization): 문제를 소프트웨어의 구성요소가 될만한 수준으로 분할하는 과정. 하나의 큰 덩어리 시스템으로 만드는 것보다, 모듈 단위로 개발하고 테스트하는 것이 더 유리하다.

 

 이러한 설계 원리만으로도 충분하다고 생각할지 모르지만, 문제는 시간이 지나면서 복잡해진 프로그램에 맞추어 코드들도 점점 더 복잡해지기 시작했고, 순서도로는 도저히 표현할 수 없을 만큼 복잡한 프로그램 구조를 갖게 되었다.

 

 

 나도 저번 학기에 C로 리눅스 명령어를 구현하는 프로그램을 짠 적이 있다. 물론 내 더러운 코드도 한몫 했지만, 정답 코드를 봐도 몇천줄이 넘는 코드는 함수만으로 표현하기에는 너무 복잡했다. 비슷한 기능을 하는 함수들이 여러개 존재하다보니 주석을 참고하지 않고서는 가독성이 너무 떨어진다는 문제를 갖게 되었다.

 왜냐하면 프로시저는 코드들을 구조화했을 뿐, 데이터 자체는 구조화하지 못했기 때문이다. 몇천줄의 코드로도 이런데, 절차적 프로그래밍 방법으로 몇만, 몇십만줄의 코드를 짜게 되면 순서도가 꼬이기 시작하면서, 사람이 코드를 읽으면서 이해하기가 너무나도 힘들어지기 때문이다.

 

 

복잡해져가는 순서도

 

 

 

 

객체지향 프로그래밍

 그래서 이러한 문제를 해결하기 위해서 나타난 것이 객체지향 프로그래밍이다. 큰 문제를 작은 문제(프로시저)들로 쪼개는 것이 아니라, 작은 문제들을 표현하는 구조(객체)를 만든 뒤에, 이 객체들을 조합하여 큰 문제를 해결하는 방법을 도입한 것이다. 

 객체지향 프로그래밍의 핵심 포인트는 당연히 객체(=클래스)이다. 기존에는 사용성에 상관하지 않고 기능에 따라 함수를 만들고 호출했다면, 객체지향 프로그래밍은 객체를 기준으로 동작을 설명하기 위해 함수를 사용한다. 실제로 우리가 사는 세상은 객체 중심으로 돌아간다. 객체마다 고유한 특성과 행동을 가지며, 다른 객체들과 정보를 주고받는 등 상호작용하면서 존재한다. 이러한 특징을 프로그램을 짤 때 활용하는 방법이 객체지향 프로그래밍이다. 자바로 예를 들면, 존재하는 모든 정보(필드)와 행동(메서드)은 객체(클래스)안에 종속된다.

 

 

여기서 객체지향의 대표적인 특징이라고 불리는 4가지(추상화, 캡슐화, 상속, 다형성) 개념이 자연스럽게 나온다. 객체를 분리하기 위해서 공통된 속성과 기능을 묶어서 이름붙이는 것을 추상화라고 하고, 객체 내부의 구현을 감추어(은닉화) side effect를 최소화하는것을 캡슐화라고 하고, 추상화된 객체끼리의 공통된 특징을 묶어 상위개념과 하위개념을 분류하고, 하위개념의 공통된 특징은 상위개념에 표현하여 사용하는 것을 상속이라고 한다. 마지막으로 상속이 이루어졌을 때 하위개념의 객체가 가지는 특성에 맞게 구현(오버라이딩)하거나, 객체 자체에서 같은 기능을 하지만 다양하게 사용할 수 있게 하는 것(오버로딩)을 다형성이라고 한다. 이 모든 개념은 객체 단위로 프로그래밍을 하기 위한 방법들이다.

 

 

 

 

객체지향 5원칙(SOLID)

 

 SOLID라고 불리는 5가지 원칙은 객체지향 프로그래밍을 할 때 지켜야 할 원칙이다. 모든 디자인 패턴들은 이 원칙을 지키며 설계(싱글톤 등)되고, 이 원칙을 지키기 위해서 다양한 디자인 패턴(MVC, MVVM 등)들이 사용되고 있다. 좋은 소프트웨어 시스템이란 시스템이 만들어지고 수명이 다할 때까지 비용이 증가하지 않는 시스템이라고도 할 수 있다. SOLID는 그러한 시스템을 만들기 위한 기본적인 가이드라인이라고 할 수 있다.

 

 

1. 단일 책임 원칙(SRP, Single Responsibility Principle)

클래스를 변경하는 이유는 단 하나여야 한다. 즉, 클래스는 단 한가지 책임(기능)만을 갖도록 설계하자.

 

 위와 같이 Person 클래스가 있다고 하자. 급여 계산, 마일리지 함수는 사람과 관련된 함수들이다. 하지만 삽입, 삭제, 갱신이라는 함수들은 사람과 관련되지 않았으며, DB를 관리하는 별도의 클래스로 관리해야 한다. 이렇게 SRP는 클래스의 목적을 명확히 함으로서 구조가 난잡해지거나 객체와 관련없는 기능(책임)이 객체 안에 들어오는 것을 막는다. 이를 통해서 유지보수를 쉽게 하고, 시스템에 변경이 발생할 때 관련 객체에서만 수정이 이루어지도록 한다.

조금 다른 관점에서 바라보면, 하나의 모듈은 하나의 액터(Actor)에 의해서만 책임지어져야 한다는 말과도 같다. 하나의 액터가 목적성을 가지고 클래스를 변경할 때, 다른 액터가 그로 인한 영향을 받아서는 안된다. 이때 영향이 발생한다면 이는 side-effect가 되며, 유지보수의 cost를 높이는 원인이 된다.

 

 

 

2. 개방 폐쇄 원칙(OCP, Open Closed Principle)

확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다. 즉, 객체 기능의 확장은 허용하되 스스로의 변경은 피해야 한다.

 

> 변경될 것과 변경되지 않을것을 구분하고, 두 모듈이 만나는 지점에 인터페이스를 정의하여 그것에 의존한다.

 

 후술할 DIP와 더불어 SOLID의 대원칙, 객체지향의 핵심이라고 불리는 원칙이다. 기존의 소스코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 뜻이다. 만약 어떤 기능을 수정할 때, 그 기능을 이용하는 다른 모듈들도 따라서 고쳐야 한다면 유지보수가 매우 번거로울 것이다.

 

https://www.youtube.com/watch?v=7c0tqHLfxlE&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC

 

이를 위해서 보통 객체지향의 추상화와 다형성을 활용한다. 추상 클래스나 인터페이스에 의존하도록 하고, 구체적인 기능이 필요하다면 구현체를 추가함으로써 기능을 추가한다.

 

 

 

3. 리스코프 치환 법칙(LSP, Liskov Subsitution Principle)

자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야 한다.

 

> 똑같은 연산을 제공하지만, 약간의 차이가 있다면 공통의 인터페이스를 만들고 구현한다. 

 

 인터페이스나 상위클래스를 구현 또는 상속받은 하위 클래스는 상위 클래스의 핵심적인 기능은 동일하게 동작해야 한다. 이 원칙을 지켜 설계해야 상속(is-a)관계를 기반으로 안정적인 다형성을 유지할 수 있다. 다시 말하면 올바른 상속을 위해서 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 법칙이라고도 할 수 있다.

 

 

 

4. 인터페이스 분리 원칙(ISP, Interface Segregation Principle)

하나의 일반적인 인터페이스보다 구체적인 여러개의 인터페이스가 낫다.

 

 한마디로 객체가 사용하지 않는 메서드에까지 의존성을 가지지 않도록 인터페이스를 분리하라는 것이다. 이를 통해서 클라이언트가 사용하지 않는 기능(인터페이스)에 변경이 발생하더라도 영향이 받지 않도록 만들 수 있다.

 

 예를 들어, 클라이언트에게 전송을 위해서 Client라는 인터페이스에 A, B라는 별도의 기능을 위한 추상메소드를 만들어 놓는다. 서버에서는 A라는 기능을 담당하는 서버와 B라는 기능을 담당하는 서버가 있다면, 둘 다 Client 인터페이스를 사용할 경우 불필요한 인터페이스가 제공되는 것이다. 이럴 경우 A라는 인터페이스와 B라는 인터페이스를 각각 만들고 Client가 이 둘을 상속하게 하면 서로 필요한 인터페이스만 사용하여 전달할 수 있다. 

 

 

 

5. 의존 역전 법칙(DIP, Dependency Inversion Principle)

소스코드는 구체 클래스가 아닌 추상 클래스(인터페이스)에 의존해야 한다.

 

 OCP와 더불어 객체지향의 핵심. 의존 관계를 맺을 때, 변하기 쉬운(클래스) 것이 아닌 변하기 어려운 것(인터페이스)에 의존해야 한다는 것이다. 쉽게 이야기하면 역할에 의존해야 구현체를 상황에 따라 유연하게 바꿀 수 있다.

 

 하지만 사실 대부분의 소프트웨어 시스템은 구체에 의존하지 않기가 힘들기 때문에 (자바에서 문자열 사용을 위해서는 java.lang.String이라는 클래스에 의존한다) '클래스이냐 인터페이스이냐'의 문제보다는 '잘 변하지 않는가 == 안정성이 보장되었는가'의 여부에 달려있다고 보는 것이 더 적절하다고 생각한다. 추가로 필연적으로 발생하는 구체 의존성의 경우 DIP를 위배하는 클래스들을 적은 수의 구체 컴포넌트 내부로 모아서 시스템의 나머지 부분과 분리하는 추상 팩토리 패턴이 사용되기도 한다.

 

 

 DIP를 지켜야 DI(Dependency Injection)이라는 기술을 유연하게 사용 가능하다. 위에서 Kid라는 클래스가 Robot, Lego라는 구체 클래스를 이용하여 관계를 맺지 않고, Toy라는 추상 클래스를 이용하여 관계를 맺게 하고 있기 때문에 setter injection을 이용하여 DI를 활용할 수 있다.

 

 

 

정리

 

 가장 이상적인 형태는 서로 다른 목적으로 변경되는 요소를 분리하고(SRP), 이들 사이의 의존성을 체계화함으로써(DIP) 변경을 최소화하는데 있다(OCP). 

 

 

 

 2학년 1학기에 들었던 소프트웨어공학 복습겸 정리해보았다. 자바와 객체지향을 막 배우던 그때보다 지금 들었다면 훨씬 더 도움이 됐을텐데.. 그래서인지 올해부터 소프트웨어공학이 3학년 과목으로 바뀌었더라.

다음에 시간나면 정리할것

1. 클래스상속(is-a), 객체 합성(has-a)

2. DI와 IoC

3. 

 

관련 글

-spring의 IoC, DI: https://eckrin.tistory.com/entry/Spring-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94

 

[Spring] IoC, 컴포넌트 스캔, 의존관계 주입

Ioc (Inversion of Control)  간단하게 말하면, 사용자의 new 선언을 방지하고, 모든 레퍼런스 변수, 관계설정, 제거, 사용까지 오브젝트(스프링 빈) 전반에 걸친 모든 과정을 프레임워크의 컨테이너(

eckrin.tistory.com

-MVC, MVP, MVVM: https://eckrin.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVC-MVP-MVV

 

MVC, MVP, MVVM

더보기 참고: https://beomy.tistory.com/43 1. MVC (Model - View - Controller) Model: 어플리케이션에서 사용되는 데이터와 그 데이터를 처리하는 부분 View: UI(User Interface) Controller: 사용자의 입력을..

eckrin.tistory.com

-spring의 MVC: https://eckrin.tistory.com/entry/Spring-MVC-%EC%97%AD%ED%95%A0%EC%9D%98-%EB%B6%84%EB%A6%AC

 

[Spring MVC] 역할의 분리와 디자인 패턴 - MVC 패턴, Spring MVC 동작

Layering  실무에서 다루는 복잡한 프로그램들은 우리가 개념을 배울 때 사용하는 예시 프로그램과 같은 간단한 구조로 이루어져있지 않다. 이러한 복잡한 프로그램을 다루는 가장 일반적인 방법

eckrin.tistory.com