[8/8] Java 기본 Summary ( w. 인프런 김영한)

차재현·2025년 1월 9일
0

[ Backend Study - Java ]

목록 보기
9/11
post-thumbnail

Section 13. 다형성과 설계

Chapter 1. 좋은 객체 지향 프로그래밍이란?

  • 객체 지향의 특징
    • 추상화
    • 캡슐화
    • 상속
    • 다형성 → 제일 중요
  • 다형성의 실세계 비유
    • 실세계와 객체 지향은 모든것을 1:1로 매칭할 수 없다.
    • 역할”과 “구현”으로 세상을 구분
  • 역할과 구현을 분리
    • 역할과 구현으로 구분하면, 세상이 단순해지고 유연해지며 변경도 편리해진다
    • 클라이언트는 역할(인터페이스)만 알면 된다
    • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다
    • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다
    • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
  • 자바 언어의 다형성을 활용
    • 역할 == 인터페이스
    • 구현 == 인터페이스를 구현한 클래스, 구현 객체
    • 객체 설계 시 역할과 구현을 명확히 분리해야 한다
  • 객체의 협력이라는 관계부터 생각
    • 혼자 있는 객체는 없다
    • 클라이언트 : 요청 | 서버 : 응답
  • 다형성의 본질
    • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
  • 역할과 구현 분리의 한계점
    • 역할(인터페이스) 자체가 변하면 클라이언트, 서버 모두에 큰 변경이 발생한다.
    • 따라서 인터페이스를 안정적으로 잘 설계하는 것이 중요하다.

💡 정리

  • 다형성이 가장 중요하다!
  • 디자인 패턴 대부분은 다형성을 활용하는 것이다!
  • 스프링의 핵심인 IoC, DI도 결국 다형성을 활용하는 것이다!
  • 스프링을 사용하면 마치 레고 블럭 조립하듯이 구현을 편리하게 변경할 수 있다!

Chapter 2. 다형성 - 역할과 구현 예제 1

  • 역할과 구현을 분리하지 않고 단순하게 코드를 개발해보면 어떨까?
  • 예시로 Driver와 K3Car, Model3Car를 작성해보자
    • 클라이언트 = Driver

    • 서버 = K3Car, Model3Car

      package poly.car0;
      
      public class K3Car {
      
          public void startEngine() {
              System.out.println("K3Car.startEngine");
          }
      
          public void offEngine() {
              System.out.println("K3Car.offEngine");
          }
      
          public void pressAccelerator() {
              System.out.println("K3Car.pressAccelerator");
          }
      }
      package poly.car0;
      
      public class Model3Car {
      
          public void startEngine() {
              System.out.println("Model3Car.startEngine");
          }
      
          public void offEngine() {
              System.out.println("Model3Car.offEngine");
          }
      
          public void pressAccelerator() {
              System.out.println("Model3Car.pressAccelerator");
          }
      }
      package poly.car0;
      
      public class Driver {
      
          private K3Car k3Car;
          private Model3Car model3Car; //추가
      
          public void setK3Car(K3Car k3Car) {
              this.k3Car = k3Car;
          }
      
          //추가
          public void setModel3Car(Model3Car model3Car) {
              this.model3Car = model3Car;
          }
      
          public void drive() {
              System.out.println("자동차를 운전합니다.");
              if (k3Car != null) {
                  k3Car.startEngine();
                  k3Car.pressAccelerator();
                  k3Car.offEngine();
              } else if (model3Car != null) {
                  model3Car.startEngine();
                  model3Car.pressAccelerator();
                  model3Car.offEngine();
              }
          }
      
      }
    • 클래스(Driver) 상에서, 다른 클래스를 알고있다면(여기선 K3Car, Model3Car 이다),
      다른 클래스를 의존하고 있다고 칭한다.
      ⭐ 클라이언트에서는 의존성을 인터페이스에 집중해야한다!

      package poly.car0;
      
      public class CarMain0 {
      
          public static void main(String[] args) {
              Driver driver = new Driver();
              K3Car k3Car = new K3Car();
              driver.setK3Car(k3Car);
              driver.drive();
      
              //추가
              Model3Car model3Car = new Model3Car();
              driver.setK3Car(null);
              driver.setModel3Car(model3Car);
              driver.drive();
          }
      }
    • 위 코드를 보면, 자동차가 늘어날 수록 Driver의 코드가 엄청나게 변경된다.

      • 멤버 변수도 추가 선언해야 하고
      • 자동차를 추가하는 메서드도 추가 선언해야 하고
      • drive 메서드에 새로 운전할 자동차에 대한 메서드들도 추가해야 한다.
    • 심지어 main 메서드에서도 driver가 새로운 자동차를 운전하려면
      set 메서드, 운전메서드를 중복으로 사용해야 한다.

  • 이렇게 역할과 구현을 분리하지 않으면 새로운 데이터 추가마다 코드에 너무 많은 변경이 일어나게 된다.
    다음 챕터에서 역할과 구현을 잘 분리하여 데이터 추가가 일어나도 변경이 적게 일어나도록 코드를 짜보자.

