객체 지향 프로그래밍?

sun·2024년 1월 15일
0

java

목록 보기
2/4

객체지향 프로그래밍 언어의 특성? (OOP)

  • 프로그램을 객체라고 불리는 독립적인 단위로 나누고, 이러한 객체들 간의 상호작용을 중심으로 소프트웨어를 설계하고 구현하는 방식.

👉 캡슐화 (Encapsulation)

코드의 안정성, 코드의 재사용성, 유지보수

  • 데이터와 해당 데이터를 처리하는 메서드를 하나의 단위인 클래스로 묶는 것.
  • 데이터 은닉 : 객체의 내부 상태를 숨기고 보호함으로써 외부에서 접근을 제한하여 정의된 메서드를 통해 상호작용하는 것으로 데이터의 무결성을 유지.
  • 접근 제어 : 일부 데이터나 외부에서 접근할 수 없도록 비공개(private)로 설정가능.
    public, protected, default, private에 따라 객체의 상태를 변경하고 조회할 수 있음.
    보안강화. 유저들에게 필요한 정보를 보여줌
  • 인터페이스 제공 : 캡슐화를 통해 공개된 메서드를 통해 외부와 상호작용.
    객체의 내부 구현을 외부에 노출하지 않고 사용자에게 명확한 인터페이스를 제공가능.
    객체의 내부구현을 수정하거나 개선할 때, 인터페이스를 변경하지 않고 가능. (외부에서는 유지됨)
    인터페이스를 다른 코드에 사용할 때 인터페이스를 수정하지 않고도 객체의 내부 구현 변경 가능.
    이로 인해 코드의 안정성과 재사용성을 높일 수 있음.

<캡슐화 코드예시>

// 하나의 소스파일에는 public class 또는 public interface 중 하나만 존재해야함.

// 인터페이스 정의
interface Shape { // <인터페이스 제공>
    double getArea();  // 도형의 넓이를 계산하는 메서드
    double getPerimeter();  // 도형의 둘레를 계산하는 메서드
}

// Circle 클래스가 Shape 인터페이스를 구현
class Circle implements Shape {
    private double radius; // <데이터 은닉>

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

// Rectangle 클래스가 Shape 인터페이스를 구현
class Rectangle implements Shape {
    private double width; // _데이터 은닉
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }

    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

public class Main {
    public static void main(String[] args) {
        // Circle 객체 생성 및 사용
        Circle circle = new Circle(5.0);
        System.out.println("Circle Area: " + circle.getArea());
        System.out.println("Circle Perimeter: " + circle.getPerimeter());

        // Rectangle 객체 생성 및 사용
        Rectangle rectangle = new Rectangle(4.0, 6.0);
        System.out.println("Rectangle Area: " + rectangle.getArea());
        System.out.println("Rectangle Perimeter: " + rectangle.getPerimeter());
    }
}

👉 상속 (Inheritance)

코드의 재사용성, 유지보수, 확장성, 계층구조 구축

  • 부모 클래스로부터 속성과 메서드를 물려받아 새로운 자식 클래스를 생성하는 개념.
  • 자식 클래스는 부모 클래스로부터 상속받은 멤버(필드,메서드)를 확장하거나 재정의(Override)하여 사용 가능하며 부모 클래스의 모든 멤버 사용 가능.
  • 자식 클래스는 부모 클래스의 특성을 가지고 있으므로 자식 클래스는 부모 클래스의 인스턴스로 취급될 수 있음. (자식 클래스의 객체를 부모 클래스의 객체처럼 사용할 수 있다는 뜻)
  • 자식 클래스에서 부모 클래스를 참조하거나 호출할 때 super 키워드 사용하여 접근.
  • 부모 클래스의 생성자는 자식 클래스에서 super()를 사용하여 명시적으로 호출 가능.
  • 장점
    부모 클래스에서 확장하여 자식클래스를 만들기 때문에 확장성에 좋음.
    변경사항이 있을 때 부모 클래스만 변경하면 되기 때문에 유지보수에 좋음.
  • 단점
    부모 클래스에서는 자식 클래스에 있는 것을 사용하지 못함.
    필요없는 것도 상속 받을 수 있음.
  • 단점보완
    1) 부모 클래스를 좀 더 세분화하여 여러 계층으로 나누고 상속받지 않아도 되는 것을 상속받게되는 현상을 줄임.
    2) 느슨한 인터페이스를 사용 (내가 정의한 것만 사용 가능하며 다른 곳에서도 사용 가능)
    3) 컴포지션 사용

