Search

Spring 기초

Created
2024/11/06 05:03
상태
Done
태그
spring

Spring이란

spring은 스프링의 핵심이 되는 스프링 프레임워크와 여러 기술들을 쉽게 사용할 수 있도록 도와주는 스프링 부트를 기본으로, 그 외의 여러 기능들로 이루어져있다.
스프링 프레임워크
스프링 DI 컨테이너나 AOP 같은 핵심 기술부터, 스프링 MVC와 같은 웹 기술, 트랜잭션이나 JDBC 같은 데이터 접근 기술, 캐시나 이메일 같은 통합 기술과 테스트에 관련된 여러 기능들을 편리하게 사용할 수 있도록 제공한다.
스프링 부트
스프링을 편리하게 사용할 수 있도록 지원하는 기능으로, 최근에는 스프링과 함께 기본으로 사용된다. Tomcat과 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 되고, 손쉬운 빌드 구성을 위한 starter 종속성을 제공한다. 그 외에도 수많은 편의 기능들을 제공한다.
스프링 데이터
데이터베이스와 연결과 CRUD(등록, 조회, 수정, 삭제)를 편리하게 사용할 수 있도록 도와주는 기능으로, 가장 많이 사용되는게 Spring Data JPA이다.
스프링 세션
세션 기능을 쉽게 사용할 수 있도록 도와주는 기능이다.
스프링 시큐리티
Oauth2나 여러 인증과 로그인, 회원가입 등 보안과 관련된 기능을 제공한다.
스프링 Rest Docs
API 문서와 엮어서, 코드의 문서화를 편리하게 도와주는 기능이다.
스프링 배치
스케줄링과 배치 처리에 특화된 기능이다.
스프링 클라우드
클라우드와 관련된 기능이다.
스프링이 좋은 이유는 개발을 편리하게 도와주는 것도 있지만, 객체 지향 언어인 자바 기반의 프레임워크로 좋은 객체 지향 어플리케이션을 개발할 수 있게 도와주기 때문이다.

SOLID를 지키며 설계하기

private final MemberRepository = new MemoryMemberRepository();
Java
복사
위 코드는 인터페이스에 의존하면서 DIP를 잘 지킨 것 같지만, 인터페이스 뿐만 아니라 구현 클래스에도 의존하고 있다. 위 코드에서 Repository를 다른 구현체로 변경하려면 아래와 같이 수정해야한다.
private final MemberRepository memberRepository = new JdbcMemberRepository();
Java
복사
이처럼 구현체가 바뀜으로 인해 코드가 바뀌는 것은 변경하지 않고 확장할 수 있어야한다는 원칙인 OCP에 위배되고 추상에만 의존해야한다는 DIP 원칙에도 위반된다. 또한 위 코드는 Repository의 구현체를 직접 선택하는 책임을 가지게 되어 SRP도 위반하게 된다.
SOLID 원칙을 지키며 작성하려면, 인터페이스에만 의존하도록 변경하면 된다.
private final MemberRepository; public MemberServiceImpl(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
Java
복사
이와 같이 인터페이스만 선언 후 실제 사용하는 곳에서 구현 객체를 대신 생성하고 주입해주는 것이다.
DIP와 OCP를 완성하고 관심사를 분리하기 위해 구현 객체를 대신 생성하고 주입하는 방법은 생성자 주입이 있다.
public class AppConfig { public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } public MemberRepository memberRepository() { return new JdbcMemberRepository(); } }
Java
복사
이처럼 구현체를 선택하는 책임을 가지는 AppConfig 클래스를 만들어 구현체를 주입 시키는 방법으로, 생성자를 통해 구현체를 주입하는 생성자 주입 방식이다.
이렇게 외부에서 의존관계를 주입해주는 것을 의존관계 주입(DI, Dependency Injection) 혹은 의존성 주입이라 한다. 이와 같이 코드를 작성하면 사용 영역의 코드 변경 일체 없이, 구성 영역만 바꾸어 구현체를 바꿀 수 있다. 또한 OCP와 DIP를 지키는 코드이며 구현체를 선택하는 책임을 분리하여 SRP까지 완성할 수 있다.

SOLID와 스프링

