https://martinfowler.com/bliki/Yagni.html
마틴 파울러의 글 Yagni를 읽고, 작성한 글입니다.
비즈니스
미래에 필요할 것으로 예상되는 일부 기능은 필요하지 않을 것이다.
→ 즉, 지금 구축할 필요가 없다.
현재 어떤 추정 기능이 필요하다고 생각할 수 있음.
→ 하지만 이는 틀릴 가능성이 있다. 요구 사항을 미리 파악하는 것은 어렵고 비용이 많이 발생한다.
따라서 지금은 쓸모 없는 기능을 분석, 개발, 테스트에 들인 비용이 발생함.
요구사항을 완전히 이해하였다고 하여도, 다른 두가지 비용이 여전히 발생한다.
- 지연 비용 (cost of delay)
아직 사용되지 않을 것을 개발하기 위해 다른 비즈니스 개발을 하지 못했다는 점에서 기회 비용이 발생한다.
실제로, 추정 기능을 개발하는 것은 다른 비즈니스로 창출되는 수익과 비교를 해야한다.
추정 기능을 개발하기 위해, 다른 비즈니스를 개발로 얻는 수익을 잃기 때문이다.
또한, 이러한 경우에는 일반적으로는 불필요한 기능을 개발할 확률이 높다.
- 운반 비용 (cost of carry)
운반 비용. 추정 기능 추가를 위해 복잡성을 추가하여, 기존의 다른 기능들을 개발하는데 비용이 늘어난다.
만약에 필요하지 않게 되어, 제거한다고 가정할때 이 제거 비용도 발생하게 된다.
그리고 이후에 사용되게 된다고 하여도, 이전에 작성한 코드는 기술부채가 되었을 가능성이 높다.
코드
추상화를 통한 향후의 유연성을 지원하는 경우에도 동일한 논리가 적용된다.
현재 요구 사항에 대한 코드를 어렵게 만드는 추상화는 좋지 않다. 실제로 사용할 준비가 될 때까지 필드나 메서드를 추가하지 않는다.
Yagni 의사결정을 하면, 추상화를 추가하는데 들이는 비용을 아껴서
- 코드 베이스의 복잡성을 크게 줄임
- 필요한 기능을 빠르게 제공할 수 있음
Yagni는 추정 기능을 지원하기 위해 기능을 추가하는 것에 적용되는 것이다.
이것은 소프트웨어를 더 쉽게 수정하기 위한 노력에 적용되는 것이 아니다.
Yagni는 변경하기 쉬운 경우에 실행 가능한 전략이다. 리팩터링을 통해 코드의 가변성을 높일 수 있는 것이므로, 권장되어야함.
테스트와 리팩터링으로 기존의 코드를 유연하게 만들어 나가야 기존의 코드를 진화적 설계할 수 있게 해준다. Yagni는 이를 기반으로 유연성을 강화해 나가는 것이다.
즉, 코드 베이스의 상태를 소홀히 해서는 안된다는 뜻이다.
클린 아키텍처? 헥사고널 아키텍처? 도메인 주도 개발?
클린 아키텍처, 헥사고날 아키텍처, 도메인 주도 개발 모두 공통적으로는
비즈니스 모델, 즉 도메인이 가장 중심이 되고, 기타 다른 변경으로부터 도메인을 지키기 위한 방법들이다.
하지만, 위의 방식을 지키기 위해서는 의존성을 잘 제어해야하고, 필연적으로 인터페이스 사용이 필요하다.
클린 아키텍처 책에도 Yagni에 관련한 내용이 24 장 부분적 경계에서 일부 언급되고 있다.
아래는 의존성의 흐름을 domain 으로만 향하도록 의존성 규칙을 따르는 코드이다.
아래 코드를 보면서 좀 더 자세히 이해해보자.
controller는 PostService 인터페이스에 의존하여, 추이 종속성을 가지지 않고 있다.
타입스크립트에서는 모노레포 구성을 가지고 별도의 빌드를 하지 않는 이상, 크게 의미 없는 코드이긴하다.
어쨋든 추이 종속성을 인터페이스로 끊어주고 있다.
@Controller('post')
export class PostController {
// post service는 인터페이스이다.
constructor(private readonly postService: PostService) {
}
@Get(':id')
async getPost(@Param('id') postId: string) {
const post = await this.postService.getPost(postId);
return ApiResponse.ok(new PostResponseDto(post));
}
};
domain 코드에서는 PostService와 PostRepository 인터페이스를 가지고 있다.
repository에서는 이를 구현하여, domain의 데이터 모델인 Post 를 반환해야한다. (따로 구현하지는 않겠습니다.)
orm의 entity를 반환하면 domain이 orm의 엔티티에 의존하게 되므로 반환을 해야한다.
controller가 인터페이스에 의존하는 것과는 다르게, repository가 인터페이스에 의존하는 것은
의존성 역전을 통해, 도메인 계층과 데이터 접근 계층의 경계를 나누는 용도로 사용된다.
// domain/post/post-service.ts
export interface PostService {
getPost(postId: string): Promise<Post>;
}
// domain/post/my-post.service.ts
@Injectable()
export class MyPostService implements PostService {
constructor(private repository: PostRepository) {
}
async getPost(postId: string): Promise<Post> {
return this.repository.findById(postId);
}
}
// domain/post/post.ts
export class Post {
constructor(
private readonly id: number,
private readonly title: string,
private readonly content: string,
) {
}
}
// domain/post/post.repository.ts
export interface PostRepository {
findById(id: string): Post;
}
사실 이렇게 접근하는 필연적으로 프로젝트 초기에는 interface와 구체 클래스는 대부분 1:1 관계가 될 것이다.
즉, 불필요한 interface를 구현하게 되는 것이고 이는 모두 비용이다.
그렇지만 interface를 사용하지 않게 되면, 도메인이 data access layer의 변경으로 부터 취약해진다.
예를들어, repository가 entity를 반환하는데, entity가 변경되는 경우, 도메인의 변경이 가해진다.
하지만 domain을 data access layer의 변경으로 부터 보호하고 싶다면?
domain에 일부 클래스에 data access layer로 접근하는 곳을 제한하면 변경의 여파를 줄일 수 있다.
예를들어, 아래와 같이 reader, writer 와 같은 객체를 두고, 여기서 entity를 도메인 데이터 객체로 변환해버린다면, 변경을 최소화 할 수 있다.
// domain/post/post.reader.ts
@Injectable()
export class PostReader {
constructor(private repository: PostRepository) {}
async readById(postId): Promise<Post> {
const entity = this.repository.findById(postId);
return this.toPost(entity);
}
private toPost(entity: PostEntity) {
return new Post(entity.id, entity.title, entity.content);
}
}
'아키텍처' 카테고리의 다른 글
DDD - Entity 인가 아닌가? (0) | 2024.09.04 |
---|---|
DDD - Entity의 개념 (0) | 2024.08.31 |
api gateway 란? (0) | 2023.12.30 |
설계 원칙 (0) | 2023.09.03 |