스프링 프레임워크는 Java EE (Java Enterprise Edition)의 개발 복잡성과 이 에디션에서 제공하는 EJB(Enterprise Java Beans) 성능 및 구조적 한계를 개선하고자 탄생한 프레임워크에요.
Java EE (Jakarta EE)
Java EE는 기업 환경의 대규모 애플리케이션 개발을 위한 플랫폼으로 Java SE (Java Standard Edition)을 확장한 것이에요. 현재 Java 기반으로하는 모든 서버 프레임워크 또는 라이브러리 들은 Java EE를 기반으로 하고 있어요.
최초에는 Java EE의 관리 및 배포를 오라클에서 했었으나 지금은 이클립스 재단으로 이관되면서 Jakarta EE로 명칭이 바뀌었고 패키지도javax에서jakarta로 바뀌었어요. 더 자세히 알고 싶은 경우 이 포스트를 추천할게요 Java EE에서 Jakarta EE로의 전환
스프링 프레임워크의 주요 특징 3가지는 다음과 같아요.
- POJO 기반 아키텍처
- IoC/DI (Inversion of Control/Dependency Injection)
- AOP (Aspect-Oriented Programming)
스프링 프레임워크는 POJO (Plain Old Java Object) 철학을 가지고 있어요. 여기서 POJO란 특정 프레임워크나 기술에 종속되지 않은 순수한 Java 객체를 의미해요. 간단한 스프링 예시를 볼게요.
// POJO 클래스
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
...getter
...setter
}
// Spring Bean 설정
@Configuration
public class AppConfig {
@Bean
public User user() {
return new User("John Doe", 30);
}
}
// Spring을 사용하여 POJO Bean 사용
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
User user = context.getBean(User.class);
System.out.println("User name: " + user.getName());
System.out.println("User age: " + user.getAge());
}
}
위 예시에서 POJO는 User에요. 이 순수한 자바 객체를 @Bean 어노테이션을 통해 스프링 빈으로 등록해서 사용하고 있어요. 이런 방식은 높은 이식성, 낮은 의존성, 테스트 용이성, 유연성 등 많은 장점을 지니며 가장 큰 이점은 개발 복잡도가 낮다는 것이에요.
POJO 철학에 반대되는 경우는 특정 클래스나 인터페이스를 상속 받아야만 하는 경우를 생각하면 돼요.
// POJO가 아닌 예시: 특정 프레임워크의 클래스를 상속받거나 의존하는 경우
public class MyServlet extends HttpServlet { // Servlet API에 종속
// ...
}
위 코드는 Servlet API를 구현하기 위해 HttpServlet 클래스를 상속받아 Servlet에 종속적인 구조에요.
IoC (제어의 역전)와 DI (의존성 주입)은 스프링 프레임워크의 핵심 개념 중 하나에요. IoC는 객체의 생성 및 관리에 대한 제어 역할을 스프링 프레임워크에게 일임하는 개념이이에요. 간단한 예시를 볼게요.
// 일반적인 프로그램
public class Car {
private Engine engine = new Engine(); // Car가 Engine을 직접 생성
public void run() {
engine.start();
}
}
// IoC가 적용된 프로그램
public class Car {
private final Engine engine;
public Car(Engine engine) { // Car는 Engine을 외부에서 주입받음
this.engine = engine;
}
public void run() {
engine.start();
}
}
일반적인 프로그램에서는 Engine이라는 코드에서 직접 생성하고 관리하는데 반해 스프링 프레임워크에서는 Engine 객체를 스프링 프레임워크에서 생성해 주입해요 (Engine 객체를 생성하는 코드가 없지만 스프링 프레임워크에서 생성했어요)
DI (의존성 주입)은 객체간 의존 관계를 설정하고 주입하는 걸 의미하는데, 위의 예시 처럼 생성자를 통한 방식이 있고 세터(Setter) 주입과 필드(Field) 주입 방식이 있어요. 이 부분은 다음 장에서 더 자세히 설명할게요.
이런 구조는 코드의 재사용성을 높이고 객체 간 의존성을 명확히할 수 있는 장점이 있어요.
AOP는 애플리케이션의 핵심 로직과 부가적인 기능을 분리하여 모듈화하는 개념이에요. 일반적인 사용 케이스는 로깅 (Logging)과 DB 히스토리 삽입이 있어요. AOP를 사용한 로깅 예시를 볼게요.
// 핵심 로직을 담당하는 클래스
public class OrderService {
public void createOrder(Order order) {
System.out.println("주문을 생성합니다.");
// ... 주문 생성 로직 ...
}
public void cancelOrder(Order order) {
System.out.println("주문을 취소합니다.");
// ... 주문 취소 로직 ...
}
}
// 로깅 Aspect
public class LoggingAspect {
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("메서드 " + methodName + " 실행 전");
}
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("메서드 " + methodName + " 실행 후 (결과: " + result + ")");
}
public void afterThrowing(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("메서드 " + methodName + " 예외 발생: " + e.getMessage());
}
}
OrderService 는 주문 생성 및 취소와 같은 핵심 로직을 담당하는 클래스이며 LoggingAspect는 기능 실행 전후와 예외 상황 등에 대한 로그를 남기는 역할을 해요.
이와 같이 AOP는 핵심 기능과 부가적인 기능을 분리할 때 주로 사용돼요. AOP를 사용하면 코드 재사용성이 높아지고 코드 복잡도가 낮아지는 장점이 있어요.
스프링 부트는 개발자들의 컨테이너리스 (Containerless) 요구 사항으로 시작되었어요. 여기서 컨테이너는 톰캣(Tomcat)과 같은 WAS (Web Application Server)를 의미해요.
스프링 부트의 계기는 컨테이너리스에 대한 요구 사항이었지만 프로젝트가 진행되면서 기존 스프링 프레임워크의 단점을 보완하는 방향으로 진행되었어요. 스프링 프레임워크는 POJO 구조와 프레임워크의 유연함을 중요하게 생각해서 개발자에게 많은 권한과 역할을 부여했는데요. 이는 개발자로 하여금 초기 개발 환경 구성 및 설정에 많은 시간을 들이게 했습니다.
이는 스프링 프레임워크를 사용하는데 큰 진입 장벽이 되었고 Java EE의 단점이었던 개발 복잡성이 여전히 남아 있게 되었어요.
스프링 부트는 스프링 프레임워크의 디자인 철학이나 아키텍처를 그대로 계승하며 몇 가지 핵심 기능들을 가지고 있어요.
- 자동 구성 (Auto Configuration)
- 스타터 의존성 (Starter Dependency)
- 내장형 서버 (Embedded Servers)
스프링 부트의 자동 구성은 개발자가 직접 설정하지 않아도 스프링 빈(Bean)들을 자동으로 생성하고 등록해주는 기능이에요. 애플리케이션에 필요한 의존성을 추가하면 개발자가 신경쓰지 않아도 스프링 부트가 필요한 구성과 설정들을 알아서 해줘요.
자주 사용되는 자동 구성 예시들은 다음과 같아요.
- spring-boot-starter-jdbc:
DataSource나JdbcTemplate에 필요한 빈을 자동으로 구성해요.- spring-boot-starter-web:
DispatcherServlet,ViewResolver,HandlerMapping에 필요한 빈을 자동으로 구성해요.- spring-boot-starter-security:
UserDetailsService,PasswordEncoder에 필요한 빈을 자동으로 구성해요.
스프링 부트는 프로젝트 개발에 필요한 스타터 의존성들을 제공해요. 스타터 의존성을 통해 필요한 라이브러리들을 일일이 추가할 필요 없이 한 번에 쉽게 추가할 수 있어요.
위에 언급한 각각의 스타터 의존성들이 포함하고 있는 라이브러리 들은 다음과 같아요.
- spring-boot-starter-jdbc:
Spring Data JPA,Hibernate등- spring-boot-starter-web:
Spring MVC,Tomcat,Jackson등- spring-boot-starter-security:
Spring Security등
스타터 의존성을 사용하게 되면 관련 라이브러리들을 손 쉽게 추가할 수도 있지만 각 라이브러리별 버전 호환성을 고려하지 않아도 되는 것도 큰 장점이에요.
스프링 부트는 컨테이너리스 애플리케이션 개발을 지원하기 위해 내장형 서버를 포함하고 있어요. 스프링 부트에서 제공하는 내장형 서버는 Tomcat, Jetty, Undertow가 있어요.
내장형 서버의 장점은 별도의 WAS 없이 바로 개발을 시작할 수 있다는 점과 Runable JAR로 구성할 수 있어 간단한 데몬 애플리케이션 등을 개발하는데 용이해요.
다만 실제 운영 환경에서는 성능 및 안전성을 고려해 별도의 WAS를 사용하는 것이 좋아요.
스프링 프레임워크가 그렇다고 스프링 부트 대비 장점이 아에 없는 것은 아니에요. 최소화된 의존성을 관리할수 있고 개발자가 더 많은 부분을 커스터마이징 할 수 있어요. 다만 스프링 부트가 점점 발전하면서 이런 부분들이 큰 의미가 없게 되었어요.
현재에 이르러서는 스프링 부트가 스프링 기반 애플리케이션 개발의 표준으로 자리 잡았어요.