본문 바로가기
가치관 쌓기/개발 돌아보기

Layered Architecture 의 단점이 무엇이라고 생각하는가? 두번째 이야기

by simplify-len 2022. 4. 13.

첫번째 이야기 ( https://happy-coding-day.tistory.com/189 )

 

 

Layered Architecture 의 단점이 무엇이라고 생각하는가?

들어가기  이전 포스팅의 내용이였던 DDD-Lite 동영상 시청 리뷰 의 내용을 정리하다가 문득 궁금증이 생겼습니다. 정명주 강사님께서 `Layered Architecture 의 단점으로 인해, Hexagonal(Onion) Architecture..

happy-coding-day.tistory.com

 

요즘에는 만들면서 배우는 클린 아키텍쳐라는 책을 읽고 있습니다.

 

이 책에서는 Layered Architecture 에 대한 단점을 언급하는 부분이 있어, 이 부분 또한 2탄으로 정리할 필요가 있겠다 라는 생각을 가졌습니다.

 

첫번째 이야기는 2021-07에 작성했으니까, 9개월만이네요.

 

첫번째 이야기 포스트에서 나왔던 내용을 쉽게 요약하면 도메인 영역과 인프라 영역이 혼재되어 있고, 이를 해결하기 위해 의존성 역전법칙을 사용한다. 입니다.

 

이번 포스트에서 이야기해볼 부분은 Layered Architecture의 단점은 'Layered Architecture는 데이터 관점의 사고방식을 유발시킨다.' 입니다.

 

우리는 유연한 소프트웨어를 만들기 위한 어떤 고민을 했었을까요? 소속되어 근무하는 곳에서는 유연한 소프트웨어를 만들기 위한 몃가지 암묵적으로 모두가 공유하고 있는 룰이 있는 것 같습니다.

 

 그 중에 첫번째가 바로 '데이터적인 사고방식을 가지지 말자. 우리는 객체지향을 하고 있는 것이기 때문에 데이터 관점으로 서비스를 바라보지 않아야 한다.'

 그 외에도 '기존의 것에 의존적이지 않아야 한다.' '코드는 비지니스를 표현해야될 도구이다.' 등의 암묵적인 규칙이 존재합니다.

 

그림 1 - Layered Architecture

왜 전통적인 Layered Architecture는 데이터 관점의 사고방식을 유발시킬까?

대부분의 애플리케이션을 개발 시작할 때, 우리는 어떻게 시작했었을까여?

 

아마도, 대다수의 사람들은 데이터베이스를 먼저 설계하는 행위를 했었을 것입니다. 그것이 잘못된 것이라 말하는 게 아니라, 그렇게 해야만 Layered Architecture 를 만족시킬 수 있기 때문이라고 생각합니다.

 

그림 1 - Layered Architecture 를 살펴보면, 가장 아래에 Infrastruture 가 존재합니다. 그러므로, 의존성의 방향에 따라 순서대로 자연스럽게 우리는 지금까지 그렇게 행동해온 것입니다.

 

 데이터베이스 설계가 첫번째로 행해지고, 이는 곧 도메인으로 연결이 됩니다. 바로 ORM(Object-relational mapping) 프레임워크를 사용하게 되면서 말이죠.

 

 여기서 말하는 도메인은 과연 그림 1에서 말하는 도메인레이어에 위치하게 되는 걸까요? 어떤 경계로 도메인과 인프라스트럭쳐를 나눌수 있을까요? ORM 프레임워크를 사용한다는 것이 도메인 영역을 만들어 낸 것일까요?

 

아래 코드는 도메인 영역일까요?

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Getter
@NoArgsConstructor
@Entity
public class Posts {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

 

다시한번 물어보고 싶습니다.

위 코드는 도메인 레이어에 위치할 수 있는 코드 일까요?

 

만약 JPA 를 사용하고 있었는데, JDBC 로 변경하고 싶다면, 위 코드도 같이 변경되어져야 합니다. 즉 영향을 미칩니다. 의존성을 가집니다. 결합도가 높습니다.

 

 이 말의 의미는 위 코드는 도메인 레이어에 위치할 수 없다는 것을 말합니다. 그림 1 에서 보이는 것과 같이 도메인은 인프라스토럭쳐를 바라보고 있기 때문에, 인프라스트럭쳐가 변경되어진다 하더라도, 도메인은 변경되어지지 않아야 합니다.

 

위 코드는 엄밀히 따지면 영속성 계층에 포함되고, 이는 인프라스트럭쳐 레이어에서도 도메인에 가까운 쪽에 위치하는 것이라 생각합니다.

 

여전히 우리는 순수한 도메인을 다루지 못했고, 전통적인 Layered Architecture 는 여전히 우리에게 데이터베이스 관점의 생각을 무의식적으로 강요하고 있다고 생각합니다.

 

그 밖에도 어떤 문제점이 있을까요?

전통적인 레이어드 아키텍쳐는 테스트하기 어려운 문제점을 야기시킵니다.

 

흔히 'Xxxservice' 라고 하는 것들에 대한 테스트의 어려움이 존재합니다.

 지금까지 몇몇의 개발자와 함께 일하면서 늘 느끼는 것인데, 만약 Posts 라는 Entity 가 있다라면, 정말 많은 개발자는 PostsService 라는 것을 관례처럼 만들고 여기에서 비지니스 로직을 포함시키려고 했었습니다. 그렇게 해야만 했던 이유는 전통적인 레이어드 아키텍쳐는 인프라가 가장 아래가 있고, 인프라 영역을 사용하기 위해서 일 것입니다. 그리고 PostSerivce 가 도메인 레이어에 위치하게 될 것입니다.

그림 2 - 서비스가 유발하는 문제점

 

 여기서 PostsService 는 무슨 역할일까요? 아무도 모릅니다. 약간 관례적으로 이렇게 사용하는 경향이 있는게 아닐까 조심스럽게 추측됩니다. 왜 이 클래스 이름은 PostsService 가 되고, 이것이 뜻하는 바가 무엇인지 납득하기 어렵습니다. 그리고 이것은 비지니스 로직을 테스트하기 위해서 많은 Mocks 를 만들어 내야만 합니다.

 

작은 코드 일때는 Mocks 1~2개 만들면 되겠지만, 만약 의존성을 가진 또다른 객체가 20~30개가 된다면 그 만큼의 Mocks 를 만들어내야만 합니다.

 

점점 규모가 커짐으로서 테스트하기가 어려워 질 것입니다.

 

DDD 에서 말하는 도메인서비스가 있습니다. 여기서는 도메인서비스에 대해서 깊이있게 다루지는 않을 것이지만- 도메인서비스는 서로다른 도메인이 협력관계일 때 사용되는 객체입니다.

전통적인 레이어드 아키텍쳐는 유즈케이스를 숨깁니다.

앞서 언급했었던 데이터베이스 관점의 사고방식은 객체의 행동이 아니라, 속성을 무의식적으로 생각하게 유도합니다.

이는 객체가 가져야할 역할, 책임을 오롯히 가져야할 이유를 상실하게 만드는 역할을 합니다.

 

또한 유즈케이스를 숨기게 되는데 이를 뜻하는 바가 무엇이냐면,

 

전통적인 레이어드 아키텍처(?) 예를 들어 Wallet 이라는 도메인이 있다면, 아래와 같은 코드가 있다고 

@RequestMapping(value = "/xxxxxxxx")
@RestController
public class WalletApiController {
    private final WalletService walletService;
    private final WalletResponseConverter walletResponseConverter;

    public WalletApiController(WalletService walletService, WalletResponseConverter walletResponseConverter) {
        this.walletService = walletService;
        this.walletResponseConverter = walletResponseConverter;
    }
	...
}

 

위와같은 코드가 있다면, WalletService 가 의미하는 바가 무엇일까? 어떤 기능이 숨겨져있는걸까?

 

무엇을 표현하는지 알려주지 않고 있다. 이게 핵심이다. 유즈케이스를 숨긴다. 많이 양보해서 DeleteWalletService 라 하는 것이 그나마 유즈케이스를 드러내고 있지만, 이 또한 명확하지 않습니다.

 

 그렇기 때문에 전통적인 레이어드 아키텍쳐는 유즈케이스를 숨기고 있습니다. 그렇다면 핵사고날은 어떠할까요?

 

핵사고날 아키텍쳐에서는 유즈케이스 라는 것이 존재합니다.

코드로서 어떤 상황인지 표현함으로써, 서비스라는 용어는 아키텍쳐 안에 숨겨집니다.

@WebAdapter
@RestController
@RequiredArgsConstructor
public class StoreEventsController {

    private final RecordPublishedEventUseCase recordUseCase;

    @PostMapping(value = "/api/event")
    public ResponseEntity storeEvent(@RequestBody EventsRequest value) {
        RecordedEventCommend commend = new RecordedEventCommend(value.getId().get(), value.getType(), value.getPayload());
        if (!recordUseCase.store(commend)) {
            return ResponseEntity.badRequest().body(EventsResponse.of(value.getId().get()));
        }
        return ResponseEntity.ok().body(EventsResponse.of(value.getId().get()));
    }
...
}

 

핵사고날에서는 위 코드에서 RecordPublishedEventUseCase 와 같이 어떤 유즈케이스를 나타내는지 표현해주고 명시적입니다.

 

댓글