ioerror

SOLID 원칙 본문

개발이야기

SOLID 원칙...

반응형

OOP(객체지향프로그래밍)의 대표적인 원칙이다.

프로그래밍 뿐만 아니라 Database 설계시에도 적용된다.

 

S : Single Responsibility Principle - 단일 책임 원칙

원칙: 클래스는 하나의 책임만 가져야 하고, 변경되는 이유도 하나뿐이어야 함

나쁜 예시(PHP)

class UserManager {
    public function createUser($data) {
        // 사용자 생성 로직
        $user = new User($data);
        
        // 데이터베이스 저장
        $this->saveToDatabase($user);
        
        // 이메일 발송
        $this->sendWelcomeEmail($user);
        
        // 로그 기록
        $this->logUserCreation($user);
        
        return $user;
    }
    
    private function saveToDatabase($user) { /* DB 로직 */ }
    private function sendWelcomeEmail($user) { /* 이메일 로직 */ }
    private function logUserCreation($user) { /* 로그 로직 */ }
}

 

좋은 예시(PHP)

class User {
    private $name;
    private $email;
    
    public function __construct($name, $email) {
        $this->name = $name;
        $this->email = $email;
    }
    
    // getter, setter 등 사용자 관련 메서드만
}

class UserRepository {
    public function save(User $user) {
        // 데이터베이스 저장 로직만 담당
    }
}

class EmailService {
    public function sendWelcomeEmail(User $user) {
        // 이메일 발송 로직만 담당
    }
}

class Logger {
    public function logUserCreation(User $user) {
        // 로그 기록 로직만 담당
    }
}

class UserService {
    private $userRepository;
    private $emailService;
    private $logger;
    
    public function __construct(UserRepository $repo, EmailService $email, Logger $logger) {
        $this->userRepository = $repo;
        $this->emailService = $email;
        $this->logger = $logger;
    }
    
    public function createUser($data) {
        $user = new User($data['name'], $data['email']);
        $this->userRepository->save($user);
        $this->emailService->sendWelcomeEmail($user);
        $this->logger->logUserCreation($user);
        return $user;
    }
}

 

나쁜예시(JS)

class UserManager {
    createUser(data) {
        // 사용자 생성 로직
        const user = new User(data);
        
        // 데이터베이스 저장
        this.saveToDatabase(user);
        
        // 이메일 발송
        this.sendWelcomeEmail(user);
        
        // 로그 기록
        this.logUserCreation(user);
        
        return user;
    }
    
    saveToDatabase(user) { /* DB 로직 */ }
    sendWelcomeEmail(user) { /* 이메일 로직 */ }
    logUserCreation(user) { /* 로그 로직 */ }
}

 

좋은예시(JS)

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
}

class UserRepository {
    save(user) {
        // 데이터베이스 저장 로직만 담당
    }
}

class EmailService {
    sendWelcomeEmail(user) {
        // 이메일 발송 로직만 담당
    }
}

class Logger {
    logUserCreation(user) {
        // 로그 기록 로직만 담당
    }
}

class UserService {
    constructor(userRepository, emailService, logger) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.logger = logger;
    }
    
    createUser(data) {
        const user = new User(data.name, data.email);
        this.userRepository.save(user);
        this.emailService.sendWelcomeEmail(user);
        this.logger.logUserCreation(user);
        return user;
    }
}

 

O: Open / Cloded Principle - 개방/폐쇄 원칙

원칙: 클래스는 확장(상속)에는 열려있고, 수정에는 닫혀있어야 함

나쁜예시(PHP)

class AreaCalculator {
    public function calculate($shapes) {
        $area = 0;
        foreach ($shapes as $shape) {
            if ($shape->type === 'rectangle') {
                $area += $shape->width * $shape->height;
            } elseif ($shape->type === 'circle') {
                $area += pi() * pow($shape->radius, 2);
            }
            // 새로운 도형을 추가할 때마다 이 메서드를 수정해야 함
        }
        return $area;
    }
}

 

좋은예시

interface Shape {
    public function area();
}

class Rectangle implements Shape {
    private $width;
    private $height;
    
    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
    
    public function area() {
        return $this->width * $this->height;
    }
}

class Circle implements Shape {
    private $radius;
    
    public function __construct($radius) {
        $this->radius = $radius;
    }
    
    public function area() {
        return pi() * pow($this->radius, 2);
    }
}

class Triangle implements Shape {
    private $base;
    private $height;
    
    public function __construct($base, $height) {
        $this->base = $base;
        $this->height = $height;
    }
    
    public function area() {
        return 0.5 * $this->base * $this->height;
    }
}

