Spring & JPA

[테코톡] SOLID + 추가 정리

진진리 2025. 4. 10. 18:43

출처: https://www.youtube.com/watch?v=7c0tqHLfxlE&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=101

 

SOLID: 객체지향 설계에서 유지보수성과 확장성을 높이기 위한 설계 원칙

SRP 단일 책임 원칙

  • 객체는 한 가지 역할(책임)만 가져야 한다.
    • 객체가 변경되는 이유는 단 한 가지여야 한다.
  • 필요성: 책임이 변경의 축이기 때문에 분할되는 것이 중요
    • 책임이 여러 개면 클래스가 커지고 책임 간의 결합도가 높아져 연쇄적인 변화가 발생
  • 어떻게?
    • 추상화를 통해 객체를 설계할 때 한 개의 역할만 갖도록 구성
    • 책임을 묶어서 생각하기 때문에 쉽지 않음... -> 책임의 분배는 설계 상황에 따라 바뀔 수 있으므로 경험이 중요

 

OCP 개방 폐쇄 원칙

  • 객체의 확장에 열려 있고 변경에는 닫혀 있어야 한다.
    • 확장에 열려 있다. -> 행위를 추가해 객체가 하는 일을 확장할 수 있다.
    • 변경에 닫혀 있다. -> 객체의 확장이 소스 코드의 변경을 초래하지 않는다.
  • 기본 코드를 쉽게 확장 가능하므로 유연성, 재사용성, 유지보수성을 얻음
  • 어떻게?
    • 추상화 -> 모듈이 잘 변하지 않는 추상화에 의존해 닫혀있고, 구현체를 추가해 확장에는 열려 있도록

 

LSP 리스코프 치환 원칙

  • 서브 타입은 자신의 기반 타입으로 교체할 수 있어야 한다.
    • 하위 클래스가 상위 클래스로 바뀌어도 역할 수행에 문제가 없어야 한다.
    • 서브 타입은 기반 타입이 정해둔 약속을 지켜야 한다.
  • 리스코프 치환 원칙의 위반 -> 개방 폐쇄 원칙의 위반
  • 어떻게?
    • 하위 클래스 is-a 상위 클래스
    • 구현 클래스 is able to 인터페이스
    • 행위에 대한 is-a가 잘 지켜져야 한다.

 

ISP 인터페이스 분리 원칙

  • 인터페이스는 자신의 클라이언트가 사용할 메서드만 가지고 있어야 한다.
    • 클라이언트는 자신이 사용하지 않는 메서드에 의존하게 되면 안된다.
    • 인터페이스가 커질 수록 같은 인터페이스를 구현하는 클라이언트간 결합도가 높아짐
  • 어떻게?
    • 자신의 클라이언트가 필요로 하는 함수만 선언
    • 인터페이스 구현은 is-able-to로 해석 -> 가능하게 만들기 위해 필요한 기능만 제공
    • 예시) Comparable 인터페이스는 구현체가 비교 가능한 객체가 되기 위해 필요한 기능만 제공

 

DIP 의존 관계 역전 원칙

  • 구체적인 것이 추상화된 것에 의존해야 한다.
    • 자주 변경되는 것에 의존하지 말아라
  • 상위 객체는 본질을 담고 정책을 결정하는 역할
  • 추상화가 구체적인 것에 의존하면 자유롭게 재사용할 수 없음
  • 어떻게?
    • 의존성은 이행적 -> 하위 수준에 변화가 생기면 연쇄적으로 변화가 발생
    • 동일 레벨에서는 추상화를 의존, 하위 레벨이 상위 레벨을 의존하게 만들어 역전


추가 정리

 

객체지향의 4가지 특성

  • 캡슐화, 상속, 추상화, 다형성
  • 오버로딩: 같은 이름을 가진 메소드나 생성자를 여러 개 정의하는 것
    • 매개변수의 타입, 순서 또는 개수가 다를 때 사용됨
    • 정적 다형성을 지원
    • 컴파일 시점에 어떤 메소드가 호출될지 결정됨
class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        // int 형식의 add 메소드 호출 (정적 다형성)
				int result1 = calculator.add(5, 3);
				// double 형식의 add 메소드 호출 (정적 다형성)
        double result2 = calculator.add(2.5, 3.7);
    }
}

 

  • 오버라이딩:상위 클래스에서 정의된 메서드를 하위 클래스에서 동일한 메서드 시그니처를 가지고 재정의하는 것
    • 런타임 다형성을 지원
    • @Override: 컴파일러에서 메소드 오버라이딩의 정확성을 검증하고 규칙을 준수하는지 확인 -> 오류 방지
class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("왈왈! 크르릉..!");
    }
}

public class Main {
    public static void main(String[] args) {
				// 다형성을 이용한 상위 클래스 참조 변수로 하위 클래스 객체를 참조
        Animal myPet = new Dog();
				// Dog 클래스의 makeSound 메소드가 호출됨 (런타임 다형성)
        myPet.makeSound();
    }
}

출력결과
왈왈! 크르릉..!

 

 

SOLID를 배우는 이유

  • 역할(인터페이스)과 구현(인터페이스를 구현한 객체)으로 구분하자.
  • 역할, 구현이라는 관점을 다형성을 통해 객체로 만들 수 있다.
  • 유연하고 변경이 용이하다.
  • 다형성의 한계: 역할이 변하게 되면 모두에 큰 변경이 발생 -> 인터페이스를 안정적으로 설계하는 것이 중요
    • 이를 위해 SOLID를 알아야 함

 

