본문 바로가기
BackEnd/Java

Spring의 3대 요소 (IoC, DI, PSA, AOP)

by Ropung 2023. 10. 13.

Spring의 3대 요소 (IoC, DI, PSA, AOP) 대해서 알아보자

Spring은 엔터프라이즈 어플리케이션 개발을 편리하게 하기 위해 등장했다.
Spring은 봄이란 뜻으로 기존 EJB가 너무 무거워서 나온 가벼움을 강조하는 Spring 경량화 프레임워크인데…

요즘의 Spring은 너무 뚱뚱해졌다.

하여간 스프링은 객체지향과 DI라는 핵심 도구를 가지고 유연하고 확장성 있는 설계를 가능하게 만들어준다.
스프링은 순수 자바 오브젝트(POJO)를 이용해 특정 환경과 기술에 종속되지 않은 비즈니스 로직 구현이 목표인데, 이를 위한 Spring의 3대 요소인 IoC, AOP, PSA가 있다.


POJO(Plain Old Java Object)란?

간단히 해석을 하면, 오래된 방식의 간단한 자바 오브젝트라는 말로서 Java EE등의 중량 프레임워크들을 사용하게 되면서 해당 프레임워크에 종속된 무거운 객체를 만들게 된 것에 반발해서 사용되게 된 용어이다.

우리는 사람들이 자기네 시스템에 보통의 객체를 사용하는 것을 왜 그렇게 반대하는지 궁금하였는데, 간단한 객체는 폼 나는 명칭이 없기 때문에 그랬던 것이라고 결론지었다. 그래서 적당한 이름을 하나 만들어 붙였더니, 아 글쎄, 다들 좋아하더라고.     - 마틴 파울러 - 
  • 그냥 단순히 말하면 순수한 자바 객체를 말한다.
  • 만약 ORM 기술을 사용하고 싶다. 하지만 ORM기술의 경우 ORM을 지원하는 Hibernate라는 프레임워크를 사용해야한다. 이는 Hibernate에 의존하고 있다고 볼 수 있다.

위에 예시처럼 의존하는 순간, POJO라고 할 수 없다.


POJO를 지향해야 하는 이유?

  • 특정 기술과 환경에 종속되어 의존하게 된 자바 코드는 가독성이 떨어지고 유지보수에 어려움이 많았다.
  • 또한 특정 기술의 클래스를 상속받게 되거나, 직접 의존하게 되는 순간 확장하기가 매우 어려워 진다.

이 말의 뜻은 자바의 객체지향 설계의 장점을 잃어버리게 된다는 뜻.


특정 기술을 사용하고 싶다면? - PSA

  • Spring이 POJO를 유지하면서 Hibernate를 사용할 수 있는 이유는 스프링에서 정한 표준 인터페이스이기 떄문이다.
  • Spring 개발자들은 ORM이라는 기술을 사용하기 위해서 JPA라는 표준 인터페이스를 정해두었다.
  • ORM 프레임워크들은 JPA라는 표준 인터페이스 아래, 구현되어 실행된다. 이것이 엔터프라이즈 기술을 도입 하면서도 POJO를 유지하는 방법이다. 이를 Spring의 PSA라고 한다.

진정한 POJO?

  • 토비 센세는 스프링에서는 특정 기술규약과 환경에 종속되지 않은 순수한 객체를 POJO로 보는것이 아닌 진정한 POJO란 객체지향적인 원리에 충실한 것으로 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 객체가 진짜 POJO라고 말한다.
  • 자바의 객체지향적 설계의 장점을 포기하고 특정 기술과 환경을 사용하기에 급급했던 예전 시절의 고통을 잊지말자는 교훈이 있다고 한다.

정리하면

  • 특정 기술 에 종속된 객체는 POJO가 아니다.
  • 기술에 종속되지 않은 순수한 객체를 POJO라고 할 수 있다.

다시 돌아와서 3대 요소? Spring Triangle를 살펴보자

위에서 말했듯 3대 요소인 IoC, AOP, PSA를 Spring Triangle이라고 한다.

Application을 구성하는 객체(Bean)가 생성되고 동작하는 틀을 제공해주며, 애플리케이션 코드를 어떻게 작성해야 하는지에 대한 기준을 제공한다.
이를 일반적으로 프로그래밍 모델이라고 부르는데, 스프링에서는 크게 3가지 핵심 프로그래밍 모델을 지원한다.

Spring핵심3요소.png


제어의 역전 / 의존성 주입 (IoC / DI)

IoC / DI 는 객체의 생명주기와 의존 관계를 관리하기 위한 모델이다.