class AreaCalculator {
    public function calculate($shapes) {
        $totalArea = 0;
        foreach ($shapes as $shape) {
            $totalArea += $shape->area(); // 새로운 도형이 추가되어도 수정 불필요
        }
        return $totalArea;
    }
}

 

나쁜예시(JS)

class AreaCalculator {
    calculate(shapes) {
        let area = 0;
        for (const shape of shapes) {
            if (shape.type === 'rectangle') {
                area += shape.width * shape.height;
            } else if (shape.type === 'circle') {
                area += Math.PI * Math.pow(shape.radius, 2);
            }
            // 새로운 도형을 추가할 때마다 이 메서드를 수정해야 함
        }
        return area;
    }
}

 

좋은예시(JS)

class Shape {
    area() {
        throw new Error("area() method must be implemented");
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    
    area() {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }
    
    area() {
        return Math.PI * Math.pow(this.radius, 2);
    }
}

class Triangle extends Shape {
    constructor(base, height) {
        super();
        this.base = base;
        this.height = height;
    }
    
    area() {
        return 0.5 * this.base * this.height;
    }
}

class AreaCalculator {
    calculate(shapes) {
        let totalArea = 0;
        for (const shape of shapes) {
            totalArea += shape.area(); // 새로운 도형이 추가되어도 수정 불필요
        }
        return totalArea;
    }
}

 

L : Liskov Subsitituion Principle - 리스코프 치환 원칙

원칙: 자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 함

나쁜예시(PHP)

class Bird {
    public function fly() {
        return "날고 있어요";
    }
}

class Penguin extends Bird {
    public function fly() {
        throw new Exception("펭귄은 날 수 없어요!"); // 부모의 계약을 위반
    }
}

function makeBirdFly(Bird $bird) {
    return $bird->fly(); // Penguin 객체가 들어오면 예외 발생
}

 

좋은예시(PHP)

abstract class Bird {
    abstract public function move();
}

interface Flyable {
    public function fly();
}

class Sparrow extends Bird implements Flyable {
    public function move() {
        return "참새가 움직여요";
    }
    
    public function fly() {
        return "참새가 날고 있어요";
    }
}

class Penguin extends Bird {
    public function move() {
        return "펭귄이 걸어요";
    }
    
    public function swim() {
        return "펭귄이 수영해요";
    }
}

function makeBirdMove(Bird $bird) {
    return $bird->move(); // 모든 새는 움직일 수 있음
}

function makeFlyableFly(Flyable $flyable) {
    return $flyable->fly(); // 날 수 있는 것들만 날게 함
}

 

나쁜예시(JS)

class Bird {
    fly() {
        return "날고 있어요";
    }
}

class Penguin extends Bird {
    fly() {
        throw new Error("펭귄은 날 수 없어요!"); // 부모의 계약을 위반
    }
}

function makeBirdFly(bird) {
    return bird.fly(); // Penguin 객체가 들어오면 예외 발생
}

 

좋은예시(JS)

class Bird {
    move() {
        throw new Error("move() method must be implemented");
    }
}

class Sparrow extends Bird {
    move() {
        return "참새가 움직여요";
    }
    
    fly() {
        return "참새가 날고 있어요";
    }
}

class Penguin extends Bird {
    move() {
        return "펭귄이 걸어요";
    }
    
    swim() {
        return "펭귄이 수영해요";
    }
}

function makeBirdMove(bird) {
    return bird.move(); // 모든 새는 움직일 수 있음
}

function makeFlyableFly(flyable) {
    if (typeof flyable.fly === 'function') {
        return flyable.fly(); // 날 수 있는 것들만 날게 함
    }
    throw new Error("This object cannot fly");
}

I : Interface Segergation Principle - 인터페이스 분리 원칙

원칙: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 됨

나쁜예시(PHP)

interface Worker {
    public function work();
    public function eat();
    public function sleep();
}

class HumanWorker implements Worker {
    public function work() {
        return "인간이 일해요";
    }
    
    public function eat() {
        return "인간이 먹어요";
    }
    
    public function sleep() {
        return "인간이 자요";
    }
}

class RobotWorker implements Worker {
    public function work() {
        return "로봇이 일해요";
    }
    
    public function eat() {
        // 로봇은 먹지 않는데 구현해야 함
        throw new Exception("로봇은 먹지 않아요");
    }
    
    public function sleep() {
        // 로봇은 자지 않는데 구현해야 함
        throw new Exception("로봇은 자지 않아요");
    }
}

 

좋은예시(PHP)

interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

interface Sleepable {
    public function sleep();
}

class HumanWorker implements Workable, Eatable, Sleepable {
    public function work() {
        return "인간이 일해요";
    }
    
