1. Spring Boot란?

1.1 Spring Framework

자바 플랫폼을 위한 오픈소스 애플리케이션 프레임워크로서, 엔터프라이즈급 애플리케이션을 개발하기 위한 모든 기능을 종합적으로 제공하는 경량화된 솔루션.

엔터프라이즈급 개발을 뜻대로만 풀이하면 기업을 대상으로 하는 개발이라는 말이다. 즉, 대규모 데이터 처리와 트랜잭션이 동시에 여러 사용자로 부터 행해지는 매우 큰 규모의 환경을 엔터프라이즈 환경이라 일컫는다.

Spring Framework는 경량 컨테이너로 자바 객체를 담고 직접 관리한다.
객체의 생성 및 소멸 그리고 라이프 사이클을 관리하며, 언제든 Spring 컨테이너로 부터 필요한 객체를 가져와 사용할 수 있다.
이는 Spring이 IOC 기반의 Framwork임을 의미한다.

IOC란?

Inversion of Control의 약자로, 말 그대로 제어의 역전이다.

일반적으로 지금까지의 프로그램은 아래의 작업을 반복했다.

  • 객체 결정 및 생성 -> 의존성 객체 생성 -> 객체 내의 메소드 호출

이는 각 객체들이 프로그램의 흐름을 결정하고, 각 객체를 구성하는 작업에 직접적으로 참여한 것이다.

즉, 모든 작업을 사용자가 제어하는 구조이다.


하지만 IOC에서는 이 흐름의 구조를 바꾼다. IOC에서는 객체는 자기가 사용할 객체를 선택하거나, 생성하지 않는다. 또한 자신이 어디서 만들어지고 어떻게 사용되는지 또한 모른다. 자신의 모든 권한을 다른 대상에 위임하여 제어 권한을 위임받은 특별한 객체에 의해 결정되고 만들어진다.

즉, 제어의 흐름을 사용자가 컨트롤 하지 않고 위임한 특별한 객체에 모든 것을 맡긴다.

IOC란, 기존 사용자가 모든 작업을 제어하더 것을 특별한 객체에 모든 것을 위임하여 객체의 생성부터 생명 주기 등 모든 객체에 대한 제어건이 넘어간 것을 IOC, 제어의 역전이라고 한다.

프레임워크는 일종의 반제품이다. 프레임워크가 내가 만든 것들을 동작시켜준다. 그렇기 떄문에 프레임워크가 어떻게 동작하는지를 이해해야 한다.

1.1.1 Framework의 구성 요소

  • 프레임워크 코어(Cold Sopt) :
    변경되지 않고 반복적으로 재사용 되는 부분. 프레임워크에서 제공하는 라이브러리들을 의미하고, 사용하는 자원 관리나 처리 흐름을 제어한다.

  • 확장 포인트(Hook Point) :
    애플리케이션을 구축할 때 사용할 확장 포인트를 제공한다. 추상 클래스나 인터페이스 형태로 제공되는 것이 일반적이다.

  • 확장 모듈(Hot Spot) :
    각 애플리케이션이 확장 포인트를 상속해서 애플리케이션만의 비즈니스를 구현하는 것을 말한다.

  • 메타 데이터 :
    프레임워크에서 제공하는 Cold Spot과 Hook Point를 상속해서 구현한 Hot Spot을 유기적으로 결합하여 동작하도록 하는 환경 설정 파일이다. 일반적으로 XML, java Config class 형태로 작성한다.

1.1.2 Framework의 장점

  • 빠른 구현시간
  • 관리의 용의성 증가
  • 개발자들의 역량 획일화
  • 검증된 아키텍처의 재사용과 일관성 유지

1.2 Spring Boot

1.2.1 Spring Boot란?

Spring Boot는 Spring 프레임워크를 기반으로 만들어진 도구이다.

Spring Boot는 복잡한 Spring 구성을 단순화하고, 더 빠르고, 광범위한 접근성을 제공하기 위해 개발되었다. 이는 개발자가 최소한의 설정으로 즉시 실행 가능한 어플리케이션을 개발할 수 있도록 지원하는 것을 목표로 하고 있다.

1.2.2 Spring Boot 주요 장점

  1. 자동 구성 (Auto-configuration) :
    • Spring Boot는 클래스패스 세팅, 다른 빈, 다양한 프로퍼티 설정을 기반으로 어플리케이션을 자동으로 구성한다.
    • 이는 개발자가 일반적인 어플리케이션 설정에 필요한 시간을 대폭 줄여준다.
  1. 독립 실행 가능한 어플리케이션 :
    • 내장 서버 지원(Tomcat, Jetty, Undertow 등)으로 인해 별도의 웹 서버를 설치할 필요 없이 어플리케이션을 실행할 수 있다.
  1. 의존성 관리 :
    • 'starter' 종속성을 통해 필요한 의존성을 쉽게 관리할 수 있다.
    • 이는 Maven이나 Gradle 구성 파일에서 필요한 라이브러리만 선언하면, Spring Boot가 호환되는 버전을 자동으로 관리한다.
  1. 운영 준비 상태 :
    • 각종 상태 체크, 건강 상태, 메트릭스 등을 제공하는 액추에이터(Actuator)로, 프로덕션 준비를 보다 수월하게 할 수 있다.
  1. 뛰어난 커뮤니티 및 기술 지원
    - Spring Boot는 활발한 커뮤니티와 지속적인 업데이트를 통해 최신 Java 기술 트렌드에 발맞춰 나가고 있다.

