* 이 포스팅은 김영한님의 스프링 기본원리 기본편 강의 내용을 정리한 것입니다.
1. 제어의 역전(Inversion of Control, IoC)
프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC) 이다.
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행. 즉, 클라이언트는 구현체를 직접 참조.
스프링 프레임워크는 Config를 통해서 프로그램의 흐름 제어를 할 수 있다. 예를 들면, 내부 또는 외부 저장소 사용 여부를 비지니스 로직 구현체 코드를 건드리지 않아도 Config 를 통해서 제어할 수 있다.
프레임워크와 라이브러리의 차이
프레임워크는 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크이다.
반면에 내가 작성한 코드가 직접 제어의 흐름을 담당하면 그것은 프레임워크가 아니라 라이브러리다.
2. 의존관계 주입 DI (Dependency Injection)
의존관계는 '정적인 클래스 의존관계'와 실행 시점에 결정되는 '동적인 객체(인스턴스) 의존 관계' 둘로 분리할 수 있다.
'정적인 클래스 의존관계'는 직접 참조하는 것이며, '동적인 의존관계'는 실행시점의 설정에 따라 실제 인스턴스가 참조되는 관계이다. OOP 원칙을 따랐다면, 역할 Interface에 정적은 의존관계를 형성하고, 구현은 config 에 따라 주입되는 방식으로 동적인 의존관계를 가진다.
IoC 컨테이너, DI 컨테이너
객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 동적으로 생성한 객체들의 생성, 주입, 삭제, 종료 등 객체들의 라이프 사이클을 관리하는 곳이다. 다른 용어로 어셈블러, 오브젝트 팩토리 등으로도 불린다.
3. Bean
Spring IoC Container 가 관리하는 객체들을 Bean 이라고 한다. Bean 설정은 주로 XML 방식과 JavaConfig 방식 2개를 주로 사용한다. 옛날 Spring 에서는 XML로 했지만 요즘엔 JavaConfig로 주로 사용한다.
XML 방식과 JavaConfig를 비교해보자. XML로 하면 많은 양의 설정들을 한 곳에서 깔끔하게 정리할 수 있고, 컴파일 없이 설정을 쉽게 바꿀 수 있다. 모놀리식 대규모 시스템에 주로 맞다고 생각한다. 반면, Java Config 방식은 중소규모 어플리케이션에 좋다. 편리하기 때문이다. 개인적 추정으로 요즘엔 대규모 모놀리식 어플리케이션보다 MSA 설계를 사용하다보니 Spring Boot를 사용하고 자연스레 Java Config가 대세로 자리 잡은 것은 것 같다.
3-1. Bean 탐색 및 인젝션 규칙
설정을 통해 클래스들을 Bean으로 설정하고, 스프링 실행을 하면 Bean으로 IoC Container에 로드가 된다. 그리고 클라이언트 객체들에게 주입을 시작하는데, Bean 을 주입할 때 우선순위 규칙이 있다.
첫째, Bean의 구체적 이름이 설정되어 있는 Bean을 찾는다. @Autowired @Qualifier("Bean 이름")
둘째, 타입이 일치하는 Bean을 찾는다. 동일한 타입이 있을 경우 스프링에선 결정할 수 없으므로 Context 로딩 중 에러가 발생하고 중단된다.
3-2. Bean 생성
3-2-1. 싱글톤
단, 한 개의 객체 인스턴스만 생성하여 공유해서 사용한다. 웹 어플리케이션의 경우, 수많은 요청마다 객체를 생성하는 것보다 메모리 소비를 줄일 수 있다. 여러 클라이언트에서 동시 참조를 하기 때문에 상태 저장을 지양해야 한다.
싱글턴 단점은 싱글턴 패턴을 구현하는 코드 자체가 많이 들어간다. 의존관계상 클라이언트가 구체 클래스에 의존하기 때문에 'DIP 위반'을 한다. getInstance()를 호출해야 하기 때문이다. 그래서 유연성이 떨어져서 안티패턴으로도 불린다.
하지만 스프링에서는 '싱글톤 컨테이너'를 제공하며, 싱글턴을 직접 구현할 필요가 없고, DIP 위반 없이 싱글턴 라이프사이클로 객체를 관리할 수 있다. 즉, 단점은 다 제거하고 장점만 적용할 수 있다.
싱글턴 사용 주의점은 상태를 가지면 안된다. 상태를 사용해야한다면, Read Only로 접근할 수 있게 하거나 지역변수, 파라미터, ThreadLocal 변수를 사용한다.
ThreadLocal 이란 이름 그대로 Thread 범위 내 라이프사이클을 가진다. 지역변수들은 해당 메소드 내에서만 존재하거나 클래스 변수는 클래스 범위 내에서만 존재한다. 하지만 여기서 더 범위를 넓혀서 한 Thread 범위 내에 공유(사용)되는 변수이다.
3-2-2. @Configuration 과 @Bean
스프링에서는 AppConfig의 Bean 등록 메소드들을 실행할 때마다, 이미 Container 에 Bean이 있으면 이미 있는 것을 반환하고, 스프링 빈이 없으면 생성해서 반환하는 코드가 동적으로 만들어진다. 이것은 실제 AppConfig를 바이트코드 조작을 통해 이루어진다. AppConfig 를 상속한 AppConfig@CGLIB 를 생성해서 스프링에서 사용한다.
@ComponentScan
- 빈 이름 기본 전략: MemberServiceImpl 클래스 -> memberServiceImpl
- 수동으로 빈 이름 등록한 것과 자동으로 빈 등록 한 것이 충돌날 떄는 기본적으로 수동이 우선순위를 가졌었는데, 최근에는 오류가 나도록 변경되었다. 디버깅하기 쉽도록. 자동으로 override 하려면, bean-definition-overriding=true 를 설정하면 된다.
좋은 개발 팁
- 어설픈 추상화나 애매한 설정 보다는 명확한 것이 좋다. 오류를 빨리 반환하는 것이 좋다.
- 의도한대로만 사용하도록 제약을 잘 걸어야 한다. getter, setter를 다 열어두고 사용하다보면 다른 개발자가 다른 로직에서 주입된 객체를 변경해벼러서 비지니스 정책이 바뀌고 잡기 어려운 버그가 발생할 수 있다.
4. 의존성 주입
- 생성자 주입을 선택 권장한다. 의존관계를 불변하게 설계할 수 있기 때문이다.
- 대부분의 의존관게 주입은 한번 일어나면 어플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 대부분 의존관계는 어플리케이션 종료 전까지 변하면 안된다. (불변)
- 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다
- 누군가 실수를 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다. (제약 충실)
=> final 사용 시, 컴파일 오류 발생을 하기 때문에 버그 방지에 좋다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없어서 불변한 객체를 유지할 수 있다.
- 필드 주입을 사용하면, 테스트 시 의존성 주입을 하려면 수정자(getter, setter)를 사용해야 한다.
4-1. 자동, 수동 올바른 실무 운영 기준
- 어플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다
- 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비지니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 보통 비지니스 요구사항을 개발할 떄 추가되거나 변경된다.
- 기술 로직 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
- 업무로직은 유사한 패턴이 있기 때문에 자동 기능을 적극 활용. @Controller, @Service, @Repository
- 기술 로직 빈은 광범위한 영향을 미치기 때문에, 수동 빈 등록으로 명화갛게 들어내는 것이 좋다.
4-2. Bean 생명 주기 콜백
데이터베이스 컨넥션 풀이나, 네트워크 소켓처럼 어플리케이션 시작 시점에 필요한 연결을 미리 해두고, 어플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다
스프링 빈의 이벤트 라이프 사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료
객체의 생성과 초기화를 분리하자
생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇게 생성된 값을 활용해서 외부 커넥션을 연결하는 등의 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 함께하는 것보다는 객체를 생성하는 부분과 초기화하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는 게 더 나을 수 있다.
3가지 방법
1. 인터페이스
단점: 스프링에 의존적이다. 외부 라이브러리에 적용할 수 없다. 거의 사용 x
* 외부 라이브러리도 Bean으로 등록할 수 있지만, 인터페이스를 구현하거나 어노테이션을 달려면 소스 코드를 수정해야해서 외부라이브러리에선 적용하지 못한다는 뜻이다.
2. 설정 정보에 초기화 메서드
코드가 아니라 설정 정보를 사용하기 때문에 외부 라이브러리에도 초기화, 종료 메서드를 사용할 수 있다.
close, shutdown 이라는 컨벤션 이름 메서드가 있으면 찾아서 종료 메서드로 설정한다. 자동 추론 기능. 끌려면 ""로 세팅
3. 종료 메서드 지정: 스프링에서 사용하는 어노테이션 사용
4. @PostConstruct, @PreDestroy 어노테이션 사용
JSR-250 자바 표준 어노테이션이다. 스프링이 아닌 프레임워크에서도 작동한다. 단점은 외부 라이브러리에는 적용하지 못한다. 외부 라이브러리를 초기화, 종료해야 하면 @BEAN 기능을 사용해야 한다. 그냥 어노테이션을 사용하자. 필요할 때 initMethod, destroyMethod 를 사용하면 된다.
Bean Scope
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 새성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프. 호출될 때마다 새 빈 생성하고 Container 내에 존재하지 않는다. @PreDestroy 가 실행이 안됨. Java로만 사용할 수 있지만, 프로토타입 빈으로 사용하면 Injection 을 할 수 있다는 장점이 있다.
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 떄까지 유지되는 스코프. Request Log를 남길 때 주로 사용. Request 마다 고유 아이디를 부여해서 로깅 때 구분할 수 있도록 할 수 있음. 예시: [UUID][Request URL]
- session: 웹 세션이 생성되고 종료될 떄까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
싱글톤에서 프로토타입 빈 사용할 때
문제점은 프로토타입 주입할 때만 생성되고, 실행할 때는 새로 프로토타입 빈이 생성이 안됨. 그렇다고 직접 구현체 생성자를 호출해서 사용하게 되면 의존역전관계 위반이 됨. 구현체에 의존하게 되버림.
DI(Dependecy Injection)이 아니라 Dependency Lookup (DL) 이 필요함.
ObjectProvider<T> 혹은 ObjectFactory<T> 를 사용. 스프링에 의존한다는 단점이 있음.
JSR-330 Provider
사용하려면 gradle에 추가해야 함.
implementation 'javax.inject:javax.inject:1'
@Component
public class SingletonBean {
private final Provider<PrototypeBean> prototypeBeanObjectProvider;
@Autowired
public SingletonBean(Provider<PrototypeBean> prototypeBeanObjectProvider) {
this.prototypeBeanObjectProvider = prototypeBeanObjectProvider;
}
public int logic(){
return prototypeBeanObjectProvider.get().increase();
}
}
'공부노트 > 스프링' 카테고리의 다른 글
Spring AOP 적용 (0) | 2022.06.25 |
---|---|
스프링 AOP (0) | 2022.06.19 |
프록시 패턴과 데코레이션 패턴 (0) | 2022.04.30 |
Strategy Pattern, Template Callback 패턴 (0) | 2022.04.24 |
Template Method 패턴 (0) | 2022.04.24 |