객체지향의 사실과 오해 정리하기 + 읽은 나의 생각 : 객체지향 아직도 잘 모르겠다는 결론

GoldenDusk·2024년 12월 6일
1

Java

목록 보기
2/2
post-thumbnail

'객체지향의 사실과 오해'는 객체지향의 본질을 파헤치며, 객체지향 설계가 어떻게 이루어져야 하는지 심도 있게 설명해주는 책이다.

🏁 책 읽기 후 나의 생각: 객체지향 아직도 잘 모르겠어

마무리보다는 앞에 두는 게 좋을 것 같아 앞에 두는 글

객체지향이란 이 책을 읽고도 사실 아직도 정확히는 모르겠다. 코드를 짜다보면.. 적혀있는 코드로 인해 객체지향을 지키지 못하는 경우들이 많을 수밖에 없으며, 자바 또한 함수형 코드도 작성할 수 있고 결국 개인 취향이 어느정도 들어가는 것이 아닐까?

그리고 결국 대부분 회사 컨벤션에 맞춰 돌아가기 때문에 어? 나는 함수형 원해 이런다고 마음대로 짜는 것 또한 안됨

그렇다 해도 객체지향이 뭔지 객체지향 언어인 자바를 이해하려면 객체지향에 대해 공부해보는 것이 중요하다고 생각함

객체지향의 중요한 점을 한 줄로 표현하자면, 사진과 같다고 함

물론 설계가 중요하긴 하지만.. 처음 설계부터 단단하게 되어 있는 경우는 드물지 않을까 싶다 뿐만 아니라 이미 구현이 되어 있는 경우, 리팩토링도 설계 다시도 쉽지 않음

결국 자기가 필요 여부에 따라 결정하면 되는 게 아닌가 싶긴 해... 요즘은 또 시장이 안좋다보니 클린 코드보다는 빠른 개발을 지향하는 편인 것 같기도 하다

그리고 어디든 들어보면 레거시는 결국 쌓이고 쌓여서 이걸 나혼자서 다시 다바꾸기는 힘드니까 자기가 필요한가를 생각해보고 공부해보는 게 좋다고 생각함

스터디 하면서 느낀게 스타트업말고는 사실 내가 원하는 대로 할 수가 없음 내가 이펙티브 자바에서 이게 좋은데 넣고 싶어 해도 안됨 어제 백엔드 빌리지에서 느낀게 중소 이상으로 가게 되면 내가 주니어일 때 회사에 기여할 수 있자는 생각보다는 그 프로젝트를 잘 이해하고 레거시를 잘 이해해서 내가 중니어쯤 됐을 때 기여할 수 있다고 생각해보자

결국 그래서 객체지향 책을 읽으므로 얻어가야 하는건 레거시 코드를 보면서 이해할 수 있을 정도면 되지 않나 싶음

1장: 객체, 설계

이번 1장을 한 개의 그림으로 표현하자면 위와 같다. 이 중 가장 중요한 단어는 객체지향이라는 말과 같이 객체이다.

👱‍♀️ 객체란 무엇인가?

객체는 독립적인 존재로, 시스템 내에서 주어진 책임을 다하기 위해 행동한다. 객체지향 시스템에서 중요한 것은 객체들이 서로 협력하여 시스템의 목표를 달성하는 것이다. 이때 각 객체는 상태(데이터)행동(기능)을 가지고 있으며, 이 두 가지를 통해 스스로 역할을 수행한다. 설계에서 중요한 것은 객체의 내부 상태가 아니라 객체가 외부와 어떻게 상호작용하는지, 즉 객체의 행동이다.

행동이 곧 객체의 본질이며, 외부에서 바라본 객체의 정체성을 결정한다.

🧑‍🔧 행동 우선의 설계

객체지향 설계의 핵심은 행동 우선의 설계에 있다.

객체의 데이터가 무엇인지보다, 객체가 어떤 행동을 할 수 있는지가 더 중요하다. 객체는 스스로 맡은 역할을 다하고, 다른 객체와의 협력을 통해 시스템이 유연하게 변화할 수 있도록 한다. 객체는 자신의 행동을 통해 다른 객체와 상호작용하며, 이 상호작용이 시스템의 기능을 결정한다.

🎲 다형성의 이해

