성장일지 11차

이태형·2025년 2월 9일

Growth Log

목록 보기
11/18

동아리 운영앱을 만들기 전에 프로젝트 구조를 어떻게 잡을지에 대해서 생각을 했어야했다.
전에 우연히 슬랙의 개발소식에서 봤던 "클린 아키텍처를 적용한 스프링 기반 애플리케이션" 이란 글을 읽었었는데, 그에 대한 내용을 이해한 대로 정리하기로 했다.

우선 클린 아키텍처란 아래 그림과 같은 구조이다

비즈니스 로직을 가장안에 감춰두고, 외부 라이브러리(Security, Repository, Controller, 외부 API 호출) 등이 외부에 위치하여 외부에서 내부를 호출하는 흐름으로 진행된다.
이때 외부 -> 내부 로만 호출이 되어야 하고, 내부 -> 외부를 참조하는 일이 없어야 한다.
그럼 내부에서 외부 데이터가 필요한 경우, 즉 외부 API 요청 데이터나 Repository 로 가져온 데이터를 비즈니스 로직이나 서비스 로직에서 사용할 일이 있을 경우가 있을 것이다.
이럴 경우에는 외부 데이터를 호출하는 인터페이스를 정의하여 응답값을 사용하고, 실제 구현은 외부에서 진행하는 형식이다.

그러면 내부에서는 외부가 어떻게 변하든지 동일한 응답값을 주면 변경할 이유가 없어진다.
또한 외부에서 다른 API를 사용하든, 다른 라이브러리를 사용하든 내부에 영향도가 적어지는 장점이 있다.
이런 구조로 지속적인 요구사항 변동과 확장에 유연하게 대응하는 구조를 가질 수가 있다.

JPA 를 사용하는 경우 DB 데이터를 활용하는 구조를 클린 아키텍처로 취하면 아래와 같은 구조가 될 것이다.

비즈니스 로직 내부에는 Entity 를 사용하여 핵심 기능을 수행하는 코드가 들어갈 것이고, 그 수행 코드에는 Repository Interface 를 활용해서 코드가 구성될 것이다.
그리고 실제 DB에 접근해서 데이터를 조회하는 코드는 외부 Repository 구현가 구현하여 내부에서는 어떤 방식으로 DB에 접근하는지 알 필요가 없어 "관심사의 분리" 가 이루어진다.

왜 관심사의 분리를 적용하는 걸까??
그 이유는 바로 변경에 대한 유연한 대응과 적은 영향도를 가지기 위해서이다.

하나의 클래스에서 여러 책임을 가지게 되면, 요구사항이 변경되거나 라이브러리가 변경되는 경우, 해당 기능을 구현하는 코드를 필수적으로 변경을 해야하는 데
하나의 클래스에서 모두 구현된 경우, 영향도가 어마어마 하다.

그냥 단순히 변경된 라이브러리를 적용한 코드를 추가하는 것으로 작업이 끝나는 것이 아니라, 그로 인한 영향을 가지는 클래스를 모두 수정해줘야 해서 OCP 원칙을 위배한다.

관심사의 분리가 적절하게 적용된 경우, 하나의 클래스는 SRP 원칙을 지키고 있어 책임이 적절하게 분산되어 있는 구조를 가진다.

따라서 라이브러리를 책임지는 클래스만 변경되면, 그 외 다른 클래스를 수정할 필요가 없어지고 OCP 원칙을 위배하지 않는 구조가 되어 변경에 유연한게 대처할 수 있다.

여기서 다른 라이브러리를 분리하는 이유는 알겠으나, 서비스 로직과 비즈니스 로직을 분리하는 이유는 무엇인지에 대해서 알아보기로 했다.
둘은 비슷한 부분이 있지만 엄연히 다른 책임을 가진다.
이렇게 분리하는 이유도 "관심사의 분리" 를 적용하기 위함이다.

비즈니스 로직은 도메인 로직이라고 하며 소프트웨어가 수행할 핵심 기능을 담당하는 로직이다.
서비스 로직은 핵심 기능을 수행하기 위해 필요한 작업을 담당하는 로직이다.
Use Case 는 비즈니스 로직을 수행하기 위해 흐름을 조정하는 작업을 담당한다.
(조금은 애매하지만 이렇게 이해하기로 했다.)

은행 앱에서 송금하는 기능 예시

은행 앱에서 송금을 하는 경우, 어느 부분이 비즈니스 로직이고 서비스 로직인지 판단해보기로 했다.
우선 아래와 같은 순서로 송금이 진행된다고 가정하자.

  1. 계좌 잔액 확인
  2. 잔액이 있는 경우 송금 or 잔액이 없으면 송금 불가 를 사용자에게 표시
  3. 송금 수수료 계산
  4. 사용자의 잔액 감소
  5. 사용자의 잔액을 DB에 저장

우선 은행 앱에서 송금에 대한 핵심 기능을 생각해보자.
계좌 잔액을 확인하고, 은행에서 정해진 수수료를 계산하고, 남은 잔액을 감소 시키는 기능이 송금을 할 때 핵심 기능이라고 할 수 있다.
이외에 잔액 판단 여부, 사용자 잔액 저장은 위에 핵심 기능을 수행하기 위해 사용되는 로직으로 생각할 수 있다.

그러면 저렇게 분리하는 이유는 뭘까??
이것도 관심사의 분리를 위해서이다.
은행 앱에서 계좌 잔액 확인, 송금 수수료 계산, 송금 후 잔액 감소는 어떤 은행 앱을 사용해도 무조건 사용되는 기능으로 은행 앱에 핵심 기능이라고 할 수 있다.
그래서 핵심 기능에 대한 변경이 거의 없다.

하지만 서비스 로직이라고 판단한 잔액 판단 여부 후 행동, 사용자의 잔액 DB 저장 을 봤을 때,
어떤 은행앱에서는 잔액이 부족한 경우 알림을 보내는 앱도 있고, 카카오 페이 같은 경우 자동 충전을 사용하는 경우도 있다. 이렇듯 핵심 기능은 동일하지만 서비스 로직은 달라진다.
또한 사용자의 잔액을 DB 로 저장하는 로직은 얼핏보면 핵심 기능이라고 생각할 수도 있다.
어느 앱이든 남은 잔액을 무조건 저장하기 때문이다.
그렇지만 DB 를 저장하는 방식이 달라질 수 있다. 저장하는 행위는 도메인 규칙보다는 애플리케이션 레이어에서 처리하고 DB와 상호작용 하는 역할을 맡기 때문이다.
이렇게 분리가 되면, 비즈니스 로직에선 특정 저장소(DB)나 기술적인 인프라에 의존하지 않고 도메인 규칙을 중심으로 동작이 가능하다. 따라서 외부 변화에 영향도를 최소화 할 수 있는 것이다.

아키텍처 구조를 지향하는 이유

물론 핵심 기능이 변경되는 경우에는 비즈니스 로직을 변경해야 한다.
단, 핵심 기능은 동일한데 외부 코드에 변화로 비즈니스 로직을 수정해야 하는 이유가 있어서는 안된다.
또한 위와 같은 구조를 가지게 되는 경우, 테스트 코드 작성이 더욱 쉬워진다.
비즈니스 로직이 외부 라이브러리와 의존이 거의 없어 단위 테스트를 작성하는 게 수월하기 때문이다.
유지보수 또한 쉬워지며 핵심 기능을 단위 테스트로 수행할 수 있어 에러율도 낮출 수 있다.

0개의 댓글