기록

[JpaTest] Layered Architecture vs Hexagonal Architecture 본문

교육/강의

[JpaTest] Layered Architecture vs Hexagonal Architecture

youngyin 2025. 1. 26. 00:00

시작하면서

소프트웨어 아키텍처는 애플리케이션의 확장성과 유지보수성을 결정짓는 핵심 요소로, 설계 선택에 따라 개발 과정과 결과물에 큰 영향을 미칩니다. Layered Architecture(계층형 아키텍처)와 Hexagonal Architecture(헥사고날 아키텍처)는 서로 다른 설계 철학을 기반으로 한 대표적인 아키텍처 방식입니다. 이 글에서는 두 아키텍처의 이론적 기반과 실무적 차이를 비교하고, Kafka와 MySQL을 활용한 현실적인 예제를 통해 그 특성을 심도 있게 분석합니다.


Layered Architecture (계층형 아키텍처)

Layered Architecture는 소프트웨어를 여러 계층으로 나누어 설계하는 전통적이고 직관적인 방식입니다. 각 계층은 서로 독립적이면서도 특정한 역할을 수행하며, 일반적으로 다음과 같은 구성으로 이루어집니다:

  1. Presentation Layer: 사용자 인터페이스를 담당하며, 요청을 처리하고 결과를 반환합니다.
  2. Application Layer: 비즈니스 로직과 흐름을 제어하며, 도메인 계층과 상호작용합니다.
  3. Domain Layer (Business Logic Layer): 핵심 비즈니스 로직을 캡슐화합니다.
  4. Infrastructure Layer: 데이터베이스, 외부 API, 메시지 브로커와 같은 외부 시스템과의 통신을 처리합니다.

장점

  • 명확한 계층 분리: 초보 개발자도 쉽게 이해할 수 있는 구조.
  • 재사용성 증가: 특정 계층의 코드가 다른 프로젝트에서도 재사용 가능.
  • 넓은 적용 범위: 다양한 프로젝트에서 검증된 표준적인 방식.

단점

  • 테스트 어려움: 계층 간 강한 결합으로 인해 단위 테스트가 복잡해질 수 있음.
  • 유연성 부족: 특정 계층의 변경이 다른 계층에 큰 영향을 줄 수 있음.
  • 비즈니스 로직과 인프라 결합: 도메인 로직이 데이터 접근 방식에 의존하는 경향.

예제 : 이벤트 처리 (Kotlin + Spring Boot)

사용자 이벤트를 수신하고, 데이터를 MySQL에 저장합니다.

@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/events")
    public ResponseEntity<Void> handleUserEvent(@RequestBody UserEventDto userEventDto) {
        userService.processUserEvent(userEventDto);
        return ResponseEntity.ok().build();
    }
}

@Service
public class UserService {
    private final UserRepository userRepository;

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

    public void processUserEvent(UserEventDto userEventDto) {
        User user = new User(userEventDto.getId(), userEventDto.getName(), userEventDto.getEmail());
        userRepository.save(user);
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

public class UserEventDto {
    private Long id;
    private String name;
    private String email;
}

구조도

Presentation Layer  ->  Application Layer  ->  Domain Layer  ->  Infrastructure Layer
(UserController)        (UserService)          (User)           (UserRepository)

Hexagonal Architecture (헥사고날 아키텍처)

Hexagonal Architecture는 애플리케이션의 비즈니스 로직(Core)을 외부 의존성으로부터 완전히 분리하는 것을 목표로 합니다. 이를 위해 포트와 어댑터 개념을 사용합니다:

  • 포트: 코어와 외부 시스템 간의 인터페이스로, 코어가 외부와 상호작용하는 방법을 정의합니다.
  • 어댑터: 포트를 구현하여 외부 시스템(예: 데이터베이스, 메시지 브로커, 사용자 인터페이스 등)과 실제로 연결되는 역할을 수행합니다.

장점

  • 테스트 용이성: 외부 시스템과의 의존성이 분리되어 단위 테스트가 간단해짐.
  • 확장성과 유연성: 새로운 어댑터를 추가하거나 기존 의존성을 교체하기 쉽습니다.
  • 비즈니스 로직의 독립성: 핵심 로직이 외부 기술에 의존하지 않음.

단점

  • 복잡성 증가: 초기 설계와 구현이 Layered Architecture보다 어려움.
  • 작은 프로젝트에 부적합: 단순한 요구사항에는 과도한 설계로 보일 수 있음.

예제: 이벤트 처리 (Kotlin + Spring Boot)

사용자 이벤트를 수신하고, 데이터를 MySQL에 저장합니다.

// Core
interface UserService {
    fun processUserEvent(userEventDto: UserEventDto)
}

class UserServiceImpl(private val userRepository: UserRepository) : UserService {
    override fun processUserEvent(userEventDto: UserEventDto) {
        val user = User(userEventDto.id, userEventDto.name, userEventDto.email)
        userRepository.save(user)
    }
}

// Ports
interface UserRepository {
    fun save(user: User)
}

// Adapters
@Repository
class JpaUserRepository(private val jpaRepository: SpringDataJpaUserRepository) : UserRepository {
    override fun save(user: User) {
        jpaRepository.save(user)
    }
}

interface SpringDataJpaUserRepository : JpaRepository<User, Long>

@Component
class KafkaUserEventConsumer(private val userService: UserService) {
    @KafkaListener(topics = ["user-events"], groupId = "user-service-group")
    fun consume(userEventDto: UserEventDto) {
        userService.processUserEvent(userEventDto)
    }
}

@Entity
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long,
    val name: String,
    val email: String
)

data class UserEventDto(
    val id: Long,
    val name: String,
    val email: String
)

구조도

      Adapters (Kafka Consumer, Repository)
                  |          ^
                  v          |
      Ports (UserService, UserRepository)
                  |
                  v
               Core (Business Logic)

Layered vs Hexagonal Architecture 비교

항목Layered ArchitectureHexagonal Architecture

구조 수직적 계층 구조 포트와 어댑터를 통한 수평적 구조
비즈니스 로직 특정 계층(주로 도메인 계층)에 포함 애플리케이션의 중심(core)에 독립적으로 위치
테스트 용이성 외부 의존성 때문에 테스트가 어려울 수 있음 외부 의존성과 분리되어 테스트가 용이
유연성 특정 계층 간 강한 결합 가능성 외부 의존성 변경에 대한 높은 유연성
학습 곡선 비교적 쉬움 비교적 어려움

결론

Layered Architecture는 단순성과 접근성 덕분에 소규모 애플리케이션이나 빠른 개발이 필요한 프로젝트에서 적합합니다. 반면, Hexagonal Architecture는 복잡한 시스템에서 확장성과 유지보수성을 극대화할 수 있는 설계 방식입니다.

 
Comments