- CRUD 용어 대신 보편적 언어 사용
- 경계 컨텍스트 정의에 이름 공간 사용
- 도메인 코드와 일반 코드 구분
- 올명지 클래스 가관화
- 사용 사례 중심으로 소프트웨어 디자인하기, 엔티티가 아닌
소프트웨어 디자인 가이드
CRUD 용어 대신 보편적 언어 사용
코드는 제품 및 사용자 문서에서 사용되는 것과 동일한 보편적 언어를 사용해야 합니다. 보편적 언어를 올바르게 사용하지 않으면 기여자 및 고객들에게 혼란을 야기할 수 있으며 번역이 또는 여러 용어의 사용이 지속적으로 발생할 때 주요한 혼란의 원인이 될 수 있습니다. 또한, 이는 커뮤니케이션 전략에도 어긋납니다.
아래 예시에서는 CRUD 용어가 모호함을 도입합니다. 이름은 epic_issues
연관 레코드를 생성한다고 말하지만, 실제로는 기존 이슈를 epic에 추가하는 것입니다. Rails 규칙에서 사용되는 epic_issues
라는 이름은 서비스 객체와 같은 높은 추상화 수준에도 노출됩니다. 이 코드는 보편적 언어보다는 프레임워크 용어를 사용합니다.
# 잘못된 예
EpicIssues::CreateService
보편적 언어 사용은 코드를 명확하게 하며, 프레임워크 용어를 번역하려는 독자에게 어떠한 인지 부하를 도입하지 않습니다.
# 올바른 예
Epic::AddExistingIssueService
CRUD는 모호하지 않은 간단한 개념을 나타내거나 기존의 보편적 언어와 일치할 때 사용할 수 있습니다.
# 괜찮은 예: 제품 언어와 일치합니다.
Projects::CreateService
새로운 클래스 및 데이터베이스 테이블은 보편적인 언어를 사용해야 합니다. 이 경우 모델 이름과 테이블 이름은 Rails 규칙을 따릅니다.
보편적 언어를 따르지 않는 기존 클래스는 가능한 경우 이름을 바꿔야 합니다. 데이터베이스 테이블 같은 일부 하위 수준의 추상화는 이름을 변경할 필요가 없습니다. 예를 들어, 모델 이름이 테이블 이름과 다른 경우 self.table_name=
을 사용하세요.
이름 바꾸기가 어려운 경우에만 예외를 허용할 수 있습니다. 예를 들어, 이름이 STI에 사용되거나 사용자에게 노출되는 경우 또는 이로 인해 호환성이 깨질 경우입니다.
경계 컨텍스트 정의에 이름 공간 사용
건강한 애플리케이션은 비즈니스 도메인 또는 인프라 코드와 관련된 컨텍스트를 나타내는 매크로 및 하위 컴포넌트로 분할됩니다.
GitLab 코드에는 많은 기능과 컴포넌트가 있어서 어떠한 컨텍스트가 관련되어 있는지 파악하기 어렵습니다. 우리는 어떠한 클래스도 그가 작동하는 컨텍스트를 나타내는 모듈/이름공간 내에 정의되어 있다고 기대해야 합니다.
우리가 클래스를 도메인 내에서 이름 공간으로 나눌 때:
- 도메인이 의미를 명확히하는 것처럼 유사한 용어는 모호하지 않아집니다.
예를 들어,MergeRequests::Diff
및Notes::Diff
. - 최상위 이름공간은 도메인 전문가로 식별된 하나 이상의 그룹과 연관될 수 있습니다.
- 컴포넌트 간의 상호 작용 및 결합을 더 잘 식별할 수 있습니다.
예를 들어,
MergeRequests::
도메인 내의 여러 클래스는Ci::
도메인과 더 많이 상호 작용하고ImportExport::
와는 덜 상호 작용합니다.
상위 수준의 이름 공간(경계 컨텍스트)을 지정하는 좋은 지침은 관련된 기능 카테고리를 사용하는 것입니다.
예를 들어, Continuous Integration
기능 카테고리는 Ci::
이름 공간으로 매핑됩니다.
# 나쁜 예
class JobArtifact
end
# 좋은 예
module Ci
class JobArtifact
end
end
프로젝트와 그룹은 일반적으로 컨테이너 개념입니다. 그들은 프로젝트나 그룹 수준에서 리포지터리나 러너와 같은 기능이 존재하도록 허용하지만 이러한 기능을 Projects::
나 Groups::
아래에 넣지는 않습니다.
Projects::
와 Groups::
이름 공간은 엄격하게 관련된 개념에만 사용해야 합니다.
예를 들어, Project::CreateService
또는 Groups::TransferService
.
컨트롤러의 경우 app/controllers/projects
와 app/controllers/groups
가 예외로 허용됩니다.
우리는 이 규칙을 통해 특정 웹 엔드포인트의 범위를 나타냅니다.
미래에 기능 카테고리가 다른 그룹으로 재할당될 수 있기 때문에 단계 또는 그룹 이름을 사용하지 마세요.
# 나쁜 예
module Create
class Commit
end
end
# 좋은 예
module Repositories
class Commit
end
end
반면에, 기능 카테고리는 때로는 너무 촘촘할 수 있습니다. 기능은 제품 및 마케팅에 따라 다르게 다루는 경향이 있지만 내부적으로 많은 도메인 모델과 동작을 공유할 수 있습니다. 이 경우, 너무 많은 경계 컨텍스트를 가지면 얕아지고 다른 컨텍스트와 더 결합될 수 있습니다.
경계 컨텍스트(또는 상위 수준 이름 공간)를 전체 앱의 매크로 컴포넌트로 간주할 수 있습니다.
좋은 경계 컨텍스트는 깊게 되어 있으므로 도메인의 복잡한 부분을 더 분해하기 위해 중첩된 이름 공간을 고려해야 합니다.
예를 들어, Ci::Config::
.
예를 들어, ContainerScanning::
, ContainerHostSecurity::
, ContainerNetworkSecurity::
와 같이 별도 및 세분화된 경계 컨텍스트를 가지는 대신에:
module ContainerSecurity
module HostSecurity
end
module NetworkSecurity
end
module Scanning
end
end
이름 공간에 정의된 클래스가 다른 이름 공간의 클래스와 많은 공통점이 있다면, 이 두 이름 공간이 동일한 경계 컨텍스트의 일부라는 가능성이 큽니다.
도메인 코드와 일반 코드 구분
위에서 제시된 지침은 주로 도메인 코드에 대한 것입니다. 도메인 코드에 대한 경우, 특정 경계 컨텍스트를 나타내는 이름 공간 아래의 Ruby 클래스를 놓아야 합니다(특정 기능 및 기능이 포함된 일관된 세트).
도메인 코드는 GitLab 제품에만 해당되는 독특한 코드입니다. 비즈니스 로직, 정책 및 데이터를 설명합니다. 이 코드는 주로 app/
과 lib/
사이에 분할됩니다.
응용 프로그램 코드베이스에는 더 많은 인프라 수준의 작업을 수행할 수 있는 일반 코드도 있습니다. 이는 로거, 계기, Redis와 같은 데이터 리포지터리의 클라이언트, 데이터베이스 유틸리티 등입니다.
애플리케이션이 실행되는 데 중요하지만, 고유한 GitLab 제품에 영향을 주지 않고 오프 스퀘어 솔루션으로 다시 작성하거나 대체할 수 있으므로 일반 코드에는 고유한 비즈니스 로직을 설명하지 않습니다. 즉, 일반 코드는 도메인 코드와 분리되어야 합니다.
오늘날 많은 일반 코드가 lib/
에 존재하지만 도메인 코드와 섞여 있습니다.
우리는 Gems 개발 가이드라인에 설명된 대로 젬을 gems/
디렉터리로 추출해야 합니다.
올명지 클래스 가관화
All과 모조, 모노, 일 전신(또 알려진 이름: god objects)에 새 데이터와 동작을 추가하지 않는 것을 고려해야 합니다.
우리는 Project
, User
, MergeRequest
, Ci::Pipeline
및 1000 LOC 이상의 클래스를 올명지로 간주합니다.
그런 클래스들은 책임이 너무 많이 있습니다. 대부분의 경우 새 데이터와 동작은 별도의 전용 클래스로 추가할 수 있습니다.
지침:
- 대상 ID에 대한 참조가 주로 필요한 경우(예:
Project#id
), 외부 키를 사용하거나 객체 주변에 특별한 동작을 추가할 수 있는 얇은 래퍼를 추가할 수 있습니다. - 올명지 클래스에 메서드를 추가하면 추가 메서드(비공개 또는 공개) 몇 개도 추가되는 경우, 해당 메서드는 전용 클래스에 캡슐화해야 한다는 신호입니다.
- 데이터 및 연결의 시작점인 Project에 메서드를 추가하고자 하는 유혹이 있습니다. 해당 데이터(또는 일부)가 아닌 속한 경계 컨텍스트에 동작을 정의해 보세요. 이는 일반적 및 과부하된 객체보다 해당 경계 컨텍스트에서 훨씬 더 관련이 있는 올명지 객체의 측면을 만드는 데 도움이 됩니다. 이는 더 많은 결합과 복잡성을 가져옵니다.
예시: 일반 모델 주위에 얇은 도메인 객체 정의하기
User
에 여러 메서드를 추가하는 대신 abuse_trust_scores
와 연결이 있기 때문에 User
에 여러 메서드를 추가하는 대신 의존성을 역전시켜 보세요.
##
# 나쁨: User 객체에 동작 추가
class User
def spam_score
abuse_trust_scores.spamcheck.average(:score) || 0.0
end
def spammer?
# 경고 기호: 특정 경계 컨텍스트에 속하는 상수를 사용합니다!
spam_score > Abuse::TrustScore::SPAMCHECK_HAM_THRESHOLD
end
def telesign_score
abuse_trust_scores.telesign.recent_first.first&.score || 0.0
end
def arkose_global_score
abuse_trust_scores.arkose_global_score.recent_first.first&.score || 0.0
end
def arkose_custom_score
abuse_trust_scores.arkose_custom_score.recent_first.first&.score || 0.0
end
end
# 사용:
user = User.find(1)
user.spam_score
user.telesign_score
user.arkose_global_score
##
# 좋음: 사용자 신뢰 점수를 나타내는 얇은 클래스 정의
class Abuse::UserTrustScore
def initialize(user)
@user = user
end
def spam
scores.spamcheck.average(:score) || 0.0
end
def spammer?
spam > Abuse::TrustScore::SPAMCHECK_HAM_THRESHOLD
end
def telesign
scores.telesign.recent_first.first&.score || 0.0
end
def arkose_global
scores.arkose_global_score.recent_first.first&.score || 0.0
end
def arkose_custom
scores.arkose_custom_score.recent_first.first&.score || 0.0
end
private
def scores
Abuse::TrustScore.for_user(@user)
end
end
# 사용:
user = User.find(1)
user_score = Abuse::UserTrustScore.new(user)
user_score.spam
user_score.spammer?
user_score.telesign
user_score.arkose_global
실제 예시: Merge Request를 참고하세요.
예시: 의존성 역전 사용하여 도메인 개념 추출하기
##
# 나쁨: 프로젝트에서 정의된 통합 관련 메서드
class Project
has_many :integrations
def find_or_initialize_integrations
# ...
end
def find_or_initialize_integration(name)
# ...
end
def disabled_integrations
# ...
end
def ci_integrations
# ...
end
# 더 많은 메서드...
end
##
# 좋음: 통합에 관련된 모든 로직이 `Integrations::`으로 둘러싸여 있습니다.
module Integrations
class ProjectIntegrations
def initialize(project)
@project = project
end
def all_integrations
@project.integrations # 여전히 AR 연결 캐싱을 활용할 수 있습니다
end
def find_or_initialize(name)
# ...
end
def all_disabled
all_integrations.disabled
end
def all_ci
all_integrations.ci_integration
end
end
end
유사한 리팩터링 실제 예제를 확인하세요.
사용 사례 중심으로 소프트웨어 디자인하기, 엔티티가 아닌
Active Record의 강점을 통해 Rails는 개발자들에게 엔티티 중심 소프트웨어를 설계하도록 장려합니다. 컨트롤러 및 API 엔드포인트는 주로 엔티티 및 서비스 객체에 대한 CRUD 작업을 나타냅니다. 새 데이터베이스 열은 서로 다른 사용 사례를 참조하더라도 기존 엔티티 테이블에 추가될 가능성이 높습니다.
이러한 안티 패턴은 다음 중 하나 이상으로 나타날 수 있습니다:
- 서로 다른 전제 조건 확인
- 다른 권한이 동일한 추상화(서비스 객체, 컨트롤러, 직렬화기)에서 확인
- 다른 부작용이 동일한 추상화에서 여러 암시적 사용 사례의 실행. 예: “필드 X가 변경되었을 때, Y를 수행”.
안티 패턴 예시
우리는 엔티티 중심인 Groups::UpdateService
를 가지고 있으며 근본적으로 다른 사용 사례에 재사용됩니다:
- 그룹 설명 업데이트, 그룹 관리자 액세스가 필요한 경우.
-
컴퓨팅 할당량인
shared_runners_minutes_limit
과 같은 네임스페이스 수준 제한 설정을 지정하는 경우, 인스턴스 관리자 액세스가 필요합니다.
이러한 2가지 다른 사용 사례는 다른 매개변수 세트를 지원합니다. 인스턴스 관리자가 shared_runners_minutes_limit
를 업데이트하고 그룹 설명도 업데이트하는 것은 예상되지 않거나 예상되지 않습니다. 마찬가지로 사용자가 브랜치 보호 규칙을 변경하고 공유 러너 설정을 동시에 변경하는 것은 예상되지 않습니다. 이는 서로 다른 도메인에서 나오는 서로 다른 사용 사례를 나타냅니다.
솔루션
엔터티보다는 사용 사례 중심으로 디자인합니다. 페르소나, 사용 사례 및 의도가 다르면 별도의 추상화를 생성하세요:
- 특정 사용 사례 도메인에 중첩된 다른 엔드포인트(컨트롤러, GraphQL 또는 REST).
- 특정 권한과 일관된 매개변수 집합을 내장하는 다른 서비스 객체.
예를 들어, 그룹 관리자가 일반적인 그룹 설정을 업데이트하는 데 사용되는
Groups::UpdateService
.Ci::Minutes::UpdateLimitService
는 인스턴스 관리자를 위한 것으로 완전히 다른 권한, 기대치, 매개변수 및 부작용을 가질 것입니다.
최종적으로, 이는 만능 클래스 다루기의 원칙을 활용함을 요구합니다. 우리는 상호 느슨하게 결합하고 높은 응집력을 달성하고자 합니다. 이는 관련 없는 사용 사례 논리를 하나의 덜 응집 클래스로 결합하는 것을 피함으로써 가능합니다. 결과적으로, 권한이 행동 전체에 일관되게 적용되므로 더 안전한 시스템이 됩니다. 마찬가지로 별도의 모델이나 테이블에 정의된 경우 관리자 수준의 데이터를 우연히 노출하지 않습니다. 동일한 사용 사례에 속하는 데이터를 읽거나 쓰기 전에 단일 권한 확인을 수행할 수 있습니다.