Priv's Blog

1. 객체지향 모델링 본문

Dev. Study Note/Design Pattern

1. 객체지향 모델링

Priv 2022. 7. 3. 23:06


 

 

1. 모델링

소프트웨어 개발에서 모델은 소프트웨어의 전체적인 구조를 파악할 수 있게 해주는 역할을 한다.

서로의 해석을 공유해 합의를 이루거나, 해석의 타당성을 검토하게 만들어주며, 현재 시스템 또는 앞으로 개발할 시스템의 원하는 모습을 가시화하는 데 도움을 줄 수 있다.

또한 시스템의 구조, 행위를 명세하여 시스템 구축에 필요한 틀과 구축된 시스템의 문서화 기능을 제공할 수도 있다.

모델은 추상화(Abstraction)를 바탕으로 만들어져야 한다.

추상화는 대상의 세부적인 모든 것을 표현하지 않고, 그 대상의 특징을 부각할 수 있는 핵심 요소들만 표현해준다.

예를 들어, 대학 전산 처리 과정에서 한 학생을 추상화하여 모델링할 때, 그 학생의 머리카락 수나 피부색 등은 생략하고, 수강하는 과목, 지도 교수, 전공 등을 모델링 대상으로 삼는 것이다.

 


 

2. UML

모델링을 하기 위해서는 시스템을 모델로 표현해주는 언어가 필요하다.

UML(Unified Modeling Language)은 이럴 때 사용되는 대표적인 언어이다.

요구 분석, 시스템 설계 및 구현 등 시스템 개발 과정에서 개발자 사이의 원활한 의사소통을 위해 표준화한 통합 모델링 언어이다.

UML 2.0에서는 다음과 같이 시스템의 구조와 동작을 표현하는 13개의 다이어그램을 제공한다.

 

2.1) 클래스 다이어그램

클래스 다이어그램은 시스템을 구성하는 클래스, 그들 사이의 관계를 보여준다.

예를 들어, 자신의 집을 표현한다고 가정한다.

방 3개, 화장실 1개, 주방 1개, 3개의 방 중심에 거실 1개라고 표현한다면, 이는 정적인 구조를 표현한 것이라고 말할 수 있다.

클래스 다이어그램은 이처럼 시간의 흐름에 따라 변하지 않는 정적인 시스템 구조를 보여주는 UML 구조 다이어그램이다.

클래스 다이어그램의 주요 구성 요소는 클래스와 관계이다.

 

2.2) 클래스

클래스란, 공통의 속성과 책임을 가지는 객체들의 집합이자, 실제 객체를 생성하는 설계도이다.

예를 들어, 컴퓨터공학을 전공하는 20210130 학번2학년 A 학생소프트웨어 공학을 전공하는 20191011 학번4학년 B 학생이 존재한다고 가정하자.

이때, 두 학생 모두 학번과 전공, 학년, 이름, 수강 과목 등의 공통적인 데이터를 가질 것이며, 전공 수업을 수강해야 한다는 책임이 있을 것이다.

여기서 A와 B 학생은 모두 하나의 객체로 여길 수 있다.

이 객체는 '학생'이라는 클래스를 기반으로 생성되며, 클래스가 존재하는 한 객체는 원하는 만큼 생성할 수 있다.

즉, 클래스는 특정 대상에 대한 일종의 '청사진'이라고도 표현할 수 있다.

//코드 1-1

public class Cat {
    private String name;
        
    
    public Cat(String name) {
        this.name = name;
    }
    
    
    public void Meow() {
        System.out.println(name + "~~~~~" + "웁니다.");
    }
}

위의 코드에서 Cat( ) 클래스는 고양이라는 객체를 만들어내는 청사진이다.

이 청사진을 기반으로 실제 고양이라는 객체를 만들어내기 위해서는 다음과 같이 new 연산자를 사용하면 된다.

Cat NorwegianForestCat = new Cat("노르웨이숲");
Cat MaineCoon = new Cat("메인쿤");

이제 고양이 2마리가 소환되었다.

코드 1-1에서 작성한 Cat 클래스를 보면 생성자를 통해 고양이 객체에 이름을 부여해줄 수 있음을 알 수 있다.

고양이를 만들었으니, 이제 노르웨이 숲이 우는 소리를 들어보자.

NorwegianForestCat.Meow();

이제 위에서 언급한 학생 클래스를 UML로 표현해보자.

UML에서 클래스를 표현할 때는 아래와 같이 세 부분으로 나눠진 박스로 표현해준다.

맨 위 1번째 박스에는 클래스의 이름이 들어간다.

