본문 바로가기
알아두면쓸데있는신기한잡학사전/고군분투흔적들

[Design Pattern] 연관 관계

by 대범하게 2021. 4. 14.
반응형

연관 관계

 

연관된 클래스 상에 실선을 그어 표시.

'교수(Professor 클래스)가 학생(Student 클래스)을 상담한다'라는 사실은 다음과 같이 나타낸다.

두 클래스 사이의 관계가 명확한 경우에 연관 관계 이름('상담한다')을 사용하지 않아도 됨.

한 클래스가 다른 클래스와 연관관계를 가지면 각 클래스의 객체는 해당 연관 관계에서 어떤 역할을 수행하게 된다. 

이러한 역할은 클래스 바로 옆 연관 관계를 나타내는 선 가까이에 적을 수 있다. 

Professor 객체들은 조언자(advisor 속성)의 역할을,

Student 객체들은 피조언자(student 속성)의 역할을 '상담한다'라는 연관 관계에서 담당한다.

역할 이름은 실제 프로그램을 구현할 때 연관된 클래스의 객체들이 서로를 참조할 수 있는 속성의 이름(변수)으로 활용할 수 있다. 

 

Checkpoint_

class Professor{
	private Student student;
} //Professor 클래스는 student 변수를 가지고 있다. student 변수의 타입은 Student 클래스이다.

class Student{
	private Professor advisor;
} //Student 클래스는 advisor 변수를 가지고 있다. advisor 변수의 타입은 Professor 클래스이다.

 

양방향 연관 관계

'상담한다' 연관 관계는 양방향 연관 관계다. 양방향 연관 관계는 UML에서 두 클래스를 연결한 선에 화살표를 사용하지 않는다. 즉, 두 클래스의 객체들이 서로의 존재를 인식한다는 의미다. 

// 메서드 다시 보기
class Professor{
	private Student student;
	
	public void setStudent(Student student) {
		this.student = student;
		student.setAdvisor(this);
	}
	
	public void advise() {
		student.advise("상담 내용은 여기에...");
	}
} 

class Student{
	private Professor advisor;
	
	public void setAdvisor(Professor advisor) {
		this.advisor = advisor;
	}
	
	public void advise(String msg) {
		System.out.println(msg);
	}
} 

public class Main {
	public static void main(String[] args) {
		Professor hongGilDong = new Professor();
		Student manSup = new Student();
		hongGilDong.setStudent(manSup);
		hongGilDong.advise();
	}

}

 

다중성 표시

두 클래스 사이의 연관관계를 나타내는 선에 아무런 숫자가 없으면 연관 관계가 일대일 관계임을 나타낸다.

즉, 교수 한 명에 학생 한 명만 연관되어 있다. 실제로 학생은 각각은 한 명의 교수와만 연관되어 있지만 일반적으로 교수 한 명을 매우 많은 학생을 상담한다. 

 

이를 나타내기 위해 연관된 객체(Studnet 객체)수를 연관된 Professor 클래스와 연결한 선 부근에 명시한다.

다중성 표기 의미
1 엄밀하게 1
* or 0..* 0 또는 그 이상
1..* 1 이상
0..1 0 또는 1
2..5 2 또는 3 또는 4 또는 5
1, 2, 6 1 또는 2 또는 6
1, 3..5 1또는 3 또는 4 또는 5

 

다음은 한 교수에 여러 학생이 연관되어 있다는 사실을 나타낸다. 그러나 여전히 한 학생은 한 교수와만 연관을 맺을 수 있다. 

즉, Professor가 1개 이상의 Studnet 객체를 변수로 가지고 있다.

 

단방향 연관 관계

연관 관계는 방향성을 가질 수 있다.

예를 들면, 다음 그림의 '수강하다' 연관 관계는 Student 클래스에서 Course 클래스로 향하도록 되어있다.

이는 학생(Student 객체)은 자신이 수강하는 과목(Course 객체)을 알지만 과목은 자신을 수강하는 학생들의 존재를 모른다는 사실을 의미함.

이렇게 한쪽으로만 방향성이 있는 연관 관계를 단방향 연관 관계라 한다. 

따라서 Student 객체는 Course 객체(들)를 참조할 수 있도록 구성해야 하지만 Course 클래스는 Student 객체를 참조할 속성이 존재하지 않아도 된다. 

