자바로 개발을 하다보면 수많은 어려움에 봉착한다.
객체 지향적으로 개발하고 싶은데, 정작 객체 지향을 구현하기 위해
핵심 비즈니스 로직의 구현이 미뤄지게 된다.
자바 언어만으로 개발을 하는 것은 정말 어렵다.
자바 언어도 기능은 안으로 숨겨서 사용하기 편하게 캡슐화가 되었으면 좋겠다.
코딩의 세계에서 상상은 현실이 되는 법.
자바를 편리하게 사용하여 객체 지향적 개발을 도와주는 Spring이 등장한다.
Java 언어만으로 객체 지향의 특징인 SOLID 원칙을 준수하는 것은 굉장히 어렵다.

예를 들어 OCP(개방 폐쇄 원칙)를 너무 잘 지킨 코드라고 할지라도,
결국에는 구현체를 변경하는 코드의 수정이 발생하기 때문에 SOLID 원칙을 준수하기 어렵다.
class Vehicle {
// 코드
}
class Car implements Vehicle {
// 코드
}
class Airplane implements Vehicle {
// 코드
}
class VehicleService {
private Vehicle vehicle;
public VehicleService(Vehicle vehicle) {
this.vehicle = vehicle;
}
}
public void dipMethod() {
Car car = new Car();
VehicleService vs = new VehicleService(car); // ** 의존 관계 주입 **
Airplane airplane = new Airplane();
VehicleService vs = new VehicleService(airplane); // ** 다른 구현체로 변경 **
}
정적 클래스 의존이 아닌 실행 시점에 객체를 결정하는
동적 객체 의존 관계(Dependency Injection)를 주입하는 경우는
구현체를 변경해야 해서 수정에서 OCP, DIP를 위반할 수 밖에 없다.

IoC(Inversion f Control, 제어의 역전)에 근거하여
의존 관계를 주입하는 별도의 외부 클래스를 설계하면
기존 클라이언트 클래스에는 코드의 수정이 발생하지 않지만,
결국 의존 관계를 주입하는 별도의 클래스는 수정이 발생할 수 밖에 없다.
코드의 수정이 발생되지 않기 위해선,
외부에서 누군가 의존 관계를 주입해주면 좋을 것 같다.
이렇게 자바의 한계를 극복하기 위해 여러 일을 처리해주는 것이 바로 Spring이다.

Spring은 Java 기반의 애플리케이션 개발을 지원하는 도구이다.
이번에는 Spring이 어떤 구조를 가지고 있어서 애플리케이션을 개발할 때
Java를 더욱 효율적으로 사용할 수 있는지 살펴보자.

스프링은 객체를 Bean이라는 형태로 관리한다.
Bean을 알아보기 전에 먼저 Bean을 보관하는 스프링 컨테이너에 대해 알아야 한다.
프로그램을 실행하면 Spring은 스프링 컨테이너를 생성한다.
스프링 컨테이너는 Bean 이름, Bean 객체를 함께 관리하는 보관소이다.
우리는 이 스프링 컨테이너에 빈을 등록해서 Spring이 관리할 수 있도록 설정할 수 있다.
어떻게 스프링 컨테이너에 Bean을 등록할 수 있을까?

