AOP랑 OOP랑 뭐가 다르지?

2025. 9. 26. 01:13·Programming

AOP란?

Aspect Oriented Programming은 횡단 관심사(Cross-Cutting Concern)의 분리를 허용함으로써 모듈성을 높이는 것을 목적으로 하는 프로그래밍 패러다임이다.

 

AOP를 사용하면 여러 객체에 공통적으로 적용되는 기능(예: 로깅, 보안, 트랜잭션 관리 등)을 핵심 로직에서 분리하여 별도의 모듈(Aspect)로 관리하게 된다. 이를 통해 개발자는 반복적인 공통 기능을 매번 구현할 필요가 없어지고, 핵심 비즈니스 로직 개발에 집중할 수 있다.

 

예시를 들어 설명하기 위해서 여러 할인 정책이 있고, 오류 추적을 위해 로깅을 추가했다고 하자.

 

 

처음엔 FixedDiscountPolicy에 단순히 System.out.println을 위·아래로 넣어 처리했다.

public class FixedDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        System.out.println("[LOG] Fixed 할인 시작 price=" + price);
        int amount = 1000;
        System.out.println("[LOG] Fixed 할인 결과 amount=" + amount);
        return amount;
    }
}

 

하지만 새로운 정책(RateDiscountPolicy)이 생긴다면 생길 때마다 또 로깅을 복붙해야 한다..

public class RateDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        System.out.println("[LOG] Rate 할인 시작 price=" + price);
        int amount = (int) (price * 0.1);
        System.out.println("[LOG] Rate 할인 결과 amount=" + amount);
        return amount;
    }
}

 

더 나아가 로그 포맷을 바꾸고 싶다면 모든 구현체를 동시에 수정해야 한다

 

즉, 중복과 산재(Scattering) 가 발생한다.

 

이 문제는 프록시(데코레이터) 로 해결할 수 있다.

핵심 정책은 할인 계산만 담당하고, 로깅은 프록시 한 곳에서 일괄 처리하는 것이다!


프록시가 뭘까?

프록시는 클라이언트가 사용하려는 실제 대상(Real Subject) 대신 요청을 받아주는 대리 객체를 말한다. 겉보기에는 진짜 객체처럼 보이지만, 실제로는 중간에서 요청을 가로채거나 추가적인 처리를 수행하는 가짜 객체다.

 

이 프록시를 어떻게 이용하느냐에 따라 2가지 패턴으로 분류할 수 있다.

 

프록시 패턴 (Proxy Pattern)

클라이언트가 타깃에 접근하는 방법 자체를 제어하기 위한 프록시.

예: 데이터베이스 커넥션을 직접 만들지 않고 프록시를 통해 필요할 때만 생성.

 

데코레이터 패턴 (Decorator Pattern)

클라이언트의 요청은 그대로 타깃에게 전달하지만, 추가 기능을 덧붙이는 목적으로 사용하는 프록시.

예: 출력 기능에 “로깅”을 덧붙여 기록을 남김.


AOP 적용해보기

위에서 정리한 프록시를 통해 데코레이터 패턴으로 AOP를 적용해보면,

핵심 정책은 할인 계산만 담당하고, 로깅은 프록시를 통해 한 곳에서 처리할 수 있다. 

 

 

우선 DiscountPolicy를 인터페이스로 하여 여러 할인 전략들을 구현체로 정의해줬다. 

public class FixedDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        int amount = 1000;
        return amount;
    }
}

public class RateDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        int amount = (int) (price * 0.1);
        return amount;
    }
}

public class SpecialDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        return (price >= 50000) ? 5000 : 0; // 5만원 이상 5000원 할인
    }
}

 

프록시 객체 역시 DiscountPolicy를 구현한 하나의 구현체로 정의되며, 다른 할인 전략과 마찬가지로 discount() 함수를 오버라이딩한다. 중요한 점은 이 프록시 객체가 생성자 주입을 통해 외부에서 실제 할인 전략 객체를 전달받는다는 것이다. 따라서 프록시 객체는 어떤 할인 전략이든 유연하게 적용할 수 있으며, 주입받은 객체의 할인 로직을 그대로 호출해 활용할 수도 있게 된다.

public class LoggingDiscountPolicyProxy implements DiscountPolicy {
    private final DiscountPolicy target;

	// DI: 생성자 주입
    public LoggingDiscountPolicyProxy(DiscountPolicy target) {
        this.target = target;
    }

    @Override
    public int discount(int price) {
        long start = System.currentTimeMillis();
        System.out.println("[LOG] " + target.getClass().getSimpleName() + " 시작 price=" + price);
        try {
            int amount = target.discount(price); //핵심 로직 실행
            System.out.println("[LOG] 결과 amount=" + amount);
            return amount;
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("[LOG] 실행 시간=" + (end - start) + "ms");
        }
    }
}

 

 