위처럼 OCP와 DIP 원칙을 지키며 개발을 하기 위해서는 만들어야 할 것이 너무 많아, 스프링에서 프레임워크로 만들어 제공하게 되었다. 스프링에서는 IoC 컨테이너/DI 컨테이너와 여러 어노테이션으로 SOLID 원칙을 지키며 쉽게 코드를 작성할 수 있도록 지원한다.
제어의 역전(IoC, Inversion of Control)
위 코드 예시를 통해 살펴보면, Member 서비스 내에서는 어떤 Repository가 구현체로 선택될 지 전혀 알 수 없다. AppConfig에서 어떤 구현체를 사용해 선택할지 결정해, 프로그램에 대한 흐름 제어 권한을 가지고 있게 된다.
이와 같이 직접 프로그램의 흐름을 제어하는 것이 아닌, 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.
같은 맥락으로 작성한 코드에서 직접 흐름을 제어한다면 라이브러리라 부르고, 프로그램에서 제어 흐름을 가져가 담당하고 작성한 코드를 콜백 형태로 처리하는 것을 프레임워크라 부른다.
의존관계 주입(DI,Dependency Injection)
의존관계에는 정적인(static) 의존관계와 동적인(dynamic) 의존관계가 있다. 정적인 의존관계는 import나 사용하는 변수들만으로 파악할 수 있는 의존관계를 말하고, 동적인 의존관계는 어플리케이션 실행 시점(런타임)에 외부에서 구현 객체를 생성하고 클라이언트에 전달되는 것이 동적 의존관계로 의존 관계 주입(DI) 혹은 의존성 주입이라 한다.
위 그림은 예시 코드의 다이어그램으로, 실선이 정적인 의존관계이고 점선이 동적인 의존관계이다. 의존 관계 주입을 사용하면, 정적인 의존관계를 변경하지 않으면서 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
IoC 컨테이너, DI 컨테이너
예시 코드의 AppConfig처럼 객체를 생성하고 관리하며 의존관계를 연결해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라 부른다. 의존관계를 주입하는 것에 초점을 맞춰 최근에는 주로 DI 컨테이너라고 불린다. 그 외에도 어셈블러 혹은 오브젝트 팩토리라고도 불리기도 한다.
@Configuration public class AppConfig { @Bean public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } @Bean public MemberRepository memberRepository() { return new JdbcMemberRepository(); } }
Java
복사
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
Java
복사
이처럼 어노테이션과 ApplicationContext를 통해 스프링 빈에 등록된 객체를 찾아 사용할 수 있다. 일반적으로 ApplicationContext을 스프링 컨테이너라 부른다.
스프링은 @Configuration이 붙은 클래스에서 @Bean이 붙은 메서드를 호출해 반환된 결과를 스프링 컨테이너에 스프링 빈으로 등록한다. 각 스프링 빈은 메서드의 이름을 스프링 빈의 이름으로 사용한다.

스프링 컨테이너와 스프링 빈

스프링 컨테이너는 AppConfig 클래스를 받아, 설정 클래스 내부를 돌면서 스프링 빈에 등록한다.
스프링 빈 이름은 일반적으로 메서드 이름을 사용하며, 직접 지정할 수 있다. 주의할 것은 같은 이름으로 등록하면 무시되거나 다른 빈을 덮어버리거나 오류가 발생할 수 있기 때문에, 항상 다른 이름으로 등록해야한다.
스프링 빈 등록 후 스프링 컨테이너는 등록된 스프링 빈 간의 의존 관계를 주입한다.
BeanFactory는 스프링 컨테이너 최상위 인터페이스로, 스프링 빈을 관리하고 조회하는 역할을 담당한다. ApplicationContext는 BeanFactory를 상속 받아 빈을 관리 및 조회하는 기능에 더해, 메세지 소스를 통한 국제화, 로컬-개발-운영 환경에 따른 환경 변수 처리, 이벤트를 발행하고 구독, 외부 리소스를 편하게 조회하는 등 수많은 부가 기능들을 제공한다.
AppConfig와 같이 @Bean으로 등록된 스프링 빈들을 ApplicationContext를 통해 꺼내어 사용할 수 있다. 빈 조회는 빈 이름이나 클래스 타입을 넣어 꺼낼 수 있고, 상속 관계를 가지는 클래스를 조회하면 하위 클래스를 전부 같이 꺼내온다.

싱글톤 컨테이너

