|

레이어드 아키텍처란 무엇인가: 핵심만 정리

레이어드 아키텍처에서 계층을 나누는 이유와 책임을 정리한 대표 이미지
계층은 폴더 장식이 아니라 책임과 변경을 분리하는 도구다

레이어드 아키텍처는 코드를 예쁘게 나누는 기술이 아닙니다. 한 화면, 한 서비스, 한 저장소 안에 뒤섞이기 쉬운 책임을 분리해서 변경을 덜 퍼지게 만드는 방식입니다. 이번 글에서는 presentation, application, domain, infrastructure를 실무 기준으로 정리하고, 왜 계층을 나누는지, 어디서 많이 오해하는지, 언제 layering이 ceremony가 되는지도 함께 보겠습니다.


무엇을 나누는가

가장 단순하게 보면 레이어드 아키텍처는 관심사가 다른 코드를 한곳에 몰아넣지 말자는 생각입니다. 주문 API 하나를 만든다고 해보면 사용자는 HTTP 요청을 보내고, 서버는 입력을 검증하고, 할인 규칙을 적용하고, 재고를 확인하고, 결제를 호출하고, 주문을 저장합니다. 이 모든 일을 컨트롤러 하나가 다 하면 처음에는 빨라 보여도 조금만 커져도 UI 형식, 저장 방식, 비즈니스 규칙, 테스트 경계가 한꺼번에 엉키기 시작합니다.

  • 화면이나 API 형식이 바뀔 때 비즈니스 규칙까지 같이 흔들린다
  • 저장 방식이 바뀔 때 도메인 판단 코드도 같이 건드리게 된다
  • 테스트하려면 HTTP, DB, 외부 API를 한꺼번에 띄워야 한다
  • 한 파일 안에서 너무 많은 문맥을 동시에 따라가야 한다

Martin Fowler도 presentation, domain logic, data access를 나누는 이유로 한 번에 생각해야 할 범위를 줄이는 것을 이야기합니다. 레이어를 나누는 이유는 추상적 미학이 아니라 변경 분리와 사고 범위 축소입니다.


왜 나눌까

실무에서 체감되는 장점은 보통 네 가지입니다. 변경이 덜 퍼지고, 읽을 때 문맥 전환이 줄고, 테스트 경계가 생기고, 협업할 때 책임이 분명해집니다. 예를 들어 주문 가능 여부 판단이 presentation 안에 들어 있으면 채널이 늘 때마다 로직이 복제되기 쉽지만, domain 쪽에 모여 있으면 UI가 바뀌어도 규칙은 같은 자리에 남습니다.

Microsoft Learn의 N-tier 문서도 레이어를 책임 분리와 의존성 관리의 구조로 설명합니다. 보통 상위 레이어는 하위 레이어의 서비스를 사용할 수 있지만, 하위 레이어가 상위 레이어를 알기 시작하면 경계가 금방 무너집니다.


presentation, application, domain, infrastructure 책임

레이어 이름은 팀마다 조금씩 다르지만, 네 층을 책임 기준으로 보면 훨씬 덜 헷갈립니다. 이번 글에서는 presentation은 입력과 출력, application은 유스케이스 흐름, domain은 핵심 규칙, infrastructure는 외부 시스템 연동으로 이해하겠습니다.

presentation

presentation은 사용자의 입력과 출력에 가장 가까운 층입니다. 웹이라면 controller, request/response DTO, serializer가 여기에 가깝고, 앱이라면 화면과 UI 상태 변환이 여기에 걸칩니다. 이 층의 질문은 보통 요청을 어떤 형식으로 받을지, 응답을 어떤 형식으로 보여줄지, 어떤 에러 문구를 내보낼지입니다. 즉 presentation은 사용자나 외부 클라이언트와 맞닿는 번역기에 가깝습니다.

application

application은 이 유스케이스를 어떤 순서로 실행할 것인가를 조율하는 층입니다. 사용자 권한 확인, 상품 조회, 주문 생성, 결제 요청, 저장, 이벤트 발행 같은 흐름이 여기에 모입니다. 여기서 중요한 점은 application이 규칙의 주인이라기보다 흐름의 주인이라는 것입니다. 할인율 계산, 취소 가능 조건 같은 핵심 규칙까지 이 층에 계속 쌓이면 application service만 비대해집니다.

domain

