Spring이란 무엇인가 - 1

최혜성·2024년 2월 2일
0

백엔드 만드는 라이브러리요~

보통 Spring (Boot)를 이제 java기반으로 웹 서버를 구축하고 운영하는데 많이 사용하곤 한다.

근데 정확히 Spring이라는게 어떤거고, 다른 웹 프레임워크 (Flask, Django)랑 뭔 차이가 있을까?

DI 컨테이너

DI (Dependency Injection)이란 말 그대로 의존성 주입으로, 개발자가 직접 객체를 생성해서 사용하는 방식이 아닌, 해당 컨테이너에서 객체를 미리 혹은 필요할때 생성해서 필요한 객체에 동적으로 주입해주는 방법이다.

예를 들어보자

class Robot {
	val hand = Hand()
    
    fun grab() {
    	hand.grap()
    }
}

위 클래스는 한개의 손을 갖고 있는 로봇 클래스이다.
로봇이 물건을 쥐게 할려면 Robot의 인스턴스를 만들고, grab 메소드를 호출하기만 하면 된다.

근데, 만약 다른 종류의 손을 사용하고 싶다면 어떻게 해야할까?
지금은 손가락이 2개인 손을 사용한다면 나중에는 5개의 손가락이 있는 손을 써야한다면?

이를 객체지향의 다형성을 이용해서 해결할 수 있다.

1)

class Robot {
	val hand = FiveFingerHand()
    fun grab() {
    	hand.grab()
    }
}

interface Hand {
	fun grab()
}

class FiveFingerHand : Hand {
	overrifde fun grab ~
}

이제는 여러 종류의 손을 지원할 수 있는데, 매번 손이 바뀔때마다 hand 프로퍼티를 변경해줘야 한다.
이를 Composition방식이 아닌 Aggregation방식으로 변경하여 해결할 수 있다.

2)

class Robot(val hand : Hand) {
	fun grab() = hand.grab()
}

이로써 여러 종류의 손을 지원할 수 있게 되었다.

여기서 Aggragation과 Composition이란?

  • Composition - 합성관계, 해당 객체를 포함하는 객체와 생성 - 소멸이 같이 이루어짐
  • Aggragation - 집약관계, 해당 객체를 포함하는 객체와 생성 - 소멸은 다름.

Composition이 1번이고, Aggragation이 2번에 해당한다. Aggregation은 생성자를 이용하여 '이미' 생성되있는 객체를 필요한 클래스에 '주입'하는 방식으로, 유연한 확장과 테스트, 사용이 가능한 장점이 있다.

2번에서는 어떤 손을 넣어도 Hand 인터페이스만 구현한다면 사용할 수 있기 때문
테스트도 이 관점에서 보면 가짜 객체 (Mock Object)를 넣어서 손이 쥐어졌는지 확인해보기도 좋구..

그래서 스프링은 이 객체의 '생성'까지 xml파일 혹은 어노테이션을 이용해서 대신 해줄 수 있고, 주입까지 관리해주기 때문에 DI 컨테이너라고 할 수 있다.

IOC

IOC (Inversion Of Control) 제어의 역전이란 개념은 좀 헷갈릴 수 있다.
쉽게 말하면 보통 프로그램은 main 함수에서 '직접' 실행해서 개발자가 정의한 로직대로 쭉쭉 실행하는 방식이다.

근데 스프링은 제어가 '프레임워크'에서 일어나므로 사용자가 직접 flow를 정의할 필요 없이 그냥 기능만 구성하면 된다.

음..

만약 Echo 프로그램을 만든다고 가정해보자
IOC가 적용안된 프로그램은 다음과 같다.

  • 프로그램을 실행한다.
  • 사용자가 입력될때까지 기다린다.
  • 입력 버퍼에 데이터가 들어오면 출력버퍼로 해당 데이터를 넣는다.
  • flush를 해서 출력한다.

이는 개발자가 직접 모든 로직의 flow를 다 짜야하는 단점이 있다.

