Baeldung의 이 글을 정리 및 추가 정보를 적은 글입니다.

1. Overview

  • 이 글에서는 IoC container에게 bean을 만들어달라고 하는 방법이 뭐가 있는지 알아볼거다.

  • xml을 사용하거나 @Bean을 bean 생성하는 method에 부착하는 방법이 있는 것은 이전 글들을 읽어봤으면 대충 눈치 챘을 것이다.

  • 그리고 마찬가지로 이전 글들을 봤다면 @ComponentScan을 이용해서 bean을 탐지하는 방법도 알고 있을 것이다. 몇 번 언급했기 때문. 정확히는 @ComponentScan에서 @Component를 탐지해가지고 그것들을 bean으로 만드는 것이다. 이 방법에 대해 좀 더 자세히 알아보자.

2. Component Scanning

  • @ComponentScan은 패키지에 있는 @Component를 스캔해가지고 그것들을 bean으로 만들어달라는 annotation이다. 이 때 패키지를 지정하는 방식이 2가지가 있다.

  • 먼저 밑처럼 패키지 이름을 그대로 사용하는 것이다.

@Configuration
@ComponentScan(basePackages = "com.baeldung.annotations")
class VehicleFactoryConfig {}
  • 다른 방식은 클래스 사용하기. 그러면 그 클래스가 속한 패키지에 대해서 스캔을 한다.
@Configuration
@ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
class VehicleFactoryConfig {}
@ComponentScan({"com.my.package.first","com.my.package.second"})
...
  • 또 아예 주어지지 않은 경우 annotate된 class가 속한 패키지에 대해서 스캔을 수행한다.

  • 또 JAva 8의 Repeatable Annotations를 활용하는게 가능해서 밑과 같은 코드도 유효하다.

@Configuration
@ComponentScan(basePackages = "com.baeldung.annotations")
@ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
class VehicleFactoryConfig {}
  • 이 방식도 가능하다.
@Configuration
@ComponentScans({ 
  @ComponentScan(basePackages = "com.baeldung.annotations"), 
  @ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
})
class VehicleFactoryConfig {}
  • 물론, xml로도 component scan이 가능하다.
<context:component-scan base-package="com.baeldung" />

3. @Component

  • scanning 대상이 되는 class를 지칭 할 때 사용된다.
@Component
class CarUtility {
    // ...
}
  • 생성된 bean의 이름은 class 첫글자가 소문자인 형태로 지정이 된다. 아니면 value라는 argument로 지정하는 것도 가능하다.
@Component(value="engine")
class CarUtility {
    // ...
}
  • 이건 모든 @Component를 포함하는 annotation들에 해당된다. 즉 밑의 annotation들도 이 기능은 전부 지원한다. 그럼 해당 annotation들은 무슨 추가 기능이 있는걸까?

4. @Repository

  • 이 annotation에 대해 설명하기 전에 먼저 Data Access Object에 대해 설명해야 한다.

Data Access Object (DAO)

  • DAO Pattern이라는 것이 있다. Java에서 시작한 개념으로 Persistence Layer과 application/business layer의 분리를 위해 사용하는 패턴이다.

  • 이렇게 말하면 어렵지만 사실 간단하다. persistence는 영속이란 뜻을 가지고 있는데, 저 layer은 데이터 영구 보관층, 즉 DB를 의미한다. 그리모 application/business layer은 애플리케이션/비즈니스 로직을 의미, 즉 애플리케이션 자체를 말하는 것이다.

  • 세상에 많은 DBMS가 있고 각각이 데이터를 관리하는 방식이 다 다르다. 이들이 전부 CRUD operation을 지원하긴 하지만 그 방식이 각각 다르다는 것이다. persistence layer측을 언제 바꿀지 모르기 때문에 어떤 DBMS에 대해 이를 고정시키는 것은 좋은 습관이 아니고, 그래서 등장한게 이 DAO다.

  • DAO 자체가 API로, 하위 DBMS가 뭔지 정확히 모르도록 하면서 business 측에서 CRUD operation을 수행할 수 있도록 해주는 것이다. 그리고 Spring의 @Repository는 이게 DAO라는 것을 지칭하는거다. 참고로 DAO라고 지칭하는거지 우리가 DAO를 구현해야하는 것은 여전히 마찬가지다(...)

@Repository
class VehicleRepository {
    // ...
}
  • 그러면 그냥 @Component 쓰면 되지 않냐고 할 수 있다. 그런데 추가 기능이 있는데, 바로 저 DAO 안에서 DBMS 건드리다가 그쪽에서 exception이 발생하면 spring의 DataAccessException의 subclass로 자동으로 번역해주는 기능이다...! 이 경우 PersistenceExceptionTranslationPostProcessor bean을 따로 만들어줘야 한다.
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}
<bean class=
  "org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