<상속 코드예시>

class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name); // 슈퍼 클래스의 생성자 호출
    }

    // 슈퍼 클래스의 메서드를 재정의
    @Override
    public void speak() {
        System.out.println(name + " barks");
    }

	// 메서드 확장
    public void wagTail() {
        System.out.println(name + " wags its tail");
    }
}

❔ 컴포지션(Composition) : 객체 지향 프로그래밍의 디자인 원칙 중 하나.
한 클래스가 다른 클래스의 객체를 포함하여 그 기능을 확장하는 방식.
즉, 한 클래스가 다른 클래스를 소유함.
한 클래스의 기능을 다른 클래스에서 재사용할 수 있음.
클래스들이 서로 독립적이며 변경에 따른 영향이 적어 시스템의 유연성과 확장성이 증가하여 낮은 결합도를 가짐.
객체의 관계가 명확해져 코드 이해와 유지보수가 용이함.

public class Engine { // 'Engine 클래스를 정의. 이 클래스는 엔진과 관련된 기능을 가지고 있음.
    public void start() {
        System.out.println("엔진이 시작됩니다.");
    }

    public void stop() {
        System.out.println("엔진이 멈춥니다.");
    }
}

//--------------------------------------------------------------

public class Car {
    private Engine engine; // Engine 객체를 멤버 변수로 포함

    public Car() {
        engine = new Engine(); // Car 객체가 생성될 때 Engine 객체도 생성
    }

    public void startCar() {
        engine.start(); // Engine 클래스의 start 메서드를 호출
        System.out.println("자동차가 출발합니다.");
    }

    public void stopCar() {
        engine.stop(); // Engine 클래스의 stop 메서드를 호출
        System.out.println("자동차가 멈춥니다.");
    }
}

//--------------------------------------------------------------

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car(); // Car 객체 생성
        myCar.startCar(); // 자동차를 출발시킵니다.
        myCar.stopCar(); // 자동차를 멈춥니다.
    }
}

👉 다형성 (Polymorphism)

코드의 안정성, 코드의 재사용성, 코드의 유연함, 유지보수, 확장성

  • 같은 이름의 메서드나 연산자가 다른 형태의 데이터 타입을 다룰 수 있도록 하는 능력.
    즉, 같은 메서드로 다른 행동을 할 수 있는 것.
  • 메서드 다형성 : 메서드 Override과 메서드 Overload.
  • 인터페이스 다형성 : 다양한 클래스들이 동일한 인터페이스를 구현하면 해당 인터페이스를 사용하는 코드에서는 클래스의 구체적인 타입을 몰라도 인터페이스를 통해 메서드를 호출 가능.
  • 객체 다형성 : 객체는 여러 형태를 가질 수 있음.
    부모 클래스 타입으로 선언된 객체는 자식 클래스의 인스턴스를 참조할 수 있으며, 이를 통해 다양한 객체를 동일한 타입으로 처리가능.
  • 다형성을 이용하지 않으면 자식 클래스들을 묶을 수 있는 방법이 없음.
    묶을 수 있다는 것의 이점은 세 가지 메서드를 각각 다 작성하지 않아도 되며 코드의 중복성을 줄여줌.
    ex)
List<Parent> parent = new ArrayList<>();
parent.add(new Child1());
parent.add(new Child2());
parent.add(new Child3());

