Spring 기초 개념(POJO / Spring Bean / Dependency Injection)

YongJin·2024년 8월 12일

Spring

  • POJO (Plain Old Java Object > 순수한 자바 객체!)

    순수하다 : 특정 프레임워크나 라이브러리에 종속되지 않는
    즉 pojo는 특정 프레임워크나 라이브러리에 종속되지 않는 순수한 자바 객체이다

    순수한 자바 객체가 spring과 무슨 관련이 있는가?

    spring이전에 EJB(Enterprise Java Bean)라는 기술이 자바 엔터프라이즈 애플리케이션 시장을 독점하고 있었으나 사용이 엄청나게 불편했다 (코드들이 EJB에 엄청나게 종속적이게 된다 > 예를 들면 작성하는 메서드에 필요없는 EJB 코드들도 @Override해서 만들었어야 했다)

pojo와 spring의 등장

  • 마틴 파울러는 당시 인기를 끌던 EJB처럼 복잡하고 제한적인 기술보다는 자바의 단순 오브젝트를 이용해 비즈니스 로직을 구현하는 편이 낫다고 생각했다. 그럼에도 개발자는 왜 자바의 단순한 객체를 사용하길 꺼리는지 궁금해했는데, 그 이유를 찾아보니 그럴싸한 이름이 없기 때문이였다. 그래서 2000년에 마틴 파울러가 컨퍼런스 발표를 준비하다가 뭔가 있어보이도록 만든 이름이 바로 POJO(Plain Old Java Object)였고, 이는 기대 이상으로 성공적이였다.
    (프레임워크나 라이브러리에 종속되지 않아 테스트와 유지보수도 비교적 용이하다)

  • 로드 존슨은 EJB의 문제점을 지적하면서 EJB 없이도 고품질의 애플리케이션 개발할 수 있다는 내용과 예제 코드를 선보였다. 그리고 이 책을 읽은 개발자 유겐 휠러(Uergen Hoeller)와 얀 카로프(Yann Caroff)가 로드 존슨에게 오픈 소스 프로젝트를 제안했고, 2004년에 탄생한 것이 바로 스프링 프레임워크이다. 실제로 해당 책의 내용을 보면 BeanFactory, ApplicationContext 등 스프링의 기본이 되는 코드들이 모두 담겨있다고 한다

(출처 : https://mangkyu.tistory.com/281)

그래서 Spirng과 pojo는 무슨 연관이 있는건가?

스프링 핵심 개발자들이 함께 쓴 Professional Spring Framework라는 책이 있다. 이 책에서 스프링의 핵심 개발자들은 아래와 같이 얘기하고 있다." 스프링의 정수(精髓)는 엔터프라이즈 서비스 기능을 POJO에 제공하는 것 "엔터프라이즈 서비스는 보안, 트랜잭션과 같은 엔터프라이즈 시스템에서 요구되는 기술들을 의미한다. 이런 기술을 POJO에 제공한다는 말은 엔터프라이즈 서비스 기술과 POJO라는 애플리케이션 로직을 담은 코드를 분리하겠다는 뜻이기도 하다. 그리고 분리가 되었지만 반드시 필요한 엔터프라이즈 서비스 기술을 POJO방식으로 개발된 애플리케이션 핵심 로직을 담은 코드에 제공한다는 것이 스프링의 가장 강력한 특징과 목표이다.


Spring과 Spring Boot는 무슨 차이?

Spring에서 수동으로 직접 설정해야 했던 것들을 Spring Boot는 이미 자동으로 처리해놓았다 > 훨씬 사용하기 간편해서 Spring Boot 쓴디.
밑의 설명에서 말하는 스프링은 모두 스프링 부트이다

  • 톰캣을 직접 설치할 필요 없음
  • (추후 설명)

Spring Bean

Spring Bean / Spring Ioc Container / 제어의 역전? 이 무엇인가?
일단 설명 전에 순수한 자바로 코드를 짜보자 (Spring이 웹 어플리케이션 서버를 만드는데 사용하는 프레임워크 인 것은 맞으나 위 개념은 웹과는 연관성이 적고 개발을 편리하게 해주는 개념이므로)

주문 서비스 클래스에서 기획이 바뀌어서 정액 할인 정책을 정률 할인 정책으로 바꾸어야 하는 상황을 가정해보자

public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; //10%
@Override
public int discount(Member member, int price) {
	if (member.getGrade() == Grade.VIP) {
		return price * discountPercent / 100;
	} else {
		return 0;
	}
  }
}

