스프링을 스프링답게 만드는 진짜 이유: IoC·AOP·PSA 삼각형의 정체

2026. 1. 18. 12:38·스프링

0. 들어가며

 

스프링은 자바 기반의 오픈소스 프레임워크입니다.
그리고 스프링은 단순히 편리한 기능을 제공하는 프레임워크가 아니라  자바로 객체지향 프로그래밍을 더 잘 할 수 있도록 도와주는 프레임워크입니다.

 

그 이유는 스프링의 뼈대에 IoC/DI, AOP, PSA, 그리고 POJO라는 핵심 개념들이 있기 때문입니다.
이 개념들은 각각 독립적인 기능처럼 보이지만 결국 공통된 목적을 가지고 있습니다.
바로 객체가 자신의 역할과 책임에만 집중할 수 있도록 만드는 것입니다.

 

그래서 스프링을 제대로 이해하기 위해서는 어노테이션이나 사용법을 익히는 것보다
이른바 ‘스프링 삼각형’ 이라 불리는 IoC/DI, AOP, PSA 그리고 POJO를 먼저 이해하는 것이 중요하다고 느꼈습니다.

 

이 글은 「토비의 스프링」을 읽으며 스프링 삼각형이 왜 등장했는지 그리고 이 개념들이 자바의 객체지향 프로그래밍과 어떤 관계를 가지고 있는지를 저의 언어로 정리한 기록입니다.

 

이 글이 스프링을 처음 깊이 이해하려는 분들에게 또는 저처럼 개념을 다시 정리하고 싶은 분들에게 작은 도움이 되었으면 합니다.


1.Spring이 뭔데?

Spring을 효과적으로 학습하려면  먼저 스프링이 지지하는 가치와 이루고자 하는 목표를 이해하는 것이 중요합니다.
그리고 그 목표를 달성하기 위해 스프링이 제공하는 구체적인 기술에는 어떤 것이 있으며, 왜 필요한지 알아야 합니다.

스프링은 단순히 기계적으로 적용할 수 있는 기술이 아닙니다.먼저 스프링을 충분히 이해하고, 그 이해를 바탕으로 창조적으로 응용해야만 스프링이 제공하는 진정한 가치를 얻을 수 있습니다.


1-1 스프링의 핵심 철학: 객체지향 중심 사고

스프링은 자바에서 가장 중요하게 가치를 두는 것은 바로 객체지향 프로그래밍이 가능한 언어라는 점입니다.

 

자바 엔터프리이즈 기술의 혼란속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로 부터 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴수 있도록 기본으로 돌아가자는 것이 바로 스프링의 핵심 철학입니다. 
그래서 스프링이 가장 많이 관심을 두는것이 오브젝트입니다. 

 

스프링을 이해하려면 먼저 오브젝트에 깊은 관심을 가져야 합니다. 애플리케이션에서 오브젝트가 생성되고 다른 오브젝트와 관계를 맺으며, 사용되고, 소멸하기까지의 전 과정을 진진하게 생각해볼 필요가 있습니다.

 

더 나아가서 오브젝트는 어떻게 설계되어야 하는지, 어떤 단위로 만들어지며 어떤 과정을 통해 자신의 존재를 드러내고 등장해야 하는지에 대해서도 살펴보아야 합니다.

 

결국 오브젝트에 대한 관심은 오브젝트의 기술적인 특징과 사용방법을 넘어서 오브젝트의 설계로 발전하게 됩니다.

 

객체지향 설계의 기초와 원칙을 비롯해서 다양한 목적을 위해 재활용 가능한 설계방법인 디자인패턴, 좀더 깔끔한 구조가 되도록 지속적으로 개선해나가는 작업인 리팩토링, 오브젝트가 기대한 대로 동작하고 있는지를 효과적으로 검증하는데 쓰이는 단위 테스트와 같은 오브젝트 설계와 구현에 관한 여러가지 응용 기술과 지식이 요구됩니다.

 

스프링은 객체지향 설계와 구현에 관해 특정한 모델과 기법을 억지로 강요하지는 않지만 오브젝트를 어뗗게 효과적으로 설계하고 구현하고, 사용하고 이를 개선해나갈것인가에 대한 명쾌한 기준을 마련해줍니다. 동시에 스프링은 객체지향 기술과 설계, 구현에 관한 실용적인 전략과 검증된 베스트 프랙티스를 평범한 개발자도 자연스럽고 손쉽게 적용할수 있도록 프레임워크 형태로 제공합니다. 

 

