C++

객체 지향이란?

Honey Badger 2023. 5. 13. 06:42

  우선 객체란 프로그램 동작의 주체가 되는 요소를 의미합니다. 모든 객체에는 상태와 동작이 존재하는데 보통 상태를 멤버 변수, 동작을 함수와 연결지어 이야기합니다. 객체 지향은 말 그대로 객체를 지향하는 즉, 객체를 통해 코드를 구성하는 방법론이라고 정의할 수 있습니다. 

 

 

객체 지향은 왜 사용할까?

  일단 절차지향 방식과 비교했을 때 객체지향은 생산성과 유지보수 용이성이 좋아서 개발자가 개발을 비교적 쉽고 빠르게 수행할 수 있기 때문이라고 생각합니다. 

 

어떠한 면에서?

  모듈화된 객체를 기반으로 코드가 작성되기 때문에 코드 재사용이 편리하고, 만약 객체를 수정할 경우 해당 객체를 사용하는 곳에 모두 일괄적으로 적용되니 유지보수가 간편합니다. 그리고 업무 분담이 쉬워 큰 큐모의 프로그래밍에 유리합니다.

 

단점은?

  절차지향과 달리 각 객체의 의존 관계로 인해 대체적으로 속도가 느리고 추상 객체, 상속, 인터페이스 등의 복잡한 개념을 제대로 이해하고 활용해야 하므로 코드의 구조를 파악하기가 어렵다고 생각합니다. 

 

 

 

 

 

객체지향의 5원칙 (SOLID) 


1. 의존성 역전 원칙 (Dependency Inversion Principle)

 

  객체는 저수준 모듈보다 고수준 모듈에 의존해야 한다는 원칙입니다. 여기서 저수준 모듈은 단순히 구현된 객체를 의미하고 고수준 모듈은 인터페이스와 같은 객체의 형태나 추상적 개념을 의미합니다. 쉽게 이야기하면 객체의 상속은 인터페이스를 통해 이루어져야 한다는 의미로 해석할 수 있습니다. 

 

예를 들어 의존성 역전 원칙을 준수한 경우와 준수하지 않은 경우 어떤 차이가 있는지 보여드리겠습니다.

1. 준수하지 않은 경우 : 한손검 무기 클래스를 만든 후 캐릭터 클래스에서 객체를 생성할 때 무기를 입력하도록 만들어보겠습니다. 이 때 자연스럽게 '한손검'이라는 추상화되지 않은 무기는 캐릭터의 코드에 본인의 의존성을 부여합니다. 이 상황에서 다른 유형의 무기를 사용하려면 캐릭터의 코드를 바꿔줘야 하겠죠? 즉, 아래 설명할 개방-폐쇄 원칙을 위배하게 됩니다. 또한 무기를 바꿀 때마다 코드를 이에 맞게 바꿔주어야 합니다. 

 

2. 준수한 경우 : 이번엔 추상화된 고수준 모듈에 의존하도록 만들어보겠습니다. 무기의 인터페이스를 생성해 앞으로 모든 공격 가능한 무기 객체가 이 인터페이스를 상속받게 만듭니다. 이렇게 되면 캐릭터 객체에서는 한손검, 활 등 특정된 객체를 파라미터로 받지 않고 해당 인터페이스를 파라미터로 받을 수 있습니다. 이렇게 되면 어떠한 무기던지 캐릭터에서 다룰 수 있게 됩니다. 

 

 

 

 

 

2. 인터페이스 분리 원칙 (Interface Segregation Principle)

 

  객체는 자신이 호출하지 않는 메소드에 의존하지 않아야 한다는 원칙입니다. 그림을 통해 알아보겠습니다.

위의 가운데 객체를 왼쪽과 오른쪽이 각각 상속했을 때 오른쪽 객체의 경우에는 1번 함수를 제외한 나머지는 필요가 없음에도 이를 상속했기 때문에 해당 함수를 가지고 있거나, 구현해야 하는 경우가 생깁니다. 

 

