본문 바로가기
독서/자바,스프링 개발자를 위한 실용주의 프로그래밍

8. 레이어드 아키텍처

by Thinking 2024. 11. 10.

8.1 레이어드 아키텍처의 최소 조건

레이어드 아키텍처는 애플리케이션을 레이어로 나누고, 각 레이어에 역할을 정합니다. 꼭 유념해야 할 사실이 하나 있는데, 여러 개발자의 필요에 의해 발전된 아키텍처입니다. 레이어드 아키텍처에서 중요한 것은 레이어 유형을 외우고, 그에 맞게 컴포넌트를 배치 하는 것이 아니라 아래와 같은 사항을 지키는 것이죠.

1) 레이어 구조를 사용한다.

2) 레이어 간 의존 방향은 단방향으로 유지한다.

3) 레이어 간 통신은 인접한 레이어에서만 이뤄지게 한다.

 

위 2), 3) 같은 제약 즉, 레이어 '구조'에 제약을 더했더니 '아키텍처'가 됐다는 의미입니다. 그래서 '아키텍처'가 뭘까요? 명확한 답은 없지만 아키텍트들이 아키텍처를 설명할 때 공통으로 말하는 내용을 취합해 아키텍처란 무엇인지를 대략 이해할 수 있습니다. 아키텍트들이 아키텍처를 설명할 때 한 가지 특징은 바로 '제약 조건'입니다.

 

아키텍처는 정책과 제약 조건을 이용해 목적을 달성합니다. 아키텍처는 제약 조건을 이용해 개발자가 해도 되는 것과 하지 말아야 하는 것을 결정합니다. 더 나아가, 해서는 안 되는 일이 개발 단계에서 일어나지 않게 원천적으로 차단합니다. 제약 조건이라는 말은 규칙으로 이해해도 되고, 이를 따를 때 규칙이 설계를 만듭니다. 그러므로 아키텍처는 형태가 아니라 정책에 가깝죠.

 

'아키텍처는 정책과 제약 조건을 이용해 목적을 달성한다'라는 말에 주목할 만한 점이 하나 더 있습니다. 바로 '목적을 달성하기 위해' 제약 조건을 사용한다는 것입니다. 즉, '목적이 무엇인가'에 따라 같은 아키텍처를 사용하더라도 제약 조건은 달라질 수 있다는 의미입니다. 제약 조건도 수단일 뿐이고, 제약 조건을 사용하는 것은 아키텍처의 특징일 뿐이지, 아키텍처를 정의하는 것은 아닙니다. 특히 레이어드 아키텍처를 예로 제약 조건의 최소는 아래로 정의할 수 있습니다.

1) 레이어 구조를 사용한다. -> 레이어 아키텍처를 사용하므로 레이어 구조를 사용하는 것은 당연합니다.

2) 레이어간 의존 방향은 단방향으로 유지한다. -> 레이어 간 의존 방향이 양방향이 되면 레이어가 사라집니다.

 

 

8.2 잘못된 레이어드 아키텍처

2가지 질문을 해보죠. 계정 시스템의 백엔드를 만들 때 어떤 작업부터 하실 건가요? 아래 모두 조금씩 문제가 있습니다.

1) JPA 엔티티 우선. JPA 엔티티를 어떻게 만들지 고민한다.

2) API 엔드포인트 우선. 계정 시스템의 API 엔드포인트를 어떻게 만들지 고민한다.

 

8.2.1 JPA 엔티티 우선 접근

JPA 엔티티를 먼저 떠올렸다는 말은 사실 DDL을 어떻게 만들지 고민했다는 말과 같습니다. 왜냐하면 실제로 JPA 엔티티를 만들면 결국 SQL 형태로 DDL이 만들어지기 때문입니다. 이는 데이터 위주의 사고, 절차지향적인 사고를 유도한다는 점에 주목해보죠. 기능을 만들기 위해 데이터베이스가 먼저 만들어져야 한다는 말은 소프트웨어 개발 의사결정에 데이터베이스가 깊이 관여한다는 의미입니다.

 

해당 애플리케이션은 요구사항에 맞는 데이터베이스가 선정되는 것이 아니라 데이터베이스 에 맞는 기능을 개발하는 방향으로 발전하게 됩니다. 이를 '프로그램이 데이터베이스에 종속됐다'라고 합니다. 결론적으로 JPA 엔티티를 우선 개발하겠다는 것은 레이어드 아키텍처를 상향식 접근 방식을 사용하겠다는 의미입니다. 그러니 좋은 전략이 아니죠.

 

 

