Skip to main content

10장 클래스

1. 📌 핵심 개념 정리

✅ 요약하기

1. 클래스 체계
  • 클래스를 정의하는 표준 자바 관례에 따르면, 가장 먼저 변수 목록이 나온다.

  • 정적 공개 상수가 있다면 맨 처음에 나온다.

  • 다음으로 정적 비공개 변수가 나오며, 이어서 비공개 인스턴스 변수가 나온다.

  • 공개 변수가 필요한 경우는 거의 없다.

  • 변수 목록 다음에는 공개 함수가 나온다.

  • 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다. 즉, 추상화 단계가 순차적으로 내려간다.

  • 프로그램은 신문기사처럼 읽힌다.

  • 캡슐화

    • 클래스 내 선언된 변수, 함수 등은 외부에서 접근 못하게 private 선언으로 숨기는 것을 의미한다.
    • 변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다.
    • 때로는 변수나 유틸리티 함수를 protected로 선언해 테스트 코드에 접근을 허용하기도 한다.
    • 같은 패키지 안에서 테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 그 함수나 변수를 protected로 선언하거나 패키지 전체로 공개한다.
    • 하지만 그 전에 비공개 상태를 유지할 온갖 방법을 강구한다. 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.

2. 클래스는 작아야 한다!
  • 클래스 설계의 기본 원칙

    • 클래스는 작아야 한다.
    • 클래스를 설계할 때도 함수와 마찬가지로 "작게"가 기본 규칙이다.
  • 클래스 크기의 척도: 책임(Responsibility)

    • 클래스가 맡은 책임을 세어보라.
    • 클래스 이름은 해당 클래스의 책임을 명확히 설명해야 한다.
    • 간결한 이름이 떠오르지 않는다면, 클래스가 너무 크다는 신호다.
    • 클래스 설명은 if, and, or, but 없이 25단어 내외로 작성해야 한다.
    • 클래스 이름이 모호하다면, 클래스의 책임이 너무 많다는 뜻이다.
  • 모호한 클래스 이름이 의미하는 것

    • 클래스 이름에 Processor, Manager, Super 같은 애매한 단어가 포함되었다면,
      • 여러 책임을 떠안고 있다는 증거다.
    • 적은 수의 거대한 클래스보다, 많은 수의 작은 클래스를 지향하라.
  • 작은 클래스가 많은 시스템 vs. 큰 클래스 몇 개뿐인 시스템

    • 두 시스템 모두 돌아가는 부품의 개수는 비슷하다.
    • 하지만 규모가 커질수록 논리와 복잡성이 증가한다.
    • 복잡성을 다루려면 체계적인 정리가 필수다.
      • 그래야 개발자가 필요한 부분을 빠르게 찾을 수 있다.
      • 그래야 변경 시 직접 영향이 미치는 컴포넌트만 이해하면 된다.
    • 거대한 다목적 클래스 몇 개로 구성된 시스템은 변경이 어려워진다.
      • 불필요한 정보까지 이해해야 하고, 코드 분석이 더욱 힘들어진다.
    • 결론: 작은 클래스를 많이 만들어 체계적으로 구성하라!

3. 단일 책임 원칙 (SRP)
  • 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.
  • SRP는 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다.
    • 클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미다.
  • 객체 지향 설계에서 가장 중요한 규칙이지만, 가장 무시되는 규칙 중 하나이다.
  • 책임, 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다. 더 좋은 추상화가 더 쉽게 떠오른다.
  • 작은 클래스 많이 두기
    • 큰 서랍 몇 개에 모두 던져 넣기보다는 작은 서랍 많이 두고 쓸모에 맞게 정리하여 넣기.

4. 응집도 (Cohesion)
  • 클래스와 응집도

    • 클래스는 인스턴스 변수 수가 적어야 한다.
    • 각 메서드는 클래스의 인스턴스 변수를 하나 이상 사용해야 한다.
    • 일반적으로 메서드가 변수를 더 많이 사용할수록 응집도가 높아진다.
    • 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 최고로 응집도가 높은 클래스다.
  • 응집도가 높은 클래스의 중요성

    • 응집도가 높다는 것은 클래스의 메서드와 변수가 서로 밀접하게 연관된다는 의미다.
    • 응집도가 높을수록 클래스는 논리적인 단위로 깔끔하게 정리된다.
    • 하지만 너무 높은 응집도는 현실적으로 구현하기 어렵고, 바람직하지 않을 수도 있다.
  • 클래스를 쪼개야 하는 신호

    • 작은 함수짧은 매개변수 목록을 유지하려다 보면
      • 특정 메서드에서만 사용하는 인스턴스 변수가 많아지는 경우가 있다.
    • 이는 새로운 클래스로 분리해야 한다는 신호다.
    • 응집도를 유지하려면, 관련된 변수와 메서드를 적절히 분리해 새로운 클래스로 나눈다.
  • 큰 함수를 작은 함수로 나눌 때의 문제

    • 예를 들어, 여러 변수를 사용하는 큰 함수가 있다고 가정하자.
    • 일부 로직을 새로운 함수로 분리하고 싶지만, 4개의 변수를 사용하고 있다.
    • 이 4개의 변수를 새로운 함수의 매개변수로 넘겨야 할까?
      • 아니다! 대신 이 변수를 클래스의 인스턴스 변수로 승격하면 매개변수 없이 사용할 수 있다.
      • 하지만 이렇게 하면 클래스의 응집력이 낮아질 위험이 있다.
  • 응집력을 유지하는 방법

    • 클래스가 응집력을 잃는다면 쪼개라!
    • 큰 함수를 작은 함수 여러 개로 나누다 보면 작은 클래스로도 쪼갤 기회가 생긴다.
    • 그 결과, 프로그램 구조가 더 체계적이고 가독성이 높아진다.

