클래스란?

클래스는 동일한 속성과 행위를 수행하는 객체들의 집합이다. 예를 들어 소프트웨어를 전공중인 학생들의 공통점은 소프트웨어를 전공한다는 사실과 동일한 전공 수업을 듣는다는 점이다. 이러한 경우 소프트웨어 전공 중인 학생은 실제 학생들의 클래스라고 말할 수 있다.

클래스를 정의하는 또 다른 관점은 인스턴스(객체)를 생성하는 설계도로 보는 것이다. 아래 소스코드는 소프트웨어를 전공하는 학생 클래스를 정의한 것이다. 클래스가 객체를 생성하는 설계도라는 관점에서 코드를 한 번 살펴보자

public class Student{
    private String name;
    private String major = "SW";

    public Student(String name){
        this.name = name;
    }

    public void study() {
        System.out.println("객체 지향 모델링 수업을 수강합니다.");
    }
}
Student student1 = new Student("학생1");
Student student2 = new Student("학생2");

student1.study();
student2.study();

위 코드를 통해서 같은 스펙을 가진 학생 객체가 두 개 생겨났다. 두 객체는 이름을 제외하면 모두 동일한 특성을 가진다. major 속성은 SW로 동일하며 study() 메소드를 실행하면 동일한 문장이 출력된다. 이것이 바로 클래스가 객체를 생성하는 설계도인 이유이다.

동일한 설계도에서는 항상 동일한 객체가 생성된다.

UML 모델링

student-uml-class

UML을 이용해서 Student 클래스를 표기한 결과는 위와 같다. 가장 위에서부터 구획별로 클래스명(ClassName), 속성(Property), 연산(Operation)을 기술한다. 만약 속성이나 연산이 없다면 생략할 수 있다.

접근 제어자

접근 제어자 표시 설명
public + 어떤 클래스의 객체든 접근 가능
private - 이 클래스에서 생성한 객체들만 접근 가능
protected # 이 클래스와 동일 패키지에 있거나, 상속 관계에 있는 하위 클래스의 객체들만 접근 가능
package ~ 동일 패키지에 있는 클래스의 객체들만 접근 가능

클래스의 속성과 연산을 정의할 때 -+와 같은 기호를 사용하는데, 이것은 가시화를 정의하는 접근 제어자이다. 자바에서의 privatepublic를 위와 같이 표기한다고 생각하면 된다.

속성과 연산

구분 표기법
속성 [접근 제어자]이름: 타입[다중성 정보] [=초기값]
연산 [접근 제어자]이름(인자: 타입): 리턴 타입

속성과 연산을 표기하는 형식은 위와 같다. 클래스 다이어그램은 개념 분석 단계에서 구현에 이르기까지 광범위하게 사용되는데, 분석 단계에서는 속성이나 연산을 구체적으로 표현하기 보다는 정의하는 것이 주를 이룬다. 이후 설계 단계에서 구체적인 타입 정보나 가시화 정보를 기술하게 된다.

[] 안에 들어있는 것은 생략해도 괜찮다는 의미이다.

관계

객체 지향 프로그래밍에서 객체 하나만을 사용하는 경우는 드물다. 보통 기능별로 객체를 나누어 지고 이들의 상호작용이 하나의 소프트웨어를 동작하게 한다. 이러한 클래스 간의 관계를 UML에서는 아래와 같이 표현한다.

연관 관계

클래스들이 개념상 연결되어 있음을 의미하며 실선으로 표시한다.

uml-association-notation

양방향 연관 관계

교수와 학생 클래스의 연관 관계와 같이 서로를 인식하는 경우를 양방향 연관 관계라고 하며, 화살표 없는 실선으로 표기한다.

professor-student-relation

만약 이들이 상담한다는 것을 나타내고 싶다면 위와 같이 실선 상단에 명시해주면 된다. 연관 관계에서의 역할 이름(rule name)도 정할 수 있는데 실선의 양 끝단에 정의해주면 된다. 이것은 이후 프로그램을 구현하는 단계에서 서로를 참조하는 속성으로 활용될 수 있다.

단방향 연관 관계

학생 한 명은 여러 수업을 수강할 수 있다. 이것을 UML을 통해서 모델링한다면 다음과 같이 표기할 수 있다.

