의존성 설계

About_work·2024년 6월 25일
0

개발 방법론

목록 보기
3/3

1. 단일 책임 원칙 (SRP)

  • 객체 지향 설계의 SOLID 원칙 중 하나로, 클래스는 하나의 책임만 가져야 한다는 원칙
  • 즉, 클래스는 하나의 기능만 수행해야 하며, 변경의 이유가 하나여야 합니다.
  • SRP를 지키는 것이 중요한 여러 이유
  • 두괄식
    • 코드의 명확성, 변경에 대한 유연성, 테스트 용이성, 재사용성을 높여줍니다.
    • 유지보수 비용을 줄임

이유 1: 코드의 명확성 증가

  • SRP를 지키면 각 클래스가 담당하는 기능이 명확해짐
    • 이는 코드를 읽고 이해하기 쉽게 만들어 줌
  • 예를 들어, User 클래스는 사용자 정보를 나타내고 관리하는 역할만 한다는 것을 명확히 알 수 있음

위반 예시:

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def save_to_database(self):
        # 데이터베이스에 사용자 정보 저장
        pass

    def send_email(self, message):
        # 사용자에게 이메일 전송
        pass
  • 이 예시에서는 User 클래스가 사용자 정보뿐만 아니라 데이터베이스 저장 및 이메일 전송 기능도 담당하고 있어, 클래스의 역할이 불명확합니다.

SRP 준수:

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

class UserRepository:
    def save(self, user):
        # 데이터베이스에 사용자 정보 저장
        pass

class EmailService:
    def send_email(self, user, message):
        # 사용자에게 이메일 전송
        pass
  • 이 예시에서는 User, UserRepository, EmailService 클래스가 각각 명확한 책임을 가지고 있습니다.
  • User는 사용자 정보를 관리하고, UserRepository는 데이터베이스 저장을 담당하며, EmailService는 이메일 전송을 담당

이유 2: 변경에 유연한 코드

  • SRP를 준수하면 특정 기능을 변경해야 할 때, 해당 기능을 담당하는 클래스만 수정하면 됨
  • 이는 코드의 변경에 대한 유연성을 높여주며, 변경으로 인한 버그 발생 가능성을 줄여줌

위반 예시:

class User:
    # ... (생략)
    
    def save_to_database(self):
        # 데이터베이스에 사용자 정보 저장
        pass

    def send_email(self, message):
        # 사용자에게 이메일 전송
        pass

만약 이메일 전송 방식을 변경해야 한다면 User 클래스의 코드를 수정해야 하며, 이로 인해 예상치 못한 버그가 발생할 수 있습니다.

1.2.1. SRP 준수:

class EmailService:
    def send_email(self, user, message):
        # 사용자에게 이메일 전송
        pass
  • 이메일 전송 방식을 변경하려면 EmailService 클래스만 수정하면 됩니다. 이는 다른 기능에 영향을 주지 않고 변경을 수행할 수 있게 합니다.

이유 3: 테스트 용이성

  • SRP를 지키면 각 클래스가 독립적으로 테스트될 수 있어, 단위 테스트 작성이 용이해짐
  • 이는 코드의 품질을 높이고, 버그를 쉽게 발견하고 수정할 수 있게 함

위반 예시:

class User:
    # ... (생략)
    
    def save_to_database(self):
        # 데이터베이스에 사용자 정보 저장
        pass

    def send_email(self, message):
        # 사용자에게 이메일 전송
        pass
  • User 클래스를 테스트하려면 데이터베이스 저장과 이메일 전송 기능을 모두 테스트해야 하며, 이는 복잡성을 증가시킵니다.

SRP 준수:

class UserRepository:
    def save(self, user):
        # 데이터베이스에 사용자 정보 저장
        pass
  • UserRepository 클래스를 테스트할 때는 데이터베이스 저장 기능만 테스트하면 됩니다.
  • 이는 테스트의 복잡성을 줄여줍니다.

이유 4: 재사용성 증가

  • SRP를 준수하면 각 클래스가 하나의 책임만 가지므로, 다른 곳에서 쉽게 재사용할 수 있음
  • 이는 코드의 재사용성을 높여주며, 중복 코드를 줄이는 데 도움이 됩니다.

위반 예시:

class User:
    # ... (생략)
    
    def save_to_database(self):
        # 데이터베이스에 사용자 정보 저장
        pass

    def send_email(self, message):
        # 사용자에게 이메일 전송
        pass

다른 곳에서 이메일 전송 기능을 사용하려면 User 클래스를 그대로 사용해야 합니다.

SRP 준수:

class EmailService:
    def send_email(self, user, message):
        # 사용자에게 이메일 전송
        pass
  • 다른 곳에서도 EmailService 클래스를 재사용하여 이메일 전송 기능을 쉽게 사용할 수 있습니다.

2. 개방/폐쇄 원칙 (OCP)

  • 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 한다는 원칙
  • 즉, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 함
  • 개방/폐쇄 원칙(OCP) 지키면 좋은 이유 (두괄식)
    • 소프트웨어의 확장성과 유지보수성을 높임
    • 기존 코드를 수정하지 않고 새로운 기능을 쉽게 추가할 수 있으며,
    • 코드의 가독성, 재사용성, 테스트 용이성을 향상

이유 1. 유지보수 용이성

  • 기존 코드를 변경하지 않으면, 기존 기능에 대한 리그레션(기능이 새로 추가되거나 변경된 후에 이전에 잘 동작하던 기능이 오작동하는 현상) 발생 가능성을 줄일 수 있음

위반 예시:

class Discount:
    def __init__(self, customer_type):
        self.customer_type = customer_type

    def get_discount(self):
        if self.customer_type == 'regular':
            return 0.1
        elif self.customer_type == 'vip':
            return 0.2
        elif self.customer_type == 'employee':  # 새로운 조건 추가
            return 0.3
  • 여기서 새로운 고객 유형을 추가할 때마다 get_discount 메서드를 수정해야 합니다.
  • 이는 기존 코드를 수정하는 과정에서 오류를 유발할 수 있습니다.

OCP 준수:

class Discount:
    def get_discount(self):
        return 0

class RegularDiscount(Discount):
    def get_discount(self):
        return 0.1

class VIPDiscount(Discount):
    def get_discount(self):
        return 0.2

class EmployeeDiscount(Discount):  # 새로운 클래스 추가
    def get_discount(self):
        return 0.3
  • 여기서는 새로운 고객 유형을 추가할 때 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하여 확장할 수 있습니다.

이유 2. 코드 재사용성 증가

  • OCP를 준수하면 코드의 재사용성이 높아집니다. 기본 클래스를 확장하여 새로운 기능을 추가할 수 있으므로, 중복 코드를 최소화할 수 있습니다.

OCP 준수:

class Discount:
    def get_discount(self):
        return 0

class RegularDiscount(Discount):
    def get_discount(self):
        return 0.1

class VIPDiscount(Discount):
    def get_discount(self):
        return 0.2

class DiscountCalculator:
    def calculate(self, discount: Discount):
        return discount.get_discount()
  • 여기서 DiscountCalculatorDiscount 클래스의 서브 클래스를 사용하여 다양한 할인율을 계산할 수 있습니다.
  • 이는 코드 재사용성을 높이고, 새로운 할인 유형을 쉽게 추가할 수 있게 합니다.

이유 3. 코드 가독성 및 유지보수성 향상

  • OCP를 준수하면 코드의 가독성과 유지보수성이 향상됩니다.
  • 각 클래스는 하나의 책임만 가지고, 다른 클래스를 확장하여 기능을 추가할 수 있기 때문에 코드 구조가 명확해집니다.

OCP 준수:

class Discount:
    def get_discount(self):
        return 0

class RegularDiscount(Discount):
    def get_discount(self):
        return 0.1

class VIPDiscount(Discount):
    def get_discount(self):
        return 0.2

class DiscountFactory:
    def get_discount(self, customer_type):
        if customer_type == 'regular':
            return RegularDiscount()
        elif customer_type == 'vip':
            return VIPDiscount()
        else:
            return Discount()
  • 여기서는 DiscountFactory 클래스를 통해 고객 유형에 따라 적절한 할인 클래스를 반환합니다.
  • 새로운 할인 유형을 추가하려면 Discount 클래스를 확장하여 새로운 클래스를 만들고, DiscountFactory에서 이를 반환하도록 하면 됩니다.
  • TODO: 위 예시 좀 애매해보임. get_discount 메서드 자체가 OCP 원칙을 어기고 있는거 아닌가?

이유 4. 테스트 용이성

  • OCP를 준수하면 코드의 단위 테스트가 용이해집니다.
  • 각 기능이 독립적인 클래스로 구현되기 때문에, 개별 클래스를 쉽게 테스트할 수 있습니다.

OCP 준수:

import unittest

class TestDiscount(unittest.TestCase):
    def test_regular_discount(self):
        discount = RegularDiscount()
        self.assertEqual(discount.get_discount(), 0.1)

    def test_vip_discount(self):
        discount = VIPDiscount()
        self.assertEqual(discount.get_discount(), 0.2)