가운데 2번째 박스에는 클래스의 특징을 나타내는 속성이 들어간다.

마지막 3번째 박스에는 클래스가 수행하는 연산(책임)을 기술한다.

경우에 따라서 속성이나 연산 부분은 생략할 수도 있다.

클래스 속성과 연산을 기술할 때 '-' 기호 또는 '+' 기호를 사용하는데, 이는 속성과 연산의 가시화(접근 제어자)를 정의한다.

가시화 정보는 외부에 속성과 연산을 어느 정도 공개하느냐에 따라 달라진다.

이때, 속성과 연산의 가시화 정보 표시는 필수 사항이 아니다.

속성 및 연산을 기술하는 상황에 따라 강조하는 것이 다를 수 있기 때문이므로, 필요에 따라 적절히 사용하면 된다.

일반적으로 분석 단계에서는 속성의 구체적인 타입 정보, 가시화 정보보다 어떤 것을 속성으로 하는지를 더 중요시한다.

또한 설계 단계에서는 코드 생성이 바로 이루어질 수 있을 정도의 구체적인 타입 정보 및 가시화 정보를 기술하는 것을 요구한다.

아래 그림에서 대괄호 속에 있는 내용은 생략할 수 있는 항목들이다.

아래의 사진은 2가지 형태의 Course 클래스 다이어그램이다.

좌측의 클래스 다이어그램은 구체적인 타입 정보, 가시화 정보를 생략한 분석 단계의 클래스이다.

우측의 클래스 다이어그램은 이를 생략하지 않고 상세하게 표현해준 설계 단계의 클래스이다.

 

2.3) 관계

클래스 1개로 이루어진 시스템은 존재하지 않는다.

설령 만들 수 있다고 해도 그것을 잘 짜인 SW라고 부르지는 않을 것이다.

한 사람이 모든 일을 다 하는 것보다, 여러 사람들이 분담해서 일을 하는 것이 더 효율적이고 빠른 것처럼, 클래스도 여러 개를 모아서 하나의 시스템을 구성하는 것이 훨씬 유리하다.

UML의 클래스 다이어그램도 서로 간의 관계를 표현할 수 있다.

● 연관 관계

2가지 개념이 연관되어 있을 때는 UML 연관 관계를 사용해 표현할 수 있다.

이는 단순하게 두 클래스 다이어그램 박스 사이에 선을 그으면 된다.

만약 "교수가 학생을 상담한다"라는 연관 관계를 표현하고자 한다면 다음과 같이 표현하면 된다.

연관 관계가 명확한 경우에는 굳이 연관 관계 이름을 적을 필요는 없다.

연관 관계를 가진다는 것은 각 클래스의 객체는 해당 연관 관계에서 어떠한 역할을 수행한다는 것이다.

이는 클래스 바로 옆, 연관 관계를 나타내는 선 가까이에 적어줄 수 있다.

예를 들어, 교수는 학생에게 상담을 해주는 조언자의 역할을 할 것이고, 학생은 교수에게 조언을 받는 피 조언자 역할을 하게 될 것이다.

연관 관계의 역할 이름은 연관된 클래스의 객체들이 서로를 참조할 수 있는 속성의 이름으로 활용할 수 있다.

'상담한다'라는 연관 관계는 양방향 연관 관계이다.

양방향 연관 관계는 화살표를 생략한 직선으로 표현한다.

이는 두 클래스의 객체들이 서로의 존재를 인식한다는 의미이다.

두 클래스 사이에 이어져 있는 선에 숫자 표기가 없다면, 이는 1:1 관계라는 의미이다.

즉, 교수 1명에 학생 1명만이 연관되어 있다는 것이다.

하지만 현실적으로 보면 교수 1명이 담당하는 학생들은 매우 많을 것이다.

이러한 1:N 관계를 나타내기 위해서는 선 부근에 숫자를 표기해준다.

이를 다중성이라고 한다.

한 교수가 여러 학생과 연관되어 있으며, 학생들은 한 교수와만 연관될 수 있음을 나타낼 경우 다음과 같다.

연관 관계는 다음과 같이 방향성을 가질 수도 있다.

학생은 자신이 수강하는 과목이 무엇인지를 알지만, 과목은 어떤 학생이 자신을 수강하는지 알 수 없다.

하지만 여기서 다중성 표현을 보면, 과목 1개에 학생 1명만 수강할 수 있는 기이한 관계를 이루고 있음을 알 수 있다.

이를 수정하면 다음과 같다.

