Clean-Architecutre #5-3부 - 아키텍트의 경계설정과 그에 따른 비용 고민에 대하여

Clean-Architecutre #5-3부 - 아키텍트의 경계설정과 그에 따른 비용 고민에 대하여

클린 아키텍처를 유지하는 방법, 경계설정에 대하여

23. 프레젠터와 험블 객체

✒︎ Q: 험블 객체란 무엇인가?

험블 객체 패턴은 행위를 테스트하기 어려운 부분과 쉬운 부분으로 나누는 패턴이다. 이 중에서 테스트하기 어려운 부분을 험블 객체라고 부른다.

복잡한 로직이나 의존성을 가진 부분과, 테스트가 어려운 부분(예를 들어, 파일 입출력, 네트워크 통신 등)을 분리하여
최대한 많은 부분을 테스트 가능하게 만드는 것이 목적입니다.

✒︎ Q: 왜 험블 객체 패턴을 사용해야 하는가?

험블 객체 패턴을 사용해서 코드의 테스트 용이성을 높이고, 최대한 많은 코드를 외부 위협 없이 테스트할 수 있다.
이런 노력은 결국 시스템의 안정성을 증가시키는 방향으로 발전한다.

🌟클린 아키텍처에서 경계란 무엇인가?

시스템 내에서 도메인 로직과 외부 요소(예를 들어, 입출력, 네트워크 통신, 데이터베이스 등)가 만나는 지점을 의미한다. 험블 객체 패턴을 사용하여 아키텍처의 경계를 더욱 명확히 식별하고 보호할 수 있다.

✒︎ 경계 보호의 이점

  • 테스트 용이성: 험블 객체 패턴을 통해 경계를 구분하면, 도메인 로직을 담고 있는 부분은 쉽게 테스트할 수 있다. 이로써, 높은 코드 커버리지와 안정성을 달성할 수 있다.
  • 유지보수성: 시스템의 한 부분을 수정할 때 다른 부분에 미치는 영향을 최소화할 수 있다.
  • 코드의 명확성: 경계가 명확하면, 코드를 읽고 이해하는 데 있어 그 목적과 역할이 뚜렷해진다. 이는 코드의 가독성과 명확성을 높여준다.

흠.. 그럼 험블 객체는 아키텍처의 경계에서 안정성을 위해 분리된 구체적이고 테스트가 힘든 저수준 계층의 객체인가보다. Ex) DB SQL 직접 접근 레포지토리 구현체 예시로 코드를 하나 짜보자.

김현우_험블 Usecase - Gateways - DB를 예시로 짜보자

Usecase와 DB 사이에는 Gateways가 존재한다.
Usecase는 SQL의 S자도 모른다. 그냥 Gateway Interface에게 요청하고, Gateway는 적절한 DB에 맞춰 구현체를 생성하여 요청에 응한다.

// Usecase
public interface UserLoginDataUseCase {
    User[] getUserLoginData(Date startDate, Date endDate);
}

// UsecaseImpl
public class UserDataService implements UserLoginDataUseCase {
    private DatabaseGateway databaseGateway;

    public UserDataService(DatabaseGateway databaseGateway) {
        this.databaseGateway = databaseGateway;
    }

    @Override
    public User[] getUserLoginData(Date startDate, Date endDate) {
        return databaseGateway.getLastNamesOfUsersWhoLoggedInBetween(startDate, endDate);
    }
}

// DatabaseGateway (테스트 가능)
public interface DatabaseGateway {
    User[] getLastNamesOfUsersWhoLoggedInBetween(Date startDate, Date endDate);
}

// Database (테스트 어려움)
public class Database implements DatabaseGateway {
    @Override
    public User[] getLastNamesOfUsersWhoLoggedInBetween(Date startDate, Date endDate) {
        // DB 접근
        return null;
    }
}

  • 로그인 유저에 대한 데이터를 가져오는 Usecase를 만들었다.
  • Usecase는 DatabaseGateway를 통해 데이터를 가져온다.
  • DatabaseGateway는 Database를 구현한 구현체를 통해 데이터를 가져온다.
  • Database는 실제 DB에 접근하여 데이터를 가져온다.

