개발생각

[객체지향] 객체지향의 사실과 오해 - 2

CandDoIt 2025. 3. 21. 14:20

본 글은 "객체지향의 사실과 오해"를 읽고 작성하는 독후감입니다.

 

객체 지향의 사실과 오해 - 1,2,3장 요약 포스팅

https://younghun123.tistory.com/7

 

[객체지향] 객체지향의 사실과 오해 - 1

본 글은 "객체지향의 사실과 오해"를 읽고 작성하는 독후감입니다. 안녕하세요? 이번 포스팅에서는 객체지향의 사실과 오해라는 책을 읽고 요약한 내용을 공유하고, 읽으면서 들었던 고민을 나

younghun123.tistory.com

 

제 4장: 협력, 책임, 역할

객체지향 패러다임에서, 설계자는 적절한 객체에게 적절한 책임을 할당합니다. 그리고 객체는 각자 책임을 갖고 역할이라는 개념으로 추상화 하며, 서로를 식별합니다. 자신이 하지 않는 일은 다른 객체에게 위임하는 책임 메세지를 주고 받으며 협력합니다. 이 원칙은 우리의 애플리케이션을 아름답게 만드는 가장 중요한 요인입니다.

 

공용 인터페이스(public interface)

책임은 객체의 외부에 제공해줄 수 있는 정보와 외부에 제공해줄 수 있는 행동의 목록입니다. 즉, 책임은 객체의 공용 인터페이스(public interface)입니다. 공용 인터페이스는 캡슐화를 실현하는데 중요한 요소입니다.

 

역할

동일한 역할을 맡은 객체들이 동일한 책임을지고, 동일한 메시지를 받을 수 있습니다. 추상화된 역할은 협력하는 과정을 추상화해 인지 과부하를 줄이고, 유연하고 단순하며 재사용가능한 협력체계를 만들 수 있습니다. 역할은 협력안에서 구체적인 객체를 추상적인 역할로 대체시킵니다. 이는 협력의 양상을 단순화하고 계층화해 애플리케이션의 설계를 이해하기 쉬운 구조로 바꾸어줍니다.

 

협력

협력은 객체의 모습을 결정하는 컨텍스트입니다. 설계자는 현실 세계의 객체를 단순히 모방하지 않습니다. 객체의 모습은 협력이라는 문맥안에서 결정되어야 합니다. 하지만, 설계자가 객체 자체에 집착하다보면 이러한 사실을 놓칠 수 있습니다. 엄밀히 말해서 현실 세계가 아닌, 소프트 세계 안에서 동작하는 객체를 창조해야 합니다. 그러므로, 소프트웨어의 설계자 혹은 구현자는 해당 소프트웨어의 궁국적인 목적을 이해하고 공부해야합니다. 그것이 비즈니스든 무엇이든.

 

책임의 범주

doing

  1. 객체는 스스로 인스턴스를 생성하거나 계산합니다.
  2. 다른 객체의 행동을 시작합니다.
  3. 다른 객체의 활동을 제어하고 조절합니다.

knowing

  1. 개인적인 정보에 대해 압니다. 이는 캡슐화와 연동되어있습니다.
  2. 관련된 객체에 대해 알고있습니다.
  3. 자신이 유도하거나 계산할 수 있는 것에 대해 알고있습니다.

 

[협력 - 책임 - 역할]을 강조하는 객체지향 프로그래밍

책임 주도 설계 원칙

  1. 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  2. 시스템 책임을 더 작은 책임으로 분할한다.
  3. 분할된 책임을 수행할 수 있는 적절한 객체를 찾아 역할을 할당한다.
  4. 객체가 책임을 수행하는 중에 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  5. 해당 객체 또는 역할에게 책임을 할당함해서, 두 객체가 협력하게 만든다.

 

제 5장: 책임과 메시지

객체 지향의 목적은 바로 message입니다. 객체를 지향하는 시스템에는 하나 이상의 여러 객체가 존재해야 합니다.

