개발자로 후회없는 삶 살기

spring PART.스프링의 본질 본문

[백엔드]/[spring | 학습기록]

spring PART.스프링의 본질

몽이장쥰 2023. 3. 19. 00:43

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

- 스프링의 탄생 배경

자바당 정파 기술로 Enterprise Java Beans라는 게 있었습니다. 당시에 기능이 잘 됐고 종합 선물 세트였는데 수천만원이 필요했습니다. 또한 진짜 어렵고 복잡하고 느렸습니다. EJB에서 지원하는 인터페이스를 다 구현하고 의존적으로 개발을 해야했습니다. > 그래서 EJB를 비판하면서 책을 쓴게 지금의 스프링입니다.

 

- 스프링의 역사

이것을 굳이 얘기하는 이유는 왜 로드존슨이 스프링을 만들었을까를 실제로 해볼 것입니다. 왜 그렇게 열광을 했는지 예제 코드로 실감해보자/ 로드존슨의 30000줄 예제를 만들어 볼 것입니다.

 

 

 

 

- 스프링이란?

1) 단독으로 실행하여 톰캣이 없어도 된다.
2) 손쉬운 빌드 구성을 제공해서 라이브러리 구성이 쉬워졌다. pip처럼
3) 스프링과 3rd parth를 구성하여 외부 라이브러리와 버전을 다 맞춰준다.

 

 

- 스프링을 왜 만들었을까?

핵심 개념에 대해 제대로 이해하고 사용해야 고급 개발자가 되는 것입니다. 안 그러면 단순하게 API 사용법만 알게 되는 것입니다.

 

=> 스프링의 핵심 개념

스프링은 자바 언어 기반의 프레임워크로 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크입니다. "한 마디로 스프링은 좋은 객체 지향 어플리케이션을 개발할 수 있게 도와주는 프레임워크"입니다. 그러면 좋은 객체 지향 프로그램이 뭐인가 알아보겠습니다.

 

+ 자바는 유연하고 변경이 용이하다는데 부품을 갈아 끼우듯이 유연하게 변경하면서 개발할 수 있는 방법으로 이게 다형성입니다.(springconfig 바꾼 거네) 이제부터 책으로만 배운 다형성 말고 진짜 다형성을 알아보겠습니다.

 

-> 다형성의 실세계 비유

이 세상을 역할과 역할을 실제하는 구현으로 보면 역할이 인터페이스고 구현이 구현체입니다. ex) 운전자 역할과 자동차 역할이 있습니다. 자동차 역할을 K3, 아반떼, 테슬라 모델3가 구현했습니다. 자동차 역할을 3개의 다른 자동차가 구현을 했습니다. 그러다 운전자는 K3를 타다가 아반떼로 차를 바꿉니다. (살짝 인터페이스의 업케스팅을 말하는 것 같습니다.) 자동차가 바뀌면 운전자에게 영향을 주나요? 안 줍니다. 자동차의 구현만 바뀐 것 뿐이지 운전자에게 영향을 안 줍니다. 

 

//역할
public interface Car {
    void myName();
}

// 구현
public class K3 implements Car{
    @Override
    public void myName() {
        System.out.println("나는 K3");
    }
}

//구현
public class AvanThe implements Car{
    @Override
    public void myName() {
        System.out.println("나는 아반떼");
    }
}

// 클라이언트
public class Human {
    //필드
    private Car car;

    //메서드
    public void getCar(Car car) {
        this.car = car;
        car.myName(); // 자동차 인터페이스
    }
}

// 실행 시점에 유연하게 변경
public class CarTest {
    public static void main(String[] args) {
        Human human = new Human();
        Scanner scanner = new Scanner(System.in);
        int chooseCar = scanner.nextInt();

        if(chooseCar == 1){
            human.getCar(new AvanThe());
        }
        else {
            human.getCar(new K3());
        }
    }
}

유연하고 변경이 용이하다의 뜻은 자동차를 테슬라에서 K3로 바꿔도 운전자는 그냥 변화없이 운전할 수 있다는 의미입니다. 이게 왜 그런 것이냐면 자동차 인터페이스에 따라서 자동차를 구현했기 때문에 운전자는 자동차 역할에 대해서만 의존하고 있으면 됩니다. 운전자(클라이언트)가 자동차의 내부 구조를 몰라도 됩니다. 자동차의 역할만 알고 있으면 클라이언트에게 영향을 주지 않습니다. 이걸 변경이 용이하고 무한히 확장 가능하다고 합니다. 클라에게 영향을 주지 않고 새로운 기능을 제공할 수 있다는 것이고 이게 왜 가능하냐면 역할과 구현으로 세상을 만들었기 때문입니다. 여기서 진짜 중요한 것은 새로운 자동차가 나와도 클라이언트는 새로운 것을 배우지 않아도 되는 것이 중요한 것입니다. 

 

=> 역할과 구현의 분리

역할과 구현으로 구분하면 세상이 단순해지고 유연해지고 변경도 편리해집니다. 

-> 장점

클라는 대상의 인터페이스만 알면 되고 구현 대상의 내부 구조를 몰라도 됩니다. 또한 구현 대상 자체를 바꿔도 영향을 받지 않습니다.

 

 

