ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring IOC, DI, AOP
    백엔드 2023. 8. 26. 11:06
    728x90
    반응형
    SMALL

    Spring에 관해서

    Spring 1.0 버전은 2004년 3월 출시되었습니다. 20년 정도의 세월 동안 자바 엔터프라이즈 어플리케이션 개발에서 정상의 위치를 차지하고 있습니다.

    스프링 프레임워크의 구성은 20여가지로 구성되어 있습니다. 이러한 모듈들은 스프링의 핵심기능을 제공해주며, 필요한 모듈만 선택하여 사용 가능합니다. (스프링 프레임워크 모듈)

    시대의 변화에 따라 마이크로서비스 아키텍처로 변환, 계속해서 진화하고 있습니다.

     

    2000년대 초반 자바 EE 어플리케이션은 작성/테스트가 매우 어려웠으며, 한 번 테스트 하기가 번거로웠습니다. 이로 인해 느슨한 결합이 된 어플리케이션 개발이 힘든 상태였으며, 특히 데이터베이스와 같이 외부에 의존성을 두는 경우 단위 테스트가 불가능했습니다.

    이러한 점들을 보완하기 위해 spring은 "테스트의 용이성"과 "느슨한 결합"에 중점을 두고 개발했습니다.

    그에 따라 스프링이 다른 프레임워크와 가장 큰 차이점인 IOC를 통한 개발이 진행되었습니다.

    또한 AOP를 사용하여, 로깅, 트랜잭션 관리, 시큐리티에서 적용할 수 있습니다. AspectJ와 같이 완벽하게 구현된 AOP와 통합하여 사용 가능합니다.

     

     

    Spring 삼각형

    Spring 삼각형
    Spring 삼각형

    • IOC / DI : 의존 관계 주입
    • AOP : 관점 중심 프로그램
    • PSA : 이식 가능한 추상화

    위 세가지는 Spring의 가장 큰 특징이자 프레임워크를 표현할 수 있는 방법입니다.

     

     

    IOC / DI

    IOC란 Inversion of Control입니다. 스프링에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리하는 것이 아닌 Spring Container에 모두 맡기게 됩니다. 즉, 개발자에서 -> 프레임워크로 객체 관리(제어)의 권한이 넘어 갔음으로 "제어의 역전" 이라고 합니다.

     

    Spring에서 객체의 생명주기를 관리하게 된다는 것을 알게 되었습니다. 그렇다면 그 객체를 사용하는 방법은 무엇일까요?

    여기서 객체를 사용하기 위해서는 객체를 주입을 받게 됩니다. 이것을 DI라고 합니다.

    DI란 Dependency Injection입니다. 외부로부터 사용할 객체를 주입을 받게 되는데 이 주입 자체를 DI라고 하고, 주입을 해주는 것은 Spring Container입니다. 이렇게 객체 관리를 Spring에서 도맡아해주는데, 이것이 IOC입니다.

     

    DI의 장점

    의존성으로부터 격리시켜 코드 테스트에 용이하게 해줍니다. DI가 없다면 특정 객체가 다른 객체에 의존하는 경우가 많은데, 다른 객체가 없더라도 해당 객체를 테스트할 수 있도록 해줍니다.

    불가능한 상황을 Mock과 같은 기술을 통하여, 안정적으로 테스트 가능합니다. 이전에는 외부와 통신이 있는 경우 배포를 해서 컨테이너가 올라와야지만 외부와 통신이 가능했기 때문에 테스트를 할 수 없었습니다.

    코드를 확장하거나 변경할 때 영향을 최소화 합니다(추상화). 필요한 객체를 주입 받아서 사용하기 때문에 코드가 변경되더라도 최대한 영향받지 않도록 해줍니다.

    순환 참조를 막을 수 있습니다.

     

    예시)

    DI 없는 경우

    interface MyCoffee {
        void brewing();
    }
    
    class Americano implements MyCoffee {
        public void brewing(){
            System.out.println("brewing Americano");
        }
    }
    
    class CafeLatte implements MyCoffee {
        public void brewing(){
            System.out.println("brewing cafe latte");
        }
    }
    
    class Macchiato implements MyCoffee {
        public void brewing(){
            System.out.println("brewing macchiato");
        }
    }
    
    public class Main{
        public static void main(String[] args){
            MyCoffee americano = new Americano();
            MyCoffee cafeLatte = new CafeLatte();
            MyCoffee macchiato = new Macchiato();
            
            americano.brewing();
            cafeLatte.brewing();
            macchiato.brewing();
        }
    }

    Americano, CafeLatte, Macchiato 객체를 사용하기 위해 모두 new를 통해 객체를 생성해 사용하고 있습니다.

     

    DI 있는 경우

    interface MyCoffee {
        void brewing();
    }
    
    class Coffee{
        private MyCoffee coffee;
        
        public Coffee(MyCoffee coffee){
            this.coffee = coffee;
        }
        
        public void brewing(){
            coffee.brewing();
        }
    }
    
    public class Main{
        public static void main(String[] args){
            Coffee americano = new Coffee(new Americano());
            Coffee cafeLatte = new Coffee(new CafeLatte());
            Coffee macchiato = new Coffee(new Macchiato());
            
            americano.brewing();
            cafeLatte.brewing();
            macchiato.brewing();
        }
    }

    위와 같이 MyCoffee interface와 Coffee Class가 존재하고, 커피의 종류로 Americano, CafeLatte, Macchiato가 있을 경우, Coffee 클래스에 커피 종류의 객체를 주입받아서 사용하게 됩니다. 이제 Coffee라는 클래스를 수정하지 않아도 원하는 종류의 커피를 주입받아서 사용할 수 있게 된 것입니다. 또한, 새로운 종류의 커피가 생겨날 경우 해당 객체를 주입받기만 해도 되므로 관리나 재사용성이 편리해지게 됩니다.

    이처럼 외부에서 사용할 객체를 주입 받는 형태를 DI라고 합니다.

     

    IOC

    Spring에서는 객체의 생명주기를 Spring Container에서 관리하게 됩니다. Spring에게 해당 객체를 관리해달라는 요청을 하는 방법은 @Component 어노테이션을 설정해주는 것입니다.

    @Component
    class Americano implements MyCoffee {
        public void brewing(){
            System.out.println("brewing Americano");
        }
    }
    
    @Component
    class CafeLatte implements MyCoffee {
        public void brewing(){
            System.out.println("brewing cafe latte");
        }
    }
    
    @Component
    class Macchiato implements MyCoffee {
        public void brewing(){
            System.out.println("brewing macchiato");
        }
    }

    @Component 어노테이션을 설정할 경우 해당 객체는 빈으로 등록됩니다.

    @SpringBootApplication 어노테이션 설정 옆의 돋보기 버튼을 누르면 주입된 빈 리스트를 살펴볼 수 있습니다.

    주입된 빈 리스트
    주입된 빈 리스트

    Spring이 실행될 때 @Component가 붙은 객체를 찾아서 직접 객체를 싱글톤 형태로 만들어 스프링 컨테이너에서 관리하게 됩니다.

     

    관리되고 있는 객체는 Spring Application Context를 통해 가져올 수 있습니다.

    @Component
    public class ApplicationContextProvider implements ApplicationContextAware {
        private static ApplicationContext context;
        
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException{
            context = applicationContext;
        }
        
        public static ApplicationContext getContext(){
            return context;
        }
    }

    setApplicationContext를 통해 Application Context를 주입해주고, getContext를 통해 가져다 사용할 수 있습니다.

     

    public class Coffee{
        ...
        public Coffee(@Qualifier("americano") MyCoffee coffee){
            this.coffee = coffee;
        }
    }
    
    @SpringBootApplication
    public class Main{
        public static void main(String[] args){
            SpringApplication.run(Main.class, args);
            ApplicationContext context = ApplicationContextProvider.getContext();
            
            Encoder coffee = context.getBean(Coffee.class);
            coffee.brewing(); // brewing americano
        }
    }

    매칭할 빈이 여러 개일 경우 @Qualifier를 통해 지정해줘야 합니다. (위에서는 Americano로 지정)

    객체의 생명 주기에 대한 권한을 Spring에게 위임하고, ApplicationContext를 통해 스프링 컨테이너에서 관리하고 있는 객체인 빈을 주입 받아 사용하게 되므로 이제 new 생성자 없이 객체를 사용할 수 있게 됩니다.

    스프링에게 객체의 관리에 대한 제어의 역전이 일어난 것을 확인할 수 있습니다.

     

     

    @Component 어노테이션을 설정해주는 방법 말고 직접 빈으로 등록할 수도 있습니다.

    @Configuration
    class AppConfig{
        
        @Bean("americano")
        public Coffee coffee(Americano americano){
            return new Coffee(americano);
        }
        @Bean("cafeLatte")
        public Coffee coffee(CafeLatte cafeLatte){
            return new Coffee(cafeLatte);
        }
        @Bean("macchiato")
        public Coffee coffee(Macchiato macchiato){
            return new Coffee(macchiato);
        }
    }
    
    @SpringBootApplication
    public class Main{
        public static void main(String[] args){
            SpringApplication.run(Main.class, args);
            ApplicationContext context = ApplicationContextProvider.getContext();
            
            Encoder coffee = context.getBean("americano", Coffee.class);
            coffee.brewing(); // brewing americano
        }
    }

    @Configuration은 한 개 클래스에서 여러 개의 빈을 등록할 수 있게 해줍니다.

    동일한 Coffee 빈이 여러개 생성되었는데, 이를 이름을 지정해줘서 구분할 수 있도록 해줍니다.

    getBean() 메서드의 매개변수에 이름을 통해서도 빈을 가져올 수 있습니다.

     

    실제 서비스 로직에서 빈을 주입 받기 위해서는 ApplicationContext를 통해서가 아닌 생성자, set메서드, 변수에 @Autowired를 통해서 직접 객체를 받아올 수 있습니다.

     

     

    AOP

    AOP는 Aspect Oriented Programming으로 관점 지향 프로그래밍입니다.

    스프링은 대부분의 경우 MVC 웹 어플리케이션을 사용합니다. MVC는 Web Layer, Business Layer, Data Layer로 정의됩니다.

    • Web Layer : REST API를 제공하며, client 중심의 로직 적용
    • Business Layer : 내부 정책에 따른 logic을 개발하는 부분입니다.
    • Data Layer : 데이터 베이스 및 외부와의 연동을 처리합니다.

    횡단 관심

    여러 외부에서 정의하는 클래스에서 다양하게 사용되고 있는 것들 중 모두 사용하는 기능이거나, 한 두개의 클래스에서만 사용할 기능들의 경우 어떤 로직을 사용을 하기 위해서는 해당 메서드에 로직을 추가해줘야지만 사용이 가능합니다(메서드의 인자값 확인, 실행 시간 확인, 입력 출력 값 인코딩 디코딩 등).이러한 경우 공통적인 로직을 반복 코딩을 하게 되는 경우가 있습니다.

    AOP는 이러한 메서드들이나 특정 구역에서 반복되는 코드 로직을 한 곳에 모아서 할 수 있도록 해줍니다.

     

    AOP 주요 어노테이션

    • @Aspect : 자바에서 사용하는 AOP 프레임워크에 포함되며, AOP를 정의하는 Class에 할당
    • @Pointcut : 기능을 어디에 적용시킬지, AOP 적용 시점을 설정
    • @Before : 메서드 실행하기 이전
    • @After : 메서드가 성공적으로 실행한 후, 예외가 발생하더라도 실행
    • @AfterReturning: 메서드 호출 성공 실행 시 throw외에 정상적인 리턴이 되었을 경우 실행
    • @AfterThrowing : 메서드 호출 실패, 예외 발생될 때 실행
    • @Around : Before / After 모두 제어

     

     

    AOP dependency 추가

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-aop'
    }

     

    RestController를 통해 요청으로 들어올 경우의 인자값과 응답을 보낼 때 리턴 값을 출력해주도록 하겠습니다.

    @RestController
    @RequestMapping
    public class RestApiController{
        @GetMapping("/get/{id}")
        public void get(@PathVariable Long id, @RequestParam String name){
            System.out.println(id+" "+name);
            return id+" "+name;
        }
        
        @PostMapping("/post")
        public void post(@RequestBody User user){
            System.out.println(user);
            return User;
        }
    }

    위와 같이 연결된 api 요청이 들어올 때마다 로그를 찍어준다고 하면, 만약 메서드가 20개 100개가 된다면 해당 로그들을 모두 반복적으로 정의를 해주어야 합니다.

    로그를 찍어주는 부분을 한 곳으로 모아줄 수 있습니다.

     

    AOP 생성

    @Aspect
    @Component
    public class ParameterAop{
        
        @Pointcut("execution(* com.example.aop.controller..*.*(..))")
        private void cut(){
            
        }
        
        @Before("cut()")
        public void before(JoinPoint joinPoint){
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodsignature.getMethod();
            system.out.println(method.getName());
            
            Object[] args = joinPoint.getArgs();
            args.forEach(System.out::println);
        }
        
        @AfterReturning("cut()", returning = "object")
        public void afterReturn(JoinPoint joinPoint, Object object){
            System.out.println(object);
        }
    }

    공통 기능을 제공하는 aop객체를 선언하기 위해 @Aspect를 설정해주고 스프링에서 객체를 관리하도록 @Component를 추가해주빈다.

    @Pointcut을 통해 어떤 지점의 메서드에 적용할 것인지 지정해줍니다. 위와 같은 경우 controller 하위의 모든 메서드에 지정이 되도록 한 것입니다.

    @Before메서드 실행 전에 실행할 메서드를 정의해줍니다. 

    @AfterReturning메서드 실행 후 리턴될 때 시행할 메서드를 정의합니다.

    두 메서드 모두 어노테이션의 괄호 안에 어떤 지정된 메서드에 실행될 것인지 pointcut 메서드의 이름을 넣어주면 됩니다. 

    JoinPoint들어가는 지점에 대한 정보를 가지고 있습니다.

    @AfterReturning의 경우 반환받은 값에 대한 정보를 받을 수 있는 object 인자도 있습니다. returning 속성에 리턴받을 인자의 이름을 정의해줍니다.

     

    위의 api 메서드의 경우 2개지만 이 숫자가 많아질수록, 또 새로운 api 메서드를 추가할 경우 계속해서 로그 관련 로직을 추가해줄 필요없이 알아서 새로운 메서드에도 적용되므로 편리하게 사용할 수 있습니다.

     

     

    어노테이션을 통해 aop 생성

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Timer{
    
    }
    
    @Aspect
    @Component
    public class TimerAop{
    
        @Pointcut("execution(* com.example.aop.controller..*.*(..))")
        private void cut(){
            
        }
        
        @Pointcut("@annotation(com.example.annotation.Timer)")
        private void enableTimer(){}
        
        @Around("cut() && enableTimer()")
        public void arround(ProceedingJoinPoint joinPoint) throws Throwable {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            
            Object result = joinPoint.proceed();
            
            stopWatch.stop();
            
            System.out.println(stopWatch.getTotalTimeSeconds());
        }
    }

    메서드에서 특정 로직이 실행될 수 있도록 하는 어노테이션을 선언해줍니다.

    @Target을 통해 어노테이션이 부착될 수 있는 타입을 정해줍니다.

    @Retention을 통해 어느시점까지 메모리를 가져갈지 지정해줍니다.

     

    enableTimer를 통해 Timer어노테이션이 붙은 메서드의 경우에 대한 pointcut을 생성합니다.

     

    arround 메서드의 경우 controller하위에 정의되고 Timer어노테이션이 붙은 메서드에서만 실해되도록 해줍니다.

    인자로는 PorceedingJoinPoint를 받을 수 있는데 proceed()를 통해 해당 메서드가 실제 동작하는 시점을 지정해줄 수 있습니다.  proceed()는 실행한 메서드의 리턴값을 반환합니다.

    proceed() 메서드 앞 뒤로 메서드 실행 전과 후에 실행할 로직들을 한 메서드 안에서 정의할 수 있습니다.

     

     

    인자값 출력값 수정

    api 요청으로 들어오는 값이 encoding이 되어서 실제 사용할 경우 decoding해서 사용하고, 응답을 encoding해서 내보내야하는 경우에도 사용할 수 있습니다.

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Decode{
    
    }
    
    @Aspect
    @Component
    public class DecodeAop{
    
        @Pointcut("execution(* com.example.aop.controller..*.*(..))")
        private void cut(){
            
        }
        
        @Pointcut("@annotation(com.example.annotation.Decode)")
        private void enableTimer(){}
        
        @Before("cut() && enableTimer()")
        public void before(JoinPoint joinpoint) throws UnsupportedEncodingException {
            Object[] args = joinPoint.getArgs();
            for(Object arg : args){
                if(arg instanceof user){
                    User user = User.class.cast(arg);
                    String base64Email = user.getEmail();
                    String email = new String({디코딩 로직...});
                    user.setEmail(email);
                }
            }
        }
        
        @AfterReturning("cut() && enableTimer()", returning = "obj")
        public void afterReturn(JoinPoint joinpoint, Object obj){
            if(obj instanceof user){
                User user = User.class.cast(obj);
                String email = user.getEmail();
                String base64Email = ({인코딩 로직...});
                user.setEmail(base64Email);
            }
        }
    }

    요청 응답이 들어올 때와 나올 때, 인자값을 바꾸고 싶은 값으로 수정을 하면 해당 aop를 사용하는 메서드에서는 수정된 값을 받고 주는 것을 알 수 있습니다.

     

     

     

     

     

     

    오늘은 Spring의 특징들에 대해 배웠습니다. 빈의 주입이라던가 spring container에서의 객체의 생명주기에 대해서 좀 더 알게 된다면 Spring을 통해 개발하는데 조금 더 수월할 것 같습니다. 또한 aop를 활용해 반복적인 로직을 줄이고 한 곳에 모아서 편리하게 사용할 수 있다는 것을 알게 되었습니다.

    반응형
    LIST

    '백엔드' 카테고리의 다른 글

    JPA @Query, Native Query, Converter  (0) 2023.09.05
    JPA Cascade, OrphanRemoval  (0) 2023.09.04
    JPA Transaction  (0) 2023.08.25
    영속성 컨텍스트, Entity Cache, Entity Lifecycle  (2) 2023.08.24
    Jpa 연관관계 살펴보기 (N:N)  (0) 2023.08.23

    댓글

Designed by Tistory.