여기서는 각 할인 클래스를 독립적으로 테스트할 수 있습니다. 새로운 할인 클래스를 추가하더라도, 그 클래스만 별도로 테스트하면 됩니다.


3. 인터페이스 분리 원칙(ISP)

  • 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
    • 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 -> 인터페이스를 분리해야 한다는 원칙
    • 클라이언트가 불필요한 의존성을 가지지 않도록 하여 코드의 유연성, 유지보수성, 재사용성을 높임
  • 이 원칙을 지키면 인터페이스의 책임이 명확해지고, 각 클래스가 필요한 기능만 구현하게 되어 코드의 가독성과 명확성이 증가
  • 또한, 새로운 기능을 추가하거나 변경할 때 기존 코드에 영향을 주지 않고도 확장할 수 있어 시스템의 유연성과 확장성을 높일 수 있습니다.

이유 1. 클라이언트의 불필요한 의존성 제거

  • 클라이언트가 자신이 사용하지 않는 메서드에 의존하게 되면, 인터페이스가 변경될 때 불필요한 영향을 받을 수 있음
  • 이는 클라이언트 코드의 유지보수를 어렵게 만들고, 예상치 못한 버그를 발생시킬 수 있습니다.

위반 예시:

class Worker:
    def work(self):
        pass

    def eat(self):
        pass

class Human(Worker):
    def work(self):
        pass

    def eat(self):
        pass

class Robot(Worker):
    def work(self):
        pass

    def eat(self):
        raise Exception("Robots don't eat")
  • 여기서 Robot 클래스는 eat 메서드를 사용할 필요가 없는데도 불구하고, Worker 인터페이스를 구현하면서 이 메서드를 포함하고 있습니다.
  • 이는 Robot 클래스가 불필요한 의존성을 가지게 되는 예시입니다.

이유 2. 인터페이스의 명확성 증가

  • 인터페이스를 분리하면 각 인터페이스가 담당하는 책임이 명확해짐
  • 이는 인터페이스를 구현하는 클래스들이 자신에게 필요한 메서드만 구현하게 되어, 코드의 가독성과 명확성을 높입니다.

ISP 준수:

class Workable:
    def work(self):
        pass

class Eatable:
    def eat(self):
        pass

class Human(Workable, Eatable):
    def work(self):
        pass

    def eat(self):
        pass

class Robot(Workable):
    def work(self):
        pass
  • 여기서는 WorkableEatable 인터페이스를 분리하여, Robot 클래스가 work 메서드만 구현하게 하고, Human 클래스는 workeat 메서드를 모두 구현
  • 이는 각 클래스가 자신이 필요한 메서드만 구현하도록 하여 인터페이스의 명확성을 증가시킴

이유 3. 유연성과 확장성 증가

  • 인터페이스를 분리하면 새로운 기능을 추가하거나 변경할 때, 기존 인터페이스나 클래스에 영향을 주지 않고도 확장할 수 있습니다.
  • 이는 시스템의 유연성과 확장성을 높여줍니다.

ISP 준수:

class Drivable:
    def drive(self):
        pass

class Vehicle(Drivable):
    def drive(self):
        pass

class Human(Workable, Eatable, Drivable):
    def work(self):
        pass

    def eat(self):
        pass

    def drive(self):
        pass
  • 여기서는 Drivable 인터페이스를 추가하여, Vehicle 클래스와 Human 클래스가 드라이빙 기능을 구현하도록 하였습니다.
  • 새로운 기능이 추가되었지만, 기존 인터페이스나 클래스에 영향을 주지 않고 확장할 수 있었음

이유 4. 단위 테스트 용이성

  • 인터페이스를 분리하면 각 인터페이스와 그 구현체를 독립적으로 테스트할 수 있음
  • 이는 단위 테스트를 용이하게 하여, 코드의 품질을 높이고 버그를 줄이는 데 도움이 됩니다.

ISP 준수:

class Workable:
    def work(self):
        pass

class Eatable:
    def eat(self):
        pass

class Human(Workable, Eatable):
    def work(self):
        pass

    def eat(self):
        pass

# 단위 테스트 예시
import unittest

class TestHuman(unittest.TestCase):
    def test_work(self):
        human = Human()
        human.work()
        # 추가적인 검증 로직

    def test_eat(self):
        human = Human()
        human.eat()
        # 추가적인 검증 로직
  • 여기서는 Human 클래스의 work 메서드와 eat 메서드를 독립적으로 테스트할 수 있습니다. 이는 테스트의 용이성을 높여줍니다.