그리고 이러한 모든 것을 가능하게 하는 것이 바로 스프링 삼각형입니다.

IoC/DI, AOP, PSA, POJO로 구성된 스프링 삼각형은 스프링이 지향하는 객체지향 철학을 실제 코드에서 구현할 수 있도록 돕는 핵심 구조입니다.

 

지금부터 하나씩 차근차근 다루어 보겠습니다


2.  IoC와 DI — 객체지향 설계를 돕는 핵심 원리

스프링 삼각형의 첫 번째 요소는 IoC(Inversion of Control, 제어의 역전)와 그 구현 방식인 DI(Dependency Injection, 의존관계 주입) 입니다.

 

2-1. IoC: 제어의 역전

IoC는 객체의 생성과 관리 책임을 직접 코드에서 수행하지 않고, 외부 컨테이너가 대신 관리하도록 하는 원리를 의미합니다.
쉽게 말해, 객체가 스스로 필요한 객체를 만들거나 관리하지 않고 필요한 객체는 스프링 컨테이너가 제공해 주는 구조입니다.

 

예를 들어, 객체지향 설계 원칙 중 하나인 의존성 역전 원칙(DIP)을 지키려면 상위 모듈이 하위 모듈에 직접 의존하지 않고 역할(인터페이스)에만 의존해야 합니다.
IoC는 이런 원칙을 실제 코드에서 자연스럽게 구현할 수 있도록 돕습니다.

 

IoC가 필요한 이유

예를 들어  주문서비스를 구현할때, 할인 정책을 직접 생성한다면,

public class OrderService {
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

 

이 경우, OrderService는 구체 클래스(RateDiscountPolicy)에 강하게 의존하게 됩니다.

나중에 할인 정책을 변경하거나 테스트용 객체로 교체하려면 OrderService 코드를 직접 수정해야 합니다.

 

때문에  객체 설계가 유연하지 않고 결합도가 높아지는 문제가 발생합니다.

이제 IOC를 적용해 보겠습니다.
IoC를 적용하면 OrderService는 자신의 역할(인터페이스)에만 의존하고 필요한 객체는 외부에서 주입받도록 설계할 수 있습니다.

public class OrderService {
    private final DiscountPolicy discountPolicy;

    // DI를 통해 외부에서 주입받는다
    public OrderService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

 

 

OrderService는 구체 클래스가 아니라 역할(DiscountPolicy)에만 의존합니다.할인 정책을 변경하거나 테스트용 객체를 주입하더라도 OrderService 코드는 수정할 필요가 없습니다.객체의 생성과 관리 책임이 외부 컨테이너로 넘어가면서, 개발자는 핵심 로직에 집중할수 있고. 코드가 훨씬 유연해지고 재사용성이 높아집니다.

 

DI라는 용어가 나왔는데요, DI는 IOC를 구현하는 구체적인 방법입니다.
이어서 DI에 대해서 살펴보겠습니다.

 


2-2. DI: 의존관계 주입

스프링에서 DI(Dependency Injection, 의존관계 주입)는 IoC를 구현하는 구체적인 방법 중 하나로, 객체가 필요한 의존성을 외부에서 주입받는 방식을 의미합니다.

 

즉, 객체가 스스로 필요한 다른 객체를 만들지 않고 외부에서 제공받아 사용하는 구조를 만들도록 돕는 것이 DI입니다.

 

 DI가 필요한 이유

예를 들어, 주문 서비스(OrderService)가 할인 정책(DiscountPolicy)을 사용하는 상황입니다.

public class OrderService {
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

위 코드에서는 OrderService가 구체 클래스(RateDiscountPolicy)에 직접 의존하고 있습니다.나중에 다른 할인 정책으로 교체하려면 OrderService 코드를 수정해야 하고,테스트용 모의 객체(Mock)를 주입하는 것도 어렵습니다.

 

즉, 결합도가 높고 유연성이 떨어지는 설계가 됩니다.

 DI 적용 예시

public class OrderService {
    private final DiscountPolicy discountPolicy;

    // 생성자를 통해 외부에서 의존 객체를 주입받는다
    public OrderService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

이제 OrderService는 RateDiscountPolicy에 직접 의존하지 않습니다. 필요에 따라 다른 구현체를 주입하거나 테스트용 객체로 교체할 수 있으며 OrderService 코드 자체는 전혀 수정할 필요가 없습니다.

 

스프링 컨테이너를 사용하면, 이러한 의존 객체 주입을 자동으로 관리해 주므로 개발자가 직접 객체를 생성하고 연결하는 부담을 줄일 수 있습니다.

 

DI 주입 방식

스프링에서는 DI를 여러 방식으로 적용할 수 있습니다.

 

