이 게시물에서는 필자가 Spring Framework의 핵심 개념들인 POJO, IoC, DI, AOP, Filter에 대해 공부한 부분들을 정리하고자 한다.
각 개념의 정의부터 실제 구현 방법, 그리고 왜 이러한 패턴을 사용하는지까지 다뤄보고자 한다.
1. POJO (Plain Old Java Object)
POJO는 특정 프레임워크나 기술에 종속되지 않은 순수한 "자바 객체"를 의미한다.
크게 세 가지의 특징을 가지는데 아래와 같다.
- 특정 클래스를 상속받지 않음
- 특정 인터페이스를 구현하지 않음
- 특정 어노테이션을 포함하지 않음
코드로 한 번 살펴보자.
// POJO 예시
public class Student {
private String name;
private int grade;
public Student() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// POJO가 아닌 예시 (특정 프레임워크에 종속됨)
// → JPA 어노테이션을 사용하고 특정 클래스를 상속받아 POJO의 특성을 잃은 예시이다.
@Entity
public class Student extends AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
}
위 특징들로 인해 예시코드에서도 확인이 가능한 것 처럼
프레임워크 없이도 테스트가 가능하고,
특정 환경이나 규약에 종속되지 않으며
코드가 단순하고 명확해진다는 이점을 가진다!
2. IoC (Inversion of Control)
IoC는 객체의 생명주기와 의존성 관리를 개발자가 아닌 프레임워크가 담당하는 것을 의미한다.
크게 세 가지의 특징을 가지는데 아래와 같다.
- 객체 생명주기 관리
- 객체의 생성, 소멸 시점을 Spring이 결정
- 싱글톤/프로토타입 등의 스코프 관리
- 의존성 관리
- 필요한 의존성을 자동으로 주입
- 순환 참조 감지 및 방지
- 결합도 감소
- 컴포넌트 간 직접적인 의존 관계가 없어짐
- 유연한 구조 변경 가능
1번과 2번의 특징들을 통해 컴포넌트 간의 직접적인 의존 관계가 없어지고, 더욱 유연한 구조 변경이 가능해진다고 이해하면 된다!
IoC 컨테이너의 사용 전과 후를 코드로 함께 살펴보자.
// IoC 컨테이너 사용 전
public class OrderService {
private PaymentService paymentService;
private UserService userService;
public OrderService() {
this.paymentService = new PaymentService();
this.userService = new UserService();
}
}
// → 객체를 직접 생성하여 의존성을 관리하는 방식으로, 결합도가 높고 테스트가 어려운 구조다.
// IoC 컨테이너 사용 후
@Service
public class OrderService {
private final PaymentService paymentService;
private final UserService userService;
public OrderService(PaymentService paymentService, UserService userService) {
this.paymentService = paymentService;
this.userService = userService;
}
}
// → Spring이 의존성을 주입해주어 낮은 결합도와 높은 테스트 용이성을 제공한다.
3. DI (Dependency Injection)의 세 가지 방식
DI는 IoC를 구현하는 디자인 패턴으로, 크게 세 가지 주입 방식이 있다.
먼저 생성자 주입은 필수적인 의존성 주입에 사용되며, 불변성을 보장하고 순환 참조를 방지할 수 있다.
*특히 테스트가 용이하다는 장점이 있다.*
아래는 생성자 주입(생성자를 통한 주입 방식)의 예시이다.
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
// → 세터 주입을 사용하여 선택적 의존성을 유연하게 변경할 수 있다.
// → 생성자 주입을 통해 필수 의존성을 명시적으로 표현하고 불변성을 보장한다.
세터 주입은 선택적인 의존성 주입에 적합하며, 런타임에 의존성을 교체할 수 있다는 특징이 있다.
*다만 순환 참조가 발생할 수 있으니 주의해야 한다.*
아래는 세터 주입(setter 메서드를 통한 주입 방식)의 예시이다.
@Service
public class UserService {
private UserRepository userRepository;
private EmailService emailService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}
마지막으로 필드 주입은 코드가 간결하다는 장점이 있지만,
리플렉션을 사용하여 주입하기 때문에 단위 테스트가 어렵고 순환 참조를 감지하기 어렵다는 단점이 있다.
아래는 필드 주입(@Autowired 어노테이션을 필드에 직접 사용하는 방식)의 예시이다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
}
// → 필드에 직접 @Autowired를 사용하여 가장 간단하지만, 테스트와 유지보수가 어려운 방식이다.
4. AOP (Aspect Oriented Programming)
AOP는 애플리케이션의 핵심 비즈니스 로직과 부가 기능을 분리하여 관리하는 프로그래밍 패러다임이다.
AOP에서는
- Aspect라는 부가 기능을 정의한 모듈,
- Pointcut이라는 부가 기능을 적용할 지점,
- Advice라는 부가 기능의 구현체,
- 그리고 JoinPoint라는 부가 기능이 적용될 수 있는
모든 지점이라는 개념이 있다고 보면된다.
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + " 실행 시간: " + executionTime + "ms");
return result;
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + " 메서드 실행 중 예외 발생: " + ex.getMessage());
}
}
// → AOP를 사용하여 메소드 실행 시간 측정과 예외 로깅을 비즈니스 로직과 분리하여 구현한 예시코드이다.
5. Filter
Filter는 웹 애플리케이션의 요청과 응답을 가로채서 처리하는 컴포넌트이다.
ServletRequest와 ServletResponse 객체를 직접 조작할 수 있으며, 서블릿 컨테이너에서 동작한다.
특히 Spring의 컨텍스트 외부에서 동작하며, URL 패턴을 기반으로 적용된다는 특징이 있다.
주로 문자 인코딩 변환, 로깅, 인증/인가, CORS 처리 등에 사용된다.
@Component
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 필터 초기화 로직
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
long startTime = System.currentTimeMillis();
chain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println(requestURI + " 처리 시간: " + (endTime - startTime) + "ms");
}
@Override
public void destroy() {
// 필터 종료 시 정리 작업
}
}
// → Filter를 사용하여 HTTP 요청/응답의 전처리와 후처리를 수행하고 실행 시간을 측정한다.
🚨Filter와 Interceptor의 차이점🚨
- 실행 시점에서 Filter는 DispatcherServlet 이전에 실행되고 Interceptor는 DispatcherServlet 이후, 컨트롤러 호출 전후에 실행된다.
- 또한 스프링 빈 사용에 있어서도 Filter는 제한적으로만 사용이 가능한 반면, Interceptor는 자유롭게 사용이 가능하다.
'Spring' 카테고리의 다른 글
[Spring] JdbcTemplate으로 데이터베이스 접근하기! (0) | 2025.01.20 |
---|---|
[Spring] 🔎 DataSource와 트랜잭션 개념정리 🔎 (2) | 2025.01.20 |
[Spring] Controller의 Return Type과 Parameter 가이드 👍 (1) | 2025.01.20 |
[Spring] Spring Singleton Pattern과 주요 어노테이션 정리 ✅ (0) | 2025.01.20 |
[Spring] ⚡️ Spring MVC와 Spring Boot 개념 정리 ⚡️ (0) | 2025.01.19 |