I'm pine thank you and you?

[Spring] Spring 핵심요소 - IoC, DI, AOP, PSA 본문

CS

[Spring] Spring 핵심요소 - IoC, DI, AOP, PSA

SollyJ 2023. 7. 10. 22:20

IoC

Inversion of Control (제어의 역전)

클래스는 개발자가 만들지만 인스턴스의 생성 및 수명 주기 관리는 Framework나 Web Container가 하는 방식

💡 제어를 Spring 컨테이너로 역전 시키기 때문에 제어의 역전이라 부르며, Spring ContainerIoC Container라고 부르기도 한다.

IoC 컨테이너에 등록만 되면, 컨테이너는 등록된 Bean 의 [생성 - 의존성 설정 - 초기화 - 소멸] 과 같이 생명주기를 관리한다.

👉 비유를 하자면 정말 바쁜 사업가(개발자)가 가사 도우미(Spring Container)에게 집안일(개발적이지 않은 것)을 시키고, 본인은 본업(기획, 설계, 프로그래밍, 개발적인 것)에 더 집중 할 수 있는 것과 같다.

예시

// 의존성 주입X
// "내껀 내가 만들어 쓸게"
class UserController {
    private UserRepository repository = new UserRepository();   // 생성자를 필요할때마다 불러 써야한다.
}

// 의존성 주입O
// "내가 필요하다는데 누군가가 생성해서 주겠지"
class UserController {
    private UserRepository repo;   // 참조변수를 선언하고

    public UserController(UserRepository repo) {   // 바로 사용 가능
        this.repo = repo;
    }
}

class UserControllerTest{
    @Test
    public void create() {
          UserRepository repo = new UserRepository();   // 생성한 것을
        UserController controller = new UserController(repo);   // 바로 주입 가능하기 때문에 테스트하기도 용이
    }
}

목적

  • 구현(개발자)과 수행(Framework)의 분리

  • 역할과 관심을 분리하여 응집도를 높이고 결합도는 낮췄기 때문에, 다른 모듈과의 결합이나 모듈 변경에 유연하게 대처할 수 있다.

    • filter의 예

      Java Stream의 map, reduce, filter, foreach도 제어의 역전 패턴이 적용돼있다.

        // 10보다 큰 수를 필터링하는 함수를 직접 구현하면, 필터링 로직이 조금이라도 바뀌면 함수자체를 수정***해야한다. (결합도 증가)
        result = filterGreaterThan10(nums);
      
        // stream내의 filter함수는 필터링 기능만 제공하고, 어떻게 필터링 할지는 filter 함수 사용자에게 맡기고 있다.
        // 따라서 필터링 로직에 변화가 생겨도 filter함수는 그대로 (결합도 낮음)
        result = nums.filter(n -> n > 10);
  • 컴포넌트 기반 개발 방법론 실현

    컴포넌트를 조합해 재사용함으로써 개발 생산성과 품질을 높이고 시스템 유지보수 비용을 최소화할 수 있는 개발 방법론

  • DI를 하기 위해서!!


DI

Dependency Injection (의존성 주입)

의존성
인스턴스 내부에서 다른 클래스의 인스턴스를 사용하는 것
“A가 B를 의존한다” 에 대해서 “의존 대상 B가 변하면, 그것이 A에 영향을 미친다”
이는 한 클래스가 바뀔 때 다른 클래스가 영향을 받는 상태가 의존 관계
주입
사용하려는 인스턴스를 전달하는 것


💡 인스턴스 내부에서 다른 클래스의 인스턴스를 직접 생성하지 않고 외부에서 전달받아서 주입

Spring 관점에서 DI

각 객체 사이의 의존성을 Spring Container가 개발자에 의해 정의된 Bean 등록 정보를 바탕으로 자동 주입 해주는 기능이다.

😃 우리가 Spring Boot로 웹을 구현할 때 Controller 나 Service를 new 키워드를 통해 Controller에서 직접 생성하지 않고, @Component, @Service, @Repository, @Controller 등의 어노테이션을 붙이면 Spring 런타임에 스캔을 통해 개발자가 정의한 의존성 정보를 자동으로 Bean 설정 정보에 등록을 하여 DI가 적용된다.

🙂 Spring에선 .xml나 ApplicationContext에 Bean을 일일히 설정해주어야한다.

목적⭐️

DI는 IoC에서 파생된 개념이기 때문에 IoC 목적과 같다.

  • 관심사 분리

    객체를 생성하는 로직 / 객체의 인스턴스를 사용하는 로직을 분리

    코드의 가독성과 재사용성이 높아지고 유지보수 용이해진다.

  • 인터페이스를 이용한 DI 구현

의존성 주입 방식1️⃣ - 생성자 주입

public class AppleComStuff {
        private String mac;
        private String iPad;
        private String iPhone;
        private String watch;
        private String airpods;

        // 매개변수가 mac과 iPad인 생성자
        public AppleCom(String mac, String iPad) {
            this.mac = mac;   // 생성자를 호출할때 의존성을 주입받는다.
            this.iPad = iPad;
        }