8.2.2 API 엔드포인트 우선 접근

해당 유형의 개발자는 시스템을 인터페이스 관점에서 어떻게 사용해야 할지를 먼저 떠올리죠. 컨트롤러를 먼저 생각하는데, 핸들러를 어떻게 작성해야 할지 고민하고, 어떤 매개변수가 필요한지 고민합니다. 생각의 흐름이 컨트롤러, 비즈니스 로직, 인프라 스트럭처와 JPA를 고민하는 방식으로 이뤄집니다. 이는 레이어드 아키텍처에서 하향식 접근법을 택한 것이라 볼 수 있죠.

 

해당 방식은 프로젝트가 스프링 프레임워크에 종속될 수 있습니다. API를 고민한다는 것은 기술 스펙을 결정하는 일입니다. 도메인이 무엇인지 파악하기 전부터 이미 API 서버를 만들겠다는 목적을 드러내는 것과 같기 때문입니다.하지만 우리의 시스템은 API 서버뿐만 아니라 다양한 유형의 서버가 될 수 있습니다. 그렇기 때문에 우리는 '스프링 API 서버 개발자'라고 불리는 것이 아니라 백엔드 서버 개발자라고 불립니다.

 

 

8.2.3 본질을 다시 생각하기

'지나치게 의존한다'라는 말은 애플리케이션이 그것 없이는 설명할 수 없는 상태라는 말입니다. 진정한 의미의 백엔드 개발자는 스프링이나 JPA 없이도 성립할 수 있는 애플리케이션을 만들 수 있어야 한다는 것입니다. 스프링과 JPA가 없는 상태에서도 애플리케이션을 만들 수 있어야하고, 특정 기술에 종속되지 않아야 한다는 것입니다. 그러기 위해 순수 자바 코드로 객체지향적인 애플리케이션을 먼저 만들 수 있어야 한다는 의미입니다. 스프링 같은 웹 프레임워크나 JPA 같은 영속성 라이브러리는 그 애플리케이션에 얹힐 뿐입니다.

 

애플리케이션의 본질은 도메인입니다. 애플리케이션을 개발한다는 것은 도메인을 파악하고, 이에 따른 도메인 모델을 구성하고, 도메인 모델을 표현하는 데 적합한 언어를 선택하고, 도메인 모델을 만들고, 도메인 기능을 제공할 기술을 선택한다는 것입니다. 아키텍처에 관해 설명할 때 '세부 사항에 대한 결정은 최대한 뒤로 미뤄라'라는 격언이 있습니다. 스프링과 JPA는 세부사항입니다. 그럼에도 기술 스택을 미리 정해두고 프로젝트를 시작하는 실수를 자주 저지릅니다. 하지만 이 반대가 되어야 하고, 도메인에 따른 기술 스택이 결정돼야 합니다.

 

 

8.3 진화하는 아키텍처

시스템 개발의 첫 시작을 도메인으로 두면 됩니다. 시스템 개발은 도메인 분석과 도메인 개발에서 출발해야 합니다. 이를 염두하고, 레이어드 아키텍처를 어떻게 진화시켜야 하는지 논의해보죠.

 

 

8.3.1 인지 모델 변경하기

개발의 첫 시작을 도메인을 개발하려면 어떤 레이어부터 개발하는 것이 좋을까요? 도메인을 개발하려면 비즈니스 레이어부터 개발하면 됩니다. 그런데 방금까지 도메인을 먼저 개발해야 한다고 하지 않았나요? 서비스 컴포넌트는 분명 비즈니스 레이어에 속하는 컴포넌트이지만 '비즈니스 레이어 = 서비스 컴포넌트'인 것은 아닙니다. 비즈니스 레이어는 도메인까지 포함하는 개념입니다. 도메인이란 비즈니스 영역이라고 말했고, 애플리케이션 개발을 위한 첫걸음으로 비즈니스 레이어, 그리고 레이어 중에서도 도메인을 가장 먼저 개발해야 합니다.

 