1.2.3 Spring Boot 단점

  1. 과도한 자동 구성 :
    • 자동 구성이 매우 편리하긴 하나, 때때로 너무 많은 자동화가 이루어져 어플리케이션의 시작 시간이 길어지거나 예상치 못한 동작을 할 때가 있다.
    • 이를 해결하기 위해서는 상세한 구성의 이해가 필요하다.
  1. 리소스 사용량 :
    • 내장 서버와 자동 구성 기능 때문에 가벼운 마이크로서비스를 위한 다른 솔루션들에 비해 상대적으로 많은 메모리와 리소스를 사용할 수 있다.
  1. 학습 곡선 :
    • Spring Boot 자체는 단순화된 접근을 제공하지만, 내부에서는 여전히 복잡한 Spring 생태계와 관련된 지식이 필요하며, Spring Boot의 자동 구성 이면의 작동 방식을 이해하기 위해선, Spring Framework에 대한 깊은 이해가 요구된다.

Spring Boot는 기존 Spring Framwork의 기능을 유지하면서 개발자가 더욱 쉽게 접근할 수 있도록 돕기 위해 만들어진 Framework이다. 이는 특히 대규모 프로젝트와 마이크로서비스 아키텍처에서 더욱 빛을 발한다.

2. 스프링 코어(DI, AOP)

2.1 DI

2.1.1 컨테이너(Container)란?

어떤 환경에서나 실행하기 위해 필요한 모든 요소를 포함하는 소프트웨어 패키지

  • 컨테이너는 인스턴스의 생명주기를 관리한다.
  • 생성된 인스턴스들에게 추가적인 기능을 제공한다.

DI Container와 Docker Container에서 두 Container는 같은 것일까?

결론부터 말하면, DI Container와 Docker Container는 서로 다른 개념이다.

DI Container는 소프트웨어 디자인 패턴에서 객체의 생성 및 의존성 관리를 자동화 하는 도구인 반면, Docker Container는 응용 프로그램을 그 종속성과 함께 패키지화하여 실행 환경의 차이로 인한 문제를 해결하는 기술이다.

정리하자면,

  • DI Container : 객체의 의존성을 관리하고 주입하는 소프트웨어 디자인 패턴 도구.
  • Docker Container : 애플리케이션과 그 종속성을 패키징하여 일관된 실행 환경을 제공하는 운영체제 수준의 가상화 기술이다.

2.1.2 IoC란?

Inversion Of Control : 제어의 역전

IoC에 대해서는 위에서 언급하였으므로, 간단하게 정리하고 넘어가도록 한다.

  • IoC : Inversion Of Control의 약어로, Inversion은 사전적 의미로는 '도치', '역전'이다. 보통 IoC를 제어의 역전이라고 한다.
  • 개발자는 프로그램의 흐름을 제어하는 코드를 작성한다. 그런데, 이 흐름의 제어를 개발자가 하는 것이 아니라 다른 프로그램이 그 흐름을 제어하는 것을 IoC라고 말한다.

2.1.3 DI란?

Dependency Injection : 의존성 주입

  • '의존성 주입'은 제어의 역행이 일어날 때, Spring이 내부에 있는 객체들간의 관계를 관리할 때 사용하는 기법이다.
  • 클래스 사이의 의존 관계를 빈(Bean) 설정 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것을 말한다.

2.1.4 DI 컨테이너에서 인스턴스를 관리할 때의 장점

  • 인스턴스의 스코프를 제어할 수가 있다.
  • 인스턴스의 생명 주기를 제어할 수 있다.
  • AOP 방식으로 공통 기능을 집어넣을 수 있다.
  • 의존하는 컴포넌트 간의 결합도를 낮춰서 단위 테스트를 쉽게 만든다.

* 스코프 : 변수, 함수, 객체 등이 유효한 범위 또는 영역을 의미한다. 즉, 특정 변수가 접근 가능하고 사용할 수 있는 콛의 부분을 정의한다.

2.1.5 DI 컨테이너

Dependency Injection Container : 의존성 주입을 관리하는 도구

객체 생성 관리, 의존성 주입 관리, 수명 주기 관리, 설정 및 구성 관리 등의 역할을 한다.

  • Spring 공식 문서에서는 IoC 컨테이너라고 기재하고 있다.
  • 하지만, 개발자들은 보통 DI 컨테이너라고 말한다.
  • Spring Framwork 외의 DI 컨테이너
    • CDI (Contests & Dependency Injection)
    • Google Guice
    • Dagger

2.1.6 DI에서 자주 사용하는 용어

  1. 의존 객체 (Dependency) :
    객체 간의 관계 중 하나로, 객체 A가 객체 B를 사용하는 경우, 객체 A가 객체 B에 의존하고 있다고 표현한다.

  2. 의존 주입 (Dependency Injection) :
    객체 간의 의존 관계를 설정하는 방법 중 하나로, 외부에서 의존 객체를 생성자, setter, 필드 등을 통해 주입하는 방법이다.

  3. Bean :
    Spring에서 DI를 사용하기 위해 생성되는 객체를 의미한다.

  4. BeanFactory :
    Spring에서 Bean을 생성하고 관리하는 컨테이너이다.

  5. ApplicationContest :
    BeanFactory를 상송한 Spring Container로, 더 다양한 기능을 제공한다.

  6. Component :
    Spring에서 Bean을 생성하기 위한 어노테이션 중 하나로, 해당 클래스를 Bean으로 등록한다.

  7. Qualifier :
    같은 타입의 Bean이 여러 개 있을 경우, 어떤 Bean을 사용할지 결정하는 용도로 사용된다.

  8. AutoWiring :
    자동으로 Bean을 주입하는 기능으로, @Autowired 애노테이션을 통해 사용된다.

