Skip to main content

디자인 패턴 with Typescript

역할 & 책임

1.책임 (Responsibility)

  • 책임은 객체나 모듈이 수행해야 하는 구체적인 작업이나 기능을 의미.
  • 맴버 변수 혹은 매소드로 표현
  • 정보 보유 책임: 객체가 어떤 데이터를 가지고 있어야 하는가에 대한 책임입니다.
    • 예를 들어, User 클래스는 사용자의 이름과 이메일을 보유할 책임이 있습니다.
  • 행동 책임: 객체가 수행해야 할 작업에 대한 책임입니다.
    • 예를 들어, UserService 클래스는 사용자를 저장하거나 검색하는 작업을 수행할 책임이 있을 수 있습니다.

2.역할 (Role)

  • 역할은 책임의 집합
  • 역할은 보통 인터페이스나 추상 클래스로 표현
  • 예)
    • 사용자 데이터를 데이터베이스에 저장하는 책임
    • 사용자 입력을 유효성 검사하는 책임
    • 특정 비즈니스 규칙을 적용하는 책임

*단일 책임 원칙(Single Responsibility Principle, SRP)을 지킨다는 것은 하나의 클래스가 하나의 역할만 담당.

SOLID

1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

클래스에 너무 많은 책임을 넣으면 안된다.

  • 정의: 클래스는 하나의 책임만 가져야 하며, 변경의 이유는 오직 하나여야 합니다.
// SRP 위반 예시
class UserService {
// 사용자의 데이터를 저장
saveUser(user: User): void {
// 데이터베이스에 사용자 정보를 저장
console.log("Saving user to the database");
}

// 사용자의 이메일을 발송
sendEmail(user: User, message: string): void {
// 이메일 발송 로직
console.log(`Sending email to ${user.email}: ${message}`);
}

// 사용자의 데이터 유효성 검사
validateUserData(user: User): boolean {
// 사용자 데이터가 유효한지 확인하는 로직
if (user.name && user.email) {
console.log("User data is valid");
return true;
}
console.log("User data is invalid");
return false;
}
}

// SRP 준수 예시
// 사용자 데이터를 저장하는 책임
class UserRepository {
saveUser(user: User): void {
// 데이터베이스에 사용자 정보를 저장
console.log("Saving user to the database");
}
}
// 사용자에게 이메일을 발송하는 책임
class EmailService {
sendEmail(user: User, message: string): void {
// 이메일 발송 로직
console.log(`Sending email to ${user.email}: ${message}`);
}
}
// 사용자 데이터의 유효성을 검사하는 책임
class UserValidator {
validateUserData(user: User): boolean {
// 사용자 데이터가 유효한지 확인하는 로직
if (user.name && user.email) {
console.log("User data is valid");
return true;
}
console.log("User data is invalid");
return false;
}
}

2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

부모 클래스를 자꾸 수정하는건 잘못되었다. 자식을 확장해라.

  • 정의: 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 합니다.

  • 사례: 만약 새로운 할인 정책을 추가해야 한다고 가정할 때, 기존 코드를 수정하지 않고 새로운 클래스를 추가하여 기능을 확장할 수 있어야 합니다.

    • 예를 들어, Discount 클래스를 확장하는 새로운 HolidayDiscount 클래스를 추가할 수 있습니다.
    // OCP 위반 예시
    class Discount {
    calculate(price: number, type: string): number {
    if (type === 'holiday') {
    return price * 0.9;
    } else if (type === 'member') {
    return price * 0.8;
    }
    return price;
    }
    }

    // OCP 준수 예시
    abstract class Discount {
    abstract calculate(price: number): number;
    }

    class HolidayDiscount extends Discount {
    calculate(price: number): number {
    return price * 0.9;
    }
    }

    class MemberDiscount extends Discount {
    calculate(price: number): number {
    return price * 0.8;
    }
    }

3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

부모의 기능을 잘쓰다가, 자식에와서 그 기능이 예상과 다르게 동작하게 하지 말라

  • 정의: 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 합니다.
  • 사례: 예를 들어, Rectangle 클래스를 상속받은 Square 클래스가 있다면,
    • 어떤 함수도 Square 객체를 Rectangle처럼 사용할 수 있어야 합니다. 그렇지 않다면 LSP를 위반한 것입니다.
    • *인지적인 측면이 있다. getArea 함수는 WxH 를 연산하는 줄 알았다.(부모), 하지만 자식에서는 WxW 혹은 HxH 으로 바뀌어 있다.
    • *다형성을 가지는것은 필요한데, 부모의 동작을 예상과 크게 벗어나면 안된다.
// LSP 위반 예시
class Rectangle {
constructor(protected width: number, protected height: number) {}

setWidth(width: number) {
this.width = width;
}

setHeight(height: number) {
this.height = height;
}

getArea(): number {
return this.width * this.height;
}
}

class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width;
}

setHeight(height: number) {
this.height = height;
this.width = height;
}
}

