객체는 다른 객체와 상호작용하면서 자신의 역할(Role
)을 수행합니다. 객체는 자신이 해야 할 일을 다른 객체에게 요청하거나, 다른 객체로부터 받은 요청을 처리하며, 이를 위해 메시지(Message
)를 주고받습니다. 객체는 메시지를 주고받으면서 다른 객체와 협력하여 작업을 수행합니다.
예를 들어, 은행 시스템에서 계좌(Account
) 객체와 고객(Customer
) 객체가 상호작용하는 과정을 살펴보겠습니다.
이 예시에서 계좌 객체는 다음과 같은 메서드를 가지고 있을 수 있습니다.
class Account {
private balance: number;
private transactions: Transaction[];
constructor(initialBalance: number) {
this.balance = initialBalance;
this.transactions = [];
}
public deposit(amount: number): void {
this.balance += amount;
this.transactions.push(new Transaction(amount, "Deposit"));
}
public withdraw(amount: number): boolean {
if (this.balance >= amount) {
this.balance -= amount;
this.transactions.push(new Transaction(amount, "Withdrawal"));
return true;
} else {
return false;
}
}
public getBalance(): number {
return this.balance;
}
}
고객 객체는 계좌 객체를 이용하여 입금이나 출금을 할 수 있습니다. 고객 객체는 다음과 같은 메서드를 가지고 있을 수 있습니다.
class Customer {
private name: string;
private account: Account;
constructor(name: string, account: Account) {
this.name = name;
this.account = account;
}
public deposit(amount: number): void {
this.account.deposit(amount);
}
public withdraw(amount: number): boolean {
return this.account.withdraw(amount);
}
public getBalance(): number {
return this.account.getBalance();
}
}
위 예시에서 Account
클래스는 계좌 객체를 생성하며, deposit
, withdraw
, getBalance
메서드를 가지고 있습니다. Customer
클래스는 고객 객체를 생성하며, deposit
, withdraw
, getBalance
메서드를 사용하여 계좌 객체의 동작을 수행합니다. 두 객체는 서로 다른 역할을 수행하며, 상호작용을 통해 입출금 서비스를 제공합니다.
객체간의 Collaboration을 구현할 때 저지르기 쉬운 실수 중 하나는 객체 간의 결합도를 높이는 것입니다.
결합도는 객체 간의 의존성을 나타내며, 결합도가 높을수록 객체 간의 관계가 강해집니다. 이러한 강한 결합 관계는 객체 지향 프로그래밍의 장점인 모듈성(Modularity)을 해치며, 코드 유지보수성을 떨어뜨리는 결과를 가져올 수 있습니다.
객체 간의 결합도를 낮추기 위해서는 객체 간의 관계를 느슨하게 만들어야 합니다. 이를 위해서는 다음과 같은 방법을 고려할 수 있습니다.
인터페이스
를 사용하여 객체 간의 상호작용을 정의합니다.추상화
를 사용하여 객체의 구현 세부사항을 숨깁니다.의존성 주입(Dependency Injection)
을 사용하여 객체 간의 결합도를 낮춥니다.이벤트 기반 아키텍처(Event-Driven Architecture)
를 사용하여 객체 간의 비동기적인 상호작용을 처리합니다.또한 객체 간의 협업을 구현할 때 다른 실수로는 객체의 책임(Responsibility)을 모호하게 만드는 것이 있습니다.
객체의 책임이 모호하다는 개념은 객체가 맡은 역할이 불명확하거나, 하나의 객체가 여러 가지 역할을 수행하도록 구현되어 있어 유지보수가 어려워지는 경우를 말합니다.
객체는 특정한 책임을 가지고 있어야 하며, 이러한 책임을 분명하게 정의하지 않으면 코드의 복잡도가 높아지고, 객체 지향의 장점을 활용할 수 없습니다.
따라서 객체의 책임을 명확하게 정의하고, 이를 기반으로 객체 간의 협업을 구현하는 것이 중요합니다.
class UserRepository {
private db: Database;
constructor(db: Database) {
this.db = db;
}
public getUsers(): User[] {
return this.db.query("SELECT * FROM users");
}
}
class UserService {
private repository: UserRepository;
constructor() {
this.repository = new UserRepository(new Database());
}
public getUsers(): User[] {
return this.repository.getUsers();
}
}
위 코드에서 UserService
클래스는 UserRepository
클래스를 사용하여 데이터베이스에서 사용자 정보를 가져오고 있습니다. 이 때 UserService
클래스는 UserRepository
클래스에 강하게 결합되어 있으며, UserRepository
클래스의 변경이 UserService
클래스에 영향을 미칠 가능성이 있습니다.
interface IUserRepository {
getUsers(): User[];
}
class UserRepository implements IUserRepository {
private db: Database;
constructor(db: Database) {
this.db = db;
}
public getUsers(): User[] {
return this.db.query("SELECT * FROM users");
}
}
class UserService {
private repository: IUserRepository;
constructor(repository: IUserRepository) {
this.repository = repository;
}
public getUsers(): User[] {
return this.repository.getUsers();
}
}
위 코드에서는 IUserRepository
인터페이스를 정의하고, UserRepository
클래스가 이를 구현하도록 하였습니다. UserService
클래스는 이제 IUserRepository
인터페이스를 사용하며, UserRepository
클래스 뿐만 아니라 다른 클래스도 IUserRepository
인터페이스를 구현하면 사용할 수 있습니다. 이를 통해 UserService
클래스는 더 이상 UserRepository
클래스에 의존하지 않으며, 결합도가 낮아지게 됩니다.
위 예시에서는 인터페이스를 사용하여 객체 간의 결합도를 낮추었습니다. 인터페이스를 사용하는 방법 이외에도, 다른 방법들을 사용하여 객체 간의 결합도를 낮출 수 있습니다.
class Customer {
private name: string;
private address: string;
private cart: Cart;
constructor(name: string, address: string) {
this.name = name;
this.address = address;
this.cart = new Cart();
}
public addToCart(item: Item): void {
this.cart.addItem(item);
}
public checkout(): void {
this.cart.checkout(this.name, this.address);
}
}
class Cart {
private items: Item[];
constructor() {
this.items = [];
}
public addItem(item: Item): void {
this.items.push(item);
}
public checkout(name: string, address: string): void {
// 주문 정보를 처리하는 로직
}
}
class Item {
private name: string;
private price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
위 코드에서 Customer
클래스는 Cart
클래스를 사용하여 장바구니에 상품을 추가하고, 결제를 처리합니다. 이 때 Customer
클래스는 Cart
클래스에 대해 명확한 책임을 갖고 있지 않으며, Cart
클래스의 세부사항을 알아야 합니다. 또한 Cart
클래스는 결제를 처리하는 메서드를 가지고 있어, Cart
클래스의 책임 역시 모호합니다.
class Customer {
private name: string;
private address: string;
private cart: Cart;
constructor(name: string, address: string) {
this.name = name;
this.address = address;
this.cart = new Cart();
}
public addToCart(item: Item): void {
this.cart.addItem(item);
}
public checkout(paymentService: PaymentService): void {
const totalPrice = this.cart.getTotalPrice();
paymentService.pay(this.name, this.address, totalPrice);
}
}
class Cart {
private items: Item[];
constructor() {
this.items = [];
}
public addItem(item: Item): void {
this.items.push(item);
}
public getTotalPrice(): number {
return this.items.reduce((total, item) => total + item.getPrice(), 0);
}
}
class Item {
private name: string;
private price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
public getPrice(): number {
return this.price;
}
}
interface PaymentService {
pay(name: string, address: string, amount: number): void;
}
class CreditCardPaymentService implements PaymentService {
public pay(name: string, address: string, amount: number): void {
// 신용카드 결제를 처리하는 로직
}
}
class PayPalPaymentService implements PaymentService {
public pay(name: string, address: string, amount: number): void {
// PayPal 결제를 처리하는 로직
}
}
위 코드에서는 Customer
클래스가 Cart
클래스와 PaymentService
인터페이스에 대해 명확한 책임을 가지고 있습니다.
Cart
클래스는 상품을 관리하는 역할만 수행하고, PaymentService
인터페이스는 결제를 처리하는 역할만 수행합니다.
이렇게 객체의 책임을 명확하게 정의함으로써, 코드의 복잡도를 줄일 수 있으며 유지보수가 용이해집니다.
class Order {
private items: Item[];
private total: number;
constructor() {
this.items = [];
this.total = 0;
}
public addItem(item: Item): void {
this.items.push(item);
this.total += item.getPrice();
}
public getTotal(): number {
return this.total;
}
public sendEmailConfirmation(): void {
// 이메일 발송 로직
}
}
class Item {
private name: string;
private price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
public getPrice(): number {
return this.price;
}
}
위 코드에서 Order
클래스는 Item
클래스를 사용하여 주문 정보를 관리하고, 이메일을 발송합니다.
이 때 Order
클래스는 주문 정보를 관리하는 역할과 이메일 발송을 처리하는 역할을 모두 담당하고 있으며, 책임이 모호합니다.
또한 이메일 발송 로직이 Order
클래스에 직접 구현되어 있어, Order
클래스의 변경이 이메일 발송 로직에 영향을 미칠 가능성이 있습니다.
class Order {
private items: Item[];
private total: number;
private emailService: EmailService;
constructor(emailService: EmailService) {
this.items = [];
this.total = 0;
this.emailService = emailService;
}
public addItem(item: Item): void {
this.items.push(item);
this.total += item.getPrice();
}
public getTotal(): number {
return this.total;
}
public sendConfirmationEmail(): void {
const email = this.createEmail();
this.emailService.send(email);
}
private createEmail(): Email {
// 이메일 생성 로직
return new Email();
}
}
class Item {
private name: string;
private price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
public getPrice(): number {
return this.price;
}
}
class Email {
// 이메일 관련 정보
}
interface EmailService {
send(email: Email): void;
}
class SmtpEmailService implements EmailService {
public send(email: Email): void {
// SMTP 프로토콜을 사용하여 이메일을 전송하는 로직
}
}
class RestEmailService implements EmailService {
public send(email: Email): void {
// REST API를 사용하여 이메일을 전송하는 로직
}
}
위 코드에서는 Order
클래스가 EmailService
인터페이스를 사용하여 이메일을 전송하는 역할만 수행하도록 구현되어 있습니다.
이렇게 객체의 책임을 명확하게 정의함으로써, 코드의 복잡도를 줄일 수 있으며 유지보수가 용이해집니다.
또한, EmailService
인터페이스를 구현한 클래스를 전달받아 이메일을 전송하도록 구현함으로써, Order
클래스와 이메일 발송 로직이 독립적으로 변경될 수 있도록 구현되었습니다.