2.1.7 DI Container 주요 용어

* DI Container는 객체간 의존 관계를 설정하고, 관리하는 역할을 수행한다.

DI Container에서 자주 사용되는 용어들은 다음과 같다.

  1. Bean :
    DI Container가 관리하는 객체를 Bean이라고 부른다. Bean은 DI Container가 생성하고, 초기화하고, 보관하며, 필요한 곳에서 제공한다.

  2. Container :
    DI Container 자체를 Container라고 부르기도 한다. DI Container는 객체의 생성과 의존 관계를 설정하는 일을 담당한다. 여기서 객체는 Bean이다.

  3. Configuration :
    Di Container가 객체를 생성하고 의존 관계를 설정하기 위해 참조하는 설정 정보를 '구성(Configuration)'이라고 한다.

  4. Injection :
    DI Container는 생성된 빈에 필요한 의존 객체를 '주입(Injection)'한다.

  5. AutoWiring :
    DI Container가 Bean과 Bean 간의 의존 관계를 자동으로 설정해주는 기능을 Autowiring(자동 주입)이라고 한다.

  6. Scope :
    Bean의 생성 주기와 관련된 범위(Scope)를 설정할 수 있다. 대표적인 Bean Scope로는 Singleton, Prototype, Request, Session 등이 있다.

  7. Proxy :
    Di Container는 Bean을 가져올 때, 해당 Bean을 감싸는 Proxy 객체를 생성할 수도 있다. Proxy 객체는 빈의 메서드 호출을 가로채서 보안, 로깅, 트랜잭션 등의 작업을 수행할 수 있다.

DI와 DI Container에서 자주 사용되는 용어를 길지만 모두 정리해봤다. 이렇게 각자 길게 정리한 이유는 한 번 정리해두면 나중에 찾아보기 좋을 것이 때문이다. 나중에 용어에 대해 헷갈리 때가 오면 이 글을 참고하도록 하자.

2.2 Bean

2.2.1 개발자가 직접 생성하는 인스턴스 vs Spring Container가 관리하는 객체

  • Java에서 인스턴스 생성

    • 개발자가 직접 인스턴스를 만든다.

      		Book book = new Book();
  • Bean은 Container가 관리하는 객체
    • 객체의 생명주기를 Container로 관리한다.
    • 객체를 Singleton으로 만들 것인지, Prototype로 만들 것인지.

2.2.2 Spring IoC Container가 관리하는 Bean

Spring Boot에서 Bean은 Spring IoC Container가 관리하는 객체를 의미한다. Spring Framwork에서는 객체의 생성, 생명주기 관리, 그리고 객체 간의 의존성을 Container가 처리하며, 이렇게 관리되는 객체를 'Bean'이라고 부른다.

2.2.3 Bean의 주요 특징

  1. 관리의 자동화 :
    • Bean은 Spring Container에 의해 인스턴스화, 구성, 관리되며, 개발자는 복잡한 객체 생성 및 관리 과정에 대해 걱정할 필요가 없다.
  1. 싱글턴 패턴 :
    • 기본적으로, Spring에서 Bean은 싱글턴 패턴을 따라 하나의 신스턴스만 생성된다.
    • 즉, 어플리케이션 내에서 해당 Bean에 대한 요청이 있을 때마다 동일한 객체 인스턴스가 반환된다.
    • 필요에 따라 프로토타입(Prototype)과 같은 다른 스코프를 설정할 수도 있다.
  1. 의존성 주입(Dependency Injection) :
    • Bean은 필요한 의존성을 자동으로 주입받는다.
    • 이는 설정 파일, 어노테이션 등을 통해 구성할 수 있으며,
      • 생성자 주입 (Constructor Injection),
      • 세터 주입(Setter Injection)
    • 등의 방법을 사용할 수 있다.

2.2.4 Bean 정의 방법

Spring에서 Bean을 정의하는 방법은 크게 세 가지이다.

  1. XML 기반 구성 :
    XML 파일 내에 <bean> 태그를 사용하여 Bean을 선언하고 구성할 수 있다.

    이 방법은 초기 Spring에서 널리 사용되었다.

  2. 자바 기반 구성 :
    @Configuration 클래스 내부에서 @Bean 어노테이션을 사용하여 메서드로부터 Bean을 생성하고 구성할 수 있다.

@Configuration 이란?

@Configuration은 하나 이상의 @Bean 어노테이션을 사용하여 Spring Container에 Bean 정의를 제공하는 클래스에 붙인다.

@Configuration을 사용하면 해당 클래스가 Spring IoC Container를 위한 설정 클래스로 인식되며, 이 클래스 안에서 정의된 메서드들이 Spring Container에 의해 관리되는 Bean을 반환한다.

이 방법은 코드 내에서 직접적이고 명확한 구성을 제공한다.

  1. 컴포넌트 스캐닝 :
    @Component, @Service, @Repository, @Controller 등의 어노테이션을 '클래스'에 적용하여, Spring Boot의 자동 스캔 기능을 사용해 자동으로 Bean을 등록할 수 있다.

    @SpringBootApplication 어노테이션은 @ComponentScan을 포함하고 있어서, 어플리케이션의 메인 클래스가 위치한 패키지 및 그 하위 패키지를 자동으로 스캔한다.