Java에서 클라이언트 코드의 수정을 최소화하기 위해
의존 관계를 주입하고 객체를 생성하는 별도의 외부 클래스를 생성했다.
Spring에서도 별도의 외부 클래스를 통해 Bean을 등록하는데,
특수한 방법으로 객체를 등록하는 방식으로 관리한다.
@Configuration
class AppConfig {
@Bean(name = repository)
@Profile("profile")
public Repository repository() {
return new Repository();
}
}
@Import(AppConfig.class) // ** 외부 설정 클래스 스프링 컨테이너 등록 **
class Application {
// 코드
}
객체를 생성하는 외부 설정 클래스에 선언하는 애노테이션이다.
이 애노테이션을 통해서 개발자는 객체 생성 클래스라는 것을 인지할 수 있다.
명시적으로 외부 설정 클래스나 Bean을 Spring Container에 등록하는 애노테이션이다.
Bean 수동 등록 시, 외부 설정 클래스를 먼저 컨테이너에 등록하기 위해 사용한다.
Bean으로 등록할 클래스에 선언하는 애노테이션이다.
@Bean 애노테이션을 선언하는 경우 생성하는 객체가 스프링 컨테이너에 등록된다.
name element를 통해 Bean 이름을 지정할 수 있다.
생략하면 클래스 맨 앞글자를 소문자로 바꾸어 등록된다(Repository → repository).
Spring은 로딩 시
application.properties의 spring.profiles.active=profileName을 읽는다.
@Profile 애노테이션이 선언된 Bean은
profileName과 일치하는 경우에만 스프링 컨테이너에 등록된다.

생성해야할 객체가 몇개 없다면 이렇게 외부 설정 클래스를 이용해도 상관이 없다.
하지만 객체가 수 십개, 수 백개로 늘어난다면?
최후에 설정 클래스는 확인하기도 어려운 등록 자체에 급급한 클래스로 전락할 것이다.
꼭 외부 설정 클래스에 Bean을 등록하지 않고도
Bean을 스프링 컨테이너에 자동으로 등록하는 방법이 있다.
등록해야할 객체, Component를 스캔하고 등록한다고 해서 Component Scan이라고 부른다.
자동으로 등록하는 편리한 기능이기 때문에 해당 기능 사용이 권장되며,
비즈니스 로직이 아닌 기술 지원 로직의 경우에만
외부 설정 클래스를 만들어 수동으로 등록하는 방식을 권장한다.
@ComponentScan(basePackages = "pacakage.path") // ** Bean 자동 등록 방식 선언 **
class Application {
// 코드
}
@Component
class Service {
// 코드
}
@Repository
class Repository {
// 코드
}
Bean 자동 등록 방식인 Component Scan을 선언하는 애노테이션이다.
애노테이션이 어딘가에 선언되어 있어야 @Component가 선언된 객체 Bean 등록이 가능하다.
@ComponentScan이 선언된 클래스의 하위 패키지는 모두 Component Scan의 대상이다.
@ComponentScan은 스캔을 선언함과 동시에 Scan을 설정하는 애노테이션이다.
basePackages를 통해 Component Scan 상위 패키지를 지정할 수 있으며,
그 외에도 includeFilters, excludeFilters element와 @Filter 애노테이션을 사용해
스캔에 포함, 제외할 객체를 설정할 수 있다.
ComponentScan은 보통 메인 애플리케이션이나 외부 설정 클래스에 함께 선언한다.

자동으로 객체를 스캔하여 등록하는 Component Scan 대상으로 지정하는 애노테이션이다.
이 애노테이션이 선언된 경우 Component Scan을 거쳐 자동으로 스프링 컨테이너에 등록된다.
웹 구조에는 공통적으로 사용되는 것들이 있다.
데이터 저장 Repository, 비즈니스 로직 Service, 흐름을 통제하는 Controller 등.
이러한 객체들은 구분을 편리하게 하기 위하여 특수한 Annotation을 선언한다.
기능은 @Component와 유사하지만, 구분을 용이하게 하기 위해 존재한다.
@Configuration의 특수 버전이다.
@ComponentScan이 포함되어 Scan을 설정할 수 있다.
핵심은 @EnableAutoConfiguration이 포함되어 있다는 점인데,
Spring 동작에 필요한 Spring Bean을 자동으로 등록해주는 역할을 한다.

