Develope/기타

더 나은 코드 품질을 위한 SOLID 원칙

oper0116 2024. 8. 30. 22:12
반응형

소프트웨어 개발에서 좋은 코드 품질을 유지하는 것은 매우 중요하다. 코드가 복잡해질수록 유지보수와 확장성이 어려워지기 때문에, 코드가 처음부터 이해하기 쉽고, 유지보수하기 쉬우며, 확장 가능한 구조로 작성되는 것이 중요하다. 이러한 목표를 달성하기 위해 객체 지향 프로그래밍과 소프트웨어 설계에서 SOLID 원칙이 탄생했다.

SOLID는 소프트웨어 개발에서 따르기 좋은 다섯 가지 기본 원칙의 약어로, 이 원칙들을 준수하면 더 나은 코드 품질을 유지할 수 있다. 각 원칙은 고유한 목적과 가치를 가지고 있으며, 이를 통해 소프트웨어 시스템을 보다 효율적이고 유연하게 만들 수 있다.

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

단일 책임 원칙은 "클래스는 하나의 책임만 가져야 하며, 클래스가 변경되는 이유는 단 하나뿐이어야 한다"는 원칙이다. 즉, 하나의 클래스는 하나의 기능만 수행해야 한다는 뜻이다.

이 원칙을 따름으로써 클래스가 특정 기능에 집중하게 되고, 코드의 가독성과 유지보수성이 높아진다.

SRP 위반 예시

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

    saveToDatabase() {
        console.log(`Saving ${this.name} to the database`);
    }

    sendEmail(message) {
        console.log(`Sending email to ${this.email}: ${message}`);
    }
}

위의 예제에서는 User 클래스가 사용자 정보를 관리하면서 동시에 데이터베이스 저장과 이메일 전송 기능을 담당하고 있다. 이는 SRP를 위반한 것이다.

SRP 준수 예시

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

class UserRepository {
    save(user) {
        console.log(`Saving ${user.name} to the database`);
    }
}

class UserNotification {
    sendEmail(user, message) {
        console.log(`Sending email to ${user.email}: ${message}`);
    }
}

// 사용 예시
const user = new User("Alice", "alice@example.com");
const repository = new UserRepository();
repository.save(user);

const notification = new UserNotification();
notification.sendEmail(user, "Welcome!");

이 예제에서 User 클래스는 사용자 정보를 관리하는 역할만 담당하고, 데이터베이스 저장과 이메일 전송은 각각 UserRepositoryUserNotification 클래스에서 처리한다.

이렇게 하면 각 클래스가 하나의 책임만 가지게 되어 유지보수가 쉬워진다.

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

개방-폐쇄 원칙은 "소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다"는 원칙이다. 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 기능을 확장할 수 있도록 설계하는 것이 중요하다.

OCP 위반 예시

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
}

class AreaCalculator {
    calculateArea(shape) {
        if (shape instanceof Rectangle) {
            return shape.width * shape.height;
        }
        // 새로운 도형 추가 시 기존 코드를 수정해야 함
    }
}

위의 코드에서는 새로운 도형이 추가될 때마다 AreaCalculator 클래스의 calculateArea 메서드를 수정해야 하는 문제가 있다.

OCP 준수 예시

class Shape {
    calculateArea() {
        throw new Error("This method should be overridden");
    }
}

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

    calculateArea() {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }

    calculateArea() {
        return Math.PI * this.radius * this.radius;
    }
}

// 사용 예시
const shapes = [new Rectangle(10, 5), new Circle(7)];
shapes.forEach((shape) => {
    console.log(`Area: ${shape.calculateArea()}`);
});

이 코드에서는 Shape 클래스를 통해 새로운 도형을 추가할 수 있으며, 기존 코드를 수정할 필요가 없다. 이렇게 하면 코드를 확장하기가 쉬워진다.

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

리스코프 치환 원칙은 "자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다"는 원칙입니다. 부모 클래스 타입의 객체를 자식 클래스 타입의 객체로 치환해도 프로그램의 동작이 일관성을 유지해야 한다.

LSP 위반 예시

class Bird {
    fly() {
        console.log("Flying");
    }
}