다형성은 객체가 동일한 메시지에 대해 서로 다른 방식으로 응답할 수 있는 특성이다. 예를 들어, draw() 메시지를 받았을 때 Circle 객체와 Square 객체는 각각 원과 사각형을 그리는 방식으로 응답한다. 다형성을 통해 코드의 유연성과 재사용성을 높일 수 있으며, 새로운 객체가 추가되더라도 기존 코드의 변경 없이 쉽게 확장할 수 있게 된다.

코드 예시: 행동 우선의 설계와 다형성

// 행동 우선 설계 예시
interface Drawable {
    void draw();
}

class Circle implements Drawable {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

class Square implements Drawable {
    @Override
    public void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

public class DrawingApp {
    public static void main(String[] args) {
        Drawable circle = new Circle();
        Drawable square = new Square();

        // 동일한 메시지(draw())에 대해 객체가 각각 다른 방식으로 응답
        circle.draw();  // 출력: 원을 그립니다.
        square.draw();  // 출력: 사각형을 그립니다.
    }
}

위 코드에서는 Drawable 인터페이스를 통해 행동을 정의하고, 이를 구현한 CircleSquare 클래스가 각각의 방식으로 draw() 메서드를 구현했다. 이렇게 하면 DrawingApp 클래스에서는 다형성을 활용해 객체의 구체적인 타입에 의존하지 않고도 동일한 메시지(draw())를 보낼 수 있다. 이를 통해 코드의 유연성확장성을 높일 수 있다.

또한, 새로운 도형(Triangle 등)을 추가하고 싶다면, 해당 도형 클래스가 Drawable 인터페이스를 구현하기만 하면 되므로 기존 코드를 수정할 필요 없이 쉽게 확장할 수 있다.

class Triangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("삼각형을 그립니다.");
    }
}

// DrawingApp에서 Triangle을 추가하는 예시
public class DrawingApp {
    public static void main(String[] args) {
        Drawable circle = new Circle();
        Drawable square = new Square();
        Drawable triangle = new Triangle();

        circle.draw();    // 출력: 원을 그립니다.
        square.draw();    // 출력: 사각형을 그립니다.
        triangle.draw();  // 출력: 삼각형을 그립니다.
    }
}

이처럼 다형성은 객체가 동일한 메시지에 대해 서로 다른 방식으로 응답할 수 있게 해주어, 코드의 재사용성유연성을 높인다. 이로 인해 새로운 요구사항이 생겨도 기존 코드를 최소한으로 수정하면서 기능을 추가할 수 있다.

2장: 객체지향 설계

🔓 캡슐화의 중요성

객체는 자신만의 데이터를 외부에서 직접 접근하지 못하도록 캡슐화해야 한다.

데이터는 객체의 행동(=메서드)을 통해서만 접근할 수 있으며, 이를 통해 객체의 상태를 보호하고 외부와의 결합도를 낮출 수 있다.

예를 들어, 은행 계좌 객체가 balance 필드를 외부에 직접 노출하지 않고, withdraw() 메서드를 통해서만 잔액을 조정할 수 있도록 설계하는 것이 캡슐화의 좋은 예이다. 이로 인해 시스템의 유지보수와 확장이 용이해지며, 객체의 상태 변경이 통제된다.

⚠️ 책임-주도 설계

책임-주도 설계는 객체가 어떤 책임을 가지고 있고, 그 책임을 수행하기 위해 어떤 협력이 필요한지를 중심으로 설계를 진행한다.

객체는 단순히 데이터를 보유한 존재가 아니라, 시스템의 일부로서 명확한 책임을 가지고 있는 행위 주체로 정의되어야 한다. 설계 과정에서는 각 객체가 맡을 책임을 먼저 결정하고, 이를 바탕으로 협력 관계를 형성하는 것이 중요하다.

코드 예시: 캡슐화의 예

public class BankAccount {
    // 잔액은 외부에서 바로 접근할 수 없도록 private으로 설정합니다.(은행 계좌에 돈이 들어 있지만, 그 돈을 직접 꺼낼 수 없도록 보호하는 방식)
    private double balance;

    // 계좌를 만들 때 시작 잔액을 설정
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // 잔액을 확인하는 메서드
    public double getBalance() {
        return balance;
    }

    // 잔액을 빼는(출금하는) 메서드. 출금할 때 잔액이 충분한지 확인합니다.
    public void withdraw(double amount) {
        if (amount > balance) {
            System.out.println("잔액이 부족합니다!");
        } else {
            balance -= amount;
            System.out.println("출금 완료: " + amount + " 남은 잔액: " + balance);
        }
    }
}

