해당 포스팅에선 가장 기초이면서도 중요한 부분에 대해 포스팅해보고자 한다.
스프링의 핵심가치는 다음과 같다.
애플리케이션 개발에 필요한 기반을 제공해서 개발자가 비즈니스 로직 구현에만 집중할 수 있도록 하는 것
말 그대로 개발자는 비니지스 로직 구현에만 집중하라 이거다.
여기에서 이제 유명한 게 3가지가 있는데, 다음과 같다.
하나씩 알아보자
IoC는 소프트웨어 디자인 원칙 중 하나로, 전통적인 프로그래밍 방식에서 코드의 흐름(제어)이 개발자에 의해 명시적으로 관리되던 방식에서 컨테이너나 프레임워크가 흐름을 관리하도록 책임을 위임하는 것을 의미한다.
이렇게 말하면 너무 어려우니까 간단하게 설명하자면 다음과 같다.
객체의 생성과 제어 흐름을 개발자가 아닌 외부 컨테이너에 맡긴다.
그렇다면 왜 이걸 제어의 역전이라고 표현할까??
답은 다음과 같다.
IoC라는 표현은 전통적인 제어 흐름이 뒤바뀌었기 때문에 붙여진 이름이다.
우린 자바에서 전통적으로 개발자가 객체를 생성하고, 객체 간의 의존성을 연결하며, 실행 순서를 직접 관리했다. 즉, 제어의 주체가 개발자였다.
예시를 들어 코드를 작성해보겠다.
public class A {
private B b;
public A() {
this.b = new B(); // A가 B의 생성과 제어를 담당
}
}
이런 식으로 우린 직접 코드를 작성하여 객체를 제어했다.
하지만 스프링에선 객체의 생성과 의존성 관리를 개발자가 아닌 외부 컨테이너가 담당한다.
즉, 개발자는 객체가 어떻게 생성되고 연결되는지 신경 쓰지 않고, 필요할 때만 사용하면 된다.
IoC를 사용하는 이유는 다음과 같다.
IoC는 주로 의존성 주입(Dependency Injection, DI)과 컨테이너를 사용해서 만들게 되는데, DI에 대해선 곧 설명할테니 먼저 컨테이너가 무엇인지에 대해 알아보자.
컨테이너는 IoC의 핵심 구현체로, 객체의 생성, 생명주기 관리, 의존성 주입 등을 담당한다.
다음과 같은 특징을 가지고 있다.
예시로 코드를 작성하면 다음과 같다.
// Service는 컨테이너에 의해 관리
@Component
public class Service {
// Repository 객체를 직접 생성하지 않지만, 컨테이너가 Repository 객체를 생성하고 주입
private final Repository repository;
@Autowired
public Service(Repository repository) { // 의존성은 컨테이너가 관리
this.repository = repository;
}
}
그렇다면 이제 DI(Dependency Injection) - 의존성 주입에 대해 알아보자.
의존성 주입(DI)은 IoC(Inversion of Control, 제어의 역전)를 구현하는 가장 널리 사용되는 방법이다.
DI의 핵심은 DI와 정말 잘 맞는다.
객체가 의존성을 스스로 생성하거나 관리하지 않고 외부에서 주입받는 것
DI의 특징은 다음과 같다.
더 자세히 알기 위해 의존성이 무엇인가, 의존성 주입은 또 뭔가에 대해 알아보자.
객체 간 관계에서 한 객체가 다른 객체에 의존하는 것을 의미한다.
예를 들어 설명하자면 다음과 같다.
객체 A가 객체 B의 기능을 사용해야 할 때, 객체 B는 객체 A의 의존성이다.
객체 간 결합도를 낮추고, 객체의 재사용성과 테스트 용이성을 높이는 설계 기법이다.
예를 들어 설명하자면 다음과 같다.
객체 A가 객체 B를 스스로 생성하는 대신, 외부에서 B 객체를 제공(주입)받는 방식
의존성을 주입하는 방식엔 크게 3가지로 나뉘어진다.
코드로 예시를 작성하면 다음과 같다.
public class Service {
private final Repository repository;
// 생성자를 통해 의존성을 주입받음
public Service(Repository repository) {
this.repository = repository;
}
}
마찬가지로 코드로 예시를 보자
public class Service {
private Repository repository;
// 세터 메서드를 통해 의존성을 주입받음
public void setRepository(Repository repository) {
this.repository = repository;
}
}
코드로 예시를 보자
@Component
public class Service {
@Autowired // 필드에 직접 의존성을 주입
private Repository repository;
}
여기까지가 DI에 관한 내용이고, 이제 AOP에 대해 알아보자
AOP는 소프트웨어 설계 패턴 중 하나로, 핵심 비즈니스 로직과 이를 보조하는 공통 관심사(Cross-Cutting Concerns)를 분리하여 모듈화하는 프로그래밍 기법이다.
이것도.. 이렇게 설명하면 어려우니까 쉽게 설명하면, 관점을 기준으로 묶어서 개발하는 방식이라고 할 수 있다.
AOP는 객체 지향 프로그래밍(OOP)을 보완하며, "무엇을 해야 하는지"에 집중한 코드 작성이 가능하도록 돕는다.
AOP의 핵심 개념에 대해 먼저 알아보자
애플리케이션 전반에 걸쳐 반복적으로 나타나는 기능이다.
예를 들어 로깅(logging), 보안(security), 트랜잭션 관리(transaction management), 예외 처리(exception handling) 등이 있다.
애플리케이션의 주요 기능을 구현하는 코드이다.
이건 뭐 예를 들 필요도 없지 않은가,, 말 그대로 비지니스 로직이다.
AOP를 사용하는 목표는 다음과 같다.
- 공통 관심사와 핵심 비즈니스 로직을 분리하여 모듈화
- 공통 관심사를 한 곳에서 관리하여 코드 중복을 제거하고 유지보수를 쉽게 만든다.
주요 용어들을 정리해보면 다음과 같다.
Aspect (관점)
- 공통 관심사의 구현체
- 로깅, 보안, 트랜잭션 관리 등을 "관점"으로 표현
Join Point (연결점)
- 프로그램 실행 중 Aspect가 적용될 수 있는 지점
- ex) 메서드 호출, 예외 처리, 필드 접근 등
Advice (충고)
- Join Point에서 실행될 동작(코드)
- Advice는 유형이 다양하게 있다.
Before Advice - 대상 메서드 실행 전에 동작
After Advice - 대상 메서드 실행 후 동작
Around Advice - 대상 메서드 실행 전후에 동작(가장 강력)
After Returning Advice - 메서드가 정상적으로 실행된 후 동작
After Throwing Advice - 메서드가 예외를 던진 후 동작
Pointcut (지점)
- Join Point를 필터링하여 Aspect를 적용할 대상을 지정
- 특정 클래스, 메서드, 패키지 등 선택 가능
Weaving (위빙)
- Aspect를 실제 코드에 적용하는 과정
- 컴파일 타임, 런타임, 로드 타임에 수행 가능
여기서 내가 중요하게 보는 부분은 다음 부분이다.
Spring AOP는 프록시(Proxy)를 기반으로 AOP를 구현한다.
먼저 그럼 프록시에 대해 알아보자
대상 객체(Target Object)에 대한 대리 객체로, 대상 객체의 메서드를 호출하기 전/후에 원하는 작업을 수행할 수 있는 구조이다.
Spring AOP에서 프록시는 Join Point(대상 메서드 호출 지점) 전후에 Advice(공통 로직)를 실행하기 위해 사용된다.
말로 하면 너무 어려운 것 같다.
코드를 작성해서 설명해주도록 하겠다.
시나리오는 다음과 같다.
- 서비스 클래스의 메서드가 호출될 때, 메서드 이름과 실행 시간을 로깅
- AOP를 사용하여 핵심 로직에 영향을 주지 않고 로깅 로직을 적용
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// Pointcut: service 패키지의 모든 메서드
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
System.out.println("[AOP] Before Method: " + joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfterMethod(JoinPoint joinPoint) {
System.out.println("[AOP] After Method: " + joinPoint.getSignature().getName());
}
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 실제 메서드 실행
long executionTime = System.currentTimeMillis() - start;
System.out.println("[AOP] Method " + joinPoint.getSignature().getName() +
" executed in " + executionTime + "ms");
return result;
}
}
이 후 서비스 클래스를 구현한다.
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class MyService {
public void performTask() {
System.out.println("Performing task...");
}
public void anotherTask() {
System.out.println("Performing another task...");
}
}
스프링부트 애플리케이션 클래스도 필요하겠지.
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AopExampleApplication {
public static void main(String[] args) {
SpringApplication.run(AopExampleApplication.class, args);
}
}
마지막으로 컨트롤러까지 작성
package com.example.controller;
import com.example.service.MyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/task")
public String performTask() {
myService.performTask();
return "Task performed";
}
@GetMapping("/another-task")
public String anotherTask() {
myService.anotherTask();
return "Another task performed";
}
}
이 때, 실행 과정은 다음과 같다.
- 빈 등록
- Spring IoC 컨테이너는 MyService 클래스를 빈으로 등록할 때, 이를 프록시 객체로 감싼다.
- 프록시는 MyService를 대신해 메서드 호출을 가로챈다.
- 메서드 호출
- 클라이언트가 /task API를 호출하면, 컨트롤러가 myService.performTask()를 호출한다.
- 이 때, 실제로는 프록시가 호출되며 Join Point(performTask 메서드)에 도달
- Advice 실행
- @Before Advice가 실행: 메서드 실행 전에 로그 출력
- 실제 performTask 메서드 실행
- @After Advice가 실행: 메서드 실행 후 로그 출력
- @Around Advice가 실행: 실행 시간 측정
이렇게 모든 과정이 진행되게 된다.
스프링에 관하여 스프링부트에 대하여 포스팅 하는 것은 정말 끊임없는 공부가 되는 듯 하다. 모든 원리 개념을 알기까지 화이팅!
https://innovation123.tistory.com/167
https://velog.io/@dltkdgus1850/Spring-%ED%94%84%EB%A1%9D%EC%8B%9C%EB%9E%80