Chapter 3. 다형성 - 역할과 구현 예제 2

  • 역할(인터페이스)과 구현(클래스, 객체)을 분리해 보자
    • 클라이언트 = Driver
    • 서버
      • 역할 = Car
      • 구현 = K3Car, Model3Car
  • 이제 다시 코드를 짜보자
    package poly.car1;
    
    public interface Car {
        void startEngine();
        void offEngine();
        void pressAccelerator();
    }
    package poly.car1;
    
    public class Model3Car implements Car {
        @Override
        public void startEngine() {
            System.out.println("Model3Car.startEngine");
        }
    
        @Override
        public void offEngine() {
            System.out.println("Model3Car.offEngine");
        }
    
        @Override
        public void pressAccelerator() {
            System.out.println("Model3Car.pressAccelerator");
        }
    }
    package poly.car1;
    
    public class K3Car implements Car {
        @Override
        public void startEngine() {
            System.out.println("K3Car.startEngine");
        }
    
        @Override
        public void offEngine() {
            System.out.println("K3Car.offEngine");
        }
    
        @Override
        public void pressAccelerator() {
            System.out.println("K3Car.pressAccelerator");
        }
    }
    package poly.car1;
    
    public class Driver {
    
        private Car car;
    
        public void setCar(Car car) {
            System.out.println("자동차를 설정합니다: " + car);
            this.car = car;
        }
    
        public void drive() {
            System.out.println("자동차를 운전합니다.");
            car.startEngine();
            car.pressAccelerator();
            car.offEngine();
        }
    }
    • ⭐ 클라이언트(Driver)에서 의존성을 인터페이스(Car)에 집중시켜서 코드 간소화 성공!
      → 클라이언트가 “구현’에 의존하지말고, “역할(인터페이스)”에 의존하게 해야 한다!

      package poly.car1;
      
      public class CarMain1 {
          public static void main(String[] args) {
              Driver driver = new Driver();
      
              //차량 선택(k3)
              K3Car k3Car = new K3Car();
              driver.setCar(k3Car);
              driver.drive();
      
              //차량 변경(k3 -> model3)
              Model3Car model3Car = new Model3Car();
              driver.setCar(model3Car);
              driver.drive();
          }
      }
    • Driver 클래스를 보면, 이제 K3Car, Model3Car를 알 필요 없이 Car라는 역할만 알고 있으면 된다.

    • 동시에 새로운 자동차가 추가되어도 main 메서드에서만 코드가 변경될 뿐이지, Driver 클래스의 코드에서는 변경되는것이 아예 없어졌다.

Chapter 4. OCP(Open-Closed Principle) 원칙

  • Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다.
    • main 메서드, 새로운 구현 대상(클래스, 객체)
  • Closed for modification : 기존의 코드는 수정되지 않아야 한다.
    • 클라이언트, 역할(인터페이스)