domain은 이 시스템이 정말로 풀어야 하는 문제의 규칙이 있는 층입니다. 주문 생성 가능 여부, 쿠폰 적용 조건, 재고 부족 시 정책, 취소 가능 시점 판단 같은 내용이 여기에 속합니다. DB가 무엇인지, HTTP 상태 코드가 무엇인지 몰라도 이해할 수 있는 코드가 많을수록 domain은 건강한 편입니다.

infrastructure

infrastructure는 DB 접근, 외부 API 호출, 메시지 브로커 연동, 파일 저장, 이메일·푸시 전송처럼 외부 세계와 실제로 붙는 층입니다. domain이 결제를 요청해야 한다는 사실을 표현한다면, infrastructure는 실제 PG API를 호출하는 구현을 가집니다. 이 층은 필요하지만 자주 바뀌고 기술 세부사항이 많아서 다른 층과 거리를 두는 가치가 큽니다.


레이어드 아키텍처 예시: 주문 요청 흐름

온라인 주문 API를 아주 단순하게 보면 흐름은 이렇습니다. presentation이 HTTP 요청을 받고, application이 주문 생성 유스케이스를 시작하고, domain이 주문 가능 여부와 할인 규칙을 판단하고, infrastructure가 DB 저장과 결제 API 호출을 수행한 뒤, application이 결과를 정리하고, presentation이 최종 응답 JSON을 만듭니다. 중요한 것은 모든 레이어가 같은 무게로 일하는 것이 아니라는 점입니다.

// 책임이 섞인 예시
class OrderController(
    private val jdbcTemplate: JdbcTemplate,
    private val paymentClient: PaymentClient
) {
    fun create(request: CreateOrderRequest): OrderResponse {
        if (request.items.isEmpty()) {
            throw IllegalArgumentException("주문 상품이 없습니다")
        }

        val totalPrice = request.items.sumOf { it.price * it.quantity }
        val discount = if (request.couponCode == "WELCOME10") totalPrice * 0.1 else 0.0
        val finalPrice = totalPrice - discount

        paymentClient.pay(request.userId, finalPrice)
        jdbcTemplate.update("insert into orders ...")

        return OrderResponse(success = true, amount = finalPrice)
    }
}

위 코드는 입력 처리, 할인 규칙, 결제 호출, DB 저장, 응답 생성이 한곳에 섞여 있습니다. 이 상태에서는 결제 정책이 바뀌어도 컨트롤러를 열어야 하고, API 응답 형식이 바뀌어도 핵심 규칙 코드 옆을 건드리게 됩니다.

class CreateOrderUseCase(
    private val productRepository: ProductRepository,
    private val orderRepository: OrderRepository,
    private val paymentGateway: PaymentGateway
) {
    fun execute(command: CreateOrderCommand): OrderResult {
        val products = productRepository.findAll(command.productIds)
        val order = Order.create(command.userId, products, command.couponCode)

        paymentGateway.pay(order.payerId, order.finalPrice)
        orderRepository.save(order)

        return OrderResult(order.id, order.finalPrice)
    }
}

이 코드가 완벽하다는 뜻은 아닙니다. 다만 요청 해석은 presentation, 유스케이스 순서 조율은 application, 가격과 쿠폰 규칙은 domain, 결제 구현과 저장 구현은 infrastructure라는 구분이 훨씬 읽기 쉬워집니다.


레이어드 아키텍처의 흔한 오해

폴더만 나누면 끝난다

controller, service, repository, domain 폴더가 있다고 해서 레이어가 잘 나뉜 것은 아닙니다. service가 SQL도 만들고 비즈니스 규칙도 다 가지거나, repository가 외부 API 응답 가공까지 하고, controller에서 바로 repository를 부르는 구조라면 책임은 여전히 섞여 있습니다. 레이어드 아키텍처의 기준은 폴더 이름이 아니라 변경 이유가 섞이지 않았는가입니다.

레이어는 많을수록 좋다

작은 CRUD 서비스에 facade, application service, domain service, repository interface, impl, mapper, assembler, presenter까지 모두 넣으면 요청 하나가 클래스를 여러 개 통과하면서 값만 옮겨 다니게 될 수 있습니다. closed layer가 경계를 엄격히 지키는 장점은 있지만, 실제 책임이 없는 중간 단계가 많아지면 오히려 오버헤드가 됩니다. 레이어는 복잡도를 나누기 위해 존재하지, 복잡해 보이기 위해 존재하지 않습니다.

domain은 무조건 거대한 모델이어야 한다