위 코드에서는 balance라는 변수를 외부에서 직접 접근하지 못하도록 private으로 설정했다. 잔액을 변경하려면 반드시 withdraw() 메서드를 사용해야 하며, 이를 통해 잔액이 부족한지 확인하고 안전하게 잔액을 조정한다. 이러한 방식으로 객체의 상태를 보호하고, 객체 외부의 직접적인 접근으로부터 안전성을 유지할 수 있다.

그림으로 한 눈에 보자면, 아래와 같다.

그림에서 원을 통해 내부의 비공개 데이터를 보호하고, 외부에서 접근 가능한 메서드를 통해서만 해당 데이터를 조작할 수 있다는 점이다.

3장: 역할, 책임, 협력

🤝 협력의 중요성

객체지향 시스템에서 중요한 것은 개별 객체가 아니라 객체 간의 협력이다.

객체는 혼자서 모든 문제를 해결하지 않는다. 서로 협력하며 역할을 수행하여 시스템 전체가 동작하게 된다. 예를 들어, 전자상거래 시스템에서 Order 객체는 Payment 객체와 협력하여 결제를 처리하고, Customer 객체와 협력하여 주문 정보를 관리한다. 객체 간의 협력을 통해 시스템의 복잡한 문제를 분산시켜 해결한다.

🔄 역할의 재사용성과 유연성

역할은 객체들이 동일한 행동을 수행할 수 있도록 하여 유연성을 높여준다.

여러 객체가 동일한 역할을 수행할 수 있다면, 시스템은 특정 객체에 종속되지 않고 유연하게 대체할 수 있다.

예를 들어, 인증 시스템에서 OAuthAuthenticatorBasicAuthenticator는 모두 Authenticator 역할을 수행할 수 있다. 이로 인해 시스템은 상황에 따라 적절한 인증 방식을 선택할 수 있으며, 확장과 유지보수가 쉬워진다.

4장: 설계 패턴

📐 디자인 패턴의 필요성

디자인 패턴은 객체지향 설계에서 자주 발생하는 문제를 해결하는 데 도움을 주는 재사용 가능한 솔루션이다.

반복적으로 마주치는 문제들을 해결하기 위해 디자인 패턴을 사용하면, 검증된 해결책을 재사용할 수 있다.

예를 들어, Observer 패턴은 객체의 상태 변화가 다른 객체들에게 자동으로 전파될 수 있도록 만들어 준다. 이 패턴은 GUI 시스템이나 이벤트 처리 시스템에서 자주 사용된다.

📝 책임-주도 설계와 패턴의 연계

책임-주도 설계를 통해 객체들의 책임을 명확히 정의한 후, 이를 바탕으로 디자인 패턴을 적용하면 더욱 견고한 시스템을 만들 수 있다.

패턴을 통해 객체 간의 협력을 명확히 하고, 책임을 분리하여 시스템의 구조를 명확하게 정의한다.

예를 들어, 복잡한 알고리즘을 여러 가지 전략으로 나누어 관리하는 Strategy 패턴을 사용하면, 객체의 행동을 다양하게 조합할 수 있다.

5장: 책임과 메시지

✉️ 메시지와 메서드의 차이

객체지향의 핵심은 객체들이 메시지를 주고받으며 협력한다는 것이다.

객체는 다른 객체에게 메시지를 보내어 특정 행동을 요청하며, 해당 메시지를 받은 객체는 이를 처리할 메서드를 실행하게 된다.

예를 들어, Printer 객체는 print() 메시지를 통해 출력을 요청받고, 이를 처리하는 메서드를 실행한다. 이로써 객체 간의 상호작용이 이루어지며, 각 객체는 자신이 받은 메시지에 적절히 응답하는 방식으로 행동한다.

// Printer 클래스 정의
public class Printer {
    // print 메서드 정의 (메시지를 받아 행동을 수행)
    public void print(String message) {
        System.out.println("출력: " + message);
    }
}

// Main 클래스에서 Printer 객체를 사용
public class Main {
    public static void main(String[] args) {
        // Printer 객체 생성
        Printer printer = new Printer();  // (객체 생성)

        // Printer 객체에 메시지를 보냄 (메시지 전달)
        printer.print("안녕하세요, 객체지향 세상!");  // (메시지)
    }
}