스프링과 객체 지향

  • 스프링은 다형성을 극대화하여 이용할 수 있도록 도와줌
  • 제어의 역전(IOC), 의존관계 주입(DI)을 다형성을 활용해서 편리하게 다룰 수 있도록 지원

 

단일 책임 원칙

  • 코드의 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 지킨 것
// 단일 책임 원칙을 지킨 예
class Reservation {
    public void reserveRoom() {
        // 방 예약 로직
    }
}

class Payment {
    public void processPayment() {
        // 결제 처리 로직
    }
}


// 단일 책임 원칙을 위반한 예
// 예약과 결제, 두 가지 다른 책임을 가집니다. 따라서 이 클래스는 SRP를 위반하고 있습니다.
class ReservationAndPayment {
    public void reserveRoom() {
        // 방 예약 로직
    }

    public void processPayment() {
        // 결제 처리 로직
    }
}

 

 

개방-폐쇄 원칙

  • 인터페이스를 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
  • 잘못된 예제
    • 기능 추가 시 기존 코드를 변경해야 한다.
class Shape {
     public void drawCircle() {
        System.out.println("원 그리기");
    }

    public void drawRectangle() {
        System.out.println("사각형 그리기");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape = new Shape();
        shape.drawCircle();

		// shape.drawRectangle();
    }
}

 

  • 다형성을 활용한 예제
    • 문제점: 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
interface Shape {
    void draw();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원 그리기");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("사각형 그리기");
    }
}


public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle();
        circle.draw();

        Shape rectangle = new Rectangle();
        rectangle.draw();
    }
}

 

  • 의존성 주입 방법
interface Shape {
    void draw();
}

class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원 그리기");
    }
}

class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("사각형 그리기");
    }
}

class ShapeRenderer {
    public void draw(Shape shape) {
        shape.draw();
    }
}

public class Main {
    public static void main(String[] args) {
        ShapeRenderer shape = new ShapeRenderer();
        shape.draw(new Circle());
        shape.draw(new Rectangle());
    }
}

 

 

리스코프 치환 법칙

  • 인터페이스 하위 클래스는 반드시 규약을 지켜야 함
  • 인터페이스를 구현한 구현체를 믿고 사용하기 위해 필요
  • 위반한 코드: 하위 클래스가 상위 클래스를 대체 불가능한 경우
class Car {
    void startEngine() {
        // 엔진 시작 동작을 구현
    }
}

class ElectricCar {
    void chargeBattery() {
        // 배터리 충전 동작을 구현
    }
}


// 하이브리드!? 충전 해야하는데..?
class HybridCar extends Car {
    // ...
}

 

  • 해결 방법
1. 인터페이스를 활용
interface ElectricChargeable {
    void chargeBattery();
}

class Car {
    void startEngine() {
        // 엔진 시작 동작을 구현
    }
}

class ElectricCar extends Car implements ElectricChargeable {
    @Override
    public void chargeBattery() {
        // 배터리 충전 동작을 구현
    }
}

class HybridCar extends Car implements ElectricChargeable {
    @Override
    public void chargeBattery() {
        // 배터리 충전 동작을 구현
    }
}

2. 추상 클래스 사용
abstract class Car {
    void startEngine() {
        // 엔진 시작 동작을 구현
    }

    abstract void additionalAction(); // 추가 동작을 서브클래스에서 구현
}

class ElectricCar extends Car {
    @Override
    void additionalAction() {
        // 배터리 충전 동작을 구현
    }
}

class HybridCar extends Car {
    @Override
    void additionalAction() {
        // 배터리 충전 동작을 구현
    }
}

 

  • 잘 지킨 코드
interface Shape {
    double getArea();
}

class Circle implements Shape {
    private double radius;

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

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

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;
    }
}

 

 

인터페이스 분리 원칙

  • 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스로 분리하자
  • 인터페이스가 명확해지고 대체 가능성이 높아진다.
// 인터페이스 정의
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

// Worker 클래스가 필요한 동작을 구현
class Worker implements Workable, Eatable, Sleepable {
    @Override
    public void work() {
        System.out.println("근무 중");
    }

    @Override
    public void eat() {
        System.out.println("식사 중");
    }

    @Override
    public void sleep() {
        System.out.println("휴식 중");
    }
}

// Manager 클래스는 클라이언트가 필요로 하는 동작을 사용
class Manager {
    public void assignWork(Workable worker) {
        System.out.print("일 할당: ");
        worker.work();
    }

    public void provideLunch(Eatable worker) {
        System.out.print("점심 제공: ");
        worker.eat();
    }

    public void allowRest(Sleepable worker) {
        System.out.print("휴식 허용: ");
        worker.sleep();
    }
}

public class Main {
    public static void main(String[] args) {
        Worker worker = new Worker();
        Manager manager = new Manager();

        // 클라이언트가 필요로 하는 동작을 호출
        manager.assignWork(worker);
        manager.provideLunch(worker);
        manager.allowRest(worker);
    }
}

 

 

의존관계 역전 원칙

  • 의존성 주입은 이 원칙을 따르는 방법
  • 변하지 않을 가능성이 높은 인터페이스(역할)에 의존
interface Engine {
    void start();
}

class GasolineEngine implements Engine {
    public void start() {
        System.out.println("Gasoline engine started.");
    }
}

class ElectricEngine implements Engine {
    public void start() {
        System.out.println("Electric engine started.");
    }
}

class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine; // 추상화된 인터페이스에 의존
    }

    public void startEngine() {
        engine.start();
    }
}