저도 이 말씀에 동의합니다. 제가 이해하는 바로서 한마디 첨언드리면,
막 개발을 한다면 의미가 적은 내용일 수 있는데, 조금 더 구조적으로 단단한 형태를 갖는다면 서비스형태로 개발하는 것은 좋은 방향이라고 생각합니다.
클래스를 정의할 때는 어떤 목적을 달성하기 위해 런타임에서 객체가 있었으면 좋겠다 라는 생각으로 클래스를 정의합니다. 클래스가 먼저가 아니라 객체가 먼저라는 것이죠. 그리고 그 객체들은 저마다의 책임을 가집니다.
뷰모델이라는 책임이 될 수도 있고, 도메인의 데이터 흐름을 표현하기 위한 일련의 과정을 나타낼 수도 있고, 단순하게 뭘 변환하거나, 확장메서드로서 쓰일수도 있고… 객체의 활용법은 문법으로 다양하게 정의할 수 있습니다.
객체가 어떤 문제를 달성하기 위한 런타임의 어떤 것이라면 첫번째로는 문제 해결을 위한 큰 뼈대로서 어떤일을 할지 정하게 됩니다. 바로 비즈니스 로직부터 개발하는 것이죠. 그렇게 해서 내가 원하는 문제를 개발합니다. cs 파일 하나에 다 만들어도 좋습니다.
그런데 이제 미래의 내가 또는 나와 지식수준이 다른 동료가 개발에 함께 참여하게 될 경우 일을 분업으로 하기 시작해야 합니다. 그래서 규칙이라는 걸 정의를 하기도 하는데 규칙을 매번 따로 정의를 세세하게 할 수 없으니 우리는 프로그래밍을 할 때 배우는 일반적인 지식을 배우려고 합니다. 그래야 세세하게 정의할 시간이 줄어들기 때문이고 편하기 때문이죠. 그리고 그건 Pattern 이라고 불리는, 세계의 여러 개발자들이 개발하다보니 공통적으로 어떤 특정 행동을 띄더라… 라고 붙어서 그걸 이름으로 박아놓은 것들입니다.
우리는 그걸 배움으로서 다른 도메인의 개발자들과 공통된 언어로 소통할 수 있게되는 마치 유비쿼터스 랭귀지처럼 사용할 수 있습니다.
그래서 이런 일반적인 지식을 가지고 원래는 한페이지에 작성했던 코드를 쪼개기 시작합니다. 추상화라고 하기도하고, 쓸데없이 파일 개수를 늘리는 작업이라고 하기도 하고. 여러 형태로 불릴겁니다.
//program.cs
int a = 1;
int b = 2;
Console.WriteLine(a + b);
한 페이지에 코드를 작성했다면 이것은 절차지향 프로그래밍이라고 불러도 손색이 없을 것입니다.
// Program.cs
// 2025.06.12 Vincent 개발
// a와 b를 더하는 기능의 프로그램. 다음 사람을 위하여 주석으로 의도를 기록함.
// 이 코드는 고객에게 전달해야하는 세금계산서를 계산하는 코드임.
int a = default;
int b = default;
Console.WriteLine(a + b);
그리고 주석으로 코드가 설명되어 있을 것입니다.
이것을 하나의 책임이 있는 객체로 만들고 싶어서 클래스를 정의하게 되고 주석을 없애고(남겨둬도 문제없음) 이름으로 이 타입을 명확하게 명시합니다.
// Accounting세금계산서.cs
public sealed partial class Accounting세금계산서
{
public int 계산element1 { get; set; }
public int 계산element2 { get; set; }
public int Account(int 요소1, int 요소2)
{
계산element1 = 요소1;
계산element2 = 요소2;
return 계산element1 + 계산element2;
}
}
// Program.cs
Accounting세금계산서 a = new();
int result = a.Account(1, 2);
Console.WriteLine($"너 님의 세금: {result}원 입니다.");
이제부터 이 일련의 코드의 흐름을 복붙었이 이 객체만 가져다쓰면 재사용이 가능한 것이고 같은 행위를 하는 객체끼리 코드 복붙없이 이 객체만 가져다 쓰면 됩니다.
// Accounting세금계산서.cs
public sealed partial class Accounting세금계산서
{
public int 계산element1 { get; set; }
public int 계산element2 { get; set; }
public int Account(int 요소1, int 요소2)
{
계산element1 = 요소1;
계산element2 = 요소2;
return 계산element1 + 계산element2;
}
}
// Program.cs
Accounting세금계산서 월급에서발생한세금 = new();
int vincent의세금1 = 월급에서발생한세금.Account(1, 2);
Accounting세금계산서 사업에서발생한세금 = new();
int vincent의세금2 = 사업에서발생한세금.Account(3,4);
Accounting세금계산서 부업에서발생한세금 = new();
int vincent의세금3 = 부업에서발생한세금.Account(5, 6);
Console.WriteLine($"vincent님의 막대한 세금폭탄: {vincent의세금1 + vincent의세금2 + vincent의세금3}");
코드는 길어졌지만 유연해지고 더 강해졌습니다.
세금을 계산하는 행위라고 semantic을 담아서 한글로 명명했습니다. 가독성이 좋지않아도 이해부탁드립니다.
그런데, 세금을 계산하려다보니까 반드시 덧셈연산이 필요한 것을 발견했습니다. 덧셈은 비즈니스로직이라고 부르기엔, 초등학생도 아는 내용이기 때문에 이것을 한번 더 추상화 시킬 수 있을 것 같은거죠. 여기서 서비스가 등장합니다.
// SumService.cs
// 얘는 비즈니스 로직에 대한 책임은 없고,
// 더하기에 대한 책임만 있음.
// 왜냐하면 이름이 Sum이니까
// 게다가 비즈니스 로직과 무관해졌으므로 함수 파라미터 이름도 item1 같은 일반적이 이름으로 정규화 가능.
public sealed partial class SumService
{
public int Sum(int item1, int item2) => item1 + item2;
}
// Accounting세금계산서.cs
// 얘는 세금계산에 대한 책임이 있음.
// 따라서 맥락상...'코드 변경에 대한 책임(Single Responsibility Principle)'도 세금계산에 관한거만 있어야 맞을 것 같음.
public sealed partial class Accounting세금계산서
{
public int 계산element1 { get; set; }
public int 계산element2 { get; set; }
public int Account(int 요소1, int 요소2)
{
계산element1 = 요소1;
계산element2 = 요소2;
SumService sumserv = new();
var result = sumserv.Sum(
item1: 계산element1,
item2: 계산element2);
return result;
}
}
// Program.cs
Accounting세금계산서 월급에서발생한세금 = new();
int vincent의세금1 = 월급에서발생한세금.Account(1, 2);
Accounting세금계산서 사업에서발생한세금 = new();
int vincent의세금2 = 사업에서발생한세금.Account(3,4);
Accounting세금계산서 부업에서발생한세금 = new();
int vincent의세금3 = 부업에서발생한세금.Account(5, 6);
Console.WriteLine($"vincent님의 '막대한' 세금폭탄: {vincent의세금1 + vincent의세금2 + vincent의세금3}");
이렇게 했더니 Program.cs는 건들지도 않고 코드를 가용성이 좋게 바꿨습니다.
추가로, SRP는 기능에 대한 책임이 아니라 코드 변경에 대한 책임이라는 것을 혼선해서는 안 됩니다.
그런데 보니까… 갑자기 그럴리는 없겠지만 법이 바뀌어가지고 세금을 계산해야하는데 곱셈이 필요해졌습니다. 그런데 보니까 곱셈을 Sum처럼 Service로 만들면 좋겠다는 생각이 듭니다. 왜냐하면 역시, 곱셈은 비즈니스 로직이라고 보기엔 초등학생도 아는 내용이거든요.
// SumService.cs
// 얘는 비즈니스 로직에 대한 책임은 없고,
// 더하기에 대한 책임만 있음.
// 왜냐하면 이름이 Sum이니까
// 게다가 비즈니스 로직과 무관해졌으므로 함수 파라미터 이름도 item1 같은 일반적이 이름으로 정규화 가능.
public sealed partial class SumService
{
public int Sum(int item1, int item2) => item1 + item2;
}
// MultiplingService.cs
// 얘는 비즈니스 로직에 대한 책임은 없고,
// 곱하기에 대한 책임만 있음.
// 왜냐하면 이름이 Multipling 이니까
// 게다가 비즈니스 로직과 무관해졌으므로 함수 파라미터 이름도 item1 같은 일반적이 이름으로 정규화 가능.
public sealed partial class MultiplingService
{
public int Mutiple(int item1, item2) => item1 * item2;
}
// Accounting세금계산서.cs
// 얘는 세금계산에 대한 책임이 있음.
// 따라서 맥락상...'코드 변경에 대한 책임(Single Responsibility Principle)'도 세금계산에 관한거만 있어야 맞을 것 같음.
public sealed partial class Accounting세금계산서
{
public int 계산element1 { get; set; }
public int 계산element2 { get; set; }
public int Account(int 요소1, int 요소2)
{
계산element1 = 요소1;
계산element2 = 요소2;
SumService sumserv = new();
var account1 = sumserv.Sum(
item1: 계산element1,
item2: 계산element2);
MultiplingService multiserv = new();
var account2 = multiserv.Mutiple(
item1: 계산element1,
item2: 계산element2)
var result = account1 + account2;
return result;
}
}
// Program.cs
Accounting세금계산서 월급에서발생한세금 = new();
int vincent의세금1 = 월급에서발생한세금.Account(1, 2);
Accounting세금계산서 사업에서발생한세금 = new();
int vincent의세금2 = 사업에서발생한세금.Account(3,4);
Accounting세금계산서 부업에서발생한세금 = new();
int vincent의세금3 = 부업에서발생한세금.Account(5, 6);
Console.WriteLine($"vincent님의 '더욱 더 막대한' 세금폭탄: {vincent의세금1 + vincent의세금2 + vincent의세금3}");
이번에도 주요 비즈니스 로직의 흐름인 Program.cs 를 손대지 않고 곱셈 기능을 하는 객체를 추가해서 휴먼에러를 줄일 수 있었습니다.
또 그런데 보니까 저 SumService, MultiplingService가 단순 연산이라서 매번 객체를 new로 생성할 필요는 없을 것 같고 private field도 없네요. 그러니까 그냥 막 재사용해도 전혀 문제가 없는 클래스입니다. 그래서 new를 좀 안하고 싶어졌습니다.
그리고 이 비즈니스 모델이 어떤 기본적인 사칙연산 서비스에 의존하고 있는지 또는 제공받고 있는지 명시적으로 한눈에 보고 싶어졌습니다.
이제 IoC Container가 등장합니다. IoC Container는 엄밀히 말하면 OOP와는 무관한 개념이지만 프로그래밍에 대한 의존성의 방향을 정리해준다는 맥락에서 유관하다고 할 수 있겠습니다.
// SumService.cs
// 얘는 비즈니스 로직에 대한 책임은 없고,
// 더하기에 대한 책임만 있음.
// 왜냐하면 이름이 Sum이니까
// 게다가 비즈니스 로직과 무관해졌으므로 함수 파라미터 이름도 item1 같은 일반적이 이름으로 정규화 가능.
public sealed partial class SumService
{
public int Sum(int item1, int item2) => item1 + item2;
}
// MultiplingService.cs
// 얘는 비즈니스 로직에 대한 책임은 없고,
// 곱하기에 대한 책임만 있음.
// 왜냐하면 이름이 Multipling 이니까
// 게다가 비즈니스 로직과 무관해졌으므로 함수 파라미터 이름도 item1 같은 일반적이 이름으로 정규화 가능.
public sealed partial class MultiplingService
{
public int Mutiple(int item1, item2) => item1 * item2;
}
// Accounting세금계산서.cs
// 얘는 세금계산에 대한 책임이 있음.
// 따라서 맥락상...'코드 변경에 대한 책임(Single Responsibility Principle)'도 세금계산에 관한거만 있어야 맞을 것 같음.
// 기본 생성자에 생성자 주입으로 SumSerivce 및 MultiplingService에 의존하고 있음을 명시. 이렇게하면 Visual Studio Code Lens에서 메서드와 클래스의 참조 수도 최소화할 수 있음. <-- 뇌절 방지 차원에서 중요
public sealed partial class Accounting세금계산서(
SumService sumserv,
MultiplingService multiserv)
{
public int 계산element1 { get; set; }
public int 계산element2 { get; set; }
public int Account(int 요소1, int 요소2)
{
계산element1 = 요소1;
계산element2 = 요소2;
var account1 = sumserv.Sum(
item1: 계산element1,
item2: 계산element2);
var account2 = multiserv.Mutiple(
item1: 계산element1,
item2: 계산element2)
var result = account1 + account2;
return result;
}
}
// Program.cs
using Microsoft.Extensions.DependencyInjection
ServiceCollection serviceCollection = new();
serviceCollection.AddTransient<Accounting세금계산서>();
serviceCollection.AddTransient<SumService>();
serviceCollection.AddTransient<MultiplingService>();
IServiceProvider servProv = serviceCollection.BuildServiceProvider();
var 월급에서발생한세금 = servProv.GetService<Accounting세금계산서>();
int vincent의세금1 = 월급에서발생한세금.Account(1, 2);
var 사업에서발생한세금 = servProv.GetService<Accounting세금계산서>();
int vincent의세금2 = 사업에서발생한세금.Account(3,4);
var 부업에서발생한세금 = servProv.GetService<Accounting세금계산서>();
int vincent의세금3 = 부업에서발생한세금.Account(5, 6);
Console.WriteLine($"vincent님의 '더욱 더 막대한' 세금폭탄: {vincent의세금1 + vincent의세금2 + vincent의세금3}");
그런데 세법이 또 개정되어서 갑자기 나눗셈이 필요하게 되었습니다…
나눗셈은 위에서 한 것처럼 service로 만들어서 IoC Container에 추가해준 뒤, 세금 계산의 책임을 담당하는 ‘Accounting세금계산서’ 에서 의존성을 걸고 사용해주면 역시 Program.cs를 손대지않고 기능을 추가할 수 있습니다.
이로써 Program.cs는 거시적으로 볼때에 비즈니스의 흐름을 한눈에 파악할 수 있게 되었고, 세부적인 디테일한 비즈니스 로직은 ‘Accounting세금계산서’ 라는 Model 이 담당하게 되었으며 더욱 더 Atomic한 Helper 수준의 단순하고, 명료한 기능들은 Service로 나뉘게 되었습니다.
이로서 뭔가 기능에 버그가 발생하면 하나만 수정해도 여러 객체들에 코드 변경없이 영향력을 줄 수 있게 되었습니다. 바로 OOP가 지향하는 점이죠.
작은 개체로 쪼개서 만드는 것은 소프트웨어를 구조적으로 단단하게 만드는 행위이며 Service는 잔근육처럼 더욱 더 단단하게 만들어주는 장치입니다.
긴 글 이만 줄입니다. 이해가 되셨으면 좋겠네요!