student-course-relation

이때 화살표는 학생에서 수업으로 향하는데 이는 학생이 수업을 인식하고 있음을 의미하며, 반대로 수업은 학생을 인식하지 못한다. 이러한 경우를 단방향 연관 관계라고 한다.

위 다이어그램에는 1..*이라는 표기가 있는데 이것은 다중성을 나타낸 것이다. 다중성은 연관되어 있는 객체의 수를 의미한다.

*0 이상을 의미하며, ..은 범위를 나타낸다. 따라서 1..*1 이상이라는 의미이다. 객체가 하나인 경우는 생략하기도 한다.

다대다 연관 관계

앞선 예시를 잘 생각해보면 뭔가 이상하다는 사실을 깨닫을 수 있다. 실세계에서는 한 명의 학생만이 수업을 수강하는 경우는 없다. 보통은 다수의 학생이 다수의 수업을 수강한다. 이것을 표현하면 아래와 같은 그림이 나올 것이다.

many-to-many-relation

이렇게 다수의 객체 - 다수의 객체가 가지는 관계를 다대다 연관 관계라 하며, 이것은 일반적으로 UML에서 양방향 연관 관계로 표현된다.

association-class

그런데 만약에 학생이 수업을 수강하면서 발생하는 성적 정보를 저장하고 싶다면 어디에 저장해야 할까? 학생이나 수업 클래스에 그대로 성적 정보를 저장한다면 다음과 같이 표현될 것이다.

홍길동 학생이 A+이다 또는 객체 지향 모델링 수업에서 A+을 받았다

다만 여기에는 어떤 수업에서 누가 해당 성적을 얻었는가에 대한 정보가 빠져 있다. 따라서 학생 성적은 학생이나 수업 클래스에 저장하는 것보다는 별도의 클래스를 만들어 저장하는 것이 옳다. 이때 사용되는 Transcript와 같은 클래스를 연관 클래스라 한다.

association-class-to-general-class

연관 클래스의 실제 구현은 일반 클래스단방향 연관 관계로 변환되어 이루어진다.

실제로 프로그램을 구현할 때, 양방향 연관 관계는 사용되지 않는다!

재귀적 연관 관계

superior-subordinate-relation

연관 관계는 때로는 재귀적이다. 예를 들면 군대에는 선임후임이라는 관계가 존재한다. 내게 선임인 군인도 누구에게는 후임이며, 내게 후임인 군인도 누군가에겐 선임이다.

recursive-association

이러한 경우 군인이라는 클래스는 선임과 후임이라는 두 클래스에 동시에 속하는 모순이 발생한다. 하지만 그렇다고 해서 두 클래스를 별도로 만드는 것은 유연성이 부족하다. 이러한 경우 재귀적 연관 관계가 사용된다.

하지만 재귀적 연관 관계에는 관계의 루프라는 문제가 남아 있다. 예를 들어 가위바위보는 가위가 보를 이기고 보는 바위를 이기고 바위는 가위를 이기는 게임이다. 이렇게 루프가 존재하는 경우는 {계층}으로 제약을 설정하여 배제해야만 한다.

{계층}은 객체 사이에는 상하 관계가 존재하며, 사이클이 존재하지 않음을 의미한다.

일반화 관계

한 클래스가 다른 클래스를 포함하는 상위 개념일 때 두 클래스 간의 관계이다.

uml-generalization-notation

자식 클래스(서브 클래스)부모 클래스(슈퍼 클래스)로부터 속성이나 연산을 물려 받을 수 있다. 그렇기 때문에 일반화 관계를 상속 관계라고도 한다.

home-appliance-inheritance

보통 일반화 관계는 is-a-kind-of 관계라고 말한다. 가전 제품과 세탁기의 관계는 세탁기 is-a-kind-of 가전 제품라고 설명할 수 있다.

집합 관계

집합 관계는 연관 관계의 특별한 경우로 전체와 부분의 관계를 명확하게 명시하고자 할 때 사용한다. 집약(aggregation)합성(composition) 두 종류의 집합 관계가 존재한다.

집약 관계

한 객체가 다른 객체를 포함하는 것을 나타낸다.