function printArea(rectangle: Rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(10);
console.log(rectangle.getArea()); // Rectangle에서는 50을 기대하지만, Square에서는 100이 출력됨
}

// LSP 준수 예시
class Shape {
getArea(): number {
throw new Error("Method not implemented.");
}
}

class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}

getArea(): number {
return this.width * this.height;
}
}

class Square extends Shape {
constructor(private size: number) {
super();
}

getArea(): number {
return this.size * this.size;
}
}

다른 예시) 부모클래스의 fly는 잘 동작하였으나, 자식클래스는 예외를 던진다.

// 부모 클래스: 새(Bird)
class Bird {
fly() {
console.log("I can fly!");
}
}

// 자식 클래스: 펭귄(Penguin)
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly.");
}
}

4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

모듈에서 미사용 인터페이스가 있다면 더 분리가 가능하다. 너무 한곳에 몰아둔것.

  • 정의: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
  • 사례: Robot 클래스는 eat() 메서드를 사용하지 않을 것입니다. 이는 ISP를 위반하는 것이므로, Worker 인터페이스를 WorkableEatable로 분리하는 것이 좋습니다.
// ISP 위반 예시
interface Worker {
work(): void;
eat(): void;
}

class HumanWorker implements Worker {
work() {
console.log('Working');
}

eat() {
console.log('Eating');
}
}

class RobotWorker implements Worker {
work() {
console.log('Working');
}

eat() {
// 로봇은 먹지 않음
}
}

// ISP 준수 예시
interface Workable {
work(): void;
}

interface Eatable {
eat(): void;
}

class HumanWorker implements Workable, Eatable {
work() {
console.log('Working');
}

eat() {
console.log('Eating');
}
}

class RobotWorker implements Workable {
work() {
console.log('Working');
}
}

5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)

고수준의 모듈(부모)에 의존해야, 저수준 모듈(자식)을 많이 수용 할 수 있다.

  • 정의: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.
  • 사례: 예를 들어, Light 클래스가 Button 클래스에 의존하고 있다면, Button이 변경될 때마다 Light도 변경해야 할 수 있습니다.
    • 이를 방지하기 위해, Light 클래스는 Switchable 인터페이스에 의존하고, Button 클래스가 이 인터페이스를 구현하도록 해야 합니다.
// DIP 위반 예시
class Button {
turnOn() {
console.log('Button turned on');
}

turnOff() {
console.log('Button turned off');
}
}

class Light {
private button: Button;

constructor(button: Button) {
this.button = button;
}

toggle() {
this.button.turnOn();
}
}

// DIP 준수 예시
interface Switchable {
turnOn(): void;
turnOff(): void;
}

class Button implements Switchable {
turnOn() {
console.log('Button turned on');
}

turnOff() {
console.log('Button turned off');
}
}

class Light {
private device: Switchable;

constructor(device: Switchable) {
// DI를 사용한 의존성 주입
this.device = device;
}

toggle() {
this.device.turnOn();
}
}

5.1 의존성 주입 (Dependency Injection, DI)

  • DIP를 구현하는 방법 중 하나이다.
  • DI를 통해서 의존성을 클래스 내부 프로퍼티가 아닌 생성자를 통해서 받도록 한다.

SOLID ( OOP vs Functional Programming )

1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

  • OOP: 클래스는 하나의 책임만 가져야 합니다.
  • FP: 함수는 하나의 일만 수행
    • 순수 함수(pure function) : 외부 상태를 변경하지 않습니다. 이로써 함수의 책임이 명확해집니다.

2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

  • OOP: 클래스는 확장 가능해야 하지만, 수정하지 않고도 기존 코드를 변경할 수 있어야 합니다.
  • FP: 함수형 프로그래밍에서는 함수들이 불변성(immutability)을 가진다.
    • 기존의 함수를 수정하는 대신, 새로운 함수를 작성하여 기존 함수를 조합하거나 확장.
    • 함수의 조합(composition)이나 고차 함수(higher-order function)를 사용

3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

  • OOP: 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 합니다.
  • FP: 함수형 프로그래밍에서는 주로 다형성을 고차 함수나 함수 합성(composition)을 통해 구현합니다.

4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

  • OOP: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 합니다.
  • FP: 함수형 프로그래밍에서 인터페이스는 함수의 시그니처로 표현.
    • ISP의 개념은 함수가 불필요하게 많은 인자를 받지 않도록 설계하는 것과 관련이 있습니다.
    • 작은 함수로 나누기

5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)

  • OOP: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.
  • FP:
    • DIP와 유사 : 의존성을 함수로 전달하여, 구체적인 구현 대신 추상적인 행동을 의존성으로 사용할 수 있다는 점
    • DI와 유사 : 의존성을 주입하는 대신, 의존성 자체를 함수의 인자로 전달하여 구현
  • 함수형 프로그래밍은 주로 불변성, 순수 함수, 함수 합성 등의 개념을 강조하기 때문에, 구현 방식이 다르다.