개발/🍃 Spring

🍀 [ 김영한 스프링 핵심 원리 - 기본편 ] Section02. 객체 지향 설계와 스프링

정소은 2025. 1. 2. 15:06

 

 

 

1.  Spring이란? 

 

 

1 )  Spring Framework

1.  핵심 기술 : Spring DI 컨테이너, AOP, 이벤트 기타
2.  웹 기술 : Spring MVC, Spring WebFlux
3.  데이터 접근 기술 : Transaction, JDBC, ORM 지원, XML 지원
4.  기술 통합 : Cache, Email, 원격 접근, Scheduling
5.  테스트 : Spring 기반 Test 지원
6.  언어 : Kotlin, Groovy

 

 

2 )  Springboot

1.  스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
2.  단독으로 실행할 수 있는 Spring 어플리케이션을 쉽게 생성
3.  Tomcat 같은 웹 서버를 내장하고 있어, 별도의 웹 서버를 설치하지 않아도 됨
4.  손쉬운 빌드 구성을 위한 starter 종속성 제공
5.  Spring과 외부 라이브러리 자동 구성
6.  메트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능 제공
7.  관례에 의한 간결한 설정

 

 

3 )  Spring 핵심 개념

좋은 객체 지향 어플리케이션을 개발할 수 있도록 도와주는 Framework

 

 

 


 

 

 

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

 

 

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

 

프로그램을 객체들의 모임으로 파악하고자 하는 것, 이때 각각의 객체는 메세지를 주고받고 데이터를 처리할 수 있다

객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만든다

 

유연하고 변경이 용이하다는 것

: 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법

: Java의 다형성 활용

 

다형성의 핵심

: 역할구현을 분리

  • 클라이언트는 대상의 역할(인터페이스)만 알면 된다
  • 구현 대상의 내부 구조를 몰라도 된다
  • 구현 대상의 내부 구조를 변경해도 영향 받지 않는다
  • 구현 대상 자체가 변경되어도 영향 받지 않는다

역할과 구현 분리 예시

위 예시에서 역할이란 

 

 

2 )  좋은 객체 지향 설계 5원칙  -  SOLID

 

좋은 객체 지향 설계란 시스템이 아무리 복잡해지더라도 변동에 유연하게 대처 가능한 설계를 의미한다.

즉, 극한으로 객체 지향을 추구하여 유지 보수성과 확장성을 높이고 코드의 재사용성을 극대화할 수 있도록 설계하는 것을 말한다.

이러한 설계를 잘 실현시키기 위한 규칙이 SOLID 원칙이다.

 

 

SRP ( Single Responsibility Principle : 단일 책임 원칙 )

: 단일 책임 원칙이란 하나의 클래스하나의 책임(=기능)을 구현해야 함을 의미한다

즉, 하나의 기능을 여러 클래스에 흩트려놓거나 여러 기능을 하나의 클래스에 통합하지 않는 것이다

예를 들어 게시판의 글쓰기, 상세 조회하기, 목록 조회하기 기능을 각각 다른 클래스에 구현해야 함을 말하는 것이다

이 원칙을 잘 지키면 변경이 생겼을 때 파급 효과가 적어진다

 

 

OCP ( Open Close Principle : 개방 폐쇄 원칙 )

: 개방 폐쇄 원칙은 ' 확장에는 개방, 수정에는 폐쇄 ' 를 의미한다

이는 Java언어의 다형성을 통해 지켜진다

 

아래는 OCP 원칙을 위반한 설계다

class Rectangle {
    public double width;
    public double height;
}

class Circle {
    public double radius;
}

class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.width * rectangle.height;
        } else if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.radius * circle.radius;
        }
        return 0;
    }
}

위 코드에 따르면 calculateArea()메서드는 새로운 객체가 생겨날 때마다 그 내용을 수정해야 한다

예를 들어 Triangle이라는 객체가 생성되면 calculateArea 내에 아래 코드를 추가해야 한다

else if(shape instanceof Triangle) {
	Triangle triangle = (Triangle) shape;
    return triangle.height * triangle.base * 0.5;
}