그러나 아래와 같이 상속 대상인 객체의 함수를 각 동작별로 구분해 인터페이스로 만들었다면, 각 객체가 필요한 인터페이스만을 상속하여 구현하면 되므로 훨씬 효율적입니다. 

 

인터페이스는 다중 상속을 지원하므로, 필요한 기능을 인터페이스로 나누면 객체에 필요한 기능을 쉽게 추가할 수 있습니다. 한 마디로 정리하면 인터페이스 분리 원칙은 객체가 반드시 필요한 기능만을 가지도록 제한하는 원칙입니다. 객체를 상속할 땐 해당 객체가 상속받는 객체에 적합한지, 의존적인 기능은 없는지 잘 판단하여 구현해야 할 것입니다. 

 

 

3. 리스코프 치환 원칙 ( Liskov Subsitution Principle)

 

  부모와 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙입니다. 역시 이대로는 잘 와닿지 않으니 직사각형과 정사각형의 관계를 예로 들어 설명해보겠습니다.

 

먼저 리스코프 치환 원칙을 위배한 경우는 다음과 같습니다. 직사각형 객체에는 너비, 높이를 지정하고 지정된 값을 통해 자신의 넓이를 계산할 수 있도록 합니다. 그 후 정사각형도 직사각형의 한 종류이니 상속받아 정사각형을 빠르게 구현할 수 있을 것이라 생각합니다. 정사각형은 너비와 높이가 같으니 오버라이딩을 통해 너비와 높이를 일치시켜주도록 합니다. 

  자 이제 넓이를 구해보겠습니다. 리스코프 치환 원칙에 의하면 자식 객체는 부모 객체를 완전히 대체할 수 있어야 하므로, 정사각형은 직사각형을 완전 대체할 수 있어야 합니다. 하지만 정사각형은 오버라이딩을 통해 너비와 높이를 일치시켜주도록 구현했으므로 다른 결과가 출력됩니다. 

 

쉽게 말하면 제대로 된 상속관계를 구현하여야 한다는 것입니다. 하나가 다른 하나를 완전히 포함하는 구조여야 합니다. 위의 예시의 경우 직사각형은 정사각형을 개념적으로 완전히 포함할 수 없습니다. 그렇다면 리스코프 치환 원칙을 지키려면 어떻게 변경하여야 할까요? 바로 사각형을 구현하고 정사각형과 직사각형이 이를 상속받도록 변경하면 됩니다. 이 원칙을 지키는 것이 조금 어려울 수 있으니 힌트를 주자면 가급적 부모 객체의 함수를 그 의도와 다르게 오버라이딩 하지 않는 것이 중요합니다. 

 

 

 

 

4. 개방-폐쇄 원칙(Open-Closed Principle)

 

  객체의 확장은 개방적으로, 객체의 수정은 폐쇄적으로 이루어져야 한다는 원칙입니다. 쉽게 예를 들어 객체 하나를 수정한다고 가정해보겠습니다. 이 때 단순히 해당 객체만 수정하는 것이 아니라 해당 객체에 의존하는 다른 객체들의 코드까지 줄줄이 고쳐야 한다면 좋은 설계로 보기 힘들 것입니다. 라이브러리를 사용하는 객체의 코드가 변경되었다고 라이브러리의 코드까지 변경되지 않는 것처럼 각 객체의 모듈화와 정보 은닉의 올바른 구현을 추구하며, 객체간의 의존성을 최소화하여 코드 변경에 따른 영향력을 낮추기 위한 원칙입니다. 이 원칙을 지키려면 기능을 확장한다고 상상했을 때 기존 코드의 변경 없이 기능을 확장할 수 있는지 살펴보아야 합니다. 

 

 

 

 

 

 

