당니의 개발자 스토리

Spring의 DI, AOP, PSA, POJO 이해하기 본문

Java, Spring

Spring의 DI, AOP, PSA, POJO 이해하기

clainy 2026. 5. 6. 11:50

Spring을 공부하다 보면 DI, AOP, PSA, POJO라는 단어가 계속 나온다.

처음 보면 당황스럽고 이해하기가 어렵다..

 

Spring은 개발자가 평범한 자바 객체로 비즈니스 로직을 작성하게 하고,

객체 사이의 관계를 알아서 연결해주고,

반복되는 공통 기능은 따로 분리해주고,

기술이 바뀌어도 코드를 크게 흔들리지 않게 도와준다.

 

이 흐름을 만드는 개념이 DI, AOP, PSA, POJO다.

 

이제 하나씩 알아보자!!


DI란?

DI는 Dependency Injection의 약자다.

즉, 의존성 주입이라고 한다.

 

여기서 먼저 의존성이라는 말을 이해해야 한다.

의존성이란 어떤 객체가 다른 객체 없이는 동작할 수 없는 관계를 말한다.

 

예를 들어 자동차는 엔진이 있어야 움직일 수 있다.

그러면 자동차는 엔진에 의존한다고 볼 수 있다.

 

코드로 보면 이런 느낌이다.

public class Car {

    private Engine engine = new Engine();

    public void drive() {
        engine.start();
        System.out.println("자동차가 출발합니다.");
    }
}

이 코드에서 Car는 Engine을 직접 만들고 있다.

new Engine()을 Car 클래스 안에서 직접 호출하고 있다.

 

겉으로 보면 별문제가 없어 보인다.

하지만 이렇게 작성하면 Car는 Engine이라는 객체에 강하게 묶인다.

 

만약 나중에 Engine을 다른 종류로 바꾸고 싶으면 어떻게 될까?

예를 들어 일반 엔진이 아니라 전기 엔진을 쓰고 싶다면 Car 코드 안쪽을 직접 수정해야 할 수 있다.

 

객체 하나를 바꾸려고 했는데,

그 객체를 사용하는 코드까지 같이 바뀌는 상황이 생긴다.

이런 구조는 유지보수하기 불편하다.

 

DI는 이 문제를 줄이기 위한 방식이다.

필요한 객체를 클래스 안에서 직접 만들지 않고,

외부에서 알아서 넣어주는 방식이다.

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("자동차가 출발합니다.");
    }
}

이번에는 Car가 new 를 통해 Engine을 직접 생성하지 않는다.

 

코드를 보면, 생성자를 통해 외부에서 Engine을 받는다.

일반 엔진이 들어오든,

전기 엔진이 들어오든,

Car는 drive()라는 자기 역할에 집중할 수 있다.

 

이렇게 필요한 객체를 외부에서 넣어주는 것이 의존성 주입이다.

이걸 Spring이 해준다!

 

예를 들어 회원가입 기능을 처리하는 MemberService가 있고,

DB에 회원 정보를 저장하는 MemberRepository가 있다고 해보자.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void join(Member member) {
        memberRepository.save(member);
    }
}

MemberService는 MemberRepository가 필요하다.

하지만 직접 new MemberRepository()를 하지 않고,

생성자로 MemberRepository를 받아서 사용한다.

 

Spring은 실행될 때 필요한 MemberRepository 객체를 찾아서 MemberService에 넣어준다.

 

개발자는 객체를 직접 만들고 연결하는 코드보다,

회원가입 로직 자체에 더 집중할 수 있다.

DI를 쓰면 객체 사이의 결합도를 낮출 수 있다.

 

결합도가 높다는 말은 객체끼리 너무 강하게 묶여 있다는 뜻이다.

하나를 바꾸면 다른 코드까지 많이 바뀌는 구조다.

 

반대로 결합도가 낮으면 객체끼리 느슨하게 연결되어 있다.

구현체가 바뀌어도 사용하는 쪽 코드가 덜 흔들린다.

 

그래서 DI를 사용하면 유지보수가 쉬워진다.

테스트하기도 편해진다.

왜? 원문 코드에서 그냥 new로 박아넣었으면 test 할 때도 불편한데

DI로 주업받는 애면 test 할 때 그냥 MockDB만 내가 박아주면 되니까 편해진다.

MemberRepository fakeRepository = new FakeMemberRepository();