domain의 핵심은 클래스 숫자가 아니라 규칙의 위치입니다. 취소 가능 여부 같은 중요한 규칙이 controller나 service 곳곳에 흩어져 있다면 domain 쪽에 모으는 편이 낫지만, 시스템이 거의 CRUD에 가깝고 복잡한 정책이 없다면 억지로 rich domain model을 만들 필요는 없습니다. 없는 복잡성을 미리 과장해서 모델링하는 것도 다른 종류의 과설계입니다.


ceremony가 되는 순간

  • service 메서드가 repository 한 줄 위임만 한다
  • DTO, entity, domain model, response model이 거의 같은 필드만 복사한다
  • 비즈니스 규칙은 거의 없는데 파일 수만 빠르게 늘어난다
  • 새 기능 하나 추가할 때 읽어야 할 파일이 지나치게 많다
  • 테스트가 쉬워진 것이 아니라 mock 설정만 늘어난다

이런 신호가 보이면 레이어를 더 넣을까보다 정말 분리할 책임이 있는가를 먼저 물어야 합니다. 작은 관리자 페이지, 단순 게시판, 내부 운영 도구처럼 규칙이 단순한 시스템은 presentation + 얇은 application + data access 정도만으로도 충분할 수 있습니다. 반대로 결제, 정산, 재고, 동기화처럼 규칙이 복잡하고 실패 비용이 큰 시스템은 domain과 infrastructure를 더 엄격히 나눌 가치가 큽니다.


현실적인 적용 기준

처음부터 정답 구조를 완성하려고 하면 대개 실패합니다. 실무에서는 보통 presentation에서 외부 입력과 출력을 정리하고, application에서 유스케이스 흐름을 모으고, 반복되거나 중요한 규칙이 보이면 domain으로 올리고, DB·메시지·외부 API 구현은 infrastructure로 밀어내는 순서가 더 안정적입니다. 분리가 실제 이득을 줄 때만 인터페이스와 매핑을 늘리면 됩니다.

  1. presentation에서 외부 입력과 출력을 정리한다
  2. application에서 유스케이스 흐름을 모은다
  3. 반복되거나 중요한 규칙이 보이면 domain으로 올린다
  4. 외부 시스템 구현은 infrastructure로 분리한다
  5. 분리가 실제 이득을 줄 때만 인터페이스와 매핑을 늘린다

이 접근의 장점은 구조가 복잡도에 맞춰 자란다는 점입니다. 처음부터 거대한 템플릿을 깔아두는 것보다, 어디서 변경이 자주 새는지를 보고 경계를 키우는 편이 훨씬 실용적입니다.


빠르게 보는 체크리스트

  • API 형식이 바뀔 때 비즈니스 규칙 코드까지 자주 흔들리는가
  • 같은 규칙이 여러 화면이나 여러 API에 복사되는가
  • 외부 API나 DB 세부사항을 도메인 판단 코드가 너무 많이 알고 있는가
  • 테스트하려는 규칙이 프레임워크나 저장소 때문에 검증하기 어려운가
  • 팀원이 늘면서 어디를 고쳐야 할지 합의가 자주 깨지는가

여기에 여러 개가 그렇다라면 레이어를 조금 더 분리할 시점일 가능성이 큽니다. 반대로 대부분 아니다라면 아직은 구조를 더 늘리기보다 단순하고 읽기 쉬운 흐름을 유지하는 편이 낫습니다.


정리

레이어드 아키텍처는 거창한 유행어가 아닙니다. presentation은 입력과 출력의 변화, application은 유스케이스 흐름의 변화, domain은 핵심 규칙의 변화, infrastructure는 기술 세부사항의 변화를 다룹니다. 이 경계가 잘 서 있으면 변경이 덜 퍼지고, 테스트가 쉬워지고, 읽는 흐름도 좋아집니다. 하지만 규칙이 단순한데도 레이어만 늘리면 구조는 금방 ceremony가 됩니다. 결국 중요한 질문은 이 시스템에서 지금 정말 분리해야 하는 책임이 무엇인가입니다.

함께 보면 좋은 글로는 객체지향에서 책임을 잘 나누는 기준, 인터페이스는 왜 필요할까, 의존성 역전 원칙은 왜 어렵게 느껴질까, 안드로이드 클린 아키텍처는 꼭 필요할까를 이어서 읽어보면 좋습니다. 외부 참고 자료로는 Martin Fowler의 Presentation Domain Data Layering, Microsoft Learn의 N-tier Architecture, Common web application architectures를 함께 보면 더 입체적으로 이해할 수 있습니다.

함께보면 좋은 글