인프런에서 백기선님의 예제로 배우는 스프링 입문 (개정판)을 수강하며 정리한 글입니다.
스프링의 세가지 핵심 개념 IoC, AOP, PSA에 대해 소개하는 강좌로, 예제 프로젝트인 petclinic을 분석하는 방식으로 진행됩니다. 강의를 들으면서 중간 중간 메모한 내용을 모아놓은 거라 요약된 표현이 많습니다. 보다 자세한 설명은 강의를 통해 확인하실 수 있습니다.

Spring IoC

Inversoin of Control : 제어권의 역전

일반적인 경우 의존성에 대한 제어권을 자기 자신이 가진다.

class OwnerController {
    private final OwnerRepository owners = new OwnerRepository();
}

OwnerController에서 사용할 Repository 객체를 직접 생성하지 않고, 생성자를 통해 바깥쪽에서 주입한다. Controller 객체를 만들 때 무조건 repository 객체가 있어야 하기 때문에, 이 객체가 없으면 객체 생성 자체가 불가능하다.

DI(의존성 주입)를 이용하면 개발자가 실수로 owners를 초기화 하지 않아서 NPE가 발생하는 문제를 예방할 수 있다.

class OwnerController {
    private final OwnerRepository owners;

    public OwnerController(OwnerRepository clinicService, VisitRepository visits) {
        this.owners = clinicService;
        this.visits = visits;
    }
}

@MockBean : Mock 객체를 만들어서 Bean(스프링이 관리하는 객체)에 등록해주는 어노테이션

@MockBean
private OwnerRepository owners;

스프링이 Bean들의 의존성을 관리하고 있으며, 아래와 같은 코드가 실행되면 OwnerRepository 타입의 Bean을 가져와서 주입해준다.

public OwnerController(OwnerRepository clinicService) {
    this.owners = clinicService;
}

Spring IoC Container

ApplicationContext (BeanFactory) : Spring의 핵심적인 클래스
Bean을 만들고 엮어주며 제공해주는 역할을 한다.

Bean은 IoC 컨테이너 안에 등록된 객체들을 의미한다. 모든 클래스의 객체가 Bean으로 등록되는 것은 아니고, IntelliJ로 봤을 때 클래스 옆에 녹색 콩 아이콘이 붙은 클래스만 Bean에 등록된다.

의존성 주입은 Bean끼리만 가능하다. Bean을 직접 가져와서 테스트 해보는 코드를 작성해보자.

    @Autowired
    ApplicationContext applicationContext;

    @Test
    public void getBeanTest() {
        OwnerRepository repository = applicationContext.getBean(OwnerRepository.class);
        Assertions.assertNotNull(repository);
    }

bean 객체 출력해보기

  • Controller에 아래와 같은 코드 추가
    private final ApplicationContext applicationContext;

    public OwnerController(OwnerRepository owners, VisitRepository visits, ApplicationContext applicationContext) {
        this.owners = owners;
        this.visits = visits;
        this.applicationContext = applicationContext;
    }

    @GetMapping("/bean")
    @ResponseBody
    public String bean() {
        return "bean : " + applicationContext.getBean(OwnerController.class);
    }
bean : org.springframework.samples.petclinic.owner.OwnerController@32708e06

Bean으로 관리하는 객체는 매번 새로 생성되지 않고, 처음에 만들어둔 객체를 재사용하게 된다.

물론 이런 식으로 ApplicationContext나 getBean()을 직접 사용할 일은 거의 없다.

Spring Bean

Bean은 IoC 컨테이너가 관리하는 객체

등록하는 방법은 크게 2가지가 존재한다.

  1. Component Scanning : Spring IoC 컨테이너가 IoC 컨테이너를 만들고 그 안에 빈을 등록할때 사용하는 인터페이스들을 라이프 사이클 콜백이라고 부른다.
    여러 라이프사이클 콜백 중 컴포넌트 어노테이션을 찾아서, 해당 클래스의 인스턴스를 Bean으로 등록하는 일을 한다.

    • @ComponentScan : 어디부터 컴포넌트를 찾아볼 것인지 알려주는 역할. @SpringBootApplication 어노테이션이 붙어있는 클래스부터 시작해 하위 클래스를 모두 찾아보게 된다.

      @ComponentScan(
        excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
      ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
      )}
      )
    • @Component

      • @Repository, @Service, @Controller, @Configuration
      • Repository는 약간 특이 케이스인데, JPA의 기능에 의해 등록된다. 어노테이션이 없어도 Repository 인터페이스를 상속 받으면 그 구현체를 Bean으로 등록한다.
  2. xml이나 자바 설정 파일에 직접 등록
    두가지 자바 설정 파일을 작성하는 방식이 더 많이 쓰이는 추세다.
    @Configuration 어노테이션을 붙인 클래스를 만들고, 그 안에서 @Bean을 사용해 직접 Bean을 정의한다.