import java.util.Vector;

class Student{
	private String name;
	private Vector<Course> courses;
	
	public Student(String name) {
		this.name = name;
		courses = new Vector<Course>();
	}
	
	public void registerCourse(Course course) {
		courses.add(course);
	}
	
	public void dropCourse(Course course) {
		if(courses.contains(course)) {
			courses.remove(course);
		}
	}
	
	public Vector<Course> getCourses(){
		return courses;
	}
} 

class Course{
	private String name;
	
	public Course(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
}

이 코드는 Student 객체 하나에 하나 이상의 Course 객체가 연관되어 있기 때문에 다중성을 구현했으며, Student 클래스에 대표적 컬렉션 자료구조인 Vector를 이용해 여러 개의 Course 클래스 객체를 참조할 수 있게 했다.

컬렉션 자료구조에는 Vector 외에도 Set, Map, ArrayList 등 여러 가지가 있으므로 상황에 맞는 적절한 자료구조를 선택해 사용하면 된다. 

 

또한 이 코드는 Student 클래스에서 Course 클래스로 향하는 단방향 연관 관계이기 때문에 Course 클래스에는 Student 객체를 참조하는 속성이 정의되어 있지 않다.

 

Course 클래스 객체 하나에 Student 객체 하나만 연관되기를 원한다.

이는 한 과목에 한 명의 학생만 수강해야 한다는 이상한 관계가 된다.

이를 여러 명의 학생이 수강할 수 있는 일반적인 수강 관계로 개선하려면 다음과 같이 다중성을 수정할 필요가 있다. 

 

다대다 연관 관계

다중성을 수정하면서 단방향 연관 관계가 양방향 연관 관계로 변했다. 일반적으로 다대다 연관 관계는 단방향 연관관계가 아닌 양방향 연관관계로 표현되는 것이 적절하다.

public class Student{
	private String name;
	private Vector<Course> courses;
	
	public Student(String name) {
		this.name = name;
		courses = new Vector<Course>();
	}
	
	public void registerCourse(Course course) {
		courses.add(course);
		course.addStudent(this);
	}
	
	public void dropCourse(Course course) {
		if(courses.contains(course)) {
			courses.remove(course);
			course.removeStudnet(this);
		}
	}
	
	public Vector<Course> getCourses(){
		return courses;
	}
} 
public class Course{
	private String name;
	private Vector<Student> students;
	
	public Course(String name) {
		this.name = name;
		students = new Vector<Student>();
	}
	
	public void addStudent(Student student) {
		students.add(student);
	}
	
	public void removeStudnet(Student student) {
		students.remove(student);
	}
	
	public Vector<Student> getStudents(){
		return students;
	}
	