제어의 역전(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결 실행했다.
  • 구현 객체가 스스로 프로그램의 제어 흐름을 조종했던 것, 개발자 입장에선 자연스러운 흐름이라 볼 수 있다.
  • 프로그램의 제어 흐름을 클라이언트가 하는게 아닌 IoC 컨테이너가 통솔하게 된 것 이다.

단순히 정리하면 프로그램의 제어 흐름을 개발자가 제어하는것이 아닌,
외부(IoC 컨테이너)에서 관리로 인해 관리 당하는 것을 제어의 역전(IoC) 이라고 한다.


의존성 주입(Dependency Injection)

  • A -> B(인터페이스)라는 의존관계를 갖는 구조일 때, A가 B의 구현 클래스 인스턴스를 외부에서 주입받는 방식을 의존성 주입이라고 한다.
  • B는 외부에서 관리하기 떄문에 변경 되더라도 A의 영향을 주지 않기 때문에 B입장에선 유연한 확장이 가능하며, A입장에서는 변경 없이 재사용이 가능하다. 이를 SOLID원칙에서 OCP라고 한다.
  • B의 구현 클래스 인스턴스를 생성하고 주입하는 역할을 스프링의 IoC컨테이너가 하게 된다

즉, 정리하면

IoC 컨테이너(외부)에서 어떤것을 넣어주는것을 DI(의존성주입)라고 한다.
IoC는 제어의 역전이라는 개념 일 뿐, 실제로 인스턴스의 관리를 IoC 컨테이너에서 담당한다.

Spring은 IoC 컨테이너를 통해 제어의 역전을 제공하기 때문에 프레임워크라고 부른다.
Spring은 관계설정의 책임을 BeanFactory(ApplicationContext)에 위임함으로써 유연한 확장이 가능하다.

컨테이너에서 관리되는 객체는 Bean객체로 다음에 다루도록 한다.

[라이브러리 프레임워크의 구분]

  • 프레임워크가 내가 작성한 코드의 틀을 제어하고, 대신 실행한다.
  • 라이브러리는 내가 작성한 코드가 직접 제어의 흐름을 담당하는 것이다.

의존성 주입(DI) 3가지 방법

의존성을 주입한다는것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다.
이를 느슨한 결합이라고 한다.

  • 느슨한 결합은 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 떄문에 유연한 구조를 가진다.
    SOLID 원칙에서 O에 해당하는 Open Closed Principle을 지키기 위해서 디자인 패턴중 전략 패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략 패턴을 사용하게 된다.
  • 이러한 주입의 종류로는 Field Injection, Setter Injection, Constructor Injection방법이 있다.

결론부터 말하면 그냥 생성자 주입쓰면 된다.


필드 주입(Field Injection)

  • 대부분의 경우 @Autowird를 사용하는데 이것이 바로 필드 주입이다.
  • 스프링에서는 필드주입을 지양하라고 하고 있다. -> 생성자 주입을 지향(권장)
@Component public class SampleController {     @Autowired     private SampleService sampleService; } 
  • 일단 필드 주입에서는 SOLID의 단일 책임 원칙(SRP)을 위반한다.
  • 순환 참조의 위험도 있고, 객체가 바뀔수있으므로 불변하지 못한다는 단점도 있다.
  • 애초에 Spring만든 개발자들이 공식문서에 쓰지말라고 하는데 굳이 쓸필요가…?

수정자 주입(Setter Injection)

  • 선택적인 의존성을 사용할 때 유용하다.
  • 상황에 따라 의존성 주입이 가능하다.
  • 스프링 3.x 문서에서는 수정자 주입을 추천했었다.
@Component public class SampleController {     private SampleService sampleService;       @Autowired     public void setSampleService(SampleService sampleService) {         this.sampleService = sampleService;     } } 
  • 의존관계 주입은 런타임시에 할 수 있도록 낮은 결합도를 가지게 구현되었다.
  • 하지만 주입에 필요한 객체가 주입이 되지 않아도 얼마든지 객체를 생성할 수 있다는것이 문제다.

생성자 주입(Constructor Injection)

  • 아래 처럼 Constructor에 @Autowired Annotation을 붙여 의존성을 주입받을 수 있다.
@Component public class SampleService {     private SampleDAO sampleDAO;       @Autowired     public SampleService(SampleDAO sampleDAO) {         this.sampleDAO = sampleDAO;     } } 
  • 굳이 위 방법보다는 아래방법이 좋은 방법이다.
  • @RequiredArgs... 어노테이션이 기본 생성자부터 다만들어준다.
  • 무조건은 아니지만 상황에 따라 적절히 쓰는게 중요하다.
@RequiredArgsConstructor public class SampleService {     private final SampleDAO sampleDAO; }  

생성자 주입을 사용해야 하는 이유

  • 생성자를 사용하는 방법이 좋은 이유는 필수적으로 사용해야하는 의존성 없이는 인스턴스를 만들지 못하도록 강제할 수 있기 떄문이다.
  • null을 주입하지 않는 한 NPE도 발생하지 않는다.
  • final이므로 불변하다.
  • 순환 의존성 파악이 가능하다.

결론
더 좋은 디자인 패턴과 코드품질을 위해선 생성자 주입이 짱짱맨이다.


ApplicationContext VS Bean Factory

ApplicationContextBean Factory를 조금 더 확장한 개념이다.
스프링의 AOP기능을 쉽게 이용할 수 있도록 해주고, MessageResource관리, 이벤트 발행 및 응용 계층을 위한 컨텍스트를 제공한다.

따라서, Bean Factory라고 말할 때는 IoC의 기본 기능에 초점을 맞추고,
ApplicationContext라고 말하면 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC엔진이라는 의미가 강조된다.


IoC 컨테이너의 객체 관리

컨테이너는 객체(bean)을 어떻게 관리해야 할지에 대한 정보를 Configuration Metadata로부터 얻는다.
컨테이너에 객체를 올리는 방법은 @(어노테이션), XML 자바 코드가 있다.

  • XML 빈<태그>방법
 <beans>     <bean id="myService" class="com.acme.services.MyServiceImpl"/> </beans> 
  • 어노테이션 방법 - Config파일을 통한 Bean 등록
@Configuration public class AppConfig {       @Bean     public MyService myService() {         return new MyServiceImpl();     } } 
  • 어노테이션 방법 - Bean Class에 명시
@Repository public class EntityRepository { } 
  • 이 밖에 @RestController, @Service, @Reposiotry 등 다양한 방법 있지만 이 정도로만 정리해두도록 한다.

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

  • AOP는 프로그래밍 패러다임으로 공통관심사를 분리하여 모듈화를 이끌어 내는것이다.
  • 객체지향에서 모듈화의 단위를 클래스라고 본다면, AOP는 모듈화의 단위를 관점(Aspect)으로 본다.
  • 관점에 따라 객체와 타입에 걸친 공통관심사(횡단 관심사)의 모듈화를 가능하게 한다.

종단횡단관심사AOP.png

  • DI가 의존성(new)에 대한 주입이라면
  • AOP는 로직(code) 주입이라 볼 수 있다.

위에 사진처럼 로깅, 보안, 트랜잭션은 횡단 관심사로 볼 수 있다.
입금, 출금, 이체와 같은 비즈니스 로직은 종단 관심사의 영역으로 볼 수 있다.

정리하면
소스코드 상에서 각각의 관점을 핵심(종단관심사), 부가적인 관점(횡단관심사- 흩어진 관심사)으로 나누어 분리하는 것을 의미한다.


서비스 추상화(Portable Service Abstraction)

  • POJO로 개발된 코드는 특정 환경이나 구현방식에 종속적이지 않아야 한다.
  • 이를 위해 스프링이 제공하는 대표적인 기술이 바로 서비스 추상화 기술이다.
  • 위에서 POJO를 다루면서 JPA또한 PSA되었기 때문에 사용할 수 있다.

MVC에서의 서비스 추상화

  • 스프링 컨트롤러에서 매서드를 작성할 때 특정 URL의 HTTP 매서드 요청을 처리하기 위해 @GetMapping, @PostMapping과 같은 어노테이션을 사용하곤 한다.
  • 이 때 서블릿 애플리케이션을 만들고 있음에도 불구하고 다음과 같은 서블릿 코드를 작성하지 않는다.
public class UserSignupServlet extends HttpServlet {     @Override   protected void doPost(       HttpServletRequest req,       HttpServletResponse resp   ) throws ServletException, IOException {     super.doPost(req, resp);     // ...   }   } 
  • 대신 어노테이션의 사용만으로 위의 서블릿 기반의 코드를 동작하게 할 수 있다.
@PostMapping("signup") @ResponseStatus(HttpStatus.CREATED) public void signup( @Valid @RequestBody UserSignupRequest request){ 	userService.signup(request.username(), request.profileImgUrl()); } 
  • 이렇게 스프링이 서비스 추상화를 제공하는 이유는 위와 같은 편의성을 제공하기 위함과 기술에 종속적이지 않은 코드를 작성할 수 있도록 도와주기 위함도 있다.
  • 위에 코드를 변경하지 않고 spring-boot-starter-web의존성을 spring-boot-starter-webflux의존성으로 변경하더라도 코드의 변경 없이 정상적으로 실행된다.

트랜잭션

  • 트랜잭션도 마찬가지고 내부적으로는 트랜잭션 코드가 추상화되어 숨겨져 있다.

DB접근

  • DB에 접근하는 방법중 jdbc를 통해 접근하거나 ORM을 이용하고자 한다면 JPA를 통해서 접근할 수 있다.

정리하면

이렇게 하나의 추상화 계층을 사용하여 어떤 기술을 내부에 숨기고 여러 서비스를 묶어두어 개발자에게 편의성을 제공해주는 것이 서비스 추상화라고 한다.


마무리하며

Spring에 대해 좀 더 깊이 탐구할 수 있게 되어 명확하게 알 수 있었다.

참고자료

POJO
스프링의 3대 요소
스프링 PSA
스프링 3대요소
스프링 3대요소 Triangle