안녕하세요, 오픈소스컨설팅 Solution-Dev 팀의 송홍섭입니다.
이번 포스팅은 소프트웨어 아키텍처에 관심 있는 분들을 위해 헥사고날 아키텍처(Hexagonal Architecture)를 둘러보려 합니다.
이 글에서는 헥사고날 아키텍처의 개념, 장단점, 그리고 기존 계층 구조의 아키텍처와의 차이점을 살펴보며, Java 예제를 통해 이해를 돕고자 합니다.
지금부터 헥사고날 아키텍처의 세계로 함께 떠나봅시다!

이미지 출처: Spring Boot and Hexagonal Architecture

헥사고날 아키텍처란?

헥사고날 아키텍처(Hexagonal Architecture), 또는 포트와 어댑터 아키텍처(Ports and Adapters Architecture)는 소프트웨어 아키텍처 중 하나로, Alistair Cockburn에 의해 제안되었습니다.
이 아키텍처의 주요 목표는 응용 프로그램의 비즈니스 로직을 외부 세계로부터 격리시켜 유연하고 테스트하기 쉬운 구조를 만드는 것입니다.
이를 위해 핵심 비즈니스 로직은 중앙의 도메인 영역에 위치하며, 입력과 출력을 처리하는 포트와 어댑터를 통해 외부와 소통합니다.

헥사고날 아키텍처의 장단점

장점:

  1. 유연성: 외부 시스템이나 인프라와의 의존성을 낮추어, 구성 요소를 쉽게 교체하거나 업데이트할 수 있습니다.
  2. 테스트 용이성: 비즈니스 로직을 독립적으로 테스트할 수 있어 품질 향상과 개발 속도 향상에 도움이 됩니다.
  3. 유지보수성: 책임이 분리되어 있어, 코드의 이해와 수정이 용이하며, 변화에 빠르게 대응할 수 있습니다.

단점:

  1. 구현 복잡성: 포트와 어댑터를 구성하고 관리하는 데 약간의 복잡성이 따릅니다.
  2. 초반 개발 시간 증가: 아키텍처를 처음 구축할 때 시간과 노력이 더 필요할 수 있습니다.

이미지 출처: Hexagonal Architecture in Java

3계층 아키텍처(3 Tier Layered Architecture)와 헥사고날 아키텍처의 차이

가장 대중적으로 사용하는 아키텍처는 3계층 아키텍처(3 Tier Layered Architecture)로 비즈니스 로직, 데이터 액세스, 프레젠테이션 계층으로 구성됩니다.
이와 달리 헥사고날 아키텍처는 비즈니스 로직에 중점을 두고 외부와 격리되어 있어, 더욱 유연하고 테스트하기 쉽습니다.

Java를 사용한 3계층 아키텍처와 헥사고날 아키텍처의 비교 및 예제

1. 3계층 아키텍처 예

// UserService.java
public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
    }
}

// UserRepository.java
public interface UserRepository {
    void save(User user);
}

// UserRepositoryImpl.java
public class UserRepositoryImpl implements UserRepository {
    public void save(User user) {
        // 데이터베이스에 사용자 저장
    }
}

2. 헥사고날 아키텍처 예

// CreateUserUseCase.java
public interface CreateUserUseCase {
    void createUser(String name, String email);
}

// CreateUserUseCaseImpl.java
public class CreateUserUseCaseImpl implements CreateUserUseCase {
    private UserRepository userRepository;

    public CreateUserUseCaseImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
    }
}

// UserRepository.java
public interface UserRepository {
    void save(User user);
}

// UserRepositoryAdapter.java
public class UserRepositoryAdapter implements UserRepository {
    public void save(User user) {
        // 데이터베이스에 사용자 저장
    }
}

위 예제에서 기존 아키텍처는 비즈니스 로직이 UserService 클래스에 있으며, UserRepository 인터페이스를 통해 데이터 액세스 계층과 소통합니다.