List<Child1> child1
List<Child2> child2
List<Child3> child3
  • 코드가 상위 클래스나 인터페이스에 의존하면, 새로운 하위 클래스를 도입하거나 기존 클래스를 변경할 때 다른 부분의 코드에 미치는 영향을 최소화. 즉, 코드의 변경 범위를 최소한으로 제한할 수 있어 유지보수가 더 쉬움.
  • 객체 지향 설계 원칙 중 하나인 "느슨한 결합"과 관련있음.동적 바인딩과 인터페이스를 활용하여 런타임에 다양한 객체 타입을 다룰 수 있음.
  • 객체 지향 설계 원칙 중 하나인 "인터페이스 분리 원칙"과 관련있음.

<다형성 코드예시>

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Dog barks");
    }

    void wagTail() {
        System.out.println("Dog wags its tail");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Cat meows");
    }

    void purr() {
        System.out.println("Cat purrs");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        Animal myAnimal = new Dog(); // 다형성: 부모 클래스 타입으로 자식 클래스 객체 생성
        myAnimal.makeSound(); // 다형성을 통해 Dog의 makeSound() 메서드 호출
        // wagTail() 메서드를 호출하려면 Dog 타입으로 명시적으로 캐스팅 해야함.
        // Dog myDog = (Dog) myAnimal;

        myAnimal = new Cat(); // 다형성: 부모 클래스 타입으로 다른 자식 클래스 객체 생성
        myAnimal.makeSound(); // 다형성을 통해 Cat의 makeSound() 메서드 호출
    }
}

👉 추상화 (Abstraction)

코드의 안정성, 코드의 재사용성, 확장성, 가독성, 복잡성 감소, 낮은 결합도, 유지보수

  • 추상 클래스와 인터페이스를 사용하여 공통된 특성과 동작을 추상화하고, 구체적인 구현은 하위 클래스에서 제공.
  • 복잡성 감소 : 불필요한 세부 사항을 신경 쓰지 않고 핵심 개념에 집중 가능.
  • 모델링 : 클래스, 인터페이스, 메서드 형태로 표현.
  • 추상 데이터 타입(ADT) : 추상화는 데이터와 해당 데이터를 처리하는 연산을 묶어서 추상 데이터 타입을 정의.
    ADT는 데이터의 논리적 구조와 동작을 정의하고 실제 구현 내용은 숨기고 인터페이스만 노출.
  • 인터페이스와 구현 분리 : 인터페이스와 구현을 분리하여 다른 객체와 상호작용하는 방식을 명확하게 정의.
    객체 지향 설계 원칙 중 하나인 "인터페이스 분리 원칙"과 관련있음.
  • 재사용성과 확장성
  • 추상 클래스와 인터페이스 : 추상 클래스는 일부 메서드가 구현되어 있지 않은 클래스, 인터페이스는 메서드의 시그니처만 정의하고 구현은 하위 클래스에서 이루어짐.
  • 추상화 시 사용하는 곳에서 무조건 구현해야 한다난 강제성이 있음.
  • 추상 클래스는 단일 상속이기 때문에 필요없는 멤버를 상속받을 수 있음.
  • 인터페이스는 서로 다른 클래스들이 인터페이스를 구현할 수 있으며 다양한 객체를 참조할 수 있음.
    이는 유연한 코드 디자인을 가능하게 함.
  • 인터페이스를 사용하면 새로운 기능을 추가하거나 기존 코드를 수정할 때 코드 변경의 영향을 최소화함.
    새로운 인터페이스 구현을 통해 기존 시스템에 새로운 기능을 쉽게 추가 가능.

상속과 인터페이스 차이

상속은 계층적 구조를 가지고 있으며 단일상속이다. (extends)
확장성은 좋지만 부모에게 필요없는 멤버를 물려받을 수 있다.
부모의 메서드를 자식이 재정의하는 오버라이딩이 가능하며 자식에서 부모의 멤버를 사용할 수 있다.
인터페이스는 추상적이며 메서드의 선언부만 있고 구현부는 없으며 다중구현이다. (implements)
인터페이스에 있는 것들은 이를 사용하는 클래스에서 반드시 구현해야 한다는 강제성이 있다.
반드시 필요한 것들만 정의하여 사용하기 때문에 필요없는 코드가 존재하지 않는다.
상속보다 결합도가 낮다.

<추상화 예시코드>

// 추상 클래스: Shape
abstract class Shape {
    // 멤버 변수는 없음