코드 설명:

  1. 객체 생성 (Printer printer = new Printer();)

    • 여기서 printerPrinter 클래스의 인스턴스
    • 객체: Printer는 행동을 수행할 수 있는 객체
  2. 메시지 전달 (printer.print("안녕하세요, 객체지향 세상!");)

    • printer 객체에게 print() 메시지를 보낸다.
    • "안녕하세요, 객체지향 세상!"은 프린터에게 전달된 내용이다.
    • 메시지: printer.print()가 바로 객체에게 보내는 메시지이다. 이는 특정 행동을 요청하는 것과 같다.
  3. 행동 수행 (print() 메서드)

    • print(String message) 메서드는 메시지를 받은 후, 해당 내용을 출력하는 역할을 한다.
    • 메서드: print()는 실제 행동을 수행하는 메서드이다. 이는 메시지를 받은 객체가 수행하는 구체적인 행동을 의미
  • 메시지: printer.print("안녕하세요, 객체지향 세상!");에서 print()는 객체에게 요청하는 행동이며 메시지입니다.
  • 객체: printer는 메시지를 받아 행동을 수행하는 객체입니다.

이 코드 예시에서 메시지는 객체에게 특정 행동을 요청하는 방식으로, Printer는 받은 메시지를 처리하여 화면에 출력한다. 이를 통해 객체들은 서로 협력하여 작업을 수행하게 된다.

🎭 다형성의 유연성

다형성을 사용하면 송신자는 수신자의 구체적인 타입을 알지 않고도 메시지를 보낼 수 있다. 송신자는 수신자가 누구인지에 관계없이 동일한 메시지를 보내고, 수신자는 자신의 방식으로 그 메시지에 응답하게 된다. 이러한 특성 덕분에 객체지향 시스템은 매우 유연하며, 새로운 타입의 객체가 추가되더라도 기존 코드를 수정할 필요 없이 시스템에 통합할 수 있다.

🏗️ 6장: 객체 지도

🏗️ 기능과 구조의 구분

시스템을 설계할 때 기능구조를 분리하는 것이 중요하다. 기능은 시스템이 제공해야 하는 서비스이고, 구조는 그 기능을 어떻게 제공할지를 정의한다.

기능 중심으로 설계하면 시스템이 변화에 취약해지지만, 안정적인 구조를 중심으로 기능을 설계하면 변화에 더 강한 시스템을 만들 수 있다.

예를 들어, 고객 정보 관리 시스템에서는 고객의 정보를 저장하고 검색하는 기능이 고객 객체의 구조에 종속되어야 하며, 이를 통해 기능과 구조가 안정적으로 유지된다.

🌐 도메인 모델링

도메인 모델은 시스템이 해결해야 할 문제를 단순화하여 표현한 것이다. 도메인 모델을 기반으로 객체들을 설계하면, 문제를 명확히 이해하고 시스템을 더 직관적으로 구현할 수 있다.

예를 들어, 병원 관리 시스템에서는 환자, 의사, 예약 등이 중요한 도메인 개념이며, 이를 모델링하여 시스템의 구조를 정의한다. 도메인 모델은 기능의 추가와 변경에도 강한 구조를 제공하여, 시스템이 변화에 더 잘 대응할 수 있도록 한다.

🔍 7장: 함께 모으기

🔍 설계의 세 가지 관점

객체지향 설계는 도메인, 명세, 구현의 세 가지 관점에서 바라봐야 한다. 도메인 관점에서는 문제를 정의하고, 명세 관점에서는 객체들이 상호작용하는 방식과 인터페이스를 정의한다. 구현 관점에서는 이 명세를 바탕으로 실제로 동작하는 코드를 작성한다.

세 가지 관점은 서로 긴밀하게 연결되어야 하며, 설계의 어느 한 부분에서 변경이 생겨도 다른 관점들이 쉽게 대응할 수 있어야 한다.

🔄 유연한 설계

유연한 설계를 위해 메시지 우선 설계를 사용한다. 메시지를 먼저 결정하고, 그 메시지를 수행할 객체를 나중에 선택함으로써 객체의 인터페이스와 구현을 분리하고 유연성을 높인다.

이를 통해 설계의 유연성을 극대화하고, 변경에 강한 시스템을 만든다. 이는 특히 시스템이 진화하고 기능이 추가될 때 유리하며, 객체 간 결합도를 줄여 유지보수를 용이하게 만든다.

출처: '객체지향의 사실과 오해' – 저자 조영호

profile
내 지식을 기록하여, 다른 사람들과 공유하여 함께 발전하는 사람이 되고 싶다. gitbook에도 정리중 ~

0개의 댓글