4. 의존성 역전 원칙 (DIP)

  • 의존성 역전 원칙(Dependency Inversion Principle, DIP)은
    • 고수준 모듈(상위 수준의 정책 결정 및 비즈니스 로직을 포함하는 모듈)저수준 모듈(구체적인 구현을 담당하는 모듈)에 의존하지 않도록 하여,
    • 두 모듈 모두 추상화(인터페이스 또는 추상 클래스)에 의존하게 합니다.
  • 의존성 역전 원칙(DIP)은 모듈 간의 결합도를 낮추고, 유연성, 유지보수성, 재사용성을 높이는 데 중요한 역할을 합니다.
  • 이 원칙을 지키면 모듈을 독립적으로 개발하고 테스트할 수 있으며, 새로운 기능을 추가하거나 변경할 때 기존 코드를 수정하지 않고도 확장할 수 있습니다.

이유 1. 모듈 간 결합도 감소

  • 고수준 모듈(상위 수준의 정책 결정 및 비즈니스 로직을 포함하는 모듈)이 저수준 모듈(구체적인 구현을 담당하는 모듈)에 직접 의존하면,
    • 저수준 모듈의 변경이 고수준 모듈에 영향을 미칠 수 있습니다.
  • DIP를 준수하면 모듈 간의 결합도를 낮출 수 있어, 변경의 영향 범위를 줄이고 시스템의 유연성을 높일 수 있습니다.

위반 예시:

class LightBulb:
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class Switch:
    def __init__(self, bulb: LightBulb):
        self.bulb = bulb

    def operate(self):
        if self.bulb.is_on:
            self.bulb.turn_off()
        else:
            self.bulb.turn_on()
  • 여기서 Switch 클래스(고수준 모듈)는 LightBulb 클래스(저수준 모듈)에 직접 의존합니다.
  • 만약 LightBulb의 구현이 변경되면 Switch 클래스도 수정해야 합니다.

DIP 준수:

class Switchable:
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class LightBulb(Switchable):
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class Switch:
    def __init__(self, device: Switchable):
        self.device = device

    def operate(self):
        if self.device.is_on:
            self.device.turn_off()
        else:
            self.device.turn_on()
  • 이 예시에서는 Switch 클래스가 Switchable 인터페이스에 의존하여, LightBulb의 구체적인 구현에 직접 의존하지 않습니다.
  • 이를 통해 LightBulb가 변경되더라도 Switch 클래스에 영향을 미치지 않습니다.

이유 2. 모듈의 독립적 개발 및 테스트

  • DIP를 준수하면 모듈을 독립적으로 개발하고 테스트할 수 있습니다.
  • 이는 각 모듈을 별도로 테스트할 수 있어, 단위 테스트 작성이 용이해지고 버그를 줄일 수 있습니다.

DIP 준수:

class Switchable:
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class LightBulb(Switchable):
    def turn_on(self):
        pass

    def turn_off(self):
        pass

# 모의 객체(Mock)를 사용한 테스트
class MockSwitchable(Switchable):
    def __init__(self):
        self.is_on = False

    def turn_on(self):
        self.is_on = True

    def turn_off(self):
        self.is_on = False

class Switch:
    def __init__(self, device: Switchable):
        self.device = device

    def operate(self):
        if self.device.is_on:
            self.device.turn_off()
        else:
            self.device.turn_on()
  • 여기서는 MockSwitchable 클래스가 Switchable 인터페이스를 구현하여, Switch 클래스를 테스트할 때 사용할 수 있음
  • 이를 통해 실제 구현에 의존하지 않고, 모의 객체를 사용하여 독립적으로 테스트할 수 있음

이유 3. 시스템 확장 용이성

  • DIP를 준수하면 새로운 기능을 추가하거나 변경할 때 기존 코드를 수정하지 않고도 확장할 수 있습니다.
  • 이는 시스템의 확장성을 높여줍니다.

DIP 준수:

class Switchable:
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class LightBulb(Switchable):
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class LEDBulb(Switchable):
    def turn_on(self):
        pass

    def turn_off(self):
        pass

class Switch:
    def __init__(self, device: Switchable):
        self.device = device

    def operate(self):
        if self.device.is_on:
            self.device.turn_off()
        else:
            self.device.turn_on()
  • 여기서는 Switchable 인터페이스를 구현하는 새로운 클래스 LEDBulb를 추가함
  • Switch 클래스는 Switchable 인터페이스에 의존하므로, 새로운 전구 유형이 추가되더라도 기존 코드를 수정할 필요가 없음
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글