아키텍처의 경계마다 험블 객체 패턴을 적용하면, 테스트 가능한 코드와 테스트 어려운 코드를 분리할 수 있다.
경계를 넘나드는 통신은 모두 간단한 데이터 구조를 수반할 때가 많고,
대개 그 경계는 테스트하기 어려운 무언가와 테스트하기 쉬운 무언가로 분리될 것이다.

이 구조를 사용하면, 테스트 가능한 DatabaseGateway를 테스트 더블로 대체하여 UserDataService의 테스트가 용이해진다.

✒︎ Q: 그럼 JPA같은 ORM은 어디에?

하이버네이트같은 ORM은 Infra Adapter Layer인 데이터베이스보다 안쪽에 속한다.
즉, Gateway Interface와 데이터베이스 사이에서 일종의 또 다른 험블 객체 경계를 형성한다.

저자는 ORM(객체 관계 매퍼)는 사실 존재하지 않는다고 말한다.

ORM은 객체와 관계형 데이터베이스 사이의 매핑을 의미한다.
그런데 객체지향 프로그래밍과 관계형 데이터베이스는 본질적으로 다른 패러다임을 가진다.

이런 “객체-관계 불일치” 문제는 ORM을 사용하더라도 완전히 해결되지 않는다.
그래서 객체-관계 매퍼보다 데이터 매퍼 로 부르는 편이 낫다고 한다. 관계형 데이터베이스 테이블로부터 가져온 데이터를 구조에 맞게 담아준다고 보기 때문이다.

특성객체 (Object-Oriented)관계형 데이터베이스 (RDBMS)
데이터 저장 형태클래스와 인스턴스테이블과 레코드
캡슐화변수 캡슐화하여
Operation 집단
상태만 저장
상속지원 (클래스 상속)일반적으로 지원하지 않음
다형성지원지원하지 않음
데이터 접근객체 메서드를 통해 접근SQL 쿼리를 통해 접근

객체와 관계형 데이터베이스의 차이

저자는 JPA의 Entity와 순수 도메인객체와의 분리를 하는 것을 염두에 둔 듯 하다.
JPA의 Entity는 단순히 RDB의 테이블 데이터를 매핑해 가져와주는 객체라고 생각이 든다.(설득됐다)

24. 부분적 경계

처음부터 아키텍처의 모든 구조를 설계해놓고 사용할 순 없다. 아키텍처는 점진적으로 발전해야 한다.
그렇다고 처음부터 전부 단일 컴포넌트에 넣고 설계를 시작할 수는 없는 노릇이다.

아키텍처의 완벽한 경계들을 생성하고 유지하는데 많은 비용이 든다.

저자는 아래와 같은 비용이 든다고 말했다.

-쌍방향의 다형적 Boundary 인터페이스 (경계를 넘나드는 통신을 위한 인터페이스)
-Input과 Output을 위한 데이터 구조
-두 영역을 독립적으로 컴파일하고 배포할 수 있는 컴포넌트로 격리하는데 필요한 의존성 관리 노력

완벽한 설계로 시작을 하면 비용이 많이 들고 개발 속도가 늦어진다. 근데 완벽한 경계를 만들지 않으면, 나중에 경계를 만들기가 어려워진다.

그럼 어떻게 하지? 부분적 경계로 만들어보자!

그래서 완벽한 경계를 구성하여 모두 분리하는 것이 아닌, 확장성이나 추후 완벽한 경계를 염두에 둔 채 부분적 경계를 구성하는 방법이 있다.
다음의 세 가지 부분적 경계를 선택한 방법을 보자. 각 방법은 명확한 장점과 단점이 있다.

🌟 마지막 단계를 건너뛰기

독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 채, 이들을 하나의 컴포넌트에 그대로 모아두는 것이다.

개발은 경계적으로 나눠서 진행하고 컴포넌트 분리를 건너뛰는 것이다.

  • 장점
    • 다수의 컴포넌트를 관리하는 작업은 하지 않아도 된다. 배포에 대한 관리 부담도 없다.
  • 단점
    • 결국 완벽한 경계를 만들 때와 동일한 코드량과 사전 설계가 필요하다.
    • 추후에 컴포넌트가 발전되고, 드디어 분리될 시기에 많은 노력이 필요하다.

개인적인 의견으로는 추후에 필요에 의해 발전된 코드를 컴포넌트별로 분리하는 노력보다

