ETC ..

[Architecture] Hexagonal Architecture 알아보기

코딩 기록하는 애기 개발자 2026. 3. 11. 18:15

목차

     

    지금까지 진행했던 대부분의 프로젝트는 Layered Architecture 기반으로 서버를 설계하고 구현해왔다. Controller, Service, Repository와 같이 역할에 따라 계층을 나누는 구조는 이해하기 쉽고 구현하기도 비교적 명확하기 때문에 초기 프로젝트를 진행할 때 자연스럽게 선택하게 되었다.

     

    하지만 프로젝트를 경험하면서 계층 간 의존성이 복잡해지거나, 비즈니스 로직이 특정 계층에 집중되는 구조를 보며 “서버 구조를 더 잘 설계할 수 있는 방법은 없을까?”라는 고민이 생기기 시작했다.

     

    이러한 고민을 하던 중 Hexagonal Architecture 라는 구조를 접하게 되었다.
    비즈니스 로직을 중심에 두고 외부 시스템과의 의존성을 분리한다는 점에서 기존의 Layered Architecture와는 다른 설계 철학을 가지고 있다는 점이 흥미롭게 느껴졌다.

     

    이번 글에서는 Hexagonal Architecture가 무엇인지, 어떤 문제를 해결하기 위해 등장했는지, 그리고 기존 Layered Architecture와 어떤 차이가 있는지 정리해보려고 한다..

     

     


     

    Hexagonal Architecture ( = Ports and Adapters Architecture) 란 ?

    Hexagonal Architecture는 Ports and Adapters Architecture라고도 불리며,
    도메인 중심 설계를 기반으로 한 아키텍처 패턴이다..

     

    이 구조의 핵심 목적은 비즈니스 로직을 외부 요소로부터 분리하는 것이다.

    일반적인 애플리케이션에서는 UI, 데이터베이스, 외부 API와 같은 요소들이 비즈니스 로직과 강하게 결합되는 경우가 많다. 하지만 Hexagonal Architecture에서는 이러한 요소들을 비즈니스 로직과 분리된 외부 요소(Adapters)로 취급한다.

     

    즉, UI나 데이터베이스는 언제든지 교체될 수 있는 외부 요소로 보고,
    애플리케이션의 핵심인
    도메인 로직은 이러한 외부 시스템에 의존하지 않도록 설계한다.

     

    이러한 구조를 통해 비즈니스 로직은 외부 시스템에 직접 의존하지 않게 되고, 애플리케이션의 구조를 더 유연하게 유지할 수 있다.

     

     

     

    Hexagonal Architecture 의 주요 구성 요소

    01 Application Core

    애플리케이션의 핵심 비즈니스 로직이 위치하는 영역이며, 일반적으로 Domain Entity 와 Use Case 로 구성된다.

     

    Domain Entity 

    Domain Entity 는 비즈니스 도메인을 표현하는 객체이다. 

    즉, 서비스에서 다루는 핵심 데이터와 그 데이터에 대한 규칙을 담고 있는 객체라고 볼 수 있다.

     

    예를 들어, `회원 서비스`를 만든다고 가정하면 `User`, `Payment`, `Product` 와 같은 객체들이 Domain Entity가 될 수 있다.

    만약 이들이 단순한 데이터 객체라면 `User`라는 객체는 다음과 같이 표현된다.

    class User {
        String name;
        String email;
    }

     

    하지만 Domain Entity는 보통 도메인 규칙을 함께 포함하기 때문에 다음과 같이 표현된다.

    class User {
        String name;
        String email;
    
        void changeEmail(String newEmail) {
            if(!newEmail.contains("@")) {
                throw new IllegalArgumentException("Invalid email");
            }
            this.email = newEmail;
        }
    }

     

    Use Case (Application Service)

    Use Case는 애플리케이션이 수행해야 하는 실제 기능 (행위) 를 의미한다.

    사용자의 요청을 처리하기 위한 비즈니스 로직의 흐름을 정의하는 역할을 한다.

     

    예를 들어 `회원가입`, `주문 생성`, `상품 구매`, `결제 처리`와 같은 기능들이 Use Case가 될 수 있다. 

    이는 보통 여러 Domain Entity를 사용해 하나의 비즈니스 흐름을 완성한다.

     

    회원가입 기능을 구현하면, 다음과 같은 흐름을 가질 수 있다.

    1. 회원 정보 검증
    2. 회원 객체 생성
    3. 데이터베이스 저장

    간단하게 다음과 같이 표현될 수 있다.

    class RegisterUserUseCase {
    
        private final UserRepository userRepository;
    
        public RegisterUserUseCase(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        public void register(String name, String email) {
    
            // 1. 회원 정보 검증
            if (!email.contains("@")) {
                throw new IllegalArgumentException("Invalid email");
            }
    
            // 2. 회원 객체 생성
            User user = new User(name, email);
    
            // 3. 데이터베이스 저장
            userRepository.save(user);
        }
    }

     

    02 Port

    Application Core 와 Adapter 사이의 통신을 담당하는 인터페이스이다.

     

    Application Core는 외부 시스템과 직접 통신하지 않고, Port를 통해 간접적으로 상호작용한다.

    즉, Application Core는 외부 기술(DB, API 등)을 직접 알 필요 없이 Port 인터페이스만 의존하게 된다.

     

    `회원 정보를 저장해야 하는 경우`나 `외부 API에서 데이터를 가져와야 하는 경우`를 예시로 들어보자. 

     

    Application Core는 직접 데이터베이스를 호출하는 대신,

    "회원 정보를 저장해 주세요." 라는 역할을 하는 Port 인터페이스를 정의한다. 

     

    다음은 `회원정보를 검증`하고, `회원정보를 저장` 하는 Port 인터페이스의 예시다.

    public interface UserRepository {
    
        void save(User user);
    
        Optional<User> findByEmail(String email);
    
    }

     

    그리고 실제 데이터베이스와 연결되는 구현은 Adapter에서 담당하게 된다.

     

     

    03 Adapter

    외부 시스템과 실제로 연결되는 구현체이다.

    Hexagonal Architecture에서는 Adapter를 Driving Adapter(Incoming Adapter)Driven Adapter(Outgoing Adapter), 크게 두 가지로 구분한다.

     

    Driving Adapter (Incoming Adapter)

    Driving Adapter애플리케이션을 호출하는 역할을 한다. 

    Web Controller, REST API 등과 같이 사용자의 요청이나 외부 이벤트를 받아

    Application Core의 Use Case를 호출하는 어댑터이다.

     

    예를 들어 사용자가 회원가입 요청을 보내면, Controller가 요청을 받아 RegisterUserUseCase를 호출하게 된다. 

    즉, Driving Adapter는 외부 요청을 애플리케이션 내부로 전달하는 역할을 한다.

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        private final RegisterUserUseCase registerUserUseCase;
    
        public UserController(RegisterUserUseCase registerUserUseCase) {
            this.registerUserUseCase = registerUserUseCase;
        }
    
        @PostMapping
        public ResponseEntity<Void> registerUser(@RequestBody RegisterUserRequest request) {
    
            registerUserUseCase.register(
                request.getName(),
                request.getEmail()
            );
    
            return ResponseEntity.ok().build();
        }
    }

     

    Driven Adapter (Outgoing Adapter)

    Driven Adapter는 Application Core 에 의해 호출되는 어댑터이다.

    데이터베이스 접근, 외부 API 호출, 메세지 큐 등과 같이 애플리케이션이 외부 시스템과 상호작용 할 때 사용된다.

     

    예를 들어 회원 정보를 저장해야 하는 경우,
    Application Core는 Port 인터페이스를 통해 요청을 전달하고, 실제 데이터베이스와의 통신은 Driven Adapter가 담당하게 된다.

    @Repository
    public class UserRepositoryAdapter implements UserRepository {
    
        private final JpaUserRepository jpaUserRepository;
    
        public UserRepositoryAdapter(JpaUserRepository jpaUserRepository) {
            this.jpaUserRepository = jpaUserRepository;
        }
    
        @Override
        public void save(User user) {
            jpaUserRepository.save(user);
        }
    
        @Override
        public Optional<User> findByEmail(String email) {
            return jpaUserRepository.findByEmail(email);
        }
    }

     

    즉, Driving Adapter애플리케이션을 구동시키는 쪽, 
    Driven Adapter애플리케이션에 의해 사용되는 쪽 이라고 이해하면 쉽다.

     

     

    Hexagonal Architecture 의 동작 흐름

    앞서 살펴본 구조를 기준으로 회원가입 요청이 들어왔을 때의 동작 흐름을 예시로 살펴보면 다음과 같다.

    1. 사용자 요청 (HTTP Request)
      : 사용자가 회원가입을 요청하면 HTTP 요청이 서버로 전달된다.
      이 요청은 먼저 Controller와 같은 Driving Adapter가 받게 된다.

    2. Driving Adapter (Controller)
      : User Controller는 사용자의 요청을 받아 필요한 데이터를 추출한 뒤, 
      Application Core에 위치한 `RegisterUserUseCase`를 호출한다.

      이 단계에서 Controller는 요청을 전달하는 역할만 담당하며, 실제 비즈니스 로직은 Application Core에서 처리된다.
    3. Application Core (Use Case)
      : `RegisterUserUseCase` 는 회원가입이라는 비즈니스 로직의 흐름을 담당한다.

      이 단계에서는 `회원 정보 검증`, `회원 객체 생성`, `회원 정보 저장 요청` 과 같은 작업이 수행된다.
      여기서 중요한 점은 Application Core가 데이터베이스에 직접 접근하지 않는다는 것이다.
    4. Port
      : Application Core는 데이터베이스에 직접 접근하는 대신` UserRepository`라는 Port 인터페이스를 통해 요청을 전달한다.

      즉, Application Core는 회원 정보를 저장해 달라는 요청만 정의하고 실제 구현은 알 필요가 없다.

    5. Driven Adapter
      : `UserRepositoryAdapte`r는 `UserRepository` Port를 구현한 Adapter이다.

      Application Core로부터 전달받은 요청을 실제 데이터베이스 작업으로 변환하여 회원 정보를 데이터베이스에 저장한다.

    6. Database
      : 마지막으로 Driven Adapter를 통해 실제 데이터베이스에 데이터가 저장된다.

     

     


     

    지금까지 Hexagonal Architecture의 개념과 주요 구성 요소, 그리고 실제 동작 흐름까지 간단하게 정리해보았다.

     

    Hexagonal Architecture는 애플리케이션의 핵심인 비즈니스 로직을 중심에 두고 외부 시스템과의 의존성을 분리하는 구조라는 점에서 기존에 익숙하게 사용하던 Layered Architecture와는 다른 관점을 제시한다. 특히 데이터베이스나 웹 프레임워크와 같은 외부 기술에 직접 의존하지 않도록 설계할 수 있다는 점이 인상적으로 느껴졌다.

     

    아직 실제 프로젝트에서 직접 적용해 본 경험은 없지만, 구조를 공부하면서 비즈니스 로직을 어떻게 보호하고 유지보수하기 좋은 구조를 만들 수 있을지에 대해 다시 생각해보게 된 것 같다.

    'ETC ..' 카테고리의 다른 글

    [AWS Community day] DAY1 - Basic 참여  (0) 2025.11.10