단일 책임 원칙 (SRP)
단일 책임 원칙이란?
단일 책임 원칙Single Responsibility Principle, SRP은 객체지향 프로그래밍과 소프트웨어 공학에서 중요한 설계 원칙 중 하나로, 하나의 클래스는 하나의 책임만 가져야 하며, 클래스가 변경되는 이유는 오직 하나 뿐이어야 한다는 것을 의미합니다. 이 원칙을 따름으로써 코드의 유지보수성, 확장성, 그리고 재사용성을 높일 수 있습니다.
어떤 클래스를 변경 해야 하는 이유는 오직 하나 뿐 이어야 한다"
- 로버트 C 마틴 -
SRP의 목적과 중요성
변경에 대한 이유 최소화
클래스가 단일 책임을 가질 때, 해당 클래스가 변경되어야 하는 이유도 하나로 제한됩니다. 이는 코드 변경으로 인한 부작용과 오류 발생 가능성을 줄여주며, 시스템의 안정성을 높입니다.
코드의 가독성 및 유지보수성 향상
하나의 책임에 집중된 클래스는 이해하기 쉽고, 읽기 쉬운 코드를 제공합니다. 이는 개발자들이 코드를 빠르게 파악하고 수정할 수 있게 하며, 유지보수에 드는 비용과 시간을 절감합니다.
코드 재사용성 증가
단일 책임을 가진 클래스는 특정한 기능에 집중하므로, 다른 프로젝트나 모듈에서 재사용하기 용이합니다. 이는 코드의 중복을 줄이고, 개발 효율성을 높입니다.
결합도 감소 및 응집도 증가
SRP를 준수하면 클래스 간의 결합도를 낮추고, 응집도를 높일 수 있습니다. 이는 시스템의 모듈화 수준을 향상시키고, 변경에 유연하게 대응할 수 있게 합니다. 단일 책임 원칙 준수 유무에 따른 가장 큰 특징 기준 척도는, ‘기능 변경(수정)’ 이 일어났을때의 파급 효과이며, SRP 적용 시, 한 책임의 변경에서 다른 책임의 변경의 연쇄작용에서 자유로울 수 있습니다.
SRP의 적용 예시
SRP를 위반한 사례
아래의 예제는 SRP를 위반한 클래스를 보여줍니다.
public class UserService
{
public void RegisterUser(string email, string password)
{
// 사용자 등록 로직
var user = new User(email, password);
// 데이터베이스에 사용자 저장
SaveToDatabase(user);
// 확인 이메일 전송
SendConfirmationEmail(user);
}
private void SaveToDatabase(User user)
{
// 데이터베이스 저장 로직
}
private void SendConfirmationEmail(User user)
{
// 이메일 전송 로직
}
}
UserService
클래스는 사용자 관리, 데이터베이스 처리, 이메일 전송의 세 가지 책임을 가집니다.- 이러한 구조는 코드의 유지보수와 확장을 어렵게 만들고, 변경 시 부작용이 발생할 가능성이 높습니다.
SRP를 준수한 사례
SRP를 적용하여 각 책임을 별도의 클래스로 분리할 수 있습니다.
public class User
{
public string Email { get; }
public string Password { get; }
public User(string email, string password)
{
Email = email;
Password = password;
}
}
public class UserRepository
{
public void Save(User user)
{
// 데이터베이스 저장 로직
}
}
public class EmailService
{
public void SendConfirmationEmail(User user)
{
// 이메일 전송 로직
}
}
public class UserService
{
private readonly UserRepository _userRepository;
private readonly EmailService _emailService;
public UserService(UserRepository userRepository, EmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
public void RegisterUser(string email, string password)
{
var user = new User(email, password);
_userRepository.Save(user);
_emailService.SendConfirmationEmail(user);
}
}
각 클래스는 하나의 책임만을 담당합니다.
UserRepository
: 데이터베이스에 사용자 정보를 저장하는 책임.EmailService
: 이메일을 전송하는 책임.UserService
: 사용자 등록 프로세스를 관리하는 책임. 이러한 분리는 코드의 유지보수성, 재사용성을 높이고, 각 부분을 독립적으로 수정 및 테스트할 수 있게 합니다.
SRP와 객체지향 4대 원칙
캡슐화
SRP와 캡슐화는 밀접하게 연관되어 있습니다. 단일 책임을 가진 클래스는 자신의 상태와 동작을 캡슐화하여, 외부에서 내부 구현에 접근하지 못하게 합니다. 이는 클래스가 자신의 책임에 집중할 수 있도록 하며, 외부의 변경으로부터 보호받을 수 있게 합니다.
상속
상속은 SRP를 실현하는 도구 중 하나입니다. 상속을 통해 공통된 기능을 부모 클래스에 두고, 자식 클래스는 각각의 책임에 집중할 수 있습니다. 다만, 상속을 남용할 경우 SRP를 위반할 수 있으므로, 주의가 필요합니다.
다형성
다형성은 SRP와 함께 사용되어, 클래스가 자신의 책임을 효과적으로 수행할 수 있도록 도와줍니다. 인터페이스와 추상 클래스를 통해 다형성을 구현하면, 클래스가 동일한 책임을 가진 다른 객체로 대체될 수 있어, 시스템의 유연성이 증가합니다.
추상화
SRP는 추상화를 통해 구체적인 구현 세부 사항을 숨기고, 클래스가 하나의 책임에 집중할 수 있도록 합니다. 추상화된 인터페이스를 통해 클래스 간의 결합도를 낮추고, 단일 책임을 가진 클래스들을 유연하게 조합할 수 있습니다.
SRP와 SOLID 원칙과의 연계
SRP와 개방_폐쇄 원칙
SRP를 준수하면 클래스가 변경에 닫혀 있고, 확장에 열려 있는 구조를 가지기 쉬워집니다. 각 클래스가 하나의 책임만 가지므로, 새로운 기능을 추가할 때 기존 클래스를 변경하지 않고도 확장이 가능합니다.
SRP와 인터페이스 분리 원칙
SRP와 인터페이스 분리 원칙ISP은 밀접하게 연관되어 있습니다. 단일 책임을 가진 인터페이스를 설계함으로써, 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 합니다. 이는 시스템의 유연성과 확장성을 높입니다.
SRP와 의존 역전 원칙
SRP를 적용하면 각 클래스가 명확한 책임과 역할을 가지게 되어, 추상화된 인터페이스를 통한 의존성 주입이 용이해집니다. 이는 의존 역전 원칙DIP를 준수하는 데 도움을 주며, 모듈 간의 결합도를 낮추고 유연성을 향상시킵니다.
SRP의 한계
과도한 분리로 인한 복잡성
단일 책임 원칙을 지나치게 엄격하게 적용하면 클래스가 지나치게 세분화되어, 오히려 코드의 복잡성을 증가시킬 수 있습니다. 예를 들어, 모든 기능을 별도의 클래스로 나누면 각 클래스 간의 상호작용이 복잡해지고, 이를 관리하기 어려워질 수 있습니다.
상호 의존성 증가
클래스를 지나치게 분리하면, 각 클래스 간의 상호 의존성이 증가할 수 있습니다. 이는 시스템이 지나치게 모듈화되어 각 모듈이 서로를 필요로 하는 경우 발생할 수 있으며, 이러한 의존성은 유지보수를 어렵게 만들 수 있습니다.
성능 저하
SRP를 적용하여 많은 작은 클래스가 생성되면, 시스템의 메모리 사용량과 성능에 영향을 줄 수 있습니다. 특히, 각 클래스 간의 상호작용이 빈번한 경우 성능 문제가 발생할 수 있습니다.
맺음말
단일 책임 원칙은 객체지향 설계에서 중요한 역할을 하며, 코드의 유지보수성, 확장성, 그리고 유연성을 높이는 데 기여합니다. 다른 객체지향 원칙들과 SOLID 원칙들과의 조화를 통해 더욱 견고하고 모듈화된 소프트웨어 시스템을 구축할 수 있습니다. 하지만, 과도하게 적용할 경우 복잡성이 증가할 수 있으므로, 실무에서는 적절한 균형을 유지하는 것이 중요합니다.