전체 흐름을 보면, 클라이언트가 할인을 요청했을 때 서버는 직접 각 할인 정책 객체를 실행하는 것이 아니라 프록시 객체를 실행한다. 따라서 요청을 가장 먼저 받는 것은 프록시이고, 이 시점에 시작 로그가 기록된다.

 

이후 프록시는 실제 할인 정책(핵심 로직)을 호출하여 할인 금액을 계산하게 하고, 다시 제어를 넘겨받아 종료 로그를 남기며 실행 시간을 측정한다.

 

결국 할인 금액 계산이라는 핵심 로직은 각 정책 구현체가 담당하고, 로그 출력과 실행 시간 측정 같은 부가 기능은 프록시가 맡는다. 이런 구조가 바로 앞에서 설명한 AOP의 개념이다.

 


OOP와 AOP는 뭐가 다르지?

OOP는 우리가 흔히 사용하는 프로그래밍 패러다임으로, 설계의 중심을 객체에 두는 방식이다.

 

코드는 전반적으로 객체를 중심으로 구성되며, 객체가 무엇을 하고(행동) 어떤 메시지·데이터를 주고받는지를 기준으로 프로그래밍한다. 또한 객체 간의 관계를 정의하고, 상속·다형성 같은 개념을 활용해 유연하고 재사용 가능한 구조를 만든다.

 

하지만 OOP만으로는 해결이 어려운 부분이 있다. 로깅, 보안, 트랜잭션 처리처럼 여러 객체에 공통으로 흩어지는 기능(= 횡단 관심사, Cross-Cutting Concern)은 코드 중복과 산재(Scattering) 를 유발한다. 그 결과 유지보수성이 떨어지고, 핵심 비즈니스 로직이 공통 코드에 묻혀 가독성이 저하된다.

 

🙋‍♂️ “그럼 공통 로깅을 함수로 빼면 되지 않나?”

 

함수(메서드)로 추출해도 각 지점에서 그 함수를 매번 직접 호출해야 한다는 문제가 남는다. 즉, 호출 구문이 애플리케이션 전반에 흩어져 다시 중복·산재가 발생한다. 이 지점이 바로 OOP만으로는 한계가 드러나는 부분이며, 이런 반복 호출을 자동화하고 중앙에서 관리하기 위해 AOP가 필요해진다.

 

AOP는 객체가 무슨 일을 하는지(행동), 어떤 영향을 받는지(관계, 상태) 와 같은 객체 중심의 설계에는 크게 신경 쓰지 않는다. 대신 공통된 관심사, 즉 기능 자체를 설계의 중심에 두고, 이를 모듈화하여 필요한 지점에 끼워 넣는 방식을 취한다.

 

예를 들어, “회원가입 기능” 같은 핵심 로직에는 집중하고, 로깅·보안·트랜잭션 같은 횡단 관심사는 별도의 Aspect로 관리한다. 이렇게 하면 핵심 로직은 오염되지 않고, 부가 기능은 자동으로 적용된다.

 

처음 AOP를 접했을 때는 “그냥 함수 아니야?” 라는 생각이 들었다. 결국 기능을 담은 함수니까 말이다.

 

하지만 중요한 차이점이 있다. 일반 함수는 개발자가 직접 호출해야 실행된다. AOP는 미리 상황과 대상(Join Point, Pointcut) 을 정해두면, 스프링이 실행 흐름에 맞게 자동으로 호출해준다.

 

즉, AOP는 “정해진 지점에 자동으로 삽입되는 함수”라고 이해할 수 있다.


AOP의 핵심 개념 정리

개념 설명 키워드
Advice 실제 실행할 부가 기능 코드 무엇을 할 것인가
Join Point Advice가 적용될 수 있는 실행 지점 어디에 끼어들 수 있는가
Pointcut 어떤 Join Point를 선택할지 지정 어디에 적용할 것인가
Aspect Advice + Pointcut 묶음 (모듈) 관심사 단위
Weaving Aspect를 실제 코드에 삽입하는 과정 언제/어떻게 적용되는가

 

위에서 만든 AOP 예시를 토대로 핵심 개념을 정리해보면, 로깅과 실행 시간 측정은 Advice에 해당한다.


Join Point는 결제 서비스에 속하는 모든 객체가 될 수 있으며, 그중에서 실제로 부가 기능을 적용한 대상은 할인 정책 객체다. 이것이 곧 Pointcut이다.

 

즉, 부가 기능 코드(Advice)와 이 기능이 적용될 지점을 지정하는 Pointcut을 합쳐 놓은 것이 Aspect다.

 

마지막으로, 이 Aspect가 실제 코드에 삽입되어 동작하도록 만드는 과정 자체가 바로 Weaving이다.