  • 생성자 주입(Constructor Injection)
    • 의존성을 생성자 매개변수로 주입
    • 불변(immutable) 객체 설계에 적합
  • 세터 주입(Setter Injection)
    • 의존성을 세터 메서드로 주입
    • 선택적 의존성에 유용
  • 필드 주입(Field Injection)
    • 의존성을 멤버 변수에 직접 주입
    • 간단하지만 테스트와 확장에는 상대적으로 불리

정리하면 DI는 IoC의 구체적인 구현 방법으로 객체지향 설계를 지키면서 유연하고 테스트 가능한 구조를 만들기 위해 사용됩니다.

 


3. AOP: 관점 지향 프로그래밍과 PSA

여러 메서드에서 동일한 코드가 반복된다면,,,!?? -> AOP가 구원하리라

스프링 삼각형의 두 번째 요소는 AOP(Aspect-Oriented Programming, 관점지향 프로그래밍)입니다.
AOP는 공통 관심사를 분리하고, 핵심 비즈니스 로직을 깔끔하게 유지하도록 돕습니다,


AOP가 필요한 이유

실제 애플리케이션을 개발하다 보면, 공통적으로 발생하는 작업이 있습니다.

  • 트랜잭션 관리
  • 로깅(logging)
  • 보안(Security)
  • 성능 측정

이런 공통 관심사(cross-cutting concern)를 각 비즈니스 로직 코드에 직접 작성하면 코드가 지저분해지고 비즈니스 로직과 기술 코드가 뒤섞여 유지보수가 어렵고 재사용성이 낮은 구조가 됩니다.

 

예를 들어 주문 처리 메서드마다 트랜잭션을 시작하고 커밋/롤백 코드를 반복 작성하면 주문 서비스의 핵심 로직이 가려지고, 수정할 때마다 여러 곳을 손봐야 하는 문제가 발생합니다.

 

AOP로 해결하기

AOP를 적용하면  공통 관심사를 핵심 로직에서 분리할 수 있습니다.

  • 핵심 비즈니스 로직: OrderService, MemberService 등
  • 공통 관심사: 트랜잭션 관리, 로깅, 보안 등

스프링에서는 AOP를 통해 공통 관심사를 별도의 모듈(Aspect)로 정의하고 필요한 시점(join point)에 자동으로 적용할 수 있습니다.

예를들어 트랜잭션 처리시 

@Aspect
@Component
public class TransactionAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            System.out.println("트랜잭션 시작");
            Object result = joinPoint.proceed();
            System.out.println("트랜잭션 커밋");
            return result;
        } catch (Exception e) {
            System.out.println("트랜잭션 롤백");
            throw e;
        }
    }
}

 

OrderService의 메서드에는 트랜잭션 관련 코드가 없지만 Aspect가 자동으로 적용되어 공통 관심사를 처리합니다.이로 인해 핵심 로직은 깔끔하게 유지되고, 공통 기능은 재사용과 관리가 용이해집니다.

 


 

AOP의 기본 개념들

AOP의 제일 큰 난관은 용어가 많아 용어가 어렵기 때문에 용어를 잘 알고 있는것이 중요합니다.

AOP는 핵심 비즈니스 로직과 공통 관심사를 분리하는 구조입니다. 이를 위해 스프링에서는 몇 가지 중요한 개념을 사용합니다.

1. Aspect(애스펙트) – 공통 관심사의 모듈

  • Aspect는 여러 클래스나 기능에 걸쳐 있는 공통 관심사를 모듈화한 단위입니다.
  • 로깅, 실행 시간 측정, 보안, 예외 처리 등 반복적으로 사용되는 기능을 하나의 Aspect로 정의합니다.
  • Aspect 내부에는 Advice(언제, 어떻게 실행할지)와 Pointcut(적용 대상)이 포함됩니다.
  • AOP중에서 가장 많이 활용되는 부분은 @Transactional(트랜잭션 관리),@Cacheable기능입니다.

2. Advice(어드바이스) – 공통 기능의 실행 시점

