[Spring] POJO와 AOP, 왜 그렇게 강조할까?

하비·2026년 2월 22일

Spring boot

목록 보기
7/7

스프링을 공부하다 보면 POJO니 AOP니 하는 어려운 용어들이 쏟아집니다. 저는 그 당시 이해도 못하고, 그냥 그렇구나 하고 넘어갔었는데요. 이걸 왜 계속 강조하는지, 왜 스프링의 핵심인지 오늘 알아보도록 하겠습니다.

1. POJO

POJO(Plain Old Java Object)는 말 그대로 '순수한 자바 객체'를 뜻합니다. EJB를 썼던 시절, 복잡한 코드 구조와 비즈니스 로직과는 상관없는 코드들 때문에, POJO가 등장하게 되었습니다. 이게 왜 중요할까요?

EJB: 옛날에는 기능을 하나 만들려고 해도 프레임워크가 시키는 대로 복잡한 클래스를 상속받아야 했습니다. 내 코드가 프레임워크에 꽉 묶여서, 프레임워크 없이는 아무것도 못 하는 '종속적인' 상태였습니다.

Spring: 스프링은 "너는 그냥 평범하게 자바 코드를 짜. 나머지는 내가 알아서 붙여줄게"라고 말합니다. 이렇게 특정 기술에 얽매이지 않아야 나중에 코드를 고치기도 쉽고 테스트하기도 편해집니다.

EJB와 Spring 코드 비교

백문이 불여일견! 우리가 작성하는 코드가 어떻게 POJO(순수한 자바 객체)가 될 수 있는지, 아니면 프레임워크의 노예가 되는지 실제 예시를 통해 비교해 보겠습니다.

1. POJO가 아닌 코드 (비-POJO: 기술 종속적)
과거 EJB나 잘못 설계된 프레임워크 환경에서의 코드입니다.

// 1. 반드시 특정 인터페이스(SessionBean)를 구현해야 함
public class MemberEJB implements SessionBean {
    
    // 비즈니스 로직과 상관없는 컨테이너용 메서드들을 의무적으로 작성
    public void ejbCreate() {}
    public void ejbRemove() {}
    public void ejbActivate() {}
    public void ejbPassivate() {}
    public void setSessionContext(SessionContext ctx) {}

    // 실제 우리가 짜고 싶은 로직
    public void addMember(String name) {
        System.out.println(name + " 님을 등록합니다.");
    }
}

문제점: 이 코드는 FrameworkService나 ContextManager 같은 외부 라이브러리 없이는 단독으로 실행이 불가능합니다. 만약 프레임워크를 바꾸고 싶다면 이 클래스 전체를 새로 짜야 합니다.

2. POJO인 코드 (순수한 자바 객체)
스프링이 지향하는 방식입니다. 어떤 외부 기술도 내 코드에 직접 침범하지 않습니다.

// 아무것도 상속받지 않은 순수한 자바 객체 (POJO)
public class MemberService {

    public void addMember(String name) {
        System.out.println(name + " 님을 등록합니다.");
    }
}

장점: 이 클래스는 스프링 없이도 작동합니다. 다른 프레임워크로 옮겨가도 그대로 쓸 수 있고, 무엇보다 자바 언어 그 자체의 기능에만 집중되어 있습니다.

2. AOP

AOP(Aspect Oriented Programming)는 우리말로 '관점 지향 프로그래밍'이라고 불립니다. 위키백과에 따르면 AOP는 횡단 관심사(Cross-cutting concerns)의 분리를 통해 모듈성을 증가시키는 프로그래밍 패러다임입니다.

