영감을 (inspire) 주고픈 개발 블로그

클린 아키텍처를 이해하기 위한 시작 본문

개발/기획-개발 flow

클린 아키텍처를 이해하기 위한 시작

inspire12 2025. 2. 16. 13:35

모든 이해의 시작은 작은 것부터 시작된다.

저는 처음 클린 아키텍처를 공부할 때, 익숙하지 않은 추상화나 의존성 역전과 같은 개념들 때문에 낯설고 어렵게 느껴졌습니다. 하지만 기본적으로 좋은 코드가 무엇인지 고민하며 실제 코드를 작성하고 읽어보면서, 점차 클린 아키텍처의 핵심 원리를 이해할 수 있었습니다. 이 글은 제가 이해한 클린 아키텍처를 이해하는 방법을 적은 글입니다.

좋은 코드는 무엇인가?

클린 아키텍처를 이해하고 적용하려면 "먼저 좋은 코드가 무엇인가"에 대한 고민에서 시작되어야 합니다.  좋은 코드를 작성하는 원칙은 간단해 보일 수 있지만 실제로 구현해보면 현실적으로 딜레마가 생깁니다. 이런 딜레마를 구조적으로 해결해 자연스럽게 좋은 코드를 만들도록 하는 것이 클린 아키텍처의 본질입니다. 일반적으로 좋은 코드는 다음과 같습니다. 

  • 읽기 좋은 코드 
  • 변경하기 좋은 코드 
  • 테스트 하기 좋은 코드

이해하기 좋고 읽기 좋은 코드는 어떻게 해야하나?

  • 코드가 짧고 단순할수록 이해하기 쉽습니다.
  • 변수와 함수가 의미하는 바가 명확한 네이밍을 가지야 합니다.

코드량은 많아지는 건 필연적입니다. 현실적으로 기능이 많아지면 당연히 길이가 길어질 수 밖에 없습니다.

기존의 레이어드 아키텍처에서 비즈니스를 담당하는 서비스 레이어는 비대해 수 밖에 없습니다. 여기서 딜레마를 느끼게됩니다. 

비즈니스 로직을 도메인 객체로

서비스 레이어의 비대함을 나누기 위해 서비스가 담당하는 비즈니스 로직을 퓨어한 도메인 객체에 넘겨주는 게 클린 아키텍처의 시작입니다. 

도메인 객체에서는 다른 의존성을 가지지 않으며 파라미터를 통해 데이터를 받아 데이터를 처리하고 결과값을 넘기는 식입니다. 도메인 객체의 함수들은 명확한 비즈니스 작업들을 처리하고 이를 함수명에 명명해놓습니다.  

데이터는 어떻게 가져올까? : Repository의 역할과 올바른 활용

그러면 데이터는 어떻게 가져와야하는지 궁금해질 수 있습니다. 도메인 객체에 데이터를 넘겨주는 부분은 Repository를 사용합니다. 여기서 Repository는 JPA에서 이야기하는 JpaRepository가 아닙니다.

 

"데이터를 어떻게 가져올지"에 대한 약속(인터페이스)만 정의합니다. 실제 데이터베이스 작업은 이 인터페이스를 구현하는 Adapter 계층에서 처리합니다. 이 Adapter에서 JpaRepository를 사용해서 데이터베이스와 소통하고, 그 결과를 도메인 계층으로 전달합니다. 이 구조 덕분에 도메인 계층은 특정 기술(JPA)에 의존하지 않아, 더 유연하고 테스트하기 쉬워집니다.

 

변수와 함수가 의미하는 바를 명확히 하기 위해선 비즈니스를 명확히 이해 해야한다. 

그래서 개발자들이 이름 짓기가 가장 어렵다는 이야기가 나옵니다. 이런 부분은 같이 일하는 분들과 이야기해서 용어를 명확히 정해놓는 게 도움이 됩니다.

public interface OrderRepository {
    Order findById(Long id);
    void save(Order order);
}
@Repository
public interface JpaOrderRepository extends JpaRepository<OrderEntity, Long> {}
@Service
public class OrderRepositoryAdapter implements OrderRepository {
    private final JpaOrderRepository jpaOrderRepository;

    public OrderRepositoryAdapter(JpaOrderRepository jpaOrderRepository) {
        this.jpaOrderRepository = jpaOrderRepository;
    }