어차피 설계부터 완벽한 경계와 같은 노력이 든다면 그냥 분리해서 만드는게 맘이 더 편하지 않을까 싶다.
어차피 오버엔지니어링의 위험은 동일한 것 같다.

일차원 경계

기존 완벽한 경계를 유지하기 위해서는 아래와 같이 양방향 (In/Out)으로 격리된 Boundary Interface를 사용한다.

김현우_일차원경계 기존 IN/OUT 으로 나뉜 양방향 Interface

초기 설정이나 지속적으로 유지할 때 비용이 많이 든다.
클래스나 인터페이스의 개수가 많아지고, 결국 무언가 추가하고 싶은 경우에 고려해야 할 사항이 많아진다는 의미다.


김현우_일차원경계_2 하나로 묶인 Boundary Interface

In/Out으로 구분하지 않고 더 간단하게 Interface 바운더리를 축약하는 것이다.

  • 장점
    • 의존성 역전으로 구분은 해뒀기 때문에 미래에 분리시에 상대적으로 간편하다.
  • 단점
    • ServiceImpl이 바로 Client를 참조하는 비밀통로를 가지고 있기 때문에, 쉽게 붕괴될 가능성이 있다.

이는 기존에 많이 사용되고 있는 방식이다.

첫 직장에서 Service를 왜 Interface로 빼는 바보같은 반복작업을 시키는거지? 라는 궁금증이 있었던 적이 있어서 웃음이 난다.

퍼사드 패턴

✒︎ Q: 퍼사트 패턴이란?

Facade라는 단어의 뜻은 건축물의 정면을 의미한다.

내가 퍼사드 패턴을 정확하게 이해했던 예시는 컴퓨터이다.

컴퓨터 사용자는 내부의 모든 구조를 알지 않아도 본체와 모니터, 키보드와 마우스만 알면 컴퓨터를 사용할 수 있다.
CPU객체, 램 객체, 메인보드 어쩌구 저쩌구는 모두 본체라는 퍼사드를 통해 사용자에게 제공된다.
우리는 본체의 전원 메서드만 실행시키면 내부의 복잡한 로직을 몰라도 컴퓨터를 사용할 수 있게 된다.

김현우_퍼사드.png 7살때부터 스타크래프트를 했던 나는 이미 퍼사드구조를 적극 사용중이었다.

클린아키텍처에서 퍼사드 패턴은 의존성 역전까지도 희생한다.
경계는 퍼사드 패턴으로 간단희 정의된다. 클라이언트는 퍼사드만 알고 있다.

하지만 클라이언트가 퍼사드를 보고 있고, 퍼사드는 모든 서비스에 대한 의존성을 가지고 있기 때문에

이는 결국 클라이언트도 모든 서비스에 대해 추이 종속성을 가지게 된 것이다.흑흑


24장 마치며