5. 단일 책임 원칙(Single Responsibility Principle)

 

  하나의 객체는 반드시 하나의 동작만의 책임을 갖는다는 원칙입니다. 우리는 코드를 설계할 때 때로는 하나의 객체가 너무 많은 동작을 담당하게 합니다. 자동차를 예를 들어 설명해보겠습니다. 자동차는 전륜, 후륜, 사륜으로 나뉘며 자동차 클래스에 이 세가지 유형을 Enum으로 정의해 각 유형에 따라 적절한 바퀴에 힘이 가해지도록 구현할 수 있습니다. 이 자동차가 짊어지는 책임은 무려 세가지나 됩니다. 이러한 경우 프로젝트에서 해당 객체의 의존성이 높아지게 되고, 이러한 현상은 객체지향의 주요 특징 중 하나인 캡슐화를 정면으로 부정합니다. 그렇다면 위의 설계를 단일 책임 원칙을 지키도록 변경한다면 어떻게 해야할까요?

 

  추상 클래스 '자동차'를 만들고 이를 상속받은 '전륜 자동차', '후륜 자동차', '사륜 자동차' 클래스를 만듭니다. 그 후 각각의 클래스에서 적절한 바퀴에 힘이 가해지도록 동작을 각각 구현한다면 하나의 객체 하나의 책임을 가질 수 있게 됩니다. 

 

 

 

 

 

 

 

 

 

 

 

 객체지향의 특징 


1. 다형성(Polymorphism)

 

  하나의 객체 혹은 함수가 여러 타입을 참조할 수 있음을 의미한다. 객체의 다형성은 객체가 상속된 부모 객체의 인스턴스로 할당 될 수 있음을 의미하는데, 피자를 상속받은 불고기 피자, 파인애플 피자가 있다고 가정했을 때 피자 타입의 인스턴스는 불고기나 파인애플 피자 타입으로 할당될 수 있습니다. 어쨌건 둘다 '피자' 라는 부모를 가지기 때문이죠. 단 주의할 점이 한가지 있다면 피자 타입의 인스턴스에 할당한 파인애플피자는 자신이 가지고 있는 함수인 '새콤하게 하기'를 사용할 수 없습니다. '피자' 타입에는 새콤하게 하기라는 함수가 없기 때문입니다. 

  함수의 다형성의 경우 우리가 흔히 아는 오버로딩으로 설명할 수 있습니다. 함수가 동일한 이름을 가지더라도 입력받는 매개변수가 다르면 각각 개별적인 함수로 취급함을 의미합니다. 

 

 

 

 

 

2. 상속(Inheritance)

 

  상속이란 객체가 다른 객체를 상속받아 상속받은 객체의 요소를 사용하는 것을 의미한다. 하나 이상의 추상 함수를 포함하는 클래스는 추상 클래스입니다. abstract 키워드를 통해 표현할 수 있습니다. 추상 함수는 자식 클래스에서 구현해야 하는 함수이므로 일반적인 함수와는 다르게 아무런 내용이 들어가있지 않은 것이 특징입니다. 예를 들어 피자 클래스에 '토핑을 뿌리기' 함수를 추상 함수로 선언해놓고 피자를 상속을 불고기피자, 파인애플피자, 치즈피자 등의 클래스에서 '토핑을 뿌리기'함수를 오버라이딩 하여 각자의 토핑 뿌리는 로직을 구현할 수 있습니다.

 

 

 

 

 

3. 캡슐화(Encapsulation)와 정보 은닉

 

  클래스의 내부 변수와 함수를 하나로 패키징하는 것을 캡슐화라고 합니다. 만약 객체에 선언된 변수나 함수를 어디서나 접근할 수 있다면 이는 객체라고 보기 어렵습니다. 캡슐화와 비슷한 개념으로 정보 은닉이 있는데 정보 은닉은 내부 구현을 숨김으로써 객체가 반드시 정해진 함수를 통해 상호작용하도록 유도합니다. 이 두 개념은 객체의 모듈화를 지향하는데 모듈화가 잘 이루어진 객체의 경우 모듈 단위의 재사용이 매우 용이합니다. 접근제어자(public, private, protected) 등을 적재적소에 잘 활용하여 객체 내부 구현 내용을 불필요하게 개방하지 않도록 하는 것이 좋습니다. 

 

'C++' 카테고리의 다른 글

클래스, 객체, 인스턴스란 무엇일까?  (0) 2023.05.13
[C++] const 위치에 따른 차이점  (0) 2023.03.26
[C++11] 람다식  (0) 2022.10.19