    public function eat() {
        return "인간이 먹어요";
    }
    
    public function sleep() {
        return "인간이 자요";
    }
}

class RobotWorker implements Workable {
    public function work() {
        return "로봇이 일해요";
    }
    
    // 로봇은 필요한 인터페이스만 구현
}

 

나쁜예시(JS)

class Worker {
    work() { throw new Error("Must implement work()"); }
    eat() { throw new Error("Must implement eat()"); }
    sleep() { throw new Error("Must implement sleep()"); }
}

class HumanWorker extends Worker {
    work() { return "인간이 일해요"; }
    eat() { return "인간이 먹어요"; }
    sleep() { return "인간이 자요"; }
}

class RobotWorker extends Worker {
    work() { return "로봇이 일해요"; }
    
    eat() {
        // 로봇은 먹지 않는데 구현해야 함
        throw new Error("로봇은 먹지 않아요");
    }
    
    sleep() {
        // 로봇은 자지 않는데 구현해야 함
        throw new Error("로봇은 자지 않아요");
    }
}

 

좋은예시(JS)

// JavaScript는 인터페이스가 없으므로 믹스인 패턴 사용
const Workable = {
    work() { throw new Error("Must implement work()"); }
};

const Eatable = {
    eat() { throw new Error("Must implement eat()"); }
};

const Sleepable = {
    sleep() { throw new Error("Must implement sleep()"); }
};

class HumanWorker {
    work() { return "인간이 일해요"; }
    eat() { return "인간이 먹어요"; }
    sleep() { return "인간이 자요"; }
}

class RobotWorker {
    work() { return "로봇이 일해요"; }
    // 로봇은 work만 구현하면 됨
}

// 믹스인 적용
Object.assign(HumanWorker.prototype, Workable, Eatable, Sleepable);
Object.assign(RobotWorker.prototype, Workable);

 

D: Dependency Inversion Principle - 의존성 역전 원칙

원칙: 고수준 모듈은 저수준 모듈에 의존하면 안 되고, 둘 다 추상화에 의존해야 함

나쁜예시(PHP)

class MySQLDatabase {
    public function save($data) {
        // MySQL 특정 저장 로직
        echo "MySQL에 저장: " . $data;
    }
}

class UserService {
    private $database;
    
    public function __construct() {
        $this->database = new MySQLDatabase(); // 구체적인 클래스에 직접 의존
    }
    
    public function saveUser($userData) {
        $this->database->save($userData);
    }
}

 

좋은예시(PHP)

interface DatabaseInterface {
    public function save($data);
}

class MySQLDatabase implements DatabaseInterface {
    public function save($data) {
        echo "MySQL에 저장: " . $data;
    }
}

class PostgreSQLDatabase implements DatabaseInterface {
    public function save($data) {
        echo "PostgreSQL에 저장: " . $data;
    }
}

class UserService {
    private $database;
    
    public function __construct(DatabaseInterface $database) {
        $this->database = $database; // 추상화에 의존
    }
    
    public function saveUser($userData) {
        $this->database->save($userData);
    }
}

// 사용 예시
$mysqlDB = new MySQLDatabase();
$userService = new UserService($mysqlDB); // 의존성 주입

$postgresDB = new PostgreSQLDatabase();
$userService2 = new UserService($postgresDB); // 쉽게 교체 가능

 

나쁜예시(JS)

class MySQLDatabase {
    save(data) {
        // MySQL 특정 저장 로직
        console.log("MySQL에 저장:", data);
    }
}

class UserService {
    constructor() {
        this.database = new MySQLDatabase(); // 구체적인 클래스에 직접 의존
    }
    
    saveUser(userData) {
        this.database.save(userData);
    }
}

 

좋은예시(JS)

class DatabaseInterface {
    save(data) {
        throw new Error("save() method must be implemented");
    }
}

class MySQLDatabase extends DatabaseInterface {
    save(data) {
        console.log("MySQL에 저장:", data);
    }
}

class PostgreSQLDatabase extends DatabaseInterface {
    save(data) {
        console.log("PostgreSQL에 저장:", data);
    }
}

class UserService {
    constructor(database) {
        this.database = database; // 추상화에 의존
    }
    
    saveUser(userData) {
        this.database.save(userData);
    }
}

// 사용 예시
const mysqlDB = new MySQLDatabase();
const userService = new UserService(mysqlDB); // 의존성 주입

const postgresDB = new PostgreSQLDatabase();
const userService2 = new UserService(postgresDB); // 쉽게 교체 가능

 

 

반응형
Comments