Outdated/Column

[OOP] 객체 간 관계, 클래스 간 관계

해달 2020. 2. 27. 18:05

목차

1. 들어가며

2. 클래스 간 관계

3. 객체 간 관계

4. 참고자료


1. 들어가며

프로그램을 개발할 때 우리는 단일 객체만을 사용하지 않는다. 여러 클래스와 여러 객체를 생성하고 사용하게 된다. 이렇게 만든 클래스, 객체 사이에는 여러 관계가 생기게 되는데, 이 관계를 잘 이용하면 유연하고, 강건한 프로그램을 만들 수 있게 된다. 이번 게시글에서는 이런 관계를 알아보도록 하겠다.

2. 클래스 간 관계

클래스 간의 관계는 정적이다. 다시 말해 코드를 작성할 때, 관계가 맺어지고, 실행 중에 변화하지 않는다. 클래스 간 관계는 크게 상속과 구체화가 있다. 클래스 간의 관계를 잘 형성해주면 강건한 일반화 프로그래밍을 할 수 있다.


상속

상속(inheritance)은 객체지향 프로그래밍의 특징인 다형성(polymorphism)을 이용해 코드 중복을 줄여주고 코드를 재사용할 수 있게 한다.[각주:1] 그러나 코드 재사용 목적만으로 상속을 이용하는 것은 바람직하지 않으며, 오직 논리적으로 타당할 경우에만 상속해야 한다. 상속을 올바르게 사용하면 상위 클래스와 하위 클래스 간에 is-a 관계가 성립하게 된다. 하지만 논리적으로 타당하더라도 객체지향에서는 적합하지 않을 수 있다.[각주:2]


Duck is a bird. Parrot is a bird. Swift is a bird.


위와 같이 상속 관계에 있는 클래스를 도식화 한 것을 상속 계층도(inheritance hierarchy)라고 한다.[각주:3] 상속에서 주의할 점은 하위 클래스에서는 상위 클래스의 인터페이스를 물려받는데, 이 때 LSP 원칙[각주:4]에 위배되지 않도록 해야 한다는 것이다. 하위 클래스에서 상위 클래스의 메소드가 필요 없는 경우도 있는데, 여러 종류의 하위 클래스가 필요 없는 경우 해당 메소드를 빈 메소드로 재정의한 중간 클래스를 도입할 수도 있다.


구체화

구체화(implement) 관계는 상속과는 다르게 논리적으로 관련이 없지만 같은 이름의 메소드를 가지는 것들을 묶는다. 대부분의 현대 언어에서는 이를 지원하기 위해 interface 기능을 제공한다. C++의 경우 추상 클래스를 활용한다.


class IFlyable

{

public:

    virtual void Fly() = 0;

}

 

class Bird : public IFlyable

{

public:

    void Fly() override

    {

        // do something

    }

}

C++은 추상 클래스를 이용하여 다른 언어의 interface 기능을 흉내낸다.

3. 객체 간 관계

객체 간 관계는 클래스 간 관계와는 다르게 동적이다. 객체 간 관계는 크게 사용과 포함으로 나뉜다.


사용

사용(use-a) 관계는 논리적인 관계로 가장 일반적인 관계다. 멤버가 아닌 다른 클래스의 객체를 메소드의 인자로 받아 사용하거나 메소드 내에서 다른 클래스의 객체를 생성하여 사용하는 경우를 일컫는다.


class Child

{

public:

    void Wash(const Towel& towel)

    {

        // do something

    }

}

 

class Camera

{

public:

// 여담이지만 여기에는 NRVO(Named Return Value Optimization)이 적용된다.

    Picture TakePicture()

    {

        Picture pic;

        // do something

 

        return pic;

    }

}

사용 관계의 코드


이를 클래스 다이어그램으로 나타내면 다음과 같다.


사용 관계의 클래스 다이어그램


포함

포함(has-a) 관계는 물리적 관계로 객체가 다른 객체를 멤버로 유지하는 경우다. 포함 관계는 관계의 범위(multiplicity)를 파악해야 하며, 이에 따라 구현 방법이 달라진다. 다시 말해 객체를 하나만 가지는지 혹은 여러 개를 가지는지 확인해야 한다. 포함 관계는 크게 부분전체(part-whole) 관계와 연관(association) 관계로 나뉜다. 부분전체 관계는 다시 집합(aggregation)과 복합(composition)으로 나뉜다. 집합과 복합은 전체가 소멸될 때 부분도 같이 소멸되냐에 따라 갈린다.



class Computer

{

    CPU* cpu;

    // 다른 멤버들

    // ...

public:

    Computer(const CPU& cpu, ...)

        : cpu{ &cpu }, ...

    {

    }

}

위의 예시에서 Computer와 CPU는 집합 관계다.
즉, Computer가 제거되어도 CPU는 제거되지 않는다.



class Computer

{

    CPU cpu;

    // 다른 멤버들

    // ...

public:

    Computer(CpuFactory::ECpuType cpuType, ...)

    {

        cpu = CpuFactory::GetInstance(cpuType);

    }

}

위의 예시에서 Computer와 CPU는 복합 관계다.
즉, Computer가 제거되면 CPU도 같이 제거된다.


클래스 다이어그램으로 나타낸 포함 관계
숫자는 관계의 범위를 나타낸다.


4. 참고자료


  1. 관련된 원칙으로 DRY(Do not Repeat Yourself)가 있다. [본문으로]
  2. 이에 대해서는 ‘[OOP] 객체지향 설계 원칙 - SOLID’ 의 리스코프 치환 원칙에서 살펴본 바 있다. [본문으로]
  3. 보통은 그림처럼 트리 구조를 형성하지만 다중 상속을 지원하는 C++의 경우 다이아몬드 구조를 형성하는 경우도 있다. 이와 관련된 문제로 죽음의 다이아몬드(DDD; the Deadly Diamond of Death)가 있다. [본문으로]
  4. 하위 타입은 항상 상위 타입으로 대체될 수 있어야 한다는 원칙이다. 자세한 것은 ‘[OOP] 객체지향 설계 원칙 - SOLID’을 참고하자. [본문으로]