스프링 프로젝트의 소프트웨어 아키텍처를 어떻게 설계해야 할까?

avatar
2025.02.20
·
38 min read

SW 마에스트로 수료 후 지인들과 시나리오 스터디를 진행하게 됐습니다.

스프링 부트 기반의 프로젝트를 시작하게 됐는데 시작 전 스터디원 모두 소프트웨어 아키텍처를, 설계를 진행해 오기로 했습니다.

그래서 이번 스터디에서 진행할 소프트웨어 아키텍처를 설계하면서 과거에 고민했던 “스프링 프로젝트의 아키텍처를 어떻게 설계해야 할까?”라는 의문에 대해 제 생각을 조금 정리해 보고 어떻게 적용했는지 정리해 보고자 합니다.

목차

  1. 왜 이런 고민을 시작했을까?

  2. 다양한 아키텍처

  3. 프로젝트에는 어떻게 적용해야 할까?

  4. 결론

들어가기 전

위와 같은 목차로 글을 시작하려고 합니다.

단순 정보 전달용 글이 아닙니다. 저의 고민과 생각이 담겨져 있기 때문에 어떤 소프트웨어 아키텍처가 어떤 특징을 갖고 있는지에 대해 학습하기 위해서는 다른분의 아티클을 읽는것을 추천드립니다.

다만 이 글을 통해 백엔드 프로젝트를 시작할 때 어떤 아키텍처를 어떻게 고려해야 하는지에 대한 개인적인 고민과 적용 방법을 공유하려 합니다

왜 이런 고민을 시작했을까?

백엔드 개발을 시작할 때

백엔드 개발을 시작할때 가장 먼저 하는것은 무엇일까요?

저는 백엔드 개발을 Node 런타임 환경에서 express.js을 사용하면서 시작했습니다.

웹 백엔드 환경에서 가장 먼저 구현하는 것은 아마 클라이언트로부터 HTTP 요청을 입력받아 데이터베이스에서 조회하거나 저장하는 역할을 구현할 것입니다.