5. @Service

  • application의 business logic임을 나타내는데 사용된다.
@Service
public class VehicleService {
    // ...    
}

6. @Controller

  • Spring MVC의 controller 역할을 담당하는 요소임을 나타내는데 사용된다.
@Controller
public class VehicleController {
    // ...
}
  • 이것도 가독성 개선 말고는 딱히 하는게 없다.

7. @Configuration

  • component scan 말고 bean을 생성하는 method를 가지고 bean을 만들 수 있다고 했다. @Configuration을 가진 class에 @Bean, 즉 bean을 생성하는 method임을 알리는데 사용하는 annotation 사용이 가능하다.
@Configuration
class VehicleFactoryConfig {

    @Bean
    Engine engine() {
        return new Engine();
    }

}

구체적인 @Bean@Configuration의 관계

  • 그런데 사실 @Bean@Configuration이 없어도 사용이 가능하긴 하다. 하? 정확히는 @Component가 달려있는 class에서는 @Bean이 달려있는 method 생성이 가능하다. @Configuration이 없는 class에서 @Bean을 사용하는 것을 lite @Bean mode라고 한다.

  • 관련 documentation

  • 이러면 알겠지만 사실 @Configuration은 단순히 bean을 정의하는 method가 존재하는 곳임을 지칭하는 것이 아니다. 본인이 @Component가 포함되어 있어서 @Bean을 사용할 수 있게 하는 것도 맞지만, 그거랑 더불어 inter-bean dependency를 정의하는것도 가능하게 해준다는게 특징이다.

  • inter-bean dependency랑 어떤 @Bean method가 같은 class의 @Bean method를 활용해가지고 dependency 관계를 가지는 것을 말한다. 즉 한 class 안에서 의존관계가 형성되는 것을 말한다. 의존 관계가 형성이 되려면 동일한 instance에 대해서 공유가 되어야 한다는 것이고, 이는 곧 singleton scope의 bean이 형성된다는 것을 의미한다. 밑은 예시 코드. 이 경우 singleton scope를 가지는 bean을 FirstService, SecondService가 가지기에 SharedDependency bean은 1개만 존재한다. 그리고 first랑 second가 shared를 기준으로 의존관계를 형성하게 된다.

@Configuration
public class SomeConfiguration {
    @Bean
    public SharedDependency sharedDependency() {
        return new SharedDependency();
    }
    
    @Bean
    public FirstService firstService() {
        return new FirstService(sharedDependency());
    }
    @Bean
    public SecondService secondService() {
        return new SecondService(sharedDependency());
    }
}
  • 참고로 여기는 하나의 @Configuration class에 대해서 수행했지만 @Configuration class에 있는 method를 호출하는 것도 가능하다.

  • 반대로 lite bean의 경우, @Bean method를 호출할 때마다 동일한 bean이 나오는게 아니라 새로운 bean이 나오게 된다. 이러면 호출할때마다 다른 bean이 나오기에 bean간의 inter-dependency를 형성하는 것이 불가능하다.

  • 그러면 둘이 무슨 용도 차이가 있는것인가? @Configuration은 환경 설정을 의미한다. 이때 여기서 말하는 환경 설정의 범위는 application 차원에서의 환경 설정인데 그러면 applicaiton 전체에 대해서 여러개의 instance가 존재하면 안되고 하나만 존재해야 하며, 타 method에서 이 configuration과 관련된 bean을 접근하려 할 때 새로 bean을 생성해서 전달하는게 아니라 관련 proxy를 제공해주고 그걸 접근하는 것이 합리적인 디자인이다. (정확히는 CGLIB proxy를 활용하는데 자세한건 넘어가겠다.) 그래서 @Bean에서 만드는 bean이 singleton scope인 것이다.

  • 그러나 위의 CGLIB 활용 proxy를 설정하는 작업 자체가 application startup time에 영향을 줄 수 있다. 또 CGLIB subclassing을 적용하는 것 자체가 class 디자인에 제약을 주는 경우가 있다. 그러면서 특정 모듈에 다른 모듈의 기능을 넣는 역할을 제공하고 싶거나 모두가 사용할 개별적인 유틸리티를 제공하고 싶은 경우에 lite bean을 사용하면 오버헤드를 최소화하면서 관련 기능 구현이 가능하다. 이럴 때 단순 bean factory 용도로 활용이 된다.

  • 다만 lite bean이 저 의도한 데로'만'사용되고 있는지 아닌지 관리하는거 자체가 매우 피곤하다. 그리고 lite bean으로 구현하려는 용도가 보통 프로그래밍 디자인 차원에서 선호되지 않는 경우거나 다른 안전한 방법이 있는 경우가 많다. 그래서 보통 선호되지 않는다.

    • 예를 들어 특정 모듈에 다른 모듈의 기능을 넣는 것은 SOLID의 S를 위배한다.
    • 또 모두가 사용한 개별적인 유틸리티로 metric 측정이 있을 수 있는데, 이는 밑의 Spring AOP로 구현하는 것이 가능하다.
  • lite bean의 예시로는 밑이 있다. 이 경우 SharedDependency bean이 1개가 아니라 3개가 생성된다.