	public String getName() {
		return name;
	}
}

Student 클래스와 Course 클래스의 연관 관계가 양방향 연관 관계이기 때문에 양쪽 클래스에서 서로를 참조할 수 있는 속성을 정의함.

또한 다중성이 다대다이므로 참조 속성은 Vector를 이용함.

 

 

연관 클래스

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

다음 그림처럼 연관 관계가 있는 두 클래스 사이에 위치하며, 연관 관계를 나타내려고 연결하는 선 중앙에서 연관 클래스까지 점선을 사용해 연결한다.

 

Q. 연관 클래스는 어떻게 구현해야 할까?

- 홍길동은 2013년에 개설한 소프트웨어 공학에서 A+를 받았다.

- 홍길서는 2013년에 개설한 소프트웨어 공학에서 C를 받았다.

- 홍길동은 2013년에 개설한 디자인패턴에서 A를 받았다.

- 홍길서은 2013년에 개설한 디자인패턴에서 B를 받았다.

- 홍길남은 2013년에 개설한 데이터베이스에서 B+를 받았다.

- 홍길동은 2012년에 개설한 디자인패턴에서 D를 받았다.

 

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

Transcript 객체는 Student 객체와 Course 객체를 연관시키는 객체이므로 Student 객체와 Course 객체를 참조할 수 있는 속성을 포함해야 한다. 또한 성적과 과목 개설년도와 같은 데이터는 Student 클래스나 Course 클래스에 속하지 않으며 두 클래스의 연관 정보이므로 이들도 Transcript클래스의 속성이어야한다.

 

그러나 학생 입장에서는 여러 과목의 성적을 받을 수 있고 마찬가지로 한 과목에서도 A+~F라는 여러 가지 성적이 산출된다. 따라서 Student 클래스와 Transcript 클래스의 연관 관계의 다중성은 일대다이며 Course 클래스와 Transcript 클래스의 다중성도 일대다가 된다. 

 

이를 기반으로 다시 표현한 것이다.

재귀적 연관 관계

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

예를 들어 직원이라는 클래스를 생각해보면 직원들 중에는 관리자 역할을 하는 직원도 있고 사원 역할을 하는 직원도 있다. 이를 간단하게 모델링하면 다음과 같다. 

역할을 클래스로 할 때 문제가 발생!!!!!

그런데 현실에선 관리자 한 명이 여러 명의 사원들을 관리한다.

물론 때로는 관리해야 하는 사원이 전혀 없는 관리자가 있을 수도 있다. 역으로 어떤 사원은 관리자가 없을 수도 있다.

그래서 문제가 발생한다.

 

만약 '홍길동' 관리자가 '홍길서' 사원을 관리한다고 하고, '홍길서'도 '홍길남'을 관리하는 관리자라면 '홍길서' 사원이라는 객체는 '관리자'와 '사원'이라는 두 클래스에 동시에 속하는 모순이 발생한다.

 

즉, '관리자'와 '사원' 역할을 클래스로 만들면 시스템이 변화할 때 유연성이 부족할 수 있으므로 가급적 역할을 클래스로 만들지 않는 것이 좋다. 그래서 생긴 것이 재귀적 연관 관계다.

 

그런데 위 그림과 같이 재귀적 연관 관계로 모델링해도 문제는 여전히 남아있다. 

만약 '홍길동'이 '홍길서'를 관리하고 '홍길서'가 '홍길남'을 관리하는 상황에서 '홍길남'이 '홍길동'을 관리하는 상황도 있을 수 있기 때문이다. 이를 '관계의 루프'라고 하는데, 이런 상황을 배제하려면 제약을 설정해야 한다.

 

제약은 '{}'안에 미리 정해진 제약뿐만 아니라 어떤 문장도 자유롭게 쓸 수 있으며 클래스나 연관 관계를 포함한 UML 모델 요소가 따라야 하는 규칙을 붙여줄 때 사용한다.

 

'{계층}'이란 객체 사이에 상하 관계가 존재하고 사이클이 존재하지 않는다는 의미이다.


Checkpoint_ 다음 클래스 다이어그램을 코드로 작성한다.

연관 클래스를 일반 클래스로 변환한 예

class Person{
	private Phone[] phones;
	
	public Person{
		phones = new Phone[2];
	}
	
	public Phone getPhone(int i) {
		if(i == 0 || i == 1)
			return phones[i]; //i=0 이면 집 전화, ㅑ=1이면 사무실 전화
		
		return null;
	}
}

이 코드는 집 전화와 사무실 전화를 배열의 인덱스를 통해 구분해야 하므로 매우 불편하다. 이런 경우는 전화기의 역할을 구분해서 사용하면 해결할 수 있다. 

 

(homePhones에 s 오타!)

public class Person{
	private Phone homePhone;
	private Phone officePhone;  
	
	public void setHomePhone(Phone phone) {
		this.homePhone = phone;
	}
	
	public void setOfficePhone(Phone phone) {
		this.officePhone = phone;
	}
	
	public Phone getHomePhone(){
		return homePhone;		
	}
	
	public Phone getOfficePhone() {
		return officePhone;
	}
}

이 코드는 집 전화와 사무실 전화 각각에 참조가 이루어지므로 setter와 getter 메서드로 상황에 맞게 원하는 전화기를 사용할 수 있다. 

 

※ setter와 getter에 대해 알아보쟈!

//게터(getter)는, private 필드를 우회적으로 접근(read)하게 합니다.

class SmartPhone{
	//private 필드 - 외부 접근 불가
	private int number;
	
	//게터 메소드 - number를 우회하여 반환
	public int getNumber() {
		return number;
	}
}
//세터(setter)는, private 필드를 우회적으로 변경(write)하게 합니다.
class SmartPhone{
	//private 필드 - 외부 접근 불가
	private int number;
	
	//게터 메소드 - number를 우회하여 반환
	public int setNumber(int n) {
		number = n;
	}
}

 

반응형