대부분 스프링 어플리케이션은 웹 어플리케이션으로, 웹 어플리케이션은 보통 여러 고객이 동시에 요청한다.
이처럼 각 요청마다 service, repository와 같은 객체들을 매번 새로 생성하면 메모리 낭비가 심각하다. 싱글톤 패턴을 통해 1개의 객체만 생성 후 공유하여 사용하도록 설계하여 이런 문제점을 해결할 수 있다.
싱글톤 패턴을 적용하면 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 여러 문제점들을 가지고 있다.
싱글톤 패턴을 구현하는 것에 많은 코드를 작성해야 한다.
의존관계상 클라이언트가 구체 클래스에 의존하여 DIP를 위반한다.
클라이언트가 구체 클래스에 의존하여 OCP를 위반할 가능성이 높다.
싱글톤 패턴을 테스트하기 어렵다.
내부 속성을 변경하거나 초기화, private 생성자로 인한 자식 클래스 생성의 어려움 등 유연성이 떨어진다.
이런 단점들로 인해 싱글톤 패턴은 안티패턴으로 불리기도한다.
스프링에서는 스프링 컨테이너가 싱글톤 컨테이너 역할을 해서, 싱글톤 객체를 생성하고 관리하는 싱글톤 레지스트리를 제공한다. 스프링 빈이 싱글톤으로 관리되는 빈으로, 스프링 컨테이너에서는 싱글턴 패턴을 적용하지 않더라도 객체 인스턴스를 싱글톤으로 관리한다.
스프링 컨테이너의 이런 기능을 통해 객체 인스턴스를 싱글톤으로 관리하면서도 싱글톤 패턴의 단점을 해결하며 싱글톤 패턴을 유지할 수 있다.
싱글톤 패턴을 사용하거나 스프링 같은 싱글톤 컨테이너를 사용할 때는, 싱글톤 객체가 상태를 유지(stateful)하지 않도록 주의해서 설계해야 한다. 특정 클라이언트에 의존적이거나 특정 클라이언트에서 변경할 수 있는 필드를 두면 안된다.
스프링 빈의 필드에서 값을 공유하도록 설계하면 추후 정말 큰 장애가 발생할 수 있고, 이는 장애 발생 시에도 원인을 찾기 굉장히 어렵기 때문에 주의해서 사용하자.
@Configuration public class AppConfig { @Bean public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } @Bean public OrderService orderService() { return new OrderServiceImpl(memberRepository(), discountPolicy()); } @Bean public MemberRepository memberRepository() { return new MemoryMemberRepository(); } }
Java
복사
위의 AppConfig 예시 코드에서는 new MemoryMemberRepository() 메서드가 MemoryMemberRepository 빈을 만들 때와 memberService 빈을 만들때, orderService 빈을 만들 때 모두 호출된다. 하지만 실제 빈을 조회해보면 세 경우 모두 같은 인스턴스를 참고하고 있는 것을 확인할 수 있다.
스프링 컨테이너는 싱글톤 레지스트리로, 스프링 빈이 싱글톤이 되도록 보장해야 한다. 하지만 스프링에서 자바 코드를 직접 수정할 수 있는 방법이 없기 때문에, 클래스를 바이트코드를 조작하는 라이브러리를 사용한다.
@Test void configurationDeep() { ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); //AppConfig도 스프링 빈으로 등록된다. AppConfig bean = ac.getBean(AppConfig.class); System.out.println("bean = " + bean.getClass()); //출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70 }
Java
복사
이와 같이 스프링 빈에 등록된 AppConfig을 꺼내 출력해보면, 실제 AppConfig를 사용하는게 아닌 AppConfig@***CGLIB라는 별도의 클래스가 출력된다. AppConfig에 @Configuration를 적용하면, 스프링 컨테이너에서 바이트코드 조작 라이브러리를 사용하여 AppConfig를 상속받은 별도의 클래스를 만들어 스프링 빈으로 등록한다.
해당 클래스를 통해 스프링 컨테이너가 스프링 빈을 싱글톤으로 관리하는 것을 보장한다. 아마도 아래와 같은 스프링 빈이 존재하면 해당 빈을 꺼내 반환하고 없으면 새로 생성해 등록 후 반환하는 코드를, 바이트조작 라이브러리를 통해 동적으로 생성한다.
@Bean public MemberRepository memberRepository() { if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { return 스프링 컨테이너에서 찾아서 반환; } else { //스프링 컨테이너에 없으면 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환 } }
Java
복사
만약 AppConfig에 @Configuration 어노테이션을 적용하지 않으면, 위의 예시 코드는 MemoryMemberRepository 빈을 만들 때와 memberService 빈을 만들때, orderService 빈을 만들 때 모두 new를 통해 호출되어 싱글톤 패턴이 깨지게 된다. 이처럼@Bean만 사용해도 스프링 빈으로 등록은 되지만, 싱글톤은 보장되지 않는다.

컴포넌트 스캔

스프링 빈 등록을 등록하기 위해서는 XML이나 @Bean을 매번 등록 해주어야하는 번거로움이 있었다. 등록해야 하는 빈이 수십, 수백개가 되면 매번 등록하는 것도 귀찮고, 실수로 누락할 위험성이 있다. 이런 문제를 해결하기 위해 스프링에서는 컴포넌트 스캔이라는 기능과 의존 관계를 자동으로 주입하는 @Autowired라는 기능을 제공한다.
@Configuration @ComponentScan( excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)) public class AutoAppConfig { }
Java
복사
이와 같이 @ComponentScan 어노테이션을 등록하고자하는 AppConfig에 붙여주기만 하면 자동으로 컴포넌트(@Component 어노테이션이 붙은 클래스)를 찾아 스캔한다.
@Component public class MemoryMemberRepository implements MemberRepository {...} @Component public class RateDiscountPolicy implements DiscountPolicy {...} @Component public class MemberServiceImpl implements MemberService {...}
Java
복사
컴포넌트 스캔은 이와 같이 @Component가 붙은 모든 클래스를 스프링 빈으로 등록하며, 스프링 빈의 기본 이름은 클래스 명의 맨 앞글자를 소문자로 바꾼 것을 사용한다. @Component("memberService2")와 같이 빈 이름을 직접 지정하는 것도 가능하다.
컴포넌트 탐색은 기본적으로 @ComponentScan 어노테이션이 붙은 클래스의 패키지부터 하위 패키지들을 순회하며 스캔한다. 그러니 가급정 설정 정보를 포함하는 클래스(위의 AppConfig)를 프로젝트의 최상단(루트)에 두는 것을 권장하고, 스프링 부트도 이 방법을 기본으로 제공한다(@SpringBootApplication 어노테이션이 프로젝트의 루트 위치에 존재). backPackages = {”hello.core”, “hello.service”}와 같이 시작위치를 따로 지정하는 것도 가능하다.
컴포넌트 스캔은 아래의 어노테이션이 붙어있는 클래스를 대상으로 탐색한다.
@Component
@Controller
@Service
@Repository
@Configuration
@ComponentScan 어노테이션에 includeFilters와 excludeFilters를 통해 추가로 스캔할 위치와 스캔에서 제외할 대상을 추가할 수 있다.
ANNOTATION : 기본값, 애노테이션을 인식해서 동작한다. ex) `org.example.SomeAnnotation`
ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다. ex) `org.example.SomeClass`
ASPECTJ : AspectJ 패턴 사용 ex) `org.example..*Service+`
REGEX : 정규 표현식 ex) `org\.example\.Default.*`
CUSTOM : `TypeFilter` 이라는 인터페이스를 구현해서 처리 ex) `org.example.MyTypeFilter`
빈 등록은 컴포넌트 스캔을 통한 자동 빈 등록과 설정 정보 클래스를 통한 수동 빈 등록이 있다. 만약 빈 등록 중 이름이 겹친다면 아래와 같이 동작한다.
자동 빈 등록 vs 자동 빈 등록
ConflictingBeanDefinitionException 예외가 발생한다.
자동 빈 등록 vs 수동 빈 등록
Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
Java
복사
이와 같은 메세지를 출력하며 수동 빈 등록으로 덮어버린다. 이런 경우에는 정말 잡기 어려운 버그가 발생할 가능성이 높기 때문에, 최근에는 스프링 부트에서 빈 충돌이 발생하면 default 설정으로 오류가 발생하도록 하고 있다. spring.main.allow-bean-definition-overriding=true을 통해 인위적으로 덮어쓰기 설정을 해주는 것도 가능하다.

