[Self Study] : 오브젝트 #1

문승현·2022년 9월 1일
0

BeDev_3

목록 보기
5/6
post-thumbnail

객체지향 프로그래밍의 세계에 들어왔기에 이에 대한 이해가 필요하다고 생각했다.
관련해서 학습 방법을 이래 저래 찾던 중, "오브젝트"라는 책을 접하게 되었다.
정확한 이름은 "오브젝트, 코드로 이해하는 객체지향 설계"로,
Java 코드를 기반으로 객체지향 설계와 유지보수에 필요한 원칙과 기법을 설명한다.

총 15장으로 구성되어있는 책의 내용을 나름의 이해를 바탕으로 정리해보고자 한다.
우선, Java로 쓰여진 코드를 C# 코드로 변경하여 작성하였고,
각 장에서 중요하다고 생각한 내용을 요약해보았다. 아래는 1장의 내용을 정리한 것이다.

책의 제목에 걸맞게 1장부터 바로 코드로 설명을 시작한다.
아래 코드는 티켓 판매 애플리케이션을 구현한 것으로,
다른 클래스에 대한 Theater 클래스의 의존성이 굉장히 높다.

책에서는 특정 클래스의 의존성이 높은 문제를 로버트 마틴의 말을 인용하며 지적한다.
좋은 프로그램은 제대로 실행되고, 변경이 용이하고, 이해하기 쉬워야 하는데,
지금의 티켓 판매 애플리케이션은 정상적으로 실행은 되지만
의존성이 높아 변경이 어렵고, 이해하기도 쉽지 않다고 말한다.

객체 사이의 의존성이 높은 경우를 가리켜 결합도(Coupling)가 높다고 말한다.
결합도가 높으면 어떤 객체가 변경될 때, 그 객체에 의존하는 다른 객체도 변경되어야 한다.
즉, 결합도가 높을수록 함께 변경해야하는 것들이 늘어나기 때문에 변경이 어려워진다.

또한 아래의 코드는 Theater 클래스의 Enter 메소드가 거의 모든 일을 다 수행한다.
TicketSeller 클래스나 Audience 클래스는 Theater 클래스의 통제를 받는다.
이는 우리의 상식과는 다른 동작 방식이기에 이해하기 쉽지 않다.

public class Invitation 
{
    public DateTime when;
}

public class Ticket
{
    private int fee;

    public int GetFee()
    {
        return fee;
    }
}

public class Bag 
{
    private int amount;
    private Invitation invitation;
    private Ticket ticket;

    public Bag(int amount)
    {
        this.amount = amount;
    }

    public Bag(Invitation invitation, int amount)
    {
        this.invitation = invitation;
        this.amount = amount;
    }

    public bool HasInvitation()
    {
        return invitation != null;
    }

    public bool HasTicket()
    {
        return ticket != null;
    }

    public void SetTicket(Ticket ticket)
    {
        this.ticket = ticket;
    }

    public void MinusAmount(int amount)
    {
        amount -= amount;
    }

    public void PlusAmount(int amount)
    {
        amount += amount;
    }
}

public class Audience
{
    public Bag bag;

    public Audience(Bag bag)
    {
        this.bag = bag;
    }

    public Bag GetBag()
    {
        return bag;
    }
}

public class TicketOffice
{
    private int amount;
    private List<Ticket> tickets = new List<Ticket>();

    public TicketOffice(int amount, List<Ticket> tickets)
    {
        this.amount = amount;
        this.tickets = tickets;
    }

    public Ticket GetTicket()
    {
        var ticket = tickets[0];
        tickets.RemoveAt(0);
        return ticket;
    }

    public void MinusAmount(int amount)
    {
        this.amount -= amount;
    }

    public void PlusAmount(int amount)
    {
        this.amount += amount;
    }
}

public class TicketSeller
{
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice)
    {
        this.ticketOffice = ticketOffice;
    }

    public TicketOffice GetTicketOffice()
    {
        return ticketOffice;
    }
}

public class Theater
{
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller)
    {
        this.ticketSeller = ticketSeller;
    }   

    public void Enter(Audience audience)
    {
        if (audience.GetBag().HasInvitation())
        {
            Ticket ticket = ticketSeller.GetTicketOffice().GetTicket();
            audience.GetBag().SetTicket(ticket);
        }
        else
        {
            Ticket ticket = ticketSeller.GetTicketOffice().GetTicket();
            audience.GetBag().MinusAmount(ticket.GetFee());
            ticketSeller.GetTicketOffice().PlusAmount(ticket.GetFee());
            audience.GetBag().SetTicket(ticket);
        }
    }
}

위의 코드를 어떻게 수정하면 좋을까?
방법은 Theater 클래스가 Audience 클래스와 TicketSeller 클래스에 관해
너무 세세한 부분까지 알지 못하도록 정보를 차단하면 된다.

