[Architecture] 4. Layered 아키텍처

KSK·2025년 5월 20일

Architecture

목록 보기
4/4

개요

  • 레이어드 아키텍처란

레이어드 아키텍처

이전 글에도 잠깐 썼지만 더 공부해보자.

레이어드 아키텍처란?

소프트웨어 시스템을 관심사 별로 구분한 '계층'으로 나누어 설계하는 아키텍처이다.

각 계층은 특정 기능과 역할을 담당한다.

특징


계층화로 분리된 책임

계층 별로 분리된 기술적인 책임과 역할을 가지고 있다.

위의 사진 기준,

  • Presentation Layer는 UI를 담당하며, 사용자와 입출력 등 상호작용을 담당한다.
  • Business Layer(Application 계층)는 주문 처리와 같은 핵심 비즈니스 로직을 처리한다.
  • Persistence Layer는 DB와의 연결을 담당한다.
  • Database Layer는 실제 DB임

각 계층은 다른 계층의 역할과 책임은 알지 못하고 관심도 없다.

예를 들어 Business Layer에선 로직이 사용자의 입장에서 UI를 통해 어떻게 보일지는 알 수 없고, 관여할 필요도 없다.

덕분에

  • 유지보수 용이
  • 재사용성

이라는 장점이 있다.


편의에 따른 계층 조정

개발자의 편의에 따라 필요한 계층을 추가할 수 있으며
기존 계층을 통합하거나 삭제할 수 있다.

일반적으로 위 처럼 4-tier 계층을 주로 사용한다.


단방향성


(이미지 출처: https://youtu.be/Ql7CoQminoM)

상위 계층이 하위 계층을 호출하는 단방향성을 갖는다.

일정한 흐름을 갖기 때문에 구조를 이해하기 쉽다는 장점이 있다.

이 단방향성에서 중요한 것은 CLOSED 란 개념이다.

각 계층은 바로 아래 계층에만 의존해야 하며, 모든 접근은 상위 계층 → 하위 계층으로만 제한되는 것을 말한다.

위 그림에서, Presentation Layer에서의 요청이 Persistence Layer로 전달되기 위해선,
Presentation Layer와 인접한 하위 계층인 Business Layer를 필수적으로 거쳐야한다.

CLOSED가 중요한 이유는, 레이어드 아키텍처의 특징인 단방향성과 계층간 관심사 분리를 보여주는 개념이기 때문이다.

반대로 OPEN 이란 개념도 있다.

항상 모든 계층이 닫혀 있어야 하는 것은 아닌데, 어느 특정 계층은 특정 기능에서만의 사용을 위해 설계되었다고 하자.

이 때 그 계층은 인접하지 않은 상위 계층에서 직접 접근할 수 있도록 열어둠으로써, 계층을 건너 뛰는 의존을 만드는 것이다.

문제점

DB 주도 설계

단방향적이고, 가장 하위 계층이 DB이기 때문에 결과적으로 프로젝트 전체가 DB에 의존한다.

이로 인해 도메인보다 DB를 위주로 설계하면서 절차지향적인 코드가 될 수 있다.

또한 DB의 변화가 다른 전체 계층에 영향을 끼칠 가능성도 존재한다.


도메인 변경 어려움

도메인 별로 계층이 분리된 것이 아닌, 기술적인 역할을 기준으로 분리되었기 때문에
하나의 도메인이 여러 계층에 분리되어 존재할 수 있다. (즉 계층간 강한 결합도를 가질 수 있다)

만약 그 도메인에 대한 코드가 일부 변경된다면 전체 코드의 변경이 필요할 수도 있다는 소리이다.

이 때 계층간 소통에 추상화된 인터페이스를 사용하여
상위 계층이 하위 계층의 인터페이스에 의존하게 한다면 계층 간 결합도가 약화되어
일부 문제를 해소할 수 있을 것 이다.


싱크홀 패턴

이는 계층 간 요청이 싱크홀 처럼 툭 떨어지는 것을 의미하는데,
즉 하위 계층에 전달이 되긴 하지만 막상 그 하위 계층에선 아무 추가 동작 없이 그저 그 다음 계층으로 전달만 하는 경우를 말한다.

Ex) 사용자로부터 DB의 데이터를 검색하는 요청이 들어왔을때, Business Layer는 요청을 Persistence Layer에게 전달하기만하고, DB에서 가져온 값을 다시 Presentation Layer에 넘겨주기만 하는 경우