왜 그동안 잘못된 접근을 했을까요? '인지 모델'이 잘못됐기 때문이죠. 비즈니스 레이어에서 도메인을 분할했습니다. 이를 도메인 레이어라는 이름으로 인지 모델에 추가했습니다. 도메인 레이어는 비즈니스 도메인을 표현하는 클래스가 모이는 곳입니다. 즉, 도메인 레이어의 객체지향적인 도메인 모델들이 활동하는 공간입니다.

Controller(Presentation) -> Service(Application) -> Domain(Domain) -> JpaRepository(Infrastructure)

 

패키지 이름이 core, common 같이 작성되는데, 이는 프로젝트에서 사용하는 공통 코드들을 한 군데에 모아서 관리하겠다는 뜻입니다. 하지만 이는 도메인 레이어의 목적과 다릅니다. 애플리케이션에서 사용하는 주요 도메인 객체들을 표현하는 것입니다. 패키지와 '레이어'는 다릅니다. 레이어를 표현하기 위한 수단으로 패키지를 사용할 수 있지만, 패키지 하나하나가 레이어드 아키텍처의 모든 레이어에 대응해야만 하는 것은 아닙니다. 모든 패키지가 레이어드 아키텍처에서 말하는 레이어에 일대일로 대응해야 되는 것은 아니고, 레이어라는 것은 패키지나 모듈 같은 형태가 아니라 개념입니다.

 

그래서 도메인 레이어는 애플리케이션에서 사용하는 주요 도메인 객체를 모아두기만 하면 될까요? 하나 더 있습니다. common, core 같은 패키지가 도메인 레이어가 되려면 이 레이어에 있는 객체들을 작성할 때 '순수 자바 코드'로 작성해야 한다는 점입니다. 이것은 도메인 레이러를 외부 라이브러리에 의존하지 않고, 자유롭게 만들기 위함입니다.

 

쉽게 말해 이 레이어에 작성된 클래스에는 @Entity 같은 JPA 애너테이션이 지정돼 있지 않아야 합니다. 외부 라이브러리에 강결합하는 형태로 의존해서는 안됩니다. 이 모든 것은 도메인 레이어가 특정 라이브러리나 프레임워크에 종속되는 상황을 피해 도메인을 순수하게 유지하기 위함입니다. @Service 같은 스프링 애너테이션도 지정돼 있지 않아야 합니다. 혹여나 외부 연동이 필요하다면 의존성 주입을 사용해 연동된 컴포넌트를 언제든 교체할 수 있게 만듭니다.

 

비즈니스 레이어 = 애플리케이션 레이어 + 도메인 레이어

 

'순수 자바 코드'를 강조하는 이유는 다른 기술에 의존하지 않을 때 얻을 수 있는 또 다른 장점이 있습니다. 도메인 레이어에 있는 클래스가 외부 라이브러리에 종속되지 않을 수 있다면, 도메인 레이어는 외부 라이브러리 없이도 개발할 수 있게 됩니다. 즉, 도메인에 집중해 도메인을 먼저 설계하고, 외부 라이브러리는 나중에 선택하게 할 수 있다는 뜻이죠.

 

추가로 불변 객체의 값을 변경하고 싶을 때, 불변 값의 수정자는 새로운 객체를 반환한다는 의미를 담아 전치사 with로 메서드 이름을 시작하기도 합니다. 영속성 객체와 도메인 객체를 변환할 때 사용하는 메서드를 가리켜 매핑 메서드라고 합니다.

 

마지막으로 도메인 레이어가 인프라스트럭처 레이어 위에 존재하지만 도메인 레이어는 인프라스트럭처 레이어를 참조하지 않는다는 사실입니다. 또한 도메인이 의존하는 외부 레이어가 존재하지 않습니다. 도메인 레이어는 JPA, 스프링 라이브러리를 임포트하면 안되고, 다른 레이어와의 결합을 완젆 끊어내기 위함입니다. 

 

 

8.3.2 JPA와의 결합 끊기

만약 JPA 대신 다른 기술을 사용한다고 했을 때 영향을 받는 범위는 Presentation, Application 입니다. 특히 애플리케이션 서비스는 가장 큰 영향을 받고, 유지보수하는 것이 어려워진다는 것입니다. 구체적인 예로, 'MyBatis를 JPA로 변경하기 위해 노력해 봤지만 실패했다'라는 이야기가 종종 있는데, '너무 많은 코드가 MyBatis에 결합돼 있기 때문에 JPA로 변경하는 것이 불가능하다'라는 점이 거론됩니다. 즉, 인프라스트럭처 레이어의 구성이 달라져도 애플리케이션 레이어가 영향을 받지 않는 구조로 설계해야 합니다. 바로 의존성 역전을 사용하는 것입니다.

 