    @Override
    public Order findById(Long id) {
        OrderEntity entity = jpaOrderRepository.findById(id)
                               .orElseThrow(() -> new RuntimeException("Order not found"));
        return mapToDomain(entity);
    }

    @Override
    public void save(Order order) {
        OrderEntity entity = mapToEntity(order);
        jpaOrderRepository.save(entity);
    }
}

참고: 비즈니스 로직을 나누기 위한 역할별 객체

도메인 객체는 비즈니스에 따라서 이름이 정해집니다. 이런 비즈니스 외에 내부적으로 객체를 만들고 각 콘텍스트를 분리(Bounded Context)하고 데이터를 주고받는 등의 프로그래밍적으로 처리해야할 로직들도 있는데 이런 것들을 분리하면 좀 더 비즈니스를 읽기 좋게 됩니다. 이건 팀의 명명 규칙에 따라 조금씩 다르게 쓰일 수 있습니다.

 

역할 별 객체 명명

    • 도메인 객체 (비즈니스 로직 처리, POJO)
      • 도메인 객체 네이밍: 일반적으로 Pure하게 표현하지만, 필요할 경우 Domain을 붙여 명확히 구분 가능
      • Aggregate: 관련된 도메인 객체들의 그룹
      • Policy, Specification: 복잡한 비즈니스 규칙 관리
    • 도메인간 연결 (Bounded Context)
      • Port - Adapter:도메인과 인프라 계층을 분리하는 인터페이스 및 구현체
      • Event Publisher, Listener: 이벤트 기반 아키텍처에서 컨텍스트 간 메시지 전달 
    • 서비스 객체(도메인 객체를 호출해 비즈니스를 조립하는 전체 로직을 실행, Bean 객체)
      • Facade,Service, Use Case: 특정 비즈니스 프로세스를 캡슐화, 도메인 객체들을 호출, 트랜잭션 관리
  • 객체 생성 및 조립
    • Factory: 객체 생성 캡슐화
    • Assembler: 여러 객체를 조합하여 응답 객체 생성
    • Mapper: DTO ↔ 도메인 객체 변환
  • 데이터 저장 및 조회:도메인 객체의 영속성을 관리하고, DB와 연결되는 객체
    • JpaRepository, (Jpa)Entity: 실제 DB에서 데이터를 저장하고 가져오는 객체, DB 데이터를 객체에 매핑
    • Query: DB 조회에서 복잡한 로직 처리
  • 외부 시스템과 연결: 도메인 객체와 외부 API, 메시지 큐, 또는 다른 시스템 간의 통신을 담당하는 객체들입니다.
    • Adapter: 외부 도메인 서비스와 연결 
    • Listener: 메시지 큐에서 이벤트를 수신
    • Publisher: 메시지를 발행하여 비동기 처리
    • Client: 외부 시스템을 호출하는 클라이언트 객체
  • 외부 시스템과 데이터 교환을 위한 전용 객체
    • Request, Response, View, Command, Query: 계층 간 데이터 변환 및 전달
  • 상태 및 동작 관리, 이벤트 기반 로직
    • State: 객체 상태에 대한 객체
    • Command: 특정 명령을 캡슐화하여 실행하는 객체
    • Strategy: State나 Command 에 따른 동적인 로직 처리
    • Event: 이벤트 발행

 

코드는 마음 편하게 변경하기 위해선 어떻게 해야하나?

  • 내가 바꾼 코드가 어디까지 영향을 가는지 명확해야한다. 
  • 변경에 대한 의사소통이 확실하게 있어야한다. 

코드를 변경하기 망설여지는 건 기술적인 난이도보다 이 코드를 건들였을 때 다른 쪽에서 문제가 나지 않을까라는 두려움이 더 큽니다. 이를 해결하기 위해 외부에서 도메인에 접근하는 부분을 명확히 하기 위한 Port-Adapter 패턴을 활용할 수 있습니다.

Port-Adapter 패턴을 활용한 변경 영향 최소화

도메인을 사용하는 서비스나 애플리케이션이 직접 도메인의 내부 구현을 호출하는 것이 아니라, 인터페이스(Port)를 통해 도메인 계층과 통신하도록 합니다.

  • 도메인을 사용하는 쪽에서 Port(interface)를 정의합니다.
  • 도메인에서 해당 Port를 구현한 Adapter를 작성합니다.
  • 서비스나 애플리케이션은 Port를 통해 도메인 객체를 호출합니다.