  • Advice는 Aspect 안에서 정의되는 실제 공통 기능 코드입니다.
  • 메서드 실행 전, 후, 예외 발생 시 등 다양한 시점에 실행될 수 있습니다.
  • 종류:
    • Before: 대상 메서드 실행 전에 수행
    • After: 대상 메서드 실행 후 수행
    • Around: 대상 메서드를 감싸서 실행 전/후 모두 제어 가능
    • AfterReturning: 정상적으로 반환된 후 수행
    • AfterThrowing: 예외 발생 시 수행

3. Join Point(조인 포인트) – Advice가 적용될 수 있는 지점

  • Join Point는 모듈화된 특정 기능이 실행될수 있는 연결 포인트
  • 일반적으로 메서드 호출 지점이 가장 많이 사용됩니다.

4. Pointcut(포인트컷) – 적용 대상 선정

  • Pointcut은 어떤 Join Point에 Advice를 적용할지 선택하는 기준입니다.
  • 예를 들어, com.example.service 패키지 안 모든 메서드, 특정 이름의 메서드 등 조건을 설정할 수 있습니다.

5. Target object(타겟) – 공통 관심사가 적용될 객체

  • Target Object는  실제 핵심 비즈니스 로직이 구현된 객체입니다.
  • AOP에서는 Target 객체를 직접 수정하지 않고, 프록시를 통해 Aspect를 적용합니다.

6. Weaving(위빙) – Aspect 적용 과정

  • Weaving은 Aspect를 Target 객체에 적용하는 과정입니다.
  • 스프링은 주로 런타임 시점에 프록시를 통해 Weaving을 수행합니다.
  • 개발자는 코드 수정 없이 공통 관심사를 적용할 수 있습니다.

4. POJO: 스프링의 단순하고 유연한 객체

스프링 삼각형의 마지막 요소는 POJO(Plain Old Java Object)입니다.
POJO는 말 그대로 그냥 평범한 자바 객체를 의미하며 특별한 프레임워크 의존 없이 작성된 자바 객체를 말합니다.

POJO의 핵심 개념

스프링에서 POJO는 특정 인터페이스를 구현하거나 상속할 필요 없이 순수 자바 객체입니다.비즈니스 로직을 구현하는 핵심 객체는 POJO로 작성하고 스프링이 제공하는 IoC/DI, AOP 등 기능을 적용하면 됩니다.



즉, POJO 자체는 스프링에 종속되지 않고 재사용성과 테스트 용이성을 극대화할 수 있습니다.


 

 

POJO가 중요한 이유

  1. 프레임워크 종속성 최소화
    • POJO는 특정 프레임워크 API에 의존하지 않으므로 나중에 스프링을 사용하지 않는 환경에서도 그대로 활용할 수 있습니다.
  2. 테스트 용이성
    • POJO는 단순 자바 객체이므로 단위 테스트 작성이 쉽습니다.
    • DI와 결합하면 Mock 객체를 주입하여 테스트도 손쉽게 수행할 수 있습니다.
  3. 유연한 설계 가능
    • POJO는 객체 설계 자체에 집중할 수 있게 해줍니다.
    • 핵심 비즈니스 로직을 POJO로 구현하고 필요한 부가 기능(로깅, 트랜잭션 등)은 AOP를 통해 적용하면
      코드가 깔끔하고 유지보수가 쉬워집니다.

POJO 예시 상황

예를 들어, 주문 시스템(Order System)을 생각해봅시다.

public class Order {
    private Long id;
    private String itemName;
    private int quantity;

    // 생성자
    public Order(Long id, String itemName, int quantity) {
        this.id = id;
        this.itemName = itemName;
        this.quantity = quantity;
    }

    // 계산 로직
    public int calculateTotalPrice(int pricePerItem) {
        return pricePerItem * quantity;
    }

    // Getter
    public Long getId() { return id; }
    public String getItemName() { return itemName; }
    public int getQuantity() { return quantity; }
}

 

위 Order 클래스는 POJO입니다. 스프링이나 다른 프레임워크에 의존하지 않고, 순수 자바 코드로만 작성되어 있습니다.
객체 생성, 필드 접근, 계산 로직 등 핵심 비즈니스 로직만 포함되어 있습니다.


POJO를 사용하는 이유

 

  • 프레임워크 의존 최소화
    • Order 클래스는 스프링 없이도 동작합니다.
    • 필요에 따라 다른 환경이나 테스트에서도 그대로 사용할 수 있습니다.
  • 테스트 용이성
    • 단위 테스트 작성이 쉽습니다.
Order order = new Order(1L, "책", 3);
int total = order.calculateTotalPrice(10000);
assert total == 30000;

 

 

DI와 결합하면, 다른 객체(예: 할인 정책)를 주입하여 테스트도 쉽게 수행할 수 있습니다.

핵심 로직 집중