애플리케이션 레이어에 Repository라는 인터페이스를 새롭게 배치합니다. JpaRepository와 결이 다른 인터페이스인데, 아무런 상속 관계를 갖지 않는 순수한 인터페이스입니다. 이 인터페이스의 구현체는 인프라스트럭처 레이어에 위치합니다. 구현체 RepositoryImpl은 JpaRepository를 이용해 도메인을 불러옵니다.

 

스프링의 의존성 주입은 타입을 기반으로 동작하기 때문에 인터페이스를 구현한 구현체가 알아서 주입됩니다. 또한 프로젝트는 더이상 DB형태에도 영향을 받지 않게 됩니다. 이는 레이어 간 연결에 의존성 역전을 적용하여 유연한 설계를 얻게 된 결과입니다. 의존성 역전은 경계를 만드는 기술이고, 변경에 의한 영향 범위가 더 이상 확산되지 않게 도와줍니다.

 

 

8.3.3 웹 프레임워크와 결합 끊기

이를 프레젠테이션 레이어에도 적용할 수 있을까요? AccountService는 프레젠테이션 레이어의 인터페이스로 지정, 그리고 AccountService 인터페이스의 구현을 애플리케이션 레이어에서 구현하게 했습니다. 하지만 이는 2가지 견해가 있을 수 있죠.

 

긍정적 해석

'의존성 역전을 적용해 유연한 구조를 갖게 됐으니 긍적적이다'라고 보는 해석입니다. 이 주장을 뒷받침할 수 있는 사실이 하나가 있으니 바로 '핵사고날 아키텍처'입니다. 헥사고날 아키텍처는 애플리케이션의 핵심인 내부 세계(도메인)와 바깥 세계(웹 요청, DB 호출)를 각각 나눠어떻게 다뤄야 하는지 주목합니다.

 

좌측 입력(input) 영역은 애플리케이션을 사용하는 사용자들의 입력을 표현. 사용자가 백엔드 서버를 대상으로 API 호출을 하는 상황. 사용자는 입력 어댑터를 통해 API를 호출하고, 입력 어댑터는 입력 포트를 통해 입력 받은 내용을 ServiceImpl 컴포넌트로 전달합니다. ServiceImpl 컴포넌트는 우리가 알던 애플리케이션 서비스입니다. DB에서 도메인을 가져오고, 네트워크를 호출하고, 도메인에 업무를 위임하는 역할을 하죠.

 

우측의 출력(input) 영역은 도메인을 영속화하거나 다른 시스템을 호출하는 상황을 표현합니다. 예로 ServiceImpl 컴포넌트는 출력 포트를 통해 DB에서 도메인을 가져옵니다. 출력 포트의 구현체는 출력 어댑터로 구현돼 있습니다. 마치 Repository 인터페이스와 RepositoryImpl 클래스 같은 관계입니다. 이것이 헥사고날 아키텍처를 설명의 간략입니다.

 

사실 그림 8.11에서 계층 구조를 없애고, 컴포넌트를 위처럼 다시 재배치하면 같은 그림을 얻을 수 있습니다. 다이어그램을 변경하고 나니 컴포넌트가 의존하는 방향과 유형(화살표의 방향, 화살표의 생김새)이 완전히 일치하죠. 레이어드 아키텍처를 발전시켰더니 헥사고날 아키텍처에 가까운 결과를 얻었습니다. 왜냐하면 이 책에서 레이어드 아키텍처를 진화시켜 온 과정이 클린 아키텍처와 헥사고날 아키텍처가 나오게 된 배경을 답습한 것이기 때문입니다.

 

헥사고날 아키텍처에서 외부 세계를 다루는 방법을 표현하고자 포트-어댑터 패턴을 사용했다고 이야기 했는데, 사실 의존성 역전을 사용한 것입니다. 의존성 역전의 이름이 다른 이름으로 포트-어댑터이기 때문이죠. 결론은 프레젠테이션 레이어와 애플리케이션 레이어에도 의존성 역전을 사용하도 됩니다. 이렇게 진화시킨 레이어드 아키텍처는 시스템의 변경 사항이 최소화되고, 유지보수 및 테스트가 용이해진다는 것이죠.

 