객체들이 존재만해서는 시스템이 될 수 없습니다. 객체들이 주고 받는 메세지는 객체들의 윤곽을 잡아줍니다. 즉, 우리는 메세지에 집중해야 합니다. 클래스와 상속 관계에만 중심을 두는 설계는 아름답지 못합니다. 객체들이 역할과 책임에 맞는 메세지를 주고 받을 때 비로소 역할 - 책임 - 협력의 메커니즘으로 작동하는 아름다운 객체지향 시스템이 완성됩니다.

 

중요한 점은 단순히 클래스의 메소드가 return 하는 데이터를 중심으로 설계하라는 말이 아닙니다. 메세지에는 객체가 다른 객체에게 무엇을 수행해야 하는지, 어떤 상태로 변화해야 하는지에 대한 정보가 담겨있습니다.

 

제 6장: 객체 지도

객체 지향의 세계를 구축하기 위한 두가지 재료가 필요합니다. 두 자료는 안정적인 재료와 불안정적인 재료로 나뉩니다.

안정적인 재료: 도메인 모델

현실 세계의 은유를 대상으로 재창조된 대상입니다. 비즈니스 규칙을 가질 수 있고, 상대적으로 변경 가능성이 낮습니다.

불안정적인 재료: 유즈케이스

사용자 상호작용 관점에서 바라봅니다. 도메인에 대한 사용자의 행위를 텍스트로 정리합니다.

 

책에서는 이렇게 추상적으로 설명하지만 조금 더 예시를 들어 설명하겠습니다.

우리는 예약이라는 도메인을 만들겠습니다. 예약은 어떤 속성을 가져야할까요? 기본적으로 예약자, 예약 시간, 담당자 등이 있을 수 있습니다. (물론 더 있을 수 있지만, 최대한 간단하게 가정하겠습니다.) 또한 예약 시간은 18시부터 20시까지만 가능합니다. 또한 staff의 role은 manager이어야합니다.  한번 코드로 표현해볼까요? 

@Setter
public class Reservation {
    private String reserver; 
    private LocalDateTime reservationTime;
    private String staff;

    public Reservation(String reserver, LocalDateTime reservationTime, String staff) {
        if (!isValidReservationTime(reservationTime)) {
            throw new IllegalArgumentException("예약 시간은 18시부터 20시까지만 가능합니다.");
        }
        if (!isManager(staff)) {
            throw new IllegalArgumentException("담당자는 manager 역할이어야 합니다.");
        }

        this.reserver = reserver;
        this.reservationTime = reservationTime;
        this.staff = staff;
    }

    private boolean isValidReservationTime(LocalDateTime reservationTime) {
        LocalTime reservationHour = reservationTime.toLocalTime();
        return !reservationHour.isBefore(LocalTime.of(18, 0)) && !reservationHour.isAfter(LocalTime.of(20, 0));
    }

    private boolean isManager(String staff) {
        return "manager".equalsIgnoreCase(staff);
    }
}

 

 

이렇게 예약 도메인의 속성과 비즈니스 규칙을 클래스로 표현했습니다.(staff 부분이 어색하긴 하지만 예시를 위함이니 그냥 넘어갑시다!) 이렇게 설계된 도메인은 객체지도에서 안정적인 재료에 해당합니다.

 

그렇다면 usecase는 무엇에 해당할까요? 바로 예약 생성, 취소와 같은 사용자의 행위를 담당하는 로직입니다. 전통적인 레이어드 아키텍처에서는 서비스의 영역에 해당할 수 있는 부분입니다.

public class ReservationUsecase {
    private final List<Reservation> reservations = new ArrayList<>();

    public void createReservation(String reserver, LocalDateTime reservationTime, String staff) {
        Reservation reservation = new Reservation(reserver, reservationTime, staff);
        reservations.add(reservation);
        System.out.println("예약이 성공적으로 생성되었습니다: " + reservation);
    }