    // 추상 메서드: 도형의 넓이를 계산하는 메서드
    abstract double calculateArea();
}

// 구체적인 도형 클래스: Circle
class Circle extends Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double calculateArea() {
        return Math.PI * radius * radius;
    }
}

// 구체적인 도형 클래스: Rectangle
class Rectangle extends Shape {
    double length;
    double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double calculateArea() {
        return length * width;
    }
}

public class AbstractionExample {
    public static void main(String[] args) {
        // Shape 타입의 배열을 생성하고 다양한 도형 객체를 저장
        Shape[] shapes = new Shape[2];
        shapes[0] = new Circle(5.0);
        shapes[1] = new Rectangle(4.0, 3.0);

        // 배열을 순회하면서 각 도형의 넓이를 출력
        for (Shape shape : shapes) {
            System.out.println("도형의 넓이: " + shape.calculateArea());
        }
    }
}

객체 지향 설계 원칙? (SOLID)

👉 단일 책임 원칙 (Single Responsibility Principle,SPR)

각 클래스는 하나의 책임만을 가져야 한다.
책임의 정의 : 클래스가 수행해야 하는 기능이나 역할.
변경의 이유 : 클래스를 변경해야 하는 이유는 단 하나여야 함. 여러 이유로 인해 클래스가 변경되어야 한다면 SRP를 위반하는 것.
즉, 클래스가 변경되어야 하는 이유는 하나이어야 하며 하나의 기능이나 역할에만 집중해야 한다.

SRP 위반의 예

  • 사용자 정보를 관리하고 데이터베이스와 통신하며 사용자 인터페이스를 업데이트 하는 클래스는 여러 책임을 갖고 있으므로 SRP를 위반.
  • 하나의 요구사항 변경으로 여러 클래스가 영향을 받는다면 SRP를 위반.

SRP 적용 방법

  • 하나의 클래스에 여러 기능이 있다면 여러 클래스로 나눌 수 있음.
    각 클래스는 독립적인 하나의 기능에만 집중.
  • 특정 기능을 다루는 작은 클래스나 메서드로 기능을 위임하여 각 클래스가 단일 책임을 갖도록 함.

SRP 장점

  • 유지보수의 용이성, 재사용성 증가, 테스트 용이성(단위별 테스트 쉬움)

👉 개방-폐쇄 원칙 (Open-Closed Principle, OCP)

소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만, 수정에 대해서는 닫혀 있어야 한다.
개방성 : 소프트웨어의 행동은 새로운 기능이나 요구사항이 생겼을 때 확장할 수 있어야 함.
(추상화, 다형성)
폐쇠성 : 기존의 코드는 새로운 기능을 추가할 때 수정되지 않아야 한다.
즉, 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있어야 한다.

OCP 위반의 예

  • 새로운 기능을 추가하기 위해 기존 클래스의 코드를 직접 수정하는 경우.(인터페이스X)
  • 기능을 추가하기 위해 기존 함수나 메서드에 if나 switch문을 추가하는 경우.

OCP 적용 방법

  • 기본 클래스 또는 인터페이스를 정의하고 필요에 따라 이를 확장하는 새로운 클래스를 생성.
  • 행동을 클래스의 집합으로 캡슐화 하고, 이를 동적으로 바꿀 수 있게 함으로써 실행 중에 알고리즘 변경.
    <코드 예시 - 로깅 시스템을 구현하며 로그 메시지를 다양한 방식으로 출력하는 방법>
/*전략 인터페이스 정의*/
// 로깅 전략에 대한 인터페이스
public interface LoggingStrategy {
    void log(String message);
}

//--------------------------------------------------------------

/*구체적인 전략 구현*/
// 콘솔에 로그를 출력하는 전략
public class ConsoleLogging implements LoggingStrategy {
    @Override
    public void log(String message) {
        System.out.println("Console: " + message);
    }
}

