본문 바로가기
Back-end/Spring 개념

17. Spring 기본개념 총정리

by devraphy 2022. 2. 18.

0. 개요

- 이번 포스팅에서는 그동안 배운 Spring의 기본 개념을 정리해보려고 한다.

- 이번 포스팅은 다음과 같은 목차로 구성된다.

 

< 목차 >

1. 객체지향과 다형성

2. SOLID원칙

3. Spring Framework를 사용하는 이유

4. Spring Container

5. Bean 수동/자동 등록

6. Dependency Injection

7. Bean Lifecycle

8. Bean Scope

9. Dependency Looking


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

댓글