"Good code is its own best documentation."
(좋은 코드는 그것 자체로 최고의 문서이다.)
- Steve McConnell
객체지향 설계의 기본 원칙 | SOLID
객체지향 프로그래밍(OOP)의 설계 원칙 5가지(SOLID)에 대해 정리해 보자.
🔎 TMI
SOLID 원칙을 제안한 인물은 로버트 C. 마틴 !
소프트웨어 공학 분야에서 널리 알려진 인물로, 소프트웨어 개발 방법론과 원칙, 특히 객체 지향 설계 및 개발에 많은 기여를 했다.
많은 소프트웨어 개발자들의 필독서라고 할 수 있는 "Clean Code"의 저자이기도 하다.
소프트웨어 개발자들에게 깨끗하고 유지보수 가능한 코드를 작성하는 방법에 대해 많은 가르침을 주는 책이다.
"Clean Code" 강력하게 추천합니다!!⭐
객체지향 설계 원칙 SOLID는 소프트웨어 디자인에서 중요한 다섯 가지 원칙을 나타낸다.
먼저 간단하게 정리해 보자면 이렇다.
- 단일 책임 원칙 (Single Responsibility Principle, SRP)
- 한 클래스는 단 하나의 책임만 가진다.
- 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
- 기존의 코드를 변경하지 않고 기능 확장이 되어야 한다.
- 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- 자식 클래스는 언제나 부모 클래스의 자리에 사용될 수 있어야 한다.
- 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다.
- 클라이언트가 필요한 메서드만 포함된 인터페이스에 의존하게 만들어야 한다.
- 의존 역전 원칙 (Dependency Inversion Principle, DIP)
- 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다.
이 원칙들은 코드를 보다 유지보수 가능하고 재사용성을 높이며, 유연하고 확장이 가능한, 이해하기 쉽게 만들도록 도움을 준다.
그리고 각각의 원칙은 특정한 측면에 집중하여 설계를 강화하고, 결합도를 줄이고 응집도를 높이는 방향으로 코드를 구성하도록 유도한다.
5가지 원칙들에 대해 더 자세히 알아보자.
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
"한 클래스는 단 한의 책임만 가진다."
즉, 클래스는 하나의 기능만을 담당해야 한다는 뜻이다.
이렇게 하면 클래스가 작아지기 때문에 이해하기 쉬워지고, 테스트하기 쉬워진다.
반대로 클래스가 여러 책임을 가지게 되면 코드의 응집도가 낮아지고 이해와 수정이 어려워지므로 유지보수가 어려워진다.
이로 인해 클래스의 변경 사유를 단 하나로 제한하여 코드의 이해와 수정을 쉽게 만든다.
예시 코드 | 단일 책임 원칙 (Single Responsibility Principle, SRP)
// "각 클래스는 하나의 책임만을 가짐"
//.# 플레이어의 상태를 관리하는 책임만을 가짐
public class Player
{
public string Name { get; set; } // 플레이어 이름
public int Health { get; set; } // 플레이어 체력
}
//.# 플레이어의 통계를 관리하는 책임만을 가짐
public class PlayerStats
{
//. 통계 출력
public void DisplayStats(Player player)
{
Console.WriteLine($"❤플레이어 {player.Name}의 체력: {player.Health}");
}
}
2. 개방 - 폐쇄 원칙 (Open/Closed Principle, OCP)
"기존의 코드를 변경하지 않고 기능 확장이 되어야 한다."
즉, 소프트웨어 개체(클래스, 함수, 모듈 등)는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 뜻이다.
기존의 코드를 변경하지 않고, 확장을 통해 새로운 기능을 추가할 수 있어야 한다.
이는 인터페이스와 추상화를 적극적으로 활용하여 실현될 수 있다.
예시 코드 | 개방 - 폐쇄 원칙 (Open/Closed Principle, OCP)
// "기존 코드를 변경하지 않고 기능을 확장함"
public abstract class Enemy
{
public abstract void Attack();
}
public class Goblin : Enemy
{
//. 고블린의 공격
public override void Attack()
{
Console.WriteLine("⚔고블린이 공격합니다!");
}
}
public class Dragon : Enemy
{
//. 드래곤의 공격
public override void Attack()
{
Console.WriteLine("⚔드래곤이 공격합니다!");
}
}
public class Game
{
//.# 기존 코드를 변경하지 않고 새로운 Enemy 타입을 추가하여 확장할 수 있음
public void PerformAttack(Enemy enemy)
{
enemy.Attack();
}
}
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
"자식클래스는 언제나 부모클래스의 자리에 사용될 수 있어야 한다."
즉, 상속받은 클래스(자식 클래스)는 기반 클래스(부모 클래스)가 제공하는 모든 기능을 동일하게 제공하거나 확장해서 제공해야 한다.
자식클래스가 "최소한" 부모클래스의 기능을 수행할 수 있다면 위의 "자식클래스는 언제나 부모클래스의 자리에 사용될 수 있어야 한다."라는 문장이 성립될 것이다.
이로 인해 다형성을 지원하고, 코드의 일관성을 유지할 수 있다.
🔎 TMI
리스코프 치환 원칙(Liskov Substitution Principle, LSP)은 바바라 리스코프(Barbara Liskov)라는 컴퓨터 과학자의 이름을 따서 명명된 원칙이다!
이 원칙은 객체 지향 프로그래밍에서 상속을 사용할 때, 서브클래스가 기반 클래스와 호환되도록 설계되어야 함을 강조한다.
예시 코드 | 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
// "자식클래스는 언제나 부모클래스의 자리에 사용될 수 있어야 한다"
public class Character
{
//. 캐릭터 이동
public virtual void Move()
{
Console.WriteLine("🏃♂️캐릭터가 이동합니다.");
}
}
public class Warrior : Character
{
//. 전사 이동
public override void Move()
{
Console.WriteLine("🏃전사가 이동합니다.");
}
}
public class Mage : Character
{
//. 마법사 이동
public override void Move()
{
Console.WriteLine("🏃마법사가 이동합니다.");
}
}
4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
"자신이 사용하지 않는 메서드에 의존하면 안 된다."
즉, 인터페이스를 작고 구체적으로, 여러 개를 만들어 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 해야 한다는 뜻이다.
이는 인터페이스를 클라이언트에 특화된 작은 단위로 분리하여 의존성을 낮추고, 시스템을 더욱 유연하게 만든다.
✒ 인터페이스(Interface)와 메서드(Method)
- 인터페이스 : 메서드의 시그니처만을 정의하며, 실제 구현은 포함하지 않는다.
- 메서드 : 클래스 내에서 정의된 함수로, 객체가 수행할 실제 동작을 구현한다.
인터페이스는 여러 클래스가 동일한 방식으로 동작하도록 보장하는 역할을 하고, 메서드는 객체의 구체적인 행동을 정의하는 역할을 한다.
예시 코드 | 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
// "클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않음"
public interface IAttackable
{
void Attack();
}
public interface IHealable
{
void Heal();
}
//.# Paladin클래스는 IAttackable와 IHealable인터페이스 모두 구현.(공격과 치유 가능!)
public class Paladin : IAttackable, IHealable
{
public void Attack()
{
//. 팔라딘 공격 스킬
Console.WriteLine("⚔팔라딘이 공격합니다!");
}
public void Heal()
{
//. 팔라딘 치유 스킬
Console.WriteLine("💕팔라딘이 치유합니다!");
}
}
//.# Archer클래스는 IAttackable인터페이스만 구현.(공격만 가능!)
public class Archer : IAttackable
{
//. 궁수 공격 스킬
public void Attack()
{
Console.WriteLine("⚔궁수가 공격합니다!");
}
}
5. 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
"고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다."
즉, 추상화된 인터페이스에 의존해야 하고, 구체적인 구현체에 의존하면 안 된다는 뜻이다.
의존관계 성립시 추상성이 높은 클래스와 의존 관계를 맺어야 한다.
이로 인해 변화에 더 유연하게 대응할 수 있고, 의존성을 줄여 모듈 간의 결합도를 낮출 수 있다.
예시 코드 | 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
// "고수준 모듈이 저수준 모듈에 의존하지 않고, 추상화에 의존함"
//.# IWeapon인터페이스는 구체적인 무기(예: Sword, Bow)들이 공통적으로 가져야 할 동작을 추상화
public interface IWeapon
{
void Use();
}
public class Sword : IWeapon
{
//.# Use() 메서드를 오버라이드하여 구체적인 무기(검) 사용 로직을 정의
public void Use()
{
Console.WriteLine("⚙검을 사용합니다!");
}
}
public class Bow : IWeapon
{
//.# Use() 메서드를 오버라이드하여 구체적인 무기(활) 사용 로직을 정의
public void Use()
{
Console.WriteLine("⚙활을 사용합니다!");
}
}
public class Player
{
private readonly IWeapon _weapon;
//.# 추상화된 인터페이스에 의존하여, 구체적인 구현체에 의존하지 않음
public Player(IWeapon weapon)
{
_weapon = weapon;
}
//.# Attack() 메서드는 _weapon.Use()를 호출하여, 전달된 무기의 구체적인 Use() 메서드를 실행
public void Attack()
{
_weapon.Use();
}
}
SOLID 원칙의 중요성
각 SOLID 원칙은 개별적으로 중요하다!
예를 들면,
SRP는 클래스의 단일 책임을 유지함으로써 코드의 명확성과 수정 및 테스트 용이성을 높인다.
OCP는 코드 변경의 범위를 줄임으로써 유연한 확장성을 가지고 안정성을 향상시킨다.
LSP는 다형성을 지원함으로써 유연한 코드를 만들어 준다.
ISP는 불필요한 의존성을 줄임으로써 클라이언트와 인터페이스 간의 결합도를 낮춘다.
DIP는 추상화를 통해 시스템의 유연성을 높이는데 기여한다.
이 5가지 원칙들은 단순히 이론적인 개념을 넘어서 실제로 소프트웨어의 설계와 구현에서 매우 중요한 역할을 한다.
이를 잘 준수함으로써 확장 가능하고 유지보수가 용이한 소프트웨어를 개발할 수 있다!
'💾 Computer Science > Software Engineering' 카테고리의 다른 글
[CS/Design Pattern] GoF 디자인 패턴 정리 (0) | 2024.07.09 |
---|---|
[CS/OOP] 객체지향 프로그래밍과 절차지향 프로그래밍의 차이 (0) | 2024.07.08 |
[CS/OOP] 객체지향 프로그래밍(OOP) 개념과 특징 (0) | 2024.07.06 |