MemberService memberService = new MemberService(fakeRepository);

이런 식으로 의존성을 외부에서 넣을 수 있으면, 상황에 따라 필요한 객체를 바꿔 끼우기 쉽다.


AOP란?

AOP는 Aspect Oriented Programming의 약자다.

우리말로는 관점 지향 프로그래밍이라고 한다.

 

AOP는 여러 기능에 반복해서 들어가는 공통 코드를 따로 빼놓고,

필요한 곳에 자동으로 적용하는 방식이다.

 

예를 들어 쇼핑몰 서비스에 회원가입, 로그인, 상품 주문, 결제, 환불 기능이 있다고 해보자.

그런데 이 기능들에는 공통으로 필요한 코드도 있다.

로그 남기기, 권한 검사, 실행 시간 측정, 트랜잭션 처리, 예외 처리 같은 코드다.

 

이런 코드들은 특정 기능 하나에만 필요한 것이 아니다.

여러 기능에 걸쳐 반복되는데, 이런 것을 횡단 관심사라고 한다.

 

AOP가 없으면 코드가 이렇게 섞일 수 있다.

public void order() {
    System.out.println("주문 시작 로그");

    checkAuth();

    // 주문 로직
    saveOrder();
    decreaseStock();

    System.out.println("주문 종료 로그");
}

이 코드에서 실제 주문 기능은 주문을 저장하고 재고를 줄이는 부분이다.

그런데 주변에 로그, 권한 검사 같은 공통 코드가 같이 섞여 있다.

 

비즈니스 로직을 보려고 했는데 공통 기능 코드가 계속 끼어 있어서

정작 중요한 흐름이 잘 안 보이는 상황이 생긴다.

 

AOP를 사용하면 이런 공통 기능을 따로 분리할 수 있다.

public void order() {
    saveOrder();
    decreaseStock();
}

주문 메서드 안에는 주문에 필요한 로직만 남긴다.

로그, 권한 검사, 실행 시간 측정 같은 공통 기능은 따로 빼서 필요한 시점에 적용한다.

 

Spring에서 자주 보는 AOP 예시@Transactional이다.

@Transactional
public void order() {
    saveOrder();
    decreaseStock();
    savePayment();
}

이 코드에는 트랜잭션 시작, commit, rollback 코드가 직접 보이지 않는다.

하지만 Spring은 이 메서드 실행 전후를 감싸서 트랜잭션 기능을 적용한다.

이게 AOP 이다.

 

AOP는 공통 기능을 따로 분리하고,

필요한 곳에 자동으로 적용하는 방식이다.

덕분에 개발자는 실제 비즈니스 기능에 더 집중할 수 있다.


PSA란?

PSA는 Portable Service Abstraction의 약자다.

의미는 복잡한 기술을 숨기고,

개발자가 일관된 방식으로 사용할 수 있게 해주는 것이다.

여기서 중요한 말은 추상화다.

 

추상화는 복잡한 내부 구현을 숨기고,

사용하는 쪽에서는 단순한 방식으로 접근할 수 있게 만드는 것이다.

 

자동차를 운전할 때를 생각해보면 쉽다.

우리는 자동차 기종마다 엔진 내부가 어떻게 동작하는지 전부 알지 않아도 된다.

 

엑셀을 밟으면 앞으로 가고,

브레이크를 밟으면 멈춘다는 방식만 알면 된다.

내부 동작은 복잡하지만,

운전자는 단순한 사용 방법만 알고도 자동차를 사용할 수 있다.

 

PSA도 비슷하다.

Spring은 여러 기술의 복잡한 차이를 감추고,

개발자가 비슷한 방식으로 사용할 수 있게 도와준다.

 

대표적인 예가 트랜잭션 처리다.

DB 접근 기술에는 여러 가지가 있다.

 

JDBC를 직접 쓸 수도 있고,

MyBatis를 쓸 수도 있고,

JPA를 쓸 수도 있다.

각 기술의 내부 구현은 다르다.

 

MyBatis에서 트랜잭션을 처리하는 방식과 JPA에서 트랜잭션을 처리하는 방식은 내부적으로 다를 수 있다.

그런데 Spring에서는 개발자가 보통 같은 방식으로 트랜잭션을 사용할 수 있다.

@Transactional
public void save() {
    // DB 저장 로직
}

개발자는 구체적으로 내부 구현을 알 필요 없이, 그냥 @Transactional을 사용하면 끝이다

 