의존관계 주입

AppConfig에서는 @Bean으로 직접 의존관계 주입을 명시 했었는데, @Component 어노테이션을 사용하면 이런 설정 정보 자체가 없기 때문에 의존 관계 주입을 클래스 안에서 해결해야한다.
의존관계 주입 방법
의존관계를 주입해주는 방법은 크게 4가지 방법이 있다.
1.
생성자 주입
@Component public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; @Authwired public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } }
Java
복사
이와 같이 @Autowired 어노테이션을 생성자에 붙이면, 스프링 빈에 등록되어 있는 빈에서 파라미터를 찾아서 의존관계를 자동으로 주입해준다.
의존관계를 주입할 때 기본 주입 전략은 타입이 같은 빈을 찾아서 주입하는 것으로, 이는 getBean(MemberRepository.class)와 동일하다고 보면 된다.
생성자 주입 방식은 생성자 호출 시점에 딱 1번만 호출되는 것이 보장되므로, 불변 객체와 필수 의존관계에 사용된다. 또한 생성자가 딱 1개만 존재하면 @Autowried를 생략해도 자동으로 주입된다.
2.
수정자(setter) 주입
@Component public class OrderServiceImpl implements OrderService { private MemberRepository memberRepository; private DiscountPolicy discountPolicy; @Autowired public setMemberRepository(MemberRepository memberRepository) { this.memberRepository = memberRepository; } @Autowired public setDiscountPolicy(DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy; } }
Java
복사
이와 같이 의존관계 주입이 필요한 필드에 대한 수정자(setter)에 @Autowired를 붙이면, 스프링 빈 전체를 등록 이후에 등록된 빈에서 찾아서 의존관계를 자동으로 주입해준다. 기본 동작은 주입할 대상이 없으면 오류가 발생하고, @Autowired(required = false)를 통해 주입 대상이 없어도 동작하게 만들 수 있다.
수정자 주입 방식은 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법으로, 선택과 변경 가능성이 있는 의존관계에 사용된다.
3.
필드 주입
@Component public class OrderServiceImpl implements OrderService { @Autowired private MemberRepository memberRepository; @Autowired private DiscountPolicy discountPolicy; }
Java
복사
이와 같이 의존관계 주입이 필요한 필드에 직접 @Autowired를 붙여 의존관계를 주입할 수 있다.
코드가 간결해서 쉽게 사용할 수 있지만, 외부에서 변경이 불가능 하다는 점으로 인해 테스트를 작성하기가 힘들다. 의존성 주입이 되지 않은 더미 데이터를 사용할 수 없기 때문에 수정자를 열거나 하는 방식으로 작성해야하는데, 차라리 수정자 주입 방식을 사용하는 것이 낫다. 이 때문에 안티 패턴으로 DI 프레임워크가 없으면 사용하면 안된다.
@SpringBootTest public class OrderServiceTest { @Autowired private MemberRepository memberRepository; @Autowired private DiscountPolicy discountPolicy; @Test void orderServiceTest() { ... } }
Java
복사
@SpringBootTest는 실제 어플리케이션을 올리고 스프링 빈을 전부 등록 후 테스트하기 때문에, 이와 같이 테스트 코드를 작성하는 경우에는 사용해도된다. 가급적 테스트 코드에서만 쓰고 그 외엔 쓰지말자.
4.
일반 메서드 주입
@Component public class OrderServiceImpl implements OrderService { private MemberRepository memberRepository; private DiscountPolicy discountPolicy; @Autowired public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } }
Java
복사
한번에 여러 필드를 주입 받을 수 있지만, 잘 사용되지 않는다.
가급적 생성자 주입 방식을 사용하자! 대부분 의존관계 주입은 한 번 발생한 이후로 종료 시점까지 의존관계를 변경할 일이 없다. 그렇기 때문에 대부분 의존관께는 어플리케이션 종료까지 불변해야한다.
수정자 주입 방식을 사용하면 수정자(setter)를 public으로 열어두어야 한다. 이런 수정자를 통해 실수로 변경할 가능성이 있기 때문에, 변경하면 안되는 필드에 대한 수정자를 열어두는 것 자체가 좋은 설계 방법이 아니다.
생성자 주입을 사용하면 객체를 생성할 때 1번만 호출되고 이후 호출되는 일이 없고, final 키워드와 함께 사용하여 불변하게 설계할 수 있다. 또한 final 키워드를 붙이면 컴파일 타임에 오류를 잡을 수 있어 생성자를 만들 때 실수로 누락할 가능성 없어진다.
lombok을 통한 간편하게 의존관계 주입하기
@Component @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; ... }
Java
복사
이와같이 lombok의 @RequiredArgsConstructor 기능을 사용하면, 해당 클래스의 final 키워드가 붙은 필드들을 파라미터로 받는 생성자를 자동으로 만들어준다. 최근 트랜드는 이를 통해 딱 1개의 생성자만 두어 @Autowired를 생략하는 방식으로 코드를 간결하게 유지한다.
의존 관계 주입 옵션
@Autowired에는 몇 가지 옵션들을 설정할 수 있다.
@Autowired(required=false) : 자동 주입할 대상이 없으면 메서드 자체가 호출이 되지 않는다.
@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
//호출 안됨 @Autowired(required = false) public void setNoBean1(Member member) { System.out.println("setNoBean1 = " + member); } //null 호출 @Autowired public void setNoBean2(@Nullable Member member) { System.out.println("setNoBean2 = " + member); } //Optional.empty 호출 @Autowired(required = false) public void setNoBean3(Optional<Member> member) { System.out.println("setNoBean3 = " + member); }
Java
복사
의존 관계 주입 시 조회된 빈이 2개 이상인 경우
하나의 인터페이스에 두 개 이상의 구현체를 컴포넌트로 등록한 경우에, 의존 관계 주입 시 NoUniqueBeanDefinitionException이 발생한다. 이를 해결하는 방법은 3가지가 있다.
1.
@Autowired 필드명 매칭
@Autowired private DiscountPolicy rateDiscountPolicy;
Java
복사
위처럼 등록된 스프링 빈의 이름과 동일하게 필드명을 설정해두면, 타입 매칭을 시도 후 여러 빈이 조회 되었을 때 해당 필드명과 동일한 스프링 빈을 찾아 주입한다.
2.
@Qualifier 매칭
@Component @Qualifier("mainDiscountPolicy") public class RateDiscountPolicy implements DiscountPolicy { ... } @Component @Qualifier("fixDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy { ... } @Autowired public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { ... }
Java
복사
@Qualifier는 위와 같이 추가 구분자를 두어 주입 시 동일한 Qualifier가 등록된 빈을 찾아 주입한다.
3.
@Primary 사용
@Component @Primary public class RateDiscountPolicy implements DiscountPolicy {} @Component public class FixDiscountPolicy implements DiscountPolicy {}
Java
복사
@Primary는 우선순위를 정하는 방법으로, 여러 빈이 매칭되면 @Primary가 붙은 빈이 우선권을 가진다.
@Qualifier와 @Primary가 동시에 적용되어 있는 경우에는 @Qualifier가 우선권을 가져간다. 스프링에서는 대부분 자동보다 수동, 넓은 범위보다는 좁은 범위의 선택이 우선 순위가 높다.
인터페이스의 모든 빈 가져오기
@Autowired public DiscountService(Map<String, DiscountPolicy> policy Map) { ... }
Java
복사
위와 같이 생성자에 주입 받고자하는 인터페이스를 Map이나 List로 받으면, 해당 인터페이스의 구현체 빈들을 모두 담아서 주입해준다.

빈 생명주기(Life-cycle)에 따른 콜백 함수

데이터베이스의 커넥션 풀이나 네트워크 소켓처럼, 어플리케이션 시작 시 필요한 연결을 확보해두고 사용하다가 종료 시 연결을 종료하는 작업을 진행하기 위해서는 적절할 시기에 초기화 작업과 종료 작업을 수행해야한다.
스프링 빈에서는 빈이 생성되고 의존관계 주입까지 끝난 후에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업이 필요하다면 스프링 빈의 의존관계 주입이 완료된 후 수행해야한다. 스프링에서는 스프링 빈의 의존관계 주입이 끝나면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 기능을 제공한다. 또한 스프링 빈이 종료되기 직전에 종료 시점을 알려주는 콜백도 제공한다.
이에 따라 스프링 빈의 이벤트 라이프 사이클을 표현하면 다음과 같다.
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 종료 콜백 → 스프링 컨테이너 종료
스프링 컨테이너와 무관하게 생명주기가 짧은 빈들도 있는데, 빈이 종료되기 전 콜백이 발생한다.
스프링에서 제공하는 초기화 콜백과 종료 콜백 기능을 지원하는 방법은 3가지가 있다.
1.
인터페이스 방식(InitializingBean, DisposableBean)
public class NetworkClient implements InitalizingBean, DisposableBean { ... @Override public void afterPropertiesSet() throws Exception { // 초기화 콜백 } @Override public void destroy()) throws Exception { // 종료 콜백 } }
Java
복사
이와 같이 InitializingBean와 DisposableBean 인터페이스를 상속받아, 초기화 콜백과 종료 콜백을 재정의(override)하여 사용할 수 있다.
위의 두 인터페이스는 스프링 전용 인터페이스로, 스프링 전용 인터페이스에 의존해야하고 메서드 이름을 변경할 수 없다. 스프링 초창기에 나온 방법들로 지금은 더 나은 방법들로 인해 거의 사용되지 않는다.
2.
설정 정보에서 초기화 메서드, 종료 메서드 지정
@Bean(initMethod = "init", dstroyMethod = "close") public NetworkClient networkClient() { ... }
Java
복사
이와 같이 빈 등록 할 때 @Bean 어노테이션에 초기화 콜백으로 사용할 메서드와 종료 콜백으로 사용할 메서드를 메서드 명으로 지정할 수 있다.
메서드 이름을 자유롭게 설정할 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다. 또한 코드가 아니라 설정 정보(메서드 명)를 사용하기 때문에 코드를 고치지 못하는 외부 라이브러리를 사용할 때도 적용할 수 있다.
destroyMethod 속성에는 추론 기능을 포함하고 있어, 종료 콜백으로 사용할 메서드 이름이 close나 shutdown이라면 따로 지정하지 않아도 자동으로 호출해준다. 따라서 종료 콜백을 등록하지 않아도 추론해서 잘 동작하며, 추론 기능을 사용하기 싫으면 destroyMethod=””처럼 빈 공백을 넣어주면 된다.
3.
@PostConstruct, @PreDestroy 어노테이션
public class NetworkClient { ... @PostConstruct public void init() { // 초기화 콜백 } @PreDestroy public void close() { // 종료 콜백 } }
Java
복사
이와 같이 @PostConstruct와 @PreDestroy 어노테이션을 초기화 콜백과 종료 콜백으로 사용할 메서드에 달아주어 지정할 수 있다.
최신 스프링에서 가장 권장하는 방법으로, 어노테이션만 붙이면 되므로 아주 간편하다. 스프링 종속적인 기술이 아니라 JSR-250 자바 표준이라 스프링이 아닌 다른 컨테이너에서도 동작하고, 컴포넌트 스캔 방식과 잘 어울리는 방식이다.
다만 외부 라이브러리 사용 시에는 적용할 수 없는데, 외부 라이브러리에 초기화 콜백과 종료 콜백을 지정하려면 @Bean 기능을 사용해야만 가능하다.

빈 스코프

우리가 알고 있는 스프링 빈이 스프링 컨테이너 시작과 함께 생성되어 종료될 때까지 유지되는 것은, 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 여기서의 스코프는 스프링 빈이 존재할 수 있는 범위를 뜻하는 것으로, 어느 시점까지 해당 스프링 빈을 유지하는가를 말한다.
스프링이 제공하는 스코프는 다음과 같다.
싱글톤 스코프 : 기본 스코프로, 스프링 컨테이너의 시작부터 종료까지 유지되는 스코프
프로토타입 스코프 : 스프링 컨테이너에서 빈의 생성과 의존관계 주입, 초기화 콜백까지만 관여하고, 그 이후에는 더 이상 관리하지 않는 스코프
웹 관련 스코프
request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
websocket : 웹 소켓이 연결되고 종료되기까지 유지되는 스코프
1.
싱글톤 스코프
싱글톤 스코프의 빈을 조회하면, 위 그림처럼 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
@Scope("singleton") // 생략 가능 static class SingletonBean { @PostConstruct public void init() { ... } @PreDestroy public void destroy() { ... } } @Test public void singletonBeanFind() { AnnotationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class); SingletonBean singletonBean1 = ac.getBean(SingletonBean.class); SingletonBean singletonBean2 = ac.getBean(SingletonBean.class); System.out.println("singletonBean1 = " + singletonBean1); System.out.println("singletonBean2 = " + singletonBean2); assertThat(singletonBean1).isSameAs(singletonBean2); ac.close(); //종료 }
Java
복사
위와 같이 테스트를 진행해보면, 빈 초기화 메서드를 실행하고 동일한 인스턴스의 빈을 조회해온 후 종료 콜백까지 정상적으로 호출된다.
2.
프로토타입 스코프
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리하기 때문에, 위 그림처럼 매 호출마다 새로운 프로토타입 빈을 생성하여 클라이언트에 반환한다. 클라이언트에 빈을 반환 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않고, 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트가 가진다.
@Scope("prototype") static class PrototypeBean { @PostConstruct public void init() { ... } @PreDestroy public void destroy() { ... } } @Test public void prototypeBeanFind() { AnnotationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); PrototypeBean singletonBean1 = ac.getBean(PrototypeBean.class); PrototypeBean singletonBean2 = ac.getBean(PrototypeBean.class); System.out.println("singletonBean1 = " + singletonBean1); System.out.println("singletonBean2 = " + singletonBean2); assertThat(singletonBean1).isNotSameAs(singletonBean2); ac.close(); //종료 }
Java
복사
프로토타입 빈은 의존 관계 주입 및 초기화 콜백 이후 스프링 컨테이너에서 관리하지 않기 때문에, 조회 해올 때마다 스프링 컨테이너는 새로운 빈을 생성하여 반환한다. 또한 같은 이유로, 스프링 컨테이너가 종료되었음에도 해당 빈들의 종료 콜백이 호출되지 않는다.
위 그림처럼 만약 이런 프로토타입 빈을 싱글톤 빈 내부에서 사용하면 의도한대로 동작하지 않을 수 있다. 이와 같은 구조에서는 싱글톤 빈 생성 후 의존 관계 주입 시에 내부의 프로토타입 빈을 생성해서 주입 받는다. 그렇기 때문에 클라이언트 A가 반복해서 요청해도 동일한 프로토타입 빈만 사용하게 된다.
싱글톤 빈 내부에서 매번 새로 생성된 프로토타입 빈을 사용하고 싶다면, 의존 관계를 외부에서 주입 받는 것이 아니라 직접 필요한 의존 관계를 찾아야한다. 이렇게 의존 관계를 찾는 것을 의존 관계 조회 혹은 탐색(DL, Dependency Lookup)이라 부르고, DL에는 몇 가지 방법이 있다.
스프링 컨테이너에 요청하기
클라이언트에서 요청이 들어올 때마다, 싱글톤 빈 내부에서 ApplicationContext의 getBean 메서드로 직접 스프링 컨테이너에 빈을 요청하는 방법이다.
static class ClientBean { @Autowired private ApplicationContext ac; public int logic() { PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); ... } }
Java
복사
다만 이 방법은 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워지게 된다.
ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공해주는 ObjectProvider를 사용하는 방법이다. 참고로 ObjectFactory에서 편의 기능이 더해진 것이 ObjectProvider이다.
static class ClientBean { @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); ... } }
Java
복사
스프링이 제공하는 DL 기능으로, 기능이 단순하여 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
Provider
JSR-330 자바 표준에서 제공하는 DL 기능인 Provider를 사용하는 방법이다.
static class ClientBean { @Autowired private Provider<PrototypeBean> provider; public int logic() { PrototypeBean prototypeBean = provider.get(); ... } }
Java
복사
스프링에서 제공하는 ObjectProvider보다 편의 기능은 적지만, 기능이 적어 더 단순하다는 점과 자바 표준이라는 장점이 있다.
이러한 ObjectProvider나 Provider는 프로토타입 빈을 사용할 때 뿐만아니라, DL 기능이 필요하다면 언제든 사용할 수 있다.
스프링 vs 자바 표준 위의 @PostContruct, @PreDestroy나 Provider 같은 경우에는 스프링에도 기능을 지원하고 자바 표준에서도 기능을 지원한다. 이런 경우에는 어느 쪽을 선택할지는 환경에 따라 선택하면 된다. 스프링이 아니라 다른 컨테이너에서도 동작하기를 원한다면 자바 표준을 사용해야하고, 그렇지 않다면 대부분 더 편리하고 다양한 기능을 지원하는 스프링이 제공하는 기능을 사용하면 된다. 만약 @PostContruct, @PreDestroy처럼 스프링에서 자바 표준을 사용하기를 권장한다면, 자바 표준을 사용하는 것이 좋다.
3.
request 스코프
request 스코프를 포함한 웹 스코프들은 웹 환경에서만 동작하고, 프로토타입 스코프와 다르게 스프링 컨테이너에서 해당 스코프의 종료 시점까지 관리한다. 그렇기 때문에 종료 콜백 또한 따로 호출할 필요없이, 빈 라이프 사이클에 따라 자동으로 호출된다.
request 스코프는 위 그림처럼 request 요청이 들어오는 시점부터 응답을 보내기까지 유지되는 스코프이다.
@Component @Scope(value = "request") public class MyLogger { private String uuid; public void log(String message) { System.out.println("[" + uuid + "]" + message); } @PostConstruct public void init() { uuid = UUID.randomUUID().toString(); System.out.println("[" + uuid + "] request scope bean create:" + this); } @PreDestroy public void close() { System.out.println("[" + uuid + "] request scope bean close:" + this); } }
Java
복사
위는 HTTP 요청이 들어왔을 때, request 스코프를 통해 로그를 남기는 예시 코드이다. 위 MyLogger 클래스의 스프링 빈은 HTTP 요청이 올 때마다 새로 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
@Controller @RequiredArgsConstructor public class LogController { private final LogService logService; private final ObjectProvider<MyLogger> myLoggerProvider; @RequestMapping("log") @ResponseBody public String logDemo(HttpServletRequest request) { MyLogger myLogger = myLoggerProvider.getObject(); myLogger.log("controller logDemo is called"); ... } }
Java
복사
이와 같이 스코프 빈을 가져와 사용할 수 있다. 주의할 점은 request 스코프 빈은 빈 생성 및 의존 관계 주입 시점에는 해당 빈이 생성되어 있지 않다는 것이다. 이 때문에 바로 MyLogger를 주입 받아서 사용하면 오류가 발생하고, 위에서 살펴본 DL 기능을 통해 요청이 들어올 때까지 빈의 생성을 지연시켜 사용해야 한다.
여기에 프록시 방식을 도입하여 코드를 더 간결하게 사용할 수 있다.
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
Java
복사
이와 같이 request 스코프 빈에 proxy를 설정하면, 의존 관계 주입 시에 MyLogger 클래스를 상속받는 프록시 객체를 주입한다.
이를 통해 아래와 같이 코드를 간결하게 줄일 수 있다.
@Controller @RequiredArgsConstructor public class LogController { private final LogService logService; private final MyLogger myLogger; @RequestMapping("log") @ResponseBody public String logDemo(HttpServletRequest request) { myLogger.log("controller logDemo is called"); ... } }
Java
복사
이 프록시 객체는 실제 요청이 들어와 메서드가 호출될 때, 프록시 객체 내부에서 실제 빈을 찾아 요청을 위임한다. 원본 객체를 상속하여 만들어졌기 때문에, 클라이언트에서는 프록시 객체의 메서드를 호출하는지 원본 객체의 메서드를 호출하는지 알 필요없이 동일하게 사용할 수 있다(다형성).
프록시 객체는 request scope와 상관이 없고, 단순히 내부 위임 로직을 포함한 싱글톤처럼 동작한다. 이 때문에 테스트를 작성하는 것이 어려워지고, 무분별하게 사용하면 유지보수가 어려워진다.

참고