        // 매개변수가 mac과 iPad, iPhone인 생성자
        public AppleCom(String mac, String iPad, String iPhone) {
            this.mac = mac;
            this.iPad = iPad;
            this.iPhone = iPhone;
        }
}

의존성 주입 방식2️⃣ - Setter 주입

public class AppleComStuff {
        private String mac;
        private String iPad;
        private String iPhone;
        private String watch;
        private String airpods;

        public void setMac(String mac) {
            this.mac = mac;
        }

        public void setIPad(String iPad) {
            this.iPad = iPad;
        }
}

의존성 주입 방식3️⃣ - 필드 주입

@Autowired 어노테이션을 사용해 필드에 의존성을 주입하는 방법

@Autowired 어노테이션은 해당 필드의 타입에 맞는 Bean을 찾아서 주입하는 역할을 한다.

@Autowired
private MemberRepository memberRepository;

의존성 주입 방식4️⃣ - 인터페이스 주입

// 인터페이스
interface AppleCom {
        void buy();
        void sell();
}

// 인터페이스 구현체
public class Mac implements AppleCom {
        @Override
        public void buy() {
            ~ 함수 내용 구현 ~
        }

        @Override
        public void sell() {
            ~ 함수 내용 구현 ~
        }
}

// 인터페이스 구현체
public class IPad implements AppleCom {
        @Override
        public void buy() {
            ~ 함수 내용 구현 ~
        }

        @Override
        public void sell() {
            ~ 함수 내용 구현 ~
        }
}

public class AppleComStuff {
        private String stuff;

        // 생성자 주입
        public AppleComStuff(String stuff) {
            this.stuff = stuff;
        }

        public void buyStuff() {
            stuff.buy();   // 상황마다 맞는 의존성을 주입 받는다. == 객체마다 맞는 메서드를 주입받는다.
        }
}

이 방법은 의존성을 주입하는 메서드를 포함하는 인터페이스를 작성하고, 인터페이스의 구현체를 통해 애플리케이션 실행 시점에 의존성을 주입받는다.

Setter 주입처럼 메서드를 외부에서 호출해야 하는 것은 유사하나, 메서드 호출이 자유로운 Setter 주입과는 다르게 @Override를 통해 메서드 구현을 강제할 수 있다는 차이점이 있다.

인터페이스를 통해 느슨한 의존성 주입을 구현할 수 있게 된다.

  • 느슨한 의존성 주입

    인터페이스가 있고 실제 구현체 클래스를 통해 상황마다 맞는 의존성을 주입 받을 수 있게되며, 객체지향의 특징인 다형성을 향상시킬 수 있게 된다는 개념

IoC와 DI의 관계⭐️

DI는 IoC라는 원칙을 지키기 위한 다양한 디자인 패턴 중 하나

AppleComStuff가 사용할 Stuff가 Mac일지 IPad일지 개발자가 설정 해두면, Spring 런타임에 스캔하여 IPad 객체를 생성해 AppleComStuff 생성자의 인자로 주입하게되는 것이다.

그리고 IPad 클래스에서 구현한 buy() 메서드를 주입받는다.

즉, Spring이 Stuff(Mac, IPad, …) 객체를 생성하여 의존 관계를 맺어주는 것을 IoC, 그 과정에서 IPad 객체를 AppleComStuff의 생성자를 통해 주입해주는 것을 DI


AOP

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

프로그램을 관점에 따라 나누어 구현한 후 결합하는 방식

💡 어떤 로직을 기준으로 Business Logic(업무처리)Common Concern(공통 관심사항)을 나누어서 모듈화 하는 것

  • 모듈?

    독립적으로 실행가능한 작업의 단위

    하나의 모듈에 모든 내용을 전부 작성하면 가독성이 떨어지고 재사용성이 떨어진다.

    역할별 모듈을 나누는 것이 중요!

    모듈화 대표적인 예 ➡️ MVC

    Model, View, Controller(모듈)가 모인 MVC는 하나의 Architecture(구조) 이다.

💁‍♀️ 우선 Middleware가 무엇인지 살펴보자.

사용자의 요청 후 서버가 요청을 처리하고 응답을 전송하는 시스템에서 요청을 처리하기 전이나 후에 동작할 내용을 수행하는 객체

업무 처리와 관련된 핵심 비즈니스 로직이 아닌, 필터링이나 변환같은 해당 요청과 직접적인 관련이 없는 공통 로직

  • 필터링

    필터링은 유효성 검사 작업과 로그인 확인 작업이 대표적이다.

  • 변환

함수로 처리하는 것과 뭐가 다른가?

함수는 Common Concern과 Business Logic이 한곳에 존재하지만,

미들웨어에서는 Common Concern만 존재한다.

Common Concern과 Business Logic이 분리되기 때문에 유지보수가 용이

🤔 그렇다면 모듈화는 왜 필요할까?

Business Logic을 예를 들어보면 주문 로직, 주문 취소, 결제 로직 등을 생각해볼 수 있다.