헥사고날 아키텍처에서는 CreateUserUseCase 인터페이스를 통해 비즈니스 로직을 정의하고, CreateUserUseCaseImpl 클래스에서 구현합니다. UserRepositoryAdapter는 UserRepository 인터페이스를 구현하며, 외부와의 소통을 담당합니다.

결론적으로, 헥사고날 아키텍처는 비즈니스 로직과 외부 요소를 격리시키기 때문에, 유연성과 테스트 용이성이 향상되는 장점이 있습니다.
이를 통해 효과적인 소프트웨어 개발을 지원할 수 있습니다.

추가로 카프카(Kafka)와 같은 외부 시스템을 연결하는 예제를 통해 헥사고날 아키텍처의 장점을 알 수 있습니다.

3. 3계층 아키텍처 Kafka 적용 예

// KafkaProducer.java
public class KafkaProducer {
    public void send(User user) {
        // 카프카에 사용자 정보 전송
    }
}

// UserService.java
public class UserService {
    private UserRepository userRepository;
    private KafkaProducer kafkaProducer;

    public UserService(UserRepository userRepository, KafkaProducer kafkaProducer) {
        this.userRepository = userRepository;
        this.kafkaProducer = kafkaProducer;
    }

    public void createUser(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
        kafkaProducer.send(user);
    }
}

4. 헥사고날 아키텍처 Kafka 적용 예

// OutputPort.java
public interface OutputPort {
    void sendMessage(User user);
}

// KafkaAdapter.java
public class KafkaAdapter implements OutputPort {
    private KafkaProducer kafkaProducer;

    public KafkaAdapter(KafkaProducer kafkaProducer) {
        this.kafkaProducer = kafkaProducer;
    }

    public void sendMessage(User user) {
        kafkaProducer.send(user);
    }
}

// CreateUserUseCaseImpl.java
public class CreateUserUseCaseImpl implements CreateUserUseCase {
    private UserRepository userRepository;
    private OutputPort outputPort;

    public CreateUserUseCaseImpl(UserRepository userRepository, OutputPort outputPort) {
        this.userRepository = userRepository;
        this.outputPort = outputPort;
    }

    public void createUser(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
        outputPort.sendMessage(user);
    }

}

기존 아키텍처에서는 UserService 클래스가 카프카와 직접 연결되어 있습니다.
이 경우 UserService는 카프카에 대한 의존성을 가지게 됩니다.

반면 헥사고날 아키텍처에서는 OutputPort 인터페이스를 통해 외부 시스템과의 의존성을 분리합니다.
카프카와 연결하는 로직은 KafkaAdapter 클래스에서 처리하며, CreateUserUseCaseImpl은 OutputPort 인터페이스를 통해 메시지를 전송합니다.
이러한 구조를 통해 외부 시스템 변경이 있을 때에도 비즈니스 로직에 영향을 최소화하고 유연성을 확보할 수 있습니다.

여기서 KafkaProducer를 카프카 외의 다른 메시징 시스템으로 교체하려면 기존 아키텍처에서는 UserService를 수정해야 합니다.
헥사고날 아키텍처에서는 OutputPort 인터페이스를 구현한 새로운 어댑터를 만들어 주입하면 됩니다.
이렇게 헥사고날 아키텍처는 외부 시스템과의 연결에서 유연성과 확장성을 제공합니다.

두 아키텍처의 가장 큰차이점을 확인하는 방법은 기존 카프카 대신 RabbitMQ로 변경하는 모습을 보면 알 수 있습니다.
다음과 아키텍처별 작업이 필요합니다.

5. 3계층 아키텍처 Kafka -> RabbitMQ 로 변경 예

// RabbitMQProducer.java
public class RabbitMQProducer {
    public void send(User user) {
        // RabbitMQ에 사용자 정보 전송
    }
}

