MTK 뉴스 & 올디스/IT 개발정보

객체지향 프로그래밍(OOP)에서 말하는 SOLID 5원칙 이란?

MTK 미디어 2024. 3. 2. 03:53
반응형

 

객체지향 프로그래밍에서 "SOLID" 원칙은 효과적인 소프트웨어 설계를 위한 다섯 가지 기본 원칙을 말한다.

이 원칙들은 코드의 유지보수성, 확장성 및 재사용성을 개선하는 데 도움이 된다. 각각의 원칙은 다음과 같다:

 

 

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

의미: 한 클래스는 하나의 책임만 가져야 한다. 여기서 '책임'이란 '변경의 이유'를 의미한다. 클래스가 둘 이상의 이유로 변경되어야 한다면, 책임이 둘 이상이라는 신호이다.

목적: 이 원칙의 목적은 클래스의 복잡성을 줄이고, 유지보수를 용이하게 하며, 변경의 영향을 최소화하는 데 있다. 클래스가 단일 책임을 가질 때, 수정해야 하는 부분이 명확해지고, 다른 부분에 미치는 영향이 줄어듭니다.

적용 예: 사용자 인터페이스와 비즈니스 로직을 분리하는 것이 좋은 예이다. 사용자 인터페이스 클래스는 화면 표시와 사용자 입력에만 집중하고, 비즈니스 로직 클래스는 데이터 처리와 규칙을 다룹니다.

 

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

의미: 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다. 이는 기존의 코드를 변경하지 않으면서 시스템의 기능을 확장할 수 있어야 함을 의미한다.

목적: 이 원칙은 시스템을 더 견고하게 만들고, 기능 확장 시 잠재적인 오류를 줄이며, 기존 코드의 재사용성을 높이는 데 목적이 있다.

적용 예: 인터페이스와 추상 클래스 사용이 이 원칙의 좋은 예이다. 이를 통해 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있다.

 

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

의미: 서브타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 프로그램에 있는 객체의 기반 타입을 서브타입의 객체로 대체해도 프로그램의 기능에 문제가 없어야 한다.

목적: 이 원칙은 상속 관계를 설계할 때 타입의 계층을 적절하게 유지하는 데 중점을 둡니다. 이를 통해 코드의 재사용성과 클래스 계층의 일관성을 유지할 수 있다.

적용 예: 정사각형과 직사각형의 관계에서, 정사각형은 직사각형의 특수한 경우로 볼 수 있지만, 너비와 높이가 독립적으로 변경될 수 있는 직사각형의 성질을 유지하지 않으면 LSP를 위반하게 된다.

 

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

의미: 클라이언트는 사용하지 않는 메소드에 의존하도록 강요되어서는 안 된다. 즉, 너무 많은 메소드를 가진 거대한 인터페이스보다는, 필요한 메소드만을 가진 더 작고 구체적인 인터페이스가 바람직한다.

목적: 이 원칙은 인터페이스가 최대한 작고 특정 목적에 맞춰져 있어야 한다고 주장한다. 이를 통해 시스템의 유연성을 높이고, 클래스 간의 불필요한 의존성을 줄일 수 있다.

적용 예: 다기능 기기(복합기)가 프린터, 스캐너, 팩스 등 여러 기능을 가지고 있을 때, 각 기능에 대한 인터페이스를 별도로 제공하는 것이 ISP의 좋은 예이다.

 

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

의미: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다. 즉, 세부 사항이 추상화에 의존하게 만들어야 한다.

목적: 이 원칙은 구체적인 클래스 간의 직접적인 의존성을 줄이고, 대신 인터페이스나 추상 클래스를 통한 간접적인 의존성을 촉진하여 시스템의 유연성과 재사용성을 높이는 데 목적이 있다.

적용 예: 데이터베이스 액세스 로직이 특정 데이터베이스 기술에 직접 의존하기보다는, 일반적인 데이터베이스 액세스 인터페이스에 의존하는 것이 DIP의 좋은 예이다.

 

 

이 원칙들은 객체지향 설계에서 중요한 지침이며, 유지보수가 쉽고, 확장 가능하며, 유연한 소프트웨어를 만드는 데 중요한 역할을 한다. 

 

각 SOLID 원칙에 대한 좋은 예와 나쁜 예를 C# 코드와 함께 자세히 설명하겠다. 

 


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

 

나쁜 예:


public class User
{
    public string Name { get; set; }

    public void SaveUser(User user)
    {
        // 사용자를 데이터베이스에 저장하는 로직
    }
}



 

이 예에서 User 클래스는 사용자 데이터를 관리하는 것 외에도 데이터베이스에 사용자를 저장하는 책임을 지고 있다. 이는 SRP를 위반하는 것으로, 한 클래스가 두 가지 책임을 갖고 있다.

좋은 예:

public class User
{
    public string Name { get; set; }
}

public class UserDB
{
    public void SaveUser(User user)
    {
        // 사용자를 데이터베이스에 저장하는 로직
    }
}


이 예에서 User 클래스는 오직 사용자 데이터를 표현하는 책임만을 지니고, UserDB 클래스는 데이터베이스 관련 작업을 담당한다. 이로써 각 클래스는 하나의 책임만을 갖게 되어 SRP를 준수한다.

 

 

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

