오브젝트 스터디 - 1주차

Henry Cho·2025년 3월 17일
0

오브젝트-스터디

목록 보기
1/1

Week 1

🔖 오늘 읽은 범위 : 1장 객체, 설계 (9~35p)


🤓 책에서 기억하고 싶은 내용

0. 프로그래밍 패러다임

  • 개발자 공동체가 동일한 프로그래밍 스타일과 모델을 공유할 수 있게 함으로써 불필요한 부분에 대한 의견 충돌을 방지
  • 프로그래밍 패러다임은 혁명적이 아닌 발전적. 객체지향이 적합하지 않은 상황에서는 언제라도 다른 패러다임을 적용할 수 있는 시야를 길러야 한다.

1. 티켓 판매 애플리케이션 구현하기

  • 소극장은 먼저 관람객의 가방안에 초대장이 들어 있는지 확인한다. 만약 초대장이 들어있다면 이벤트에 당첨된 관란객이므로 판매원에게서 받은 티켓을 관람객의 가방 안에 넣어준다. 가방 안에 초대장이 없다면 티켓을 판매해야 한다. 이 경우 소극장은 관람객의 가방에서 티켓 금액만큼을 차감한 후 매표소에 금액을 증가시킨다. 마지막으로 소극장은 관람객의 가방 안에 티켓을 넣어줌으로써 관람객의 입장 절차를 끝낸다.

2. 무엇이 문제인가?

  • 모든 모듈은 제대로 실행돼야 하고, 변경이 용이해야 하며, 이해하기 쉬워야 한다. (로버트 마틴)
  • 위 예제는 제대로 동작하지만 변경 용이성과 읽는 사람과의 의사소통은 만족하지 못함
    • 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재

예상을 빗나가는 코드

  • 이해 가능한 코드는 동작이 우리의 예상에서 크게 벗어나지 않아야 한다
  • 코드 이해를 위해 여러 가지 세부적인 내용들을 한꺼번에 기억하고 있어야 한다
    • Theater 의 enter 메서드 이해를 위해 Audience 가 Bag 을 가지고 있고, Bag 안에는 현금과 티켓이 들러있으며, TicketOffice 에서 티켓을 판매하고, TicketOffice 안에 돈과 티켓이 보관돼 있다는 모든 사실을 동시에 기억하고 있어야 한다.

변경에 취약한 코드

  • Audience와 TicketSeller 를 변경할 경우 Theater 도 함께 변경해야 한다
  • 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만들어라
    • 서로 다른 클래스가 내부에 대해 더 많이 알면 알수록 Audience 를 변경하기 어려워 진다
    • 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거해야 한다.

3. 설계 개선하기

  • 관람객과 판매원이 서로 세부적인 부분을 알지 못하도록 정보를 차단하여 자율적인 존재로 만들어라

자율성을 높이자

  • Theater.enter 에서 TicketOffice 에 접근하는 모든 코드
public class Theater {
	// ..
    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);
        }
    }
}

=> 를 TicketSeller 내부로 숨긴다