스프링 컨테이너는 객체를 생성하여 활용할 수 있다.
과거 BeanFactory 인터페이스를 활용하여 생성했지만,
기능이 추가되어 ApplicationContext 인터페이스로 객체를 생성한다.
파라미터에는 @ComponentScan 애노테이션이 선언된 클래스를 전달한다.
해당 클래스를 참고하여 Component Scan을 수행하는데
보통 외부 설정 클래스를 파라미터로 전달한다.
// ** Spring Container 객체 생성 **
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
참고로 파라미터에는 .class 확장자의 자바 파일 외에도
xml 파일과 같은 다른 파일도 전달 가능하다.
DefinitionReader라는 존재가 파일을 BeanDefinition이라는 인터페이스로 추상화하여
파일이 달라도 해석이 가능하다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName); // ** Bean 이름으로 조회 **
}
Clazz clazz = ac.getBean(Clazz.class); // ** 클래스 타입으로 조회 **
스프링 컨테이너 객체를 생성한 후,
getBean() 메서드에 Bean 이름, 클래스 타입 등을 파라미터로 전달하면
Bean 객체를 얻을 수 있고, 다운캐스팅 하여 객체의 메서드도 호출할 수 있다.

Spring은 Spring Container에 객체를 Bean으로 등록하여 사용한다는 것을 알았다.
그렇다면 객체의 의존 관계는 어떻게 주입할 수 있을까?
의존 관계 주입에는 총 4가지 방식이 있다.
하지만 의존 관계 주입은 거의 변경될 일이 없기 때문에 생성자 주입을 사용하고
부득이하게 변경 가능성이 있는 경우에만 수정자 주입(setter 주입)을 사용한다.
의존 관계를 주입하는 애노테이션이다.
각 주입 방식에 함께 선언한다.

class Service {
private final Repository repository;
// ** 생성자 메서드에 애노테이션 선언 **
@Autowired // ** 생성자가 1개인 경우 생략 가능 **
public Service(Repository repository) {
this.repository = repository;
}
}
생성자 메서드를 통해 의존 관계를 주입하는 방법이다.
객체를 생성하는 생성자 호출 시점에 1회 호출된다.
Spring은 객체를 생성한 후, 의존 관계를 주입하게 되는데
생성자 주입을 사용하게 되면 객체 생성과 의존 관계 주입이 동시에 일어난다.
불변, 필수 의존 관계에 사용된다.
생성자가 1개인 경우 @Autowired는 생략할 수 있다.

class Service {
private final Repository repository;
// ** setXXX 메서드에 애노테이션 선언 **
@Autowired
public setRepository(Repository repository) {
this.repository = repository;
}
}
setXXX() 메서드를 통해 의존 관계를 주입하는 방법이다.
setXXX() 메서드를 호출하지 않고 Spring이 자동으로 인식하여 의존 관계를 주입하는데,
값을 직접 변경하지 않고 자바가 setXXX, getXXX 메서드를 인식하는
자바빈 프로퍼티 방식으로 동작한다.
생성자 주입처럼 불변 관계에 있는 의존 관계와 달리,
의존 관계 변경 가능성이 있는 경우에 사용한다.
의존 관계를 변경하는 경우 변경 메서드를 호출하여 변경할 수 있다.

class Service {
@Autowired // 필드에 선언
private final Repository repository;
}
필드에 @Autowired를 선언하여 의존 관계를 주입하는 방식이다.
간단해서 사용하기 편리하지만, 테스트 코드 작성 시에
DI 프레임워크가 있어야만 동작이 가능해서 사실상 사용이 지양된다.

class Service {
private final Repository repository;
private final ServiceOption serviceOption;
// ** 메서드에 애노테이션 선언 **
@Autowired
public springMethod(Repository repository, ServiceOption serviceOption) {
this.repository = repository;
this.serviceOption = serviceOption;
}
}
일반 메서드를 통해 의존 관계를 주입하는 방식이다.
메서드를 호출할 필요 없이 Spring이 자동으로 의존 관계를 주입하며,
setter 주입과 달리 한번에 여러 객체를 주입할 수 있다.
하지만 가독성이 떨어져 사용이 지양된다.