IOC가 적용된 프로그램은 다음과 같다.

  • 개발단계
    개발자는 프레임워크로부터 다음과 같은 메소드를 제공받을 수 있다.

    1. 사용자의 입력을 받은경우 실행되는 메소드.
    2. 사용자의 출력버퍼로 데이터를 넣는 메소드.

    이를 이용해서 개발을 진행할 수 있다.
    우선 사용자의 입력을 받는 메소드를 override해서 입력받은 데이터를 가져온다.
    이후, 입력받은 데이터를 출력버퍼로 보내는 메소드를 이용해서 출력을 진행한다.

이제 해당 프로그램을 실행하고 문자열을 입력하면 출력이 이루어질것이다.

개발자가 버퍼에 데이터가 들어올때까지 기다리는 과정 필요없이, 해당 프레임워크에서 입력 데이터가 들어오면 자동적으로 개발자가 만든 클래스를 호출해주기 때문이다.

그래서 개인적으론 IOC가 사실상 Event-Driven 개발이 아닌가 싶기도 하다.

그 안드로이드에서 onClick처리할때 OnClickListener같은거 넣는데 직접 해당 메소드를 호출하는게 아니니까 얘도 IOC에 의해서 이루어지는듯함.

AOP

AOP (Aspect Oriented Programming)은 OOP도 아니고 절차지향도 아니고 뭘까?

AOP는 관점지향 프로그래밍으로, 그냥 쉽게 말하면 공통된 기능을 여러군데 복붙하지 말고, 해당 기능들이 우선적으로 거치는 필터를 씌워서 걔보고 '해줘' 하는것이다.

그러면 매번 기능수정시 모든 클래스에 가서 해당 기능을 수정할 필요 없이 필터에 구현된 하나의 클래스만 수정하면 되는 부분이니 좀더 수월한 부분이 있다.

스프링의 AOP는 AspectJ등 동적으로 클래스를 생성하는 라이브러리를 사용한다.
만약 웹 요청이 들어오고 나간걸 로깅하고 싶다면 직접 요청을 로깅할 수 있지만,
AOP에서는 프록시 객체를 만들어서 처리할 수 있다.

  • 프록시 객체
    프록시 객체는 말그대로 대신 처리해주는 객체이다. 그냥 필터 씌워진 클래스라 생각하면 편하다.

이제 프록시 객체는 원본 클래스 대신 클래스에 주입되어, 실제 원본 클래스를 호출하면 자기가 호출되어서 미리 선언한 필터 기능 등등을 수행하고 이후 원본 클래스를 실행한다.

class Origin {
	fun printMe() = println("내가 진짜야..!")
}

class Proxy(val origin : Origin) : Origin {
	override fun printMe() { 
    	println("음~ 맛있다")
        origin.printMe() // super.printMe()로 바뀔수 있음
    }
}

class Caller(origin : Origin) {
	fun test() {
    	origin.printMe()
    }
}

만약 Caller에 Origin 객체가 아닌 새로 생성된 Proxy객체가 주입된다면 어떻게 될까?
Proxy의 print가 먼저 호출되고 Origin이 호출될것이다.
이제 Proxy객체는 실제 Origin이 호출되기전, 후에 원하는 기능을 쉽게 추가하고 관리할 수 있겠다.

실제 스프링의 proxy는 보통 @RestControllerAdvice등과 같은 어노테이션으로 쉽게 기능을 집어넣을 수 있다. 내부에 구현된 프록시 객체는 Java의 Refelction 기능을 이용해서 원본 객체의 변수(private까지 가능)를 참조하고, 메소드를 호출할 수 있다고 한다.

근데.. reflection 자체가 일반 메소드 호출보다 좀 많이 느리다고 한데.. 이런 관점 분리의 입장에서 보면 어느정도 피를 흘리는건 감수를 하는게 맞는거 같기도 하고

profile
KRW 채굴기

0개의 댓글