비지니스 로직과 객체지향 설계
비즈니스 로직이란?
비즈니스 로직Business Logic은 애플리케이션이 해결하려는 실제 문제와 관련된 규칙과 절차를 나타내는 코드를 의미합니다. 이는 단순히 데이터를 처리하거나 사용자 인터페이스UI를 제공하는 것이 아니라, 시스템이 처리해야 할 핵심 업무 규칙을 담고 있습니다.
비즈니스 로직의 역할
비즈니스 로직은 애플리케이션이 특정 도메인 문제를 해결하기 위해 정의된 작업 흐름과 규칙입니다. 이를 통해 애플리케이션이 어떤 방식으로 데이터를 처리하고, 결정을 내리며, 결과를 생성할지를 제어합니다. 예를 들어, 도서관 관리 시스템에서의 비즈니스 로직은 다음과 같은 규칙을 포함할 수 있습니다:
- 사용자가 대출할 수 있는 최대 도서 수는 5권이다.
- 연체된 사용자는 추가 도서를 대출할 수 없다.
- 도서가 예약된 경우, 다른 사용자는 대출할 수 없다. 이러한 규칙은 도서관의 운영 방식에 맞추어 설정된 것으로, 비즈니스 로직은 이를 기반으로 시스템이 어떻게 작동할지 결정합니다.
비즈니스 로직과 관련된 컴포넌트
비즈니스 로직은 일반적으로 다음과 같은 컴포넌트로 구성됩니다:
- 데이터 처리 로직: 시스템이 어떻게 데이터를 읽고, 변경하고, 저장할지를 정의합니다. 예를 들면, 사용자 요청에 따라 도서의 대출 상태를 변경하는 로직등이 있습니다.
- 결정 로직: 비즈니스 규칙에 따라 특정 상황에서 어떤 결정을 내려야 할지를 정의합니다. 사용자가 도서를 대출할 자격이 있는지 확인하는 로직등이 이에 해당됩니다.
- 업무 규칙과 흐름: 실제 비즈니스 도메인의 요구 사항을 반영한 규칙과 절차로, 데이터나 요청이 어떤 절차를 거쳐 처리되는지를 결정합니다.
비즈니스 로직 vs. 애플리케이션 로직
- 비즈니스 로직: 애플리케이션이 해결하는 업무 규칙과 흐름에 해당합니다. 이는 도메인 전문가나 비즈니스 이해 관계자들의 요구 사항을 바탕으로 정의됩니다. 대출 정책이나 할인 계산 방식 등이 이에 해당됩니다.
- 애플리케이션 로직: 사용자의 인터페이스 처리나 데이터베이스와의 상호작용 등 비즈니스 규칙을 구현하는 기술적 부분입니다. 예를 들어, 데이터를 어떻게 저장하고 검색할지, 화면에 어떤 방식으로 표시할지 등이 포함됩니다.
비즈니스 로직의 예시
도서관 관리 시스템을 예로 들면, 비즈니스 로직은 도서의 대출 규칙, 반납 기한, 연체 시 연체료 계산 방식 등을 정의합니다. 다음은 비즈니스 로직의 간단한 예시입니다:
public class LibraryService
{
private const int MaxBooksAllowed = 5;
public bool CanBorrow(User user)
{
// 사용자가 이미 대출한 책이 5권을 초과하면 대출 불가
if (user.BooksBorrowed >= MaxBooksAllowed)
{
return false;
}
// 연체된 도서가 있으면 대출 불가
if (user.HasOverdueBooks())
{
return false;
}
return true;
}
}
이 예시에서, CanBorrow
메서드는 사용자가 도서를 대출할 수 있는지 여부를 결정하는 비즈니스 로직을 포함하고 있습니다.
객체지향과 비즈니스 로직
객체지향 프로그래밍OOP에서 비즈니스 로직은 주로 도메인 모델을 통해 구현됩니다. 도메인 모델은 비즈니스 개념과 규칙을 반영한 클래스를 정의하여 시스템의 요구 사항을 충족시키는 핵심입니다. 객체지향 설계를 통해 비즈니스 로직을 구현할 때는 다음의 객체지향 원칙들이 적용됩니다:
4대 원칙과 비지니스 로직직
캡슐화
캡슐화Encapsulation는 데이터와 그 데이터를 처리하는 메서드를 하나의 객체 안에 묶어 외부에서 직접 접근할 수 없도록 하는 원칙입니다. 비즈니스 로직을 캡슐화하면, 시스템의 각 부분이 서로 독립적으로 동작할 수 있으며, 특정 클래스의 내부 구현을 변경해도 외부에 영향을 주지 않게 됩니다.
예를 들어, 도서 대출 비즈니스 로직이 LibraryService
클래스에 캡슐화되어 있으면, 대출 방식이 바뀌더라도 외부에서 이 변경 사항을 알 필요 없이 서비스를 사용할 수 있습니다.
public class User
{
private List<Book> borrowedBooks = new List<Book>();
public void BorrowBook(Book book)
{
borrowedBooks.Add(book);
}
public IReadOnlyList<Book> GetBorrowedBooks()
{
return borrowedBooks.AsReadOnly();
}
}
User
클래스는 대출된 도서 목록을 관리하지만, 외부에서는 이 목록에 직접 접근할 수 없고, GetBorrowedBooks
메서드를 통해 읽기 전용으로 접근할 수 있습니다. 이렇게 내부 데이터를 보호하고, 비즈니스 로직을 캡슐화하여 유지보수성을 높입니다.
상속
상속Inheritance을 통해 비즈니스 로직을 계층 구조로 관리할 수 있으며, 기존 클래스의 기능을 확장하여 새로운 비즈니스 요구 사항을 반영할 수 있습니다. 상속을 사용하면 중복된 코드 없이 다양한 비즈니스 규칙을 처리하는 다양한 클래스를 만들 수 있습니다. 예를 들어, 일반 사용자와 프리미엄 사용자의 대출 규칙이 다를 때, 상속을 사용하여 기본 대출 로직을 재사용하면서, 각 사용자 유형에 따라 대출 규칙을 다르게 설정할 수 있습니다.
public class User
{
public virtual int MaxBooksAllowed => 5;
}
public class PremiumUser : User
{
public override int MaxBooksAllowed => 10;
}
PremiumUser
클래스는 기본 User
클래스의 대출 규칙을 확장하여, 프리미엄 사용자는 최대 10권의 책을 대출할 수 있도록 설정합니다.
다형성
다형성Polymorphism은 동일한 인터페이스를 사용하여 서로 다른 클래스의 객체를 일관되게 처리할 수 있게 합니다. 이를 통해 비즈니스 로직이 특정 구현에 의존하지 않고, 확장성 있는 방식으로 설계될 수 있습니다.
도서관 관리 시스템에서 User
클래스를 상속하는 다양한 사용자 유형이 존재할 수 있으며, 각 사용자의 대출 규칙이 다를 때 다형성을 사용하여 동일한 CanBorrow()
메서드를 호출하더라도 각 사용자의 규칙에 따라 적절한 처리가 이루어지게 할 수 있습니다.
public bool CanBorrow(User user)
{
return user.BooksBorrowed < user.MaxBooksAllowed;
}
이 메서드는 User
객체가 PremiumUser
이든 User
이든 상관없이 각자의 대출 규칙에 따라 동작합니다.
추상화
추상화Abstraction는 복잡한 비즈니스 로직을 간단한 인터페이스로 표현하여, 세부 구현을 숨기고 클라이언트가 비즈니스 로직을 쉽게 사용할 수 있도록 합니다. 이를 통해 시스템의 복잡성을 줄이고, 다양한 비즈니스 로직을 일관되게 처리할 수 있습니다.
예를 들어, 대출 규칙을 추상화하여 IBorrowRule
인터페이스로 정의하고, 구체적인 규칙들은 각 클래스에서 구현할 수 있습니다.
public interface IBorrowRule
{
bool CanBorrow(User user);
}
public class RegularBorrowRule : IBorrowRule
{
public bool CanBorrow(User user)
{
return user.BooksBorrowed < 5;
}
}
public class PremiumBorrowRule : IBorrowRule
{
public bool CanBorrow(User user)
{
return user.BooksBorrowed < 10;
}
}
이러한 추상화는 비즈니스 로직을 유연하게 확장할 수 있는 기반을 제공합니다.
SOLID 원칙과 비즈니스 로직
객체지향 설계에서 비즈니스 로직을 구현할 때 SOLID 원칙을 적용하면 코드의 유지보수성과 확장성이 높아집니다. 비즈니스 로직에 SOLID 원칙을 적용하는 방식은 다음과 같습니다:
- 단일 책임 원칙SRP: 각 클래스는 하나의 책임만 가지며, 비즈니스 로직이 한 클래스에 집중되지 않도록 역할을 분리합니다. 예를 들어, 대출과 반납 로직을 각각의 서비스로 분리하여 구현할 수 있습니다.
- 개방_폐쇄 원칙OCP: 비즈니스 로직은 확장에 열려 있으면서도 수정에는 닫혀 있어야 합니다. 새로운 대출 규칙을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계합니다.
- 리스코프 치환 원칙LSP:
User
클래스를 상속하는PremiumUser
클래스가 동일한 방식으로 대출 규칙을 적용받도록 보장하여, 상속 구조의 일관성을 유지합니다. - 인터페이스 분리 원칙ISP: 비즈니스 로직에서 필요한 기능만 제공하는 인터페이스를 정의하여, 클라이언트가 불필요한 기능에 의존하지 않도록 합니다.
- 의존 역전 원칙DIP: 비즈니스 로직이 구체적인 구현이 아닌 인터페이스에 의존하도록 하여, 비즈니스 규칙의 변경이 쉽게 적용될 수 있도록 만듭니다.
맺음말
객체지향 설계 원칙을 적용한 비즈니스 로직은 코드의 재사용성과 확장성을 크게 향상시킵니다. 이를 통해 비즈니스 규칙이 변화할 때에도 최소한의 수정으로 시스템을 유연하게 유지할 수 있으며, 캡슐화, 상속, 다형성, 추상화를 통해 비즈니스 도메인의 복잡한 요구 사항을 효과적으로 처리할 수 있습니다.
심화 학습
비즈니스 로직의 복잡성 관리
실제 비즈니스 시스템에서는 다양한 규칙과 예외 사항이 존재하며, 시간이 지남에 따라 비즈니스 로직은 복잡해질 수 있습니다. 이를 관리하기 위해서는 다양한 설계 패턴과 방법론을 활용해 비즈니스 로직을 모듈화하고, 이를 쉽게 유지보수할 수 있어야 합니다.
규칙 엔진 사용
규칙 엔진Rule Engine은 비즈니스 규칙을 코드와 분리하여, 비즈니스 규칙을 관리하는 별도의 엔진으로 처리하는 방식입니다. 이를 통해 비즈니스 규칙을 변경해야 할 때 애플리케이션 코드를 수정하지 않고도 쉽게 규칙을 관리하고 유지보수할 수 있습니다. 예를 들어, 도서 대출 시스템에서 대출 규칙을 쉽게 추가하거나 변경할 수 있도록 규칙 엔진을 사용하면 다음과 같은 구조로 구현할 수 있습니다.
public class BorrowRuleEngine
{
private readonly List<IBorrowRule> _rules;
public BorrowRuleEngine(List<IBorrowRule> rules) => _rules = rules;
public bool CanBorrow(User user, Book book)
=> _rules.All(rule => rule.CanBorrow(user, book));
}
BorrowRuleEngine
은 다양한 대출 규칙(IBorrowRule
)을 사용하여 대출 가능 여부를 판단합니다.- 이를 통해 새로운 대출 규칙이 추가되더라도 기존 코드를 수정할 필요 없이, 규칙 엔진에 새로운 규칙을 추가하기만 하면 됩니다.
도메인 이벤트와 이벤트 주도 설계Event-Driven Design
도메인 이벤트는 시스템 내에서 중요한 상태 변화가 발생했을 때 이를 이벤트로 표현하는 방식입니다. 비즈니스 로직이 복잡해질수록 도메인 이벤트를 통해 비즈니스 규칙을 명확하게 표현하고, 다양한 컴포넌트가 해당 이벤트에 반응하도록 설계할 수 있습니다.
public class BookBorrowedEvent
{
public int BookId { get; }
public int UserId { get; }
public DateTime BorrowedAt { get; }
public BookBorrowedEvent(int bookId, int userId, DateTime borrowedAt)
{
BookId = bookId;
UserId = userId;
BorrowedAt = borrowedAt;
}
}
public class BorrowService
{
private readonly IEventDispatcher _eventDispatcher;
public BorrowService(IEventDispatcher eventDispatcher)
=> _eventDispatcher = eventDispatcher;
public void BorrowBook(Book book, User user)
{
book.Borrow();
var bookBorrowedEvent = new BookBorrowedEvent(book.Id, user.Id, DateTime.UtcNow);
_eventDispatcher.Dispatch(bookBorrowedEvent);
}
}
BookBorrowedEvent
는 도서가 대출되었을 때 발생하는 도메인 이벤트입니다.- 이를 통해 이벤트를 발행하고, 다른 시스템이나 서비스가 이 이벤트에 따라 적절한 로직을 실행할 수 있게 합니다.
비즈니스 로직 최적화
비즈니스 로직이 복잡해질수록 성능 최적화도 중요해집니다. 특히 대규모 시스템에서는 비즈니스 로직의 효율성을 높여 성능 저하를 방지해야 합니다.
CQRS와 비즈니스 로직
CQRSCommand Query Responsibility Segregation 패턴은 비즈니스 로직에서 명령Command과 조회Query를 분리하여 각각의 로직에 맞는 최적의 방식을 사용할 수 있게 합니다. 이를 통해 비즈니스 로직이 복잡한 시스템에서도 성능을 최적화할 수 있습니다.
- Command: 시스템 상태를 변경하는 작업 (예: 도서 대출).
- Query: 시스템 상태를 읽는 작업 (예: 도서 정보 조회). CQRS를 적용하면 명령과 조회 작업을 독립적으로 최적화할 수 있어, 성능 요구사항에 맞게 비즈니스 로직을 분리하여 처리할 수 있습니다.
// Command: 도서 대출
public class BorrowBookCommand
{
public int BookId { get; }
public int UserId { get; }
public BorrowBookCommand(int bookId, int userId)
{
BookId = bookId;
UserId = userId;
}
}
// Query: 도서 상태 조회
public class BookQueryService
{
private readonly IBookReadRepository _readRepository;
public BookQueryService(IBookReadRepository readRepository)
=> _readRepository = readRepository;
public BookDto GetBookById(int bookId)
=> _readRepository.GetBookById(bookId);
}
- 명령(
BorrowBookCommand
)과 조회(BookQueryService
)를 분리하여 각각의 성격에 맞는 최적화를 수행할 수 있습니다. - 명령과 조회는 서로 다른 데이터 저장소를 사용할 수도 있으며, 조회는 캐싱 또는 읽기 전용 데이터베이스를 사용할 수 있습니다.
마이크로서비스와 비즈니스 로직의 분리
비즈니스 로직을 마이크로서비스 아키텍처에 맞춰 분리하여 관리할 때는, 각 서비스가 독립적으로 동작하고 서로 간의 의존성을 최소화하는 것이 중요합니다. 비즈니스 로직이 각 마이크로서비스에 맞춰 분리되면, 각 서비스가 독립적으로 확장 가능하며 유지보수가 용이해집니다. 예를 들어, 도서 대출 시스템에서 도서 관련 비즈니스 로직은 “도서 서비스”, 사용자 관련 비즈니스 로직은 “사용자 서비스"로 분리할 수 있습니다. 이때 서비스 간의 통신은 도메인 이벤트를 통해 이루어집니다.