// 파일에 로그를 기록하는 전략
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class FileLogging implements LoggingStrategy {
    @Override
    public void log(String message) {
        try (PrintWriter out = new PrintWriter(new FileWriter("log.txt", true))) {
            out.println("File: " + message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

//--------------------------------------------------------------

/*컨텍스트 클래스 구현*/
// 로거 클래스는 로깅 전략을 사용합니다.
public class Logger {
    private LoggingStrategy strategy;

    public Logger(LoggingStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(LoggingStrategy strategy) {
        this.strategy = strategy;
    }

    public void log(String message) {
        strategy.log(message);
    }
}

//--------------------------------------------------------------

/*사용 예시*/
public class Main {
    public static void main(String[] args) {
        // 초기 전략을 콘솔 로깅으로 설정
        Logger logger = new Logger(new ConsoleLogging());

        // 콘솔에 로그를 남깁니다.
        logger.log("First log entry");

        // 로깅 전략을 파일 로깅으로 변경
        logger.setStrategy(new FileLogging());

        // 파일에 로그를 남깁니다.
        logger.log("Second log entry");
    }
}

// 처음에는 ConsoleLogging 전략을 사용하지만, 실행 중에 FileLogging 전략으로 변경 가능.
// 이 방식은 실행 중에 객체의 행동을 변경할 수 있게 하여, OCP 원칙을 준수.
  • 알고리즘 구조는 그대로 두고 일부 단계를 자식 클래스에서 재정의할 수 있게 함으로써 특정 단계의 구현을 변경.
    <코드 예시 - 데이터 처리>
/*추상 클래스 (템플릿 메서드 정의)*/
public abstract class DataProcessor {
    // 템플릿 메서드
    public final void process() {
        readData();
        processData();
        writeData();
    }

    // 기본적인 동작을 정의
    public void readData() {
        System.out.println("Reading data...");
    }

    // processData는 서브클래스에서 구현해야 하는 추상 메서드
    public abstract void processData();

    // 기본적인 동작을 정의
    public void writeData() {
        System.out.println("Writing data...");
    }
}

//--------------------------------------------------------------

/*구체적인 자식 클래스 구현*/
// JSON 데이터를 처리하는 서브클래스
public class JsonDataProcessor extends DataProcessor {
    @Override
    public void processData() {
        System.out.println("Processing JSON data...");
    }
}

// XML 데이터를 처리하는 서브클래스
public class XmlDataProcessor extends DataProcessor {
    @Override
    public void processData() {
        System.out.println("Processing XML data...");
    }
}

//--------------------------------------------------------------

/*사용 예시*/
public class Main {
    public static void main(String[] args) {
        DataProcessor jsonProcessor = new JsonDataProcessor();
        DataProcessor xmlProcessor = new XmlDataProcessor();

        System.out.println("JSON Processor:");
        jsonProcessor.process();

        System.out.println("\nXML Processor:");
        xmlProcessor.process();
    }
}

// JsonDataProcessor와 XmlDataProcessor 클래스는 이 processData()를 각각 JSON과 XML 데이터 처리 방식으로 구현.
// DataProcessor 클래스의 process() 메서드는 알고리즘의 구조를 정의, 실제 데이터 처리 방식은 자식 클래스에서 결정.
// 이 방식은 템플릿 메서드 패턴의 전형적인 사용 예시이며, 코드 재사용과 유연성을 증가.

OCP 장점

  • 유연성과 확장성, 유지보수의 용이성, 재사용성 증가

👉 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

자식 클래스는 언제나 그들의 기반 클래스로 대체될 수 있어야 한다.
즉, 프로그램에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체해도 프로그램의 정확성이 깨지지 않아야 한다.

LSP 위반의 예

  • 자식 클래스에서 부모 클래스의 메서드를 오버라이딩 할 때 기대하는 기능을 수행하지 않거나 예외를 발생시키는 경우.
  • 자식 클래스에서 메서드를 오버라이딩 할 때 더 엄격한 입력 조건을 요구하는 경우.
    <코드 예시>
/*부모 클래스 (Animal) _ setAge 메서드가 모든 양수의 나이를 허용*/
public class Animal {
    protected int age;

    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("Age must be positive");
        }
    }
}

//--------------------------------------------------------------

/*자식 클래스 (Dog) _ setAge 메서드를 오버라이딩하여 나이가 20보다 작아야 한다는 더 엄격한 조건을 요구*/
public class Dog extends Animal {
    @Override
    public void setAge(int age) {
        if (age > 0 && age < 20) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("Age must be between 1 and 19");
        }
    }
}

// Dog 클래스는 Animal 클래스보다 더 엄격한 입력 조건을 가지므로 LSP를 위반.
// Animal 객체를 Dog 객체로 대체하려고 할 때, Animal 클래스에 허용되는 나이가 Dog 클래스에서 허용되지 않을 수 있음.
// 기존의 Animal 객체를 사용하던 코드가 Dog 객체로 대체되었을 때 정상적으로 작동하지 않을 수 있음.
  • 자식 클래스에서 부모 클래스의 메서드를 오버라이딩하고 더 약한 결과를 반환하는 경우.
    <코드 예시 _ 은행 계좌 클래스>
/*부모 클래스 _ withdraw 메서드를 가지고 있으며, 출금이 성공하면 true를 반환*/
public class BankAccount {
    protected double balance;

    public BankAccount(double balance) {
        this.balance = balance;
    }

    public boolean withdraw(double amount) {
        if (amount > 0 && balance >= amount) {
            balance -= amount;
            return true;
        }
        return false;
    }
}

//--------------------------------------------------------------

/*자식 클래스 _ withdraw 메서드를 오버라이딩하지만, 부모 클래스와 달리 출금 시도가 성공하더라도 항상 false를 반환*/
public class LimitedBankAccount extends BankAccount {
    public LimitedBankAccount(double balance) {
        super(balance);
    }

    @Override
    public boolean withdraw(double amount) {
        super.withdraw(amount);
        return false;  // 항상 false 반환, 부모 클래스의 동작을 약화시킴
    }
}

// LimitedBankAccount 클래스는 BankAccount의 withdraw 메서드를 오버라이딩하지만, 부모 클래스에서 정의된 기능을 약화.
// BankAccount의 인스턴스를 LimitedBankAccount의 인스턴스로 대체하면, 기존 코드에서 예상한 출금 성공 여부의 확인이 잘못됨.
// 이는 소프트웨어의 예측 가능성과 신뢰성을 저하.

LSP 적용 방법

  • 자식 클래스에서 부모 클래스를 오버라이딩 할 때 기능을 변경하지 않고 확장하는 방식으로 접근.
  • 메서드의 사전 조건과 사후 조건을 명확하게 정의.
  • 때때로 상속 대신 컴포지션을 사용하는 것이 더 적합할 때를 고려.

❔ 메서드의 사전조건 : 메서드나 함수가 실행되기 전에 충족되어야 하는 조건. 상태나 매개변수 조건을 명시.
ex) 특정 매개변수가 null이 아니어야 함, 특정 숫자가 양수이어야 함.
❔ 메서드의 사후조건 : 메서드나 함수가 실행이 완료된 후에 만족되어야 하는 조건.
ex) 메서드가 데이터베이스에서 레코드를 삭제한 후 그 레코드가 더 이상 존재하지 않아야 함.