class Penguin extends Bird {
    fly() {
        throw new Error("Penguins cannot fly");
    }
}

Penguin 클래스가 부모 클래스인 Birdfly 메서드를 제대로 구현하지 않아, LSP를 위반하고 있다.

LSP 준수 예시

class Bird {}

class FlyingBird extends Bird {
    fly() {
        console.log("Flying");
    }
}

class NonFlyingBird extends Bird {}

class Sparrow extends FlyingBird {}

class Penguin extends NonFlyingBird {
    swim() {
        console.log("Penguin is swimming");
    }
}

여기서는 FlyingBirdNonFlyingBird 클래스를 구분하여, 자식 클래스들이 부모 클래스의 행동을 올바르게 대체할 수 있도록 했다. 이를 통해 코드의 일관성을 유지할 수 있다.

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

인터페이스 분리 원칙은 "하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스를 사용하는 것이 낫다"는 원칙이다. 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 하며, 이를 통해 인터페이스의 복잡성을 줄일 수 있다.

ISP 위반 예시

class WorkerInterface {
    work() {
        throw new Error("This method should be overridden");
    }

    eat() {
        throw new Error("This method should be overridden");
    }
}

class HumanWorker extends WorkerInterface {
    work() {
        console.log("Human working");
    }

    eat() {
        console.log("Human eating");
    }
}

class RobotWorker extends WorkerInterface {
    work() {
        console.log("Robot working");
    }

    eat() {
        throw new Error("Robots do not eat");
    }
}

여기서 RobotWorkereat 메서드를 사용할 필요가 없지만, WorkerInterface에 의해 강제로 구현해야 한다. 이는 ISP를 위반한 사례이다.

ISP 준수 예시

class Workable {
    work() {
        throw new Error("This method should be overridden");
    }
}

class Eatable {
    eat() {
        throw new Error("This method should be overridden");
    }
}

class HumanWorker extends Workable {
    work() {
        console.log("Human working");
    }
}

class HumanEater extends Eatable {
    eat() {
        console.log("Human eating");
    }
}

class RobotWorker extends Workable {
    work() {
        console.log("Robot working");
    }
}

여기서는 WorkableEatable 인터페이스를 분리하여, 각 클래스가 필요한 기능만 구현하도록 했다. 이를 통해 불필요한 의존성을 줄이고, 코드를 더 이해하기 쉽게 만들 수 있다.

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

의존성 역전 원칙은 "고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다"는 원칙이다. 즉, 구체적인 구현이 아닌, 추상화된 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.

DIP 위반 예시

class MySQLDatabase {
    connect() {
        console.log("Connecting to MySQL database");
    }
}

class PasswordReminder {
    constructor() {
        this.db = new MySQLDatabase();
    }
}

PasswordReminder 클래스는 MySQLDatabase에 강하게 결합되어 있어, 다른 데이터베이스로의 전환이 어렵다. 이는 DIP를 위반한 예시이다.

DIP 준수 예시

class Database {
    connect() {
        throw new Error("This method should be overridden");
    }
}

class MySQLDatabase extends Database {
    connect() {
        console.log("Connecting to MySQL database");
    }
}

class MongoDBDatabase extends Database {
    connect() {
        console.log("Connecting to MongoDB database");
    }
}

class PasswordReminder {
    constructor(db) {
        this.db = db;
    }
}

// 사용 예시
const db = new MySQLDatabase(); // 또는 MongoDBDatabase()
const reminder = new PasswordReminder(db);
reminder.db.connect();

여기서는 Database라는 추상 클래스를 도입하여, PasswordReminder가 구체적인 데이터베이스 구현이 아닌 추상화된 인터페이스에 의존하도록 했다. 이를 통해 데이터베이스를 쉽게 교체할 수 있다.

결론

SOLID 원칙은 객체 지향 프로그래밍에서 높은 코드 품질을 유지하는 데 필수적인 다섯 가지 원칙을 제시한다. 이 원칙들을 잘 적용하면 코드의 유연성과 재사용성이 높아지고, 유지보수 및 확장이 쉬워진다. 결과적으로 더 나은 품질의 소프트웨어를 만들 수 있으며, 이는 개발자와 사용자의 만족도를 동시에 높일 수 있다.

반응형