@Service
public class SomeConfiguration {
    @Bean
    public SharedDependency sharedDependency() {
        return new SharedDependency();
    }
    
    @Bean
    public FirstService firstService() {
        return new FirstService(sharedDependency());
    }
    @Bean
    public SecondService secondService() {
        return new SecondService(sharedDependency());
    }
}

8. Sterotype Annotaions and AOP

  • 앞에서 annotation들을 저렇게 구별한 이유가 가독성뿐만이 아니라 추후 각 annotation에 대해서 다른 기능을 지원하기 위해서라고... 내가 적었고 사실 Spring이 그렇게 문서화했다.

  • 이 annotation별 기능을 pointcut이라고 한다. @Pointcut annotation을 사용하기 때문... 그런데 가만히 생각해보면 우리가 그 pointcut를 만들면 안되나라는 생각을 할 수 있다. 그리고 가능하다.

  • 예를들어 DAO layer의 method들의 실행시간을 측정하고 싶으면 밑과 같이 코드를 짜면 된다.

@Aspect
@Component
public class PerformanceAspect {
    @Pointcut("within(@org.springframework.stereotype.Repository *)")
    public void repositoryClassMethods() {};

    @Around("repositoryClassMethods()")
    public Object measureMethodExecutionTime(ProceedingJoinPoint joinPoint) 
      throws Throwable {
        long start = System.nanoTime();
        Object returnValue = joinPoint.proceed();
        long end = System.nanoTime();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(
          "Execution of " + methodName + " took " + 
          TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
        return returnValue;
    }
}

@Aspect, Aspect Oriented Programming

  • 이 글을 진짜 많이 참고했고, 그래서 이 글을 대신 봐도 된다.

  • 자 갑자기 많이 복잡해지는데 하나하나 알아보도록 하자. 먼저 저 annotation에 대해 얘기하려면 AOP, 즉 Aspect Oriented Programming에 대해 얘기를 안할 수가 없다. AOP는 프로그램 패러다임 중 하나다. (다른 프로그램 패러다임으로 OOP가 있다.)

  • 저게 뭔지 위키백과에 검색해보면 이런 소리를 한다.

In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns.

  • 그러면 여기서 cross-cutting concern이 뭔지 궁금해질테니 이를 찾아보면

In aspect-oriented software development, cross-cutting concerns are aspects of a program that affect several modules, without the possibility of being encapsulated in any of them.

  • 영어가 힘들텐데 사실 별거 아니다. cross-cutting concern은 비즈니스 로직에 해당되지 않는데 여러 모듈에서 활용하는 중복된 코드를 지칭하는 것이다. 이는 OOP에서 잘 없애지 못하는 개념이고 AOP에서 잘 없애는 개념이다. 왜냐면 AOP가 애초에 저거 담당 패러다임이기 때문. 애초에 저 concern을 AOP에서의 aspect라고 정해버렸다.

  • 자 여기서 잠깐 생각해보자. Spring Application에서 Aspect에 해당할 법한 요소는 뭐가 있을까? 몇가지 생각해보면 성능 측정, 보안, 로그 등이 있다. 이들은 모듈마다 다 필요로 할법한 기능이지만 모듈에 해당하는 기능이라고 보기에는 매우 애매하다.

  • 위의 코드가 무엇인가? 실행시간 측정이라고 했다. 어? Aspect네? 그래서 이걸 갑자기 여기서 사용하고 있는 것이다. 실제로 AOP의 예제로 많이 사용된다.

proxy pattern and Spring AOP

  • 알겠는데 그래서 대체 어떻게 AOP 형식으로 프로그래밍을 하는건가? Spring에서 AOP 형식으로 프로그래밍하는걸 Spring AOP라고 하며 documentation이 아예 따로 있다. AspectJ를 지원한다.