이게 PSA의 장점이다.

기술이 바뀌어도 사용하는 코드가 크게 흔들리지 않는다.

public interface TxManager {
    void begin();
    void commit();
    void rollback();
}

이 인터페이스를 기준으로 여러 구현체가 있을 수 있다.

public class MyBatisTxManager implements TxManager {

    public void begin() {
        // MyBatis 방식 트랜잭션 시작
    }

    public void commit() {
        // MyBatis 방식 커밋
    }

    public void rollback() {
        // MyBatis 방식 롤백
    }
}
public class JpaTxManager implements TxManager {

    public void begin() {
        // JPA 방식 트랜잭션 시작
    }

    public void commit() {
        // JPA 방식 커밋
    }

    public void rollback() {
        // JPA 방식 롤백
    }
}

사용하는 쪽에서는 구체적인 구현체를 매번 신경 쓰지 않아도 된다.

TxManager라는 추상화된 타입을 기준으로 사용할 수 있다.


POJO란?

POJO는 Plain Old Java Object의 약자다.

그냥 쉽게 생가갛면, 평범한 자바 객체라는 뜻이다.

 

특정 프레임워크나 라이브러리에 강하게 묶이지 않은 평범한 자바 클래스다.

예를 들어 이런 클래스는 POJO에 가깝다.

public class Member {

    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

이 클래스는 특별한 프레임워크 클래스를 상속하지 않는다.

특정 인터페이스를 반드시 구현하지도 않는다.

 

그냥 자바 문법으로 만든 평범한 객체다.

반대로 어떤 프레임워크를 사용하기 위해 반드시 특정 클래스를 상속해야 한다면,

그 코드는 특정 기술에 강하게 묶이게 된다.

 

예를 들면 이런 느낌이다.

public class MemberService extends SomeFrameworkClass implements SomeFrameworkInterface {

}

이렇게 되면 MemberService는 SomeFrameworkClass나 SomeFrameworkInterface에 강하게 의존하게 된다.

 

프레임워크를 바꾸거나,

테스트를 하거나,

단순한 자바 코드로 실행해보고 싶을 때 불편해질 수 있다.

 

Spring은 가능하면 평범한 자바 객체를 그대로 사용할 수 있도록 설계되었다.

 

비즈니스 로직을 구현할 때는 그냥 평범한 객체를 만들고,

Spring이 그 객체를 관리하면서 필요한 기능을 붙여주는 방식이다.

 

예를 들어 이런 Service 클래스가 있다고 해보자.

@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void order(Order order) {
        orderRepository.save(order);
    }
}

이 클래스는 기본적으로 자바 클래스다.

 

OrderService는 주문이라는 비즈니스 로직을 담고 있다.

Spring은 이 객체를 관리하면서 DI로 OrderRepository를 넣어준다.

 

그리고 @Transactional을 보고 AOP 방식으로 트랜잭션을 적용해준다.

DB 접근 기술이 달라져도 PSA를 통해 일관된 방식으로 사용할 수 있게 도와준다.

 

즉, POJO를 기반으로 DI, AOP, PSA가 적용되는 구조다.


그래서 Spring의 주요 특징을 POJO 기반의 DI, AOP, PSA라고 말한다.

 

POJO의 장점은 코드가 단순하다는 것이다.

특별한 서버나 복잡한 환경이 없어도 순수한 자바 코드만으로 기능을 테스트할 수 있기 때문이다.

 

예를 들어 계산 기능이 있는 클래스라면 Spring 서버를 띄우지 않고도 테스트할 수 있다.

public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }
}

이런 코드는 그냥 객체를 만들고 메서드를 호출하면 된다.

Calculator calculator = new Calculator();

int result = calculator.add(10, 20);

이처럼 POJO는 자유롭고 유연하다.

 

비즈니스 로직을 구현하기 위해 불필요하게 복잡한 규칙을 따르지 않아도 된다.

그래서 개발자는 기능 구현에 더 집중할 수 있다.

 

DI는 필요한 객체를 외부에서 넣어주는 방식이다.
AOP는 여러 곳에 반복되는 공통 기능을 분리해서 필요한 곳에 적용하는 방식이다.
PSA는 복잡한 기술 차이를 추상화해서 일관된 방식으로 사용할 수 있게 하는 방식이다.
POJO는 특정 프레임워크에 강하게 묶이지 않은 평범한 자바 객체다.