Gradle과 함께하는 Backend Layered Architecture

Riiid Teamblog
Riiid Teamblog KR
Published in
15 min readDec 11, 2020

By 백근영

백근영 님은 Riiid의 Back-end Engineer로 산타토익을 포함한 Backend Foundation 업무를 담당하고 있습니다.

개발자들은 언제나 코드의 유지보수 용이성을 염두에 두어야 합니다. 유지보수 용이성을 갖춘 소프트웨어는 기존의 코드를 최대한 건드리지 않고 모듈의 동작을 확장할 수 있게 되며, 기술의 변화에 기민하게 반응하여 어플리케이션을 더욱 세련된 형태로 발전시켜 나갈 수 있게 만들어줍니다.

소프트웨어가 이러한 유연성을 갖추기 위해서는 다양한 측면에서의 고민이 필요한데요, 이번 글에서는 그 중 code base architecture를 견고하게 설계하는 방법에 대해 이야기해보려고 합니다. code base architecture는 어플리케이션의 핵심이 되는 비즈니스 로직을 여러가지 기술적인 의존성으로부터 격리시킴으로써 확장과 유지보수를 돕는 역할을 하기 때문에, 유연성 있는 소프트웨어가 갖추어야 할 필수적인 요소 중 하나라고 말할 수 있습니다.

Riiid에서는 DDD(Domain Driven Design)에 소개된 layered architecture를 주 reference로 삼아 code base architecture를 설계하였고, 빌드 자동화 툴로는 gradle을 이용하고 있습니다. 지금부터 Riiid에서 layered architecture에 gradle을 접목시켜, layered architecture가 궁극적으로 지향하는 바를 달성하고자 했던 노력의 과정을 소개하려고 합니다.

서론

먼저 layered architecture와 gradle이 무엇인지 간단히 알아보고, gradle의 어떤 기능 때문에 layered architecture를 구현하는 데 gradle을 적극적으로 활용하게 되었는지 설명드리도록 하겠습니다.

Layered architecture란?

layered architecture란 말 그대로 계층이 나뉘어져 있는 아키텍쳐를 뜻합니다. layered architecture의 주된 목표는 어플리케이션을 여러 개의 굵직한 횡단 관심사(cross-cutting concern)로 분리해, 각각의 layer는 하나의 관심사에만 집중할 수 있도록 하는 것입니다. 각각의 layer에 대한 명칭은 출처마다 조금씩 다르지만 근본적인 역할과 목적은 같으며, 대표적으로 DDD(Domain Driven Design)에서는 아래와 같은 구조의 layered architecture를 소개하고 있습니다.

각각의 layer의 역할에 대해서는 아래에서 추가적으로 설명하겠지만, layered architecture의 궁극적인 목표는 application layer와 domain layer가 기술에 대해 가지는 의존성을 최소화하여, 오직 순수한 비즈니스 로직을 작성하는 데에 집중할 수 있게 하는 것입니다. application layer와 domain layer에 기술 의존성이 깊숙히 침투한다면 어플리케이션의 핵심 비즈니스 로직이 기술의 변경(라이브러리의 종류 혹은 버전 변경 등)에 강하게 결합 될 것이고, 이는 어플리케이션의 기능 확장과 유지보수 관점에서 엄청난 cost를 야기할 것입니다.

이렇게 뚜렷한 장점을 갖고 있는 layered architecture이지만, 각 layer의 바람직한 역할을 정의하고 이를 단순히 권고하는 수준에서 그친다면 그다지 큰 의미를 얻지 못할 수도 있습니다. 인간의 언제나 같은 실수를 반복하기 때문이죠… 휴먼 에러가 발생할 가능성을 생각한다면, layered architecture는 개념적인 가이드 수준으로만 머무를 것이 아니라 아키텍쳐 구조를 엄격하게 제어해 줄 특별한 장치들을 통해 “구현”될 필요가 있습니다. 그 장치는 바로 아래에서 설명드릴 gradle이라는 빌드 툴입니다!

Gradle이란?

gradle은 성능과 유연함에 초점을 둔 오픈소스 빌드 툴입니다. gradle은 굉장히 다양한 기능을 제공해주지만, 이번 포스트에서 집중할 gradle의 기능은 바로 멀티프로젝트 기능입니다.

Multi-Project Builds in Gradle