2.2.5 Spring Boot에서 JavaConfig를 이용해 Book인스턴스 생성 및 사용 예제

Spring에서 여러 인스턴스를 생성하려면, 각 인스턴스에 대해 별도의 Bean으로 정의해야 한다. 이번 예제는 각각 다른 설정을 가진 Book 인스턴스 세 개를 생성하므로, 각각의 Book 객체에 대한 Bean을 BookConfig 클래스에 정의한다.

@Configuration이 붙은 클래스를 Java Config라고 한다. 클래스이지만, 일종의 설정인 것이다.

메소드의 이름은 Bean의 이름이 된다!!

public class Book {
	private String title;
    private int price;
    
    public Book(String title, int price) {
    	this.title = title;
        this.price = price;
    }
    
    //Getter
@Configuration
public class BookConfig {
	@Bean
    public Book book1() {
    	return new Book("Java", 10000);
    }
    
    @Bean
    public Book book2() {
    	return new Book("Spring Book", 12000);
    }
    
    @Bean
    public Book book3() {
    	return new Book("Microservices", 15000);
    }
}
@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    Book book1 = context.getBean("book1", Book.class);
    Book book2 = context.getBean("book2", Book.class);
    Book book3 = context.getBean("book3", Book.class);
    
    System.out.println("Book 1 Title: " + book1.getTitle() + ", Price: " + book1.getPrice());
    System.out.println("Book 2 Title: " + book2.getTitle() + ", Price: " + book2.getPrice());
    System.out.println("Book 3 Title: " + book3.getTitle() + ", Price: " + book3.getPrice());
  }
}

ApplicationContextgetBean() 메서드는 첫 번째 인자로 Bean의 이름을 사용하여 특정 Bean을 가져올 수 있다.


2.3 Bean Scope

  • DI Container는 Bean의 생존 기간도 관리한다.
  • Bean의 생존 기간을 빈 스코프(Bean Scope)라고 한다.
  • Spring Framework에서 사용 가능한 스코프
    • singleton
      • DI Container를 기동할 때 Bean 인스턴스 하나가 만들어지고, 이후 부터는 그 인스턴스를 공유하는 방식이다.
      • 기본 스코프이다.
    • prototype
      • DI Container에 Bean을 요청할 때마다 새로운 Bean 인스턴스가 만들어진다.
      • 멀티 스레드 환경에서 오동작이 발생하지 않아야 하는 Bean일 경우 사용한다.
    • request
      • HTTP 요청이 들어올 때마다 새로운 Bean 인스턴스가 만들어진다.
      • 웹 애플리케이션을 만들 때만 사용할 수 있다.
    • session
      • HTTP 세션이 만들어질 때마다 새로운 Bean 인스턴스가 만들어진다.
      • 웹 애플리케이션을 만들 때만 사용할 수 있다.

2.3.1 Scope 설정

자바 기반의 설정에서는 @Bean 어노테이션이 붙은 메소드에 @Scope 어노테이션을 추가해서 스코프를 명시한다.

Bean은 기본적으로 singleton이지만, prototype으로 할 경우 매번 새로운 인스턴스가 생긴다.

@Bean
@Scope("prototype")
MyService myService() {
	return new MyService();
}

다른 스코프의 빈 주입

  • 스코프 별 오래사는 순서
    • singleton > session > request
  • DI Container에 의해 주입된 Bean은 자신의 Scope랑 상관없이 주입받는 Bean의 Scope를 따르게 된다.
    • prototype scope bean을 singleton scope bean에 주입할 경우, prototype scope bean은 singleton bean이 살아있는 한 다시 만들 필요가 없기 때문에 결과적으로 singleton과 같은 수명을 살게 된다.

2.4 ❗의존성 주입 방법❗

  • 생성자 기반 의존성 주입 방식

    (constructor-based dependency injection)
  • 설정자 기반 의존성 주입 방식

    (setter-based dependency injection)
  • 필드 기반 의존성 주입 방식

    (field-based dependency)

2.4.1 생성자 기반 의존성 주입

생성자 기반 의존성 주입 방식은 각 클래스의 필수 의존성을 생성자를 통해 주입받는 방식이다.

아래의 예제는 PostServicePostRepository의 의존성을 주입하는 방법을 보여준다.

Post.java

public class Post {
    private Long id;
    private String title;
    private String content;
    
    // Getter, Setter, AllArgsConstructor
}

PostRepository.java

public class PostRepository {
  private List<Post> posts = new ArrayList<>();
  
  public void save(Post post) {
    posts.add(post);
  }
  
  public Optional<Post> findById(Long id) {
    return posts.stream().filter(post -> post.getId().equals(id)).findFirst();
  }
  
  public List<Post> findAll() {
  	return posts;
  }
}

PostService.java

아래의 PostService 클래스는 PostRepository에 대한 의존성을 생성자를 통해 주입받는다.

public class PostService {
  private final PostRepository postRepository;
  
  //생성자를 통해 Pository 의존성 주입
  public PostService(PostRepository postRepository) {
	  this.postRepository = postRepository;
  }
  
  public void publishPost(Post post) {
 	 postRepository.save(post);
  }
  