5. 변경하기 쉬운 클래스
  • 대다수 시스템은 지속적인 변경이 가해진다. 그리고 뭔가 변경할 때마다 시스템이 의도대로 동작하지 않을 위험이 따른다. 깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.

  • 코드 수정 시 건드릴 코드가 최소인 시스템 구조가 바람직하다.

  • 변경으로부터 격리

    • 요구사항은 변하기 마련이고, 이에 따라 코드도 변한다.
    • 상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다.
    • 그래서 우리는 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.
    • 상세한 구현에 의존하는 코드는 테스트가 어렵다.
    • 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다.
    • 결합도가 낮다는 소리는 각 시스템 요소가 다른 요소로부터 그리고 변경으로부터 잘 격리되어 있다는 의미다. 시스템 요소가 서로 잘 격리되어 있으면 각 요소를 이해하기도 더 쉬워진다.
    • 결합도를 최소로 줄이면 자연스럽게 DIP(의존성 역전 원칙)를 따르는 클래스가 나온다. 본질적으로 DIP는 클래스가 상세 구현이 아닌 추상화에 의존해야 한다는 원칙이다.

2. 🤔 이해가 어려운 부분

🔍 질문하기

1. 결합도
  • 어려웠던 부분: 결합도란 무엇을 의미하고 결합도를 낮추는 예시 살펴보기
  • 이해한 점
    • 결합도(Coupling)는 한 모듈(클래스, 함수)이 다른 모듈과 얼마나 강하게 연결되어 있는지를 나타내는 개념입니다.
      • 높은 결합도 (High Coupling): 한 모듈이 다른 모듈의 내부 구현에 직접 의존하는 경우, 변경이 어렵고 유지보수 비용이 증가합니다.
        class PayPalPaymentProcessor {
            void processPayment(double amount) {
                System.out.println("Processing payment of $" + amount + " via PayPal");
            }
        }
        
        class OrderService {
            private PayPalPaymentProcessor paymentProcessor;
        
            public OrderService() {
                this.paymentProcessor = new PayPalPaymentProcessor(); // 직접 의존
            }
        
            public void checkout(double amount) {
                paymentProcessor.processPayment(amount);
            }
        }
        
      • 낮은 결합도 (Low Coupling): 한 모듈이 다른 모듈과 독립적으로 동작할 수 있도록 인터페이스 또는 추상화 계층을 두는 방식으로 결합을 줄입니다.
        // 추상화 계층 추가
        interface PaymentProcessor {
            void processPayment(double amount);
        }
        
        // PayPal 구현
        class PayPalPaymentProcessor implements PaymentProcessor {
            @Override
            public void processPayment(double amount) {
                System.out.println("Processing payment of $" + amount + " via PayPal");
            }
        }
        
        // Stripe 구현
        class StripePaymentProcessor implements PaymentProcessor {
            @Override
            public void processPayment(double amount) {
                System.out.println("Processing payment of $" + amount + " via Stripe");
            }
        }
        
        // OrderService는 구체적 클래스가 아니라 인터페이스에 의존
        class OrderService {
            private final PaymentProcessor paymentProcessor;
        
            public OrderService(PaymentProcessor paymentProcessor) {
                this.paymentProcessor = paymentProcessor; // 인터페이스에 의존
            }
        
            public void checkout(double amount) {
                paymentProcessor.processPayment(amount);
            }
        }
        

2. 변경으로부터 격리
  • 어려웠던 부분: 책에서 말하는 구체적인 클래스에서 상세한 구현에 의존하는 클래스가 어떤 클래스를 말하는 건지 이해하기 어려웠다.
  • 이해한 점: 인터페이스나 추상 클래스없이 작성한 클래스를 구체적인(Concrete) 클래스라고 말한다.
    • 예시
      public class Driver {
          private Car car = new Car();
      }
      
      이 경우 Car의 동작이 바뀌면 Driver에도 영향을 미친다.
    • 따라서 다음과 같이 코드를 개선한다:
      public interface Vehicle {
          void drive();
      }
      
      public class Car implements Vehicle {
          @Override
          public void drive() {
              System.out.println("자동차를 운전한다.");
          }
      }
      
      public class Driver {
          private Vehicle vehicle;
      
          public Driver(Vehicle vehicle) {
              this.vehicle = vehicle;
          }
      
          public void drive() {
              vehicle.drive();
          }
      }
      

3. 응집도를 판단하는 기준
  • 응집도(Cohesion)를 판단 기준
    • 모듈 내부의 기능들이 얼마나 밀접하게 관련되어 있는가?
    • 변경의 이유 서로 연관성이 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐 있는가?
    • 모듈이 명확하고 일관된 책임을 가지고 있는가?
    • 메서드 수준, 함수 수준, 모듈 수준.
  • 응집도와 대비되는 개념으로 결합도(Coupling)가 있다.
  • 결합도는 소프트웨어 시스템에서 모듈 간의 상호 의존도를 측정한 것으로, 모듈이 밀접하게 연결되어 있는 정도를 나타낸다.
  • 결합도가 높으면 변경하고 검토해야 하는 모듈 수가 많아지는 단점이 있다.

3. 📚 참고 사항

📢 논의하기

1. 관련 자료 공유