양방향 연관 관계는 서로의 존재를 서로가 알고 있음을 의미한다.

단방향 연관 관계는 한쪽은 상대의 존재를 알지만, 다른 한쪽은 상대의 존재를 모른다는 것을 의미한다.

즉, 그림 1-11처럼 구성되면 학생은 과목의 존재를 알지만, 과목은 학생의 존재를 모르는 것이 된다.

여기서 N:N 관계는 양방향 연관 관계가 되므로, 구현하기가 복잡하다.

이를 극복하기 위해 보통 N:N 관계를 1:N 단방향 연관 관계로 변환해서 구현한다.

그림 1-12에서 학생의 성적 정보를 저장해두려고 할 때, 해당 정보는 어디에 저장해야 하는지 알아보자.

일단 그림에서 언급된 Student, Course 클래스에는 이 정보를 둘 수 없다.

성적이라는 정보는 학생과 과목 두 클래스의 객체가 모두 존재할 때에만 의미가 있기 때문이다.

이러한 경우에 사용하는 클래스를 연관 클래스라고 한다.

연관 클래스는 연관 관계에 추가할 속성이나 행위가 있을 때 사용되는 클래스이다.

아래 사진을 보자.

연관 클래스를 표현할 때는 위 사진과 같이 연관 관계를 나타내려고 연결한 실선 중앙에 점선으로 연결하면 된다.

위 예에서 Student 클래스와 Course 클래스, Transcript 클래스를 추출할 수 있다.

Transcript 객체는 Student 객체와 Course 객체를 연관시키는 객체이므로, Student 객체와 Course 객체를 참조할 수 있는 속성을 포함해야 한다.

성적, 과목 개설 연도와 같은 데이터는 Student 클래스, Course 클래스에 속하지 않으며, 두 클래스의 연관 정보이므로 이들도 Transcript 클래스의 속성이어야 한다.

하지만 학생 입장에서 보면, 여러 과목의 성적을 받을 수 있으며, 그 성적의 범위도 A+ ~ F로 다양하다.

이는 즉, Student 클래스와 Transcript 클래스의 연관 관계의 다중성은 1:N이며, Course 클래스와 Transcript 클래스의 다중성도 1:N임을 의미한다.

같은 학생이 같은 과목을 여러 번 수강할 수도 있기 때문에, 주어진 학생과 과목에 여러 개의 성적 정보가 연관될 수도 있다.

그림 1-15는 어떤 연관 관계의 이력을 작성하는 형태의 클래스 다이어그램이다.

클래스 이름 아랫부분에 어떤 연관 관계의 이력을 작성하여 학생의 도서관 대출 이력을 표현한다.

즉, 아래 사진의 왼쪽 연관 클래스는 오른쪽과 같이 일반적인 클래스로 변환되어 구현된다는 것이다.

연관 관계는 때때로 재귀적일 수 있다.

재귀적 연관 관계는 동일한 클래스에 속한 객체들 사이의 관계이다.

예를 들어, 직원이라는 클래스가 존재한다고 가정했을 때, 직원들 중에는 관리자 또는 사원 역할을 맡는 직원들이 있을 것이다.

이를 모델링해보자.

그림 1-16은 간단한 관리자/사원 사이의 관계를 모델링하였지만, 비현실적이다.

관리자 1명이 여러 명의 사원들을 관리하는 것이 현실적이며, 상황에 따라 어떤 사원은 관리자가 없을 수도 있다.

이러한 변수는 예상 밖의 문제를 야기한다.

예를 들어, 'A' 관리자가 'B' 사원을 관리한다고 가정하자.

그런데 'B' 직원도 'A' 직원을 관리하는 관리자라면, 'B' 직원에게는 사원과 관리자 두 클래스에 모두 속하는 모순이 발생한다.

이는 '관리자' 및 '사원' 역할을 클래스로 구현했을 때, 변수에 따른 유연성이 부족할 수 있음을 증명한다.

이럴 때 사용할 수 있는 것이 재귀적 연관 관계다.

여기서 만약 'A' 직원이 'B' 직원을 관리하고, 'B' 직원이 'A' 직원을 관리하는 상황에서, 'C' 직원이 'A' 직원을 관리하는 상황이 나올 수도 있다.

이를 관계의 루프라고 말하는데, 이런 상황을 배제하기 위해서는 연관 관계에 제약을 두어야 한다.

제약은 클래스나 연관 관계를 포함한 UML 모델 요소가 따라야 하는 규칙을 붙여줄 때 사용한다.

제약의 내용은 중괄호 안에 작성하며, 미리 정해진 제약이나 다른 문장을 자유롭게 작성할 수 있다.

