본문 바로가기

spring

스프링 3대 기술

 

 

 

 

 

IOC / DI

 

 

 

IoC(Inversion of Control, 제어의 역전)

 

IoC는 객체의 생성부터 생명주기 관리까지 모든 객체에 대한 제어권을 개발자가 아닌 스프링 컨테이너가 대신 관리하는 것을 말한다. 전통적인 방식에서는 개발자가 new 키워드로 직접 객체를 만들고 필요한 의존성을 연결했지만, IoC에서는 컨테이너가 객체를 생성하고 주입하며 전체 흐름을 관리한다.

 

즉, 애플리케이션 코드가 필요한 객체를 직접 호출하지 않고, 컨테이너가 흐름에 맞게 객체를 대신 공급하는 구조다. 이 때문에 흔히 헐리우드 원칙(Don’t call us, we’ll call you)이라고도 불린다.

 

스프링에서 IoC 컨테이너는 객체(Bean)를 생성하고, 의존성을 연결하며, 생명주기를 관리한다. 따라서 개발자는 객체 생성과 조립을 신경 쓰지 않고 비즈니스 로직에만 집중할 수 있다.

 

 


 

DI(Dependency Injection, 의존관계 주입)

 

 

DI란 객체 간의 의존관계를 외부에서 주입하는 것을 의미한다. 의존한다는 것은 A가 B를 사용할 때 B가 바뀌면 A에도 영향을 준다는 관계를 뜻한다. 예를 들어 요리사(A)가 레시피(B)에 의존한다고 하면, 레시피가 바뀔 때마다 요리사의 행동도 수정되어야 한다. 이처럼 레시피가 바뀌면 요리사가 영향을 받는 것을 “의존한다”라고 표현할 수 있다.

 

DI의 핵심은 객체 내부에서 직접 필요한 객체를 생성하거나 연결하지 않고, IoC 컨테이너가 객체를 대신 생성하고 그 의존성을 주입해준다는 점이다. 이로써 객체는 구체적인 구현체에 묶이지 않고, 외부에서 어떤 구현체를 주입받을지 유연하게 변경할 수 있다. 보통 인터페이스를 두어 클래스 레벨에서는 구현체에 고정되지 않게 설계하며, 실제로 어떤 구현체가 들어올지는 런타임 시점에 IoC 컨테이너가 결정한다.

 

의존성을 주입하는 방법에는 크게 세 가지가 있다. 가장 먼저, 생성자 주입 방식이 있다. 이 방법은 객체가 생성될 때 생성자를 통해 필요한 의존성을 함께 받아오는 방식이다.

 

생성자 주입의 가장 큰 장점은 객체가 만들어지는 시점에 필수 의존성이 반드시 주입되도록 강제할 수 있다는 점이다. 덕분에 불완전한 상태의 객체가 만들어지는 것을 방지할 수 있고, 의존성이 한 번 설정되면 이후 변경되지 않으므로 불변성도 자연스럽게 확보된다 그래서 스프링에서는 생성자 주입을 가장 권장하는 방식으로 꼽는다.

 

두 번째는 Setter 주입이다. 객체가 생성된 이후에 setter 메서드를 통해 의존성을 주입하는 방식이다. 이 방법은 선택적인 의존성을 처리할 때나 런타임 중에 의존성을 교체해야 하는 상황에서 유용하다. 하지만 필수 의존성까지 setter로 주입하게 되면, 개발자가 주입을 깜빡하거나 누락할 수 있기 때문에 런타임 오류가 발생할 위험이 있다.

 

마지막으로 필드 주입 방식이 있다. 이는 필드에 직접 어노테이션(@Autowired) 등을 붙여 의존성을 주입하는 방법이다. 코드가 간단하다는 장점이 있지만, IOC 컨테이너가 없으면 객체를 생성하기 어렵고 테스트 코드 작성에도 제약이 많다. 또한 외부에서 의존성을 변경하거나 주입 과정을 명시적으로 드러낼 수 없기 때문에 유지보수성도 떨어진다. 이러한 이유로 실무에서는 필드 주입은 지양하는 것이 좋다고 여겨진다.

 


 

DIP(Dependency Inversion Principle, 의존 역전 원칙)

 

 

DI를 설명할 때 자주 등장하는 개념이 바로 DIP이다. DIP는 객체가 구체 클래스가 아니라 추상화(인터페이스)에 의존해야 한다는 원칙이다. DI는 DIP 없이도 동작할 수 있지만, DIP를 적용하면 DI의 장점을 극대화할 수 있다.

 

예를 들어, 요리사 클래스가 특정 “한식 레시피” 구현체에 직접 의존하면 교체가 어렵다. 반면 “레시피 인터페이스”에 의존하게 만들면, IoC 컨테이너가 한식, 양식, 중식 레시피 중 어떤 구현체를 넣어줄지는 런타임에 자유롭게 바꿀 수 있다. 즉, DI는 DIP 원칙을 따르면 더 좋은 설계가 되며, IoC 컨테이너는 이러한 의존성 관리 과정을 자동으로 처리해준다.

 

 


 

AOP

 

 

AOP (Aspect Oriented Programming, 관점 지향 프로그래밍)

 

