자바 기반의 오픈소스 애플리케이션 프레임워크로, 엔터프라이즈급 애플리케이션을 효율적으로 개발할 수 있도록 지원하는 프레임워크이다.
제어의 역전(IoC), 의존성 주입(DI), 관점지향 프로그래밍(AOP), POJO 등의 개념을 바탕으로 생산성을 높이고 유지보수를 쉽게 한다.
기존 엔터프라이즈급 프레임워크들의 복잡하고 무거운 비즈니스 로직 개발을 가볍고 유연하게 만들어주는 경량급 프레임워크이다.
객체의 생성과 생명주기 관리를 개발자가 아닌 Spring 컨테이너가 담당하여 유연성과 확장성이 높아진다.
객체(인스턴스)의 생성부터 소멸까지 객체의 생명주기를 개발자가 아닌 스프링의 컨테이너가 관리하는 것을 의미한다.
개발자는 new 연산자, 인터페이스나 클래스 호출 방식 등으로 객체를 생성하거나 소멸시킨다.
이에 대한 제어권을 외부(프레임워크)에 둠으로서 결합도가 낮아지고, 유연성과 확장성이 향상된다.
public class TestService {
private FirstService firstService = new FirstService();
}
public TestService(FirstService firstService {
this.first = firstService;
}
외부에 존재하는 객체를 받으며(IoC), 외부에서 주입받는 DI와도 관련이 있다.
객체 간 의존성을 개발자가 관리하는 것이 아닌, 프레임워크가 주입해 주는 방식으로 결합도를 낮추고 유지보수를 용이하게 한다.
객체를 스프링 컨테이너에서 Bean으로 생성하고, 이렇게 생성된 객체를 지정한 객체에 주입해주는 것을 의존성 주입이라고 한다.
객체가 코드 상에서 객체 생성에 관여 하지 않아도 되며, 컨테이너가 생명주기를 관리하고 의존관계를 관리해 의존도를 낮추어준다.
public class Vehicle{
private VehicleType type;
public Vehicle() {
this.type = new Bicycle();
}
}
interface VehicleType{}
class Car implement VehicleType{}
class Bicycle implement VehicleType{}
위의 경우 Vehicle 클래스의 타입을 변경하기 위해선 코드 자체의 수정이 필요하며, Vehicle 클래스가 VehicleType 에 의존적이라고 할 수 있다.
결합도가 매우 높아 생성자의 변경이 필요한 상황으로, 유연성이 떨어진다.
이를 의존성의 3가지 주입방식으로 해결하게되면 다음과 같다.
@Component
public class Vehicle {
@Autowired
private VehicleType type;
}
@Component
public class Vehicle {
private final VehicleType type;
@Autowired
private Vehicle(VehicleType type) {
this.type = type;
}
}
@Component
public class Vehicle {
private final VehicleType type;
@Autowired
private setVehicleType(VehicleType type) {
this.type = type;
}
}
위의 세가지 방식 모두 Vehicle 클래스에서 type을 외부에서 주입받는 형식으로, VehicleType이 변경되더라도 Vehicle을 수정 할 필요가 없다.
객체 불변성, 순환참조, 테스트 측면에서 스프링은 생성자 주입방식을 권장하고 있다.
핵심 비즈니스 로직과 공통 기능(횡단 관심사, Cross-cutting Concern) 을 분리하여 관리하는 개념이다.
Spring 프레임워크에서 제공하는 AOP 기능으로, 로깅, 트랜잭션 관리, 보안, 예외 처리 등 공통적으로 사용되는 기능을 핵심 로직과 분리하여 관리할 수 있다.
여러 모듈에서 로그, 보안 검사, 트랜잭션 처리 등 공통적으로 필요한 기능이 있다, 이런 기능들이 서비스 로직에 함께 사용되면 코드가 복잡해지고 반복적으로 나타나며 유지보수가 어려워 진다.
아래는 로그 처리의 예시 이다
public class PaymentService {
public void processPayment() {
System.out.println("로그 기록: 결제 프로세스 시작"); // 공통 기능 (횡단 관심사)
System.out.println("결제 처리 중...");
System.out.println("로그 기록: 결제 프로세스 완료"); // 공통 기능 (횡단 관심사)
}
}
이와같이 기능마다 로그기능이 함께 있다면 유지보수가 어려울 것이다.
AOP를 적용하게 되면
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.PaymentService.*(..))")
public void logBefore() {
System.out.println("로그 기록: 결제 프로세스 시작");
}
@After("execution(* com.example.service.PaymentService.*(..))")
public void logAfter() {
System.out.println("로그 기록: 결제 프로세스 완료");
}
}
execution 이하 경로에 해당하는 로직에 자동적으로 로그기능이 동작하게 되어 코드의 반복이 줄고, 유지보수가 쉬워지게 된다.
Aspect
Join Point
Advice
Pointcut
Weaving
@Aspect
@Component
public class LoggingAspect {
// ✅ Pointcut: test() 메서드 실행을 대상으로 함
@Pointcut("execution(* com.example.MyService.test(..))")
public void testMethodPointcut() {}
// ✅ Advice: 메서드 실행 전에 실행됨 (시간 측정 시작)
@Before("testMethodPointcut()")
public void logBefore() {
System.out.println("시간 측정 시작...");
}
// ✅ Advice: 메서드 실행 후에 실행됨 (시간 측정 종료)
@After("testMethodPointcut()")
public void logAfter() {
System.out.println("시간 측정 종료...");
}
}
Join Point → test() 메서드가 실행되는 순간
Advice → logBefore()(Before Advice), logAfter()(After Advice)
Pointcut → "execution(* com.example.MyService.test(..))" (test() 메서드 실행 시 Advice 적용)
POJO는 특정 프레임워크나 기술에 종속되지 않고, 순수 Java 만을 사용하여 만들어진 객체 를 의미한다.
Java의 기본 문법을 따르며 인터페이스, 상속, 특정 어노테이션 등을 강제하지 않는다.
즉, 필드와 getter와 setter 정도의 기능을 갖는 기본 객체라고 볼 수 있다.
public class User {
private String name;
private int age;
// 기본 생성자
public User() {}
// 생성자 오버로딩
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter & Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}