그림 1-18에서는 '{계층}'이라는 제약을 설정한다.

이 제약은 객체 사이에 상하 관계가 존재하며, 사이클이 존재하지 않는다는 것이다.

즉, 관리자는 사원을 관리하는 관리자가 될 수 있지만, 사원이 관리자를 관리하는 관리자가 될 수는 없다는 제약을 걸고 있다는 의미이다.

● 일반화 관계

한 클래스(부모)가 다른 클래스(자식)를 포함하는 상위 개념일 때, 이 두 클래스 사이에는 일반화 관계가 존재한다.

일반화 관계가 존재할 때 자식 클래스(또는 서브 클래스)라고 불리는 하위 클래스는 부모 클래스(또는 슈퍼 클래스)라고 불리는 상위 클래스로부터 속성 및 연산을 물려받을 수 있다.

이는 객체지향 개념에서 일반화 관계를 상속 관계라고 부르는 이유이다.

가전제품과 TV, 세탁기, 식기세척기의 관계를 생각해보자.

여기서 가전제품(Machine)을 부모 클래스, TV를 자식 클래스라고 본다.

UML에서는 일반화 관계를 다음 사진과 같이 표현한다.

가전제품은 세탁기, TV, 식기세척기의 공통적인 속성이나 연산을 제공하는 틀로 생각할 수 있다.

이 3가지 가전제품들 모두 제조번호, 제조년도, 브랜드, 제조회사 등과 같은 공통적인 속성을 가지고 있고, 전원 켜기/끄기와 같은 공통적인 연산도 가지고 있을 것이다.

이처럼 공통적인 속성이나 연산들은 가전제품 클래스에서 구현하고 상속을 받아 사용하면 된다.

여기서 주의할 것은 각각의 가전제품 종류마다 전원 켜기/끄기 방법이 다 다를 수 있다는 것이다.

이러한 연산의 구현은 각각의 자식 클래스에서 정의해야 한다.

즉, 부모 클래스인 가전제품 클래스는 연산을 구현하지 않고, 빈 껍데기만 제공하여 자식 클래스에서 연산을 구현한다.

이를 추상 메서드라고 한다.

이 추상 메서드를 1개 이상 가지는 클래스를 추상 클래스라고 부르며, 이는 다른 일반적인 클래스와 달리 객체 생성이 불가능하다.

UML에서는 추상 클래스와 메서드를 이탤릭체로 써서 구분하거나, 스테레오 타입으로 표시한다.

스테레오 타입은 <<, >> 기호 안에 원하는 이름을 넣는데, 그 이름은 키워드라고 부른다. 

 집합 관계

집합 관계는 UML 연관 관계의 특별 경우로, 전체와 부분의 관계를 명확하게 명시하고자 할 때 사용한다.

집약과 합성 2가지 종류의 집합 관계가 존재한다.

집약 관계는 한 객체가 다른 객체를 포함하는 '전체', '부분'과의 관계를 의미한다.

이때 전체 객체의 라이프타임과 부분 객체의 라이프타임은 독립적이다.

즉, 전체 객체가 메모리에서 사라져도, 부분 객체까지 사라지는 것을 의미하지는 않는다.

합성 관계는 전체를 가리키는 클래스 방향에 채워진 마름모로 표시되며, 부분 객체가 전체 객체에 속하는 관계를 의미한다.

즉, 전체 객체가 메모리에서 사라지면, 부분 객체까지 사라지는 것을 의미한다.

공유할 수 있는 객체를 사용할 때는 합성 관계가 아닌 집약 관계를 사용한다.

이때 부분 객체의 라이프타임은 전체 객체의 라이프타임에 의존한다.

즉, 집약 관계와 합성 관계를 구분하려면, 전체 객체와 부분 객체의 라이프타임 의존성을 살펴보면 된다.

이러한 집합 관계를 그림으로 표현하면 다음과 같다.

// 코드 1-2

public class Computer {
    private MainBoard mb;
    private CPU cpu;
    private Memory ram;
    private PowerSupply power;
    
    public Computer() {
        this.mb = new MainBoard();
        this.cpu = new CPU();
        this.ram = new Memory();
        this.power = new PowerSupply();
    }
}

위 코드에서 생성자가 컴퓨터 부품이 되는 Computer 객체를 생성해서 바인딩하고 있다.

즉, 아래와 같이 Computer 객체가 1개 생성되면, 해당 객체의 부품들을 이루는 하위 객체들이 생성자 안에서 바인딩된다.

