0. 개요
- 이번 포스팅에서는 그동안 배운 Spring의 기본 개념을 정리해보려고 한다.
- 이번 포스팅은 다음과 같은 목차로 구성된다.
< 목차 >
1. 객체지향과 다형성
a) OOP의 정의
- 객체지향 프로그래밍은 수많은 부품을 이용하여 자동차를 조립하는 것처럼 프로그램을 작성하는 것이다.
- 즉, 여러 개의 부품처럼 코드를 기능 단위로 쪼개어 하나의 프로그램을 완성시키는 방법이다.
- 그렇다면 어떤 기준으로 코드를 기능 단위로 쪼갤 수 있을까?
b) 다형성(Polymorphism) - 역할과 구현의 분리
- 코드를 기능 단위로 쪼개는 기준은 코드의 역할과 구현이다.
- 이는 다형성의 근본이 되는 개념으로, 프로젝트의 일부분이 되는 코드를 부품처럼 교체가 가능하도록 설계하기 위함이다.
- 역할과 구현의 분리를 코드에 적용하기 위해, 이 개념으로부터 객체지향의 4가지 특성이 파생된다.
→ 상속, 추상화, 캡슐화, 그리고 다형성
c) 역할의 정의 - 상속(Inheritance)과 추상화(Abstraction)
- 역할은 interface를 이용하여 정의한다.
- interface는 부품이 될 코드가 프로그램에서 어떤 기능과 역할을 수행할지 정의한다.
- interface이므로 실제 내부 로직을 구현하지 않고 추상화를 통해 메서드를 정의한다.
d) 역할의 구현 - 캡슐화(Encapsulation)
- 역할은 interface를 상속받은 class로 구현된다.
- class는 인터페이스의 추상 개념(= 메서드)을 구현해내는 과정이다.
- 추상 개념을 구현할 때에는 반드시 목적에 맞게 메서드를 구현해야 한다.
2. SOLID 원칙
- 프로그램의 사이즈가 커질수록 객체지향을 준수하며 프로그래밍하는 것은 어려운 일이다.
- 여러 역할과 구현체가 얽히면서 개발자가 간과한 부분에서 객체지향의 특성이 위배될 수 있기 때문이다.
- 이처럼 큰 규모의 프로젝트에서도 객체지향을 잘 유지할 수 있도록 제시하는 규칙이 SOLID다.
a) SRP(Single Responsibility Principle) - 단일 책임 원칙
- 프로젝트를 구성하는 하나의 부품은 반드시 한 가지 책임만 가져야 한다.
- 즉, 하나의 부품이 한 가지 이상의 역할 또는 기능을 수행하면 안 된다.
- 여기서 하나의 부품이란 인터페이스, 클래스, 메서드 등을 의미한다.
b) OCP(Open-Close) - 개방/폐쇄 원칙
- 프로그램의 확장에는 열려 있으나 변경에는 닫혀있다.
- 이는 프로그램의 확장을 위해서 기존의 코드를 수정해서는 안된다는 것을 말한다.
- 이 개념은 변경/확장의 대상과 범위가 일치해야 한다는 의미로, 대상 외 다른 코드를 수정해서는 안된다는 의미다.
c) LSP(Liskov Substitution) - 리스 코프 치환 원칙
- 부모 클래스의 객체를 사용하는 부분을 자식 클래스의 객체로 바꾸더라도 프로그램은 정상 작동해야 한다.
- 이는 상속을 기반한 개념으로, 자식 클래스는 부모 클래스의 기능과 속성을 모두 가진다는 의미다.
- 즉, 부모 클래스가 수행 가능한 것은 자식 클래스도 수행 가능해야 한다는 것이다.
d) ISP(Interface Segregation) - 인터페이스 분리 원칙
- 범용적인 인터페이스 하나보다 구체적인 인터페이스 여러 개를 사용하는 것이 낫다.
- 이는 인터페이스의 역할을 구체적인 한 가지로 규정해야 한다는 것을 의미한다.
- 구현체가 인터페이스를 상속받아 구현할 때, 사용하지 않는 메서드까지 구현해야 할 필요가 없어야 한다는 것이다.
e) DIP(Dependency Inversion) - 의존관계 역전 원칙
- 이전 포스팅에서 DI를 명시했던 것을 생각해보자.
- client가 구현체를 직접 선택하는 것이 아닌 인터페이스를 이용하여 의존관계를 주입받는다.
- 즉, 개발자는 구현체보다 인터페이스를 어떻게 설계할 것인가에 더욱 집중해야 한다는 의미다.
- 다형성의 기본원칙인 역할과 구현에서, 개발자는 역할을 정의하는 것에 더욱 집중해야 한다는 것이다.
3. Spring Framework를 사용하는 이유
- Java와 SOLID 원칙을 이용하면 온전히 객체지향 프로그래밍을 구사할 수 있을까?
- 이 둘만 가지고는 불가능하다. 그 이유와 해결방법을 알아보자.
a) OCP와 DIP를 위배한다
- Java와 SOLID 원칙을 이용해도 온전한 객체지향 프로그래밍을 구사할 수 없다.
- 그 이유는 Java만으로 OCP와 DIP원칙을 준수할 수 없기 때문이다.
- OCP와 DIP원칙의 핵심은 다음과 같다.
→ OCP: 프로그램이 변경/확장되어도 변경/확장되는 부분 외 다른 코드를 수정해서는 안된다.
→ DIP: 의존관계를 선언할 때, 구현체가 아닌 인터페이스를 대상으로 명시한다.
- 위의 두 가지 결론을 생각해보면 다음과 같은 질문이 든다.
→ OCP: 프로그램에 새로운 구현체를 추가했을 때 어떻게 기존의 코드를 수정하지 않을 수 있을까?
→ DIP: 의존관계는 객체 간의 관계를 명시한 것인데, 인터페이스로 의존관계를 어떻게 형성할까?
b) 어떻게 하면 OCP를 준수할 수 있을까?
- OCP에 의거하여, 프로그램을 확장할 때 기존의 코드를 수정해서는 안된다.
- 그렇다면 새로운 객체가 프로그램에 추가되었을 때, 기존의 코드는 어떻게 이 새로운 객체를 이해할 수 있을까?
- 여기서 다형성의 개념이 다시 한번 등장한다.
- 프로그램이 객체 중심적으로 작동되는 것이 아니라 인터페이스 중심적으로 동작하면 가능하지 않을까?
- 즉, 프로그램이 인터페이스를 우선적으로 인식하고 해당 인터페이스를 상속받은 구현체를 찾아 사용하는 방식이라면
가능하지 않을까?
c) 어떻게 하면 DIP를 준수할 수 있을까?
- DIP에 의거하여, 의존관계를 명시할 때 구현체가 아닌 인터페이스를 사용하여 의존관계를 명시한다.
- 의존관계는 객체 간의 관계인데 어떻게 인터페이스로 명시된 관계를 프로그램이 이해할 수 있을까?
- OCP와 동일하게 프로그램은 인터페이스 간의 관계를 이해하고, 각 인터페이스를 상속받은 구현체를
찾아 사용하는 방식이라면 가능하지 않을까?
d) OCP와 DIP 위배를 해결하는 방법
- 위에서 추측한 OCP와 DIP를 준수하는 방법에는 한 가지 공통점이 있다.
- 프로그램 내부적으로 인터페이스를 타입(자료형)으로 인지하여 해당 인터페이스를 상속받는 구현체를 찾아내는 것이다.
- 그럼 여기에 꼬리 질문이 따라온다.
→ 누가 인터페이스를 기반으로 구현체를 찾아줄 것인가?
→ 그 구현체는 어떻게 찾을 것인가?
- 이 꼬리 질문을 바탕으로 추측할 수 있는 결론은 다음과 같다.
→ 누군가 대신 구현체를 찾아주면 된다.
→ 프로그램 내부의 모든 구현체를 모아놓은 저장소를 만들면 된다.
e) Spring을 사용하는 이유
- Spring Framework는 위에서 말한 결론의 역할을 모두 수행한다.
→ Spring은 인터페이스를 기반으로 해당 인터페이스를 상속받은 객체를 찾아준다.
→ Spring은 모든 구현체를 모아놓은 Spring Container라는 저장소를 이용하여 구현체를 검색한다.
- 즉, Spring을 사용하는 이유는 Java를 이용해 온전한 객체지향 프로그래밍을 구사하기 위함이며,
Java 만으로는 구현할 수 없는 개념(OCP, DIP)을 구현할 수 있도록 Spring Framework가 도와주는 것이다.
- 그리고 이와 같은 Spring의 기능 및 역할로 인하여 IoC가 발현된다.
f) IoC(Inversion of Control)
- Spring의 핵심 역할 중 하나는 인터페이스를 상속받은 구현체를 관리하고 찾아준다는 것이다.
- 이는 프로그램을 작성하는 개발자의 코드에 의한 것이 아닌, Spring Framework의 자체적인 기능이자 역할이다.
- 그러므로 프로그램의 전반적인 흐름이 Spring Framework에 의해 조절된다.
→ Spring이 인터페이스를 기반으로 해당 인터페이스를 상속받은 구현체를 대신 찾아준다.
→ Spring은 검색한 구현체를 인터페이스로 명시된 의존관계에 주입해준다.(LSP, 인터페이스를 구현체로 치환)
- 이처럼 외부에 의한 프로그램의 흐름이 유지되는 구조를 IoC라고 표현한다.
4. Spring Container
- Spring Container는 Spring Framework를 이용한 프로그램 내부에 존재하는 모든 구현체를 관리하는 저장소다.
- Spring Container에 대해서 알아보자.
a) ApplicationContext
- Spring Container의 진짜 이름은 ApplicationContext다. 이 또한 인터페이스로 정의되어 있다.
- Spring Container는 구현체의 생성, 사용, 의존관계 형성의 책임을 가지고 있다.
- 이와 같은 책임을 가짐으로, IoC 또는 DI 컨테이너라고 불리기도 한다.
b) ApplicationContext의 종류와 구현
- Spring은 다양한 Container 구현체를 지원한다.
- ApplicationContext 자체가 인터페이스이며, 어떤 형식의 설정 파일을 사용하느냐에 따라 다양한 Container를 구현할 수 있다.
// 1. Annotation 기반 Spring Container
ApplicationContext ac = new AnnotationConfigApplicationContext(설정파일명.class);
// 2. XML 기반의 Spring Container
ApplicationContext ac = new GenericXmlApplicationContext("Xml 설정파일명");
// 3. 사용자 설정
ApplicationContext ac = new ???ApplicationContext(???);
- Spring은 Annotation을 사용한 방식을 권장하고 있다.
c) Bean Factory, Bean Container, Bean Definition
- Spring Container가 다양한 구현체를 가질 수 있는 이유는 다형성을 사용하기 때문이다.
- 쉽게 말하자면 ApplicationContext가 인터페이스로 정의되어 있기 때문이다.
- ApplicationContext는 BeanFactory 인터페이스를 상속받는다.
- BeanFactory는 ApplicationContext의 상위 개념으로, Bean을 관리하고 조회하는 기능을 제공한다.
- Spring Container 내부에서 Bean 관리 역할을 수행하는 부분은 Bean Container이다.
- Bean Container 내부에는 Bean Definition이 있는데, 이 인터페이스 덕분에 다양한 Container를 구현할 수 있다.
- Bean Definition은 설정 파일의 정보를 읽어 Bean을 생성한다.
- 이때 내부의 Bean Definition Reader가 설정 파일을 읽고 Bean 구현체의 종류를 선택한다.
- 이와 같은 동작원리로 Spring Container는 다양한 구현체를 가질 수 있다.
d) Spring Container가 Bean을 관리하는 방법
- Spring은 기본적으로 AnnotationConfigApplicationContext를 Container의 구현체로 사용한다.
- 어노테이션 기반의 Spring Container는 Singleton 방식으로 객체(= Bean)를 관리한다.
- Singleton이란, 단 하나의 객체만을 생성하여 이를 공유하는 방식이다.
5. Bean 수동/자동 등록
- Spring Framework는 어노테이션 기반의 Spring Container를 기본으로 제공한다.
a) Spring Bean 수동 등록 - @Configuration, @Bean
- 수동 등록이란, 설정 파일을 이용한 Spring Container의 생성 및 Bean 등록 방식을 말한다.
- 설정 파일에는 인터페이스의 구현체가 사용되며, 구현체 간의 의존관계 또한 명시한다.
- @Configuration과 @Bean은 어노테이션 기반 Spring Container의 설정 파일을 작성할 때 사용된다.
- 설정 파일에 @Configuration을 부착하면, 내부에 존재하는 Bean은 모두 Singleton 방식으로 관리된다.
- 이와 같은 Bean 관리방식으로 인하여 Spring Container는 Singleton Container라는 이름으로 불리기도 한다.
→ @Configuration: 해당 파일이 설정 파일이며, 해당 설정 파일을 Spring Container로 사용할 것임을 명시
→ @Bean: 설정 파일 내부의 객체에 부착되는 어노테이션, Spring Container가 관리하는 객체임을 명시
@Configuration
public class AppConfig {
@Bean public TestService testService() {
return new TestServiceImpl(testRepository());
}
@Bean public TestRepository testRepository() {
return new TestMemoryRepository();
}
}
b) Bean 자동 등록 - @ComponentScan, @Component
- 자동 등록이란, 설정 파일 없이 어노테이션을 이용하여 Spring Container에 Bean을 등록하는 방식을 말한다.
- 이를 위해 @ComponentScan과 @Component 어노테이션을 사용한다.
→ @ComponentScan: 프로그램 내부에 존재하는 모든 Bean을 탐색한다. @Component를 Bean으로 인식한다.
→ @Component: Spring Container에게 해당 객체가 Bean이라는 것을 명시한다.
c) @ComponentScan의 동작원리
- ComponentScan의 탐색 경로는 @SpringBootApplication이 붙어있는 main() 메서드 파일의 위치를 기본으로 한다.
- main() 메서드가 명시된 파일은 프로그램의 최상위 경로에 존재한다.
- 그러므로 ComponentScan은 최상위 경로를 시작으로 모든 하위 클래스를 탐색하게 된다.
- ComponentScan이 진행되면 클래스 내부에 존재하는 @Component 어노테이션을 찾고, 이를 Bean으로 인식한다.
- Bean을 찾으면 이는 Spring Container에 Singleton 방식의 Bean으로 등록 및 관리된다.
d) ComponentScan의 대상
- Spring은 @Component를 포함하는 다양한 어노테이션을 제공한다.
- 다음 어노테이션은 모두 ComponentScan의 대상이며, Bean으로 Spring Container에 등록된다.
→ @Controller: Spring은 해당 클래스를 Controller로 인식한다.
→ @Repository: Spring은 해당 클래스를 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 spring 예외로 변환한다.
→ @Service: 개발자 간의 Semantic annotation으로, 비즈니즈 로직을 처리하는 역할임을 명시한다.
→ @Bean: 설정 파일에 명시되는 Bean으로, 이 또한 ComponentScan의 대상으로 Container에 Bean으로 등록된다.
→ @Configuration: Spring은 해당 클래스를 설정 정보로 인식하고, 이 설정 정보 또한 Bean으로 관리된다.
더불어, 설정 파일 내부에 존재하는 @Bean을 찾아 Bean으로 등록한다.
6. DI(Dependency Injection)
- Spring의 역할 중 하나가 의존관계를 명시하는 인터페이스의 구현체를 검색하여 의존관계를 형성하는 것이다.
- 이처럼 의존 관계 형성을 위해 외부에서 구현체를 찾아 주입해주는 이 기능을 DI라고 부른다.
- 설정 파일을 사용하지 않고 어노테이션을 통해 의존관계를 명시하는 자동 의존성 주입 방법에 대해 알아보자.
a) @Autowired
- @Autowired 어노테이션은 생성자, 수정자(Setter), 메서드에 부착할 수 있다.
- 부착되는 대상에 상관없이 내부에서 사용되는 객체와 해당 클래스의 객체가 의존 관계를 형성함을 명시한다.
- @Autowired를 사용하기 위해서는 반드시 의존 관계를 형성하는 대상 모두 Spring Bean으로 등록되어야 한다.
b) @Autowired의 동작원리
- Spring은 ComponentScan 과정에서 @Autowired를 인식한다.
- @Autowired가 붙은 코드에서 인터페이스 정보를 기반으로 해당 인터페이스를 상속받은 Bean을 Container에서 찾는다.
- Spring은 @Autowired가 명시되어 있는 클래스의 구현체와 인터페이스의 구현체를 이용해 의존 관계를 형성시킨다.
c) Bean이 존재하지 않는 경우
- 의존 관계를 형성하기 위해 필요한 구현체가 Spring Container에 존재하지 않는 경우 에러가 발생한다.
→ NoSuchBeanDefinitionException
- 이는 다음과 같은 방법으로 해결할 수 있다.
→ @Autowired(required = false): 구현체가 없는 경우, 해당 메서드를 실행하지 않는다.
→ @Nullable: null 값을 허용, 의존관계는 형성되지 않으나 메서드의 로직은 실행된다.
→ Optional <T>: null 값을 허용, 의존관계는 형성되지 않으나 메서드의 로직은 실행된다.
d) 동일한 인터페이스를 상속받은 Bean이 2개 이상인 경우
- Spring Container에서 동일한 인터페이스를 상속받은 구현체가 2개 이상 검색되는 경우 에러가 발생한다.
→ NoUniqueBeanDefinitionException
- 이는 다음과 같은 방법으로 해결할 수 있다.
→ 필드명 매칭: 메서드 내부의 인터페이스의 이름을 구현체의 이름과 동일하게 선언한다.
→ @Qualifier("이름"): 특정 Bean에 추가적인 구분자를 붙이는 것으로, 이름표를 부착하는 방식이다.
→ @Primary: 특정 Bean을 우선적으로 선택하도록 명시한다.
→ @Qualifier와 @Primary가 함께 사용되는 경우, @Qualifier가 우선권을 갖는다.
7. Bean Lifecycle
a) Bean Lifecycle 개념
- Spring에서 관리되는 모든 Bean은 Spring에 의해 생성되고 Spring이 종료될 때 함께 소멸된다.
- 이처럼 Bean의 생성부터 소멸까지의 과정을 Bean Lifecycle이라고 한다.
b) Bean Lifecycle
- Bean은 다음과 같은 과정을 거친다.
Continer 생성 → Bean 등록 → DI 주입 → 초기화 → Bean 사용 → 소멸 → Spring 종료
c) Bean Lifecycle callback
- Bean의 생명주기에 따른 특정 시점에 특정 메서드를 실행하도록 설정할 수 있다.
- Spring은 다음과 같은 다양한 방법을 지원한다.
→ InitializingBean 인터페이스 & DisposableBean 인터페이스
→ @Bean(initMethod = "초기화 메서드", destroyMethod = "소멸 메서드")
→ @PostConstruct & @PreDestroy 어노테이션
8. Bean Scope
a) Bean Scope 개념
- 모든 Bean은 Lifecycle을 가지고 있다.
- 다만, Bean의 종류에 따라 Spring Container에 의해 관리되는 기간이 달라진다.
- 이 기간의 차이에 의한 Bean의 종류를 Bean Scope라고 부른다.
b) Bean Scope의 종류
- Bean은 크게 3가지 종류로 구분되며, 각 종류마다 서로 다른 Scope를 가진다.
→ Singleton Bean Scope
→ Prototype Bean Scope
→ Web Bean Scope
c) Singleton Bean Scope
- Spring이 Bean에게 기본적으로 부여하는 Scope
- Spring Container와 동일한 생명주기를 갖는 Bean이다.
d) Prototype Bean Scope
- Spring Container에 의해 구현체가 생성되고, 의존관계 주입이 완료될 때까지만 Container에 의해 관리된다.
- 의존관계 주입이 완료되고 나면 해당 구현체에 대한 책임은 client에게 양도된다.
- 즉, client가 직접 소멸 메서드를 호출해야 한다.
- Prototype Bean은 client의 요청에 따라 새롭게 생성된다.
e) Web Bean Scope
- 웹 작업을 처리할 때 사용되는 Bean에게 부여되는 scope이다.
- Web Bean은 각각의 역할에 따른 생명주기를 갖는다.
→ Request Scope: HTTP 요청을 처리하기 위해 생성되는 객체에게 부여되는 Scope
→ Session Scope: 웹 세션이 생성 및 종료까지 유지된다. HTTP Session과 동일한 생명주기를 갖는다.
→ Application Scope: 웹 Servlet Context와 동일한 범위로 유지된다.
9. DL(Dependency Lookup)
a) DL 개념
- HTTP Request Bean을 사용하는 코드의 경우, 프로그램의 빌드 시점에서 오류가 발생한다.
- 실제 HTTP Request가 들어오기 전까지, request Bean은 존재할 수 없는 구현체이기 때문이다.
- 이처럼 생성되지 않은 객체를 임의로 생성하여 사용할 수 있도록 하는 기능을 DL이라고 한다.
- 즉, Spring Container에게 가짜 구현체를 만들어 달라고 요청하는 것이다.
b) DL을 사용하는 방법
- Spring에서는 다음과 같은 기능을 통해 DL을 구현한다.
→ ObjectFactory, ObjectProvider
→ JSR-330의 Provider
→ @Scope(value="스코프", procyMode = 상수)
'Back-end > Spring 개념' 카테고리의 다른 글
16. Bean Scope에 대하여 (0) | 2022.02.17 |
---|---|
15. Bean 생명주기 콜백 (0) | 2022.02.16 |
14. Annotation을 만드는 방법 (0) | 2022.02.15 |
13. @Autowired의 다양한 문제 해결방법 (0) | 2022.02.14 |
12. Dependency Injection 방법 (0) | 2022.02.11 |
댓글