LSP 장점

  • 재사용성과 확장성, 유지보수의 용이성, 소프트웨어의 견고성

👉 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

클라이언트는 자신이 사용하지 않는 인터페이스를 구현하도록 강요받지 않아야 한다.
이를 방지하기 위해 ISP는 인터페이스를 더 작고, 구체적인 목적에 맞게 분리할 것을 권장.

ISP 위반의 예

  • MultiFunctionPrinter라는 인터페이스에 print(), scan(), fax() 등의 메서드가 모두 포함되어 있다고 가정했을 때 만약 어떠한 프린터 클래스가 이 인터페이스를 구현하려면 모든 메서드를 구현해야 하는데 프린터가 팩스 기능을 지원하지 않는다면 fax() 메서드는 불필요.

ISP 적용 방법

  • 각각의 기능에 대한 별도의 인터페이스를 만들고, 각 클래스에서 필요한 인터페이스만 구현하도록 함.
  • 각 클래스는 필요한 기능에만 의존하게 되며 불필요한 의존성으로부터 자유로워짐.

ISP 장점

  • 유연성과 재사용성 용이, 유지보수성 증가, 결합도 감소

👉 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안된다.
인터페이스는 구체적인 구현에 의존하지 않아야 하며 구체적인 구현이 인터페이스에 의존해야한다.
즉, 상세한 구현보다는 인터페이스나 추상 클래스에 의존해야 합니다.

