말 그대로 순차적으로 코드를 적어나가는 것이다. 필요한게 있으면 계속 순서대로 추가하며 구현하는 방식이다. 단순할 때야 직관적으로 느껴지겠지만 규모가 커질 수록 직관성이 현저하게 떨어진다.
이런 비구조적 프로그래밍에서는 goto문을 활용한다. goto문(C언어)은 CPU가 코드의 다른 지점으로 점프하도록하는 제어 흐름 명령문이다. 다시 말해서 프로그램의 흐름을 원하는 위치로 이동시킬 때 사용되는데, 현재는 프로그램의 자연스러운 흐름을 방해한다하여 사용하지 않는다. 아무튼 프로그램이 커지면 goto문을 무분별하게 사용하게 되고 결국 스파게티 코드가 된다. 코드가 어떻게 연결되어 있는지 확인조차 못하게 되는 문제가 발생하는 것이다.
이런 문제점을 해결하기 위해 탄생한 것이 절차적, 구조적 프로그래밍이다. 반복될 가능성이 있는 것들을 재사용이 가능한 함수(프로시저)로 만들어 사용하는 방식이다. 이전 글에서도 살펴보았던 것처럼 보통 절차는 프로시저(함수)를 뜻하고, 구조는 모듈은 뜻한다. 모듈은 함수보다 더 작은 의미이긴 하지만 요즘은 큰 틀과 같은 의미로 사용된다. 프로시저 및 절차적 프로그래밍에 대해서 이전 글에서 더 자세히 설명하고 있다.
하지만 이런 패더다임에도 문제점이 존재한다. 바로 너무 추상적이라는 것이다. 실제로 사용되는 프로그램들은 추상적이지만은 않다. 함수는 논리적 단위로 표현되지만, 실제 데이터에 해당하는 변수나 상수 값들은 물리적 요소로 되어있기 때문이다.
예를 들어, 책에 해당하는 자료형을 구현하고 또 그 책과 관련된 함수를 구현해야 한다고 생각해보자. 구조적인 프로그래밍에서는 이를 따로 따로 구현해야 한다. 결국 많은 데이터를 만들어야 할 때, 구분하기 힘들고 비효율적으로 코딩할 가능성이 높아진다. 물리적으로 같은 위치에 구현할 수 있지만 논리적으로는 함께 할 수 없는 것이 절차적, 구조적 프로그래밍이다.
이런 맥락에서 단순히 물리적인 위치 뿐만 아니라 논리적인 연결성까지 유지하기 위해서 만들어진 패러다임이 객체지향 프로그래밍이다. 클래스를 생성하고 클래스마다 필요한 필드를 선언하고 필요에 따라 getter, setter를 사용한다. 필요한 함수, 프로시저를 객체 내부에 구현해서 논리적 일관성을 유지한다. 이렇게 특정한 개념의 자료형과 함수를 함께 묶어서 관리하기 위해 탄생한 것이 객체지향 프로그래밍이다. 이렇게 프로그래밍을 하면 객체 간의 독립성이 생기고 중복코드의 양이 줄어드는 장점이 생긴다. 또한 독립성이 확보되면 유지보수에도 도움이 된다.
가장 중요한 점은 객체 내부에 필드와 함수가 같이 존재하는 것이다.
프로그램을 명령어의 목록으로 보는 기존의 명령형 프로그래밍 패러다임의 시각에서 벗어나 프로그램 구현에 필요한 여러 개의 독립된 단위인 객체, 그리고 그 객체들 사이의 협력의 추상화로 프로그래밍하는 패러다임이다.
프로그램을 유연하고 쉽게 변경할 수 있도록 작성할 수 있어서 대규모 소프트웨어 개발에 많이 사용된다.
절차지향 프로그래밍에 비해 느린 실행 속도
객체 지향 프로그래밍은 캡슐화와 격리구조에 때문에 절차지향 프로그래밍과 비교하면 실행 속도가 느리다.
필요한 메모리양의 증가
객체지향에서는 모든 것을 객체로 생각하기 때문에 추가적인 포인터 크기의 메모리와 연산에 대한 비용이 들어가게 된다.
클래스(Class) : 같은 종류(또는 문제 해결을 위한)의 집단에 속하는 속성과 행위를 정의하는 것. OOP의 기본 사용자 정의 데이터 타입.
클래스는 다른 클래스 또는 외부 요소와 독립적으로 디자인해야 한다.
객체(Object) : 클래스의 인스턴스. 객체는 자신 고유의 속성이 있고 클래스에서 정의한 행위를 할 수 있다.
인스턴스(instance) : 실제로 메모리에 할당되어 동작하는 모양을 갖춘 것을 의미합니다. 클래스는 인스턴스가 만들어지기 위한 일종의 설계코드이고, 이 설계 대로 객체가 메모리에 올라가 활동하는게 되는 것이 객체이다.
메서드(Method) : 객체가 실제로 행동하는 함수. 메서드를 통해 객체에 명령을 전달할 수 있고, 객체 간의 상호작용이 일어난다. 이 모든 행위들이 일어나는 것을 메서드를 호출한다고 한다.
추상화(abstraction)
객체들이 공통적인 특징(기능, 속성)을 도출하는 작업이다. 추상적인 개념에 의존하여 설계해야 유연함을 갖출 수 있다. 즉 세부적인 사물들의 공통적인 특징을 파악한 후 하나의 집합으로 만들어내는 것이 추상화이다. 예를 들어, 톰 크루즈, 폴, 제인은 사람이라는 공통점이 있고 이 추상화된 집합에 공통적인 특징(이름, 나이, 성별 등)을 만들어 활용한다. 이렇게 추상화로 구현해두면 새로운 사람들 만들 때 추가로 새롭게 만들어 필드값을 수정해주면 된다.
캡슐화(encapsulation)
낮은 결합도를 유지할 수 있도록 설계하는 것을 의미한다. 다시 말해서 한 객체의 변화가 다른 객체에 미치는 영향을 최소화 시키는 것을 의미한다. 또 이를 위해서는 실제로 구현되는 부분을 외부에 드러내지 않도록 하여 정보를 은닉화한다.
결합도(coupling)란, 어떤 기능을 실행할 때 다른 클래스나 모듈에 얼마나 의존적인가를 나타내는 말이다. 객체지향에서 독립적으로 만들어진 객체들 간의 의존도를 최대한 낮게 만드는 것이 중요하다. 이런 추상화를 통해 필드와 메소드를 따로 관리하는게 목적이기 때문에 당연한 이야기이기도 하지만, 소프트웨어 공학에서 이야기하는 좋은 설계가 바로 이러한 점을 중시하기 때문이다.
객체 간의 모듈 간의 요소가 밀접한 관련이 있는 것으로 구성하여 응집도를 높이고 결합도를 줄여야 요구사항 변경에 대처하는 좋은 설계 방법
그렇다면, 캡슐화는 어떻게 높은 응집도와 낮은 결합도를 갖게 할까? 위에서 언급했던 것처럼 정보 은닉을 활용한다. 외부에서 접근할 필요가 없는 것들은 private으로 접근하지 못하도록 제한을 두는 것이다. 이런 접근 제한자들을 사용해서 객체 안의 필드를 은닉화 시킬 수 있다.
상속성(inheritance)
일반화 관계(Generalization)라고도 하며, 여러 객체들이 지난 공통된 특성을 부각시켜 하나의 개념이나 법칙으로 성립하는 과정이다. 이 상속은 또 다른 캡슐화다. 자식 클래스를 외부로부터 은닉하기 때문이다. 자동차라는 클래스는 구체적으로 어떤 차인지가 은닉화 되어있다. 대리운전자인 경우에 자동차를 운전하는게 중요하지 어떤 차를 운전하는지는 중요하지 않다. 그래서 대리운전자 클래스는 자동차만 알면 되기 때문에 구체적인 자동차의 종류는 은닉화되어 있는 것이다. 한편 새로운 자동차들이 추가된다고 해도, 대리운전자가 영향을 받을 필요가 없다. 그렇기 때문에 대리운전자 클래스 입장에서는 어떤 자동차인지를 확인할 수 없도록 자동차의 이름을 은닉화하여 구현하는 것이다.
상속의 장점
이렇게 상속 관계에서는 단순히 하나의 클래스 안에서 속성 및 연산들의 캡슐화에 한정되지 않는다. 즉, 자식 클래스 자체를 캡슐화하여 '자동차 클래스'와 같은 외부에 은닉하는 것으로 확장되는 것이다. 이처럼 자식 클래스를 캡슐화해두면, 외부에선 이러한 클래스에 영향을 받지 않고 개발을 이어갈 수 있다.
상속의 단점
상위 클래스(부모 클래스)의 변경이 어려워진다.
부모 클래스에 의존하는 자식 클래스가 많을 때 부모 클래스의 변경이 어렵다. 그만큼 많은 자식들이 영향을 받기 때문이다.
불필요한 클래스가 증가할 수 있다.
유사 기능 확장시, 필요 이상의 불필요한 클래스를 만들어야 하는 상황이 발생할 수 있다.
상속이 잘못 사용될 수 있다.
같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면, 문제가 발생할 수 있다. 상속 받은 클래스가 부모 클래스와 IS-A 관계가 아닐 때 이에 해당한다.
해결책 : 컴포지션, 객체 조립
컴포지션은 필드에서 다른 객체를 참조하는 방식으로 구현된다. 상속에 비해 비교적 런타임 구조가 복잡해지고, 구현이 어려운 단점이 존재하지만 변경시 유연함을 확보하는데 장점이 크다. 따라서 같은 종류가 아닌 클래스를 상속하고 싶을 때는 객체 조립을 우선적으로 적용하는 것이 좋다. 상속은 IS-A 관계가 성립할 때, 재사용 관점이 아닌 기능의 확장 관점일 때 사용하자.
다형성(polymorphism)
서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력을 다형성이라고 한다. 다형성은 상속과 함께 활용할 때 큰 힘을 발휘한다. 코드를 간결하게 해주고, 유연함을 갖게 해준다. 즉, 부모 클래스의 메소드를 자식 클래스가 오버라이딩해서 자신의 역할에 맞게 활용하는 것이 다형성이다. 다형성을 이용하면, 구체적으로 현재 어떤 클래스 객체가 참조되는지는 무관하게 프로그래밍하는 것이 가능하다.
상속 관계에 있으면, 새로운 자식 클래스가 추가되어도 부모 클래스의 함수를 참조해오면 되기 때문에 다른 클래스 영향을 받지 않게 된다.
SRP(Single Responsibility) : 단일 책임의 원칙
클래스는 단 하나의 책임을 가져야 한다. 클래스를 변경하는 이유는 단 하나여야 한다. 이를 지키지 않으면, 하나의 책임의 변경에 의해 다른 책임과 관련된 코드에 영향이 갈 수 있다.
OCP(Open-Closed) : 개방 폐쇄의 원칙
확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다. 기능을 변경하거나 확장할 수 있으면서, 그 기능을 사용하는 코드는 수정하지 않는다. 이를 지키지 않으면, instanceOf와 같은 객체가 어떤 클래스인지, 어떤 클래슬 상속받았는지 확인하는데 쓰이는 연산자를 사용하거나 다운 캐스킹이 일어난다.
LSP(Liskov Substitution) : 리스코프 치환 원칙
상위 타입의 객체를 하위 타입의 객체로 치환해도, 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다. 상속 관계가 아닌 클래스들을 상속 관계로 설정하면, 이 원칙이 위배된다.
ISP(Interface Segregation) : 인터페이스 분리의 원칙
인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다. 각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 각 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 해야 한다.
DIP(Dependency Inversion) : 의존 역전의 원칙
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다. 즉 저수준 모듈이 변경되도 고수준 모듈은 변경할 필요가 없는 것읻