-> 자바 언어에서 다형성

자바언어의 다형성 활용을 보면 객체를 설계를 할 때 역할과 구현을 명확히 분리해서 설계를 하는 것입니다. 설계시 역할(인터)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듭니다. 역할이 더 중요합니다!

 

자바는 오버라이딩으로 다형성을 합니다. 다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있습니다. (new AvanThe, new K3) > 이전 강의를 생각해보면 클라이언트가 멤버 서비스라고 보면 클라는 멤버 레포를 의존합니다. 의존한다는 것이 뭐냐면 내가 얘를 알고 사용한다는 것입니다. 그런데 멤버 레포지토리 인터에다가 메모리와 Jdbc를 생성해서 대입할 수 있습니다. 메모리를 넣으면 메모리의 save가 호출되고 Jdbc를 넣으면 Jdbc의 save가 호출됩니다.

 

-> 다형성의 본질

인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다는 것이고 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계를 시작해야합니다. 클라이언트에서 코드하나 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있는 것이 다형성의 본질입니다! 따라서 인터페이스를 안정적으로 설계하는 것이 진짜 중요합니다.

 

-> 스프링과 객체 지향

지금까지 다형성이 가장 중요하다고 했습니다. 스프링은 다형성을 극대화할 수 있도록 도와줍니다. 제어의 역전(IoC), DI는 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원합니다. 어쩌면 이게 전부이고 마치 레고 블럭을 조립하듯이 구현을 편리하게 변경할 수 있고 메모리 멤버 레포에서 Jdbc 멤버 레포에서 Jpa 멤버 레포로 막 바꾸는 데도 기존 코드에 영향이 전혀 없었습니다.

 

 

- SOLID 원칙

좋은 객체 지향 설계의 5가지 원칙을 정리하겠습니다.

 

1. SRP(Single responsibility principle)

단일 책임 원칙으로 "하나의 클래스는 하나의 책임만 가져야한다"는 말입니다. 하지만 책임이라는게 모호합니다. 실무에서 책임이라는게 클수도 있고 작을 수도 있습니다. 그러면 판단의 기준은 변경이라고 봅니다다. 뭔가 변경이 있을 때 파급이 적으면 SRP를 잘 따른 것입니다. ex) UI를 변경하는데 sql부터 다 바꿔야하면 SRP를 잘못한 것입니다.

이 책임의 범위를 크고 작게 잘 조절하는 것이 객체 지향의 묘미입니다!

 

2. OCP(Open/closed principle)

개방 폐쇄 원칙으로 이게 가장 중요한 원칙입니다. 소프트웨어 요소는 "확장에는 열려 있으나 변경에는 닫혀 있어야한다."는 말로 참 이상한 게 "뭔가 기능을 확장을 하려면 코드를 변경해야할 텐데 변경을 안해도 된다." 라는 말로 어떻게 코드의 변경없이 기능을 추가할 수 있을지 참 이상합니다.

 

이전 예제 자동차 다형성을 생각해보면 자동차를 바꿔도 운전자는 바꾸지 않아도 됐었습니다. 이게 바로 개방 폐쇄 원칙입니다. 다형성을 활용하면 OCP를 할 수 있습니다. 인터페이스를 구현한 새로운 클래스를 하나 만드는 것은 기존 코드를 변경을 전혀 하지 않습니다. 즉 새로운 클래스를 만들어 기능을 확장했지만 기존 코드의 변경은 닫혀있는 것입니다.

 

 

 

근데 사실 기존 코드도 바뀝니다. 앞에서 배운 메모리 멤버 레포를 jdbc 멤버 레포로 바꾸려면 위 사진처럼 코드를 바꿔야합니다. 구현 객체를 바꾸려면 클라이언트 코드인 멤버 서비스를 변경을 해야합니다. 분명 다형성을 사용하여 인터페이스(역할)를 잘 만들었고 구현 클래스(구현)를 잘 만들었지만 OCP의 원칙을 지킬 수 없습니다. 

 

> 이는 만드는 것까지는 잘 만들었습니다. 하지만 적용을 하려고 보니 OCP가 깨지는 것입니다. OCP가 깨진다는 말은 클라이언트를 변경을 해야하는 것입니다. 소프트웨어의 기존 코드를 변경를 변경해야하는 것입니다. 그래서 OCP의 원칙을 지킬 수가 없습니다.

-> 문제 해결

그럼 이 문제를 어떻게 해결해야할까요? 객체를 생성하고 관계를 맺어주는 별도의 조립, 설정자가 필요합니다. 이 별도의 뭔가가 스프링 컨테이너가 해주는 것입니다. 

 

 

3. LSP(Liskov substitution principle)

리스코프 치환 원칙으로 자동차 인터페이스가 있을 때 구현체가 있고 악셀이라는 기능을 구현을 할 것입니다. 인터페이스의 메서드 설계가 악셀을 밟으면 앞으로 10을 가야하는 원칙이라면 구현할 때 10을 가게 짜야한다는 것으로 인터페이스의 설계를 기능적으로 보장을 해줘야한다는 것입니다. 앞으로 가야하는데 뒤로 가게 구현하면 LSP를 맞추지 않은 것입니다.

 

 

 