즉, 개념적이나 물리적으로 객체 내부의 세부적인 사항을 외부에 감추면 되는데,
이를 캡슐화(Encapsulation)라고 부른다.
캡슐화를 통해 객체 외부에서 내부로의 접근을 제한하면
객체와 객체 사이의 결합도를 낮추어 설계를 좀 더 쉽게 변경할 수 있게된다.
우선, Theater에서 TicketOffice에 접근하는 코드를 TicketSeller 내부로 숨기자.

수정된 Theater에서는 TicketOffice에 접근하지 않는다.
Theater는 TicketOffice가 TicketSeller 내부에 존재한다는 사실을 알지 못한다.
단지, TicketSeller가 SellTo 메시지를 이해하고 응답할 수 있다는 사실만 알고 있을 뿐이다.

다시 말해, Theater는 오직 TicketSeller의 인터페이스(Interface)에만 의존한다.
여기서 명시적으로 인터페이스와 구현(Implementation)을 구분하지는 않았지만,
TicketSeller 내부에 TicketOffice 인스턴스를 포함하는 것은 구현의 영역에 속한다.
이와 같이 객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개하는 것은
객체 사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하는 기본적인 설계 원칙이다.

그런데 TicketSeller를 살펴보면 Audience의 GetBag 메소드를 호출하여
Audience 내부의 Bag 인스턴스에 직접 접근하는 것을 알 수 있다.
TicketSeller와 동일한 방법으로 Audience를 캡슐화하면 아래와 같다.

판매자가 티켓을 판매하기 위해 TicketOffice를 사용하는 부분을 TicketSeller 내부로 옮기고,
관람객이 티켓을 구매하기 위해 Bag을 사용하는 부분을 Audience 내부로 옮긴 것이다.
다시 말해 자기 자신의 문제를 스스로 해결하도록 코드를 변경한 것으로,
이제 코드는 정상 동작할뿐만 아니라 변경이 용이하고 이해 가능하게 바뀌었다.

정리하자면 불필요한 의존성을 제거하고 객체 사이의 결합도를 낮추는 것이 중요하다.
이를 위한 방법이 캡슐화이며, 이를 통해 객체들이 낮은 결합도와 높은 응집도를 가지고
상호 작용하도록 설계하는 것이 훌륭한 객체지향 설계인 것이다.

완성코드

namespace After
{ 
    public class Invitation 
    {
        public DateTime when;
    }

    public class Ticket
    {
        private int fee;

        public int GetFee()
        {
            return fee;
        }
    }

    public class Bag 
    {
        private int amount;
        private Invitation invitation;
        private Ticket ticket;

        public Bag(int amount)
        {
            this.amount = amount;
        }

        public Bag(Invitation invitation, int amount)
        {
            this.invitation = invitation;
            this.amount = amount;
        }

        private bool HasInvitation()
        {
            return invitation != null;
        }

        public bool HasTicket()
        {
            return ticket != null;
        }

        private void SetTicket(Ticket ticket)
        {
            this.ticket = ticket;
        }

        private void MinusAmount(int amount)
        {
            amount -= amount;
        }

        public void PlusAmount(int amount)
        {
            amount += amount;
        }

        public int Hold(Ticket ticket)
        {
            if (HasInvitation())
            {
                SetTicket(ticket);
                return 0;
            }
            else
            {
                MinusAmount(ticket.GetFee());
                SetTicket(ticket);
                return ticket.GetFee();
            }
        }
    }

    public class Audience
    {
        private Bag bag;

        public Audience(Bag bag)
        {
            this.bag = bag;
        }

        public int Buy(Ticket ticket)
        {
            return bag.Hold(ticket);
        }
    }

    public class TicketOffice
    {
        private int amount;
        private List<Ticket> tickets = new List<Ticket>();

        public TicketOffice(int amount, List<Ticket> tickets)
        {
            this.amount = amount;
            this.tickets = tickets;
        }

        public Ticket GetTicket()
        {
            var ticket = tickets[0];
            tickets.RemoveAt(0);
            return ticket;
        }

        public void MinusAmount(int amount)
        {
            this.amount -= amount;
        }

        public void PlusAmount(int amount)
        {
            this.amount += amount;
        }
    }

    public class TicketSeller
    {
        private TicketOffice ticketOffice;

        public TicketSeller(TicketOffice ticketOffice)
        {
            this.ticketOffice = ticketOffice;
        }

        public void SellTo(Audience audience)
        {
            ticketOffice.PlusAmount(audience.Buy(ticketOffice.GetTicket()));
        }
    }

    public class Theater
    {
        private TicketSeller ticketSeller;

        public Theater(TicketSeller ticketSeller)
        {
            this.ticketSeller = ticketSeller;
        }   

        public void Enter(Audience audience)
        {
            this.ticketSeller.SellTo(audience);
        }
    }
}

0개의 댓글