이끌든지 따르든지 비키든지

Framework/Spring

[Spring] 스프링 프레임워크 핵심 원리

SeongHo5 2023. 11. 27. 11:12


Spring Framework는 2003년에 처음 출시되었다.

처음에는 복잡한 엔터프라이즈 자바 개발을 단순화하기 위한 목적으로 개발되었으며,

특히 엔터프라이즈 자바 빈즈(EJB)의 복잡성과 무거운 구조를 대체할 수 있는 가벼운 대안으로 등장했다.

 


주요 특징

 

1. 관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)

- 관점을 기준으로 횡단 관심사를 분리해 모듈화 할 수 있다.

 

2. 제어 역전(IoC, Inversion of Control)

- 객체 생성에 대한 제어를 프레임워크에 위임해, 스프링이 객체의 생명 주기를 관리한다.

 

3. 의존성 주입(DI, Dependency Injection)

- 객체 간 의존성을 외부에서 주입해 직접적으로 의존성을 관리하지 않아도 된다.

 

 


 

관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)

 

애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나눌 수 있다.


핵심 기능은 해당 객체가 제공하는 고유의 기능이다. 예를 들어서 OrderService의 핵심 기능은 주문 로직이다.

이런 기능은 해당 객체의 주 역할과 직접적으로 연관된다.


부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다. 예를 들어서 로그 추적 로직, 트랜잭션 기능이
있다. 이러한 부가 기능은 단독으로 사용되지 않고, 핵심 기능과 함께 사용된다. 

로그 추적 기능은 어떤 핵심 기능이 호출되었는지 로그를 남기기 위해 사용한다. 

그러니까 부가 기능은 이름 그대로 핵심 기능을 보조하기 위해 존재한다.

 

그런데 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다.

예를 들어서 모든 애플리케이션 호출을 로깅해야 한다면?

이러한 부가 기능은 횡단 관심사(cross-cutting concerns)가 된다. 

 

횡단 관심사 예시

 

이렇게, 애플리케이션을 바라보는 관점을 각각의 기능(회원, 커뮤니티 등)에서 횡단 관심사(로깅, 트랜잭션 등) 관점으로 달리 보는 프로그래밍 방식을 관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)이라 한다.

 

스프링 AOP는 프록시 패턴을 사용하여 관점을 적용하는데, 자세한 내용은 다음 글에서 알아보자

 

( ※ AOP는 객체 지향 프로그래밍(OOP)를 대체하는 패러다임이 아닌, 횡단 관심사의 효율적인 처리를 위한 보조 개념임을 알아두자 )

 


 

제어 역전(IoC, Inversion of Control)

 

전통적인 프로그래밍에서는 애플리케이션의 흐름을 개발자가 작성한 코드가 제어한다. 예를 들어, 객체의 생성과 종속 객체의 생성, 메소드 호출 등이 명시적으로 개발자의 코드에 의해 결정된다.

 

하지만 IoC에서는 이러한 제어가 프레임워크에 의해 이루어진다. 즉, 개발자는 '무엇을' 할 것인지만 정의하고, '어떻게' 할 것인지는 프레임워크가 결정하는 것이다.

 

IoC의 장점


낮은 결합도(Low Coupling):

  • 객체 간의 의존성이 줄어들어, 코드 변경 시 영향을 미치는 범위가 감소한다.


유연성 및 확장성 향상:

  • 의존성이 외부에서 관리되므로, 컴포넌트를 쉽게 교체하거나 업데이트할 수 있다.


유지보수 용이:

  • 코드가 간결해지고, 의존성 관리가 용이해져 유지보수가 편리해진다.


테스트 용이성:

  • 의존성 주입을 통해 모의 객체(Mock Objects)를 쉽게 사용할 수 있어, 단위 테스트 작성이 쉽다.

 

IoC 컨테이너

 