const express = require("express");
const User = require("../models/user");
const router = express.Router();
router
  .route("/")
  .get(async (req, res, next) => {
    try {
      const users = await User.findAll();
      res.json(users);
      console.log(users);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

클라이언트로부터 특정 URL 기반으로 HTTP 요청을 받아 데이터베이스에서 조회하거나 저장합니다.

하나의 파일에서 HTTP 요청을 처리하고 예외을 핸들링하거나(코드에는 실제 catch후 아무런 액션을 취하지 않습니다) 데이터베이스 ORM(Sequelize)을 직접 사용해 가져오는 로직을 다룹니다.

단순히 User를 조회하고 저장하는 기능만 있는 웹 어플리케이션이라면 해당 코드는 전혀 문제가 있지 않습니다.

자 여기서 끝내겠습니다

.

.

.

3536

당연히 여기서 끝나지 않습니다.

시간이 흘러 글쓴이는 좀더 복잡한 로직을 갖고 있는 코드를 작성하기 시작했습니다.

개발을 조금 하다 보니 데이터베이스 관련 로직들은 분리하는 패턴에 대해 익히고 repository라는 네이밍에 대해 알게 됐습니다.

controller라는 곳에서 HTTP 요청을 받고, service에서 로직을 처리한다는 “얕은” 개념에 대해 알고 코드에 적용해보려고 했습니다.

아래는 아무런 고민 없이 “모든” 로직을 service라는 이름의 함수에 넣은 코드입니다 (혐 주의, 제가 직접 짠 코드를 가져온 것입니다, 다 안읽으셔도 됩니다)

export class MultiRoomService {
  ...
  async createMultiRoom(
    body: MultiRoomCreateRequest,
    user: DeserializeAccessToken,
  ): Promise<MultiRoom> {
    const participatedRoom = await this.roomStatusRepository.findByUserId(
      user.id,
    );
    if (participatedRoom.length > 0) {
      throw new HttpException('이미 방에 참여중입니다', 400);
    }
    let multiRoom;
    try {
      multiRoom = await this.multiRoomRepository.create({
        ...body,
        status: 'Open',
        multiRoomMember: {
          create: {
            userId: user.id,
            isOwner: true,
          },
        },
      });
      await this.roomStatusRepository.create({
        roomId: multiRoom.id,
        userId: user.id,
        socketId: this.socketGateway.socketId,
      });
      //소켓 연결
      this.socketGateway.server
        .in(this.socketGateway.socketId)
        .socketsJoin(multiRoom.id.toString());
    } catch (err) {
      console.error(err);
      throw new HttpException('방 생성 실패', 400);
    }

    // Job 등록
    const autoStartJob = await this.jobsService.addJobMultiRunBroadCastStart(
      multiRoom.id,
      body.startDate,
    );
    const autoStartJobCacheTtl =
      subTime(new Date(multiRoom.startDate), new Date()) + 50;
    await this.globalCacheService.setCache(
      `roomJob:${multiRoom.id}`,
      `bull:multiRun:${autoStartJob.id}`,
      {
        ttl: autoStartJobCacheTtl,
      },
    );
    const prepareJob = await this.jobsService.prepareMultiRunBroadCast(
      multiRoom.id,
      body.startDate,
    );
    const prepareJobCacheTtl =
      subTime(new Date(multiRoom.startDate), new Date()) + 50;
    await this.globalCacheService.setCache(
      `roomNotiJob:${multiRoom.id}`,
      `bull:multiRun:${prepareJob.id}`,
      {
        ttl: prepareJobCacheTtl,
      },
    );
    return multiRoom;
  }
}

간단하게 설명드리면 러닝이라는 도메인에서 러닝을 여러 사람들이 러닝을 하기 위해 러닝방(runningRoom)을 만드는데 그 이후 자동으로 러닝 방 사람들이 러닝을 시작할 수 있도록하는 Job을 등록하고 Socket을 연결하는 코드가 들어가 있습니다.

3537

무엇이 문제일까요?

createMultiRoom이라는 메소드가 어떤 행위를 수행하는지 알기 위해서 코드라인별로 분석해 확인해야하고 해당 메소드가 너무 많은 기능을 처리하고 있습니다.

당시 공모전이 끝나고 한달 후 다시 코드를 봤을때 무슨 기능을 처리하는지 직접 작성한 저 또한 이해하기 힘들고 유지보수가 점점 힘들어진다고 생각했습니다.

단순 공모전 용도의 코드였지만, 실제 프로덕트의 복잡한 요구사항이라면? 위와 같은 코딩 스타일은 분명 “무언가 문제”가 있다고 생각해 “어떻게 하면 해결할 수 있을까?” 라는 고민을 하게 됩니다.

문제 분석과 방향성 탐색

좋은 소프트웨어 시스템은 깔끔한 코드(clean code)로부터 시작한다. 좋은 벽돌을 사용하지 않으면 빌딩의 아키텍처가 좋고 나쁨은 그리 큰 의미가 없는 것과 같다. 반대로 좋은 벽돌을 사용하더라도 빌딩의 아키텍처를 엉망으로 만들 수 있다. 그래서 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데 그게 바로 SOLID 이다 - 클린 아키텍처 책 중 -

클린 아키텍처의 책에는 위와 같은 내용이 있습니다. 사실 SOLID 원칙은 개발자라면 모르는 사람이 없고 저 또한 당시 위 코드를 작성할때 개념적으로 어렴풋이 알고 있었지만 실제 적용하려고 노력해본적은 없었습니다.

그 이유는 (SOLID 원칙 == 객체지향설계) 라고 생각했기 때문인데 실제 SOLID는 단순히 객체지향 설계에서만 적용되는 개념이 아니라, 시스템 및 소프트웨어 설계 전반에서도 적용할 수 있는 원칙입니다.

코드를 가져온 김에 몇가지 SOLID를 기반으로 저의 코드를 보면 다음과 같은 내용을 위반하고 있습니다.

  • 단일 책임 원칙 위반 (SRP)

  • 낮은 수준 모듈에 대한 직접 의존 (데이터베이스, 소켓, 캐시)

  • 확장성 부족 (OCP 위반)

먼저 해볼 수 있는 것은 각각의 책임에 맞게 분리하고 추상화에 의존하게 하는 코드를 작성하는 것 입니다.

3538

(실제 리팩토링을 수행하지 않아서 해당 코드는 단순히 예시입니다)

만약 위와 같이 분리한다면 각각의 클래스들은 어디 패키지(폴더)에 위치해야 할까요?

당시에 Controller/ Service / Repository 3개의 영역이 무슨 역할을 정확히 담당하는지 몰라서 상당히 많은 고민을 했습니다.

SOLID 원칙을 준수해서 각 책임을 수행하는 클래스로 분리하고 추상화를 적용하기 위한 인터페이스를 만들었을때 각각의 클래스는 어느 곳에 위치하고 어떤 기준으로 분류해야 할까요

이를 해결하기 위해 먼저 소프트웨어 아키텍처에 대해 알아야 합니다.

그래서 다음 목차에서는 레이어드 아키텍처와 헥사고날 아키텍처에 대해 설명하겠습니다.

다양한 아키텍처

레이어드 아키텍처

3539

계층형 아키텍처, 멀티 티어 아키텍처라고 불리는 설계 방식은 “레이어”라는 분류 체계로 코드를 분류합니다.

각각을 간단하게 설명하면 다음과 같습니다.

레이어

설명

프레젠테이션 레이어 (Presentation Layer, Controller)

사용자와의 상호 작용을 처리하고 결과를 처리합니다.

스프링에서 @Controller Annotation을 통해 주로 적용합니다.

비즈니스 레이어
(Business Layer, Service)

애플리케이션의 비지니스 로직을 처리하는 역할을 담당합니다.

데이터의 검증, 가공, 비지니스 규칙 처리 등의 역할을 담당하며 스프링에서는 주로 @Service Annotation을 통해 적용합니다. 위 예시 코드가 위치한 영역입니다.

인프라스트럭처 레이어 (Infrastructure Layer, Repository)

외부 시스템과의 상호 작용을 담당합니다. 대표적으로 데이터베이스와 같은 외부 시스템과의 상호작용을 담당하기에 Repository라는 표현을 적용해봤습니다.

데이터를 저장하거나 조회하는 역할을 주로 담당하기 때문에 Persistence Layer(영속성)라고 작은 의미로 표현하기도 합니다.

계층화를 함으로써 얻는 이점은 각 계층이 특정 “부분”에만 관심사를 갖고 있다는 점입니다.

프로그래밍을 하는 사람이라면 “관심사의 분리”라는 단어를 들어봤을 겁니다. 해당 아키텍처는 이 관심사의 분리의 결과물이 여러 레이어로 표현이 됩니다.

각 레이어마다 관심사를 분리하면 어떤 장점이 있을까요?

프레젠테이션 레이어는 사용자와의 상호 작용을 처리하고 있습니다.
이는 Web이 될 수 있고, 콘솔 게임이 될 수 있는 등 다양한 형태로 사용자와 상호작용 할 수 있습니다.
하지반 비지니스 레이어는 프레젠테이션 레이어가 어떤 방식으로 사용자와 상호작용 하는지 관심이 없습니다.

그저 비지니스 레이어는 비지니스는 로직을 다루고 처리하는 방식에만 관심이 있기 때문에 프레젠테이션 레이어가 HTTP 통신하는 API 서버가 되든 CLI 프로그램이 되는 상관 없습니다

Q. 그러면 반대로 데이터베이스를 교체하면 어떤 영역이 영향을 받을까요?

단방향 레이어드 아키텍처 구조에서는 비지니스 레이어만 영향을 받습니다.

상위 레이어는 하위 레이어의 구현을 알고 있기 때문에 하위 레이어가 변경되면 상위 레이어가 영향을 받게 됩니다.

이는 다시말해, 양방향 레이어드 아키텍처로 변하게 된다면(모든 레이어가 서로 알고있다면) 서로가 서로에 영향을 주는 구조이고, 정리하자면 순환 의존성을 갖게 되는 구조로 변경되기 때문에 레이어드 아키텍처를 구성한다면 피해야 하는 상황입니다.

다시 돌아와서 상위 레이어가 하위 레이어를 알고 있는 상황에서 발생할 수 있는 다른 문제는 데이터 위주의 사고를 하게 될 수 있습니다.

비지니스 로직을 수행하기 위해 하위 레이어인 Repository(Infrastructure)에 대해 알고 있고 이를 기반으로 코드를 작성해야 하기 때문에 데이터 중심적인 사고는 “유연한 설계”를 하는데 어려움을 겪을 수 있습니다.

3541

저희는 애플리케이션을 만들고 있습니다. 애플리케이션에서 중요한것은 무엇일까요?

간단한 예시로 회원가입시 이메일 검증을 해서 회원가입 처리를 한다고 봅시다.

  1. 이메일로 이미 가입되어 있는지 확인한다.

  2. 이메일 인증을 위해 메일을 전송한다.

저희는 애플리케이션의 비지니스 로직을 구현할때 “행위”를 중심으로 설계합니다. (위 2가지 행위를 수행해야 합니다)

회원의 이메일 정보가 MySQL에 저장되는지, Redis에 저장되는지, MyBatis를 사용하는지, JDBC를 통해 Query를 날리는지.. 메일이 자바 코드로 구현되어 전송되는지 외부 라이브러리를 사용하는지 이는 비지니스 로직을 구현할때 우선순위가 아닙니다.

중요한건 “도메인” 입니다. 하지만 서비스 레이어가 직접적으로 인프라스트럭처 코드를 알고 있다면 , 회원의 이메일 정보를 SQL을 직접 날려서 구현하다가, JPA를 적용한다고 할때 서비스 레이어에 영향을 줄 수 있습니다.

어떻게 해결해야 할까요?

3543

먼저 "도메인 레이어" 라는 새로운 계층을 추가합니다. 도메인 레이어는 비지니스 도메인을 표현하는 클래스가 모이는 곳이며 도메인 레이어 안의 객체들끼리 역할과 협력을 가지고 협력합니다.

해당 레이어의 클래스들은 애플리케이션이 집중해서 해결해야 하는 비지니스 로직에 집중하는 코드이며 도메인 레이어를 중심으로 개발하면 됩니다.

이는 레이어를 구분하며 외부와의 통신을 담당하는 기능과, 핵심 비지니스 영역간의 관심사를 한 단계 분리했다고 표현할 수 있습니다.

또한 도메인 레이어에 존재하는 오브젝트들이 비지니스 로직을 다루게 된다면 자연스럽게 기존 Service레이어는 얇게 유지 됩니다. 이런 Service 레이어는, 기반 서비스 계층 혹은 애플리케이션 계층 (Application Layer)라고 불리며 핵심 비지니스 로직 외에 외부 세계와의 통신, 트랜잭션 등의 기능을 담당합니다.

  • Application

    • 도메인 로직과 함께 사용되는 기반 환경의 로직을 수행함.

    • 인프라스트럭처와의 통신을 담당한다.

  • Domain

    • 핵심 비지니스 로직이 구현되며, 외부의 특정 기술이나 의존을 최대한 피한다.

문제는 아직 남아있습니다. 애플리케이션 계층이 인프라스트럭처의 코드를 직접 알고 있는 상황에서 앞서 말한 문제는 아직까지 남아있는 상황인데요. 이는 의존성 역전을 통해 해결이 가능합니다.

3544

의존성 역전을 사용하면 더 이상 핵심 도메인 로직과 서비스가 외부 세계인 인프라스트럭처의 영향을 받지 않습니다.

이렇게 아키텍처를 구성하면 저희는 이제 핵심 도메인 로직 개발에만 집중할 수 있는 레이어드 아키텍처를 구성할 수 있습니다.

조금 정리해보겠습니다.

레이어드 아키텍처는 “관심사의 분리”로 각 레이어마다 담당하는 관심사를 기준으로 레이어가 분리됐습니다.
일반적인 레이어드 아키텍처는 단방향 의존성 흐름을 가지며, 여기서 하위 수준인 인프라스트럭처 레이어를 서비스 레이어가 의존하는것은 데이터 중심적 사고와 유연한 설계를 방해하는 문제점이 있었습니다.
이를 해결하기 위해 인프라스트럭처와 서비스레이어 간의 의존성 역전을 통해 해결하고 도메인 영역이라는 비지니스 로직에 집중하는 영역과 애플리케이션이라는 기반 서비스와 인프라스트럭처 영역을 추가했습니다.

헥사고날 아키텍처와 비교

다른 아키텍처인 헥사고날 아키텍처에 대해서도 간단하게 알아보겠습니다.

육각형 아키텍처, 포트 앤 어댑터 패턴이라고도 불리는 패턴은 알리스테어 콕번이 만든 아키텍처이며 애플리케이션의 핵심인 내부 도메인과 외부 바깥 세계를 포트와 어댑터로 연결하고 도메인을 외부 세계와 완전히 독립적으로 동작하게 만드는 방법입니다.

3545

왼쪽의 InputAdapter는 애플리케이션을 사용하는 사용자들의 입력을 표현한 것이며 이들은 Input Port을 통해 애플리케이션의 기능을 호출합니다.

Input Port는 ServiceImpl을 통해 Domain과 데이터베이스 등의 외부 호출을 위해 Output Port을 호출합니다.

사실 헥사고날 아키텍처를 소개한 이유는 2가지가 있습니다.

  1. 헥사고날 아키텍처를 보면 의존성 방향이 중앙의 Domain으로 향하고 있습니다.

    앞서 레이어드 아키텍처에서 추상화에 의존하는 방향으로 생각해보면 고수준의 도메인이나 애플리케이션은 저수준의 데이터베이스에 의존하지 않게 DI를 적용했는데 헥사고날 아키텍처는 아키텍처의 구조 상으로 강제하고 있습니다.

  2. 레이어드와 비슷한 구조를 갖고 있습니다.

    그림을 다시한번 표현해보겠습니다.

3546

기존 레이어드 아키텍처와의 차이점이 보이시나요?

헥사고날 아키텍처는 모든 외부 요소와 의존성 역전을 수행해 레이어간의 결합도를 끊도록 수행했습니다. 하지만 서비스 레이어에 대해 의존성 역전을 적용한다면 어느정도 비슷한 형태의 모습을 띄게 됩니다.

간단하게 아까 러닝 코드 쪽에서 만든 클래스를 인프라스트럭처 레이어에서 DI가 적용된 레이어드 아키텍처 구조에 맞게 배치한다면 다음과 같이 적용될 것 같습니다.

3548

정리

사실 레이어드 아키텍처와 헥사고날 아키텍처를 얘기한 이유는 다음의 문제를 얘기하는데 도움을 받고자 조금 길게 설명했는데요

스프링 프로젝트의 소프트웨어 아키텍처를 어떻게 설계해야 할까?

네! 이제 글의 제목이 나왔습니다. 어떻게 해야 할까? 에 대해 고민하기 전 생각해봐야 할 포인트는 2가지가 있습니다.

  1. 스프링 프로젝트를 사용하고 있다

저희는 스프링의 강력한 기술인 IOC와 DI(Dependency Injection)를 통해 의존성 역전(DI, Dependency Inversion)을 쉽게 구현할 수 있습니다.

  1. 소프트웨어 아키텍처에 대한 이해, 소프트웨어 아키텍처가 무엇이지? 단순히 패키지를 나누는 것인가?

소프트웨어 아키텍처를 명확하게 내릴 수는 없지만, 한가지는 말할 수 있는 것은 무언가 목적을 달성하기 위해 정책과 제약 조건을 정하는 것 입니다.

레이어드 아키텍처에서는 “빠른 개발”과 “명확한 관심사의 분리” 라는 목적을 갖고 “단일 의존성 방향”이라는 조건을 유지한 채 개발해나가는 것이고

헥사고날 아키텍처는 “외부 세계와 도메인간의 격리” 를 위해 “포트와 어댑터”, “중앙으로 향하는 의존성” 이라는 조건을 통해 해결합니다. 어떤 아키텍처가 무조건 정답이다 할 수는 없지만 만약 개발을 시작한다면 특정한 “목적”을 수행하기 위해 “제약 조건”을 정하는 과정을 진행해야 합니다.

프로젝트에 적용하기

레이어드부터 시작해보자 (feat, 같은 관심사는 같은 영역에 위치시키기)

불필요한 추상화는 개발 속도만 늦출 뿐이라고 생각합니다. 아키텍처를 설계하며 적절한 타협의 과정을 팀원들과 수행해야 합니다.

하지만 이번 시나리오 스터디에서는 혼자 진행하기 때문에, 가장 많이 사용되고 간단한 레이어드 아키텍처를 도입하겠습니다.

각각의 레이어는 같은 관심사를 가진 클래스들만 모아둡니다

controller -> service(application) -> domain -> infrastructure

이번 시나리오 스터디에서 공연을 예매 시스템을 만들 예정입니다. 처음부터 메시지 기반의 시스템을 구축한다거나 다양한 Input이 존재하는것이 아닌 오직 API 통신만 있을 것 같습니다.

결론적으로 프레젠테이션 레이어의 의존성 역전은 진행하지 않는것이 지금은 좋아보입니다. 가장 큰 이유는 애플리케이션의 영역을 다른 것으로 갈아 끼울 일이 거의 발생하지 않을것 같을 뿐더러 중앙으로 향하는 의존성 방향은 유지될 수 있기 때문입니다.

다만 도메인 영역은 추가하려고 합니다. 이번 프로젝트에서 예매라는 도메인 하나를 집중해볼 예정이기 때문에 빈약한 도메인이 아닌 예매라는 도메인 한정에서 많은 도메인 로직들이 포함될것 같습니다.

데이터베이스는 세부사항이다

클린 아키텍처에서는 데이터베이스는 세부사항이라고 합니다. 즉 저수준의 내용이라고 말하죠. 이건 도메인 입장에서 맞는 말입니다.

그러면 다시 말해 자바, 스프링으로 개발할때 JpaEntity나 DataJpa Interface같은 기술을 인프라스트럭처 레이어에 위치시켜야 할까요?

이 내용은 뜨거운 감자라고 생각합니다. 각각의 장단점이 확실한 것 같습니다.

만약 JpaEntity와 도메인 모델을 분리한다면

장점:

  • Jpa를 완벽하게 숙달하지 않았다면 도메인 모델을 설계할때 Jpa 관련 스펙을 생각하지 않고 설계할 수 있습니다.

  • 즉 객체관의 관계를 순수 POJO로 표현할 수 있게 되면서 독립적으로 개발할 수 있는 장점을 얻게 됩니다.

  • 혹시나 JPA를 쓰다가 바꾼다면 상대적으로 리스크가 줄어듭니다.

단점:

  • JPA가 지원하는 강력한 기능을 사용할 수 없습니다.

    • 지연 로딩, 간편한 인터페이스 등

  • 자칫 오버엔지니어링, 과도한 추상화가 될 수 있습니다.

    • 프로젝트에서 JPA를 쓰다가 다른 것으로 변경할 일이 있을까요?

  • JPA의 기술 철학은 DB가 어떻게 연결을 하든 ORM을 통해 오브젝트로 맵핑을 해주겠다는 철학인데 모델이 분리된 상황에서는 자칫 2개의 오브젝트로 관리하는 형태가 될 수 있음.

이 글을 쓰는 지금 저는 도메인 모델과 JPA Entity 모델을 분리했습니다.

이후 인프라스트럭처 레이어에서 맵핑을 진행하고 있는데 구현한 이후 느꼈던 경험은 다음과 같습니다.

  1. 도메인 POJO 객체를 설계할때 JPA 고민 없이 객체간의 맵핑을 편하게 할 수 있었고, Repository의 구현을 모든 서비스 로직이 완료될때까지 메모리 기반의 Map으로 처리하면서 세부사항(데이터베이스)에 의존하지 않는 좋은 경험을 했습니다.

  2. 단점은 JPA를 연결할때 “같은 객체”를 두번 개발하는 느낌을 받았습니다. 저의 도메인 객체관의 연관관계를 JPA에서 모두 지원하고 있었기 때문입니다.

아마 다음 프로젝트에는 두 장점을 살려서, POJO기반의 객체로 설계 후 JPA을 넣는 과정으로 개발할 것 같습니다.

위 부분은 여러번 경험해보고 각자 개인의 정답을 찾는게 좋은것 같습니다.

유연한 구조를 지향하기

중간에 적었던 회원가입 예제를 가져오겠습니다.

“이메일 인증을 위해 메일을 전송한다.”

여기서 메일이라는 예시를 들었는데 이 처럼 외부와의 통신을 담당하는 기능은 핵심 비지니스 로직이 아닌 세부사항, 즉 저수준의 기능 중 하나입니다.

고수준의 애플리케이션이 저수준의 인프라스트럭처에 직접적으로 의존하는것은 아키텍처의 제약조건 상으로 막아야 합니다

3549

무언가 목적을 달성하기 위해 정책과 제약 조건을 정하는 것 이라는 소프트웨어 아키텍처의 설명을 기반으로 다시 말하면

“유연한 구조, 유지보수성을 높이기” 위해 “세부사항, 인프라스트럭처 레이어의 기능을 의존성 역전을 수행한다”

소프트웨어 아키텍처가 이런 부분을 지원해줘야 한다고 생각합니다.

개발을 시작할때

이제 어떻게 아키텍처를 구성해야 하고 감을 잡았다면 해야하는게 무엇일까요?

3550

그건 바로 “도메인”부터 개발하는 것 입니다.

위 그림 처럼 상향식 접근으로 시작해야 합니다. 가장 중요한 도메인 부터 차근차근 개발해나가다 보면 Service를 구현하기 위해 외부 생태계를 먼저 고민하는것이 아닌 Interface로 정의해 두고, 서비스를 개발을 완료해 나가면 어느 순간 레이어드 아키텍처에서 의존성 역전이 된 구조가 될 것입니다.

결론

소프트웨어 아키텍처는 나름의 규칙 및 제약사항을 통해 코드의 구조를 설계하는 것입니다.

하지만 “소프트”웨어 아키텍처 답게 필요한 상황과 규칙이 있다면 지속적으로 설계를 바꿔나가는 것이 더 중요하다고 생각하는데요

예를들어 도메인 레이어를 추가하겠다는 규칙을 정했지만 실제 구현을 하고 나서 빈약한 도메인인 경우 필요가 없다고 판단해 3개의 레이어만 유지한다던가

프로젝트 시작부터 여러 외부 의존성과 복잡한 도메인 로직으로 인해 헥사고날 아키텍처를 적용한다던가 등

다양한 상황에 대응하기 위해 결국 저희는 잘 변하지 않으면서 핵심인 “도메인”부터 개발을 해 나가면서 추상화에 의존하게 코드를 짜고 SOLID를 지키며 관심사를 분리하는 연습을 지속해나간다면 좋은 소프트웨어 아키텍처를 구축할 수 있지 않을까 생각이 듭니다.

reference

  • [NHN FORWARD 22] 클린 아키텍처 애매한 부분 정해 드립니다. https://www.youtube.com/watch?v=g6Tg6_qpIVc

  • 클린 아키텍처/ 로버트 C. 마틴

  • 만들면서 배우는 클린 아키텍처 / 톰 허버그

  • 자바/스프링 개발자를 위한 실용주의 프로그래밍/ 김우근







- 컬렉션 아티클