📝 프로젝트를 진행하며 학습하고 적용한 Spring 에 관하여 정리해보고자 합니다.
스프링은 어떤 특정한 하나가 아니라 여러가지 기술들의 모음이라고 볼 수 있다.
ex) 스프링 프레임워크(핵심), 스프링 부트(여러 스프링 기술들을 편리하게 사용하게 도움), 스프링 데이터, 스프링 시큐리티..
핵심기술
: 스프링 DI 컨테이너, AOP, 이벤트..웹 기술
: 스프링 MVC..데이터 접근 기술
: 트랜잭션, JDBC, ORM 지원..기술 통합
: 캐시, 스케줄링..테스트
: 스프링 기반 테스트 지원
스프링을 편리하게 사용할 수 있도록 지원하는 기능
Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨 (단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성 가능)
손쉬운 빌드 구성을 위한 starter 종속성 제공 등..
(참고로, 스프링 부트는 중간에 편리하게 사용하게 만드는 기능이기 때문에 스프링 프레임워크과 별도로 사용할 수 있는 것은 아니다)
스프링은 자바(Java
) 언어 기반의 프레임워크이다.
자바 언어의 가장 큰 특징은 객체 지향 언어
인데, 스프링은 이러한 객체지향 언어가 가진 특징을 살려서 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.
(물론, 기타적으로 웹 애플리케이션을 만들고, 전자 정부 프레임워크이기 때문에 등의 이유도 있다)
💡 '좋은 객체 지향 애플리케이션을 개발하는데 어떻게 도움을 주는가?' 에 대해서 알아보자면,
객체 지향의 핵심은 다형성
이다. 하지만 다형성 만으로는 클라이언트 코드 변경을 하게 되어 OCP, DIP를 지키기가 어렵다.
그래서 이때, 스프링은 다형성 을 극대화해서 편리하게 이용할 수 있게 지원해주는 기능이다.
그 중에서 제어의 역전(IoC),
의존관계 주입(DI)
는 다형성을 활용해서 역할
과 구현
을 편리하게 다룰 수 있도록 지원한다.
이를 통해서 우리는 역할에 따른 구현 을 레고 블럭 조립하듯이 맘대로 편리하게 변경할 수 있다. (이때 역할이 가장 중요하므로 잘 설계되어있어야 문제가 안생긴다!)
" 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것을 의미한다. "
제어의 역전에서 제어
와 역전
은 무엇일까?
제어
한다.(= 관리한다는 의미) public class A {
private B b;
public A(){
this.b = new B();
}
}
역전
시켜 외부에서 관리하는 것. (= 제어를 역전시켰다) public class A {
private B b;
public A(B b){
this.b = b;
}
}
이처럼 제어의 역전 IoC((Inversion of Control) 은,
클라이언트 구현 객체가 직접 다른 구현체를 지정,호출하는 것이 아니라 프레임워크가 대신 호출해줌으로써 제어의 흐름을 가져가는 것이다. (여기서 외부 라 함은 객체를 기준으로 봤을 때의 외부를 의미한다.)
이제 구현체는 자신의 로직만 담당할뿐 어떤 구현 객체들이 실행될지는 모른다. (인터페이스만 알고있는 것이다)
이를 통해 객체 생명주기 관리를 개발자 대신 스프링(컨테이너) 가 관리해주게 된다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService (MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
IoC 프로그램 모델은 곧 역할과 책임의 분리라는 내용과 관련이 있다.
역할과 책임을 분리해 응집도를 높이고 결합도를 낮추며, 이에 따라 변경에 유연한 코드 를 작성할 수 있는 구조가 될 수 있다.
결국 IoC를 사용하면 결과적으로 객체지향 원칙을 잘 지키는 코드를 만들 수 있다.
서브웨이에 간 것을 예시로 들어보자. 🌯
- 우선, 제어의 역전이 없다면 (❌),
우리는 직접 원하는 대로 재료를 선택한다면 알바생들은 이미 레시피(코드)에 지정되어 있는 재료의 샌드위치가 아니여서 당황을 겪게된다.
각 재료들에 대한 제어권 이 객체 내부에 있기 때문이다. 만약 요구를 반영하고자한다면 객체 내에 큰 변경이 생기게 된다.
- But, 제어를 역전시킨다면 (⭕️),
각 재료들에 대한 제어권을 우리가 갖게 되어 우리가 직접 조합을 요청하여 자신만의 조합의 샌드위치를 주문할 수 있다.
객체 내부에서 재료의 종류를 제어해 변경이 자유롭지 못하던 코드가 외부에서 제어를 받으면서 변경이 자유롭게 가능해진다.
이를 통해 위에서 제어의 역전이 없을 때 생기는 큰 변경에 대한 문제를 해결할 수 있게 되었다.
Don't call us, we'll call you
우리가 어떤 것을 주도하여 호출하는 것이 아니라, 주도권은 빼앗기고 호출 당하기를 기다리는 모습과 유사하다.
IoC 원칙을 구현하기 위한 여러가지 방법(Pattern)에는
Service Locator, Factory, Template Method, Strategy, Dependency Injection 등이 존재한다.
" 객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다. "
의존관계 주입에서 의존관계
와 주입
은 무엇일까?
의존성
클래스 간 의존 관계(의존성)가 있다면, 한 클래스가 바뀌면 다른 클래스도 영향을 받는다.
결국 의존은 영향을 받는 관계라는 것을 의미한다.
주입
의존성을 다른 곳으로부터 주입해주는 것이다.
이처럼 의존성 주입 DI(Dependency Inversion) 는
제어의 역전이 일어나는것을 전제 로 스프링 내부의 객체들 간의 관계를 관리할 때 사용한다.
애플리케이션 실행 시점(런타임)에 (이전에는 인터페이스) 객체 외부에서 실제 구현 객체를 생성해서 그 참조값을 전달하므로써, 클라이언트와 서버의 실제 의존관계가 연결 되는 것이다. (이때, 자바에서는 인터페이스를 사용하려 의존적인 관계를 처리한다./ DIP가 적용된 DI의 경우)
의존관계 주입의 장점은 애플리케이션 코드를 변경하지 않고도 이제 의존관계를 변경할 수 있다는 점이다.
public class Pizza {
Bread bread;
Cheese cheese;
Sauce sauce;
public Pizza(){
this.white = new WhiteBread();
this.mozzarella = new MozzarellaCheese();
this.chili = new ChiliSauce();
}
}
public class Pizza {
Bread bread;
Cheese cheese;
Sauce sauce;
public Sandwitch(Bread bread, Cheese cheese, Sauce sauce){
this.bread = bread;
this.cheese = cheese;
this.sauce = sauce;
}
}
3가지 방법이 존재한다.
1.생성자 주입 , 2.Setter 주입 , 3.Interface 주입
Spring Framework에서 DI는 어떻게 이루어질까?
예시로 @Controller, @Service 를 들 수 있다.
Controller, Service 는 의존관계를 지니고 있다. Controller 에서 Service 를 호출할때, Service 를 받는 생성자가 어딘가에서 호출되어야하지만 instance를 주입하는 코드를 쓰지 않아도 가능하다.
스프링에서 객체 생성을 자동으로 생성해주는 역할
스프링 빈으로 등록되면 스프링이 자동으로 인스턴스를 생성하며 이때 필요한 의존성도 주입해준다.
Spring의 의존관계를 자동으로 주입하는 방법은 @Autowired
를 사용하면 된다.
@Autowired
는 스프링 컨테이너가 주입하려 하는 객체의 타입이 일치하는 객체를(등록된 스프링 빈을) 찾아서 자동으로 주입 한다.
Autowired 방법에도 필드,생성자,setter 주입이 있다.
(이때 생성자 주입은 완전한 생성자일 경우 NullPointerException
을 방지할 수 있다.)
ex) DiscountPolicy 타입에 맞는 얘를 찾아와서 의존관계 주입을 자동으로 해준다. 이때 런타임 이전에는 RateDiscountPolicy, FixDiscountPolicy 둘 중 어느 구현 객체가 주입될지는 알 수 없다.
@Autowired
public OrderService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
DIP(Dependency Inversion Policy) 는 인터페이스에 의존하고 구현체에 의존하면 안된다.
상위 레벨의 모듈은 절대 하위 레벨 모듈에 의존하지 않고 둘 다 추상화에 의존 해야한다는 것이다.
구현체는 인터페이스들을 호출하지만 실제 실행 시점에 어떤 구현 객체들이 실행(주입)될지는 모른다.
우리는 import 코드를 통해 정적인 클래스 의존관계는(인터페이스) 파악할 수 있지만, 실제 실행 시점의 동적인 클래스 의존관계는(구현체 객체) 파악할 수 없다.
의존성을 주입(DI)하더라도 구체 클래스로 의존성을 주입하게 되면 변경이 어렵게된다.
때문에 DIP를 이용하여 의존성을 분리 시켜야한다.
즉, 상위계층이 하위계층에 의존하는 상황을 Interface를 이용해 반전시켜 하위계층의 구현으로부터 독립시킨다.
DIP를 적용하게되면 기존 생성자 주입 코드와 비교할 때 의존의 방향이 역전되며 변경으로부터 자유로워진 것을 알 수 있다.
DIP 적용 전 DI
public class Pizza {
WhiteBread white;
MozzarellaCheese mozzarella;
ChiliSauce chili;
public Sandwitch(WhiteBread white, MozzarellaCheese mozzarella, ChiliSauce chili){
this.white = white;
this.mozzarella = mozzarella;
this.chili = chili;
}
}
DIP 적용 후 DI
public class Pizza {
Bread bread;
Cheese cheese;
Sauce sauce;
public Sandwitch(Bread bread, Cheese cheese, Sauce sauce){
this.bread = bread;
this.cheese = cheese;
this.sauce = sauce;
}
}
피자 주문에서도 DIP 를 생각해볼 수 있다. 🍕
위의 피자 코드를 보면, 피자는 고수준 모듈이고 모짜렐라 치즈는 저수준 모듈이라고 볼 수 있다.
- DIP 적용 전 (❌)
처음 피자는 모짜렐라 치즈가 기본으로 설정되어있어, 고수준 모듈(피자)이 저수준 모듈(모짜렐라)에 의존하고 있는 형태이다.
하지만 만약 모짜렐라를 더 이상 쓸 수 없어 체다 치즈로 변경하고자 한다면, 모짜렐라에 의존하고 있던 피자 코드도 영향을 받게 된다.
- DIP를 통해 해결 (⭕️)
피자는 더 이상 구체적으로 모짜렐라에 의존하지 않고 어떤 종류의 치즈일지 모르는 추상화된 치즈(인터페이스)에 의존하게 된다. 저수준 모듈인 치즈의 여러 종류들도 치즈라는 인터페이스에 의존하며, 고수준 모듈과 저수준 모듈이 모두 Cheese 라는 인터페이스에 의존하게 된다.이때, Cheese는 고수준 모듈인 Pizza 입장에서 만들어졌고 따라서 상황이 역전되어 저수준 모듈이 고수준 모듈에 의존하게 된 것을 알 수 있다. 이렇게 의존방향이 역전된 것을 DIP 라고 한다. (+ 앞서 보았던 IoC 코드에서도 고수준 모듈인 샌드위치가 저수준 모듈인 재료의 종류에 의존하지 않고, Bread, Sauce 등 재료의 인터페이스에 의존하며 이미 DIP가 적용되어있는 것을 알 수 있다.)
IoC
, DIP
는 원칙(Principle)이고, DI
는 IoC를 달성하기 위한 디자인 패턴 중 하나이다.
Spring DI/IoC
는 프레임워크로 스프링이 DI를 자동으로 해줌으로써 프로그램의 제어권을 가져가는 역할을 해준다.
둘 다 클래스 간 결합을 느슨히 하기 위한 목적을 지녔다.
한 클래스의 변경이 다른 클래스들에게 미치는 영향을 최소화 함으로써 애플리케이션을 지속가능성있고 확장성있게 만들 수 있게 한다.
그래서 서로 시너지가 높은 IoC와 DI 를 함께 사용하고자 한다.
둘 다 같은 목적을 지녔지만,
IoC는 제어
의 역전, DIP는 의존방향
의 역전이다.
IoC는 제어권이 내부에서 외부로 넘어간 것이다. 어떤 재료가 들어오는지 알 수 없다.
여기서 WhiteBread 라고 칭했지만 우리가 직접 new WhiteBread() 라고 지정했을 때와 다르게 정확히 구운 상태의 WhiteBread 일지 냉동 상태의 WhiteBread 일지 모르는 것이다. (결제 방법이랑 비교해보면 Card 가 선택된 것은 알았으나 할인율이 적용된 카드인지 그냥 일반 카드인지 모르는 것이다.)
public class Sandwitch {
WhiteBread white;
MozzarellaCheese mozzarella;
ChiliSauce chili;
public Sandwitch(WhiteBread white, MozzarellaCheese mozzarella, ChiliSauce chili){
this.white = white;
this.mozzarella = mozzarella;
this.chili = chili;
}
}
하지만 위의 코드는 제어의 역전은 이루어졌지만, 필드가 구체 클래스로 구현되어있어 변경에 자유롭지 못하다.
(고수준 모듈이 저수준 모듈에 의존하는 문제점을 보이고 있다)
이때 DIP 를 적용하게 되면 필드가 인터페이스로 구현되어있기 때문에 상속받는 모든 종류의 재료들이 들어올 수 있어 변경에 자유로워진 것을 알 수 있다. 또한 고수준 모듈 입장에서 만들어진 인터페이스에 저수준 모듈이 의존하게 되어, 고수준 모듈과 저수준 모듈이 모두 추상화에 의존하게 되며 의존방향이 역전되었다고 볼 수 있다.
public class Sandwitch {
Bread bread;
Cheese cheese;
Sauce sauce;
public Sandwitch(Bread bread, Cheese cheese, Sauce sauce){
this.bread = bread;
this.cheese = cheese;
this.sauce = sauce;
}
}
AOP(Aspect Oriented Programming)는
애플리케이션의 핵심적인 기능과 부가적인 기능을 분리해 각각 모듈화 하는 방법이다.
기존의 객체 지향 프로그래밍(OOP)에서는 기능 별로 클래스를 분류했음에도 불구하고 여전히 로그 등 공통적으로 반복되는 중복 코드가 발생하는 단점이 발생한다.
이때, OOP로 핵심 기능에서 부가 기능을 독립적으로 분리하기 어려운 문제점 을 AOP가 해결해준다.
AOP는 핵심 비즈니스 로직과 부가 기능(Aspect
)를 분리하는 등 OOP를 보완하는 역할이다. (객체지향적 가치를 지키게 도와준다)
때문에 핵심 비즈니스 로직에 더욱 집중 할 수 있고, 반복적인 코드들을 한 곳에서 유지하고 관리할 수 있다는 이점이 있다.
+) 공통 모듈인 인증, 로깅, 트랜잭션 처리에 용이하다.
프록시 기반의 AOP 구현체이다.
프록시는 Advice(실질적으로 부가기능을 담은 구현체)를 타겟 객체에 적용하면서 생성되는 객체이다.
스프링은 Target(부가기능을 부여할 대상) 객체에 대한 프록시를 만들어서 제공한다. 타겟을 감싸는 프록시는 실행시간(RunTime)에 생성된다.
프록시 객체를 사용하는 이유는 기존 코드의 변경 없이 접근 제어 또는 부가 기능 추가를 위해서다.
스프링 빈에만 AOP를 적용할 수 있다.
모든 AOP 기능을 제공하는 것이 목적이 아니라, 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체 간 관계 복잡도 증가 등)를 해결하기 위한 솔루션을 제공하는 것이 목적이다.
@Aspect
어노테이션을 붙여 이 클래스가 Aspect를 나타내는 클래스라는 것을 명시하고, @Component
를 통해 빈으로 등록해준다.
@Before
, @After
, @Around
등의 어노테이션을 통해 타겟 메서드의 Aspect 실행 시점을 지정할 수 있다.
스프링 컨테이너(=IoC 컨테이너)는
스프링에서 자바 객체들(빈)을 관리하는 공간을 의미한다.
자바 객체의 생명 주기를 관리하며, 생성된 자바 객체들(빈)을 관리하며 추가적인 기능을 제공하는 역할을 한다.
스프링 컨테이너 안에는 스프링 빈 저장소가 있는데
Map<Key,Value> 와 같은 형태로 <key
: 빈 이름, value
: 빈 객체> 형태로 빈(Bean)
을 저장하고 관리한다.
(스프링 빈 등록은 수동 등록 혹은 컴포넌트 스캔을 통해서 @Component
애노테이션이 붙은 클래스들을 빈으로 자동 등록한다.)
스프링 컨테이너에서는 빈(Bean)들을 관리하기 위해 IoC와 DI의 원리가 적용된다.
(개발자는 new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸하지만, 스프링 컨테이너를 사용하면 해당 역할을 대신해 준다. 또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어 준다.)
객체들은 기본적으로 싱글톤으로 관리되기 때문에 여러 클라이언트의 요청에 맞게 매번 객체를 생성하는게 아니라, 스프링 빈에서 관리되고 있는 객체를 공유해서 효율적으로 재사용할 수 있다.
(*빈(Bean) : 우리가 자바 객체를 생성한뒤 스프링 컨테이너에 등록해주면, 등록된 자바 객체를 스프링 빈이라 한다)
BeanFactory
빈들을 관리한다는 의미로 컨테이너를 빈 팩토리(BeanFactory)라고 부른다.
빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 한다.
Bean을 조회할 수 있는 getBean() 메소드가 정의되어 있다.
ApplicationContext
BeanFactory기능을 포괄하며 추가적으로 여러가지 기능을 추가한 것이라고 볼 수 있다.
Bean을 등록, 생성, 조회, 반환 관리하는 기능은 BeanFactory와 같다.
스프링의 각종 부가 기능을 추가로 제공한다. (ex. 리스너로 등록된 빈에게 이벤트 발생, 환경변수 구분 등)
스프링 빈(Bean
)은 스프링 컨테이너에 의해 관리되는 자바 객체(POJO)를 의미한다.
스프링 빈은 별다른 설정을 주지 않으면 기본적으로 싱글톤 빈이 된다. 싱글톤 빈은 스프링 컨테이너에서 한 번만 생성되며, 컨테이너가 사라질 때 제거된다.
스프링이 주로 적용되는 대상은 자바 엔터프라이즈 서버환경 이다. 자바 엔터프라이즈 서버환경이란, 서버 하나당 최대로 초당 수십에서 수백번씩 브라우저나 다른 시스템으로부터의 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경이다.
만약 매번 클라이언트에서 요청이 올때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다면, 부하가 걸리게 되면서 서버가 감당하기 힘들어진다. 그래서 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유하여 동시에 사용한다.
결국, 요청이 들어올 때마다 매번 객체를 생성하지 않고, 이미 만들어진 객체를 공유하기 때문에 효율적인 사용이 가능하다.
또한 static 메소드나 private 생성자 등을 사용하지 않아 객체지향적 개발을 할 수 있고, 테스트하기 편리하다.
싱글톤 패턴에서 인스턴스는 전체 애플리케이션 중에서 처음 한 번만 초기화되어 애플리케이션이 종료될 때까지 메모리에 남아있는다.
만약 싱글톤이 상태를 갖게 된다면 멀티 스레드 환경에서 동기화 문제가 발생할 수 있다.
(*동기화 문제 : 하나의 자원에 여러 프로세스가 접근할 수 있는 문제)
스프링에서 빈은 별다른 설정(private 생성자, static 변수 등)을 주지 않아도 기본적으로 싱글톤 빈으로 등록되기 때문에 싱글톤 빈은 상태를 가져도 항상 Thread-Safe할 것이라는 착각을 할 수 있다.
하지만 스프링은 싱글톤 레지스트리를 통해 별다른 코드 없이 비즈니스 로직에 집중하고 테스트 코드에 용이한 싱글톤 객체를 제공해 주는 것 뿐이다. 때문에 만약에 싱글톤 빈이 상태를 갖게 되고, 아무런 동기화 처리를 하지 않는다면 멀티 스레드 환경에서 부작용이 발생할 수 있으니 주의해야 한다.
(*싱글톤 레지스트리 : 스프링이 직접 싱글톤 형태의 오브젝트를 관리하는 기능을 제공)
빈이 생성되고, 존재하고, 적용되는 범위를 빈의 스코프라고 한다. 스프링 빈의 기본 스코프는 싱글톤이다.
스프링에서 Singleton과 Prototype 빈 스코프를 제공하고 있으며,
스프링 MVC 웹 애플리케이션을 사용할 경우 웹 스코프를 제공한다.
Singleton Spring Bean의 LifeCycle은 다음과 같다.
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료
이때, 콜백 메서드는 무엇일까?
콜백 메서드를 등록하면 특정 이벤트가 발생했을 때 해당 메소드가 호출된다.(= 콜백)
Bean을 등록하는 방법은 기본적으로 2가지가 있다.
컴포넌트 스캔과 자동 의존관계 설정
@Component
어노테이션을 붙이면 해당 어노테이션으로 등록된 클래스들은 스프링 컨테이너에 의해 자동으로 생성되어 스프링 빈으로 등록된다.
• @Component
의 경우 클래스 또는 인터페이스 단위에 붙일 수 있다.
자바 코드로 직접 스프링 빈 등록
설정 클래스를 따로 만들어 @Configuration
어노테이션을 붙이고,
해당 클래스 안에서 빈으로 등록할 메소드를 만들어 @Bean
어노테이션을 붙여주면 자동으로 해당 타입의 빈 객체가 생성된다.
• @Bean
의 경우 메소드 또는 어노테이션 단위에 붙일 수 있다. 개발자가 컨트롤이 불가능한 외부 라이브러리들을 Bean으로 등록하고 싶은 경우에 사용된다.
[참고]
스프링 핵심원리
https://jobjava00.github.io/language/java/framework/spring/container/
https://vagabond95.me/posts/about-ioc-dip-di/
https://www.youtube.com/watch?v=8lp_nHicYd4&t=294s
https://dev-coco.tistory.com/163?category=1056309
https://kim6394.tistory.com/161 /Spring
https://steady-coding.tistory.com/594
https://devfunny.tistory.com/12 /Bean
https://velog.io/@backtony/면접-시리즈2-Spring-JPA