스프링에서 제어 역전을 담당하는 요소를 IoC 컨테이너 또는 애플리케이션 컨텍스트(Application Context)라 부르는데, IoC 컨테이너가 객체의 생성, 구성 등 생명주기 관리 전 과정을 책임지고 담당하기 때문에 개발자는 비즈니스 로직에 더 집중할 수 있고, 코드의 유지보수와 확장성이 향상된다.

 


 

의존성 주입(DI, Dependency Injection)

의존성 주입(Dependency Injection, DI)은 객체 간의 의존성을 외부에서 제공하는 방식이다.

이 방식은 객체의 생성과 사용에 필요한 의존성을 객체 스스로가 아니라 외부의 컨테이너(스프링의 경우 Spring IoC 컨테이너)가 관리하도록 한다.

의존성 주입은 소프트웨어 설계의 결합도를 낮추고, 유연성 및 재사용성을 높여주며, 테스트를 용이하게 한다.

 

 

의존성 주입의 구현

 

1. 필드 주입 방식

@Component
public class OrderService {

    // 필드 주입
    @Autowired
    private CustomerRepository customerRepository;

    // OrderService의 메소드...
}

 

클래스 내부 필드에 @Autowired 어노테이션을 통해 의존성을 주입하는 방식이다.

 

 

2. 생성자 주입 방식

@Component
public class OrderService {

    private final CustomerRepository customerRepository;

    // 생성자를 통해 의존성 주입
    public OrderService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    // OrderService의 메소드...
}

 

 

또는 lombok의 @RequiredArgsConstructor를 활용한 생성자 주입도 가능하다.

@RequiredArgsConstructor를 사용하면 개발자가 직접 생성자를 작성하는 번거로움 없이 안전하고 효율적으로 의존성을 관리할 수 있다.

 

@Component
@RequiredArgsConstructor
public class OrderService {

    private final CustomerRepository customerRepository;

    // OrderService의 메소드...
}

 


 

어떤 방식을 사용해야할까?

 

두 가지 방식이 있지만, 주로 권장되는 방식은 생성자 주입 방식인데, 아래 내용을 통해 이유를 알아보자.

 

 

생성자 주입과 필드 주입 방식의 비교


초기화의 완전성 (Initialization Completeness):

  • 생성자 주입: 클래스의 필수 의존성이 모두 생성자를 통해 주입되므로, 객체가 생성될 때 완전한 상태로 초기화된다.
  • 필드 주입: 필드에 직접 의존성을 주입하기 때문에, 클래스의 인스턴스가 만들어진 후 언제든지 필드가 변경될 수 있어 초기화의 완전성이 보장되지 않는다.


불변성 (Immutability):

  • 생성자 주입: final 키워드를 사용하여 의존성을 불변 상태로 만들 수 있어 생성 후 변경되지 않음을 보장할 수 있다.
  • 필드 주입: final 키워드를 사용할 수 없으므로, 불변성을 보장할 수 없다.

 

테스트 용이성 (Testability):

  • 생성자 주입: 테스트 시에 필요한 의존성만을 주입하여 객체를 쉽게 생성할 수 있다.
  • 필드 주입: 별도의 구성 없이는 필드에 모의 객체(Mock)를 주입하기 어려워, 테스트 구성이 복잡해진다.

 


 


정리 - 생성자 주입 권장 이유

  • 명확한 의존성: 모든 의존성이 생성자를 통해 명확하게 제공되므로, 클래스가 필요로 하는 의존성이 무엇인지 쉽게 파악할 수 있다.
  • 객체의 안전한 초기화: 객체가 완전한 상태로 생성되어 초기화가 보장된다.
  • 불변성 보장: 객체가 한 번 생성된 후에는 변경되지 않아 상태 관리가 쉽고, 버그 발생 가능성이 줄어듦
  • 단위 테스트 용이: 모의 객체를 주입하기 쉬워 테스트 코드 작성이 용이하다.

 


 

참고:

인프런 '스프링 핵심 원리 - 고급 편' , 김영한

&

https://velog.io/@witwint/Spring-AOPAspect-Oriented-Programming