C#

C#] SOLID 원칙 1. Single Responsibility Principle (SRP 원칙)

Fastlane 2024. 1. 26. 16:22
728x90
반응형

project 개발 시, 우리는 유지보수가 용이하고 가독성있는 코드를 작성하려고 한다. 

이를 위해서, 모든 class들은 각자 임무가 있어야 하고 잘 동작해야 한다. 

 

class가 하나 이상의 임무를 갖지 않는 것은 매우 중요하다. 그렇지 않으면 유지보수가 어려워진다. 

 

SRP를 위반하는 코드 작성 뒤, 리팩토링 해보자. 

 

Console Project 생성

간단한 model class를 만들자.

public class WorkReportEntry
{
    public string ProjectCode { get; set; }
    public string ProjectName { get; set; }
    public int SpentHours { get; set; }
}

 

project를 다루는 class를 추가하자.

public class WorkReport
{
    private readonly List<WorkReportEntry> _entries;

    public WorkReport()
    {
        _entries = new List<WorkReportEntry>();
    }

    public void AddEntry(WorkReportEntry entry) => _entries.Add(entry);

    public void RemoveEntryAt(int index) => _entries.RemoveAt(index);

    public override string ToString() =>
        string.Join(Environment.NewLine, _entries.Select(x => $"Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
}

 

파일 저장같은 함수도 추가한다. 

public class WorkReport
{
    private readonly List<WorkReportEntry> _entries;

    public WorkReport()
    {
        _entries = new List<WorkReportEntry>();
    }

    public void AddEntry(WorkReportEntry entry) => _entries.Add(entry);

    public void RemoveEntryAt(int index) => _entries.RemoveAt(index);

    public void SaveToFile(string directoryPath, string fileName)
    {
        if(!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }
        
        File.WriteAllText(Path.Combine(directoryPath, fileName), ToString());
    }

    public override string ToString() =>
        string.Join(Environment.NewLine, _entries.Select(x => $"Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
}

 

이 코드의 문제점

이 class에 Load 또는 UploadToCloud 와 같은 함수를 추가할 수 있다. 하지만 추가할 수 있다고, 그렇게 해야만 하는 것은 아니다. 

 

WorkReport class에는 한 가지 문제가 있다. 

하나 이상의 responsibility를 갖는 것이다. 

 

work report entries를 추적하는 것 뿐만 아니라, file도 저장한다. SRP 위반이다. 

리팩토링 해보자. 

 

SRP에 맞게 리팩토링

Class를 나눈다. 

public class FileSaver
{
    public void SaveToFile(string directoryPath, string fileName, WorkReport report)
    {
        if (!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }

            File.WriteAllText(Path.Combine(directoryPath, fileName), report.ToString());
        }
    }
}

 

public class WorkReport
{
    private readonly List<WorkReportEntry> _entries;

    public WorkReport()
    {
        _entries = new List<WorkReportEntry>();
    }

    public void AddEntry(WorkReportEntry entry) => _entries.Add(entry);

    public void RemoveEntryAt(int index) => _entries.RemoveAt(index);

    public override string ToString() =>
        string.Join(Environment.NewLine, _entries.Select(x => $"Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
}

 

class Program
{
    static void Main(string[] args)
    {
        var report = new WorkReport();
        report.AddEntry(new WorkReportEntry { ProjectCode = "123Ds", ProjectName = "Project1", SpentHours = 5 });
        report.AddEntry(new WorkReportEntry { ProjectCode = "987Fc", ProjectName = "Project2", SpentHours = 3 });

        Console.WriteLine(report.ToString());

        var saver = new FileSaver();
        saver.SaveToFile(@"Reports", "WorkReport.txt", report);
    }
}

 

코드 개선

SaveToFile 함수는 work report 를 file로 저장을 한다. 하지만 더 개선할 수 있을까? 이 함수는 WorkReport class와 tight하게 연결되어 있다. 만약 Scheduler class를 만들고 file로 저장해야 한다면 어떻게 할까?

 

interface를 추가하고 WorkReport가 구현하도록 수정하자. 

public interface IEntryManager<T>
{
    void AddEntry(T entry);
    void RemoveEntryAt(int index);
}

 

public class WorkReport: IEntryManager<WorkReportEntry>

이제 SaveToFile 함수 signature를 수정하자. 

public void SaveToFile<T>(string directoryPath, string fileName, IEntryManager<T> workReport)

 

class Program
{
    static void Main(string[] args)
    {
        var report = new WorkReport();
        report.AddEntry(new WorkReportEntry { ProjectCode = "123Ds", ProjectName = "Project1", SpentHours = 5 });
        report.AddEntry(new WorkReportEntry { ProjectCode = "987Fc", ProjectName = "Project2", SpentHours = 3 });

        var scheduler = new Scheduler();
        scheduler.AddEntry(new ScheduleTask { TaskId = 1, Content = "Do something now.", ExecuteOn = DateTime.Now.AddDays(5) });
        scheduler.AddEntry(new ScheduleTask { TaskId = 2, Content = "Don't forget to...", ExecuteOn = DateTime.Now.AddDays(2) });

        Console.WriteLine(report.ToString());
        Console.WriteLine(scheduler.ToString());

        var saver = new FileSaver();
        saver.SaveToFile(@"Reports", "WorkReport.txt", report);
        saver.SaveToFile(@"Schedulers", "Schedule.txt", scheduler);
    }
}

 

SRP의 장점

SRP를 구현하면서 여러가지 개선이 되었다. 첫번째로 덜 복잡해졌다. 덜 복잡해지므로, 가독성이 좋아지고 유지보수가 좋아진다. 

코드 재사용도 높아지고 테스트도 쉬워진다. 

 

class가 서로 decoupled된다. 

 

SRP의 잠재적 단점

SRP는 구현하기 어렵다. 불가능한 것은 아니지만, 더 많은 리소스가 투입된다. 

SRP구현은 class를 작은 함수와 함께 compact하게 작성하도록 한다. 언뜻 보기엔 좋아보이지만, 커다란 class가 작고 많은 class로 쪼개지면서 구조적 리스크를 만든다. 잘 조직되거나 그룹화되지 않으면, 시스템의 많은 부분을 변경해야 하고 이해하기 어렵게 만든다. 

 

결론

SRP를 구현하는 것은 코딩을 할때 늘 염두해두고 있어야 한다. 리팩토링은 흔한 일이며, 누구도 완벽한 코드를 작성할 수 없다. class가 어떤 일을 하는 건지 확신할 수 없을 때, SRP에 맞는 리팩토링을 하자. 나 뿐만 아니라, 차후에 다른 개발자에게도 유지보수하는데 도움이 된다. 

728x90
반응형