❔ 고수준 모듈 : 비즈니스 로직을 담당하는 모듈
❔ 저수준 모둘 : 데이터베이스, 네트워크, 파일 시스템과 같은 세부적인 작업을 담당하는 모듈

DIP 위반의 예

  • 고수준 모듈이 저수준 모듈의 구체적인 구현에 직접 의존
    <코드 예시 _ 이메일 서비스>
/*저수준 모듈: 구체적인 이메일 서비스 구현*/
public class EmailService {
    public void sendEmail(String message, String receiver) {
        // 이메일을 보내는 로직 구현
        System.out.println("Email sent to " + receiver + ": " + message);
    }
}

//--------------------------------------------------------------

/*고수준 모듈: 고객 관리 시스템*/
public class CustomerService {
    private EmailService emailService;

    public CustomerService() {
        // 고수준 모듈이 저수준 모듈의 구체적인 구현에 직접 의존
        this.emailService = new EmailService();
    }

    public void notifyCustomer(String message, String email) {
        // 고객에게 이메일을 보냄
        emailService.sendEmail(message, email);
    }
}

// CustomerService 클래스(고수준 모듈)가 EmailService 클래스(저수준 모듈)의 구체적인 구현에 직접적으로 의존.
// 이로 인해 EmailService의 구현이 변경되면 CustomerService도 영향을 받을 수 있음.
// EmailService를 다른 서비스로 교체하거나, 다른 방식으로 이메일을 보내야 할 경우, CustomerService 클래스의 변경이 불가피.
// 이 설계는 시스템의 유연성과 확장성을 제한하며, 테스트를 더 어렵게 만듬.

DIP 적용 방법

  • 고수준 및 저수준 모듈 모두가 인터페이스나 추상 클래스에 의존하도록 설계
  • 객체가 필요로 하는 의존성을 외부에서 주입하는 방법을 사용하여 각 모듈간의 결합도를 낮춤.
    객체가 자신의 의존성을 직접 생성하지 않고, 외부(예: 생성자, 세터 메서드, 팩토리 메서드)를 통해 주입받게 함으로써 객체 간의 결합도를 낮추고 유연성 및 재사용성을 향상
    <코드 예시 _ 이메일 서비스를 사용하는 고객 관리 시스템>
/*인터페이스 정의*/
public interface EmailService {
    void sendEmail(String message, String receiver);
}

//--------------------------------------------------------------

/*인터페이스의 구체적인 서비스 구현*/
public class SimpleEmailService implements EmailService {
    @Override
    public void sendEmail(String message, String receiver) {
        // 실제 이메일 전송 로직
        System.out.println("Sending email to " + receiver + ": " + message);
    }
}

//--------------------------------------------------------------

/*고수준 모듈: 의존성 주입을 사용*/
// 고수준 모듈인 CustomerService는 EmailService의 구체적인 구현 대신 인터페이스에 의존
public class CustomerService {
    private EmailService emailService;

    // 생성자를 통한 의존성 주입
    public CustomerService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void notifyCustomer(String message, String email) {
        emailService.sendEmail(message, email);
    }
}

//--------------------------------------------------------------

/*사용 예시*/
// CustomerService 객체를 생성할 때, 필요한 EmailService 구현체(SimpleEmailService)를 외부에서 주입
public class Main {
    public static void main(String[] args) {
        EmailService emailService = new SimpleEmailService();
        CustomerService customerService = new CustomerService(emailService);

        customerService.notifyCustomer("Welcome to our service!", "customer@example.com");
    }
}

DIP 장점

  • 유연성과 재사용성 용이, 유지보수의 용이성, 테스트 용이성(단위별 테스트 쉬움)

profile
Please, Steadily

0개의 댓글