반응형
의도를 드러내는 인터페이스(Intention-Revealing Interfaces)
정의
- 의도를 드러내는 인터페이스는 메서드, 클래스, 인터페이스 등의 이름이 그 기능과 목적을 명확히 드러내도록 설계하는 것을 의미
- 이는 코드의 가독성을 높이고, 유지보수성을 향상시키며, 개발자 간의 의사소통을 원활하게 한다
중요성
- 코드의 의도를 명확히 드러내면, 다른 개발자가 코드를 이해하고 사용하기 쉬워진다
- 이는 특히 도메인 주도 설계에서 도메인 전문가와 개발자 간의 의사소통을 강화하는 데 매우 중요하다
- 메서드 이름은 그 목적을 명확히 표현해야 한다
- 불명확한 메서드 이름:
- public class Order { public void update(OrderDetails details) { // Order 업데이트 로직 } }
- 의도를 드러내는 메서드 이름:
- public class Order { public void updateOrderDetails(OrderDetails details) { // Order 업데이트 로직 } }
- 클래스와 인터페이스 이름은 그 역할과 책임을 명확히 드러내야 한다
- 불명확한 클래스 이름:
- public class Processor { public void process(Order order) { // Order 처리 로직 } }
- 의도를 드러내는 클래스 이름:
- public class OrderProcessor { public void process(Order order) { // Order 처리 로직 } }
- 불필요한 정보를 숨기고, 중요한 정보를 드러내야 한다
- 불명확한 메서드 시그니처:
- public void setValues(int a, int b, int c) { // 값 설정 로직 }
- 의도를 드러내는 메서드 시그니처:
- public void setOrderValues(int orderId, int quantity, int price) { // 값 설정 로직 }
부수효과가 없는 함수
부수효과가 없는 함수란?
- 부수효과가 없는 함수는 입력값만을 사용하여 결과를 계산하고, 함수 외부의 상태를 변경하지 않는 함수순수 함수(Pure Function)라고도 불린다
특징
- 입력값에만 의존: 함수의 결과는 오직 입력값에만 의존하며, 외부 상태나 전역 변수에 의존하지 않는다.
- 외부 상태 변경 없음: 함수는 외부 상태를 변경하지 않는다. 이는 전역 변수, 파일, 데이터베이스 등의 상태를 변경하지 않음을 의미한다.
- 참조 투명성(Referential Transparency): 동일한 입력값에 대해 항상 동일한 결과를 반환한다. 이는 함수 호출을 값으로 대체해도 프로그램의 동작이 동일함을 의미한다.
부수효과가 없는 함수의 예제
- 순수 함수
- public class MathUtils { // 부수효과가 없는 순수 함수 public static int add(int a, int b) { return a + b; } }
- 부수효과가 있는 함수
- public class MathUtils { private int lastResult = 0; // 부수효과가 있는 함수 (전역 상태 변경) public int add(int a, int b) { lastResult = a + b; // 전역 상태를 변경 return lastResult; } }
- 부수효과가 없는 도메인 로직
- public class OrderCalculator { // 부수효과가 없는 함수: 입력값에만 의존하고 외부 상태를 변경하지 않음 public static BigDecimal calculateTotal(List<OrderItem> items) { return items.stream() .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } }
부수효과가 없는 함수를 작성하는 방법
- 함수 내 상태 변경 피하기: 함수 내에서 상태를 변경하지 않도록 한다. 예를 들어, 전역 변수나 클래스 멤버 변수를 수정하지 않는다.
- 외부 자원 접근 피하기: 함수 내에서 파일, 데이터베이스, 네트워크 등의 외부 자원에 접근하지 않도록 한다.
- 입력값만 사용하기: 함수는 오직 입력값만을 사용하여 결과를 계산하도록 한다. 이는 함수의 결과가 입력값에만 의존하게한다.
단언 (중요, 실무에서 흔히 놓치는 실수)
단언(Assertion)이란?
- 단언은 프로그램 실행 중 특정 조건이 참인지 확인하는 표현
- 단언문이 실패하면 프로그램은 오류를 보고하고, 이를 통해 버그를 조기에 발견할 수 있다.
목적
- 코드의 특정 상태가 기대하는 조건을 만족하는지 검증하여, 잘못된 상태에서 발생할 수 있는 오류를 방지한다.
- 즈니스 규칙과 제약 조건을 명확히 표현하고 검증한다.
단언의 중요성
- 코드의 신뢰성 향상: 단언을 통해 비즈니스 규칙과 제약 조건을 코드에 명시적으로 표현하면, 코드의 신뢰성이 향상된다.
- 디버깅과 유지보수 용이성: 단언문이 실패하면 즉시 오류를 보고하므로, 디버깅이 용이해지고, 문제의 원인을 빠르게 파악할 수 있다.
- 문서화 역할: 단언문은 코드 자체의 문서화 역할을 한다.코드의 예상 동작과 비즈니스 규칙을 명확히 표현하여, 코드의 이해도를 높일 수 있다.
단언 사용 예제
- 단순한 단언문
public void withdraw(BigDecimal amount) {
assert amount.compareTo(BigDecimal.ZERO) > 0 : "Withdrawal amount must be positive";
// 출금 로직
}
- 도메인 객체에서 단언 사용
public class Order {
private List<OrderItem> items = new ArrayList<>();
public void addItem(OrderItem item) {
assert item != null : "Order item cannot be null";
assert item.getQuantity() > 0 : "Order item quantity must be greater than zero";
items.add(item);
}
}
- 생성자에서 단언 사용
public class Customer {
private String name;
private String email;
public Customer(String name, String email) {
assert name != null && !name.isEmpty() : "Customer name cannot be null or empty";
assert email != null && email.contains("@") : "Invalid email address";
this.name = name;
this.email = email;
}
}
단언과 예외 처리의 차이
- 단언
- 주로 개발 중에 코드의 상태를 검증하는 데 사용
- 단언문은 개발자가 코드를 실행하면서 예상하지 못한 상태를 조기에 발견하는 데 도움을 준다
- 단언문이 실패하면 AssertionError가 발생하며, 이는 보통 개발 환경에서만 활성화된다
- 예외 처리
- 런타임에 발생할 수 있는 오류를 처리하는 데 사용
- 예외 처리는 사용자 입력 검증, 파일 I/O 오류 등 예상 가능한 오류 상황을 처리하는 데 사용
- 예외 처리 메커니즘을 통해 오류를 잡아내고 적절한 대처를 할 수 있다
단언의 위치
- 도메인 모델의 중요한 제약 조건:
- 단언은 도메인 모델의 중요한 제약 조건을 검증하는 데 사용
- 예를 들어, 주문 항목의 수량이 0보다 커야 한다는 조건을 단언문으로 표현할 수 있다
- 메서드의 시작 부분:
- 단언은 주로 메서드의 시작 부분에 위치하여, 메서드가 실행되기 전에 입력값이 유효한지 검증
- 객체 생성 시:
- 생성자 내에서 객체의 초기 상태가 유효한지 검증하기 위해 단언을 사용할 수 있다
- 이는 객체가 항상 일관된 상태로 생성되도록 보장한다
단언 사용 시 주의사항
- 실행 환경:
- 단언문은 보통 개발 환경에서 활성화되며, 프로덕션 환경에서는 비활성화될 수 있다
- 따라서 단언문을 사용하여 중요한 비즈니스 로직을 검증하는 것은 피해야 한다
- 명확한 메시지:
- 단언문이 실패할 때 제공되는 메시지는 명확하고 구체적이어야 한다
- 이는 디버깅 시 문제를 빠르게 파악하는 데 도움을 준다
- 적절한 사용 범위:
- 단언은 예상치 못한 상태를 검증하는 데 사용되어야 하며, 정상적인 흐름에서 발생할 수 있는 오류 처리는 예외 처리 메커니즘을 사용해야 한다
개념적 윤곽
개념적 윤곽(Conceptual Contour)이란?
- 개념적 윤곽은 도메인의 자연스러운 경계를 따라 소프트웨어 시스템을 설계하는 방법
- 이는 도메인의 의미와 구조에 맞게 모델을 분할하고, 각 부분이 독립적이면서도 조화롭게 동작하도록 한다
개념적 윤곽의 중요성
- 도메인 이해 증진: 도메인의 자연스러운 경계를 정의함으로써, 도메인 전문가와 개발자가 공통의 언어로 소통할 수 있다
- 유연한 설계: 도메인의 경계를 명확히 하면, 각 부분이 독립적으로 변경되고 확장될 수 있다.
- 복잡성 관리: 도메인의 자연스러운 경계를 따라 시스템을 분할하면, 각 부분의 복잡성을 줄이고 관리하기 쉽게
개념적 윤곽 설정 시 고려사항
- 도메인 전문가와의 협력: 도메인 전문가와 긴밀히 협력하여, 도메인의 자연스러운 경계를 이해하고 정의할 것. 이는 도메인 지식이 모델에 잘 반영되도록 한다
- 경계의 명확성: 각 경계가 명확히 정의되어야 한다. 경계가 불명확하면, 모듈 간의 의존성이 높아져 복잡성이 증가할 수 있다.
- 모듈 간의 상호작용 최소화: 모듈 간의 상호작용을 최소화하여, 각 모듈이 독립적으로 동작할 수 있도록 한다. 이는 시스템의 유연성과 유지보수성을 높인다.
- 일관성 유지: 도메인 모델이 시스템 전반에 걸쳐 일관되게 유지되도록 한다. 각 모듈은 도메인의 일관성을 해치지 않도록 설계되어야 한다.
독립형 클래스
독립형 클래스란?
- 특정 도메인에 종속되지 않고 여러 도메인 또는 모듈에서 재사용 가능한 클래스
- 공통 기능을 제공하며, 시스템 전반에 걸쳐 여러 컨텍스트에서 유용하게 사용될 수 있다
독립형 클래스의 특징
- 재사용성: 특정 도메인에 종속되지 않으므로, 여러 도메인 또는 모듈에서 재사용할 수 있다. 이는 코드의 중복을 줄이고 유지보수성을 높인다.
- 도메인 독립성: 도메인 모델과 독립적으로 존재하며, 특정 비즈니스 로직보다는 일반적인 기능을 제공하는 데 초점을 맞춘다.
- 범용성: 다양한 상황에서 사용될 수 있도록 범용적인 기능을 제공한다. 이는 시스템 전반에 걸쳐 일관된 동작을 보장한다.
독립형 클래스의 예
- 유틸리티 클래스: 유틸리티 클래스는 공통적으로 사용되는 기능을 제공하며, 도메인 모델과 독립적으로 존재
public class StringUtils {
// Null 또는 빈 문자열인지 확인하는 메서드
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
// 문자열을 특정 길이로 자르는 메서드
public static String truncate(String str, int length) {
if (str == null) {
return null;
}
return str.length() > length ? str.substring(0, length) : str;
}
}
- 값 객체(Value Object): 값 객체는 식별자가 없고 불변성을 가지며, 시스템 전반에 걸쳐 사용될 수 있다.
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount == null || currency == null) {
throw new IllegalArgumentException("Amount and currency must not be null");
}
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
// 금액을 더하는 메서드
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// 금액을 비교하는 메서드
public boolean isGreaterThan(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return this.amount.compareTo(other.amount) > 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return amount + " " + currency.getCurrencyCode();
}
}
연산의 닫힘
연산의 닫힘이란?
- 연산의 닫힘은 특정 집합과 연산이 주어졌을 때, 그 연산을 통해 생성된 결과가 항상 그 집합 내에 속하는 성질을 의미
- 즉, 집합 내의 요소들에 연산을 적용했을 때, 결과도 항상 그 집합 내에 포함되도록 보장하는 것을 말한다.
연산의 닫힘의 중요성
- 일관성 유지: 연산의 닫힘을 보장하면, 도메인 모델의 일관성을 유지할 수 있다. 이는 모델의 상태가 예측 가능하고 안정적으로 유지되도록 한다.
- 무결성 보장: 연산의 닫힘을 통해 불변식(invariant)을 유지할 수 있다. 이는 도메인 객체가 항상 유효한 상태를 가지도록 보장한다.
- 단순성 및 이해도 향상: 연산의 닫힘을 적용하면 도메인 모델의 복잡성을 줄이고, 이해하기 쉬운 모델을 만들 수 있다. 이는 코드의 가독성과 유지보수성을 높인다.
연산의 닫힘의 예제
- Money 클래스: Money 클래스에서 금액을 더하는 연산을 고려해보자. 금액을 더한 결과가 항상 Money 객체로 반환되도록 하면, 연산의 닫힘을 보장할 수 있다.
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount == null || currency == null) {
throw new IllegalArgumentException("Amount and currency must not be null");
}
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
// 금액을 더하는 메서드: 연산의 닫힘을 보장
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.equals(money.amount) && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return amount + " " + currency.getCurrencyCode();
}
}
- Order 클래스: Order 클래스에서 주문 항목을 추가하는 연산을 고려해보자. 주문 항목을 추가한 결과가 항상 유효한 Order 객체가 되도록 하면, 연산의 닫힘을 보장할 수 있다.
public class Order {
private final List<OrderItem> items = new ArrayList<>();
public void addItem(OrderItem item) {
if (item == null || item.getQuantity() <= 0) {
throw new IllegalArgumentException("Invalid order item");
}
items.add(item);
}
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
public BigDecimal getTotalAmount() {
return items.stream()
.map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Override
public String toString() {
return "Order{" +
"items=" + items +
'}';
}
}
연산의 닫힘을 보장하는 방법
- 불변 객체 사용: 불변 객체는 상태가 변경되지 않으므로, 연산의 닫힘을 쉽게 보장할 수 있다. 불변 객체의 메서드는 새로운 객체를 반환하므로, 항상 유효한 상태를 유지할 수 있다.
- 명시적 검증: 연산 결과가 유효한지 검증하는 로직을 추가하여, 항상 유효한 상태를 유지하도록 한다. 예를 들어, 금액이 음수가 되지 않도록 검증하거나, 주문 항목의 수량이 0 이상인지 검증한다.
- 적절한 예외 처리: 연산 중에 유효하지 않은 상태가 발생할 경우, 적절한 예외를 발생시켜 잘못된 상태가 전파되지 않도록 한다. 이는 시스템의 일관성과 안정성을 유지하는 데 중요하다.
반응형
LIST
'도서' 카테고리의 다른 글
DDD - 에릭 에반스, (11장 모델과 디자인 패턴의 연결) (0) | 2024.06.29 |
---|---|
DDD - 에릭 에반스, (11장 분석 패턴의 적용) (0) | 2024.06.29 |
DDD - 에릭 에반스, (6장 도메인 객체 생명주기) (0) | 2024.06.29 |
DDD - 에릭 에반스, (5장 소프트웨어에서 표현되는 모델) (0) | 2024.06.29 |
DDD - 에릭 에반스, (4장 도메인 격리) (0) | 2024.06.29 |