Spring의 3대 요소에 알아보기 전에 Spring의 탄생 이유와 뿌리를 알아보고 시작하자.
특정 기술과 환경(EJB = Enterprise JavaBeans)에 종속되어 의존하게 된 자바 코드는 유지보수가 어렵고, 확장성이 떨어지고, 가독성이 떨어진다. 이는 객체지향 언어인 자바의 장점들이 사라지게 되는 것이므로 POJO라는 개념이 등장하게 되었다.
Getter, Setter로 구성된 가장 순수한 형태의 자바 객체를 POJO라 한다.
Spring은 POJO를 사용하는 장점과 EJB에서 제공하는 엔터프라이즈 서비스와 기술을 그대로 사용할 수 있도록 해주는 대표적인 POJO 프레임워크이다.
Spring Framework의 주축이 되는 3가지 개념을 말한다.
- IoC(Inversion of Control)
- AOP(Aspect Oriented Programming)
- PSA(Portable Service Abstraction)
IoC는 객체의 생성부터 생명주기 관리까지 모든 객체에 대한 제어권을 개발자가 아닌 프레임워크인 Spring이 가져가게 된다는 것을 말한다. 컴포넌트 의존관계 설정(Component dependency resolution), 설정(Configuration), 생명주기(LifeCycle)을 해결하기 위한 디자인 패턴이다.
IoC컨테이너(=스프링 컨테이너)는 객체를 생성하고 의존성을 관리하고 책임지는 컨테이너 이다. 인스턴스 생성부터 소멸까지 인스턴스의 생명주기를 개발자가 아닌 이 컨테이너가 대신 해준다. 그로 인해 객체 관리 주체가 개발자가 아닌 스프링 프레임워크(제어의 반전)가 되기 때문에 개발자는 로직에만 더 집중할 수 있게 된다.
- IoC컨테이너는 객체의 생성을 책임지고, 의존성을 관리한다.
- POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다.
- 개발자들이 직접 POJO를 생서할 수 있지만 컨테이너에게 맡긴다.
- 객체 생성 코드가 없으므로 TDD가 용이하다.
IoC컨테이너의 인스턴스가 어플리케이션을 통틀어 단 하나만 있다는 것을 보장하기 위해 싱글톤패턴을 사용한다. Bean이 싱글톤패턴으로 정의되면 IoC컨테이너가 해당 Bean에 대한 단 하나의 인스턴스를 생성하고 생명주기를 관리한다. 이렇게 함으로써 공유되는 자원을 관리하기 쉬워지고 어플리케이션 내에서 일관성을 유지하기 쉬워진다.
DL(Dependency LookUp)과 DI(Dependency Injection)이 있지만 실질적으로 쓰이는 DI에 대해서만 알아보고자 한다. (DL은 컨테이너의 종속성이 증가하기 때문에 잘 쓰이지 않는다)
의존성을 주입하려면 먼저 객체들을 스프링 컨테이너에 등록하여 "빈(Bean)"으로 만들어야 한다.
빈(Bean)이란 IoC컨테이너가 관리하는 객체이다.
방법으로는 직접 자바 코드로 등록하는 방법과 클래스 선언부에 @Component를 사용하는 것이다.
필자는 @Component만 써봤기 때문에 이 방법에 대해서만 서술하고자 한다.
Bean이 등록 되었으면 이제 의존성 주입을 통해 사용하면 된다.
- Setter Injection(수정자 주입) : Setter 메소드를 만들고 이를 통해 의존성 주입
- Field Injection(필드 주입)
- @Autowired사용하여 의존성 주입
- 편리하긴 하나 SOLID중 SRP위반, 테스트가 불편함, final선언 불가로 인한 불변성 보장이 되지 않음, 순환 의존성 등의 문제로 인해 권장되지 않는 방법이다.- ⭐︎ Constructor Injection(생성자 주입)
- Spring에서 권장하는 방법이다
- 필드 주입의 대부분의 문제들이 보완되는 의존성 주입 방법이다.
위에 말한 것처럼 Bean은 컨테이너에 의해 관리되고, 컨테이를 빈팩토리(BeanFactory)라고 부른다.
- 객체의 생성과 객체 사이의 런타임 관계를 DI 관점에서 볼 때 컨테이너를 BeanFactory라고 한다.
- BeanFactory에 여러가지 기능을 추가한 어플리케이션컨텍스트(Application Context)가 있다.
- BeanFactory계열의 여러 인터페이스가 있다.
- 이 인터페이스들을 구현한 클래스는 단순히 컨테이너에서 객체를 생성하고 DI를 처리하는 기능만 한다.
- Bean 등록, 생성, 조회, 반환에 대한 관리를 한다.
- 팩토리 디자인 패턴을 구현한 것으로 BeanFactory는 빈을 생성하고 분배하는 책임을 가진다.
- Bean을 조회할 수 있는 getBean() 메소드가 정의되어 있다.
- 기본적인 관리 기능은 BeanFactory와 동일하다.
- 스프링의 각종 부가 기능을 추가로 제공한다.
- 국제화가 지원되는 텍스트 메시지를 관리 해준다.
- 이미지같은 파일 자원을 로드할 수 있는 포괄적인 방법을 제공한다.
- 리스너로 등록된 빈에게 이벤트 발생을 알려준다.
⭐︎ Application Context가 더 편리하기 때문에 이를 사용하는 것이 더 좋다.
AOP는 프로그래밍 패러다임으로서 OOP의 관심사의 분리(기능의 분리)문제를 해결하기 위해 만들어졌다.
OOP를 더 OOP답게 쓸 수 있게 해주는 것이 AOP이며, AOP를 적용해 POJO개발을 더 쉽게 만들 수 있다.
AOP는 기능을 핵심 관심 사항(Core Concern)과 공통 관심 사항(흩어져 있는 관심사,Cross Cutting Concern)으로 분리시키고 각각을 모듈화 하는 것을 의미한다.
- 서비스 로직을 포함하는 기능을 핵심 기능(Core Concern)
- Core Concern = 비지니스 로직- 핵심 기능을 도와주는 부가적인 기능을 부가 기능(Cross Cutting Concern)이라고 부른다.
- Cross Cutting Concern = Core Concern을 실행하기 위한 DB연결, 로깅, 파일 입출력 등- OOP를 적용해도 핵심 기능에서 부가 기능을 쉽게 분리된 모듈로 작성하기 어려운 문제점을 AOP가 해결해준다.
- AOP는 부가 기능을 Aspect(관점)로 정의하여, 핵심 기능에서 부가 기능을 분리함으로써 핵심 기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있게 도와주는 개념이다.
⚡︎Proxy Pattern in AOP
: 프록시 패턴이란 다른 객체에 대한 접근을 제어하는 Wrapper객체를 만드는데 사용되는 디자인 패턴이다. 이를 이용해 어플리케이션 코드에서 Cross Cutting Concern을 분리할 수 있어 모듈화 및 유지보수가 보다 용이하다.
프록시 객체는 타켓 빈과 동일한 인터페이스를 구현하여 동적으로 생성된 클래스의 인스턴스이다. 이 프록시 객체는 타켓 빈에서 메서드 호출 전, 후에 호출되는 Advice가 포함되어 있다.
- Spring은 타겟 객체에 대한 프록시를 만들어서 제공한다.
- 타켓의 Wrapper프록시는 RunTime에 생성된다.
- 프록시는 Advice를 타겟 객체에 적용하면서 생성되는 객체이다.
- 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위함이다.
필자가 사용했었던 것들을 예시로 써보고자 한다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception ex) {
// log the exception
// return a view or redirect to an error page
}
}
@ExceptionHandler를 사용하여 동일한 클래스, @ControllerAdvice가 있는 클래스들의 메소드가 던진 예외를 처리할 수 있다. 각각의 메소드에서 처리해야 했던 예외들(Cross Cutting Concern)을 GlobalExceptionHandler 클래스 한 곳에 모아 모듈화하여 어플리케이션의 비지니스 로직(Core Concern)과 분리시켜 결합도를 낮췄다(Loose Coupling).
@Service
public class MyService {
@Transactional
public void saveData(Data data) {
// insert data into the database
}
}
사건의 순서대로 정리해보자.
- saveData메소드는 @Transactional로 인해 반드시 트랜잭션에 포함되어야 한다는 것을 알 수 있다.
- 해당 메소드가 호출되면 Spring은 @Service를 통해 빈을 생성하고 해당 빈에 대한 프록시 객체를 생성한다.
- 해당 프록시 객체에는 트랜잭션 관리 Advice가 포함된다.
- 해당 Advice는 메소드 호출 전, 후 호출되고 트랜잭션이 성공하면 commit, 예외가 발생하면 rollback하게 된다.
트랜잭션이란 여러 작업을 하나의 단위로 묶어 실행하는 것이며 이를 Spring에서 AOP를 통해 구현할 수 있다. AOP형식의 Advice를 사용하여 트랜잭션을 시작하고, commit되거나 rollback되는 경계를 정의할 수 있다. 이렇게 트랜잭션 로직(Cross Cutting Concern)을 비지니스 로직(Core Concern)으로 부터 분리하여 모듈화하고 유지보수관리가 용이하게 만들었다(Maintainable하게 만들었다).
PSA란 환경의 변화와 관계없이 일관된 방식의 기술로의 접근 환경을 제공하는 추상화 구조를 말한다.
POJO원칙을 철저히 따른 Spring의 기능으로 Spring에서 동작할 수 있는 Library들은 POJO원칙을 지키게끔 PSA형태의 추상화가 되어있음을 의미한다.
추상화 계층을 사용하여 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공해주는 것이 서비스 추상화(Service Abstraction)이다.
하나의 추상화로 여러 서비스를 묶어둔 것을 Spring에서 Portable Service Abstraction이라고 한다.
PSA는 다른 말로 하면 ORM(Object Relation Mapping), 잘 만든 인터페이스라고 할 수 있다.
그렇다면 PSA의 예시에 대해 알아보자.
원래 Servlet을 사용하려면 HttpServlet을 상속받은 클래스를 만들고 Get, Post 등에 대한 메소드를 오버라이딩하여 사용해야 한다.
@Controller에 대해 생각해보자. 클래스 선언부에 이 어노테이션을 사용하면 클래스 내부에서 메소드에 @GetMapping, @PostMapping등을 사용할 수 있고 클래스 선언부에 @RequestMapping을 사용할 수 있다. 이것이 "잘 만든 인터페이스"의 예시이다. 이러한 편의성 제공이 서비스 추상화의 목적이다. 우리는 @Controller, @GetMapping같은 기능들의 뒷단의 복잡한 코드에 대해서 깊게 고려하지 않아도 되고 이런 기술들을 기반으로 하여 기존 코드를 거의 변경하지 않아도 된다.
또한, 실행 서버를 Tomcat이 아닌 netty로 변경하고 싶다면 프로젝트 설정에 spring-boot-starter-web 의존성 대신 spring-boot-starter-webflux 의존성을 받도록 하면 된다.
여러가지 복잡하고 잘 만들어진 인터페이스들, 의존성같은 기술들을 기반으로 하여 Spring은 개발자에게 편리한 환경을 제공한다.
JDBC를 사용해서 DB접근하고 작업, 트랜잭션을 구현할 때는 연결, 작업할 쿼리문, commit, rollback하는 코드를 전부 작성해야 했다. 그러나 Spring에선 메소드에 @Transactional을 통해 트랜잭션 처리를 간단하게 할 수 있다.
Spring에서는 ORM이라는 기술을 사용하기 위해서 JPA라는 표준 인터페이스가 정의되어 있고 Hibernate같은 여러 ORM 프레임워크들은 이 JPA라는 표준 인터페이스 아래에서 구현되어 실행된다.
⭐︎위에서 말한 것 처럼 Hibernate는 POJO기반의 Persistence 프레임워크이다.
개인적으로 가장 느끼는 부분이 많았던 것은 @Service와 @Transactional이다. 미니프로젝트 같은 것을 진행하면서 수도 없이 많이 사용했지만 이 두 어노테이션이 Spring의 개념과 이렇게 깊이 관련있다는 것은 새롭게 느껴졌다.
Reference
https://dev-coco.tistory.com/83
https://dev-coco.tistory.com/82
https://dev-coco.tistory.com/69