Port-Adapter 예제 (결제 서비스)

실제 코드를 예시로 들면 다음과 같습니다.

public interface PaymentPort {
    void processPayment(Order order);
}
@Service
public class PaymentAdapter implements PaymentPort {
    private final ExternalPaymentService externalPaymentService;

    public PaymentAdapter(ExternalPaymentService externalPaymentService) {
        this.externalPaymentService = externalPaymentService;
    }

    @Override
    public void processPayment(Order order) {
        externalPaymentService.pay(order.getId(), order.getAmount());
    }
}

 

이렇게 설계하면 도메인의 내부 구현이 외부에 노출되지 않고, 변경 시 영향 범위를 쉽게 파악할 수 있습니다. 즉, 도메인 객체를 사용하는 부분을 명확하게 분리하고, 불필요한 의존성을 차단함으로써 변경에 대한 두려움을 줄일 수 있습니다.

외부 API 사용 시의 고려 사항

만약 코드가 사용되는 부분이 외부 서버라면 그 영향도를 알기 어렵습니다. 이를 해결하기 위한 방법은 다음과 같습니다.

  • 로깅 및 모니터링을 활용하여 API 호출을 추적합니다.
  • Rate Limit 설정을 통해 어디에서 API가 호출되고 있는지 파악합니다.
  • 버전 관리를 통해 API 변경에 대한 쿠션 역할을 수행합니다.

그리고 사용하는 쪽에 문의를 주는 형태로 하거나 버전을 나누는 식으로 분리해서 처리해야한다. 

 

이처럼 코드를 변경하기 쉽게 만들기 위해서는 변경에 대한 완충 역할을 하는 설계를 사전에 준비해야 합니다. 그렇지 않으면 작은 변경이 예상치 못한 문제를 초래할 수 있습니다.

테스트하기 좋은 코드는 어떻게 해야하나?

  • 코드 간 의존성이 적어야한다.
  • 비즈니스 의도를 명확해야 한다. 

테스트 코드를 만들어 놓으면 변경 시 문제를 미리 알 수 있고 코드에서 알기 어려운 히스토리나 제약사항들을 미리 알 수 있습니다. 

그러나 막상 테스트 코드를 실제 적용해보려고 하면 구현의 너무 복잡해서 포기하게 됩니다. 

테스트를 짜는 것 자체의 어려움도 있지만 기존 코드들이 여러 Spring bean 객체들을 가지고 있으면 하나를 테스트 해보기 위해 관련된 모든 의존성 있는 서비스 객체들을 일일이 구현해야한다는 문제들이 생깁니다. 그러면서 테스트 무용론도 생기게 됩니다.

테스트 코드의 주체는 퓨어한 도메인 객체로 한다 

이러한 문제를 해결하려면, 앞에서 말한 도메인 객체를 활용해야합니다. 테스트의 주요 대상은 비즈니스로직을 가진 도메인 객체가 되어야 합니다. 그리고 도메인 객체는 퓨어한(Pure)한 자바 객체 여야합니다.(POJO)
실제 테스트할 때도 도메인에 넘겨줄 데이터들만 미리 테스트 시나리오에 맞게 test fixture나 fake, mock, stub 등의 가짜 객체들을 만들고 넘겨주면 됩니다. 

그리고 테스트 단계에서 @SpringBootTest 를 쓰지 않아도 됩니다. 테스트 속도도 빠르고 인프라 부담도 줄고 됩니다. 

마무리

클린 아키텍처는 단순한 개념이 아니라, 현실적인 문제를 해결하기 위한 생각보다 더 실용적인 접근 방식입니다. 처음에는 어렵게 느껴질 수 있지만, 조금씩 원리를 이해하고 적용해보면서 그 가치를 직접 경험할 수 있습니다.

처음에 클린 아키텍처가 레이어드 아키텍처와 반대되는 개념으로 오해도 했었습니다. 그러나 클린 아키텍처는 복잡해진 로직들을 기존 서비스를 보완하는 방식이라고 생각하는게 좋습니다.

처음부터 완벽할 필요는 없습니다. 작은 부분부터 적용하면서 점진적으로 개선해 나간다면, 더 읽기 쉽고 변경하기 편하며 유지보수하기 좋은 코드를 만들 수 있을 것입니다.

반응형