ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • '오브젝트' 책 보고 공부하기 - ① 객체, 설계
    JAVA공부/JAVA 2023. 5. 16. 02:19

    1) 티켓 판매 애플리케이션 구현하기 (코드 작성 그대로)

     

    초대장

    public class Invitation {
        private LocalDateTime when; //초대일자
    }

     

    티켓

    public class Ticket {
        private Long fee; //티켓 금액
    
        public Long getFee() {
            return fee;
        }
    }

    가방

    public class Bag {
    
        private Long amount; //보유현금
        private Invitation invitation; //초대장
        private Ticket ticket; //티켓
    
        public Bag(Long amount) {
            new Bag(amount, null);
        }
    
        public Bag(Long amount, Invitation invitation) {
            this.amount = amount;
            this.invitation = invitation;
        }
    
        public boolean hasInvitation() {
            return invitation != null;
        }
    
        public boolean hasTicket() {
            return ticket != null;
        }
    
        public void setTicket(Ticket ticket) {
            this.ticket = ticket;
        }
    
        public void minusAmount(Long amount) {
            this.amount -= amount;
        }
    
        public void plusAmount(Long amount) {
            this.amount += amount;
        }
    
    }

    관람객

    public class Audience {
        private Bag bag;
    
        public Audience(Bag bag) {
            this.bag = bag;
        }
    
        public Bag getBag() {
            return bag;
        }
    }

    매표소

    public class TicketOffice {
        private Long amount;
        private List<Ticket> tickets = new ArrayList<>();
    
        public TicketOffice(Long amount, Ticket... tickets) {
            this.amount = amount;
            this.tickets.addAll(Arrays.asList(tickets));
        }
    
        public Ticket getTicket() {
            return tickets.remove(0);
        }
    
        public void minusAmount(Long amount) {
            this.amount -= amount;
        }
    
        public void plusAmount(Long 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);
            }
        }
    }

     

    기능 구현이 끝났다. 다른것은 기능설명을 할 필요가 없지만. Theaterenter의 기능에 대해 알아보자.

    1. 관람객이 입장한다.
    2. 소극장은 관람객의 가방안에 초대장이 있는지 확인한다.
      1. 초대장이 있으면 관람객 가방안에 티켓을 넣어준다.
      2. 초대장이 없으면 2-1.관람객의 가방안에 티켓금액을 차감 후
        2-2.매표소의 금액을 증가시키고
        3-3. 관람객의 가방안에 티켓을 넣어준다.


    2) 무엇이 문제인가

    책 내용을 읽기전에 내가 생각하는 점을 써보았다.

    1. 중복코드가 존재한다.

    2. 내부함수들을 한 메서드안에서 모두 꺼내어 쓰는 느낌의 경향이 있다.

    3. 가독성이 떨어진다.

     

    다시 책내용으로 돌아가자..

     

    로버트 마틴의 소프트웨어 모듈이 가져야하는  세 가지 기능에 대해 얘기를 해주신다.

    첫 번째, 실행 중 제대로 동작하는 것

    두 번째, 간단한 작업만으로도 변경이 가능해야하는 것

    세 번째, 개발자가 쉽게 읽고 이해해야 할 수 있어야 하는것

     

    분명 위의 코드는 첫 번째의 기능은 만족하고 있으나 나머지 둘은 만족하지 못 한다.

     

     

     

    왜 이해하기 어려운가?

     

    1. 현실과 상이한 코드짜임새 때문이다.

     

    위의 기능을 읽어보면 판매원과 관람객은 극장에 의해서 표와 돈을 주고 받는 것처럼 되어있다.

    그런데, 현실은 어떠냐면.. 관람객이 돈을 주고 판매원이 돈을 받고 매표소에 보관하고 관람객에게 티켓을 건넨다.

    그러면 관람객이 티켓을 가방에 보관한다.

    그렇다. 관람객과 판매원이 컴퓨터나 로봇처럼 수동적으로 변해버렸다.

     

    2. Theater의 enter를 이해하기 위해 많은 것을 알아야한다.

     

    극장은 관람객이 입장한다만 알고 있으면 된다.

    그러나, 위 코드 내용에서는 audience가 bag을 가지고 있고, 그 안에는 현금과 티켓을 가지고있으며,

    ticketSeller가 ticketOffice에서 티켓을 판매하고... 등등의 너무 자세한것들을 알아야 하는 것이 문제다.

     

     

     

    왜 변경이 어려운가?

     

    Theater의 Enter에서 많은 것을 확정짓는다.

    관람객이 가방만 들고다닌다고 확정하고 가방안에만 현금이 있다고 확정하고, 카드로만 결제해야되고

    등등 모든 기능을 확정 짓는다. 이로인해 추후 변동이 되는 것을 쉽게 허락하지 않는다.

     

    가령 관람객이 가방을 들고 있다는 가정이 바뀌었다고 생각해보면 Audience 클래스에서 Bag클래스를

    제거하는 것 뿐 아니라, enter메서드에서도 많은 수정이 필요하다.

     

    이것을 객체 사이의 의존성이 라고 하는데, 이것을 없애는 것은 정답은 아니다.

    결합도(coupling)를 낮춰 변경을 용이하게 해야한다.

     

     


    3) 설계 개선하기

     

    자율성 높이기

    1. Audience는 Bag에 TicketSeller는 TicketOffice에 직접 접근하도록 수정하기 

     

    코드가 다음과 같이 변경되었다.

     

    관람객

    public class Audience {
        //... 추가
    
        public Long buy(Ticket ticket) {
            if (bag.hasInvitation()) {
                bag.setTicket(ticket);
                return 0L;
            } else {
                bag.minusAmount(ticket.getFee());
                bag.setTicket(ticket);
                return ticket.getFee();
            }
        }
    }

    판매원

    public class TicketSeller {
       //.. 추가
        public void sellTo(Audience audience) {
            ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
        }
    }

    극장

    public class Theater {
       // .. 변경
        public void enter(Audience audience) {
            ticketSeller.sellTo(audience);
        }
    }

     

     

     

    깨달은 나의 리팩토링 실력

     

    처음에 ticketSeller까지 옮기는것까지는 할 수 있었는데, Audience로 주체를 옮기는 것은 솔직히 생각 못 하였다.

    위에는 공통적인 요소를 새로 만들어 amount = 0L값을 리턴할 수 있도록 변경해주는 것이 포인트이다.

    저렇게 놓고보니 나의 코드들이 수동적인 부분이 많아겠구나를 느꼈다.

     

     

     

    무엇이 개선됬는가

     

    우선, 객체간의 의존성이 많이 줄었다.

    TicketSeller가 할일은 TicketSeller에게, Audience가 할일은 Audience에게 맡기게되면서 Theater가 판매원을 간섭하지않고, Audience를 간섭하지 않게 되었다.

    이에따라 위에서 말한 '변경 용이성'이 증대 될 수 있다. Theater가 더 이상 TicketSeller의 내부상황을 직접 관여하지 않기

    때문에 TicketSeller의 상세 구현내용이 바뀌어도 인터페이스만 바뀌지않게 처리하면 문제가 없다.

    이는 마치 회사에서 대표가 조직장에게 업무를 맡기고 더 이상 관여하지 않는다는 느낌(결과만 받으면됨)과 유사하다.

     

    두번째로, 관람객이 직접 가방을 관리하고, 판매원이 직접 티켓을 팔고 요금을 보관하므로 코드흐름에 대한 이해도가 높아진다. 가끔, 내가 짠 옛날 코드를 읽다가 아니 왜? 라는 단어가 나왔는데 이러한 문제가 많았으리라 생각한다.

     

     

     

    어떻게 한 것인가  ★★★

     

    객체가 가지고 있는 부분들을 하나의 주체가 다 꺼내쓰는 것이 아닌 객체 스스로 자신의 일을 처리하게 만들었다.

     

     

     

    캡슐화와 응집도

     

    핵심은 객체 내부의 상태를 캡슐화 하고, 객체간에는 오직 메시지를 통해서만 상호작용하도록 한다.

    Ticketseller의 sellTo메서드, Audience의 buy메서드가 그 메시지이다.

    밀접하게 연관된 작업만 수행하고 연관성 없는 것은 다른 객체에게 위임한다. -> 응집도를 높이는 방향

     

     

     

    절차 지향과 객체 지향

     

    맨 처음 적은 enter메서드를 보자 enter메서드가 프로세스 이며, Audience, TicketSeller, Bag, TicketOffice는 데이터로서

    존재하였다. 이 처럼 프로세스와 데이터를 별도의 모듈에 위치 시키는 방식을 절차지향적 프로그래밍이라고 한다.

    위에서 봤듯이 절차지향 프로그래밍은 직관적이지 못하다. Theater를 제외한 다른 객체들이 수동적인 존재로서

    존재한다. 변경시에도 많은 제약을 받으며, 이해하기 어려운 코드가 발생한다.

     

    변경하기 쉽게 개선된 코드가 바로 객체 지향 프로그래밍이다.

    기존에는 Theater가  Audience, TicketSeller, Bag, TicketOffice 모두를 의존하여 사용하였으나,

    개선된 Theater는 TicketSeller에만 의존하며, TicketSeller는 Audience와 TicketOffice에 의존하고,

    Audience는 Bag에 의존한다.

     

    훌륭한 객체지향 설계 핵심은 캡슐화를 통해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다.

     

     

     

    책임의 이동

     

    모든일을 도맡아했던 Theater에게서 책임이 분리되어 각각의 객체에게로 넘어갔다.

    알고보면 일떠넘기기와 같은 안좋은 느낌?도 들긴 하지만 역할분담이라고 생각하면 좋을 것 같다.

    객체지향 설계에서 또 다른 핵심은 적절한 객체에 적절한 책임을 분담하는 것이다. 

     

    위에 코드에서 파악해보자

    TicketSeller의 책임은 ? 티켓을 판매하는것

    Audience의 책임은 ? 티켓을 사는것

    Theater의 책임은 ? 관람객을 입장시키는 것

     

    객체에 적절한 책임을 부여하면 이해하기 쉬운 코드가 될 수 있다.

    즉, 자율성을 높이고 응직도를 높이고 결합도를 낮추어 최소한의 의존성만을 남기는 것이 훌륭한 객체지향 설계이다.

     

     

     

    더 수정해보자

     

    Bag과 TicketOffice의 자유도를 높여보자.

     

    가방

    public class Bag {
    	
        // hold method 추가
        public Long hold(Ticket ticket) {
            if (hasInvitation()) {
                setTicket(ticket);
                return 0L;
            } else {
                setTicket(ticket);
                minusAmount(ticket.getFee());
                return ticket.getFee();
            }
        }
    }

     

    매표소

    public class TicketOffice {
       
       //sellTicketTo 메서드추가
    
        public void sellTicketTo(Audience audience) {
            plusAmount(audience.buy(getTicket()));
        }
    }


    매표원

    public class TicketSeller {
       
       //sellTo 메서드 변경
    
        public void sellTo(Audience audience) {
            ticketOffice.sellTicketTo(audience);
        }
    }

     

    가방과 매표소에 자율성을 줌으로서 스스로 해결 할 수 있게 되었다.

     

    그러나 자세히 보면 TicketOffice가 Audience에 대해 알아야된다는 점이 추가되었는데,

    이 시점이 트레이드 오프 시점이다. 

    TicketOffice의 자율성을 선택할지, Audience에 대한 결합도를 낮출 것인지 선택해야한다.

     

    이 사례에서 이것을 말해준다.

    1. 기능을 설계할때 한 가지 이상의 방법이 있다는 것

    2. 모든 사람을 만족시키는 설계를 하기는 어렵다는 것

     

    코드 설계를 할 때, 어떻게 해야 모두를 만족시키지라는 생각으로 짰었는데, 조금 마음의 짐을 덜어놓게 되었다..

     

     

     

    그래 거짓말이다!

     

    사실 처음에 직관적인 코드를 짜야한다고 했지만, Theater가 우리를 입장시킨다던지, TicketOffice가 티켓을 판다던지..

    전혀 주체가 될 수 없는 사물, 장소이다. 객체는 단순히 실세계의 개념적인 개체를 의미하지 않는다.

    결국 훌륭한 객체지향의 설계는 객체를 자율적으로 행동하는 설계를 가리킨다.

    마치 생명과 지능을 가진 존재처럼 말이다.

     


     

    4) 객체 지향 설계

     

     

    설계가 왜 필요한가

     

    우리가 짜는 프로그램은 두 가지 요구사항을 만족하여야한다.

     

    1.기능이 작동되어야한다.

    2.언제나 쉽게 변경할 수 있어야한다.

     

    언제나 쉽게 변경 될 수 있어야한다가 중요한 이유는 프로그램은 요구사항에 맞춰서 항상 변해야 하기 때문이다.

    변경되지 않으면 버그가 발생하지 않을 수 있지만, 변경이 되는 경우에는 버그가 발생할 수 있다. 

    이를 최소화하기 위해 좋은 설계는 필수인 것이다.

     

     

     

    객체지향 설계

     

    훌륭한 객체지향 설계는 협력하는 객체사이의 의존성을 적절하게 관리하는 설계이다.

    세상에 엮인 것이 많은 사람일수록 변하기 어려운 것처럼 객체도 마찬가지로 강하게 결합될수록 변경하기 어렵다.

    데이터와 프로세스를 객체안의 덩어리로 모으는 것은 첫걸음일 뿐이니 의존성을 적절하게 조절함으로써

    변경에 용이한 설계를 하자.

     

     

     

    정리

     

    1장에 대한 내용은 리팩토링을 과정을 거치면서 객체의 자율성에 부과해주고, 결합도를 낮춰줌으로써 

    객체지향적인 설계가 어떻게 좋은지 설명해준다.

    나는 지금까지 절차지향적인 코드를 짜온 것이 느껴졌다.

    그러면서 코드길이가 길어지고, 이곳을 수정할때, 저곳도 수정해야하고

    특히, 테스트를 할 때 하나의 메서드를 하는데도 복잡성이 높았다.

    또, 클래스를 다룰때, 단순히 값을 전달하는 매개체 역할의 클래스가 많았는데, 클래스의 역할을 너무 한정지어서

    생각했던 것 같다. 
    1장만 읽었는데도 나의 문제점이 많은게 보여서 의미있었던 장이었다.

     

Designed by Tistory.