wonslee / object-study

📔오브젝트 예제 코드를 따라 공부, 토론하는 스터디 그룹
0 stars 1 forks source link

1장 공부 내용 #3

Open wonslee opened 8 months ago

wonslee commented 8 months ago

블로그 : https://zorbathegeek.tistory.com/61

공부하며 작성한 내용이기 때문에 오류 사항이 있을 수 있습니다. 잘못된 부분은 피드백 부탁드립니다!

1장 내용

기능 명세

의존성 : 다른 객체 내부를 알면 알수록 변경에 취약해진다

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 {
            // seller - office - ticket, audience - Bag 를 알고 있음.
            Ticket ticket = ticketSeller.getTicketOffice().getTicket(); 
            audience.getBag().minusAmount(ticket.getFee());

                        ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

문제는 변경에 취약하다는 것이다.

  • 관람객이 가방을 들고 있지 않다면 어떻게 해야 할까?
  • 관람객이 현금이 아니라 신용카드를 이용해서 결제 한다면 어떻게 해야 할까?
  • 판매원이 매표소 밖에서 티켓을 판매해야 한다면 어떻게 해야 할까?

이런 과정이 변경되는 순간 모든 코드가 일시에 흔들리게 된다.

이것은 객체 사이의 의존성과 관련된 문제다.

의존성은 변경에 대한 영향을 암시한다.

의존성이라는 말 속에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포되어 있다.

… 우리의 목표는 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이다.

객체끼리 깊이 알면 알수록 의존성이 높아지고, 결합도가 높아집니다.

특히 객체 내의 필드로 깊이 들어갈수록, 추후 객체 내부 구현이 변경되었을 때 의존중인 객체의 코드도 바뀔 가능성이 높아집니다.

디미터 법칙

다른 객체가 어떠한 자료를 갖고 있는지 속사정을 몰라야 한다. ****(여러 개의 . 도트를 사용하지 말라)

객체 지향 프로그래밍에서 가장 중요한 것은 "객체가 어떤 데이터를 가지고 있는가?"가 아니라, "객체가 어떤 메세지를 주고 받는가?" 이다.

https://mangkyu.tistory.com/147

위의 교훈은 디미터 법칙과 일맥상통합니다.

객체들간의 메세지라는게 생각보다 정말 중요한 개념이었다는걸 조금은 느끼게 된 것 같습니다.

단순히 데이터를 담는 대상으로써 객체를 바라보는게 아니라, 추후 변경을 용이하게 하도록 하려면 어떤 데이터(메서드)를 숨기고 어떤 데이터(메서드)를 드러낼지를 고민하는 관점에서 객체를 바라보는게 객체지향이라고 생각합니다.

조영호님의 리팩토링

public class Theater {
    private TicketSeller ticketSeller;

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

        // ticketSeller에게 티켓 관련 책임을 위임
    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
    }
}
public class TicketSeller {
        // ...

        // 관객의 구매 책임은 Audience 에게 위임
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}
public class Audience {
        // ...

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

중요한건 티켓 구매의 정책 로직(조건문 부분)을 Theater에서 분리해 Audience의 buy 메서드에 집중시켰다는 점입니다.

이전 코드에선 Theater가 구매 정책 로직을 알고 있기 때문에, 정책이 바뀔 때마다 Theater 객체 내부를 바꿔야 합니다.

반면 조영호님의 코드에선 구매 정책이 바뀌더라도 Audience 객체의 구현만 바꾸면 됩니다.

확실히 Theater에 있던 구매 정책 로직을 분리하고 어느 객체에 응집시키는게 나아보입니다.

캡슐화와 응집도

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

밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도가 높다고 말한다.

객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다.

이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍이라고 부른다. …

두 방식 사이에 근본적인 차이를 만드는 것은 책임(기능)의 이동이다.

리팩토링 이전에는 작업 흐름이 주로 Theater에 의해 제어된다는 사실을 알 수 있다.

프로젝트에 적용해보기

이번 장에서 배운 내용을 현재 진행중인 프로젝트 코드에 적용해 리팩토링했습니다.

기능 명세

시간표와 시간표의 셀 객체가 있습니다.

시간표는 이름 필드를 가집니다(막학기).

시간표에는 셀들이 있고, 셀들은 과목명, 교수명, 요일, 시작 교시와 끝 교시 필드들을 가집니다.

IMG_0874

그리고 시간표의 셀들끼리 겹쳐선 안 됩니다. 즉 서로 요일이 같고, 교시가 하나라도 겹치게 되면 유효한 시간표 셀이라고 할 수 없습니다.

리팩토링 대상은 그 유효성 검사를 하는 코드입니다.

리팩토링

시간표 객체는 셀 리스트를 갖고 있고, 자신의 셀 리스트를 순회하면서 인자로 받은 셀에 대해 하나씩 요일과 교시를 비교합니다.

리팩토링 이전:

// Timetable.java : 시간표 객체
public class Timetable {
        private Long id;
    private User user;
    private String name;
    private List<Cell> cellList = new ArrayList<>();

        // 교시가 하나라도 겹치는지 검증. cellList를 순회하면서 인자로 들어온 cell과 교시를 비교
        public void validateOverlap(Cell cell) {
                this.cellList.stream()
                                // 스케줄의 구현 내용을 모두 알고 있음
                        .filter(it -> it.getDay().equals(cell.getDay()) && 
                                                IntStream.rangeClosed(it.getStartPeriod(), it.getEndPeriod())
                                        .anyMatch(i -> i == cell.getStartPeriod() || i == cell.getEndPeriod()))
                        .findAny()
                        .ifPresent(it -> {
                            throw new TimetableException(ExceptionType.DUPLICATE_TIMETABLE_DAY_PERIOD);
                        });
}
public class Cell {
    private TimetableDay day;
    private Integer startPeriod;
    private Integer endPeriod;
}

여기서 포인트는 시간표 객체에서 셀 객체의 내부 정보를 깊이 알고 있었다는 점과 스케줄 객체의 중복 검사 책임을 대신 지고 있었다는 점입니다.

즉 결합도가 높고 응집도가 낮은 상태였습니다.

리팩토링 이후:

public class Timetable {
        private Long id;
    private User user;
    private String name;
    private List<Cell> cellList = new ArrayList<>();

        public void validateOverlap(Cell cell) {
                // 겹침 여부 로직은 schedule으로 책임 이동
        if (scheduleList.stream().anyMatch(it -> it.isOverlapped(cell))) {
            throw new TimetableException(ExceptionType.DUPLICATE_TIMETABLE_DAY_PERIOD);
        }
        }
}
public class Cell {
    private TimetableDay day;
    private Integer startPeriod;
    private Integer endPeriod;

    // 셀끼리 요일이 같은 상태에서 교시가 하나라도 겹치는지 여부
    public boolean isOverlapped(Cell cell) {
        return this.day.equals(cell.day) &&
                IntStream.rangeClosed(this.startPeriod, this.endPeriod)
                        .anyMatch(period -> period == cell.startPeriod || period == cell.endPeriod);
    }
}

겹침 여부를 반환하는 로직을 셀로 옮겼습니다.

리팩토링함으로써 Cell의 응집도를 높이고 객체들간의 결합도도 낮출 수 있었습니다.

매우 뿌듯합니다.