계층형 아키텍처
계층형 아키텍처란?
계층형 아키텍처Layered Architecture는 소프트웨어 시스템을 여러 역할과 책임에 따라 독립적인 계층으로 나누어 설계하는 방식입니다. 이를 통해 각 계층 간의 결합도를 낮추고 시스템의 복잡성을 줄이며, 유지보수성과 확장성을 높일 수 있습니다. 특히 대규모 시스템에서 구조적인 설계를 위해 자주 사용되는 패턴입니다.
계층형 아키텍처의 목적
계층형 아키텍처의 주요 목표는 각 계층이 명확한 역할을 수행하도록 하여, 각 계층이 독립적으로 변경될 수 있게 하여 시스템의 유지보수성과 유연성을 높이는 것입니다. 계층 간 상호작용을 최소화함으로써 안정적인 시스템 확장이 가능합니다.
계층 구조의 기본 개념
계층형 아키텍처는 일반적으로 세 가지 주요 계층으로 나뉩니다:
프레젠테이션 계층
사용자와 상호작용하며 UI를 처리하는 계층Presentation Layer입니다. 사용자의 입력을 받아 비즈니스 계층으로 전달하고, 처리된 결과를 사용자에게 반환합니다.
비즈니스 계층
애플리케이션의 핵심 로직을 처리하는 계층Business Layer으로, 비즈니스 규칙을 적용하고 데이터를 처리합니다. 이 계층은 프레젠테이션 계층과 데이터 접근 계층 사이에서 중개 역할을 합니다.
데이터 접근 계층
데이터베이스와 상호작용하여 데이터를 관리하는 계층Data Access Layer으로, 데이터를 저장, 조회, 수정, 삭제하는 작업을 담당합니다.
실무에서의 계층형 아키텍처
폴더 및 네임스페이스 구조
실무에서는 폴더와 네임스페이스 구조를 명확히 구분하여 유지보수성을 높입니다. 이는 각 계층별로 폴더와 네임스페이스를 적절히 설계해 모듈 간 결합도를 줄이고 독립성을 유지하는 데 중요한 역할을 합니다.
폴더 구조 예시
/MyProject
│
├── /Web
│ └── Controllers, Views
│
├── /Application
│ └── Services, DTOs, Interfaces
│
├── /Domain
│ └── Entities, ValueObjects
│
└── /Infrastructure
└── Repositories, Data
- Web: 사용자 인터페이스와 관련된 코드를 포함하며, Controllers는 요청 처리, Views는 데이터를 표시합니다.
- Application: 비즈니스 로직과 데이터 전송 객체DTO를 처리하는 계층입니다.
- Domain: 핵심 도메인 로직과 비즈니스 규칙을 정의합니다.
- Infrastructure: 데이터베이스 및 외부 시스템과의 통신을 담당하는 계층입니다.
네임스페이스 설계 예시
namespace MyProject.Web.Controllers
{
public class BookController { }
}
namespace MyProject.Application.Services
{
public class BookService { }
}
namespace MyProject.Domain.Entities
{
public class Book { }
}
namespace MyProject.Infrastructure.Repositories
{
public class BookRepository : IBookRepository { }
}
이와 같은 구조는 각 계층의 역할을 명확히 구분하며, 모듈 간 결합도를 최소화합니다.
실무에서의 네임스페이스 설계 원칙
- 도메인 중심 설계: 네임스페이스는 각 기능 또는 도메인에 맞게 명명되어 모듈의 역할을 직관적으로 파악할 수 있어야 합니다.
- 폴더 구조와 네임스페이스의 일관성: 폴더 구조와 네임스페이스는 일관성을 유지해야 하며, 폴더 구조가 변경되면 네임스페이스도 함께 변경됩니다.
- 결합도 최소화: 네임스페이스 간 의존성을 최소화하여, 모듈 간의 결합도를 줄이고 시스템의 유연성을 높입니다.
- 재사용성 고려: 네임스페이스는 각 계층에서 독립적으로 재사용 가능한 코드를 포함해야 하며, 다른 계층에 의존하지 않는 모듈을 설계하는 것이 중요합니다.
계층 간 의존성 관리
의존성 주입을 통한 결합도 최소화
계층 간 결합도를 줄이기 위해 의존성 주입Dependency Injection을 활용하여 구체적인 구현에 의존하지 않고, 추상화된 인터페이스를 통해 의존성을 관리할 수 있습니다.
// 비즈니스 계층 인터페이스
public interface IBookService
{
void AddBook(Book book);
Book GetBookById(int id);
}
// 데이터 접근 계층 인터페이스
public interface IBookRepository
{
Book FindById(int id);
void Save(Book book);
}
// 비즈니스 계층 구현
public class BookService : IBookService
{
private readonly IBookRepository _bookRepository;
public BookService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public void AddBook(Book book)
{
_bookRepository.Save(book);
}
public Book GetBookById(int id)
{
return _bookRepository.FindById(id);
}
}
위 예시에서 비즈니스 로직과 데이터 접근 로직은 인터페이스로 분리되어 있어, 구체적인 구현에 의존하지 않으며 계층 간 독립성을 보장합니다.
계층형 아키텍처의 장점
- 유지보수성: 각 계층이 독립적으로 관리되므로, 특정 계층을 수정해도 다른 계층에 영향을 미치지 않아 유지보수가 용이합니다.
- 확장성: 새로운 기능을 추가할 때 각 계층별로 필요한 부분만 확장할 수 있어 시스템 확장이 용이합니다.
- 재사용성: 계층 간 모듈이 독립적으로 재사용 가능하며, 코드 중복을 줄일 수 있습니다.
계층형 아키텍처의 단점
- 복잡성 증가: 계층화된 구조는 초기 설계가 복잡해질 수 있으며, 과도한 계층 분리는 불필요한 복잡성을 초래할 수 있습니다.
- 성능 문제: 각 계층 간의 데이터 전달이 반복되면서 성능 저하가 발생할 수 있으며, 이를 해결하기 위한 최적화가 필요할 수 있습니다.
닷넷에서의 계층화
MVC 패턴과 계층화 설계
ASP.NET의 Model-View-ControllerMVC 패턴은 프레젠테이션 계층과 비즈니스 로직, 데이터 접근을 명확하게 분리하여 계층형 아키텍처를 자연스럽게 지원합니다.
MVVM 패턴과 계층화 설계
WPF의 Model-View-ViewModelMVVM 패턴은 프레젠테이션 로직과 비즈니스 로직을 분리하여 계층화를 지원합니다. 이는 프레젠테이션 레이어와 비즈니스 로직을 독립적으로 관리할 수 있도록 돕습니다.
프레젠테이션 레이어 분리 문제
.NET Framework에서 WinForms 같은 전통적인 애플리케이션은 프레젠테이션과 비즈니스 로직이 밀접하게 연결되어 있어, 분리가 어려울 수 있습니다. 하지만 이 경우에도 의존성 주입을 활용하거나, 서비스 계층을 분리하여 설계를 개선할 수 있습니다.
맺음말
계층형 아키텍처는 각 계층의 책임을 명확히 하여 시스템을 유지보수하기 쉽게 하며, 확장성과 재사용성을 높이는 데 필수적인 설계 방식입니다. Visual Studio와 같은 도구에서 폴더 구조와 네임스페이스를 잘 설계하여, 계층 간 결합도를 최소화하고 유지보수성을 극대화할 수 있습니다.
심화 학습
비즈니스 요구에 따른 계층 확장
일반적인 계층형 아키텍처에서는 프레젠테이션 계층Presentation Layer, 비즈니스 계층Business Logic Layer, 데이터 접근 계층Data Access Layer 등으로 시스템을 나누어 설계합니다. 하지만 실무에서는 이 기본 구조에서 추가적인 계층이 필요할 수 있습니다.
서비스 계층 추가
복잡한 시스템에서는 비즈니스 로직을 더욱 분리하기 위해 서비스 계층을 도입하는 경우가 많습니다. 서비스 계층은 비즈니스 로직을 캡슐화하여, 프레젠테이션 계층과 비즈니스 로직 간의 강한 결합을 방지합니다.
public class BorrowService
{
private readonly IBookRepository _bookRepository;
public BorrowService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public void BorrowBook(int bookId, int userId)
{
var book = _bookRepository.FindById(bookId);
if (book.IsAvailable)
{
book.Borrow();
_bookRepository.Save(book);
}
}
}
BorrowService
는 비즈니스 계층의 로직을 캡슐화하여, 프레젠테이션 계층과의 결합을 줄입니다.- 서비스 계층은 특히 대규모 시스템에서 코드의 재사용성을 높이고, 변경에 유연하게 대응할 수 있도록 설계됩니다.
데이터 전송 객체의 활용
계층 간 데이터를 주고받을 때 데이터 전송 객체Data Transfer Object, DTO를 사용하면, 각 계층에서 필요한 데이터를 명확히 정의하고 전달할 수 있습니다. DTO는 계층 간 데이터 전달을 단순화하고, 계층 간의 결합을 낮추는 역할을 합니다.
public class BookDto
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsBorrowed { get; set; }
}
public class BookService
{
private readonly IBookRepository _bookRepository;
public BookService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public BookDto GetBookById(int id)
{
var book = _bookRepository.FindById(id);
return new BookDto
{
Id = book.Id,
Title = book.Title,
IsBorrowed = book.IsBorrowed
};
}
}
BookDto
는 데이터베이스에서 가져온 책 객체를 프레젠테이션 계층으로 전달할 때 사용됩니다.- 이를 통해 도메인 객체(
Book
)와 프레젠테이션 계층 간의 불필요한 결합을 줄이고, 필요한 데이터만 전달할 수 있습니다.
단일 책임 원칙과 계층 분리
심화 학습에서는 단일 책임 원칙SRP을 지키면서 계층을 분리하는 것이 중요합니다. 각 계층은 명확한 책임을 가지도록 설계되어야 하며, 한 계층이 너무 많은 책임을 지게 되면 유지보수가 어려워질 수 있습니다.
public class BookController
{
private readonly IBookService _bookService;
public BookController(IBookService bookService)
{
_bookService = bookService;
}
public IActionResult GetBook(int id)
{
var bookDto = _bookService.GetBookById(id);
return View(bookDto);
}
}
BookController
는 사용자 요청을 처리하고, 데이터에 대한 세부 로직은BookService
로 위임합니다.- 프레젠테이션 계층의 컨트롤러는 비즈니스 로직을 직접 처리하지 않으며, 서비스 계층에 책임을 위임함으로써 단일 책임 원칙을 준수합니다.
계층형 아키텍처의 한계와 대안
계층형 아키텍처는 시스템을 명확하게 분리할 수 있다는 장점이 있지만, 지나치게 복잡해질 경우 성능 저하 및 유지보수 어려움이 발생할 수 있습니다. 이러한 한계를 극복하기 위해 CQRS, 헥사고날 아키텍처 등의 대안적인 패턴이 종종 도입됩니다.
CQRS와 계층형 아키텍처의 조합
CQRSCommand Query Responsibility Segregation는 쓰기 작업과 읽기 작업을 분리하는 패턴으로, 계층형 아키텍처에서 비즈니스 로직을 확장할 때 유용하게 사용할 수 있습니다.
public class BorrowBookCommand
{
public int BookId { get; set; }
public int UserId { get; set; }
}
public class BorrowBookCommandHandler
{
private readonly IBookRepository _bookRepository;
public BorrowBookCommandHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public void Handle(BorrowBookCommand command)
{
var book = _bookRepository.FindById(command.BookId);
book.Borrow();
_bookRepository.Save(book);
}
}
BorrowBookCommand
는 도서를 대출하는 쓰기 작업을 처리하고,BorrowBookCommandHandler
는 해당 명령을 처리합니다.- 이러한 구조를 통해 쓰기와 읽기 작업을 분리하고, 계층별로 책임을 더욱 명확하게 할 수 있습니다.
계층형 아키텍처의 최적화
계층형 아키텍처에서 성능을 최적화하는 방법도 중요합니다. 많은 계층이 있을 경우 각 계층을 거치는 동안 발생하는 성능 문제를 관리하는 방법을 학습하는 것이 필요합니다.
캐싱을 활용한 최적화
캐싱을 사용하면 데이터베이스에 직접 접근하는 비용을 줄이고, 프레젠테이션 계층의 응답 속도를 개선할 수 있습니다.
public class CachedBookService : IBookService
{
private readonly IBookService _inner;
private readonly IMemoryCache _cache;
public CachedBookService(IBookService inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public BookDto GetBookById(int id)
{
return _cache.GetOrCreate(id, entry => _inner.GetBookById(id));
}
}
CachedBookService
는 기존의BookService
에 캐싱 로직을 추가하여, 동일한 데이터에 대해 반복적인 데이터베이스 접근을 줄입니다.- 이를 통해 성능 최적화가 가능하며, 계층형 아키텍처에서 발생할 수 있는 성능 문제를 해결할 수 있습니다.