c1 = new Computer();

또한 그렇게 생성된 Computer 객체의 부품들은 c1이 사라질 때 함께 사라진다.

즉, 합성 관계를 띄고 있는 것이다.

이제 이를 다이어그램으로 나타내 보자.

// 코드 1-3

public class Computer {
    private MainBoard mb;
    private CPU cpu;
    private Memory ram;
    private PowerSupply power;
    
    public Computer(MainBoard mb, CPU cpu, Memory ram, PowerSupply power) {
        this.mb = mb;
        this.cpu = cpu;
        this.ram = ram;
        this.power = power;
    }
}

위 코드는 집약 관계로 짜인 코드이다.

즉, Computer 객체가 사라져도, 부품을 구성하는 객체들은 사라지지 않는다.

그 이유는 생성자의 매개변수를 통해 그저 참조만 해서 구현하였기 때문이다.

이를 다이어그램으로 표현해보자.

 의존 관계

일반적으로 한 클래스가 다른 클래스를 사용하는 경우는 다음과 같이 3가지가 있다.

  • 클래스의 속성에서 참조할 때
  • 연산의 인자로 사용할 때
  • 메서드 내부의 지역 객체로 참조될 때

한 클래스의 객체를 다른 클래스 객체 속성에서 참조하는 경우, 참조하는 객체가 변경되지 않는 한 두 클래스의 객체들이 오랜 기간 동안 협력 관계를 통해 기능을 수행한다고 볼 수 있다.

예를 들어, 자동차(Car 클래스)를 소유한 사람(Person 클래스)이 자동차로 출근을 한다고 가정하자.

이때 사람과 자동차는 연관 관계이며, Person 클래스의 속성으로 Car 객체를 참조한다.

자동차와 주유기(GasPump 클래스)의 관계를 살펴보면 이와 또 다르다.

자동차 주유는 특정 주유소의 특정 주유기만 고집할 수 없다.

이는 즉, 상황에 따라 사용하는 주유기의 종류가 매번 달라지는 것을 의미한다.

이를 OOP로 표현하면, 아래 코드와 같이 주유기를 인자 또는 지역 객체로 생성해 구현해야 할 것이다.

public class Car {
    
    // CODE //
    
    public void FillGas(GasPump pump) {
        pump.getGas(amount);
    }
}

UML에서 이러한 의존 관계는 점선으로 표현한다.

아래 사진을 보자.

 인터페이스와 실체화의 관계

인터페이스는 책임이다.

어떤 객체의 책임은 해야 하는 일로서, 필수적으로 처리해야 하는 작업을 의미한다.

실제로 인터페이스를 사용할 때, 메서드의 실체를 구현해주지 않으면 에러를 내뿜는 것을 볼 수 있다.

즉, 객체가 외부에 제공하는 서비스 및 기능들은 수행하는 책임으로 취급하는 것이다.

예를 들어 TV 리모컨은 TV 전원, 볼륨 제어 등의 책임을 수행해야 한다.

TV 리모컨만이 아니라 형광등 스위치, 수도꼭지도 이러한 책임이 존재한다.

인터페이스를 어떤 공통되는 능력이 있는 것을 대표하는 관점으로도 볼 수 있다.

예를 들어 비행기, 새는 공통적으로 날 수 있다.

이러한 공통적인 능력을 기준으로 함께 그룹화할 수 있는 메커니즘이 인터페이스이다.

여기서 주의할 점은 인터페이스 자체가 실제로 책임을 수행하는 객체가 아니라는 것이다.

실제로 책임을 수행하는 것은 Plane과 Bird와 같은 것이며, 인터페이스는 이들이 수행해야 하는 책임이 무엇인지를 정의할 뿐이다.

따라서 책임과 이를 실제로 실현하는 클래스는 관계를 분리해서 보여주어야 한다.

아래 사진은 Bird 클래스와 Plane 클래스를 Flyable 인터페이스 실체화 관계를 간단하게 표현한 것이다.

일반화 관계는 'is a kind of' 관계이지만, 실체화 관계는 'can do this' 관계이다.

인터페이스는 작은 원으로, 클래스와 인터페이스는 실선으로 연결한다.

 


 


수고하셨습니다!


'Dev. Study Note > Design Pattern' 카테고리의 다른 글

6. 싱글턴 패턴  (0) 2022.07.03
5. 스트래티지 패턴  (0) 2022.07.03
4. 디자인 패턴  (0) 2022.07.03
3. SOLID 원칙  (0) 2022.07.03
2. 객체지향 원리  (0) 2022.07.03
Comments