public class TicketSeller {
    // ..
    public void sellTo(Audience audience) {
        if (audience.getBag().hasInvitation()) { 
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketOffice.plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}
  • 캡슐화: 개념, 물리적으로 객체 내부의 세부적인 사항을 감추는 것
    • 이를 통해 변경하기운 객체를 만든다
    • 내부로의 접근 제한을 통해 객체와 객체 사이의 결합도를 낮춰 설계를 좀 더 쉽게 변경할 수 있게 된다.
  • 최종 Threater
public class Theater {
    private TicketSeller ticketSeller;
    
    public Theater(TicketSeller ticketSeller) { 
        this.ticketSeller = ticketSeller;
    }
    
    public void enter(Audience audience) { 
        ticketSeller.sellTo(audience);
    }
}
  • Theater 는 ticketOffice 가 TicketSeller 내부에 존재한다는 사실(구현 영역)을 알지 못하며, ticketSeller가 sellTo 메시지를 이해하고 응답할 수 있다는 사실(인터페이스)만 알고 있다.
    • 객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개 => 결합도를 낮추고 변경하기 쉬운 코드를 작성

그 림 1 . 2 너 무 많 은 클래 스 에 의 존 하 는 T h e a t e r

그림 1.4 Threater의 결합도를 낮춘 설계

  • Theater 에서 TicketOffice 로의 의존성이 제거되었다.
  • 이번에는 Audience 의 캡슐화를 개선
    • Audience.buy 메서드를 추가하고 TicketSeller.sellTo 메서드에서 getBag 에 접근하는 부분을 buy 로 이동
public class TicketSeller {
    // ..
    public void sellTo(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketOffice.getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketOffice.plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

=>

public class Audience {
    private Bag bag;

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

    public Long buy(Ticket ticket) {
        if (bag.hasInvitation()) {
            bag.setTicket(ticket);
            return 0L;
        } else {
            bag.setTicket(ticket);
            bag.minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
}
  • Audience 는 자신의 가방안에 초대장이 있는지 스스로 확인

  • 직접 처리하므로 더 이상 Audience 가 Bag 을 소유하고 있다는 사실을 외부에 알림 필요 없음

  • TicketSeller 가 Audience 의 인터페이스에만 의존하도록 수정

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}
  • 내부 구현이 캡슐화 되었으므로 Audience 의 구현을 수정하더라도 TicketSeller 에는 영향을 미치지 않음

무엇이 개선 되었는가

  • 수정된 Audience 와 TicketSeller는 자신이 가지고 있는 소지품을 스스로 관리
  • Audience나 TicketSeller의 내부 구현을 변경 하더라도 Theater를 함께 변경할 필요가 없어짐

어떻게 한 것인가

  • 자기 스스로 문제를 해결하도록 코드를 변경하여 객체의 자율성을 높임

캡슐화와 응집도

  • 객체 내부의 상태를 캡슐화 하고 객체간에 오직 메시지를 통해서만 상호작용 하도록 만드는 것이 핵심
  • 응집도가 높다: 밀접하게 연관된 작업만 수행하고 연관성 없는 작업은 다른 객체에 위임
    • 자율적인 객체는 결합도를 낮출 뿐만 아니라 응집도를 높임

절차지향과 객체지향

Procedural Programming

  • 기존코드의 Theater.enter 는 process, Audience, TicketSeller, Bag, TicketOffice 는 Data
  • Process 와 Data 를 별도의 모듈에 위치시키는 방식
  • Theater 가 다른 Data 객체 모두에 의존하고 있음
    • 자율적이지 않음 => 코드 읽기 어려움
    • 데이터 변경으로 인한 영향을 지역적으로 고립시키기 어려움 => 변경에 취약

Object-Oriented Programming

  • Data 와 Process가 동일한 모듈 내부에 위치하도록 프로그래밍
  • 모든 의존성을 없앨 수는 없지만 하나의 변경으로 인한 여파가 여러 클래스로 전파되는 것을 효율적으로 억제

책임의 이동

  • 책임 = 기능
  • 책임이 Theater 에 집중 된 경우
  • 필요한 책임이 여러 객체에 분산되어 있는 경우

더 개선할 수 있다

  • bag 은 여전히 Audience 에 의해 끌려다니는 수동적인 존재 => Bag 내부로 캡슐화
public class Bag {
    private Long amount;
    private Ticket ticket;
    private Invitation invitation;

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

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

    private boolean hasInvitation() {
        return invitation != null;
    }

    private void minusAmount(Long amount) {
        this.amount -= amount;
    }
}
  • Audience 가 Bag 의 구현이 아닌 인터페이스에만 의존하도록 수정
public class Audience {
    public Long buy(Ticket ticket) {
        return bag.hold(ticket);
    }
}
  • TicketSeller 역시 TicketOffice에 있는 Ticket 을 마음대로 꺼내서 자기 멋대로 Audience 에게 팔고 그 돈을 마음대로 TicketOffice 에 넣어버림
public class TicketSeller {
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}
  • TicketOffice 가 자율 객체가 되도록 sellTicketTo 메서드 추가
    • TicketSeller.sellTo 메서드의 내부로직을 이동
public class TicketOffice {
    public void sellTicketTo(Audience audience) {
        plusAmount(audience.buy(getTicket()));
    }

    private Ticket getTicket() {
        return tickets.remove(0);
    }

    private void plusAmount(Long amount) {
        this.amount += amount;
    }
}
  • 이제 TicketOffice.sellTicketTo 인터페이스에만 의존하므로 호출만 하면 된다.
public class TicketSeller {
    public void sellTo(Audience audience) {
        ticketOffice.sellTicketTo(audience);
    }
}
  • 그러나 기존에는 없던 TicketOffice 와 Audience 사이에 의존성이 추가되었다
    • TicketOffice 의 자율성은 높였지만 전체 설계의 결합도는 상승했다.
  • 의사결정 필요 => TicketOffice의 자율성 보다는 Audience 에 대한 결합도를 낮추는 것이 더 중요하다
  • 어떤 기능을 설계하는 방법은 한 가지 이상일 수 있다.
  • 설계는 트레이드 오프의 산물이다. 어떤 경우에도 모든 사람들을 만족시킬 설계를 만들 수 없다.

그래, 거짓말이다!

  • 의인화: 현실에서는 수동적인 존재라도, 소프트웨어 객체를 설계할 때는 능동적이고 자율적인 존재로 설계

4. 객체지향 설계

  • 설계란 코드를 배치하는 것이다.
  • 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계

변경 수용이 중요한 해야 하는 이유

  • 요구사항이 항상 변경되기 때문
    • 초기에 모든 요구사항을 수집하는 것은 불가능
      • 모두 수집했어도 요구사항은 바뀔 수 밖에 없음
  • 코드를 변경할 때 버그가 추가될 가능성이 높기 때문

객체지향 설계

  • 협력하는 객체 사이의 의존성을 적절하게 관리하여 변경에 용이한 설계

🤔 떠오르는 생각

UML 정리

Association (실선)

  • 의미: 실선은 연관(Association) 관계를 나타냅니다. 이는 한 클래스가 다른 클래스의 객체를 참조하거나 사용하는 관계를 의미합니다.
  • 예시:
    • Audience와 Bag 사이의 실선: Audience 클래스가 Bag 객체를 참조합니다 (getBag() 메서드로 확인 가능).
    • TicketSeller와 TicketOffice 사이의 실선: TicketSeller가 TicketOffice 객체를 참조합니다 (getTicketOffice() 메서드로 확인 가능).
    • Theater와 TicketSeller 사이의 실선: Theater가 TicketSeller 객체를 참조합니다.
  • 화살표: 실선 끝에 화살표가 있는 경우, 참조 방향을 명시적으로 나타냅니다. 예를 들어, Audience → Bag은 Audience가 Bag을 참조한다는 뜻입니다.

Dependency (점선)

  • 의미: 점선은 의존(Dependency) 관계를 나타냅니다. 이는 한 클래스가 다른 클래스의 객체를 일시적으로 사용하거나, 메서드 호출 등을 통해 의존하지만, 지속적인 참조 관계는 아니라는 것을 의미합니다.
  • 예시:
    • Bag과 Invitation 사이의 점선: Bag 클래스가 hasInvitation() 메서드를 통해 Invitation 객체를 일시적으로 사용합니다.
    • Bag과 Ticket 사이의 점선: Bag이 setTicket() 메서드를 통해 Ticket 객체를 사용합니다.
      TicketOffice와 Ticket 사이의 점선: TicketOffice가 getTicket() 메서드를 통해 Ticket 객체를 반환하거나 사용합니다.
  • 화살표: 점선 끝에 화살표가 있는 경우, 의존 방향을 나타냅니다. 예를 들어, Bag → Ticket은 Bag이 Ticket에 의존한다는 뜻입니다.

비교표

구분Association (실선)Dependency (점선)
지속성지속적인 관계 (필드로 참조)일시적인 관계 (메서드 호출 시 사용)
구조적 여부구조적 (클래스 설계에 반영됨)비구조적 (특정 상황에서만 사용)
코드에서 표현클래스 필드(멤버 변수)로 객체를 가짐메서드 매개변수, 지역 변수로 객체를 사용
UML 표현실선점선
예시 비유사람과 자동차 (소유 관계)사람과 공구 (사용 관계)

🔎 질문

  • TicketOffice의 자율성 보다는 Audience 에 대한 결합도를 낮추는 것이 더 중요하다 라고 정성적인 표현이 나왔는데.. 실제 설계에서 이를 정량화 하여 판단할 수 있을까?

정량화 접근법

1. 결합도(Coupling) 측정

  • CBO (Coupling Between Objects): 한 클래스가 다른 클래스와 얼마나 의존하는지를 나타내는 메트릭입니다. 예를 들어, TicketOffice가 Audience의 내부 구조(예: 필드나 메서드)에 직접 접근한다면 CBO가 높아집니다. 결합도를 낮추려면 TicketOffice가 Audience의 구체적인 구현 대신 추상화된 인터페이스나 메서드만 사용하도록 설계할 수 있습니다.
  • 메서드 호출 수: TicketOffice에서 Audience로의 직접 호출 횟수를 측정하여 의존성의 정도를 파악할 수 있습니다.

2. 자율성(Autonomy) 측정

  • 자율성은 객체가 자신의 책임을 얼마나 독립적으로 수행하는지를 나타냅니다. 이를 정량화하려면 TicketOffice가 외부 객체(Audience 등)에 얼마나 의존하지 않고 동작하는지 분석할 수 있습니다.
  • LCOM (Lack of Cohesion of Methods): 클래스 내부 메서드들이 서로 얼마나 연관되어 있는지를 측정합니다. TicketOffice의 자율성이 높다면, 내부 메서드들이 외부 객체 없이도 일관되게 동작할 가능성이 높습니다.

3. 의존성 방향 분석

  • TicketOffice가 Audience를 호출하는 방향과 횟수를 분석하여 의존성 그래프를 그릴 수 있습니다. 이 그래프에서 Audience에 대한 의존성이 과도하다면 결합도가 높다고 판단할 수 있습니다.
    실제 코드 예시를 통한 판단
    책에서 다룬 예시를 간단히 가정해보겠습니다:

결합도 높은 설계:

public class TicketOffice {
    public void sellTicket(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            audience.getBag().setTicket(new Ticket());
        } else {
            audience.getBag().minusAmount(10000);
            audience.getBag().setTicket(new Ticket());
        }
    }
}

여기서 TicketOffice는 Audience의 Bag 객체에 직접 접근하며, hasInvitation, minusAmount 같은 내부 로직에 의존합니다. CBO가 높고, Audience의 구현이 바뀌면 TicketOffice도 수정해야 합니다.
결합도 낮춘 설계:

public class TicketOffice {
    public void sellTicket(Audience audience) {
        audience.buy(new Ticket());
    }
}

public class Audience {
    private Bag bag;
    public void buy(Ticket ticket) {
        if (bag.hasInvitation()) {
            bag.setTicket(ticket);
        } else {
            bag.minusAmount(10000);
            bag.setTicket(ticket);
        }
    }
}

이 경우 TicketOffice는 Audience의 buy라는 인터페이스만 호출하며, 내부 구현은 Audience가 처리합니다. CBO가 낮아지고, Audience의 변경이 TicketOffice에 영향을 덜 미칩니다.

정량화의 한계와 판단

  • 메트릭 계산: 예를 들어, 첫 번째 설계의 CBO는 2(Audience, Bag)일 수 있고, 두 번째 설계는 1(Audience)입니다. 호출 횟수도 첫 번째는 3~4번, 두 번째는 1번으로 줄어듭니다.
  • 트레이드오프: 자율성을 높이기 위해 TicketOffice가 모든 로직을 처리한다면 코드 중복이 생기거나 책임이 모호해질 수 있습니다. 반면 결합도를 낮추면 Audience가 더 캡슐화되고 재사용성이 높아집니다.
  • 맥락 의존성: 정량화된 숫자만으로 "더 낫다"고 단정하기 어렵습니다. 예를 들어, TicketOffice가 단순히 티켓 판매소 역할만 해야 한다면 결합도를 낮추는 두 번째 설계가 더 적합할 수 있습니다.

결론적으로, 결합도와 자율성을 정량화하려면 CBO, 호출 횟수, 의존성 그래프 같은 메트릭을 활용할 수 있습니다. 하지만 책에서 강조하듯, "결합도를 낮추는 것"이 설계의 단순성, 유지보수성, 재사용성 측면에서 더 큰 가치를 제공한다면, 이를 우선시하는 것이 합리적일 수 있습니다. 정량화는 판단의 보조 도구일 뿐, 최종 결정은 설계의 목적과 요구사항에 따라 달라집니다.

📝 소감 3줄 요약

  • Data 와 Process가 동일한 모듈 내부에 위치하도록 프로그래밍 해야 의존성을 줄일 수 있다.
  • 현실에서는 수동적인 존재라도, 소프트웨어 객체를 설계할 때는 능동적이고 자율적인 존재로 설계 하라.
  • 설계란 코드를 배치하는 것(일 뿐)이다. 어렵게 생각할 필요 없이 잘 배치하기만 하면 된다. 근데 그 잘이 어려워요..
profile
Full stack tech visionary

0개의 댓글