할인 정책 변경

public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

인터페이스를 사용한다 하더라도 new로 RateDiscountPolicy의 인스턴스에 의존해야 한다. 결국 객체지향의 SOLID 원칙 (OCP,DIP)을 지키지 못하게 된다 > 개발자가 직접 new 로 인스턴스를 만들어 내고 있으니깐 말이다
기대한 건 이러한 구조인데

실제는 이렇게 되어버리고 말았다.

이렇게 되어 버린 것이다
어떻게 해결할 수 있을까?

public class AppConfig {

	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}

	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(),discountPolicy());
	}
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}	
	public DiscountPolicy discountPolicy() {
		return new FixDiscountPolicy();
	}
}

이렇게 AppConfig 클래스를 구현해서 외부에서 인스턴스를 만들어서 넣어준다면 인스턴스를 직접 new 로 생성해주어야 하는 문제를 해결할 수 있을 것이다.'

할인 정책이 바뀌어도 Appconfig의 코드만 바꿔주면 되고 다른 영역은 영향을 받지 않는다.

이렇게 프로그램의 제어 흐름을 직접 제어하는게 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.


그럼 순수한 자바로도 컨테이너 만들 수 있는거 아닌가??
Spring을 사용하면 AppConfig의 역할을 스프링이 해주고(컨테이너 클래스를 직접 안 만들어도 된다, 필요하면 만들어야 겠지만)객체간의 의존 관계도 알아서 맺어준다.
순수한 자바로 만들 때와 다른 점은 Spring은 컨테이너 클래스에 등록된 Bean들을 싱글턴으로 관리해 준다는 점이다


Spring Bean 과 Spring Container(Spring Bean을 관리하는 컨테이너)
설명하기 전에 코드로 먼저 본다면

설명을 위해서 강의를 보고 만든 클래스(AppConfig)이고 이를 ApplicationContext 인터페이스로 선언하고 AnnotationConfigApplicationContext의 매개변수에 넣어 컨테이너를 생성했다.
컨테이너에 만들어놓은 클래스들을 반환해주는 메서드를 생성하고 직접 @Bean으로 등록시키면 컨테이너에 빈이 등록된다.
(웹 서비스를 위한 목적으로 프로그램을 만든다면 직접 컨테이너 클래스를 만들지 않아도 된다.> 스프링이 만들어준다)

정리하자면

  • Spring Bean
    스프링 컨테이너에서 관리되는 객체를 말한다. 스프링 컨테이너에 의해서 인스턴스화되고 관리된다
  • Spring Container
    (Bean을 넣어놓고 관리하는 그릇(?) 아님 말 그대로의 컨테이너) 애플리케이션 내의 Bean들의 생명주기를 관리한다
    ApplictionContext란 인터페이스로 선언한다
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • (Spring Boot에서 Container)
    실행 클래스에서 @SpringBootApplication 어노테이션이 자동으로 컨테이너를 만들어준다