이렇게 프레젠테이션 레이어에 의존성 역전을 적용하면 애플리케이션과 웹 프레임워크의 결합도 끊을 수 있게 됐다고 평가할 수 있습니다. 이는 웹 프레임워크인 spring-web과 결합이 끊어진 것이지, Spring 프레임워크와 결합이 끊어진 것은 아닙니다. 

 

 

부정적 해석

굳이 프레젠테이션 레이어에 의존성 역전을 적용할 필요가 있느냐고 주장합니다. 요지는 프레젠테이션 레이어에 의존성 역전을 적용한다고 해도 얻을 수 있는 이점이 모호하다는 점이죠. 다시 말해, 프레젠테이션 레이어를 하위 레이어인 애플리케이션 레이어, 도메인 레이어의 변경으로부터 독립시키고 싶을 때 의존성 역전을 적용하는 것이 유의미합니다. 그렇지 않는 경우는 그 효과가 굉장히 미미하죠.

 

그런데 상식적으로 프레젠테이션 레이어는 그대로인데 애플리케이션 레이어가 변경되는 일이 일어나긴 어렵습니다. 애플리케이션의 본질인 애플리케이션, 도메인 레이어가 다른 형태로 변경될 일은 없기 때문입니다. 더불어 프레젠테이션 레이어의 코드는 보통 재사용될 수 없습니다. 그러니 의존성 역전을 적용했을 때 이점은 더더욱 희석되죠.

 

API는 도메인 시스템을 사용하기 위한 인터페이스입니다, 즉, 컨트롤러와 핸들러를 다른 프로젝트에서 사용한다는 것은 사실상 도메인이 완전히 일치하는 시스템일 때나 가능한 일입니다. 이러한 이유로 의존성 역전을 프레젠테이션 레이어에 적용하는 것이 무의미 하다는 주장이 있죠.

 

 

평가

결과적으로 정리하면 아래와 같습니다.

 

긍정

1) 경계를 강제할 수 있게 됩니다.

2) 외부 세계와 내부 세계에서 벌어지는 모든일에 일관된 패턴을 적용할 수 있습니다.

3) 프레젠테이션에 레이어에 있는 컴포넌트를 테스트해야 할 때 테스트가 쉬워집니다.

 

부정

1) 의존성 역전을 적용했을 때 얻을 수 있는 실효성이 모호합니다.

2) 의존성 역전을 적용하지 않아도 도메인 모델은 외부 세계에 독립적입니다.

3) 애플리케이션 레이어가 프레젠테이션 레이어에 의존하는 것이 부자연스럽습니다.

 

모두 의견이 정당합니다. 애초에 아키텍처의 세계에 정답은 없고, 문제 상황과 문제 상황에 부딪혔을 때 가는 과정만이 존재할 뿐입니다. 결국 소프트웨어를 개발하며 벌어지는 모든 의사결정은 트레이드오프 싸움입니다. 

 

 

8.4 새로운 접근법

도메인 레이어부터 개발한다는 것은 명백한 상향식 접근법입니다. '도메인을 개발', '애플리케이션 서비스를 개발', '애플리케이션 서비스가 사용할 인터페이스를 구성', '이에 대응하는 컨트롤러, JPA를 만들어 시스템을 완성해야 한다'라고 했습니다. 이는 상향식 접근법을 따르죠.

 

헥사고날 아키텍처와 같은 그림입니다. 아키텍처의 핵심은 생김새가 아닙니다. 어디까지가 어떤 레이어읹, 이게 육각형 모양인지, 팔각형 모양인지가 중요한 것이 아니라 '왜 이런 형태를 띠고 있는지'가 중요합니다.

 

 

8.5 빈약한 도메인

단순한 애플리케이션을 만드는 데 복잡한 아키텍처를 적용하는 것은 오버 엔지니어링에 속합니다. 대부분의 소프트웨어 개발은 언더 엔지니어링으로 이뤄지는 경우가 많으므로, 항상 이를 경계해야 합니다.

'독서 > 자바,스프링 개발자를 위한 실용주의 프로그래밍' 카테고리의 다른 글

9. 모듈  (0) 2024.11.13
7. 서비스  (0) 2024.11.12
[6-11 중] 6. 안티패턴  (2) 2024.11.09
5. 순환참조  (1) 2024.11.09
4. SOLID  (0) 2024.11.09