
소프트웨어 아키텍처는 애플리케이션의 확장성과 유지보수성을 결정짓는 핵심 요소로, 설계 선택에 따라 개발 과정과 결과물에 큰 영향을 미칩니다. Layered Architecture(계층형 아키텍처)와 Hexagonal Architecture(헥사고날 아키텍처)는 서로 다른 설계 철학을 기반으로 한 대표적인 아키텍처 방식입니다. 이 글에서는 두 아키텍처의 이론적 기반과 실무적 차이를 비교하고, 주차 시스템을 활용한 현실적인 예제를 통해 그 특성을 심도 있게 분석합니다.
Layered Architecture는 소프트웨어를 여러 계층으로 나누어 설계하는 전통적이고 직관적인 방식입니다. 각 계층은 서로 독립적이면서도 특정한 역할을 수행하며, 일반적으로 다음과 같은 구성으로 이루어집니다:
@RestController
@RequestMapping("/parking")
public class ParkingController {
private final ParkingService parkingService;
public ParkingController(ParkingService parkingService) {
this.parkingService = parkingService;
}
@PostMapping("/tickets")
public ResponseEntity<ParkingTicket> issueTicket(@RequestBody VehicleDto vehicleDto) {
ParkingTicket ticket = parkingService.issueTicket(vehicleDto);
return ResponseEntity.ok(ticket);
}
}
@Service
public class ParkingService {
private final ParkingRepository parkingRepository;
public ParkingService(ParkingRepository parkingRepository) {
this.parkingRepository = parkingRepository;
}
public ParkingTicket issueTicket(VehicleDto vehicleDto) {
Vehicle vehicle = new Vehicle(vehicleDto.getLicensePlate());
ParkingTicket ticket = new ParkingTicket(vehicle);
parkingRepository.save(ticket);
return ticket;
}
}
@Repository
public interface ParkingRepository extends JpaRepository<ParkingTicket, Long> {}
Presentation Layer -> Application Layer -> Domain Layer -> Infrastructure Layer
(ParkingController) (ParkingService) (ParkingTicket) (ParkingRepository)
Hexagonal Architecture는 애플리케이션의 비즈니스 로직(Core)을 외부 의존성으로부터 완전히 분리하는 것을 목표로 합니다. 이를 위해 포트와 어댑터 개념을 사용합니다:
// Core
interface ParkingService {
fun issueTicket(vehicleDto: VehicleDto): ParkingTicket
}
class ParkingServiceImpl(private val parkingRepository: ParkingRepository) : ParkingService {
override fun issueTicket(vehicleDto: VehicleDto): ParkingTicket {
val vehicle = Vehicle(vehicleDto.licensePlate)
val ticket = ParkingTicket(vehicle)
parkingRepository.save(ticket)
return ticket
}
}
// Ports
interface ParkingRepository {
fun save(ticket: ParkingTicket)
}
// Adapters
@Repository
class JpaParkingRepository(private val jpaRepository: SpringDataJpaParkingRepository) : ParkingRepository {
override fun save(ticket: ParkingTicket) {
jpaRepository.save(ticket)
}
}
interface SpringDataJpaParkingRepository : JpaRepository<ParkingTicket, Long>
Adapters (REST API, Repository)
| ^
v |
Ports (ParkingService, ParkingRepository)
|
v
Core (Business Logic)
| 항목 | Layered Architecture | Hexagonal Architecture |
|---|---|---|
| 구조 | 수직적 계층 구조 | 포트와 어댑터를 통한 수평적 구조 |
| 비즈니스 로직 | 특정 계층(주로 도메인 계층)에 포함 | 애플리케이션의 중심(core)에 독립적으로 위치 |
| 테스트 용이성 | 외부 의존성 때문에 테스트가 어려울 수 있음 | 외부 의존성과 분리되어 테스트가 용이 |
| 유연성 | 특정 계층 간 강한 결합 가능성 | 외부 의존성 변경에 대한 높은 유연성 |
| 학습 곡선 | 비교적 쉬움 | 비교적 어려움 |
📌 이 글은 TDD 강의를 학습한 내용을 바탕으로 재구성하였습니다. 문제가 되는 부분이 있다면 수정하겠습니다.