// UserService.java
public class UserService {
    private UserRepository userRepository;
    private RabbitMQProducer rabbitMQProducer;

    public UserService(UserRepository userRepository, RabbitMQProducer rabbitMQProducer) {
        this.userRepository = userRepository;
        this.rabbitMQProducer = rabbitMQProducer;
    }

    public void createUser(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
        rabbitMQProducer.send(user);
    }
}

RabbitMQProducer 생성하고 UserService 클래스에서 KafkaProducer 대신 RabbitMQProducer를 사용하여 메시지를 전송하도록 수정합니다.

UserRepository userRepository = new UserRepositoryImpl();
RabbitMQProducer rabbitMQProducer = new RabbitMQProducer();
UserService userService = new UserService(userRepository, rabbitMQProducer);

UserService 인스턴스를 생성할 때, UserRepository와 함께 RabbitMQProducer를 주입합니다.
기존 아키텍처에서 카프카를 RabbitMQ로 변경하기 위해 UserService 클래스를 수정해야 합니다.
이렇게 기존 아키텍처는 외부 시스템의 변경에 대응하기 위해 비즈니스 로직에 해당하는 클래스를 수정해야 하는 단점이 있습니다.


반면 헥사고날 아키텍처에서는 어댑터를 교체하는 방식으로 비즈니스 로직의 수정 없이 외부 시스템의 변경에 대응할 수 있습니다.

6. 헥사고날 아키텍처 Kafka -> RabbitMQ 로 변경 예

// RabbitMQProducer.java
public class RabbitMQProducer {
    public void send(User user) {
        // RabbitMQ에 사용자 정보 전송
    }
}

// RabbitMQAdapter.java
public class RabbitMQAdapter implements OutputPort {
    private RabbitMQProducer rabbitMQProducer;

    public RabbitMQAdapter(RabbitMQProducer rabbitMQProducer) {
        this.rabbitMQProducer = rabbitMQProducer;
    }

    public void sendMessage(User user) {
        rabbitMQProducer.send(user);
    }
}

6-1. 어댑터 교체
CreateUserUseCaseImpl의 인스턴스를 생성할 때, KafkaAdapter 대신 RabbitMQAdapter를 주입하면 됩니다.

UserRepository userRepository = new UserRepositoryAdapter();
RabbitMQProducer rabbitMQProducer = new RabbitMQProducer();
OutputPort outputPort = new RabbitMQAdapter(rabbitMQProducer);
CreateUserUseCase createUserUseCase = new CreateUserUseCaseImpl(userRepository, outputPort);

이처럼 헥사고날 아키텍처는 외부 시스템의 변경에 빠르게 대응할 수 있게 해주며, 비즈니스 로직에 영향을 미치지 않습니다.
이를 통해 개발 효율성과 유지보수성이 향상되며, 확장성 있는 소프트웨어를 구축할 수 있습니다.

마치며

헥사고날 아키텍처에 대한 짧은 여정이 마무리 되었습니다.
이 글을 통해 헥사고날 아키텍처의 개념과 기존 아키텍처와의 차이점, 그리고 외부 시스템과의 연결에서 헥사고날 아키텍처의 유연성을 확인해보았습니다.
여러분이 이 글에서 얻은 지식을 소프트웨어 개발에 유용하게 활용하시길 바랍니다.
다음에도 즐거운 소프트웨어 이야기로 찾아뵙겠습니다.
그때까지 즐거운 코딩 되세요!

안녕하세요. 오픈소스컨설팅에서 Solution-dev팀의 송홍섭입니다. 다양한 개발 경험과 DevOps 경험이 있습니다. 개발 문화에 많은 관심을 갖고 있습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *

One reply on “헥사고날 아키텍처(Hexagonal Architecture) : 유연하고 확장 가능한 소프트웨어 디자인 🌟 feat. Java Example”

  • ㅇㅇ
    2023년 12월 22일 at 1:51 pm

    잘 보고 갑니다