    public void cancelReservation(String reserver) {
        reservations.removeIf(reservation -> reservation.getReserver().equals(reserver));
        System.out.println(reserver + "님의 예약이 취소되었습니다.");
    }
}

 

7장: 함께 모으기(feat. 마틴 파울러)

개념 관점

도메인 안에 존재하는 개념과 개념들 사이의 관계표현합니다.

⇒ 실제 도메인의 비즈니스 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심입니다.

 

명세 관점

개발자의 영역입니다. 소프트웨어 세계 안에서 살아 숨쉬는 객체들의 움직임에 초점을 둡니다. 프로그래머는 객체가 협력을 위해 ‘무엇’할 수 있는지에 초점을 맞춰야 합니다. 우리는 반드시 인터페이스와 구현을 분리해야합니다. 인터페이스와 구현이 분리되어야 우리는 변화에 탄력적으로 대응할 수 있습니다. 소프트웨어는 변합니다. 우리는 이 사실을 명심해야 합니다. 객체지향은 반드시 “구현이 아닌 인터페이스에 프로그래밍하라.” 라는 원칙을 따라야합니다.

 

구현 관점

프로그래머에게 가장 익숙한 관점입니다. 해당 관점에서 프로그래머는 객체들이 책임을 수행하는 데 필요한 동작을 코드로 구현할 수 있습니다. 개발자는 사전에 정의한 인터페이스를 구현하는데 필요한 속성과 메서드를 클래스에 추가할 수 있습니다.

 

 

마무리하며...

익숙하지만 낯설었던 사실들

객체지향의 사실과 오해(조영호) 책의 요약을 모두 마쳤습니다. 해당 서적을 읽으며 여지껏 형식에 맞추어 코딩을 하지만 왜? 라는 의문을 갖지않던 자신을 발견했습니다. 김영한님의 스프링 강의를 처음 들으며 레이어드 아키텍처를 접할 때는 단순히 따라가기에 바빴습니다. 또한 백엔드 개발자로서 팀 안에서 헥사고날 아키텍처를 접하면서, 동료의 코드를 따라가기 바빴던 스스로가 생각나며 그때 작성했던 코드들이 생각났습니다. cqrs, domain, usecase와 같은 개념들이 등 단순히 어려운 아키텍처가 아닌 변화에 대응하기 위해 고민된 결과물이구나라고 생각했습니다. 소프트웨어의 유일한 본질은 변화이고, 개발자들은 항상 변화에대해 고민해야합니다. 그리고 이는 사용자의 피드백에 빠르게 대응하고, 더 좋은 방향으로 변화할 수 있는 좋은 소프트웨어 설계의 뼈대가 됩니다. 앞으로 개발자로서 소프트웨어를 설계할 때 조금 더 비즈니스 관점으로 접하는데 시야를 열어주는 좋은 인사이트인 것 같습니다.

 

추상화

"왜 추상화가 필요합니까?", "느슨한 결합을 위해서요." 인터페이스의 필요성에대한 내 생각이었습니다. 책을 읽기 전까지!

틀린 말은 아닙니다. 하지만 이제는 객체를 설명하고 역할과 책임, 협력을 약속하기 위해서라고 말하고 싶습니다. 협력이라는 컨텍스트안에서 서로를 몰라도 되는 유연함이 생겨납니다. 그리고 이는 캡슐화의 개념과 이어지죠. 학교에서 객체지향 프로그래밍을 배우고, 클래스와 인터페이스 추상 클래스에 대한 개념을 배웠지만 이렇게 명쾌하게 이해한 적은 처음입니다. 하지만 아이러니하게도 이 책에는 코드는 거의 없습니다. 아직 내가 알고있다고 생각하지만, 정확이 알고 있는 것들이 많이 없다는 사실을 직시하며 앞으로 더 열심히 달려야겠습니다.