이는 단순히 함수 호출만을 하거나 쓸데없는 메모리 공간을 낭비하는 오버헤드를 발생시키므로, 적절한 조치가 필요하다.

이 때 위에서 언급한 OPEN 을 활용하는데, 계층 간 단순히 요청이나 데이터의 전달만 이루어지는 경우 레이어를 개방하여 상위 계층에서 계층을 건너뛴 직접 요청을 하는 방식으로 수정할 수 있다.

다만 주의해야할 것은 역시 책임과 역할의 분리이며, OPEN 이 너무 남발될 경우 분리가 제대로 이루어지지 않아 유지보수에 어려움이 발생할 수 있으므로 적절한 수준의 선택이 필요하다.


선택의 기준은 일반적으로 80-20 규칙 을 따른다.

이는 어느 계층으로 들어오는 요청의 20%는 단순 통과 처리하고,
80%는 비즈니스 로직을 수행한다는 규칙인데,
만약 이 비율이 역전되어 단순 통과만 하는 요청이 많아진다면, 해당 계층의 개방을 고려한다.


사용

레이어드 아키텍처를 사용하는 일반적인 경우는 다음과 같다.

규모가 작은 앱이나 웹

도메인 복잡도가 낮고, 기능 흐름이 단순한 프로젝트에 적합하다.

ex) 간단한 CRUD 기반의 웹/앱, 관리자 대시보드, 블로그, 쇼핑몰 MVP 등

또한 소규모의 스타트업에서 빠른 결과가 필요할 때 아키텍처의 복잡도를 줄일 수 있으므로 좋은 선택이 될 수 있다.

-> 단, 빠르게 변하는 요구사항이나 실험적인 구조를 많이 도입할 스타트업에는 오히려 무거울 수도 있음. 따라서 초기 구조로서 간단하게 쓰되, 나중에 변경을 염두에 두는 유연성이 필요하다.

예시

아주 간단하게 코드로 살펴보려고 한다.

시나리오는 다음과 같다.

유저 정보가 저장된 DB에서 성인인 유저만 사용자에게 표시하는 기능의 구현

  • UserRepository는 이름과 나이를 가져옴 (Persistence Layer)
  • UserService는 성인 여부 판단 로직을 수행 (Business Layer)
  • UserController는 사용자에게 보여줄 메시지를 구성하여 출력 (Presentation Layer)
data class User(
    val name: String,
    val age: Int
)
// Persistence Layer
class UserRepository {
    fun getUser(): User {
        // 실제로는 DB나 API에서 데이터를 가져오는 역할
        return User(name = "Alice", age = 20)
    }
}
// Business Layer
data class UserProfile(
    val name: String,
    val isAdult: Boolean
)

class UserService(
    private val userRepository: UserRepository // 하위계층인 Persistence Layer에 의존
) {
    fun getUserProfile(): UserProfile {
        val user = userRepository.getUser()
        val isAdult = user.age >= 18
        return UserProfile(name = user.name, isAdult = isAdult)
    }
}
// Presentation Layer
class UserController(
    private val userService: UserService // 하위계층인 Business Layer에 의존
) {
    fun showUserProfile() {
        val profile = userService.getUserProfile()
        val status = if (profile.isAdult) "an adult" else "a minor"
        println("User ${profile.name} is $status.")
    }
}
profile
그런게어딨어그냥하는거지

0개의 댓글