Bean으로 등록하는 방법들 (XML / @Configuration 어노테이션을 사용한 클래스에 등록하기 / (@Component, @Service, @Repository) Annotation 사용

  • xml방식 > 레거시 아니면 이제는 쓰이지 않음
  • @Configuration 어노테이션을 사용한 클래스에 등록하기 > 위 코드 이미지 참고
  • 컴포넌트 스캔과 의존관계 자동 주입
    실제로 등록해야 하는 빈들이 수백개가 된다면 과면 일일이 @Bean어노태이션을 달아줄 수 있을까? (설정 클래스의 코드도 커지고 누락될 경우도 있음) 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다
    설정 클래스에 @ComponentScan 어노테이션 걸어주고 스프링 빈으로 등록됐으면 하는 클래스들에 @Component어노테이션 걸어주면 스프링 빈으로 등록이 된다 > 의존관계 주입은 어떻게 되는거지??? > 자동 의존관계 주입 생성자 위에다 @Autowired 어노테이션 걸어주어야 한다 그런데 이때 생성자 메서드가 하나만 존재할 경우 @Autowired 어노테이션이 생략 가능하다

** 추가적으로 AnnotaitonConfigurationContextBean을 new로 생성할 때 매개변수 자리에 bean이 될 클래스를 넣어줘도 bean으로 등록이 되긴 한다

Bean의 요구 조건
1. Java 클래스로 정의되어야 한다 (스프링 컨테이너에 의해 관리되고 인스턴스화 된다)
2. Spring Bean으로 등록될 수 있는 어노테이션이 있어야 한다 (xml로도 등록 가능한데 불편해서 잘 쓰이지 않는다) > @Component, @Service, @Repository,
@Controller 아니면 스프링 컨테이너 클래스 안에 @Bean으로 설정
3. 필요한 경우 의존성 주입이 되어야 한다. (Bean은 필요에 따라 다른 Bean에 대한 의존성을 주입받아야 한다)
4. 범위가 설정 되어야 한다.

Bean의 관리 방식 (기본적으로는 Singleton으로 관리된다 / 기타 나머지는?)

  • Singleton
    기본적으로 Bean이 singleton으로 관리되는 이유는 그래야 Bean을 하나만 생성한다 클라이언트 요청 있을 때 마다 Bean이 생성되면 메모리가 감당하지 못할 수 있다.
    싱글턴으로 관리되는 Bean이면 클래스 필드 잘 고려해서 사용해야 한다. 싱글턴으로 관리되는 객체에서 필드 값이 사용자마다 바뀌어 버리면 a라는 유저가 1000원을 내야 하는데 b라는 유저가 2000원을 내야 하는 걸 a유저가 내게 되는 끔찍한 상황이 벌어질 수 있다.
    지금이야 간단해 보이겠지만
    Bean**이 몇 백개가 되고 클래스 필드가 문제를 일으키는 상황이 온다면 문제 원인을 찾아내기 힘들어 질 수 있다.
    즉, 스레드 세이프하지 않다고 할 수 있고 지역변수와 메서드의 매개변수를 활용해서 코드를 짜야 한다
  • prototype Bean
    프로토타입의 빈은 요청할 때 마다 새로운 인스턴스가 생성된다. 즉, 각 요청은 독립적인 Bean 인스턴스를 받게 된다.
    (주의점)
    싱글톤으로 관리되는 빈에 주입이 되는 프로토타입 빈은 싱글턴 빈을 요청한다고 해서 새롭게 프로토타입 빈이 주입되는게 아니니 새로운 프로토 타입 빈을 기대하는 로직을 만들어야 하는 경우 주의할 것 (싱글턴으로 관리되어지는 빈이 생성되는 시점에 프로토타입 빈이 주입되어진다 > 계속 같은 프로토타입 빈을 사용하는 것이다)

해결하는 방법이 applicationContext를 @Autowired하고 컨테이너에 등록된 prototypeBean을 새롭게 생성되길 원하는 메서드안에서 getBean하면 해결이 되긴 하는데... 이렇게 되면 DI(Dependency Injection)이 아니라 DL(Dependncy Lookup > 의존관계 탐색)이 되고 스프링 컨테이너에 종속적이게 된다
스프링에는 지정된 프로토타입 빈을 찾아주는 기능도 있다 (ObjectiveProvider) > 스프링에 의존적
그래서 JSR-330 Provider 라는 라이브러리가 존재한다 (자바 라이브러리)

  • Requset

그래서 프로토타입 빈과 Request는 언제 사용해야 하는거자?

현실적으로 쓰지 않는다, 쓸 일이 없다 > 생명주기를 따로 관리해야 빈들은 어떻게 관리해야 할까?
빈으로 등록시키지 않는다. spring의 컨테이너에 빈을 등록시키는 가장 큰 이유는 pojo에 역할을 부여해주기 위해서이다(의존성 주입의 경우 중요한 역할을 해주지만 부가적인 역할이다)
그렇다면 spring의 시작과 끝과는 다른 생명주기를 가진다는 말은 스프링과 관계가 없는 기능일 가능성이 매우 크다. > 그럴거면 굳이 스프링에 의존할 필요가 없다 자바 코드로 직접 작성하고 끼워넣는다


  • Dependency Injection(의존 관계 주입) (Spring에서 지원하는 Dependency Injection)

의존 관계 / 의존 관계 주입 ?

  • 의존 관계
    A가 B를 사용하고 있는 경우
    "의존대상 B가 변하면, 그것이 A에 영향을 미친다." 이것이 의존관계이다 >
    위 코드 사진을 다시 가져와보면

    MemberServiceImpl 클래스를 만드는 생성자 메서드에 MemberRepository 가 필요하다
    이 경우 MemberServiceImpl이 MemberRepository에 의존한다라고 한다
    이 코드에서 MemberRepository는 인터페이스인 것을 살피고 넘어가자

  • 의존 관계 주입
    스프링 컨테이너에 의존하는 클래스 Bean이 등록되어 있어야 하고 의존 관계를 맺는 클래스 쪽에서 @Autowired 어노테이션을 선언하면 (생성자 메서드가 하나이면 @Autowired 어노테이션 생략 가능함) > 요즘은 생성자 주입 방식만이 권장된다
    스프링 컨테이너가 알아서 의존 관계를 맺는 클래스의 인스턴스(Bean)를 넣어준다.
    사진의 코드는 내가 졸업작품으로 만든 코드에서 가져왔다 (클래스의 모든 메서드는 설명의 맥락 및 이미지의 크기상 다 가져오지 않았다)



    실수로 ManagerService에 롬복을 달지 않았지만, 중요한 점은 나는 이 코드를 만들면서
    직접 new를 사용해서 Controller 클래스 코드에서 ManagerService 뿐만 아니라 다른 Service클래스의 인스턴스를 선언해서 사용한 적이 없다.
    그리고 위에서 설명하지 않았지만 @ComponentScan 어노테이션과 @Component 어노테이션이 없는 이유는

    스프링의 메인 실행 클래스의 @SpringBootAppliation어노테이션이 @ComponentScan과 @Configuration등 다양한 어노테이션을 내장하고 있다. 그리고
    @Controller / @Service / @Repository 어노테이션 모두가 @Component 어노테이션을 내장하고 있다 > 컨테이너도 만들어져 있고
    빈 등록도 자동으로 해준다

그래도 수동으로 컨테이너 클래스와 빈 등록이 필요는 할 거 같은 순간이 있을거 같은데...

  • 이런 경우가 존재한다. > 외부 라이브러리를 불러와 사용하는 경우 개발자가 어노테이션을 붙여줄 수 없다. 따라서 @Configuration으로 컨테이너를 직접 만들고 빈으로 등록이 필요하다
  1. Field Injection: 필드에 @Autowired 어노테이션을 붙여 의존성을 주입 (비권장) > 이미 코드에 사용하는 순간 부터 IDE가 쓰지 말라고 경고한다. 그리고 테스트 코드를 짤 수도 없다. 테스트 코드를 만들 때는 생

  2. Setter Injection: 세터 메소드를 통해 의존성을 주입 (비권장)

3. Constructor Injection: 생성자를 통해 의존성을 주입한다. 최근에는 이 방식이 권장된다(순환 참조를 막아준다)

(출처 및 참고 : https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/)

  • 나머지 의존 관계 주입은 왜 권장되지 않는가? 순환 참조? 생성자 주입만이 어떻게 순환 참조를 막아주는가?

(나머지 주입들이 권장되지 않는 이유)
일단 셋 다 Runtime에러가 터져버린다 하지만 생성자 주입 방식은 인스턴스의 생성 자체가 실패하기 때문에 스프링을 실행시키면서 바로 알아차릴 수 있다. 그러나 Field Injection과 Setter Injection은 의존 관계를 맺는 클래스가 빈으로 등록 되어 있지 않더라도 인스턴스가 생성되다가 사용시에 Runtime에러가 터져버린다 > 일일이 실행시키면서 문제가 생긴 부분을 찾아야 한다

(순환 참조)
2개의 클래스의 서로가 의존관계를 맺어서 돌고 도는 현상 >
A클래스를 만드는데 B가 필요하고 B클래스를 만드는데 A클래스가 필요
"필드 주입이나, 수정자 주입은 객체 생성시점에는 순환참조가 일어나는지 아닌지 발견할 수 있는 방법이 없다."

(생성자 주입은 어떻게 순환참조를 막아주는가?)
컨테이너가 빈을 생성하는 시점에 객체 생성에 사이클관계가 생기기 때문이다.
정상 작동되게 끔 막아주는게 아니다! 애플리케이션 구동을 하기 전 컨텍스트 에러를 일으켜서 어느 부분에서 문제가 났는지 알려준다(즉, 순환 참조 자체를 만들지 말아야 한다)

profile
더 나은 사람이 되고 싶습니다

0개의 댓글