  public List<Post> getAllPosts() {
	  return postRepository.findAll();
  }
}

PostConfig.java

Spring의 설정 클래스에서 PostRepositoryPostService를 Bean으로 정의하고, 의존성을 주입한다.

//@Configuration : 설정 클래스
@Configuration
public class PostConfig {

  @Bean
  public PostRepository postRepository() {
  	return new PostRepository();
  }
  
  @Bean
  public PostService postService(PostRepository postRepository) {
  	return new PostService(postRepository);
  }
}

MainApplication.java

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    PostService postService = context.getBean(PostService.class);
    
    postService.publishPost(new Post(1L, "Hello World", "This is a spring boot application."));
    postService.getAllPosts().forEach(post -> {
    System.out.println("Post ID: " + post.getId() + ", Title: " + post.getTitle());
    });
  }
}

2.4.2 설정자 기반 의존성 주입

설정자 기반 의존성 주입 방식은 객체의 필수 의존성이 아닌, 선택적 의존성을 주입할 때 주로 사용된다.

아래의 예제는 EmailService 클래스가 NotificationService에 의존하는 방법을 보여준다.

설정자 주입은 필수적이지 않은 의존성에 유용하며, 객체 생성 후 언제든지 의존성을 변경할 수 있는 유연성을 제공한다.

NotificationService.java

알림을 처리하는 NotificationService 클래스

public class NotificationService {
  public void sendNotification(String message, String recipient) {
  // 실제 알림 전송 로직은 여기에 구현
  	System.out.println("Sending notification to " + recipient + ": " + message);
  }
}

EmailService.java

EmailService 클래스는 NotificationService를 사용하여 이메일 알림을 보낸다.

public class EmailService {
  private NotificationService notificationService;
  
  // 설정자(Setter) 메서드를 통한 의존성 주입
  public void setNotificationService(NotificationService notificationService) {
  	this.notificationService = notificationService;
  }
  
  public void sendEmail(String message, String recipient) {
    // 이메일 전송 로직은 실제로는 복잡할 수 있지만 여기서는 단순화
    notificationService.sendNotification(message, recipient);
  }
}

AppConfiguration.java

Spring 설정 클래스에서 EmailServiceNotificationService를 Bean으로 정의하고, 의존성을 주입한다.

@Configuration
public class AppConfiguration {

  @Bean
  public NotificationService notificationService() {
  	return new NotificationService();
  }
  
  @Bean
  public EmailService emailService() {
  	EmailService emailService = new EmailService();
    emailService.setNotificationService(notificationService()); // 설정자 주입
	
    return emailService;
  }
}

MainApplication.java

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    EmailService emailService = context.getBean(EmailService.class);
    
    emailService.sendEmail("Hello Spring!", "user@example.com");
  }
}

2.4.3 필드 기반 의존성 주입

필드 기반 의존성 주입 방식은 Spring Framework에서 어노테이션을 사용하여 클래스 필드에 직접 의존성을 주입하는 방법이다.

이 방식은 코드의 양을 줄이고 간결하게 의존성을 주입할 수 있으나, 단위 테스트를 어렵게 만들고, 클래스 외부에서 의존성을 설정할 수 없는 등의 당점이 있다.

여기서는 @Autowired 어노테이션을 사용한다.

UserRepository.java

@Repository
public class UserRepository {
  public String findUserNameById(Long userId) {
    // 실제 애플리케이션에서는 데이터베이스 조회 로직이 포함될 것입니다.
    // 여기서는 단순화를 위해 하드코딩된 값을 반환합니다.
    return "User" + userId;
  }
}

UserService.java

UserService 클래스는 UserRepository에 의존하여 사용자 데이터를 관리한다. 이 클래스에서는 필드 주입을 사용한다.

@Service
public class UserService {
  @Autowired
  private UserRepository userRepository;
  
  public String getUserName(Long userId) {
  	return userRepository.findUserNameById(userId);
  }
}

MainApplication.java

MainApplication 클래스에서는 Spring Boot 애플리케이션을 실행하고 UserService를 통해 사용자 이름을 조회합니다.

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    UserService userService = context.getBean(UserService.class);
    
    System.out.println(userService.getUserName(1L)); // "User1"을 출력
  }
}

필드 주입은 Spring Container가 객체를 생성한 후, 의존성을 클래스의 private 필드에 직접 주입한다.

필드 주입 방식은 매우 편리하긴 하나,
클래스가 스프링 컨테이너에 너무 강하게 의존하게 되며,
필드에 final 키워드를 사용할 수 없어 객체의 불변성을 보장할 수 없다는 단점이 있다.


2.4.4 오토 와이어링(autowiring)

  • 자바 기반 설정 방식에서 @Bean 메소드를 사용하거나, XML 기반 설정 방식에서 <bean> 요소를 사용하는 것처럼 명시적으로 빈을 정의하지 않고도 DI 컨테이너에 빈을 자동으로 주입하는 방식이다.

  • 오토와이어링 방식

    • 타입을 사용한 방식 (autowiring by type)
    • 이름을 사용한 방식 (autowiring by name)