uml-aggregation-notation

  • 전체를 가리키는 클래스 방향에 빈 마름모 표시
  • 부분 객체를 다른 객체와 공유할 수 있음
  • 전체 객체와 부분 객체의 라이프타임은 독립적

합성 관계

부분 객체가 전체 객체에 속하는 관계이다.

uml-composition-notation

  • 전체를 가리키는 클래스 방향에 채워진 마름모 표시
  • 부분 객체를 다른 객체와 공유할 수 없음
  • 부분 객체의 라이프타임은 전체 객체에 의존

차이점

집약 관계와 합성 관계는 얼핏보면 비슷해 보이지만 큰 차이를 가지고 있다. 가장 중요한 차이는 라이프타임이다. 전체 객체가 소멸되었을 때 부분 객체가 남아 있다면 그것은 집약 관계이다. 반대의 경우는 합성 관계라고 생각하면 된다.

컴퓨터를 조립하는 예시를 가지고 이 둘의 차이를 알아 보겠다.

computer-components-aggregation

public class Computer {
    private MainBoard mainBoard;
    private CPU cpu;
    private Memory memory;
    private PowerSupply powerSupply;

    public Computer(MainBoard mainBoard, CPU cpu, Memory memory, PowerSupply powerSupply){
        this.mainBoard = mainBoard;
        this.cpu = cpu;
        this.memory = memory;
        this.powerSupply = powerSupply;
    }
}

컴퓨터 객체는 외부에서 만들어진 메인보드, CPU, 메모리, 파워서플라이를 받아서 생성된다. 따라서 컴퓨터 객체가 소멸되더라도 컴퓨터를 구성하는 부분 객체들은 사라지지 않고 메모리에 남아 있게 된다. 따라서 위 소스코드는 집약 관계를 나타낸 것이라는 사실을 알 수 있다.

computer-components-composition

public class Computer {
    private MainBoard mainBoard;
    private CPU cpu;
    private Memory memory;
    private PowerSupply powerSupply;

    public Computer(){
        this.mainBoard = new MainBoard();
        this.cpu = new CPU();
        this.memory = new Memory();
        this.powerSupply = new PowerSupply();
    }
}

이전 예시와 달리 위 코드는 컴퓨터를 생성하는 동시에 구성품들이 생성된다. 따라서 해당 요소들의 생명주기는 전체 객체에 의존하게 된다. 컴퓨터가 소멸되는 동시에 부분 객체들도 사라지기 때문에 합성 관계라고 볼 수 있다.

의존 관계

다른 클래스에서 제공하는 기능을 사용할 때 나타나는 관계이다.

uml-dependency-notation

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

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

dependency-relation

사람이 차를 소유하고 있고, 이 차는 주유소에서 충전한다는 것을 다이어그램으로 나타내면 위와 같다. 이때, 사람-차 간의 관계는 연관 관계이지만 차-주유소 간의 관게는 의존 관계이다.

public class Person {
    private Car car;

    public void setCar(Car car){
        this.car = car;
    }
    ...
}

public class Car {
    public void fillGas(GasPump p){
        p.getGas(amount);
        ...
    }
}

사람이 타고 다니는 차는 매번 변화하는 것이 아니므로 Person클래스의 속성으로 Car객체를 참조한다. 반면 차를 주유하는 주요소는 매번 같지 않으므로 인자지역 객체를 통해 구현한다.

실체화 관계

인터페이스와 이것의 책임들을 실체화한 클래스 간의 관계이다.

책임이란 객체가 해야 하는 일 내지 할 수 있는 일을 말한다.

uml-realization-notation

인터페이스 자체는 실제로 책임을 수행하는 객체가 아니다. 실체화를 통해서 만들어진 객체가 인터페이스에 정의된 책임을 수행하게 된다.

flyable-interface

예를 들어 날기 위한 책임을 담은 Flyable이라는 인터페이스를 실체화한 PlaneBird 클래스는 해당 인터페이스의 책임을 구현해야만 한다. 이러한 점에서 인터페이스는 어떤 공통되는 능력이 있는 것들을 대표한다는 관점으로 볼 수도 있다. 그렇기 때문에 실체화 관계를 can-do-this 관계라고 부른다.

참고문헌