즉, 수정에 폐쇄적이어야 한다는 원칙을 위반한 코드다

 

아래는 OCP를 잘 지킨 설계다

interface Shape {
    double calculateArea();
}

class Rectangle implements Shape {
    public double width;
    public double height;

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

class Circle implements Shape {
    public double radius;

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

class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}

Rectangle, Circle 객체가 모두 Shape라는 인터페이스를 상속받고 있기 때문에

calculateArea()를 각각의 객체에서 재정의하여 사용할 수 있다

즉, 확장에 개방적이라는 의미다

 

[ OCP 원칙의 한계 ]

Shape shape = new Rectangle();
Shape shape = new Circle();

Shape의 구현 객체인 Rectangle과 Circle 내의 코드를 사용하고자 하면

위와 같이 객체를 생성하는 코드를 상황에 따라 바꿔줘야 한다 

즉, 구현 객체 변경하려면 클라이언트 코드를 변경해야 한다

 

 

LSP ( Liskov substitution principle : 리스코브 치환 원칙 )

: 자식 클래스는 언제든지 부모 클래스로 치환해도 문제없이 작동해야 한다는 원칙

구체적으로 말하자면 부모 클래스의 메서드를 자식 클래스가 오버라이딩하더라도 메서드의 본래의 의도를 해쳐서는 안 된다는 것이다

 

아래는 LSP를 위반한 설계다

class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostriches can't fly");
    }
}

Ostrich(날 수 없는 새) 클래스는 Bird 클래스를 상속받지만 fly()를 제대로 구현하지 못한다

따라서 Ostrich는 Bird를 완전히 대체하지 못한다

 

아래는 LSP를 잘 지킨 설계다

class Bird {
    // 공통 기능을 여기에 넣음
}

class FlyingBird extends Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Ostrich extends Bird {
    // Ostrich는 fly() 메서드를 가지지 않음
}

위 설계에서는 fly()를 FlyingBird클래스 내에 구현한다

Ostrich(날 수 없는 새)는 Bird만을 상속받고 FlyingBird는 상속받지 않으면 된다

그렇게 하면 Ostrich는 부모인 Bird를 완전히 대체할 수 있게 되는 것이다

 

 

ISP ( Interface segregation principle : 인터페이스 분리 원칙 )

: 하나의 인터페이스는 하나의 책임(=하나의 기능)을 구현한다는 원칙

SRP와 비슷한 원칙으로 하나의 인터페이스 내에서는 하나의 기능을 구현해야 한다는 원칙이다

특히나 인터페이스는 다중 상속이 가능하기 때문에 최대한 기능을 분리하는 것이 유리하다

다만, 인터페이스는 최초 설계 상태를 최대한 유지해야 한다

즉, 변경이 웬만해선 없어야 한다

 

DIP ( Dependency inversion principle : 의존 관계 역전 법칙 )

: 클래스가 아닌 추상화된 인터페이스나 추상 클래스에 의존하도록 설계하라는 원칙

앞서 SRP에서 말했듯 각 클래스는 최대한 독립적으로 하나의 기능만을 구현해야 한다

따라서 클래스간의 의존성을 낮추기 위해 클래스끼리 직접 상속받는 것이 아니라

클래스의 상위이며 변동이 거의 없는 인터페이스를 의존하도록 설계하는 것이 좋다 

 

[ DIP의 한계 ]

Shape shape = new Circle();

다형성만으로는 결국 구현 클래스에 의존할 수밖에 없음

 

 


 

 

위에서 나타난 한계들을 해결하기 위해 Spring이 생겨남

→  DI : 의존성 주입 / DI 컨테이너 제공 : 자바 객체들을 컨테이너 안에 넣어놓고 의존관계 설정

⇒ 클라이언트 코드의 변경 없이 기능 확장 가능

 

 

< 실무에서의 현실적인 사용 >

기능을 확장할 가능성이 없다면 구체 클래스 직접 사용하고

향후 꼭 필요할 때 리팩토링해서 인터페이스를 도입