그리고 Common Concern은 공통적으로 적용되는 관심사항으로 시스템 로깅이나 보안 등과 관련된 기능들이 될 수 있다.

그런데, Business Logic과 Common Concern에 대한 코드나 로직이 함께 작성되어 있다면, 중복 코드가 작성 될 수 있고, 후에 수정을 하려고 할 때도 찾기가 어려워진다.

또한, 공통으로 적용되어 있으므로 일일히 수정해주어야한다는 번거로움이 있다.

중복 코드 문제를 해결하기 위해서는 공통 관심사항에 대한 코드들을 별도의 객체로 분리하여, 비즈니스 로직에서 공통 관심사항을 구현한 코드를 호출하는 방식으로 동작하도록 변경해야 한다.

이는 결국 AOP의 원칙을 따라야 한다는 것이며, 애플리케이션에 전반적으로 적용되어 있는 공통 관심사항들을 핵심 비즈니스 로직으로부터 분리해내기 위해서 AOP가 필요하다는 것을 알 수 있다.

🔁 OOP와 AOP의 관계

spring 공식 문서

AOP는 OOP를 보완하는 방법이다. (OOP에서 확장된 개념이라고 봐도 될 것 같다.)

OOP는 클래스 단위로 모듈화를 하는 반면, AOP는 관점(공통 vs 비즈니스)을 중심으로 모듈화를 한다.

따라서 AOP는 트랜잭션 관리, 필터링, 변환 따위를 모듈화 할 수 있다는 것

(잘 이해한게 맞을까?🥲)

🔠 관련 용어

  • Aspect

    공통 관심사를 모듈화 한 것을 의미한다.

    또한, AOP의 기본 모듈로써 애플리케이션의 핵심 기능을 가지고 있지는 않지만 애플리케이션을 구성하기 위한 중요한 요소이다.

  • Target Object

    Target은 Aspect 즉, 공통 관심사를 적용할 대상을 뜻한다.

    클래스나 메서드가 이에 해당된다.

  • Advice

    Advice란 Target에게 제공할 공통관심사가 구현된 모듈이다.

    실질적으로 어떤 부가 기능을 해야 할지를 정의하고 있는 구현체이다.

  • JoinPoint

    JoinPoint란 애플리케이션이 실행되었을 때, Advice가 적용될 위치를 의미한다.

    이는 메서드 진입 지점이나 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용이 가능하다.

  • PointCut

    Pointcut이란 Advice에 적용할 JoinPoint를 선별하는 작업이나 기능을 정의한 모듈이다.

    즉, JoinPoint에 대한 상세 스펙을 정의한 것으로 구체적으로 Advice가 실행될 지점을 정할 수 있다.


PSA

Portable Service Abstraction

해석하면,

휴대용 서비스 추상화?

바꿔끼기 좋은 서비스 추상화?

말이 너무 어렵다.

인프런의 백기선님은 잘 만든 인터페이스 라고 표현하였다 !

필요성

확장성이 좋지 못한 코드 or 기술에 특화되어 있는 코드는 수정이 어렵고, 해당 기술을 잘 알아야지만이 유지보수가 가능하다는 단점이 있다.

하지만, 잘 만든 인터페이스가 있다면 이 인터페이스는 인터페이스대로 있고, 내 코드는 내 코드대로 있는 것이다.

잘 만든 인터페이스로 내 코드를 작성하면 확장성도 좋을 뿐더러 다른 것으로 바꿀때도 바꿔끼기(portable) 좋다는 의미이다.

또한, 서비스가 추상화되었다는 것은 서비스의 내용을 몰라도 그 서비스를 이용할 수 있다는 것이다.

가령, JDBC → Hibernate → JPA로 바꾸고자 한다면, 인터페이스만 바꿔주고 인터페이스 내 메서드만 구현해주면 내 로직(정확히는 비즈니스 로직)은 그대로 있는 것이다!

그리고 우리가 JDBC가 어떻게 구현되었는지를 몰라도 사용 할 수 있다.

트랜잭션 서비스 추상화

스프링에서 PSA를 사용한 예로 트랜잭션을 들 수 있다.

@Transactional 을 쓰게 되면,

스크린샷 2023-07-09 오후 12.02.11.png

Platform Transaction Manager 인터페이스를 구현해서 각각에 맞게 Jpa TransactionManager, Datasource TransactionManager, Hibernate TransactionManager 등이 알아서 등록이 된다.

트랜잭션.png

따라서 JPA → Hibernate로 바뀌어도 코드를 수정할 필요가 없다.

그리고 여기서 AOP의 개념도 다시 한번 상기시킬 수 있는데,

데이터를 저장 및 동기화 할 때 마다(트랜잭션이 발생할 때 마다) commit(), rollback() 메서드를 일일히 호출해야한다.

이것은 공통 관심사항이라고 볼 수 있다.

즉, 공통 관심사항을 인터페이스로 만들어 놓은 것은 AOP, 이것을 상황에 맞게 알아서 이식해주는 것은 PSA라고 볼 수 있다!