4. ISP(interface segregation principle)

인터페이스 분리 원칙으로 자동차 인터페이스가 하나 있는데 운전과 관련된 기능들도 막 있고 정비도 있습니다. 이런 경우 자동차 인터페이스 하나만 있으면 너무 크니깐 운전 인터페이스와 정비 인터페이스로 분리하는 것입니다. 이렇게 되면 사용자 클라이언트를 운전자 클라와 정비사 클라로 분리할 수 있습니다. 정비와 운전 각각이 변화해 영향을 주지 않게 하는 것입니다.

 

 

5. DIP(Dependency inversion principle)

 

의존 관계 역전의 원칙으로 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안 됩니다. 의존성 주입은 이 원칙을 따르는 방법 중 하나입니다. 쉽게 얘기하면 클라이언트 코드가 구현 코드를 바라보지 말고 인터페이스만 바라보라는 뜻입니다. 멤버 서비스(클라)가 멤버 레포만 봐야지 메모리 멤버 레포를 보면 안된다는 것입니다. 역할에 의존해야 한다는 말입니다. 

 

 

 

> 운전자는 자동차 역할에 대해서만 알아야지 K3에 대해 알 필요없습니다. 시스템을 언제든지 갈아끼울 수 있게 해야하고 그게 가능하려면 역활에 의존해야지 구현에 의존하면 절대 안된다는 것입니다. 역할에 의존해야하는 것이 DIP와 아예 같은 말입니다.

 

> OCP에서 말한 멤버 서비스는 인터페이스에 의존합니다. 그런데 구현 클래스도 동시에 의존합니다. 코드를 보면 그런건데 의존한다는 게 뭐냐면 내가 저 코드를 안다는 것입니다. 아니깐 아! 저걸 써야지! 하고 사용할 것입니다. > 따라서 멤버 서비스는 멤버 레포만 아는 게 아니라 메모리 멤버 레포까지 알고 있는 것입니다. 그래서 메모리 멤버 레포를 다른 것으로 바꾸려고 할 때 코드를 변경해야 하는 것입니다. 

> 클라이언트가 구현 클래스를 직접 선택하고 있는 것이고 DIP를 위반하고 있는 것이고 추상화에도 의존하고 구체화에도 의존하고 있다는 것입니다. 그럼 어떻게 해야할까요? 멤버 서비스는 설계할 때 멤버 레포에만 의존하도록 설계를 해야합니다. 오른쪽 코드는 멤버 서비스가 멤버 레포(역할)만 의존하는 것이 됩니다!

 

-> 정리

객체 지향의 핵심은 다형성입니다. 하지만 다형성만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없습니다. 메모리 멤버에서 jdbc로 변경없이 갈아끼울 수 없습니다. 사실 다형성 만으로는 OCP, DIP를 지킬 수가 없습니다. 그래서 뭔가가 더 필요합니다. 사실 구현체를 알아야 코드가 돌아갑니다! 인터페이스만 알면 구현체가 없는데 어떻게 코드가 돌아가겠습니까. 뭔가가 더 필요합니다.

- 스프링과 객체지향

스프링 이야기에 객체 지향 얘기가 왜 이렇게 많이 나오냐면 스프링은 OCP, DIP가 가능하게 지원을 해주는 기술입니다. DI, DI 컨테이너를 제공하고 이것으로 클라의 코드의 변경없이 기능을 확장할 수 있고 쉽게 부품을 교체하듯이 개발을 할 수 있는 것입니다. 


-> 정리

1. 모든 설계에 역할과 구현을 분리해야합니다. 자동차 예제를 떠올려야합니다.
2. 하지만 자바 객체 지향으로는 DIP, OCP를 위반할 수 밖에 없어서 대신 해주는 스프링이 필요합니다. 
3. 이상적으로 모든 설계에 인터페이스부터 다 설계해두고 하는 게 좋습니다. 그러면 어떤 DB를 사용할지 정해지지 않은 상황에서 다른 것을 가지고 개발을 해야하는 상황일 때 개발이 가능하다는 장점이 있습니다. 멤버 서비스를 개발할 때 멤버 레포 인터페이스만 보고 개발을 할 수 있는 것입니다. 이때 간단한 기능을 가지는 구현체를 하나 만들어 놓고 개발을 해라 그게 메모리 멤버 레포입니다. 그 후 나중에 DB가 정해지면 이를 확장하는 것입니다.


4. 하지만 이상적으로는 인터페이스를 도입하는게 좋지만 추상화라는 비용이 발생합니다. 코드가 추상화가 되니 런타임에서 구현 코드를 꼭 한 번더 바라봐야합니다. 따라서 기능을 확장할 가능성이 없다면 그냥 바로 구현 클래스를 쓰고 향후 꼭 기능 확장이 필요할 때 리팩토링해서 인터페이스를 도입하는 것도 방법입니다. 하지만 무조건 확장 가능성이 있으면 처음부터 인터페이스를 도입해야하고 이게 경험이고 좋은 개발자의 길입니다.

 

 

Comments