  • 저걸 다 확인하긴 좀 그러고 간략하게 설명할건데 이를 알려면 proxy pattern이라는 것을 알아야 한다. 뭔가 비슷한 개념을 이전에 봤는데 뭐 저거랑 같은건 아니지만 프록시라는 개념을 그때 이해했으면 됬다. 앞에 소개한 글에서 proxy pattern을 잘 소개했다.

  • 그 글을 보면 알겠지만 proxy pattern은 이미 인터페이스를 implement한 형태로 존재하는 class에 추가 기능을 넣거나 접근 제어를 하고 싶을 때 사용하는 pattern이다. 감싸고 있는 녀석이 프록시. 하지만 이것도 그 글에서 언급한 것처럼 중복 코드가 많은건 여전하다. 즉 이거 자체가 솔루션은 아니고, 이걸 활용한 Spring AOP가 솔루션이다. 이것도 앞에 소개한 글을 참고하면 좋다.

  • 좋다, 그러면 proxy pattern으로 Spring AOP가 런타임 때 bean을 감싸고 거기에 추가기능을 넣고 있으니, 저 성능 측정같은 aspect를 proxy 형성때 감싸도록 지정하면...되겠는데 어떻게 하는가.

  • 뭐 그건 계속 배워나갈건데 일단 aspect에 해당하는 method (및 이것저것)을 포함한 class를 지정해야 하고 거기에 @Aspect가 쓰인다.

@Pointcut

    @Pointcut("within(@org.springframework.stereotype.Repository *)")
    public void repositoryClassMethods() {};
  • @Pointcut은 어떤 코드의 어떤 시점에 우리가 끼어들고 싶을 때 그 위치를 지칭하는데 사용되며 보통 어떤 method가 실행되는 시점을 나타내는데 사용된다. 이때 정확한 위치를 pointcut expression이라는 녀석으로 지정해야 한다. 여기서 AspectJ의 pointcut designator(PCD)이 등장하고... 뭐 요상한 AsepctJ 문법이 적용되는데 여기서 집중적으로 다루진 않겠다.

  • 괄호 전의 단어가 PCD에 해당하는데 보통 어떤 method 실행을 나타낸다는 뜻의 execution을 쓰지만 여기서는 within을 썼다. 이건 무슨 뜻이냐면 AspectJ expression이 포괄하는 영역의 모든 method execution에 대해서 join하라는 뜻. 그래서 코드의 경우에는 @Repository가 annotate된 class의 모든 method의 실행 시점을 지칭하는 pointcut을 만들기 위해 위처럼 했다.

  • 이 때 @Pointcut 밑에 있는 그냥 일반 코드를 pointcut signature이라고 한다. 아무 implementation이 되지 않은 method인데... 이 method의 이름이 이 @Pointcut이 가리키는 위치를 대표하는 이름이다.

@Around and advice

  • 그리고 앞의 이름을 여러 annotation이 활용하는데 그 중 하나가 @Around다. 보면 repositoryClassMethods()를 보유하고 있는데 이 pointcut에 대해서 '주변에' 무슨 코드를 실행한다는 것을 의미할 때 쓰인다.

  • 그 코드는 밑에 정의되어 있는데 여기서 ProceedingJoinPoint class에 해당하는 joinPoint라는 parameter이 있는걸 볼 수 있다. @Around에서만 쓰이는 녀석인데 저게 @Pointcut에 의해 가리켜진 시점이다. 그 시점 전에 이제 무슨 코드를 수행하고, joinPoint.proceed()로 코드 진행 계속 하고, 다시 그 후에 코드를 실행하고 종료하면 어떤 method의 주변에 무슨 코드를 수행하는 것이 가능해지죠.

참고로 다른 advice들도 joinPoint parameter을 가지는게 가능한데 그 녀석들의 class는 JoinPoint다. 이 둘의 차이점은 이 글을 참고하자.

  • 그래서 @Around라는 annotation 이름을 가지는 것이다.

  • 참고로 joinPoint.getSignature().getName()도 하고 있는데 getSignature 은 말 그대로 Signature을 반환하며, 거기는 또 getName을 호출하면 join point 관련 method 이름이 나오게 된다. 그러니까 성능 측정한 method 이름이 뭔지 알아내려고 저러고 있는 것이다.

  • @Around처럼 어떤 pointcut을 기준으로 전이나 후나 주변이나 막 실행하는, 그러니까 aspect에 해당하는 것을 Spring에서는 advice라고 부른다. 참고. 더 다양한 advice들에 대해 알고 싶으면 이 글 참고

  • 여튼, 이 AOP 관련 프로그래밍 문법을 활용하면 특정 @Component 확장 annotation을 가진 class들에 대해서 추가 기능을 넣는 것이 가능하다.

profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글