> [원문](https://enterprisecraftsmanship.com/posts/separation-of-concerns-in-orm/) 관심사 분리가 무엇이며 왜 그렇게 중요한지에 대해 알아보자. 도메인 로직과 영속성 로직 사이의 경계를 허무는 코드 예시들과 기능들을 함께 알아보자. > [! Note] 도메인 로직과 영속성 로직 > > 도메인 로직 (Domain Logic): > > - 비즈니스 규칙과 업무 규칙을 담고 있는 코드입니다 > - 예를 들어, 은행 시스템에서 '계좌 잔고는 마이너스가 될 수 없다' 같은 규칙을 구현한 코드가 이에 해당합니다 > - 실제 비즈니스 프로세스와 규칙을 표현하는 핵심 로직입니다 > > 영속성 로직 (Persistence Logic): > > - 데이터를 저장하고 불러오는 것과 관련된 모든 코드를 말합니다 > - 데이터베이스 연결, 쿼리 실행, 데이터 매핑 등을 처리합니다 > - 예를 들어, SQL 쿼리를 실행하거나 ORM을 통해 객체를 데이터베이스에 저장하는 코드가 이에 해당합니다 ## ORM에서의 관심사 분리 소프트웨어 개발에서 우리는 여러 관심사를 다룬다. 대부분의 어플리케이션에서는 최소한 UI, 비즈니스 로직, 데이터베이스 라는 세 가지가 명확하게 정의되어 있다. SoC는 단일 클래스가 아닌 전체 애플리케이션에 적용되는 SRP라고 생각할 수 있다. 대부분의 경우 이 두 개념은 서로 바꿔 사용할 수 있따. ORM의 경우, SoC는 도메인 로직과 영속성 로직의 분리에 관한 것이다. 도메인 엔티티가 자신이 어떻게 저장되는지 모르고, 데이터베이스에 비즈니스 로직이 포함되어 있지 않다면 코드베이스가 관심사를 잘 분리했다고 할 수 있다. 물론 이러한 관심사들을 완벽하게 분리하는 것이 항상 가능한 것은 아니다. 때로는 일관성과 성능 문제로 인해 경계를 무너뜨려야 할 수 있다. 하지만 항상 가능한 깔끔한 분리를 고려해야 한다. 우리는 단순히 도메인 로직과 영속성 로직을 분리할 수만은 없으며, 이들을 함께 연결해주는 무언가가 필요하다. 바로 여기서 ORM이 등장한다. ORM을 통해 도메인 엔티티와 데이터베이스가 서로를 알지 못하는 방식으로 엔티티를 적절한 데이터베이스 테이블에 매핑할 수 있다. ![[Pasted image 20250212215731.png]] ## 왜 Soc가 중요할까? 애플리케이션의 관심사를 분리하는 방법에 대한 많은 정보가 있다. 하지만 왜 신경써야 할까? 그렇게 중요할까? ![[Pasted image 20250212215919.png]] 하나의 클래스에서 서로 다른 책임들을 함께 두면, 해당 클래스의 모든 작업에서 이들의 일관성을 동시에 유지해야 한다. 이는 빠르게 조합의 폭발(combinatorial explosion)로 이어진다. 게다가, **복잡도는 대부분의 개발자들이 생각하는 것보다 훨씬 더 빠르게 증가한다.** 클래스에 책임이 추가도리 때마다 복잡도는 기하급수적으로 증가한다. 이러한 복잡도를 다루기 위해, 우리는 이러한 책임들을 분리해야 한다: ![[Pasted image 20250212215856.png]] 관심사의 분리는 단순히 코드를 보기 좋게 만드는 문제가 아니다. SoC는 개발 속도에 매우 중요하다. 더욱이, 이는 프로젝트의 성공에도 매우 중요하다. 인간은 작업 메모리(working memory)에 최대 9개의 객체만을 유지할 수 있다고 한다. 적절히 관심사가 분리되지 않은 애플리케이션은 이러한 관심사의 요소들이 서로 상호작용할 수 있는 엄청난 양의 조합으로 인해 개발자를 매우 빠르게 압도한다. 관심사를 높은 응집도를 가진 조각들로 분리하면 개발하는 애플리케이션을 '분할 정복(divide and conquer)'할 수 있다. 다른 애플리케이션 구성 요소들과 느슨하게 결합된 작고 격리된 컴포넌트의 복잡도를 다루는 것이 훨씬 더 쉽다. ## 영속성 로직이 도메인 로직으로 새어나갈 때 영속성 로직이 도메인 로직으로 새어나가는 몇 가지 예시를 살펴보자. ### 사례 #1: 도메인 엔티티에서 객체의 영속성 상태를 다루는 경우 ```csharp public void DoWork(Customer customer, MyContext context) { if (context.Entry(customer).State == EntityState.Modified) { // Do something } } ``` 객체의 현재 영속성 상태(예: 이 객체가 데이터베이스에 존재하는지 여부)는 도메인 로직과 아무런 관련이 없다. 도메인 엔티티는 비즈니스 로직과 관련된 데이터만을 다루어야 한다. ### 사례 #2: ID를 다루는 경우 ```csharp public void DoWork(Customer customer1, Customer customer2) { if (customer1.Id > 0) { // Do something } if (customer1.Id == customer2.Id) { // Do something } } ``` ID를 다루는 것은 가장 흔한 형태의 영속성 로직 침투다. ID는 엔티티가 데이터베이스에 어떻게 저장되는지에 대한 구현 세부사항이다. 도메인 객체를 비교하고 싶다면, 기본 엔티티 클래스에서 동등성 멤버를 오버라드하고 `customer1.ID == customer2.ID` 대신 `customer1 == custoer2`를 작성하면 된다. > [!Note] 도메인 로직과 영속성 로직의 분리 > 데이터베이스의 ID를 직접 비교하는 것은 영속성 로직에 해당한다. 이러한 구현 세부사항은 도메인 로직에서 숨겨져야 한다. > > C#에서는 엔티티 기본 클래스에서 Equals와 연산자를 오버라이딩하여 `customer1 == customer2` 같은 도메인 친화적인 방식으로 비교를 수행할 수 있다. 내부적으로는 ID 비교가 일어나지만, 이 영속성 관련 세부사항은 도메인 로직에서 감춰진다. ### 사례 #3: 도메인 엔티티 속성 분리 ```csharp public class Customer { public int Number { get; set; } public string Name { get; set; } // Not persisted in database: can store anything here public string Message { get; set; } } ``` 이런 코드를 작성하는 경향이 있다면, 멈추고 모델에 대해 다시 생각해봐야 한다. 이러한 코드는 엔티티에 관련 없는 요소들을 포함했다는것을 나타낸다. 대부분의 경우, 모델을 리팩터링하고 이러한 요소들을 제거할 수 있다. ## 도메인 로직이 영속성 로직으로 새어나갈 때 ### 사례 #1: 연쇄 삭제 데이터베이스에서 연쇄 삭제를 설정하는 것이 새어나감의 한 예시다. 데이터베이스 자체는 언제 삭제를 트리거할지에 대한 어떤 로직도 포함해서는 안된다. 이 로직은 분명히 도메인의 관심사다. 이러한 로직은 C#/Java 등의 코드에만 있어야 한다. ### 사례 #2: 저장 프로시저 데이터베이스의 데이터를 변경하는 저장 프로시저를 만드는 것이 또 다른 예시이다.도메인 로직이 데이터베이스로 새어나가지 않도록 하고, 부수 효과가 있는 코드는 도메인 모델에 유지해라. 하지만 두 가지 특별한 경우를 짚고 넘어가야 한다. 첫째, 대부분의 경우 읽기 전용 저장 프로시저를 만드는 것은 괜찮다. 부수 효과가 있는 코드는 도메인 모델에, 부수 효과가 없는 코드는 저장 프로시저에 두는 것은 CQRS 원칙과 완벽하게 일치한다. 둘째, SQL 문에 일부 도메인 로직을 넣는 것을 피할 수 없는 경우가 있다. 예를 들어, 특정 조건에 맞는 객체들을 일괄 삭제하려하면 SQL DELETE 문이 훨씩 더 빠른 선택이 될 것이다. 이러한 경우에는 순수 SQL을 사용하는 것이 좋지만, 반드시 다른 데이터베이스 관련 코드(예: 리포지터리)와 함께 배치해야 한다. 데이터베이스 테이블의 기본값은 데이터베이스에 존재하는 도메인 로직의 또 다른 예시이다. 엔티티가 기본적으로 가지는 값은 코드에서 정의되어야 하며, 데이터베이스의 처분에 맡겨져서는 안된다. 애플리케이션 전체에 퍼져있는 이러한 도메인 로직 조각들을 한데 모으는 것이 얼마나 어려운지 생각해봐라. 이들을 한 곳에 모아두는 것이 훨씬 좋다. ## 요약 대부분의 새어나감은 도메인 관점이 아닌 데이터 관점으로 생각하는데서 비롯된다. 많은 개발자들이 개발하는 어플리케이션을 그저 그렇게 인식한다. 그런 사람들에게 엔티티는 단순히 데이터베이스에서 UI로 데이터를 전송하는 저장소일 뿐이며, ORM은 단지 SQL 쿼리의 데이터를 C# 객체로 수동으로 복사하지 않아도 되게 해주는 도우미일 뿐이다. 때로는 이러한 사고방식의 전환이 어려울 수 있다. 하지만 이를 해내면, 특히 대규모 프로젝트에서 소프트웨어를 훨씬 더 빠르게 구축할 수 있게 해주는 표현력 있는 코드 모델의 새로운 세계가 열린다. 물론 우리가 원하는 수준의 분리를 항상 달성할 수 있는 것은 아니다. 하지만 대부분의 경우, 깔끔하고 응집력있는 모델을 만드는 것을 막는 것은 없다. 대부분의 프로젝트가 실패하는 것은 비기능적 요구사항을 충족하지 못해서가 아니다. 대부분은 개발자들이 어떤 것도 변경할 수 없게 만드는 방대한 양의 엉망진창인 코드에 묻혀버린다. 이러한 코드에 커밋된 모든 변경사항은 애플리케이션 전체에 연쇄적인 장애를 일으킨다. 이러한 재앙은 코드를 분리만 해도 피할 수 있다. 분할하고 정복해라. 분리하고 구현해라.