
스프링 핵심 원리 - 기본편
노트 작성자
Mo Gi
객체 지향 설계와 스프링
스프링이란?
아무리 기술이 복잡하고 개념이 많더라도 처음에 나오는 핵심 컨셉은 굉장히 단순하게 시작한다. Spring도 처음이는 3만줄의 코드로 시작했지만 현재는 모든 것을 다 알기 힘들 정도의 거대한 생태계를 자랑한다.
왜 Spring이 좋은 기술이되었고 무엇때문에 이렇게까지 많은 프레임워크와 기술들이 발달하게 되었는지를 알아야한다. 이 프레임워크가 발달한 배경과 핵심컨셉을 알아야 맥락을 파악할 수 있게되고 왜 이런 API들을 사용해야하는지에 대해서 알게 되는 것이다. 이 부분이 굉장히 중요하다.
Spring은 객체지향언어가 가진 강력한 특징을 살려내는 프레임워크로, 좋은 객체 지향 어플리케이션을 개발할 수 있도록 도와주는 프레임워크라는 점에서 우리는 객체 지향적인 프로그래밍적 사고를 가져야될 것이다.
좋은 객체 지향 설계의 5가지 원칙(SOLID)
SOLID : 좋은 객체 지향 설계의 5가지 원칙 by 로버트 마틴
1. SRP (Single Responsibility Princible)
- 한 클래스는 하나의 책임을 가져야한다.
- 여기서 책임이라는 단어가 모호한 단어로 보일 수 있는데, 얼마나 책임을 져야하는지가 문맥과 상황에 따라 다르기 때문
- 중요한 기준은 변경이다. 변경이 있을 때 파급효과가 적으면 이 원칙을 잘 따른 것
2. OCP (Open/Closed Principle)
- 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다(?)
- 자동차가 디젤차를 전기차로 바꿔도 운전자는 영향받지 않는다. 공연도 역할과 구현이 나뉘어져 있어 서로 영향받지 않는다. 이것이 OCP이다. 변경을 하지말고 다형성을 적극적으로 활용하여 확장을 하고 변경을 하지 않도록 설계하여야 한다.
하지만 OCP에는 문제점이 있다. 구현 객체를 변경하기 위해서는 클라이언트 코드를 변경해야한다는 점이다. 디젤차를 전기차로 바꾸려면 모듈을 전기차로 바꾸어야하는데, 이 과정에서 코드가 변경된다는 것, 클라이언트의 코드를 바꿔야한다는 것이 원칙을 위반하게 된다는 것이다.
따라서 이 원칙을 깨지 않고 모듈을 바꾸기 위해서는 객체와의 연관관계를 맺어주는 별도의 조립, 설정자가 필요해졌는데, 이 역할을 Spring이 수행하게된다.
3. LSP 리스코프 치환 원칙 (Liskov Substitution Principle)
- 인터페이스에는 기능을 설계할 때 기능이 어떤식으로 사용되어져야하는지에 대한 규약을 세우게 된다. 예를 들어, 엑셀을 밟으면 차가 앞으로 가게 된다. 하지만 뒤로 가게 설정하여도 컴파일하는데 문제가 없고, 코드 자체에는 문제가 없다.
하지만 우리가 생각하는 엑셀은 차가 앞으로 나가게 하기위한 장치이지, 뒤로가도록 설계하지는 않는다. LSP는 인터페이스가 기능에 대한 규약에 대해서 보장이 되어야한다는 원칙을 말한다.
4. ISP (Interface Segregation Principle)
- 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 한 인터페이스에서 담당하는 기능의 범위가 커지면 일부분을 바꾸기 위해서 여러가지 작업을 해야해서 효율성이 떨어진다.
- 따라서 각 기능의 범위별로 인터페이스를 분리한다면 인터페이스가 명확해지고, 대체 가능성도 높아질 뿐더러, 서로의 인터페이스에 영향을 주지 않게 된다.
5. DIP (Dependency Inversion Principle)
- 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.
- 구현 클래스에 의존하지말고 인터페이스에 의존하라는 뜻이다.
- 운전자가 자동차가 가지고 있는 역할, 기능에 대해서 알아야지 K3 기종을 엄청나게 잘 알고있다면 다른 차종을 운전하기가 쉽지 않을 것이다. 공연도 마찬가지이다. 배우가 공연 내용과 대본에 대해서 잘 알고 있어야지 상대 배우에 대해서만 알고 있다면 정상적으로 공연이 진행이 될까?
- 클라이언트가 인터페이스에 의존해야 유연하게 변경이 가능해진다.
- 하지만 우리가 이전 강의에서 구현했던 MemberService 클래스를 보자. MemberRepository 추상 객체와 MemoryMemberRepository 구현 객체 두 가지 모두 의존하고 있다. 여기서 MemberMemberRepository를 JdbcMemberRepository 구현 객체로 바꾼다면? 추상화보다 구체화에 의존하게 되어 DIP를 위반하게 된다.
위의 다섯 가지 원칙을 통해 총 정리를 하자면,
객체지향의 핵심은 다형성이지만, 다형성 만으로는 쉽게 부품을 갈아 끼우듯 개발하기가 어렵다. 이 말은 OCP와 DIP 원칙을 준수하기가 어렵다는 뜻이다. 다형성 만으로는 구현 객체를 변경 시 클라이언트 코드도 함께 변경되고, 막을 수 없다.
따라서 이를 준수할 수 있을만한 무언가가 필요하다. 이것이 Spring framework의 등장이다.
스프링 핵심 원리 이해1 - 예제 만들기
회원 도메인 설계
실무 프로세스
도메인 협력관계 : 기획자도 볼 수 있는 그림, 전체적인 기능들과 클라이언트, 서버와의 관계, 인터페이스와 구현체는 어떤 것들이 있는지를 작성
클래스 다이어그램 : 구체적으로 인터페이스 이름과 구현체 클래스의 이름을 정확히 작성하고 의존관계가 어떻게 되는지를 작성
객체 다이어그램 : 회원 클래스 다이어그램은 추상객체와 구현체가 전부 동적으로 돌아가는 시스템으로 구현될 예정이기 때문에 보다 간략하게 클라이언트와 서비스, 리포지토리 객체들의 대략적인 관계를 전체적으로 볼 수 있게끔 흐름을 파악하기위한 다이어그램
회원 도메인 개발
Alt + Insert : Generator (생성자명령어)
주문과 할인 도메인 개발
F2를 누르면 오류가 난 키워드로 이동한다.
스프링 핵심 원리 이해2 - 객체 지향 원리 적용
새로운 할인 정책 개발
관심사의 분리
final을 사용하는 이유 : 기본자 혹은 생성자를 통해서 해당 변수에 데이터값이 주입되어야한다.
새로운 구조와 할인 정책 적용
Ctrl + Alt + M : Refactor > extends method (메소드 확장)
스프링으로 전환하기
@Configuration : 어플레케이션의 설정 정보를 등록하는 어노테이션
@Bean : 메서드를 Spring Container에 등록한다.
ApplicationContext, AnnotationConfigApplicationContext : 어노테이션 기반으로 구성된 스프링 컨테이너를 호출하는 인터페이스, 메소드, 매개값으로는 설정정보 어노테이션을 붙인 클래스를 넣어주어야 한다.
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(AppConfig.class);
// AppConfig에 @Bean 어노테이션으로 등록한 메서드들을
DI 컨테이너에 등록해서 관리를 해주는 객체
getBean() : 컨테이너에서 @Bean으로 등록한 메서드를 꺼내온다. 매개값으로는 등록한 메서드의 이름(괄호 제외), 그리고 꺼내오려는 클래스 파일 이름을 적어주면 된다.
MemberService memberService = applicationContext.getBean
("memberService", MemberService.class);
// 컨테이너에서 memberService 메소드의 객체를 꺼낸다.
리턴하는 객체를 memberService 변수에 저장
스프링 컨테이너와 스프링 빈
컨테이너에 등록된 모든 빈 조회
AnnotationConfigApplicationContext 객체를 생성,
등록한 Application Bean을 조회할 수 있다.
getBeanDefinitionNames() : 등록한 Bean의 이름을 배열로 리턴한다. String[] 으로 받아주는 것이 좋다.
getBean(String beanName) : 파라미터의 이름을 가진 Bean의 객체주소를 반환한다. Object 타입으로 반환한다.
getBeanDefinition(String beanName) : Bean의 대한 메타정보 객체를 반환한다. BeanDefinition 객체로 반환한다.
getRole() : BeanDefinition 객체에서 사용할 수 있는 메서드. 스프링이 내부에서 사용하는 Bean은 getRole() 메서드로 구분할 수 있다.
BeanDefinition.ROLE_APPLICATION : 사용자가 지정하여 생성한 Bean
BeanDefinition.ROLE_INFRASTRUECTURE : 스프링이 내부에서 사용하는 Bean
스프링 빈 조회 - 동일한 타입이 둘 이상
같은 타입의 객체를 리턴하는 메서드 2개를 컨테이너에 등록하고, getBean() 메소드에 파라미터를 타입만 넣게되면 NoUniqueBeanDefinitionException이 발생하게 된다. 두 메서드 모두 같은 객체를 반환하니, 스프링에서는 어떤 메서드를 골라야할 지 선택할 수 가 없는 것이다. 따라서 이 경우에는 메서드 이름을 입력해서 bean을 리턴 받아야한다.
getBeansOfType() 메서드는 파라미터에 리턴받고싶은 타입의 클래스 파일을 입력하면 Hashmap 형태로 해당 타입의 모든 메서드를 반환한다.
key는 메서드이름+value 로 되어있고, value에는 객체주소가 입력되어져 리턴되는 방식이다. keyset()과 get() 메서드를 이용하여 모두 출력이 가능하다.
Assertions - assertThrows (예외클래스이름, 콜백): 만약 테스트 코드를 실행했을 때 예외가 발생하면 성공처리한다. 예외클래스 이름을 첫 번째 파라미터로 넣어주고, 두 번째는 실행되는 코드를 콜백으로 작성하면 된다. junit 으로 import 해야한다.
Assertions - assertThat(class)IsInstanceOf(Class.class) : 클래스 인스턴스의 타입이 맞는지를 검사하는 테스트 코드, 앞에는 클래스 객체, 뒤에는 클래스파일이름을 작성해주어야 한다. assertThat 이므로 assertj를 import 해야한다.
스프링 빈 조회 - 상속 관계
부모객체를 조회하면 자식 객체는 따라나온다.
RateDiscountPolicy로 하면 더 구체적이여서 메서드를 보기에 좋지않나? return 타입에 굳이 DiscountPolicy 인터페이스를 해 놓는 이유 :
구현과 역할을 정확히 구분하기 위해서이다. 리턴 타입에 DiscountPolicy가 오면 할인정책과 관련된 객체가 리턴되는 구나를 생각할 수 있고, Dependency Injection을 할 때도 DiscountPolicy를 상속하는 클래스들이 무엇이 오더라도 인터페이스 타입에 해당하기 때문에 아래 리턴하는 객체의 코드만 바꾸어주면 되니 다음과 같이 작성한다.
다양한 설정 형식 지원 - 자바 코드, XML
XML 설정 사용
최근에는 스프링 부트 덕분에 XML 기반의 설정은 거의 사용하지 않는다. 하지만 레거시 프로젝트에서는 XML기반이 꽤 있고, XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한 번쯤 배워두는 것이 좋다.
자바 파일이 아닌 경우에는 main 폴더에 resource 폴더가 있다. 거기에 파일을 넣어주면 된다.
xml 파일을 생성하고 난 다음, <bean> 태그를 작성하면 bean을 등록할 수 있다.
bean 태그의 속성은 id, class가 있다. 아이디는 메서드 이름, class는 호출하려는 클래스 파일의 경로를 작성하면 된다. 예를들어 memberService 메서드를 호출하기 위해서는
<bean id="memberService" class="hello.core.member.MemoryMemberService"> 와 같은 형식으로 작성한다.
또한 클래스를 호출하여도 생성자 파라미터를 넣어줘야하는 경우가 있는데 이럴 때는<constructor-arg> 태그를 넣어줘서 생성자로 호출할 인터페이스(클래스)의 이름과 클래스 파일명을 넣어주면 된다.
<constructor-arg name="memberRepository" ref="memberRepository" />
생성자에 memberRepository 객체를 넣어주어야해서 또 다른 bean인 memberRepository를 참조하도록 했다.
<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />
memberRepository의 bean 태그이다. id로 memberRepository를 호출하면 MemoryMemberRepository 구현체가 있는 클래스 파일 경로를 통해 구현체를 호출하게된다.
정리하자면, @Bean 어노테이션은 <bean> 태그,
생성자 파라미터는 <constructor-arg> 태그,
메서드 이름은 id 프로퍼티, import는 class 프로퍼티 정도로 보면 될 것 같다. construcor 태그는 참조할 bean을 ref 속성으로 설정해주어야 한다.
xml로 등록한 bean을 컨테이너에 등록하고 호출할 때는 GenericXmlApplicationContext를 사용하여야 한다. 파라미터는 String 타입으로 xml 파일의 이름과 파일확장자명을 붙여주면 된다.
스프링 빈 설정 메타 정보 - BeanDefinition
BeanDefinition : 역할과 구현을 개념적으로 나누어서 컨테이너에 메타정보를 전달하는 객체.
컨테이너는 빈을 등록할 때 BeanDefinition에 의존을 하는데, 이럴 경우 자바파일로 Bean을 설정하든 xml로 설정하든, 어떤 파일로 Configuration을 하던간데 컨테이너는 BeanDefinition 객체를 불러오면 되므로 유연성을 가지게 된다. 여기서도 역할과 구현을 나누는 다형성이 적용된다.
스프링이 다형성을 활용하여 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용한다는 점이라는 것만 이해하면 된다. BeanDefinition은 인터페이스이고, 빈을 등록한 다음 설정파일을 호출하는 시점에 빈을 BeanDefinition 객체에 등록하여 호출하게 된다.
참고 : getBeanDefinition은 GenericXmlApplicationContext를 상속받은 객체에서만 사용이 가능하다. ApplicationContext로는 사용못하니 주의
XML로 불러온 Bean 객체의 BeanDefinition을 출력하면 FactoryMethod, FactoryBeanName 부분이 null로 표기가 되어있는데, 이는 자바코드로 호출한 것이 아니기 때문이다.
Bean을 등록하는 방법은 여러가지가 있지만 크게 나누면 어노테이션을 통해 등록하는 방식과 XML을 사용하여 직접 Bean을 등록하는 방식이 있다. 어노테이션을 사용하면 Bean을 등록시 FactoryMethod, FactoryBeanName에 해당 메소드 이름과 빈 이름이 등록되게 된다.
싱글톤 컨테이너
웹 애플리케이션과 싱글톤
Assertions.assertThat.isNotSame() : 파라미터 2개를 서로 비교했을 때 같지 않을 경우 테스트를 성공으로 처리
싱글톤 패턴
싱글톤 패턴 : 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
싱글톤 패턴을 사용하려면 생성자를 private로 막아주어야 한다. 함부로 객체 생성을 못하게 막고, 클래스 내에서 객체를 하나 생성한 다음 그 객체를 호출하는 메서드를 따로 만들어서 객체를 생성하지 않고 호출하게끔 만들어주어야 한다.
객체를 생성할 때 private static final을 사용한다. 하나 밖에 없는 객체이기 때문에 정적과 상수를 쓸 수 있는 조건에 포함되는 것 같긴하다.
isSameAs와 isEqualTo의 차이점 : isSameAS는 == 비교지만 isEqualTo는 equals 메소드로 비교하는 것과 같다.
싱글톤 패턴의 문제점
- 구현해야하는 코드 자체가 많아진다.
- 의존관계상 클라이언트가 구체 클래스에 의존하게 된다(DIP위반)
- 클라이언트가 구체 클래스에 의존하기 때문에 OCP를 위반할 가능성이 높아진다.
- 테스트 하기가 어렵다.(유연하게 테스트 하기 어렵다. 이미 확정되어서 로딩된 상태이기 때문에 동적이지못함)
- 내부속성 변경, 초기화가 어려움
- private 생성자로 자식 클래스를 만들기 어려움
- 안티패턴으로 불린다.
싱글톤 컨테이너
스프링 빈이 싱글톤으로 관리되는 빈이다.
스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도 싱글턴으로 관리한다. 컨테이너는 객체를 하나만 생성해서 관리한다.
스프링 컨테이너는 싱글턴 컨테이너 역할을 한다. 이렇게 싱글턴 객체를 생성하고 관리하는 기능을 싱글턴 레지스트리 라고 한다.
스프링 컨테이너의 이러한 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하는 동시에 객체를 싱글턴으로 유지가 가능하다.
싱글턴을 사용함으로써 발생하는 단점을 싱글턴 컨테이너가 해결하는 것이다.
스프링의 기본 빈 등록 방식은 싱글턴이다. 하지만 싱글턴 방식만 지원하는 것은 아니다. 요청 시 새로운 객체를 생성해서 반환하는 기능도 제공한다. 이는 빈 스코프 기능인데, 뒷 강의에서 설명할 예정
싱글톤 방식의 주의점
싱글턴 객체는 여러 사용자들이 하나의 객체를 공유하여 사용하므로, 상태를 유지(stateful)하게 설계해서는 안된다.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적이면 Read only 상태로 놔두어야한다.
- 필드 대신에 자바에서 공유되지않는 지역변수, 파라미터, ThreadLocal등을 사용해야한다.
- 스프링 빈의 필드에 공유값을 설정하면 정말 큰 장애가 발생하므로 지양해야한다.
따라서 클래스 내의 필드변수는 사용하면 값이 변경될 위험이 있으므로 값을 바로 return해서 지역변수로 저장하게 설계하는 것이 좋다. 클래스 내의 상태를 표시하지 않도록 하는 무상태(Stateless)로 설계하는 것이 좋다.
@Configuration과 싱글톤
이전 강의 :
memberRepository() 메서드는 new MemoryMemberRepository() 객체를 호출한다. new 가 붙어있어서 완전히 새로운 객체를 만들어낸다.
그렇다면 memberRepository() 메서드를 생성자 파라미터에 넣는 memberService()와 orderService() 메서드를 호출하면 두 메서드가 가각 가지게되는 MemoryMemberRepository 참조주소는 달라야 한다. 하지만 테스트 해 본 결과, 같은 주소를 가지고 있었다. Spring 컨테이너에 등록이 되어있어서 싱글턴방식이 적용이 된 것인데, 분명 메서드에는 새로운 객체를 만들어서 전달한다고 되있었다.
이렇게 될 경우, 두 메서드는 서로 같은 객체를 공유하게 되고, 이럴 경우 데이터가 변경될 때 문제가 발생할 수 있다. 무상태가 깨지는 것이다.
@Configuration과 바이트코드 조작의 마법
@Configuration을 사용하는 이유
클래스를 @Configuration으로 지정하고, AnnotationConfigApplicationContext 객체를 생성하면 Spring이 클래스 안에있는 Bean들을 등록하면서 싱글턴 패턴이 유지되도록 프레임워크를 통해서 관리한다.
또한 @Configuration을 사용하면 스프링이 AppConfig 클래스를 AppConfig@CGLIB의 형태로 AppConfig를 상속하는 가상클래스를 만들어낸다. 가상 클래스는 AppConfig에 있는 클래스를 오버라이딩해서 생성한 객체가 있을 경우 생성한 객체를 리턴하고, 객체가 없을 경우에는 부모 클래스인 AppConfig에 있는 메소드를 실행하여 새로운 객체를 얻어오게하면서 싱글턴 패턴이 유지되도록 한다.
만약 @Configuration 어노테이션 없이 @Bean 어노테이션만 붙여서 객체를 생성하고, 테스트를 하면 컨테이너에 Bean이 등록은 되지만 우리가 처음에 생각했던 것 처럼 객체가 여러 개 생성되게 된다. 싱글턴 패턴이 깨져버리는 것이다. 따라서 설정정보 클래스에는 @Configuration 어노테이션을 사용하는 것이 좋다.
컴포넌트 스캔
컴포넌트 스캔과 의존관계 자동 주입 시작하기
컴포넌트 스캔, 의존관계 자동 주입
등록해야할 빈이 수십, 수백개가 되면 일일이 등록하기가 귀찮아지고, 설정 정보도 커지고 , 누락하는 문제도 발생한다. 이를 방지하기 위해 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
여기에 자동으로 의존관계를 주입하는 @Autowired 어노테이션도 제공한다.
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
컴포넌트를 스캔할 때 스캔하지 않을 컴포넌트를 정한다. Configuration 어노테이션이 붙은 클래스는 컴포넌트를 하지 않도록 하는 코드
@Configuration 어노테이션은 수동으로 빈을 컨테이너에 등록하기 위해서 사용하는 어노테이션이다. @Configuration 어노테이션도 @ComponentScan 대상에 포함된다.
따라서 @ComponentScan을 사용했을 때 Configuration을 사용한 클래스가 메소드가 충돌할 경우에는 예외가 발생할 수 있다.
@Component : 해당 클래스를 컨테이너에 등록한다. 하지만 의존관계 주입까지는 설정하지 못한다.
@Autowired : 코드에 맞는 타입의 클래스 객체를 생성하여 (Bean에 존재하는) 자동으로 해당 클래스에 의존관계를 주입한다.
@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이 때 스프링 빈의 기본 이름은 클래스 명을 사용하되 맨 앞글자만 소문자를 사용한다는 특징이 있다.
예를들어 MemberServiceImpl 클래스를 @Component로 등록하면 빈 이름은 memberServiceImpl 로 등록되어진다.
이름을 직접 지정하고 싶을 경우 @Component("memberService") 형식으로 등록하면 된다.
생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 기본적으로는 해당 타입을 상속하는 구현체가 있는지 확인하고 만약 있다면 그 구현체를 생성자의 파라미터로 주입하게 되는 방식이다.
ac,getBean(MemberRepository,class)와 같다.
근데 만약 상속하는 클래스가 2개 이상이라면? 어떻게 되는지는 이후 강의에서 설명한다.
Bean을 조회하는 방식은 이전과 같다. Configuration 클래스를 AnnotationConfigApplicationContext 객체의 파라미터로 넣고 조회하여 사용하면 된다.
정리
1. 설정파일을 하나 만들어서 클래스 위에 @Configuration, @ComponentScan 어노테이션을 작성한다.
2. 구현체에 @Component 어노테이션을 붙이면 스프링이 @Component 어노테이션이 있는 클래스를 컨테이너에 등록한다.
3. 클래스의 생성자에 @Autowired를 넣어준다. 이건 의존관계를 설정해야하는 경우에 넣어주어야 한다. @Autowired를 입력하면 컨테이너에 등록되어있는 빈 중에 해당 타입과 맞는 객체를 찾아서 자동으로 주입한다. 만약 타입에 해당하는 클래스가 2개 이상이라면 어떻게 처리되는지는 뒷강의에서 설명한다.
탐색 위치와 기본 스캔 대상
@ComponentScan(
basePackages = "hello.core.member",
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
이전에 excludeFilters를 통해서 특정 클래스를 제외하고 컴포넌트를 스캔하는 방법을 언급했었는데, basePackages는 폴더경로를 문자열로 입력하면 해당 폴더에 있는 클래스만 컴포넌트 스캔을 진행하는 어노테이션이다.
이 속성을 사용하는 이유는 프로젝트가 방대한 경우 자바코드를 전부 뒤져서 컴포넌트를 찾기에는 시간이 너무 오래걸린다. 따라서 원하는 폴더 경로만 스캔하고싶을 경우에는 basePackages 속성을 사용한다. 중괄호를 사용하여 배열 형태로 입력하면 여러 폴더를 선택하여 스캔할 수 있다.
basePackageClasses는 해당 클래스가 있는 패키지 내의 컴포넌트를 스캔하는 속성이다.
package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;
@Configuration
@ComponentScan(
basePackageClasses = AutoAppConfig.class,
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
이렇게 작성했을 경우에는 hello.core 내에 있는 모든 컴포넌트를 스캔하게 된다.
아무것도 작성하지 않았을 경우 @Configuration을 설정한 클래스를 기준으로 클래스가 포함된 패키지와 하위 패키지의 모든 컴포넌트들을 스캔하기 시작한다.
권장하는 방법은 프로젝트 폴더 최상위에 설정 정보 클래스를 놓는 것이다. Spring boot에서도 권장하는 방법이다. 사실 @SpringBootApplication 어노테이션이 붙어있는 CoreApplication 클래스에는 @ComponentScan 어노테이션이 붙어있어서 어차피 모든 파일을 한번 스캔한다. 너무 비중있게 다루지는 말자.
@Component
@Controller
@Service
@Repository
@Configuration
컴포넌트를 스캔 시 @Component 뿐만 아니라 위의 어노테이션도 스캔한다. 보면 MVC 패턴에 사용되는 계층을 가독성이 좋게 Component가 아닌 해당 역할을 어노테이션으로 지정하는 것임을 알 수 있다.
에노테이션은 상속관계라는 것이 없다. 어노테이셔닝 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고 스프링이 지원하는 기능이다.
@Controller : 스프링 MVC 컨트롤러로 인식
@Repository : 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환해준다. DB예외를 추상화해서 스프링 예외로 변환하는 것이다.
이렇게 하는 이유는 만약 DB를 변경할 일이 생겨서 DB를 변경했는데, 그 DB에서 나는 오류가 이전에 사용했던 DB의 예외처리와 다르게 적용된다면, 코드가 꼬일 가능성이 있다. 따라서 스프링 예외처리 코드로 바꾸어주는 작업이 필요하다.
@Service : 특별한 처리하지는 않는다. 다만 개발자가 이 어노테이션을 보고 비즈니스로직을 처리하는 클래스라는 점을 식별하기에 좋다.
중복 등록과 충돌
자동빈 등록 vs 자동빈 등록
> 빈 충돌 에러가 발생, 수정해주어야함.
수동 빈 등록 vs 자동 빈 등록
> 수동 빈으로 등록한 메서드가 자동 빈으로 등록한 메서드를 오버라이딩 해버림. 실무에서는 이러한 부분을 의도하여 설계하지 않음, 만약 이런 상황으로 오버라이딩 되서 버그가 발생하게 되면 찾기 힘든 버그가 될 가능성이 높음.
따라서 스프링 부트는 최근에 수동 빈과 자동 빈이 충돌하면 오버라이딩 하지 않고 예외를 발생시키도록 기본값을 바꾸었다. 따로 스프링 설정에서 오버라이딩을 true로 바꾸어주면 자동적으로 오버라이딩 되어서 실행된다.
개발은 명확하지 않은 것은 하면 안된다. 개발은 혼자하는 것이 아니기 때문에 애매한 상황을 만들지 않고, 명확한 코드를 만드는 것이 가장 중요하다.
코드를 작성하더라도 가독성을 위해서 짧게 작성하는 것과 명확하지만 코드를 좀 더 써야된다는 것 중 하나를 선택한다면 명확하게 작성하는 것을 선택하는 것이 좋다. 나중에 버그가 생기면 잡을 수가 없기 때문에 명확히 코드를 작성하고 예외처리를 확실하게 해두는 것이 좋다.
의존관계 자동 주입
다양한 의존관계 주입 방법
의존 관계 주입 4가지
- 생성자 주입 : 말 그대로 생성자로 주입, 지금까지 진행한 방법. 생성자 호출 시점에 딱 1번만 호출되는 점이 보장됨, 불변/필수의존관계에 사용한다.
생성자는 두 번 호출이 안된다. 강제호출이 안되기 때문에 불변(좋은 개발습관은 제약이 있게 설계하는 것, 모든것에 열려있으면 뭘 수정해야할지 알기 힘듦) > 불변
공연 중간에 배우를 바꿀일이 없어야한다 > 공연이 시작하는 순간 배우는 바꿀수 없음 : 불변
필수 : 우리가 구현체 클래스가 작동하기 위해서 필요한 필드변수를 선언했었다. (MemoryMemberRepository, DiscountPolicy 등) 필드 변수를 선언한 이유는 어플리케이션이 실행됐을 때 해당 변수에 값을 저장하고 클래스가 가진 비즈니스 로직을 수행하기 위한 것이다.
따라서 웬만해서는 그 필드변수가 null값을 가지지 않도록 해야한다. 생성자 주입에서 생성자에 값을 넣지 않으면 컴파일 오류가 발생한다. 생성자에 값을 넣어야하도록 개발자가 의도하여 설계한 것이기 때문이다.
개발문서에서 null을 넣어도 된다고 작성하지 않는 이상 생성자에 null을 넣는 것은 허용되지않는다. 구현체의 정상적인 로직이 수행될 수 없기 때문이다. 이것이 필수이다.
* 생성자가 딱 하나만 있으면 @Autowired를 생략해도 된다. 2개 이상부터는 작성해야함. 스프링 빈에만 해당된다. (파라미터가 2개 이상이라는 뜻이 아니다. 생성자가 하나여야된다는 뜻(생성자 오버로딩이 아닐 때))
[수정자 주입]
Setter 메서드를 사용해서 수동으로 의존관계를 주입한다. setXXX() 형태로 되어있는데,
예를들어 MemberRepository 객체 의존관계를 지정할 때는
public void setMemberRepository(MemberRepository memberRepository) {
this.MemberRepository = memberRepository;
}
다음과 같이 Setter메서드를 사용하여 지정하는 것이다. Setter 메서드에 @AutoWired를 사용하면 자동주입이 된다. (없으면 의존관계 주입 안됨)
특징
- 선택, 변경 가능성이 있는 의존 관계에 사용한다.
의존관계가 필수적인 경우가 아니면 의존관계를 넣어도 되고 안넣어도 상관없다. setter 메서드를 사용하면 선택적으로 구현이 가능하다.
변경은 중간에 내가 인스턴스를 바꾸고 싶을 경우에 사용한다(그럴일 거의 없음)
*@Autowired(required = false) : 스프링이 오토와이어를 보더라도 의존관계 주입을 하지 않는다. 또한 주입할 대상이 없을 경우에는 어노테이션이 예외를 출력하는데, 위와 같이 설정하면 주입할 대상이 없어도 동작이 가능하게 할 수 있다.
[필드 주입]
필드 변수에 바로 @Autowired를 작성하여 의존관계를 주입.
private임에도 불구하고 의존관계 주입이 가능
"field injection is not recommend"
테스트 코드를 작성할 때는 순수한 자바코드로 실행이 되기 때문에 스프링으로 의존관계 주입이 되어서 서버에서 사용이 가능하더라도 테스트에서는 NullPointerException이 발생하게된다.
따라서 테스트를 위해서 생성한 메서드는 setter를 또 따로 만들어주어야되는데, 이러면 수정자 주입을 사용하는게 더 나은 상황이 되어버린다.
결론 : DI프레임워크가 없으면 작동되지않는 방법. 비권장
[일반 메서드 주입]
한번에 여러 필드를 주입 받을 수 있다. 일반적으로 잘 사용하지 않음, 생성자 주입이나 수정자 주입에서 다 해결 가능한 케이스
옵션 처리
자동 주입 대상 옵션처리 방법
1. @Autowired(required = false) : 자동주입할 대상이 없으면 수정자 메서드 자체가 호출이 안된다.
2. @Nullable : 스프링 프레임워크에서 지원, 자동 주입할 대상이 없으면 null이 입력된다.
3. Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
생성자 주입을 선택해라!
생성자 주입을 선택해야하는 이유
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 DI는 변하면 안된다.(불변해야한다.)
수정자 주입을 사용할 경우 setter는 public으로 열어두어야된다.(보안) > 누군가 실수로 변경할 가능성, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계방법이 아니다.
생성자 주입은 생성할 때 딱 한 번만 호출이 된다. 따라서 불변에 성립하는 주입이다.
[누락]
수정자 주입을 사용하게 되면 순수한 자바코드로 해당 클래스의 메소드만 테스트를 하고 싶을 때 클래스를 실행하기 위해 어떤 구현체가 필요한지 알기 힘들다. 반대로 생성자 주입을 사용했을 떄는 테스트 코드를 작성할 때 생성자에 구현체를 넣어주어야 되므로 어떤 구현체가 필요한지를 바로 파악할 수가 있다.
또한 수정자 주입을 할 때는 가짜객체를 넣어주어서 테스트코드가 작동되게끔 해야되는데 생성자주입에서는 가능하지만 수정자 주입에서는 불가능하다. 따라서 생성자 주입을 사용해야하는 이유 중 하나가 된다. 객체가 누락되는 것이다.
[final 키워드를 지킬 수 있음]
final은 변하지 않는 값을 선언할 때 사용하는 것이다. final은 초기화 될때 변수에 값이 들어와야한다. 들어오지 않으면 자바에서 컴파일 오류를 발생시킨다. final을 선언하지 않으면 생성자 코드를 잘못 작성했을 때, 테스트 코드를 실행하면 아무런 반응이 안나오고 어디서 오류가 나는지를 한 눈에 알아보기 힘들다. 따라서 final을 사용할 수 있는 생성자 주입을 사용하는 것이 좋다.
(컴파일 오류가 가장 빠른 오류이다 > 가장 빨리 오류를 잡을 수 있어서 자바에서 오류를 출력하도록 유도)
생성자 주입방식은 프레임워크에 의존하지 않고 순수 자바 언어의 특징을 잘 살리는 방법이다.
기본으로 생성자 주입을 사용하고 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
항상 생성자 주입을 선택하고, 가끔 옵션이 필요할 때 수정자 주입을 선택하는 것이 좋다. 필드 주입은 비권장한다.
롬복과 최신 트랜드
생성자 코드를 만드는 과정을 줄이기 위해서 롬복을 통해 최적화가 가능하다.
롬복 세팅
1. 인강 문서에 롬복 세팅하는 코드를 build.gradle에 복사 붙여넣기 하고 우측 상단에 코끼리 클릭, External Libraries에서 롬복이 있는지 확인한다.
2. file - settings에 들어가서 plugins에 lombok 있는지 검색, 2021년 버전에서는 번들로 들어가있어서 따로 해줄필요없음
3. annotation processors에 들어가서 Enable Annotation Processing 체크
클래스의 어노테이션에 @Getter, @Setter 를 붙여주면 따로 메서드를 선언하지 않아도 getter setter 메서드 사용이 가능해진다.
@RequiredArgsConstructor
필드 변수에 필수값으로 되어있는 (final) 변수를 기준으로 생성자 코드를 생성한다. Ctrl+f12를 누르면 메서드가 어떻게 생성되어져 있는지를 볼 수 있는데, 생성자에 final 변수로 선언한 값들이 입력되어져 있다.
조회 빈이 2개 이상 - 문제
* 깃을 통해서 코드를 받아도 무조건 테스트를 돌려야한다. 왜냐하면 테스트가 모두 통과된 상태에서 내 코드를 짜야한다. 깔끔한 상태에서 하지 않으면 내가 상대방의 코드를 수정해줘야되는 상황이 발생하게된다.
@Autowired는 기본적으로 타입으로 조회한다.
만약 생성자 주입으로 DiscountPolicy 타입을 받아올 때 등록된 Bean이 RateDiscountPolicy와 FixDiscountPolicy 2개가 등록되어있으면 어떻게 될까?
두 객체 보두 DiscountPolicy의 구현체이기 때문에 아마 에외가 발생할 것이다.
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
대충 해석해보자면 하나의 빈이 매칭되어야하는데 같은 타입의 빈이 2개가 발견되어서 NoUniqueBeanDefinitionException이 발생했다.
이 때 생성자 주입할 때 타입을 하위타입으로 할 수 있지만 이것은 DIP를 위배하게 되고 유연성이 떨어진다. 그리고 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다.
@Autowired 필드 명, @Qualifier, @Primary
여러 개 타입의 구현체 빈이 존재할 때 해결할 수 있는 방법
@Autowired는 타입 매칭을 시도한다. 그리고 여러 빈이 있으면 필드 이름이나 파라미터 이름으로 빈 이름을 추가 매칭한다.
이전 강의에서 DiscoutPolicy의 구현체인 Rate와 Fix 둘 다 빈에 등록이 되어있어서 스프링이 어떤 빈을 주입해야할지 몰라서 에러가 발생했었다.
private final MemberRepository memberRepository;
private final DiscountPolicy rateDiscountPolicy;
이번에는 OrderServiceImpl에서 생성자 주입을 받는 필드 변수인 DiscoutPolicy의 변수이름을 rateDiscountPolicy로 바꾸고 테스트를 진행하면 성공한다. 왜냐하면 스프링이 같은 타입의 빈이 2개 있다는 것을 인지하고 난 다음, 필드변수의 이름을 빈과 비교하여 같은 빈이 있어서 그걸 바꾸어주었기 때문이다.
@Autowired
1. 타입을 먼저 매칭, 빈이 하나만 존재하면 바로 의존관계 주입
2. 타입 매칭의 결과가 2개 이상일 떄 필드명, 파라미터 명으로 빈 이름을 매칭한다.
@Quilifier : 추가 구분자를 붙여주는 방법, 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
@Quailifier는 어느자리에서나 사용이 가능하다. 클래스의 상단이든, 생성자 변수의 타입 앞에 붙이든, 필드 변수 앞에 붙이든 상관이 없다.
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
먼저 FixDiscountPolicy에 "fixDiscountPolicy"라는 Quailifier 어노테이션을 작성했다.
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("fixDiscountPolicy")DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
OrderServiceImpl의 생성자 파라미터에 @Qualifier("fixDiscountPolicy")를 넣어주었다.
이렇게 될 경우 빈에 등록되는 DiscountPolicy 타입의 구현체는 2개지만 @Qualifier 어노테이션을 통해 별칭을 따로 부여했기 때문에 스프링에서는 별칭으로 넣어주어야할 타입의 빈을 식별하여 생성자 주입을 하게 된다. 테스트 코드를 실행했을 때 실패하는 코드 없이 전부 통화하게 된다.
@Qualifier 정리
1. @Qualifier끼리 매칭
2. 빈 이름 매칭
3. 'NoSuchBeanDefinitionException' 예외 발생
@Primary 사용
@Primary는 우선순위를 지정하는 방법이다. Autowired로 여러 빈이 매칭되면 @Primary의 우선순위를 통해 매칭이 이루어진다. 사용법은 매우 간단하다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
같은 타입의 빈이 여러개 있을 때 우선적으로 매칭하고 싶은 구현체 클래스에 @Primary 어노테이션을 붙이면 된다.
메인 DB와 서브 DB와 관련된 커넥션 구현체 클래스가 있다고 했을 때, 메인 DB에 @Primary를 넣어줘서 메인 DB를 우선적으로 사용할 수 있게끔 설정해줄 수 있다.
우선권은 Primary보다 Qualifier가 더 높다. 자동보다는 수동이 우선권이 더 높다.
애노테이션 직접 만들기
Annotation 직접 만들기
@Qualifier("String") 에서 String은 컴파일러에서 체크하지 못한다. 따라서 내가 별칭을 잘못 지정했다 하더라도 정상적으로 스프링이 실행되어버린다. 오류가 어디서 일어나는지 찾을 수 없는 것이다.
이 문제를 막기 위해서, 어노테이션을 아예 새로 만들 수가 있는데, 사용 방법은 다음과 같다.
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
코드를 살펴보면 MainDiscountPolicy라는 어노테이션을 생성하였고, Target, Retention, Inherited, Documented, Qualifier 어노테이션이 붙어있는 것을 볼 수 있다.
사실 위에서부터 4개까지의 어노테이션들은 Qualifier.java 파일로 들어가면 볼 수 있는 어노테이션들이다. 거기서 복사한 다음 내가 만들고자하는 어노테이션이 붙여넣기를 하고 @Qualifier로 별칭을 지정해줘서 구현체에 @MainDiscountPolicy 어노테이션을 작성하고 생성자 파라미터에 @MainDiscountPolicy을 하면 빈이 여러개 있더라도 @MainDiscountPolicy로 지정한 클래스를 생성자에 주입하게 된다.
어노테이션은 오타가 발생하면 아예 컴파일러에서 오류가 나기 때문에, 단순히 Qualifier를 사용하다가 오타가 발생해서 일어날 수 있는 문제를 해결하기에 좋은 방법이다. 오류를 추적하기 편하게 하기위해서는 어노테이션을 직접 만드는 방법도 고려해보는 것이 좋다.
조회한 빈이 모두 필요할 때, List, Map
동적으로 필요한 할인정책을 그때그때 선택하여 사용할 수 있도록 하는 방법을 알아보자.
비즈니스 로직 처리는 Service 클래스에서 하므로 Service 클래스에 Map 객체 또는 List 객체에 빈을 넣어서 사용하면 된다. 당연히 AutoWired를 통해서 빈 객체를 넣을 수 있다.
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(2000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
먼저 정적 클래스를 하나 만들었다. DiscountService 클래스인데, 빈으로 등록되어있는 모든 DiscountPolicy 타입의 구현체들을 키와 값의 형태로 Map 컬렉션에 넣도록 생성자 주입까지 작성하였다.
그리고 AnnotationConfigApplicationContext 객체를 통해 DiscountPolicy.class, DiscountService.class 클래스 파일을 빈에 등록했다.
(이미 ComponentScan을 통해서 빈에 등록되어있는 경우에는 빈의 설정정보를 그냥 가져온다.)
이렇게 될 경우, DiscountService에 있는 생성자를 보고 스프링이 DiscountPolicy에 해당하는 구현체 빈 클래스들을 Map 컬렉션에 하나씩 넣어주게 된다. 순서는 랜덤이다.
여기서 키 값이 어떻게 들어가지는지 궁금해질 텐데, 기본적으로 등록되는 빈 이름이 된다. FixDiscountPolicy는 fixDiscountPolicy로, RateDiscountPolicy는 rateDiscountPolicy로 빈 이름이 등록되어진다. key는 생성된 빈 이름이 들어가고, 싱글턴 방식으로 등록된 객체주소를 value값으로 받아와서 Map 객체 안에 들어가게 된다.
이제 로직을 만들고 Assertion으로 테스트코드를 확인해보자.
DiscountService 클래스에는 discount 메서드가 있다. 파라미터로는 멤버변수, 가격과 discountCode가 있다. discountCode는 Map 객체에 있는 key를 입력하면 된다. rateDiscountPolicy를 입력하면 rateDiscountPolicy 객체에 있는 discount 메서드를 실행한 결과값을 리턴하도록 설정하고 리턴하도록 하였다.
자동, 수동의 올바른 실무 운영 기준
1. 편리한 자동 기능을 기본으로 사용하자.
스프링이 나오고 난 이래 자동이 점점 선호되는 추세이다. 스프링은 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 또한 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고 다양한스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계되었다.
설정 정보를 기바능로 구성하는 부분과 실제 동작하는 부분을 명확히 나누느 것이 이상적이지만 주입할 대상을 일일이 적어주는 과정은 상당히 번거롭다.
결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
그렇다면 수동 빈 등록은 언제 사용하는 것일까?
애플리케이션은 크게 업무 로직과 기술지원 로직으로 나눌 수 있다.
업무 로직 빈은 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
기술지원 빈은 기술적인 문제나 공통 관심사(AOP)를 처리할 떄 주로 사용된다. DB 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
업무 로직은 그 수가 많고, 한번 개발하게되면 컨트롤러, 서비스, 리포지토리 처럼 유사한 패틴 있다. 이 경우는 자동기능을 적극적으로 사용하는 것이 좋다. 보통 문제가 발생해도 어디서 발생했는지 파악하기가 어렵지 않다.
기술로직은 그 수가 상대적으로 업무로직보다 적고, 어플리케이션 전반에 걸쳐서 고아범위하게 영향을 미친다. 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 따라서 기술지원 로직은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.
"어플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수에 좋다."
한 가지 고민해봐야할 점이 하나 있는데, 추상화이다.
추상화는 내가 개발할 때는 정말 편리하고 좋지만, 내가 작성한 코드를 다른 동료개발자가 봤을 때 한 눈에 알아보기 힘들다는 단점이 있다.
예를들어 이전강의에서 DiscountService 클래스를 생각해보자 Map 객체에 DiscountPolicy 타입의 빈들이 전부 담겨진다. 우리는 rateDiscountPolicy, fixDiscountPolicy를 만들어 봐서 어떤 빈이 들어갈지 알 수 있다. 하지만 다른 동료개발자들은 처음 코드를 봤을 때 어떤 객체가 Map 컬렉션에 담기는지 한 눈에 알아보기 힘들다.
담기는 빈 객체들이 몇 십개 된다면 과연 동료개발자가 내 코드를 파악하고 제대로 개발을 수행할 수 있을까? 사실 정답은 없다. AppConfig를 통해서 수동으로 빈을 등록하면 가독성은 좋아지지만 번거로운 작업이 될 것이다.
따라서 추상객체와 구현체들이 같은 타입인 경우에는 패키지에 따로 모아두는 것이 좋다. 다른 개발자가 딱 보고 어떻게 구성되어있는지를 파악할 수 있도록 설계하는 것이 핵심이다.
[정리]
편리한 자동 기능을 기본으로 사용하자.
직접 등록하는 기술 지원 객체는 수동 등록을 하는 것이 좋다.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자.
[질의응답]
@Bean과 @Component의 차이
@Bean은 수동 빈 등록을 하기 위해서 사용하는 어노테이션이다. @Configuration으로 등록한 설정 정보 클래스의 메서드에 @Bean을 입력하면 컨테이너에 수동으로 등록이 된다.
@Component는 자동으로 빈 등록을 하기 위해서 사용되는 어노테이션이다. @ComponentScan이 되어있는 자동 설정정보 클래스가 있으면 모든 @Component 클래스를 스캔해서 컨테이너에 등록한다.
@Bean은 수동 빈 등록, @Component는 자동 빈 등록이라는 점을 구분짓기 위해서 사용한다. 만약 @Configuration 클래스에 @Component를 사용하면 수동 빈으로 등록하려는 메서드를 자동 빈 등록으로 한다는 점이 충돌되어서 예외가 발생하게 된다.
빈 생명주기 콜백
빈 생명주기 콜백 시작
DB Connection Pool :
어플리케이션 서버가 로딩될 때 DB연결을 미리 맺어놓는다. 10개~100개 정도의 연결객체를 연결해서 Pool에 저장해 놓고, 사용자가 요청할 때 연결객체를 전달한다음 close() 메서드가 호출될 때 다시 pool에 저장하는 방식.
스프링 빈은 간단한 라이프사이클을 가진다
객체 생성 > 의존관계 주입
스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음이 되야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야한다. 그렇다면 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까?
스프링은 의존관계 주입이 완료됨녀 스프링 빈에게 콜백메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.
스프링 빈의 이벤트 라이프 사이클
1. 스프링 컨테이너 생성
2. 스프링 빈 생성
3. 의존관계 주입(생성자 주입, 수정자 주입, 필드 주입...)
4. 초기화 콜백 : 빈이 생성되고 빈의 의존관계 주입이 완료된 후 호출
5. 사용
6. 소멸 전 콜백 : 빈이 소멸되기 직전에 호출됨
7. 스프링 종료
" 객체의 생성과 초기화를 분리하자 "
단일책임원칙, 객체를 생성하는 생성자에는 메모리를 할당해서 객체를 생성하는 것에만 집중하도록 구현하는 것이 좋다.
객체가 초기화된다는 것은 객체가 동작한다는 것이다. 외부와 커넥션을 맺고 동작한다는 역할을 수행하는 것이다.
대부분의 경우에는 객체가 동작하도록 하는 행위는 별도의 초기화 메서드로 분리하도록 설계하는 것 유지보수관점에서 훨씬 좋다. 내부 값들을 간단하게 변경하는 정도로 단순한 경우에는 생성자에서 간단하게 처리하는 것이 더 나을 수도 있다.
인터페이스 InitializingBean, DisposableBean
인터페이스 InitializingBean, DisposableBean
- InitializingBean : 추상메서드 afterPropertiesSet() 이 있다. 초기화 작업이 끝나면 실행될 코드를 작성할 수 있음.
- DisposableBean : 추상메서드 destroy()가 있음. 빈 생명주기가 끝날 때( 컨테이너에 있는 빈을 없앨 때) 호출되는 메서드
초기화, 소멸 인터페이스의 단점
- 스프링 전용 인터페이스, 해당 코드가 스프링 전용 인터페이스에 의존하게 된다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
(인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다.)
빈 등록 초기화, 소멸 메서드
인터페이스를 상속받지 않고 @Bean에 등록할 때 자동으로 초기화, destroy 메서드를 설정하는 방법이 있다.
먼저 NetworkClient 메서드에 다음과 같이 메서드를 만들어 놓는다.
public void init() {
System.out.println("NetworkClient.Init");
connect();
call("초기화 연결 메세지");
}
public void close() {
System.out.println("NetworkClient.close()");
disconnect();
}
그리고 @Configuration 설정 클래스에 있는 @Bean 메서드에 다음과 같이 작성한다.
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
@Bean으로 등록할 때 initMethod, destoryMethod를 작성하고 해당 메서드를 적용할 메서드 이름을 적어주면 된다.
설정 정보 사용 특징
- 메서드 이름을 자유롭게 줄 수 있다.
- 스프링 빈이 스프링 코드에 의존하지 않는다.
- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.
@Bean의 destroyMethod 속성에는 특별한 기능이 있다. 라이브러리는 대부분 close, shutdown 이라는 이름의 종료 메서드를 주로 사용한다.
@Bean의 destroyMethod는 기본값이 (inferred)로 등록되어있다. 이 추론기능은 close, shutdown라는 이름의 메서드를 자동으로 호출해준다. 종료메서드를 추론해서 호출하는 것이다.
따라서 직접 스프링 빈으로 등록하면 종료메서드는 따로 적어주지 않아도 잘 동작하게 된다.
따라서 위의 코드에서 destroyMethod를 작성하지 않아도 알아서 close 메서드를 추론하여 자동으로 실행시켜준다.
애노테이션 @PostConstruct, @PreDestroy
@PostConstruct, @PreDestroy 애노테이션 특징
- 최신 스프링에서 가장 권장하는 방법
- 애노테이션 하나만 붙이면 되므로 매우 편리
- 패키지를 보면 'javax.annotation.PostConstruct'이다. 스프링에 종속적이지 않고 JSR-250 이라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
- 컴포넌트 스캔과도 잘 어울린다. (@Bean을 등록하는 것이 아님)
- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야하면 @Bean의 기능을 사용하도록 하자.
[정리]
@PostConstruct, @PreDestroy 애노테이션을 사용하자.
코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야하면 @Bean의 initMethod, destroyMethod를 사용하도록 하자.
빈 스코프
빈 스코프란?
Bean Scope
- SingleTon : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- ProtoType : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다. 종료 메서드 호출이 되지 않는다.
웹 관련 스코프
- request : 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
- session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
프로토타입 스코프
[프로토타입 스코프]
프로토타입 스코프를 스프링 컨테이너에 조회하면 항상 새로운 인스턴스를 생성해서 반환한다(싱글턴이 아니다.)
프로토타입 빈을 등록했을 경우, 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계를 주입하고, 초기화까지만 처리한다. 클라이언트에 빈을 반환하고 빈을 아예 관리하지 않는다. 따라서 클라이언트가 받은 프로토타입 빈을 관리할 책임을 갖는다.
따라서 @PreDestroy같은 종료 메서드는 호출되지 않는다.
@Configuration 어노테이션을 주지 않더라도 AnnotationConfigApplicationContext 파라미터에 클래스를 넣어주면 해당 클래스는 컨테이너에 등록이 된다.(정확히 말하면 컴포넌트 스캔 목록에 해당되게된다.)
일반 스코프와 프로토타입 스코프의 차이점
- 일반 스코프라면 (Singleton) 스프링 컨테이너가 등록될 때 빈이 생성되면서 init() 메서드가 실행되게 된다.(@PostConstruct)
- 프로토타입 스코프는 스프링 컨테이너가 등록되면서 객체가 생성되지 않는다. 당연히 init() 메서드도 실행되지 않는다. 클라이언트가 프로토타입의 빈을 요청하면 그때 객체를 새로 생성하고 init() 메서드까지 한 다음에 클라이언트한테 객체를 던져주고 끝난다. 또한 같은 객체가 아닌 클라이언트가 요청할 때마다 새로운 객체를 생성해서 클라이언트한테 던져준다.
클라이언트에게 객체를 줘버리기 때문에 컨테이너가 죽을 때destroy() 메서드는 실행되지 않는다. 만약 destroy()를 호출할 필요가 있을 경우 코드를 통해 수정으로 호출해주어야 한다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
Ctrl + alt + n : return 문 합치기
만약 싱글턴 빈에 프로토타입 빈을 의존관계 주입하게 되면 어떻게 될까? 싱글턴 빈은 컨테이너가 생성될 때 최초로 의존관계 주입이 되기 때문에 컨테이너에서 프로토타입 객체를 하나 생성해서 싱글턴 빈에 넣어줄 것이다.
하지만 이렇게 된 다음 클라이언트가 싱글턴 빈을 호출하면 싱글턴 빈 안에 존재하는 프로토타입 빈만 계속 호출이 된다. 프로토타입 빈이 싱글턴이 되어버린 것이다.
프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라 새용할 때마다 새로 생성해서 사용하는 것을 원할 것이다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결
@Scope("singleton")
static class ClientBean {
// private final PrototypeBean prototypeBean;
@Autowired
ApplicationContext applicationContext;
// public ClientBean(PrototypeBean prototypeBean) {
// this.prototypeBean = prototypeBean;
// }
public int logic() {
applicationContext = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
이전 강의에서 싱글턴 빈에 프토토타입 빈을 의존관계 주입했을 때 프로토 타입 빈이 싱글턴이 되어버리는 현상을 보았었다. 따라서 위의 코드 처럼 클라이언트가 logic() 메서드를 실행하면 ApplicationContext를 통해 컨테이너에 등록되어있는 프로토타입 빈을 가져오게하도록 코드를 작성했었다.
이 처럼 의존관계를 외부에서 주입(DI)받는게 아니라 직접 필요한 의존 관계를 찾는 것을 Dependency Lookup(DL) 의존관계라고 한다.
하지만 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면 스프링 컨테이너에 종속적인 코드가 되고, 단위테스트도 어려워지게 된다.
## ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
위에서 작성했던 코드를 다음과 같이 바꿔보았다. 간단한 테스트를 위해서 필드주입으로 사용하였고, ObjectProvider<>를 사용하여 PrototypeBean을 직접 호출하여 호출할 때마다 새로운 객체를 받아오도록 하였다.
logic() 메서드를 보면 getObject() 메서드를 사용하여 컨테이너한테 프로토타입 빈의 새 객체를 요청한 다음 로직을 실행하는 것을 볼 수 있다.
클라이언트가 메서드를 호출할 때마다 컨테이너는 새로운 객체를 주므로 테스트코드에서는 count의 수치가 1이 되어야 할 것이다.
원래는 ObjectFactory가 있었으나 ObjectProvider가 ObjectFactory를 상속하면서 여러가지 추가 기능들을 제공하는 객체가 만들어졌다.
- ObjectFactory : 기능이 단순하고 별도의 라이브러리가 필요없다. 스프링에 의존
- ObjectProvider : ObjectFactory를 상속한다. 옵션이나 스트림 처리 등 편의 기능이 많고 별도의 라이브러리가 필요없다. 스프링에 의존한다.
## Provider(javax.inject)
JSR-330인 자바 표준의 라이브러리이다. 라이브러리 이므로 그리들에 없다면 새로 설치를 해주어야 한다. dependencies 안에
implementation 'javax.inject:javax.inject:1'
를 작성해주면 된다.
Provider라는 이름의 여러 객체들이 있으니 사용할 때는 javax.inject인지 확인하고 사용하여야 한다.
ObjectProvider는 getObject()를 통해 새로운 객체를 컨테이너에 요청한다. Provider는 get() 메서드를 사용하여 컨테이너에 새 객체를 요청할 수 있다.
Provider는 자바 표준이고, 기능이 매우 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
특징 :
- get() 메서드 하나로 기능이 매우 단순함
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
[정리]
프로토타입 빈은 언제 사용할까? 매 번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 실무에서는 싱글턴 빈으로 대부분의 문제가 해결되기 떄문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
프로토타입 뿐만 아니라 DL이 필요한 경우에는 위의 객체들을 모두 사용할 수 있다.
※ Provider, ObjectProvider 어떤걸 사용해야되는지 : ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존고나계 추가가 필요없기 때문에 편리하다.
만약 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.
request 스코프 예제 만들기
UUID : 자바에서 제공하는 API로 겹치지 않는 식별자를 제공한다.
randomUUID : 무작위 난수를 통한 UUID를 생성하여 UUID 객체를 반환한다. 따라서 문자열로 아이디를 얻기 위해서 뒤에 toString() 을 사용해주어야 한다.
로그를 출력하기위한 MyLogger 클래스를 만들었다.
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public MyLogger(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "] [" + requestURL + "] [" + message + "]");
}
@PostConstruct
public void init() {
String uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PostDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
@scope(value = "request")를 사용해서 request 스코프로 지정하였다. 이게 이 빈은 HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸된다.
이 빈이 생성되는 시점에 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성하여 저장하도록 하고, 이 빈은 HTTP 요청 당 하나씩 생성되므로 uuid를 저장해두면 다른 HTTP 요청과 구분이 가능하다.
@PreDestroy를 통해 빈이 종료되는 시점에 콘솔에 문장을 출력하는 메서드를 지정하였다.