🔥 우리는 Chapter 3에서 역할과 구현을 분리하여 클라이언트와 서버 관계를 충족하는 코드를 작성해보았다.

  • Chapter 3 코드에서 새로운 차량(Model3Car)이 추가되어도
    • 클라이언트(Driver)와 역할(Car)의 코드는 변경되지 않았다.
      → Closed for modification 충족
    • Model3Car라는 구현 대상을 생성 및 main 메서드에서 차량 설정 및 운전 코드를 생성했다.
      → Open for extension 충족

즉, 역할과 구현을 분리하여 클라이언트와 서버 관계를 충족시킨다면 OCP 원칙을 지키는 것이다!!

➕ 전략 패턴(Strategy Pattern)
디자인 패턴 중에 가장 중요한 패턴을 하나 뽑으라고 하면 전략 패턴을 뽑을 수 있다. 전략 패턴은 알고리즘을
클라이언트 코드의 변경 없이 쉽게 교체할 수 있다. 방금 설명한 코드가 바로 전략 패턴을 사용한 코드이다.

Car 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 차량이 전략의 구체적인 구현이 된다.
인터페이스를 통해 구현들을 클라이언트 코드( Driver )의 변경 없이 손쉽게 교체할 수 있다.

Chapter 5. 다형성 문제와 풀이

  • 다형성을 적용하며 OCP 원칙을 준수하기 위해서는 → 인터페이스를 사용하는 “클라이언트”의 코드 변경을 최소화해야 한다
    ( 클라이언트와 인터페이스간의 “의존성”을 확인해보면 좋다 )

문제 1. 다중 메세지 발송

  • 클라이언트 : 없음 (문제 조건으로 변경 불가능)
  • 서버
    • 인터페이스 : Sender (문제 조건으로 구현 필요)
    • 구현 : EmailSender, SmsSender, FacebookSender

문제 2. 결제 시스템 리팩토링

(문제 내용 : “김영한의 Java 기본 - 12. 다형성과 설계” PDF 참고)

  • 클라이언트 : PayService (전략 패턴을 위해 변경 최소화)
  • 서버
    • 인터페이스 : Pay
    • 구현 : KakaoPay, NaverPay

🌠 (중요!!) 다형성&OCP원칙을 고려한 문제 풀이 방법

  1. PayService에서 KakaoPay, NaverPay를 의존하고 있다.
    결과적으로 클라이언트가 인터페이스를 의존하게 해야한다.

  2. if-else 문에서 결제 방법 확인 및 결제작업(.pay())을 같이 하고 있다.

    코드 덩어리에 2개 이상의 기능이 섞여있다면, 분리해보는 시도를 해보자

  3. 혹시 모르니 중간마다 같은 결과를 출력하는지 실행해보도록 하자

    코드를 변경할 때 마다, 의도에 맞게 돌아가는지 항상 테스트 해봐야 한다.

➕ Null Object Pattern

→ 예외인 Null 값도 아예 하나의 경우로 미리 정해두어 코드에서 null을 다루지 않게 한다!

  • 설명
    • 준비해둔 값 외의 다른 Pay가 들어오면, 우리는 null을 반환하여 구별하고 있다.
      이때, Null을 반환하는 경우를 구현으로서 하나 만들어서 경우를 대비한다.
    • 이렇게하면 코드에서 null을 다루지 않기에 NullPointerException을 예방할 수 있고,
      Pay라는 인터페이스에 개발자의 제어 하에 있는 확실한 값이 들어오게 된다.

"김영현의 실전 Java - 기본" 강의 수료를 마치며..

프로젝트를 진행하며 항상 Java의 기본기가 완벽하지 않다는 느낌을 받았습니다.
해당 강의를 통해 Java의 기본기를 다질 수 있었고, 주기적인 복습으로 추후 프로젝트에서 백엔드 Java 개발시에 설계 및 이슈 해결하는데 많은 도움이 될 것이라 생각합니다.

더 나은 백엔드 개발자 차재현이 되도록 노력하겠습니다!

profile
Develop what? and why?

0개의 댓글