AOP는 관점 지향 프로그래밍이라고 한다. 이는 객체 지향 프로그래밍(OOP)의 한계를 보완하기 위해 등장한 개념으로, 여러 클래스에 흩어져 있는 공통된 기능을 한 곳으로 모듈화하여 관리할 수 있도록 해준다. 예를 들어 로깅, 보안, 트랜잭션 관리와 같은 기능은 비즈니스 로직과는 별개이지만, 대부분의 서비스 전반에 걸쳐 반복적으로 필요하다. 이러한 공통된 기능을 AOP에서는 횡단 관심사라고 부르며, 이를 별도의 단위로 분리해 관리하는 것이 AOP의 핵심이다.

 

관점 지향이라는 말은 프로그램을 바라보는 관점을 핵심 로직과 부가 로직으로 구분한다는 의미다. 핵심 로직은 애플리케이션이 실제로 수행해야 할 비즈니스 기능이고, 부가 로직은 이를 둘러싼 로깅, 보안, 트랜잭션과 같은 기능이다.

 

 

OOP에서 모듈화의 기본 단위가 클래스라면, AOP에서 모듈화의 단위는 관점이다. 즉, AOP는 관점을 기준으로 프로그램을 나누어, 핵심 로직과 부가 로직을 서로 깔끔하게 분리할 수 있도록 한다.

 

스프링에서 AOP는 주로 런타임 시점에 적용됩니다. 즉, 실제 요청이 들어와 메서드가 실행되기 전후에 부가 로직이 프록시를 통해 함께 실행되는 방식입니다. 스프링 AOP는 기본적으로 프록시 기반으로 동작하기 때문에, 마치 메서드를 감싸는 형태로 동작하며 결과적으로 메서드 단위에서만 부가 기능을 적용할 수 있습니다. 그리고 이 프록시는 스프링 컨테이너가 관리하는 빈(Bean)에 대해서만 생성되므로, AOP 역시 빈을 대상으로 동작하게 됩니다.


AOP관련 용어

  • Aspect: 공통 기능을 모듈화한 단위 (예: 로깅, 보안)
  • Target: Aspect가 적용될 객체
  • Advice: 실제로 실행되는 부가기능 코드
  • Join Point: 부가기능을 끼워 넣을 수 있는 지점 (스프링은 메서드 실행 시점)
  • Pointcut: 많은 Join Point 중에서 “어떤 메서드에 부가기능을 적용할지”를 골라내는 조건 예를 들어, “Service 패키지의 모든 메서드”

 

스프링에서의 Advice 시점

  • @Before: 메서드 실행 전
  • @After: 메서드 실행 후 (성공/실패 무관)
  • @AfterReturning: 메서드 정상 종료 후
  • @AfterThrowing: 메서드 예외 발생 시
  • @Around: 메서드 실행 전후를 모두 제어

AOP의  활용

 

대표적으로 로깅 기능을 AOP로 분리하면, 모든 서비스 메서드마다 중복되는 로깅 코드를 작성할 필요 없이 한 곳에서 관리할 수 있다. 또한 트랜잭션 처리도 AOP로 분리하여 핵심 비즈니스 코드와 독립적으로 관리할 수 있으며, 이는 코드 가독성과 안정성을 높인다. 마지막으로 보안 기능 역시 AOP로 적용할 수 있다. 특정 메서드 호출 전 인증이나 권한 체크를 일괄적으로 수행하도록 설정하면, 보안 관련 코드의 중복을 줄이고 시스템 전반의 일관성을 유지할 수 있다.

 

 


 

PSA

 

 

PSA (Portable Service Abstraction, 서비스 추상화)

 

PSA는 환경의 변화와 관계없이 일관된 방식으로 기술에 접근할 수 있도록 해주는 추상화 구조다. 쉽게 말해, 개발자가 특정 기술의 세부 API에 직접 의존하지 않고, 스프링이 제공하는 공통 인터페이스를 사용함으로써 코드의 이식성과 유지보수성을 높여준다. PSA의 목적은 “잘 만든 인터페이스”를 통해 다양한 기술을 교체하거나 확장할 때 코드 변경을 최소화하는 데 있다.

 


 

PSA의 대표적인 예시 : Spring Web MVC

 

원래 Servlet을 직접 사용할 때는 HttpServlet을 상속받고 doGet()이나 doPost() 메서드를 오버라이딩해야 했다. 하지만 스프링 MVC를 사용하면 @Controller와 @GetMapping, @PostMapping과 같은 어노테이션만으로 웹 요청을 처리할 수 있다. 개발자는 복잡한 서블릿 API를 몰라도 요청과 응답을 간단하게 다룰 수 있는 것이다.

 

// 과거 Servlet 방식
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().write("Hello Servlet");
    }
}

// Spring MVC 방식
@Controller
public class HelloController {
    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello Spring MVC";
    }
}

 

 

위 예시에서 볼 수 있듯이, 개발자가 직접 서블릿 API를 다루는 대신, 스프링이 제공하는 PSA 계층을 통해 훨씬 단순한 코드로 동일한 기능을 구현할 수 있다. 더 나아가 실행 환경을 톰캣에서 Netty로 변경하더라도, 코드를 수정할 필요 없이 spring-boot-starter-web을 spring-boot-starter-webflux로 바꾸는 것만으로 손쉽게 전환할 수 있다. 이는 서비스 추상화가 제공하는 강력한 장점이다.