멀티 프로젝트 기능은 하나의 gradle project 하위에 여러개의 gradle subproject를 생성할 수 있는 기능으로, 그렇게 생성된 subproject는 각각 독립적으로 외부 의존성을 관리할 수 있다는 특징이 있습니다. 불필요한 의존성을 최소화하고, 특히 도메인 지식이 존재하는 장소에 기술에 대한 의존성을 제거한다는 layered architecture의 핵심 아이디어와 굉장히 잘 어울릴 것 같은 기능입니다! 😊

layered architecture에 gradle을 접목시켜보자

위에서 설명드렸듯이, gradle의 multi-project 기능은 layered architecture의 핵심 아이디어와 매우 잘 어울립니다. 따라서 Riiid에서는 layered architecture를 구성하는 각 layer를 하나의 gradle subproject로 만들기로 결정했고, 지금부터는 이 결정에 따른 구체적인 작업 과정을 공유하도록 하겠습니다.

본론

layer 살펴보기

layered architecture를 구성하는 각각의 layer가 어떤 역할을 하는지 먼저 간략하게 살펴보도록 하겠습니다.

presentation layer

애플리케이션의 가장 프론트 엔드에 위치하는 레이어로, 백엔드 어플리케이션의 경우 presentation layer는 아래와 같은 역할을 합니다.

  • cilent로부터 request를 받고 response를 return하는 api 정의
  • api route별 로깅, 보안 등의 전처리

presentation layer은 request와 response를 serving하는 protocol을 무엇으로 하냐에 지배적인 영향을 받습니다. Riiid의 경우 컴포넌트 간 통신에서 grpc protocol을 사용하기 때문에, presentation layer는 grpc 관련 외부 라이브러리와 Protobuf 파일에 대한 의존성을 다수 가지고 있게 됩니다.

application layer

application layer에 존재하는 함수들의 집합은 곧 “어플리케이션의 요구사항”입니다.

application layer에는 고수준으로 추상화된 “어플리케이션 기능”이 담겨 있습니다. 이 layer는 실제 비즈니스 로직을 담고 있지는 않고, domain layer에 존재하는 비즈니스 로직과 data access logic을 orchestrate하여 새로운 추상화를 창출하는 공간이라고 정의하였습니다. 구체적으로는 이 layer에 service 객체들이 담기게 될텐데요, service란 특정한 행위를 추상화하는 객체를 말합니다. “어플리케이션의 기능”이라 함은 곧 어떠한 행위를 뜻하기 때문에, application layer는 자연스럽게 service 객체들을 담게 되는 것입니다.

추가로, 평소 DDD에 관심을 갖고 계셨던 분들이라면 domain layer에도 service 객체가 존재할 수 있다는 것을 알고 계실텐데요, 저희 Riiid에서는 application service를 다음과 같이 정의함으로써 application layer와 domain layer 사이의 경계를 명확히 하였습니다.

  • application service란 어플리케이션 요구사항에 1대1로 대응되는 기능을 pure Kotlin syntax로 작성한 객체이다. 여기에는 비즈니스 로직을 작성하지 않으며, domain layer에 작성되어 있는 비즈니스 로직을 호출하기만 한다.

domain layer

domain layer는 핵심 비즈니스 로직이 담기는 곳으로, 어플리케이션의 가치를 결정하는 가장 중요한 layer입니다. 이 layer는 어떤 외부 관심사에도 의존하지 않고 순수한 비즈니스 로직만을 담아야 합니다. 여기에 담기는 비즈니스 로직은 낮은 추상화 수준을 갖고 있으므로, 어플리케이션 기능 단위로 직접 활용될 여지는 크지 않습니다. 하지만 모든 어플리케이션 기능을 구현하는 데에 기반 재료로 활용될 atomic한 로직들이 모두 이곳에 존재하기 때문에, 개발자들은 기능을 개발할때 이 layer에 가장 신경을 많이 써야 할 것입니다.

code base architecture 방법론이 등장한 이유도 비즈니스 로직이 여러 기술적인 이슈들에 강하게 결합되어 있어 우리의 즐거운 개발 경험을 방해하는 것을 막기 위함이라고 이미 몇 차례 강조 드린 바 있습니다. 그런 만큼 이 domain layer를 순수하게 유지하는 것은 앞으로의 유지보수, 확장성을 결정하는 가장 중요한 요인일 것입니다.

infrastructure layer

infrastructure layer는 위에서 설명한 3개의 layer가 각각 자신이 맡은 역할을 온전히 수행할 수 있도록 기술적인 부분에서 지원해주는 layer입니다. 저희 팀에서는 infrastructure layer가 맡아야 하는 기능을 크게 두 가지로 분류했습니다.

  1. 프레임워크를 통한 어플리케이션 구동