  • POJO는 비즈니스 로직에만 집중할 수 있게 해주고 로깅, 트랜잭션, 보안과 같은 공통 관심사는 AOP로 분리하면 됩니다.

POJO와 스프링의 관계

스프링은 POJO 기반 설계 철학을 지향합니다. 핵심 객체를 POJO로 두고 IoC/DI로 객체를 연결, AOP로 공통 관심사를 적용하면
애플리케이션이 유연하고 유지보수하기 쉬운 구조가 됩니다.

즉, POJO는 스프링에서 비즈니스 로직의 중심, IoC/DI와 AOP는 POJO를 효과적으로 관리하고 확장하도록 돕는 역할을 합니다.

 

 


마치며

 

이번 글에서는 스프링의 핵심 구조인 스프링 삼각형을 중심으로 IoC/DI, AOP, POJO가 각각 무엇이고 왜 필요한지에 대해서 알아보았습니다.

스프링은 단순히 기능을 제공하는 프레임워크가 아니라 객체지향 설계를 제대로 구현하고, 재사용성과 유지보수성을 높이며, 핵심 비즈니스 로직에 집중하도록 돕는 철학을 가지고 있습니다.

  • IoC/DI는 객체 간 결합도를 낮추고, 역할 중심 설계를 가능하게 합니다.
  • AOP는 공통 관심사를 핵심 로직에서 분리하여 코드의 가독성과 재사용성을 높입니다.
  • POJO는 프레임워크에 종속되지 않은 순수 자바 객체로, 핵심 로직을 단순하고 유연하게 구현할 수 있게 합니다.

이 세 가지 요소가 서로 유기적으로 연결되어야 스프링의 진정한 가치를 경험할 수 있습니다.
즉, 스프링 삼각형은 단순한 기술 개념이 아니라, 좋은 객체지향 설계를 실현할 수 있는 프레임워크의 뼈대라고 할 수 있습니다.

 

스프링을 잘  학습하려면  스프링이 지향하는스프링의 핵심 가치를 이해하고, 스프링 스스로가 그 가치를 어떻게 적용해서 만들어져 있는지를 이해하는 것이 중요하다고 생각됩니다.

 

 

 

저작자표시 비영리 (새창열림)

'스프링' 카테고리의 다른 글

Spring Validation 완전 정복 - 올바른 유효성 검증 전략  (0) 2026.01.29
스프링에서 스코프(Scope)란 무엇인가?  (0) 2026.01.19
스프링 배치란?  (0) 2025.12.11
@Data 사용을 지양 해야 하는 이유  (0) 2025.10.22
@Data 사용을 지양해야 하는 이유  (0) 2025.04.21
'스프링' 카테고리의 다른 글
  • Spring Validation 완전 정복 - 올바른 유효성 검증 전략
  • 스프링에서 스코프(Scope)란 무엇인가?
  • 스프링 배치란?
  • @Data 사용을 지양 해야 하는 이유
깊은바다속꼬북이
깊은바다속꼬북이
  • 깊은바다속꼬북이
    CodeBlossom
    깊은바다속꼬북이
  • 전체
    오늘
    어제
    • 분류 전체보기 (53) N
      • 라이징 캠프 (4)
      • 객채지향 개발론 (3)
      • 스프링 (10) N
      • 네트워크 (2)
      • 자바 (16)
      • 자료구조 (3)
      • 운영체제 (0)
      • 데이터베이스 (4)
      • 디자인패턴 (7)
      • JSP (1)
      • 개발 알쓸신잡 (2)
      • 일반 교양 (0)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

    java 버전별 특징
    mockito라이브러리
    한국어 검색
    junnit5프레임워크
    프로그램밍 언어
    개발자 철학
    jit-compiler
    jvm 클래스 로더
    디자인패턴
    템플릿 메서드 패턴(Template Method Pattern)
    JUnnit5
    spring
    자바 Socket 클래스
    싱글턴 패턴(Singleton Pattern)
    객체지향
    JVM
    자료구조
    백엔드
    GC
    MySQL 파서
    디자인 패턴
    어댑터 패턴(Adapter Pattern)
    트랜잭션 전파레벨
    MySQL 옵티마이저
    java data area
    전략 패턴(Strategy Pattern)
    개발 교훈
    개발 철학
    스프링
    java
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
깊은바다속꼬북이
스프링을 스프링답게 만드는 진짜 이유: IoC·AOP·PSA 삼각형의 정체
상단으로

티스토리툴바