2.4.5 타입을 사용한 오토와이어링 방식

  • @Autowired 어노테이션은 타입으로 오토와이어링을 하는 방식
  • Setter 인젝션, 생성자 인젝션, Field 인젝션에서 모두 활용 가능.
  • 타입으로 오토 와이어링을 할 때는 기본적은로 의존성 주입이 반드시 성공한다고 가정.
    • 주입할 타입에 해당하는 빈을 찾지 못하면 NoSuchBeanDefinitionException이 발생한다.
    • 필수 조건을 사용하고 싶지 않을 경우 `@Autowired(required = false)로 설정한다.
  • Injection을 할 수 있는 여러개의 Bean을 발견하게 될 경우에는 NoUnuqueBeanDefinitionException이 발생한다.
    • 여러개일 경우 @Qualifier 어노테이션으로 Bean 이름을 지정한 후 선택해서 사용해야 한다.
  • @Primary 어노테이션을 사용하면 @Qualifier를 사용하지 않았을 때, 우선적으로 선택할 Bean을 지정할 수 있다.
  • @Qualifier 역할을 하는 사용자 정의 어노테이션을 사용해서 표현할 수도 있다.
    • 사용자 정의 어노테이션에 @Qualifier를 설정한다.

2.4.6 이름으로 오토와이어링 하기

  • 빈의 이름이 필드명이나 프로퍼티명일치할 경우, 빈 이름으로 필드 인젝션을 할 수 있다.

  • JSR-250 사양을 지원하는 @Resource 어노테이션을 활용한다.

  • @Resource 어노테이션에는 name 속성을 생략할 수 있는데,
    필드 인젝션을 하는 경우에는 필드 이름과 같은 이름의 이 선택되고,
    Setter 인젝선을 하는 경우에는 프로퍼티 이름과 같은 이름의 이 선택된다.

  • 생성자 인젝션에서는 @Resource 어노테이션을 사용할 수 있다.

2.4.7 이름으로 오토 와이어링 예제

이 예제에서는 @Resurce 어노테이션을 사용하여 Field Injection을 수행한다.

ExampleService.java

public class ExampleService {
  public String getServiceName() {
 	 return "Example Service";
  }
}

AppConfiguration.java

설정 클래스에서는 ExampleService를 Bean으로 정의하며, @Resource를 통해 주입할 예정이다.

@Configuration
public class AppConfiguration {
  @Bean(name = "examService")
  public ExampleService exampleService() {
  	return new ExampleService();
  }
}

MainApplication.java

메인 클래스에서 @Resource를 사용하여 ExampleService 인스턴스를 주입받는다.

이때 필드 이름 exampleService는 설정된 빈의 이름과 일치한다.

@SpringBootApplication
public class MainApplication {

  @Resource(name = "examService")
  private ExampleService exampleService;
  
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    MainApplication app = context.getBean(MainApplication.class);
    System.out.println(app.exampleService.getServiceName()); // "Example Service" 출력
  }
}

중간에 언급된 JSR-250은 뭘까?

JSR-250은 자바 커뮤니티 프로세스(JCP)를 통해 정의된 자바 표준화요청(Java Specification Request) 중 하나이다. 자바 플랫폼에서 공통적으로 사용되는 어노테이션들을 정의하기 위한 목적으로 개발되었다.

JSR-250은 여러 기본적인 어노테이션을 포함하며, 이러한 어노테이션들은 리소스 주입, 생명주기 콜백 등의 기능을 제공하여, 개발자가 보다 편리하게 해준다.

주요 어노테이션

@PostConstruct

  • 객체의 생성과 의존성 주입이 완료된 후, 초기화 목적으로 실행할 메서드에 사용된다.
  • 해당 메서드는 객체가 생성된 후 단 한 번만 호출되며, 초기화 작업에 주로 사용된다.

@PreDestroy

  • 컨테이너에 의해 빈이 제거되기 전에 호출될 메서드에 사용된다.
  • 리소스의 해제, 정리 작업을 수행하는 데 사용될 수 있다.

@Resource

  • 리소스나 서비스에 대한 참조를 주입받기 위해 사용된다.
  • 이름, 타입 등을 기반으로 의존성을 주입하는 데 사용된다.
  • Ex. 데이터 소스, 세션, 기타 환경 자원 등을 주입받는 데 활용된다.


2.5 컴포넌트 스캔❗

2.5.1 컴포넌트 스캔(Component Scan)이란?

Java Config 파일에 @Bean으로 일일이 등록하는 것이 아니라, Bean이 될 수 있는 클래스들을 찾아서 자동으로 등록하게 하는 방법!

  • 클래스 로더(Class Loader)를 스캔하면서 특정 클래스를 찾은 다음, DI Container에 등록하는 방법을 말한다.
  • 별도의 설정이 없는 기본 설정에서는 다음과 같은 어노테이션이 붙은 클래스가 탐색 대상이 되고, 탐색된 컴포넌트는 DI Container에 등록된다.
    • @Component, @Controller, @Service, @Repository, @RestController
    • @Configuration
    • @ControllerAdvice
    • @ManagedBean(java.annotation.ManagedBean)
    • @Named(jabax.inject.Named)
  • 다음과 같은 방법으로 특정 패키지 이하를 스캔한다.
    • @ComponentScan(basePackages = "examples.di")
  • 기본 스캔 대상 외에도 추가로 다른 컴포넌트를 포함하고 싶을 경우, 필터를 적용한 컴포넌트 스캔을 할 수 있다. Spring Framework는 다음과 같은 필터를 제공한다.
    • 어노테이션을 활용할 필터(ANNOTATION)
    • 할당 가능한 타입을 활용한 필터(ASSIGNABLE_TYPE)
    • 정규 표현식 패턴을 활용한 필터(REGEX)
    • AspectJ 패턴을 활용한 필터(ASPECTJ)
@ComponentScan(basePackages = "examples.di" includeFilters = {
	@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
    								classes = {MyService.class })
})
  • 이본 스캔 대상에 필터를 적용해 특정 컴포넌트를 추가하는 것과 반대로 excludeFilters 속성을 이용해서 걸러낼 수도 있다.

2.5.2 Component Scan 예제

MessageService.java

@Service
public class MessageService {
	public String getMessage () {
    	return "Hello from MesageService";
    }
}

DataRepository.java

@Repository
public class DataRepository {
	public String getDate() {
    	return "Data From DataRepository";
    }
}

MainApplication.java

@SpringBootApplication
public class MainApplication {
  @Autowired
  private MessageService messageService;
  @Autowired
  private DataRepository dataRepository;
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    MainApplication app = context.getBean(MainApplication.class);
    System.out.println(app.messageService.getMessage());
    System.out.println(app.dataRepository.getData());
  }
}

2.6 Bean의 생명주기

2.6.1 빈의 생명주기란?

  • DI Container에서 관리되는 빈의 생명주기는 크게 다음의 세 가지 단계가 있다.
    1. 빈 초기화 단계(initialization)
    2. 빈 사용 단계(activation)
    3. 빈 종료 단계(destruction)

2.6.2 Bean 생명주기 예제

빈의 초기화와 소멸 과정에서 호출되는 메소드들을 직접 정의하고, JSR-250의 @PostConstruct@PreDestroy 어노테이션을 활용하여, Spring의 initMethoddestroyMethod를 사용한다.

MyBean.java

public class MyBean implements InitializingBean, DisposableBean {
    public MyBean() {
        System.out.println("My Bean 생성자 호출");
    }
    @PostConstruct
    public void postConstruct() {
        System.out.println("@PostConstruct 메소드 호출");
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean의 afterPropertiesSet 메소드 호출");
    }
    
    public void init() {
        System.out.println("사용자 정의 init 메소드 호출");
    }
    @PreDestroy
    public void preDestroy(){
        System.out.println("@PreDestroy 메소드 호출");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean의 destroy 메소드 호출");
    }
    
    public void cleanup() {
        System.out.println("사용자 정의 cleanup 메소드 호출");
    }
}

AppConfiguration.java

Spring 설정 클래스(@Configuration)에서 MyBean을 Bean으로 등록하고, 초기화(initMethod) 및 소멸(destroyMethod) 메소드를 지정한다.

@Configuration
public class AppConfiguration {
  @Bean(initMethod = "init", destroyMethod = "cleanup")
  public MyBean myBean() {
  	return new MyBean();
  }
}

MainApplication.java

어플리케이션 컨텍스트를 직접 종료하여 빈의 소멸 과정을 테스트한다.

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);
    // 애플리케이션 컨텍스트를 가져와서 사용하고 직접 종료시킵니다.
    context.close();
  }
}

이 애플리케이션을 실행하면, MyBean의 생성자, @PorstConstruct 메소드, InitializingBeanafterPropertiesSet 메소드, 사용자 정의 init 메소드가 순서대로 호출된다.

애플리케이션 종료 시, @PreDestroy 메소드, DisposableBeandestroy 메소드, 사용자 정의 cleanup 메소드가 호출된다.

호출 결과

MyBean 생성자 호출
@PostConstruct 메소드 호출
InitializingBean의 afterPropertiesSet 메소드 호출
사용자 정의 init 메소드 호출
2024-05-08T15:01:52.496+09:00 INFO 50535 ---
[demo] [ main] com.example.my.MainApplication
: Started MainApplication in 0.376 seconds (process running for 0.584)
@PreDestroy 메소드 호출
DisposableBean의 destroy 메소드 호출
사용자 정의 cleanup 메소드 호출

2.7 빈 설정 분할

2.7.1 빈 설정 분할이란?

DI Container에서 관리하는 빈이 많아지면 많아질수록 설정 내용도 많아져서 관리하기가 어려워진다.

이럴 때는 빈 설정 범위를 명확히 하고 가독성도 높이기 위해 모적에 맞게 문활하는 것이 좋다.

자바 기반의 설정 분할

@Import 어노테이션을 사용한다.

XML 기반 설정의 분할

<import> 요소를 사용한다.

2.7.2 빈 설정 분할 예제

Spring Boot에서 Java 기반 설정을 분할하고, @Improt 어노테이션을 사용하여 다른 설정 클래스를 임포트하는 방법을 설명하는 예제 코드이다.

이 예제에서는 두 개의 설정 클래스를 만들고, 하나의 클래스에서 다른 클래스를 import하여 관리를 분할하는 방법을 보여준다.

  • MainConfig는 ServiceConfig를 임포트(@Import)하고 있다. 이는 두 설정 클래스 간의 명시적인 연결을 나타낸다.

  • ServiceConfig는 MessageService 클래스의 인스턴스(@Bean)를 생성하며, 이는 의존성 주입을 나타낸다.

  • MainApplication은 MessageService를 사용하고 있다.

MessageService.java

MessageService 클래스는 간단한 메시지를 반환하는 서비스 로직을 포함한다.

public class MessageService {
	public String getMessage() {
    	return "Hello from MessageService";
    }
}

ServiceConfig.java

첫 번째 설정 클래스는 Service 관련 Bean을 정의한다.

@Configuration
public class ServiceConfig {

	@Bean
    public MessageService messageService() {
    	return new MessageService();
    }
}

MainConfig.java

두 번째 설정 클래스는 애플리케이션의 메인 설정을 담당하며, ServiceConfig를 임포트한다.

@Configuration
@Import(ServiceConfig.class)
public class MainConfig {
	//application의 Main configuration
    //추가적인 Bean 설정은 이곳에 작성될 수 있습니다.
    MainConfig() {
    	System.out.println("MainConfig created");
    }
}

MainApplication.java

Spring Boot 애플리케이션의 메인 클래스에서 설정된 Bean을 사용한다.

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    MessageService messageService = context.getBean(MessageService.class);
    System.out.println(messageService.getMessage()); // "Hello from MessageService" 출력
  }
}

@Import 어노테이션은 ServiceConfig 클래스를 MainConfig에 임포트하여, MainApplicationMessageService 빈을 사용할 수 있도록 해준다.

이 방식으로 애플리케이션의 설정을 여러 파일로 나누어 관리할 수 있으며, 각 설정 파일은 특정 기능이나 컴포넌트 그룹에 초점을 맞출 수 있다.

이는 대규모 프로젝트에서 설정의 복잡성을 줄이고, 관리를 용이하게 하는데 도움이 된다.


2.8 Spring Boot Profile

2.8.1 Profile별 설정 구성

Spring Framwork에서는 설정 파일특정 환겅이나 목적에 맞게 선택적으로 사용할 수 있도록 그룹화할 수 있으며, 이 기능을 Profile이라고 한다.

  • 자바 기반 설정 방식에서 Profile을 지정할 때는 @Profile 어노테이션을 사용한다.
    • @Profile("dev")
    • @Profile("dev", "real")
  • XML기반 성정에서는 Mbeans 요소의 profile 속성을 활용한다.

2.8.2 Profile 실행 예제

Spring Boot에서 환경별(프로파일별)로 다른 설정을 제공하는 방법을 보여주는 예제이다.
이 예제에서는 devprod 두 가지 프로파일을 만들고, 각 프로파일에 맞는 설정을 @Profile 어노테이션을 사용하여 적용한다.

DataService.java

간단한 데이터 서비스를 제공하는 클래스이다.

public class DataService {
  private String environment;
  
  public DataService(String environment) {
  	this.environment = environment;
  }
  
  public String getEnvironment() {
  	return environment;
  }
}

DevelopmentConfig.java

개발 환경(dev)용 설정을 포함하는 클래스이다.

@Configuration
@Profile("dev")
public class DevelopmentConfig {
  @Bean
  public DataService dataService() {
  	return new DataService("Development environment");
  }
}

ProductionConfig.java

생산 환경(prod)용 설정을 포함하는 클래스이다.

@Configuration
@Profile("prod")
public class ProductionConfig {
  @Bean
  public DataService dataService() {
  	return new DataService("Production environment");
  }
}

DefaultConfig.java

@Configuration
@Profile("default")
public class DefaultConfig {
  @Bean
  public DataService dataService() {
  	return new DataService("Default environment");
  }
}

MainApplication.java

Spring Boot의 메인 애플리케이션 클래스에는 DataService 빈을 요청하여 환경에 맞는 메시지를 출력한다.

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext context = SpringApplication.run(MainApplication.class, args);
    DataService dataService = context.getBean(DataService.class);
    System.out.println(dataService.getEnvironment()); // 환경에 따라 다른 결과 출력
  }
}

프로파일 설정 실행 방법

이 코드를 실행할 때 환경에 맞는 프로파일을 활성화하기 위해 다음과 같은 방법을 사용할 수 있다.

  1. 개발 환경에서 실행할 때 :
java -Dspring.profiles.active=dev -jar target/your-application.jar

이 명령은 개발 환경 설정을 활성화한다.

  1. 생산 환경에서 실행할 때 :
java -Dspring.profiles.active=prod -jar target/your-application.jar

이 명령은 생산 환경 설정을 활성화한다.

이 예제는 프로파일에 따라 다르게 설정된 빈을 로드하고, 프로파일이 활성화되면 해당 환경에 맞는 데이터 서비스를 제공하는 방식을 보여준다.

이를 통해 개발과 생산 환경을 명확하게 구분할 수 있다.

실행결과

Default encironment

IntelliJ에서 Profile을 prod로 지정하고 실행하면,

Production environment

가 출력된다.


📖오늘을 마치며

이번 글을 작성한 목적은 SpringBoot에 대해 공부하는 시간을 갖기 위함이다. SpringBoot를 배우는 기간에 하필 몸이 좋지 않아 응급실 갖다오고 병원 다니다보니 수업을 빠지는 날이 많았다. 그래서 이번 글은 강의를 들으며 궁금했던 것이나 내용에 대해 생각해본 부분들을 빼고 작성하였다.

이후 강의를 들으며 Bean의 개념을 따로 찾아보아도 이해되지 않는 부분이 많았는데 이번에 쭉 정리해보니 조금은 이해하는 데 도움이 되었다. velog의 장점은 자동으로 목차가 정리되어, 나중에 원하는 내용을 찾기 쉽다는 것이다. 이 글에 Spring Boot에 대해 개념부터 예제까지 꼼꼼히 정리하였으니, 추후 기억이 안 나는 부분이 있다면 이 글을 쉽게 참고할 수 있을듯 하다.

0개의 댓글