위에서 정의한 3개의 layer는 각자의 역할을 잘 수행할 것이라고 기대되지만, 결과적으로 이 layer들을 통합해서 하나의 어플리케이션으로 serving하는 역할을 해줄 무언가가 따로 필요합니다. 저희는 백엔드 프레임워크로 spring boot를 쓰고 있는데요, 어플리케이션을 정상적으로 구동하기 위해 필요한 요소들은 대략 아래와 같습니다.

  • @SpringBootApplication 어노테이션이 달린 App 클래스
  • App 클래스를 실행하는 main 함수
  • application.yml 등으로 정의된 application properties
  • 적절한 spring bean을 띄우기 위한 configuration 클래스들

2. 기술 종속성이 강한 구현체를 제공

말씀드렸듯이 domain layer에서는 외부 의존성을 최소화해야 하므로, 순수하게 로직을 구현할 수 없는 경우는 domain layer에는 인터페이스를 두고 구현체를 infrastructure layer로 분리하는 방법을 선택했습니다.

이러한 예시로 repository의 경우를 꼽을 수 있습니다. repository란 DB에 존재하는 영속 데이터로의 접근을 관리하는 객체를 아울러 이르는 용어로, repository는 어플리케이션이 어떤 DBMS를 사용하는지, 어떤 ORM을 사용하는지 등 데이터베이스에 대한 정보를 알고 있습니다. DB에 관한 세부사항은 명백히 도메인 지식과는 관계가 없는 부분이므로, domain layer에는 interface만 작성하고 이에 대한 구현체는 infrastructure layer에서 작성하도록 하였습니다.

여기서 구현체는 DBMS로 mysql을 사용할 수도 있고, postgresql을 사용할 수도 있습니다. 또한 ORM으로는 jpa를 사용할 수도 있고, myBatis를 사용할 수도 있습니다. 이렇듯 기술에 대한 세부사항은 언제든 변경될 수 있는 것이기 때문에 인터페이스와 구현체를 서로 다른 layer에 격리시키는 것이 큰 의미를 얻을 수 있습니다. 객체 지향의 핵심 아이디어 중 하나인 “추상화”를 아주 바람직하게 적용한 예시가 될 수 있겠죠? 😊

(의존성의 방향이 infrastructure → domain 이 되므로, domain은 기술에 대한 의존성 없이 순수하게 유지될 수 있습니다.)

위에서 말씀드린 2가지 역할은 도메인 지식과 상관이 없는 내용이므로 infrastructure layer에 속해야 하는 것은 맞지만 서로 결이 다른 역할을 하고 있기 때문에 infrastructure layer 내에서도 한 번 더 subproject로 분리할 필요성을 느꼈습니다. 결과적으로 1번은 어플리케이션을 구동한다는 의미에서 boot라는 이름으로, 2번은 기술 구현체들을 제공해준다는 의미에서 provider라는 이름으로 subproject를 만들기로 결정하였습니다.

layer를 gradle subproject로 만들기

이제 위에서 결정한 대로 gradle subproject를 만들어봅시다! gradle subproject를 새로 생성하는 방법은 아주 간단합니다. root gradle project에 존재하는 setting.gradle.kts에 다음과 같이 추가해주면 됩니다.

하지만 이렇게 되면 최상위 경로에 해당 subproject들이 생성되기 때문에, 다른 파일 및 폴더들(.idea라든지, .gradle이라든지, .gitignore라든지 …)와 섞여있어 보기에 별로 예쁘지가 않습니다 😭

하지만 gradle은 굉장히 유연하게 스크립트를 customize할 수 있는 빌드 툴이므로, 이 부분 또한 잘 해결해낼 수 있습니다! 바로 프로젝트의 logical path와 physical path를 다르게 설정할 수 있는 기능인데요, 바로 아래와 같은 함수를 settings.gradle.kts에 정의하고, include대신 includeProject로 subproject를 생성하면 됩니다. (kotlin DSL을 사용하고 있기 때문에 kotlin syntax로 작성이 가능합니다.)

저희 팀에서는 gradle subproject를 모두 src 디렉토리 안에 모아놓고, logical path는 최상위에 위치할 수 있도록 아래와 같이 설정을 해주었습니다.

결과적으로 프로젝트는 아래와 같은 생김새가 될 것입니다.

각 layer 간의 의존성을 도식적으로 나타내면 아래와 같습니다.

grpc protocol 의존성 제어하기

