개발자 끄적끄적
클래스 다이어그램 정리 본문
1. Association(연관)
- 클래스 간의참조 관계
- A->B는 A가 B를 참조한다는 의미
- ex)
class Engine { };
class Car {
public :
void setEngine(Engine *newEngine { engine = newEngine; }
private :
Engine *engine;
};
- Car class는 Engine 클래스를 참조하고 있지만 두 클래스 사이에는 연관 관계가 있지만 포함관계는 아니다
2. Inheritance(상속)
- ex)
class Vehicle{
public:
void drive() { ... }
};
class Car : public Vehicle{
public:
void honk() { ... }
};
- Car 클래스는 Vehicle 클래스를 상속받는다. 따라서 Car 클래스는 자신의 메서드인 honk()와 함께 Vehicle클래스의
drive() 메서드도 사용할 수 있다
3. Realization / Implementation(구현)
- 인터페이스와 그 인터페이스를 구현하는 클래스 간의 관계를 나타낸다
- 인터페이스는 메스드 시그니처만 제공하며, 해당 메서드 구현은 인터페이스를 구현하는 클래스가 담당한다
- ex)
class Drivable{
public:
virtual void drive() = 0;
virtual void stop() = 0;
};
class Car : public Drivable {
public:
void drive() override { ... }
void stop() override { ... }
};
- Drivable 인터페이스는 drive()와 stop() 두 개의 함수를 정의하고 있다. Car 클래스는 Drivable 인터페이스를 구현하며, 이를 통해 drive()와 stop 메서드에 대한 구현을 제공한다
4. Dependency(의존)
- 한 클래스가 다른 클래스에 의존하는 관계이다
- 일반적으로 메스드 내에서 다른 클래스를 사용할 때 발생한다
- Association 관계는 해당 클래스의 멤버 변수로 할당할 때 사용하고 Dependency는 관계는 로컬 변수, 파라미터, 반환 값으로 호출되는 메소드가 실행되는 동안에만 유지가 될 떄 사용한다
- ex)
class Engine { };
class Car{
public:
void startEngine(Engine &engine) { ... }
};
- Car 클래스의 startEngine 메서드는 Engine 클래스의 객체를 사용하므로, Car 클래스는 Engine 클래스에 의존한다
5. Aggregation(집합)
- 전체 클래스와 부분 클래싀 사이의 약한 포함 관계를 나타낸다
- 전체 클래스가 사라져도, 부분 클래스는 독립적으로 존재할 수 있다
- ex)
class Wheel { };
class Car{
public:
void setWheel(Wheel *newWheel) {wheel = newWheel;}
private:
Wheel *wheel;
};
- Car 클래스는 Wheel을 클래스를 포함하지만 Car 객체가 소멸되어도 Wheel 객체는 독립적으로 존재할 수 있다
6. Composition(합성)
- 전체 클래스(whole)와 부분 클래스(part)사이의 강한 포함관계를 나타낸다
- 전체 클래스가 사라지면 부분 클래스도 함께 사라진다
- ex)
class Engine{ };
class Car {
public:
Car() : engine(new Engine()) { }
~Car() {delete engine;}
private:
Engine *engine;
};
- Car 클래스가 Engine 클래스를 포함하고 있다. Car 객체가 소멸될 때 Engine 객체도 함께 소멸된다
<객체지향 설계 5원칙>
1. SRP(Single responsibility principle)
- 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야 한다
- ex)
// SRP 준수 예제
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void calculateSalary() {
// 급여 계산 로직
}
}
class ReportGenerator {
public void generateReport(Employee employee) {
// 보고서 생성 로직
}
}
- 여기서 Employee 클래스는 급여 계산과 관련된 책임만을 가지며, 보고서 생성은 별도의 ReportGenerator 클래스로 분리되었습니다.
이렇게 하면 각 클래스는 하나의 책임만 갖게 되어 SRP를 준수하게 됩니다.
2. OCP(Open/closed principle)
- 개방 폐쇄 원칙 : 소프트웨어요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다
- ex)
// 도형을 그리는 인터페이스
interface Shape {
void draw();
}
// 원 클래스
class Circle implements Shape {
public void draw() {
System.out.println("원을 그립니다.");
}
}
// 사각형 클래스
class Rectangle implements Shape {
public void draw() {
System.out.println("사각형을 그립니다.");
}
}
// 그림 그리는 클래스
class Drawing {
public void drawShape(Shape shape) {
shape.draw();
}
}
- 위의 코드에서 Shape 인터페이스를 구현한 클래스들(Circle과 Rectangle)은 확장에 열려 있습니다. 이것은 새로운 도형 클래스를 추가할 때 기존 코드를 수정하지 않고도 가능합니다.
Drawing 클래스는 Shape 인터페이스를 사용하여 도형을 그리는데, 이것은 기존 코드를 수정하지 않고 새로운 도형을 추가할 수 있음을 보여줍니다.
- 예를 들어, 삼각형 클래스를 추가하려면 다음과 같이 할 수 있습니다.
// 삼각형 클래스
class Triangle implements Shape {
public void draw() {
System.out.println("삼각형을 그립니다.");
}
}
- 이렇게 하면 Triangle 클래스를 추가하더라도 Drawing 클래스나 기존 도형 클래스를 수정할 필요가 없으므로 OCP를 준수하고 있습니다.
3. LSP(Liskov substitution principle)
- 리스코프 치환 원칙 : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다
- ex)
class Bird {
public void fly() {
System.out.println("날아갈 수 있습니다.");
}
}
class Sparrow extends Bird {
// Sparrow는 Bird를 확장하면서 추가적인 행위를 정의하지 않음
}
class Ostrich extends Bird {
public void fly() {
// 타조는 날지 못하므로 오버라이딩하여 구현을 변경
System.out.println("날지 못합니다.");
}
}
public class Main {
public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird ostrich = new Ostrich();
sparrow.fly(); // "날아갈 수 있습니다." 출력
ostrich.fly(); // "날지 못합니다." 출력
}
}
- 위의 코드에서 Bird 클래스는 fly 메서드를 가지고 있으며, Sparrow 클래스는 이 메서드를 오버라이딩하지 않고 상속합니다. 반면에 Ostrich 클래스는 fly 메서드를 오버라이딩하여 새로운 구현을 제공합니다. 그러나 Sparrow와 Ostrich 모두 Bird 타입의 객체로 대체할 수 있으며, 프로그램은 정상적으로 작동합니다.
이것이 Liskov 치환 원칙을 따르는 예제입니다.
4. ISP(Interface segregation principle)
- 인터페이스 분리원칙 : 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보단 낫다
- ex)
// ISP 위반 예제
interface Worker {
void work();
void eat();
}
class Human implements Worker {
public void work() {
// 일하는 로직
}
public void eat() {
// 식사하는 로직
}
}
class Robot implements Worker {
public void work() {
// 일하는 로직
}
public void eat() {
// 로봇은 먹지 않는데도 먹는 메서드를 구현해야 함
}
}
- 위의 코드에서 Worker 인터페이스는 두 가지 메서드인 work와 eat을 가지고 있습니다. 그런데 Robot 클래스는 eat 메서드를 구현해야 하는데, 로봇은 먹지 않으므로 이는 ISP를 위반하는 예제입니다.
- ex) ISP를 준수하는 코드
// ISP 준수 예제
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() {
// 일하는 로직
}
public void eat() {
// 식사하는 로직
}
}
class Robot implements Workable {
public void work() {
// 일하는 로직
}
}
- 위의 코드에서 Worker 인터페이스를 Workable과 Eatable로 분리했습니다.
이렇게 하면 클라이언트는 자신이 필요로 하는 메서드만을 구현하면 되므로 ISP를 준수합니다.
Robot 클래스는 Workable 인터페이스만 구현하면 되므로 eat 메서드를 구현할 필요가 없습니다.
5. DIP(Dependency inversion principle)
- 의존관계 역전 원칙 : 추상화에 의존하고 구체화에 의존하면 안된다
- 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조 하라는 원칙
- 사용자가 상속 관계로 이루어진 모듈을 가져다 사용할 때, 하위 모듈을 직접 인스턴스를 가져다 쓰지 말라는 뜻
- ex)
// 저수준 모듈
class LightBulb {
public void turnOn() {
System.out.println("전구가 켜집니다.");
}
public void turnOff() {
System.out.println("전구가 꺼집니다.");
}
}
// 고수준 모듈
interface Switch {
void operate();
}
class RemoteControl implements Switch {
private LightBulb bulb;
public RemoteControl(LightBulb bulb) {
this.bulb = bulb;
}
public void operate() {
bulb.turnOn();
}
}
- 위의 코드에서 RemoteControl 클래스가 LightBulb 클래스에 직접 의존하고 있으므로 DIP를 위반하는 예제입니다. 이를 DIP 원칙을 준수하는 방식으로 수정하겠습니다.
- ex)
// 저수준 모듈
interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
public void turnOn() {
System.out.println("전구가 켜집니다.");
}
public void turnOff() {
System.out.println("전구가 꺼집니다.");
}
}
// 고수준 모듈
interface Switch {
void operate();
}
class RemoteControl implements Switch {
private Switchable device;
public RemoteControl(Switchable device) {
this.device = device;
}
public void operate() {
device.turnOn();
}
}
- 이제 RemoteControl 클래스는 Switchable 인터페이스에 의존하고 있으며, LightBulb 클래스가 이 인터페이스를 구현하므로 DIP를 준수하고 있습니다.
이렇게 하면 고수준 모듈(RemoteControl)이 저수준 모듈(LightBulb)에 직접 의존하지 않고 추상화(Switchable)에 의존하게 되어, 시스템이 더 유연하고 확장 가능해집니다.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
<1번 문제>
<1번 문제 답>
<1번문제 해설>
Reader와 Writer는 interface이고
클래스 ConsoleInput은 Reader를 상속(implement)한다 --▷
클래스 ConsoleOutput은 Writer를 상속(implement)한다 --▷
클래스 Plus는 Reader와 Writer에 의존한다(파라미터, 반환값 호출) -->
*implements는 부모의 메소드를 반드시 오버라이딩(재정의)한다
<2번 문제>
다음 중 맞는 설명은? 4번
1. DIP를 만족한다.
2. LSP를 사용하여 설계되었다.
3. Plus는 메소드가 하나이기 때문에 SRP를 만족한다.
4. 입력방식에 대해 OCP를 만족하지 못한다.
<4번 문제>
입력 방식 및 출력 방식에 대하여 OCP(개방 폐쇄 원칙)를 만족한다.
이제 ConsoleOutput과 ConsoleInput 클래스는 각각 Writer과 Reader 인터페이스에 의존하고 있으며, Plus 클래스가 이 인터페이스를 구현하므로 DIP를 준수하고 있습니다. -> DIP(의존관계 역전 원칙)를 만족한다.
<6번 문제>
<6번문제 답>
클래스 Plus는 ConsoleInput과 ConsoleOutput에 의존한다
<8번 문제>
<8번 문제 정답>
b, d
'소프트웨어공학' 카테고리의 다른 글
명세 기반 테스트 (0) | 2024.05.21 |
---|---|
소프트웨어 테스팅 (0) | 2024.05.13 |
소프트웨어 아키텍처 (0) | 2024.05.07 |