@Configuration
public class SampleConfig {

    @Bean
    public SampleController sampleController() {
        return new SampleController;
    }
}

@Configuration 어노테이션이 컴포넌트 스캐닝할 때 읽히게 되고, 안에서 정의한 bean들이 IoC 컨테이너에 정의된다.

의존성 주입 (Dependency Injection)

  • @Autowired : 생성자, setter, 필드에 붙일 수 있다. Spring 5 이후로는, 매개변수가 하나뿐인 생성자라면 어노테이션을 생략해도 DI가 자동으로 적용된다.
    • Spring에서 권장하는 위치는 생성자. 필수적으로 생성해야 하는 레퍼런스 없이는 해당 클래스의 인스턴스 생성 자체가 불가능.
    • 순환 참조/상호 참조하는 의존성 문제가 발생한다면 필드나 setter에 붙이면 됨. 물론 가급적 이런 일이 발생하지 않게 하는 것이 좋다.
    • 필드에 붙일 경우 final과는 함께 사용할 수 없다.

Spring AOP

AOP는 Aspected oriented programming (관심사 중심 프로그래밍)

여러 메서드에서 공통적으로 해야하는 일의 코드가 중복된다면, 따로 모아서 재활용하는 것.

  • @Transctional이 AOP 기반으로 만들어진 어노테이션이다.
  • AOP는 코드가 없는데도 코드가 있는 것처럼 해줌

개발자가 작성한 코드

@GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
        Owner owner = this.owners.findById(ownerId);
        model.addAttribute(owner);
        return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}

AOP를 이용해 개발자가 작성하지 않았어도 특정한 코드가 삽입되는 듯한 효과를 낼 수 있다.

@GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
        // 개발자가 작성하지 않은 코드가 삽입
        AAA...
        BBB...

        Owner owner = this.owners.findById(ownerId);
        model.addAttribute(owner);
        return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}

구현 방법

기존 코드를 건들지 않고 새 기능을 추가하는 방법 세가지

  1. 컴파일
  2. 바이트코드 조작 : 클래스 로더가 클래스를 읽어오는 단계에서(.java를 .class로) 조작하는 것
  3. 프록시 패턴 : 스프링 AOP가 사용하는 방법

AOP가 어떻게 동작하는지 알아보기 위해, 실행 시간을 측정하는 어노테이션 @LogExecutionTime을 만들어보자.

 

1. 인터페이스 생성

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {

}

2. 어노테이션이 붙은 scope에서 실행할 코드 작성

@Component    // bean으로 등록
@Aspect
public class LogAspect {

    Logger logger = LoggerFactory.getLogger(LogAspect.class);

    /**
     * @param joinPoint annotation이 실행되는 target
     */
    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Object proceed = joinPoint.proceed();

        stopWatch.stop();
        logger.info(stopWatch.prettyPrint());

        return proceed;
    }
}

Spring PSA

Service Abstraction으로 제공되는 기술을 다른 기술 스택으로 간편하게 바꿀 수 있는 확장성을 갖고 있는 것이 Portable Service Abstraction. 줄여서 PSA라고 한다.

기존 코드를 거의 변경하지 않아도 사용 기술을 간단하게 바꿀 수 있다.

  • 스프링은 서블릿을 사용하는 프로그램인데 서블릿을 사용하지 않고있다. 그 대신 @GetMapping이나 @PostMapping을 통해 특정 url로 요청이 들어왔을 때, 해당 블록이 요청을 처리하도록 구현 되어있다. 이렇게 추상화 계층을 사용해 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공하는 것을 Service Abstraction이라고 한다.
  • Spring Web MVC, Spring Transaction, Spring Cache 등이 모두 Portable Service Abstraction에 해당된다.
  • 스프링은 MVC라는 추상화 기법을 사용. Spring Web MVC를 사용하면 서블릿을 low level로 직접 구현할 필요가 없어진다.
    • View : templates
    • Model : Repository
    • Controller : Controller

스프링은 원래 Tomcat 기반으로 돌아가는데, dependency에서 web을 webflux로 바꾸고 다시 실행해보면 Netty 기반으로 돌아간다. 스프링의 PSA 덕분에 코드를 거의 바꾸지 않고도 톰캣이 아닌 완전히 다른 기술로 실행이 가능하다는 의미.

  • 스프링 트랜잭션 (Atomic한 작업을 트랜잭션이라 부름)
    • commit, rollback을 명시적으로 호출하지 않아도, 어노테이션만 붙이면 트랙잭션 처리가 이루어짐