나쁜 예:
public class Discount
{
    public double CalculateDiscount(string type, double amount)
    {
        if (type == "Special")
        {
            return amount * 0.20;
        }
        return amount * 0.10;
    }
}



이 예에서 Discount 클래스는 할인 유형에 따라 다른 할인율을 적용한다. 새로운 할인 유형이 추가될 때마다 CalculateDiscount 메소드를 수정해야 한다. 이는 OCP를 위반한다.


좋은 예:
public abstract class Discount
{
    public abstract double CalculateDiscount(double amount);
}

public class RegularDiscount : Discount
{
    public override double CalculateDiscount(double amount) => amount * 0.10;
}

public class SpecialDiscount : Discount
{
    public override double CalculateDiscount(double amount) => amount * 0.20;
}

이 예에서 Discount 클래스는 확장 가능한 추상 클래스이다. 새로운 할인 유형은 Discount를 상속받는 새로운 클래스를 만들어 추가된다. 기존 클래스를 변경할 필요가 없으므로 OCP를 준수한다.

 

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

나쁜 예:
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int GetArea() => Width * Height;
}

public class Square : Rectangle
{
    public override int Width
    {
        set { base.Width = base.Height = value; }
    }

    public override int Height
    {
        set { base.Width = base.Height = value; }
    }
}



이 예에서 Square는 Rectangle을 상속받지만, 너비와 높이를 동일하게 설정하는 제약이 있다. 이는 Rectangle의 인스턴스를 Square로 대체했을 때 기대하는 행동이 변경되므로 LSP를 위반한다.

좋은 예:



public interface IShape
{
    int GetArea();
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public int GetArea() => Width * Height;
}

public class Square : IShape
{
    public int SideLength { get; set; }

    public int GetArea() => SideLength * SideLength;
}

여기서 Rectangle과 Square는 각각 IShape 인터페이스를 구현한다. 이들은 독립적인 클래스로, 서로 대체 가능하지 않으며, 각각의 GetArea 메소드를 통해 고유의 행동을 가집니다. 이로써 LSP를 준수한다.


 

 

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

 

나쁜 예:
public interface IMachine
{
    void Print();
    void Scan();
    void Fax();
}

public class MultiFunctionPrinter : IMachine
{
    public void Print() { /* 인쇄 로직 */ }
    public void Scan() { /* 스캔 로직 */ }
    public void Fax() { /* 팩스 로직 */ }
}

IMachine 인터페이스는 여러 기능을 갖고 있지만, 모든 기기가 이 모든 기능을 필요로 하지는 않는다. 이러한 디자인은 ISP를 위반한다.

좋은 예:
public interface IPrinter
{
    void Print();
}

public interface IScanner
{
    void Scan();
}

public class Printer : IPrinter
{
    public void Print() { /* 인쇄 로직 */ }
}

public class Scanner : IScanner
{
    public void Scan() { /* 스캔 로직 */ }
}

public class MultiFunctionMachine : IPrinter, IScanner
{
    private IPrinter printer;
    private IScanner scanner;

    public MultiFunctionMachine(IPrinter printer, IScanner scanner)
    {
        this.printer = printer;
        this.scanner = scanner;
    }

    public void Print() { printer.Print(); }
    public void Scan() { scanner.Scan(); }
}

이 예에서는 각 기능에 대해 별도의 인터페이스를 제공한다. MultiFunctionMachine은 IPrinter와 IScanner를 모두 구현하여 필요한 기능을 제공한다. 이는 ISP를 준수한다.

 


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

나쁜 예:
public class User
{
    public string Name { get; set; }
}

public class MySQLUserRepository
{
    public User GetUser(int id)
    {
        // MySQL 데이터베이스에서 사용자를 검색하는 로직
        return new User { Name = "John Doe" };
    }
}

public class UserService
{
    private MySQLUserRepository repository = new MySQLUserRepository();

    public User GetUser(int id)
    {
        return repository.GetUser(id);
    }
}

UserService는 구체적인 MySQLUserRepository에 의존하고 있다. 이는 DIP를 위반하며, 다른 종류의 저장소로 변경하기 어렵게 만듭니다.

좋은 예:
public interface IUserRepository
{
    User GetUser(int id);
}

public class MySQLUserRepository : IUserRepository
{
    public User GetUser(int id)
    {
        // MySQL 데이터베이스에서 사용자를 검색하는 로직
        return new User { Name = "John Doe" };
    }
}

public class UserService
{
    private IUserRepository repository;

    public UserService(IUserRepository repository)
    {
        this.repository = repository;
    }

    public User GetUser(int id)
    {
        return repository.GetUser(id);
    }
}

여기서 UserService는 IUserRepository 인터페이스에 의존한다. 이로써 구체적인 저장소 구현에 대한 의존성이 줄어들고, 다른 종류의 저장소로 쉽게 전환할 수 있다. 이는 DIP를 준수한다.

이러한 예제들은 각 SOLID 원칙을 적용했을 때와 적용하지 않았을 때의 차이를 명확하게 보여준다. 

 

코드의 구조와 유지보수성이 크게 향상되며, 더 견고하고 유연한 소프트웨어 디자인을 가능하게 한다.

반응형