class Service {
// ** Repository가 Bean으로 등록되지 않았다면 예외 발생 **
private final Repository repository;
@Autowired(required = false) // ** 예외 없이 동작 **
public Service(Repository repository) {
this.repository = repository;
}
@Autowired
public Service(@Nullable Repository repository) { // ** 예외 없이 동작 **
this.repository = repository;
}
@Autowired(required = false)
public Service(Optional<Repository> repository) { // ** 예외 없이 동작 **
this.repository = repository;
}
}
Spring이 객체를 생성한 후, 의존 관계를 주입할 때
의존 관계를 주입할 객체가 Bean으로 등록되지 않았다면 예외를 발생시킨다.
하지만 @Autowired 애노테이션에 required = false로 element를 선언하는 경우,
또는 파라미터에 @Nullable 애노테이션을 선언하여 전달하는 경우,
required = false로 선언 후 Optional<>로 파라미터를 전달하는 경우에는
예외 없이 동작하고 등록되지 않은 Bean은 null로 의존 관계가 주입된다.
(Optional<>의 경우 Optional.empty가 주입된다.)

class ServiceOption {
// 코드
}
// ** ServiceOption 하위 타입 2개 Bean 등록 **
@Component
class FirstServiceOption implements ServiceOption {
// 코드
}
@Component
class SecondServiceOption implements ServiceOption {
// 코드
}
class Service {
private final Repository repository;
private final ServiceOption serviceOption;
// ** 의존 관계 주입 시 예외 발생 **
@Autowired
public springMethod(Repository repository, ServiceOption serviceOption) {
this.repository = repository;
this.serviceOption = serviceOption;
}
}
인터페이스에 대한 구현체 여러개를 Bean으로 등록할 수 있다.
하지만 해당 인터페이스 의존 관계 주입을 시도할 경우 예외가 발생한다.
어떻게 해결할 수 있을까?
class Service {
private final Repository repository;
private final ServiceOption serviceOption;
@Autowired
public springMethod(Repository repository, ServiceOption firstServiceOption) {
this.repository = repository;
this.serviceOption = firstServiceOption; // ** 필드명으로 매칭 **
}
}
의존 관계 주입 시에 필드명을 조회할 여러 개의 Bean들 중 하나로 지정하면,
Spring은 필드명으로 매칭하여 예외 없이 의존 관계를 주입한다.

class ServiceOption {
// 코드
}
@Component
@Qualifier("firstServiceOption") // ** Qualifier 선언 **
class FirstServiceOption implements ServiceOption {
// 코드
}
@Component
@Qualifier("secondServiceOption") // ** Qualifier 선언 **
class SecondServiceOption implements ServiceOption {
// 코드
}
class Service {
private final Repository repository;
private final ServiceOption serviceOption;
@Autowired // ** Qualifier 선언 **
public springMethod(Repository repository,
@Qualifier("firstServiceOption") ServiceOption serviceOption) {
this.repository = repository;
this.serviceOption = serviceOption;
}
}
클래스와 파라미터에 각각 동일한 @Qualifier 애노테이션을 선언하면,
조회할 Bean이 2개 이상이어도 Qualifier에 선언된 이름을 통해 매칭한다.

class ServiceOption {
// 코드
}
@Primary // ** @Primary 애노테이션 추가 **
class FirstServiceOption implements ServiceOption {
// 코드
}
@Component
class SecondServiceOption implements ServiceOption {
// 코드
}
class Service {
private final Repository repository;
private final ServiceOption serviceOption;
@Autowired
public springMethod(Repository repository, ServiceOption serviceOption) {
this.repository = repository;
this.serviceOption = serviceOption;
}
}
클래스에 @Primary 애노테이션을 추가하면
조회할 Bean이 2개 이상일 때 해당 클래스의 객체에 우선권을 부여한다.

Java에 대해 열심히 공부하고 본격적으로 애플리케이션을 개발하기 위해
Java 기반의 Spring에 대해 알아봤다.
Spring을 사용하기 전에 스프링 컨테이너를 사용하는 구조와
Bean을 등록하고 의존 관계를 주입하는 과정까지.
Spring의 스타트 지점을 확실히 알고 사용하자.