횡단 관심사란 로깅(Logging), 트랜잭션(Transaction), 보안, 권한 체크 등 여러 모듈이나 클래스에서 공통으로 필요하지만, 해당 모듈의 핵심 비즈니스 로직과는 직접적인 관련이 없는 기능을 말합니다. AOP는 이러한 부가 기능들을 핵심 로직에서 분리하여 관리하는 것이 핵심입니다.
(출처: 테코톡(https://www.youtube.com/watch?v=hdO_V7EMU4s&t=15s))

왜 AOP가 필요할까?

만약 서비스의 모든 메서드에 실행 시간을 측정하는 기능을 추가해야 한다고 가정해 봅시다. 메서드가 100개, 혹은 1억 개라면 어떻게 될까요?

  • 코드 중복: 모든 메서드에 동일한 시간 측정 로직이 들어갑니다.
  • 유지보수의 어려움: 측정 단위를 밀리초에서 나노초로 변경해야 한다면 모든 코드를 수정해야 합니다.
  • 가독성 저하: 비즈니스 로직과 부가 기능이 섞여 있어 코드를 이해하기 어렵게 만듭니다.

AOP를 사용하면 이러한 부가 기능을 독립적인 모듈로 분리하여 변경 지점을 하나로 모을 수 있고, 객체 지향적인 단일 책임 원칙(SRP)을 지키며 OOP를 보완할 수 있습니다.

AOP 핵심 용어 정리

AOP를 이해하기 위해 반드시 알아야 할 용어들입니다.

  • Target (타겟): 부가 기능을 부여할 대상(클래스, 메서드 등)입니다.
    ex) "계좌이체", "입출금", "이자 계산" 로직이 담긴 은행 애플리케이션의 서비스 객체
  • Advice (어드바이스): 실질적으로 수행할 '부가 기능' 그 자체입니다. (언제 실행할지에 따라 Before, After, Around 등으로 나뉩니다)
    ex) 메서드 실행 시간을 측정하기 위해 시작 시간과 종료 시간을 기록하는 "시간 측정 로직"
  • Join Point (조인 포인트): 어드바이스가 적용될 수 있는 모든 지점을 말합니다. (Spring AOP에서는 메서드 실행 시점으로 한정됩니다)
    ex) factorial(), factPlus(), minus() 메서드들이 각각의 조인 포인트가 될 수 있습니다
  • Pointcut (포인트컷): 조인 포인트 중에서 실제로 어드바이스를 적용할 지점을 선별하는 규칙입니다.
    ex) "이름이 print로 시작하는 모든 메서드"에 적용, "hello.proxy.aop 패키지"에 있는 모든 함수에 적용
  • Aspect (애스펙트): Advice와 Pointcut을 합친 개념으로, AOP의 기본 모듈입니다. Spring AOP에서는 이를 Advisor(어드바이저)라고 부르기도 합니다.
    ex) @Aspect 어노테이션이 붙은 Logging 클래스
  • Weaving (위빙): 핵심 로직(Target)과 부가 기능(Advice)을 연결하여 하나로 만드는 과정입니다.
    ex) 스프링이 실행될 때 빈 후처리기를 통해 타겟 객체 대신 부가 기능이 포함된 프록시 객체를 생성하여 연결하는 것
  • Proxy (프록시): 클라이언트의 요청을 가로채서 부가 기능을 먼저 수행하고 타겟에게 요청을 넘겨주는 대리인 역할을 합니다.
    ex) 클라이언트가 BasicCalculator를 호출할 때, 이를 감싸고 있는 ExecutionTimeCalculator가 요청을 대신 받아 시간 측정을 먼저 수행하는 구조

Spring AOP의 동작 원리