세상의 모든 것은 trade off, 등가교환, 저울질이니…정답은 없다니 심란하다.

                                     .-===-.
                                      \   /
                                      |   |
                                    __|:::|__
       .-===-.                 _.--'  |:::|  `-._
        \   /           __    /      (:::::)     \
        |:::|          |  |   \       `---'      /
      __|:::|__        |..|    ``--...____...--''
 _.--'  |:::|  `-._   /_/\_\     ___..-(O/
/      (:::::)     \  |  __...--' __..-''
\       `---'      /_.--(o)_...--'
 ``--...____...--''__..--'_|
        \O)___..--'   \ \/ /
         .-------------|''|-------------.
        /              |__|              \
       /__________________________________\
       '----------------------------------'

trade off

25. 계층과 경계

아키텍처에서 계층과 경계를 만든다는 것은 비용이 큰 일이라고 했다.
그렇다고 경계를 무시하고 만든다면, 나중에 다시 추가하는 비용은 더욱 크다.

그래서 나보고 어떻게 하라고?..

계층과 경계를 만드는 일은 비용이 많이 든다는 경각심을 가지고, 우리는 아키텍처 경계가 언제 필요할지 신중하게 파악해야 한다.
너무 미리 경계를 정의해 추상화 하는 것은 오버 엔지니어링이다.
대부분의 경우 과한 것 보다 모자름이 낫다. 이 아키텍쳐 설계에서도 예외는 아닌가보다.

김현우_과유불급.png 명심 또 명심~

우리는 설계를 단발적으로 생각하는 경향이 있다. 건축과 비유를 많이 해서 그럴 수도 있다.
건물은 한 번 올라가면 수정이 어렵다. 그러나 다시 처음으로 돌아가 생각해보면 Software Architecture는 지속적인 수정이 핵심이다.
프로젝트 초기부터 차근차근 아키텍처를 성장시켜 나가는 것이 중요하다.
육아는 쉬운일이 아니듯이, 아키텍처를 올바르게 키우기 위해서는 지속적인 관심과 사랑이 필요하다.

훌륭한 아키텍트로써 미래를 내다보고 현명하게 추측해야 한다.

비용을 산정하고, 경계를 어디에 둘 것인지, 완벽히 구현할 경계는 어디고 부분적 경계를 할 곳은 어딘지,
오버엔지니어링 쉐도우 복싱은 아닌지, 변경이 될 것 같은데 지나친 부분은 없는지
꾸준히 프로젝트가 진행되고 소스코드가 추가되고, 구조가 추가될 때 마다 고려해야 한다.

모두를 위해 봉사하지만 그림자속에 존재하는 고담시의 다크나이트가 생각난다…

게임을 예시로 변화하는 설계 예시

그럼 지금부터 클린 아키텍처에서 나오는 아키텍트라는 다크나이트가 무슨 고민을 해야 하는지 예시로 알아보자.
저자는 움퍼스 사냥 이라는 게임을 예시로 들었다.

김현우_움퍼스.png 70년대 시작된 게임

🌟 1. 제일 컴팩트한 설계

단순히 동아리방에서 친구들끼리 할 게임을 설계해서 만든다고 생각해보자.
이동, 공격 등 단순한 기능을 가진 단일 컴포넌트로 200줄짜리 코드뭉치로도 가능할 것이다.

🌟 2. 언어적, 데이터적 경계 추가

우리가 게임이 성공해서 각 국가별로 확장하고, 데이터 저장소를 분리한다고 생각해보자.
이런 경우에는 언어적 경계와 데이터적 경계를 추가해야 한다.
Game의 핵심 Rules(불변) 을 추출해서 중심을 구성하고, In(언어적), Out(데이터 저장) 으로 분리해본다.

김현우_언어데이터경계추가 단순 경계 구성

🌟 3. 지원 매체 확장

사업이 더 잘되어서 콘솔이 아니라 모바일 시장을 점령하면 어떻게 할건가?
그래서 우리의 게임 지원 모델을 더 확대해봤다.
점선은 Interface(다형성) 객체이다.

김현우_개선된다이어그램 본격적으로 개선된 아키텍처

이제 우리는 핵심 도메인인 GameRules를 필요에 따라 영어, 스페인어 등으로 지원이 가능하다.
그리고 그 언어별로 전달되는 구현객체는 SMS, Console로 선택할 수 있다.
저장소는 Cloud 혹은 Flash 메모리에 저장할 수도 있다.

김현우_단순화된다이어그램 Client -> TextDelivery -> Language -> GameRules -> Storage

요청 IN / OUT과 계층으로 나누면 위와 같은 그림이 나올 것이다. 계층이 잘 나뉘어져 있다.

🌟 4. 온라인으로 바뀌면?

개인 플래이가 아니라 멀티유저가 사용하게 된다면 어떻게 될까?
네트워크 컴포넌트가 추가되어야 한다.

김현우_네트워크흐름추가 네트워크 흐름 추가

🌟 5. 게임이 더 커지면?

단순 Game Rules로 처리할 수가 없을 것 같다. 우리의 게임은 세계적인 게임이 되어서 모든 게임을 지배할것이다.
그러므로 MSA로 구축하고 사용자의 이동 관리는 고객의 컴퓨터에, 실제 사용자들은 서버에서 분리하여 관리하면 좋겠다.
김현우_마이크로 동아리 방에서 오버하지 말라가 절로 나오는 설계다

마무리

동아리 방에서 200줄짜리 코드에서 전 세계적인 서비스를 염두에 두기까지 어떤 생각이 들었는가?
이런 고민을 하면서 아키텍처를 설계하는 것이 아키텍트의 일이다.
어디까지가 오버고 어디까지가 언더인가..

많은 경험과 고민이 필요한 일인 것은 분명하다.

김현우_GTPT chatGPT는 T인게 분명하다