다시 말해, 로깅과 실행 시간 측정을 할인 정책에 실제로 적용하는 행위가 곧 Weaving이다.

 


AOP가 적용되는 시점이 다르다

AOP에서 부가 기능(Advice)을 핵심 로직에 결합(Weaving)하는 시점에 따라 다음과 같이 구분할 수 있다.

 

1. 컴파일 타임 위빙 (Compile-time Weaving)

  • 소스 코드를 컴파일할 때, 바이트코드에 부가 기능을 삽입한다.
  • 대표적으로 AspectJ가 이 방식을 지원한다.
  • 장점: 실행 시 오버헤드가 적다.
  • 단점: 컴파일러가 따로 필요하고, 빌드 과정이 복잡해질 수 있다.

 

2. 로드 타임 위빙 (Load-time Weaving)

  • 클래스가 JVM에 로드되는 시점에 클래스 로더가 바이트코드를 조작하여 부가 기능을 삽입한다.
  • AspectJ LTW(Load-Time Weaving) 방식이 여기에 해당한다.
  • 장점: 컴파일 타임보다 유연하다. (바이트코드 로딩 시점에 조작 가능)
  • 단점: JVM 옵션 설정, 로더 설정이 필요해 상대적으로 설정이 번거롭다.

 

3. 런타임 위빙 (Runtime Weaving)

  • 애플리케이션 실행 중에 프록시 객체를 생성하여 부가 기능을 삽입한다.
  • Spring AOP가 이 방식을 사용한다.
  • 장점: 설정이 단순하고, Spring 빈으로 자동 관리 가능.
  • 단점: 프록시 기반이라 메서드 실행 지점(Join Point) 만 지원한다. (생성자/필드 접근은 불가)

 

지금까지는 AOP가 무엇인지, 그리고 이를 순수 객체 관점에서 어떻게 구현할 수 있는지를 살펴보았다. 순수 OOP 관점에서는 객체를 중심으로 핵심 로직을 설계하고, 필요한 기능을 상속이나 인터페이스 구현을 통해 확장하는 방식으로 문제를 해결한다. 하지만 로깅, 보안, 트랜잭션 처리처럼 여러 객체에 흩어져 반복되는 공통 관심사까지 OOP만으로 관리하려다 보면 코드가 중복되고 구조가 복잡해지기 쉽다.

 

이런 한계를 보완하기 위해 등장한 것이 바로 AOP다. 핵심 로직과 부가 기능을 명확히 분리하고, 필요한 지점에만 교차해서 적용할 수 있게 함으로써 코드의 가독성과 유지보수성을 크게 높여준다.

 

다음 글에서는 단순히 개념적인 수준을 넘어서, Spring이 실제로 어떻게 프록시 객체를 만들어 OOP 기반의 코드에 AOP를 결합하는지를 구체적으로 살펴볼 것이다. Spring의 빈 생명주기와 프록시 생성 과정, 그리고 우리가 작성한 Advice가 실제 코드에 적용되는 순간을 따라가 보면, OOP와 AOP가 어떻게 조화를 이루는지 더 깊이 이해할 수 있을 것이다.

'Programming' 카테고리의 다른 글

우테코 3주차 회고  (0) 2025.11.26
우테코 2주차 회고  (0) 2025.11.26
Discord에 500 Error Webhook으로 알림 보내기  (0) 2025.11.26
SOLID 원칙: 객체 지향 설계의 5가지 기본 원칙  (0) 2025.09.18
'Programming' 카테고리의 다른 글
  • 우테코 3주차 회고
  • 우테코 2주차 회고
  • Discord에 500 Error Webhook으로 알림 보내기
  • SOLID 원칙: 객체 지향 설계의 5가지 기본 원칙
baeminn
baeminn
새로운 기술을 익히는 데 그치지 않고, 실제 문제 해결에 적용하며 구조와 한계를 함께 고민해왔습니다. 서비스 설계, 공간 데이터 분석, 도구 개발 등 다양한 프로젝트를 통해 문제 해결에 필요한 기술을 직접 선택하고 적용해 왔으며, 하나의 역할에 머무르지 않고 필요한 영역까지 책임지는 개발자로 성장하고자 합니다!
  • baeminn
    BaeLog
    baeminn
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • Programming
      • Java
      • CS
        • DB
        • OS
      • Web
      • Spring
      • GeoInfo
      • Infra N
        • Kubernates
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    sharding
    select
    멀티플렉싱
    데이터베이스
    dbcp
    AOP
    redis
    infra
    우테코
    MAP
    partitioning
    io
    OOP
    set
    쉬운코드
    database
    n+1
    Java
    OS
    Datebase
    poll
    hash
    I/O
    cs
    replicaton
    spring
    db
    epoll
    matcher
    spring aop
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
baeminn
AOP랑 OOP랑 뭐가 다르지?
상단으로

티스토리툴바