Spring AOP는 런타임 시점에 프록시 패턴을 사용하여 동작합니다.

  1. 객체 생성 및 전달: 스프링이 빈(Bean) 객체를 생성하면 이를 빈 후처리기(Bean Post Processor)에 전달합니다. 이때 전달되는 빈은 아직 부가 기능이 적용되지 않은 상태입니다.
  2. 대상 여부 확인: 빈 후처리기는 Advisor(애스펙트)를 조회한 뒤, Pointcut(포인트컷)이라는 선정 규칙을 확인하여 해당 빈이 AOP 적용 대상인지 체크합니다,. Pointcut은 수많은 Join Point(조인 포인트) 중 실제로 Advice를 적용할 지점을 선별하는 역할을 합니다,.
  3. 프록시 생성 및 등록: 적용 대상(Target)이라면 원래 객체인 Target(타겟) 대신, 부가 기능이 결합된 Proxy(프록시) 객체를 동적으로 생성합니다,. 이처럼 핵심 기능과 부가 기능을 연결하는 과정을 Weaving(위빙)이라고 하며, 생성된 Proxy가 실제 빈 객체로 스프링 컨테이너에 등록됩니다,.
  4. 메서드 호출 및 실행: 클라이언트가 메서드를 호출하면 컨테이너에 등록된 Proxy가 요청을 대신 받습니다,. Proxy는 정의된 Advice(어드바이스)를 먼저 실행하여 부가 기능을 수행한 후, 실제 Target 객체의 로직을 호출하여 핵심 기능을 처리합니다,. 스프링 AOP에서 이 모든 과정이 일어나는 Join Point는 '메서드 실행 시점'으로 한정됩니다

코드로 직접 보기

1. AOP 적용 전

먼저, 메서드의 실행 시간을 측정해야 하는 상황입니다. AOP가 없다면 아래와 같이 핵심 비즈니스 로직(출력)부가 기능(시간 측정)이 한 곳에 뒤섞이게 됩니다.

public class HelloService {
    public void printA() {
        // [부가 기능: 시간 측정 시작]
        long startTime = System.currentTimeMillis(); 

        // [핵심 로직]
        for (int i = 0; i < 1000; i++) {
            System.out.println("hello A");
        }

        // [부가 기능: 시간 측정 종료 및 로그 출력]
        long endTime = System.currentTimeMillis();
        System.out.println("총 걸린 시간: " + (endTime - startTime));
    }
}

문제점: 만약 이런 메서드가 100개, 1억 개라면 모든 메서드에 동일한 시간 측정 코드를 일일이 작성해야 하며, 이는 심각한 코드 중복과 유지보수의 어려움을 야기합니다.


2. Spring AOP 적용 후
AOP를 사용하면 핵심 로직은 건드리지 않고, 부가 기능만 별도의 클래스로 분리할 수 있습니다.

(1) Target: 핵심 로직만 남은 클래스
이제 HelloService에는 비즈니스 본연의 기능만 남습니다.

@Component
public class HelloService {
    public void printA() {
        // 부가 기능은 사라지고 핵심 로직만 남음
        for (int i = 0; i < 1000; i++) {
            System.out.println("hello A");
        }
    }
}

(2) Aspect: 부가 기능을 정의한 모듈
@Aspect 어노테이션을 사용하여 Advice(무엇을)Pointcut(어디에)을 정의합니다.

@Aspect
@Component
public class LogAspect {

    // [Pointcut]: hello.proxy.aop 패키지 내의 print로 시작하는 모든 메서드에 적용
    @Around("execution(* hello.proxy.aop..print*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        
        // [Advice]: 실행 전 부가 기능
        long startTime = System.currentTimeMillis(); 

        try {
            // [Join Point]: 실제 타겟의 핵심 로직(printA 등)을 실행
            Object result = joinPoint.proceed(); 
            return result;
        } finally {
            // [Advice]: 실행 후 부가 기능
            long endTime = System.currentTimeMillis();
            System.out.println("총 걸린 시간: " + (endTime - startTime));
        }
    }
}

주의할 점

Spring AOP는 프록시 기반으로 동작하기 때문에, 클래스 내부에서 자신의 다른 메서드를 호출할 때는 AOP가 적용되지 않습니다. 프록시를 거치지 않고 타겟 객체의 메서드를 직접 호출하기 때문입니다. 또한, Spring AOP는 스프링 컨테이너가 관리하는 빈(Bean)에만 적용 가능하다는 점을 명심해야 합니다.

profile
멋진 개발자가 될테야

0개의 댓글