위와 같은 구조를 gradle로 구현하면서 얻을 수 있는 또 하나의 통찰이 존재합니다. 바로 “application layer와 domain layer에는 통신 protocol에 대한 의존성이 없어야 한다”라는 것인데요, protocol에 종속적인 코드가 깊숙히 침투하게 되면 우리 어플리케이션의 protocol을 바꾸고자 할 때 presentation layer만 갈아끼우고 싶은데, domain layer와 application layer까지 모두 함께 수정해야 하는 불상사가 일어날 것입니다 …

지금 말한 “protocol에 종속적인 코드” 중 대표적인 것으로 Grpc Protobuf 파일을 예로 들 수 있습니다. 우리는 gradle multi-project 방식으로 application layer가 가져야 하는 의존성을 독립적으로 정의할 수 있으므로, “Protobuf 파일이 application layer에 존재해선 안된다”라는 제약사항을 단순 가이드 수준으로 두지 않고 컴파일 시점에 강제할 수 있게 됩니다.

결과적으로 presentation layer에서는 Protobuf 파일을 pure Kotlin DTO로 변환하는 작업을 수행해주어야 하고, application layer에서는 순수 DTO로만 정의된 인터페이스를 갖는게 가능해집니다. 아래 그림은 어떤 layer가 Protobuf에 의존성을 가지고 있는지를 보여주고 있고, 더불어 이 layer들이 application layer 혹은 domain layer를 호출할 때 Protobuf를 순수 DTO 객체로 변환해주어야 함을 나타내고 있습니다.

이로써 우리는 application layer와 domain layer가 기술에 대해 가지는 의존성을 최소화하였고, pure Kotlin만을 이용해 순수한 비즈니스 로직을 작성하는 것이 가능해졌습니다! 앞으로 이 아키텍쳐 위에서 어플리케이션을 개발할 생각을 하니 벌써부터 가슴이 두근거리네요! 😁

요약

이번 포스트에서는 grpc + spring boot 환경에서 gradle을 이용해 layered architecture을 구현하는 방법에 대해 알아보았습니다. 이와 같이 code base architecture를 구축함으로써 얻을 수 있는 장점을 다시 한 번 요약해보면 다음과 같습니다.

layered architecture의 장점

  • 각 레이어를 loosely coupling 된 형태로 구축하면서, 각자 자신의 관심사에만 집중할 수 있음
  • 특히 핵심 비즈니스 로직을 순수하게 유지함으로써 유지보수와 확장성 측면에서 이득을 얻을 수 있음
  • 각 레이어에 서로 다른 추상화 수준을 가진 상태와 행동을 위치시킴으로써 코드 재사용성을 높일 수 있음

gradle과의 시너지

  • 자체적으로 외부 의존성을 관리하는 subproject 기능을 통해, 각 layer의 올바른 역할을 강제할 수 있게 됨
  • 잠깐의 편의성과 타협하지 않고 개발 사이클 내내 best practice에 대해 고민하게 된다는 점에서, 문화적으로도 좋은 영향을 줄 수 있음

마치며

개발 경험을 얼마나 유쾌하게 만들 것인가는 개발자들 스스로에게 달려있다고 생각합니다. 아키텍쳐에 대한 큰 고민 없이 짜여진 코드는 소프트웨어의 복잡성을 기하급수적으로 증가시키는 결과를 낳게 될 것이고, 이는 개발에 대한 경험을 상당히 불쾌하고 험난한 것으로 만들어 버립니다. 개발자들은 어떻게 하면 작업을 더 쉽고, 재미있게 할 수 있을 지에 대해 항상 고민해야 합니다. 그러한 측면에서 견고한 code base architecture의 구축은 아주 커다란 의미를 가지고 있다고 말씀드리고 싶습니다.

이번 포스트가 기술적인 부분에서 뿐만 아니라 개발자로서의 철학과 관련해서도 깊은 생각을 해볼 수 있는 계기가 되길 바라며, 앞으로 더욱 유쾌한 개발 경험을 만들어나가기 위해 할 수 있는 노력들에 대해 자주 소개해보도록 하겠습니다!

--

--

Riiid Teamblog
Riiid Teamblog KR

교육 현장에서 실제 학습 효과를 입증하고 그 영향력을 확대하고 있는 뤼이드의 AI 기술 연구, 엔지니어링, 이를 가장 효율적으로 비즈니스화 하는 AIOps 및 개발 문화 등에 대한 실질적인 이야기를 나눕니다.