절차지향과 객체지향
절차지향 프로그래밍 - Procedural Programming
흔히 객체(클래스)를 사용해 프로그래밍하는 것을 객체지향이라고 한다. 그렇기에 클래스를 사용하는 언어(자바 등)를 객체지향 언어라고 하고 C와 같은 언어를 절차지향 언어라고 한다. 그렇다면 객체를 사용하여 프로그래밍하면 객체지향이고 절차지향 프로그래밍이 아닐까? 그렇지않다. 아래 코드를 보자
class Invitation(val invitationDate: LocalDateTime)
class Ticket(val fee: Long)
class Bag(var amount: Long, val invitation: Invitation? = null, var ticket: Ticket? = null) {
fun hasInvitation(): Boolean = invitation != null
fun hasTicket(): Boolean = ticket != null
fun plusAmount(amount: Long) = this.amount + amount
fun minusAmount(amount: Long) = this.amount - amount
}
class Audience(val bag: Bag)
class TicketOffice(val amount: Long, val tickets: MutableList<Ticket>) {
fun getTicket(): Ticket = tickets.removeAt(0)
fun plusAmount(amount: Long) = this.amount + amount
fun minusAmount(amount: Long) = this.amount - amount
}
class TicketSeller(val ticketOffice: TicketOffice)
class Theater(val ticketSeller: TicketSeller) {
fun enter(audience: Audience) {
val ticket = ticketSeller.ticketOffice.getTicket()
if (!audience.bag.hasInvitation()) {
audience.bag.minusAmount(ticket.fee)
ticketSeller.ticketOffice.plusAmount(ticket.fee)
}
audience.bag.ticket = ticket
}
}
위 코드는 소극장이 티켓을 판매하고 관객을 입장시키는 역할을 하는 코드다. 특이 사항이라면 이벤트로 초대장을 가져온 관객은 티켓 판매소에서 초대장을 무료로 티켓을 바꿔준다는 점이다. 클래스를 사용해 프로그래밍을 했지만 위 코드는 절차지향이다. 클래스는 단지 데이터를 담고 있을 뿐 프로그램의 모든 프로세스는 enter()
메소드에서 수행된다. 이렇듯 프로세스와 데이터를 별도의 모듈(클래스)에 위치시키는 것이 절지향이다
절차지향의 문제는 변경의 취약하다는 점이다. 프로세스를 수행하는 Theater
클래스는 언뜻보면 TicketSeller
에만 의존하는 것처럼 보이지만 실제로는 거의 모든 클래스에 의존한다. 전체적으로 봤을 때 Theater -> TicketSeller -> TicketOffice
, Theater -> Audience -> Bag
의 계층적 의존관계를 가진다. 직접적으로 의존하는 TicketSeller, Audience
가 변경된다면 필연적으로 enter
메소드도 변경되어야 한다. 그렇기 때문에 절차지향은 유지보수가 어렵고 얽혀있는 의존성 때문에 수많은 버그가 발생하는 원인이 된다. 이렇게 객체 사이의 의존성이 과하게 결합되어 있는 경우 결합도(coupling)가 높다고 한다
객체지향 프로그래밍
객체지향의 목적은 결합도를 낮춰 변경이 용이한 구조를 가지는 것이다. 위 코드의 문제는 Theater
클래스가 Audience, TicketSeller
클래스를 통해 다른 클래스에 직접적으로 접근해 결합도가 높아진다는 것이다. 이를 해결하기 위해서는 클래스를 통해 다른 클래스로 직접적으로 접근하지 못하게 해야한다. 즉 클래스를 단순한 데이터로 사용하는 것이 아니라 클래스에 자율성과 책임을 부여하는 것이다. 이를 위한 달성하기 위한 도구가 캡슐화
이다.
class Theater(val ticketSeller: TicketSeller) {
fun enter(audience: Audience) {
ticketSeller.sellTo(audience)
}
}
class TicketSeller(private val ticketOffice: TicketOffice) {
fun sellTo(audience: Audience) {
val ticket = ticketOffice.getTicket()
val amount = audience.buy(ticket)
ticketOffice.plusAmount(amount)
}
}
class Audience(private val bag: Bag) {
fun buy(ticket: Ticket): Long {
bag.setTicket(ticket)
return if(bag.hasInvitation) {
0L
} else {
val fee = ticket.fee
bag.minusAmount(fee)
fee
}
}
}
위 코드를 보면 TicketSeller, Audience
에 각각의 책임(판매, 구매)를 부여하고 책임에 대한 프로세스를 수행하게 하여 자율성을 부여했다. 또한 TicketSeller, Audience
의 내부 객체(상태)를 캡슐화해 외부에서 접근할 수 없도록 하고 객체 간의 메세지를 통해서만 상호작용하도록 하였다. 이렇게 클래스와 밀접하게 연관된 작업만을 수행하고 연관없는 작업은 다른 객체에게 위임하는 객체를 응집도(Cohension)가 높다고 한다. 객체에 자율적인 책임을 부여하면 응집도가 높으면서 결합도는 낮은 설계를 가져 객체지향의 목적을 달성할 수 있다.
자율성과 결합도
TicketSeller, Audience
에만 자율성과 책임을 부여했지만 Bag, TicketOffice
에도 마찬가지로 자율성과 책임을 부여해 설계를 개선할 수 있다.
class TicketOffice(val amount: Long, val tickets: MutableList<Ticket>) {
fun sellTicketTo(audience: Audience) {
val ticket = getTicket()
val amount = audience.buy(ticket)
plusAmount(amount)
}
fun getTicket(): Ticket = tickets.removeAt(0)
fun plusAmount(amount: Long) = this.amount + amount
fun minusAmount(amount: Long) = this.amount - amount
}
class TicketSeller(private val ticketOffice: TicketOffice) {
fun sellTo(audience: Audience) {
ticketOffice.sellTicketTo(audience)
}
}
TicketOffice
에 자율성과 책임을 부여했다. 응집도는 높아졌지만 TicketOffice
가 Audience
에 의존하며 결합도는 높아졌다. 이러한 경우 응집도과 결합도 중 어떤 것이 더 우선해야하는지 고려한 후 적절히 선택해야한다. 즉, 모든 조건을 만족하는 완벽한 설계는 없기에 그 중 최선이라 생각되는 부분을 우선해 설계해야한다
의인화
TicketOffice
와 Bag
은 실제 세계에서 수동적인 존재이다. 객체지향 세계에서 수동적인 존재를 능동적인 존재로 취급해 설계하는 것을 의인화
라고 한다. 객체는 실제 세계의 무언가를 소프트웨어로 투영한 것이 산물이라고 한다. 그렇지만 필요하다면 실제 세계와 다르더라도 능동적으로 설계하는 것이 좋은 설계가 될 수 있음을 생각해야 한다. 책에서는 Bag
에도 자율성과 책임을 부여하였지만 실제 세계와 같은 수동적인 존재로 단순한 데이터 클래스로 사용하는 것도 방법이 될 수 있다고 생각한다
References
- 오브젝트: 코드로 이해하는 객체지향 설계 1장, 조영호 저