소프트웨어 개발에서 코드 설계는 프로젝트의 성공을 좌우하는 중요한 요소입니다. 잘 설계된 코드는 유지보수가 용이하고, 확장 가능하며, 버그를 줄일 수 있습니다. 이번 글에서는 코드 설계 원칙에 대해 간단히 설명하겠습니다.
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
하나의 클래스는 하나의 책임만 가져야 합니다. 즉, 클래스는 하나의 기능만 수행하도록 설계되어야 합니다. 만약 하나의 클래스가 여러 책임을 맡게 되면, 해당 클래스에 변경이 필요할 때마다 여러 기능에 영향을 미치게 되어 버그가 발생하기 쉽습니다.
원칙 위반 코드
아래 User 클래스는 사용자 정보 저장과 이메일 전송 두 가지 책임을 가지고 있습니다. 이는 SRP를 위반합니다.
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# 데이터베이스에 사용자 정보 저장
pass
def send_email(self, message):
# 사용자에게 이메일 전송
pass
개선된 코드
이제 User 클래스는 사용자 정보를 보유하는 책임만 가지고 있고, 다른 책임들은 각각의 클래스로 분리되었습니다.
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save_to_database(self, user):
# 데이터베이스에 사용자 정보 저장
pass
class EmailService:
def send_email(self, user, message):
# 사용자에게 이메일 전송
pass
2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
코드는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다. 즉, 기존 코드를 수정하지 않고도 기능을 추가할 수 있어야 합니다. 기존 코드를 직접 수정하게 되면 예기치 않은 오류가 발생할 가능성이 높아지고, 유지보수가 어려워집니다.
원칙 위반 코드
아래 클래스는 새로운 할인 정책이 추가될 때마다 수정이 필요합니다.
class Discount:
def __init__(self, customer, price):
self.customer = customer
self.price = price
def get_discount(self):
if self.customer == "VIP":
return self.price * 0.8
else:
return self.price * 0.9
개선된 코드
이제 새로운 할인 정책이 추가되더라도 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하면 됩니다.
class Discount:
def get_discount(self, price):
pass
class RegularDiscount(Discount):
def get_discount(self, price):
return price * 0.9
class VIPDiscount(Discount):
def get_discount(self, price):
return price * 0.8
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
자식 클래스는 부모 클래스를 대체할 수 있어야 합니다. 즉, 자식 클래스가 부모 클래스의 기능을 온전히 수행할 수 있어야 합니다. 자식 클래스가 부모 클래스의 기능을 제대로 수행하지 못하면 코드의 일관성이 깨지고, 예기치 않은 동작을 유발할 수 있습니다.
원칙 위반 코드
Ostrich 클래스는 Bird 클래스를 상속받았지만, fly 메서드를 호출할 때 예외를 발생시킵니다. 이는 LSP를 위반합니다.
class Bird:
def fly(self):
pass
class Sparrow(Bird):
def fly(self):
print("Sparrow flying")
class Ostrich(Bird):
def fly(self):
raise Exception("Ostrich can't fly")
개선된 코드
이제 Ostrich 클래스는 fly 메서드를 가지지 않으며, 대신 run 메서드를 가지고 있어 LSP를 준수합니다.
class Bird:
pass
class FlyingBird(Bird):
def fly(self):
pass
class Sparrow(FlyingBird):
def fly(self):
print("Sparrow flying")
class Ostrich(Bird):
def run(self):
print("Ostrich running")
4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
설명: 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스가 낫습니다. 즉, 인터페이스는 필요한 기능만 제공해야 합니다. 너무 많은 기능을 포함한 인터페이스를 구현하려면 불필요한 기능까지도 구현해야 하므로 코드가 복잡해지고 유지보수가 어려워집니다.
원칙 위반 코드
Robot 클래스는 eat 메서드를 구현할 필요가 없습니다. 이는 ISP를 위반합니다.
class WorkerInterface:
def work(self):
pass
def eat(self):
pass
class Worker(WorkerInterface):
def work(self):
print("Working")
def eat(self):
print("Eating")
class Robot(WorkerInterface):
def work(self):
print("Working")
def eat(self):
raise Exception("Robots don't eat")
개선된 코드
이제 Robot 클래스는 불필요한 eat 메서드를 구현하지 않아도 됩니다.
class Workable:
def work(self):
pass
class Eatable:
def eat(self):
pass
class Worker(Workable, Eatable):
def work(self):
print("Working")
def eat(self):
print("Eating")
class Robot(Workable):
def work(self):
print("Working")
5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)
설명: 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다. 구체적인 클래스에 의존하게 되면 코드의 유연성이 떨어지고, 다른 구현체로 변경하기 어려워집니다.
원칙 위반 코드
Switch 클래스는 LightBulb 클래스에 직접 의존하고 있어 DIP를 위반합니다.
class LightBulb:
def turn_on(self):
print("LightBulb: ON")
def turn_off(self):
print("LightBulb: OFF")
class Switch:
def __init__(self, bulb: LightBulb):
self.bulb = bulb
def operate(self, state):
if state == "ON":
self.bulb.turn_on()
elif state == "OFF":
self.bulb.turn_off()
개선된 코드
이제 Switch 클래스는 Switchable 인터페이스에 의존하게 되어 DIP를 준수합니다.
class Switchable:
def turn_on(self):
pass
def turn_off(self):
pass
class LightBulb(Switchable):
def turn_on(self):
print("LightBulb: ON")
def turn_off(self):
print("LightBulb: OFF")
class Switch:
def __init__(self, device: Switchable):
self.device = device
def operate(self, state):
if state == "ON":
self.device.turn_on()
elif state == "OFF":
self.device.turn_off()
추상 메소드를 사용하면 DIP를 준수하기 더욱 용이합니다. 아래 수정된 코드에서 Switch 클래스는 Switchable이라는 추상 클래스에 의존하고 있으며, LightBulb와 Fan 클래스는 각각 Switchable을 구현합니다. 이렇게 하면 Switch 클래스는 구체적인 구현 클래스에 의존하지 않게 되어 DIP를 준수하게 됩니다.
from abc import ABC, abstractmethod
class Switchable(ABC):
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
class LightBulb(Switchable):
def turn_on(self):
print("LightBulb: ON")
def turn_off(self):
print("LightBulb: OFF")
class Fan(Switchable):
def turn_on(self):
print("Fan: ON")
def turn_off(self):
print("Fan: OFF")
class Switch:
def __init__(self, device: Switchable):
self.device = device
def operate(self, state):
if state == "ON":
self.device.turn_on()
elif state == "OFF":
self.device.turn_off()
위의 SOLID 원칙들을 지키면 코드는 유지보수하기 쉬워지고, 확장 가능하며, 더 안정적으로 동작하게 됩니다. 초보자라면 먼저 단일 책임 원칙과 개방-폐쇄 원칙부터 이해하고 적용해보세요. 나머지 원칙들도 차차 익혀가면 됩니다.
'기술' 카테고리의 다른 글
반도체 패키징에서 주목받고 있는 유리 기판 (Glass Core Substrate) (0) | 2024.07.19 |
---|---|
Claude Workbench 사용법: API 키 발급부터 사용법까지 (Anthropic Console) (0) | 2024.07.16 |
Claude 3.5 Sonnet 사용법 및 기능 소개 (0) | 2024.07.14 |
고대역폭 메모리(High Bandwidth Memory)란? (0) | 2024.07.12 |