스프링 개념과 REST API (작성중)

SummerToday·2025년 1월 12일
0
post-thumbnail

스프링 패턴과 패러다임

스프링은 기본적으로 제어의 역전(IoC: Inversion of Control)으로도 알려진 의존성 주입(DI: Dependency Injection), 관점 지향 프로그래밍(AOP: Aspect-oriented-Programming)을 지원한다.

스프링은 자바 외에 그루비 및 코틀린과 같은 JVM 언어도 지원한다.

IoC(제어의 역전)

  • 프로그램의 흐름을 개발자가 아닌 프레임워크가 제어한다.

  • 개발자는 필요한 로직만 구현하고, 나머지 흐름은 프레임워크가 관리.

  • 스프링(Spring) 프레임워크가 객체 생성, 의존성 주입 등을 자동으로 처리.

  • 개발자는 로직에 집중하고, 흐름 제어는 프레임워크에 맡겨 개발 효율성과 재사용성을 높일 수 있다.

  • 객체 지향 프로그래밍(OOP)의 의존성 주입(DI)과 함께 사용되며, 현대 프레임워크에서 매우 일반적인 원칙이다.


AOP

  • Aspect-Oriented Programming

  • 프로그램의 핵심 비즈니스 로직과는 별개로, 부가적인 기능(예: 로깅, 트랜잭션 관리, 보안)을 효율적으로 분리하여 모듈화하는 프로그래밍 방식이다.

  • AOP는 OOP와 함께 작동 하는 프로그래밍 패러다임이다.

  • OOP에서는 한 클래스에서 하나의 책임만 다루는 것이 좋은 관행이며, 이것을 단일 책임 원칙(SRP, Single Responsibility Principle)이라고 불린다.

  • AOP의 장점

    • 중복 코드 제거
      여러 곳에서 반복되는 부가 로직(예: 로그, 보안)을 한 곳에서 관리 가능.

    • 핵심 로직과 부가 로직의 분리
      코드 가독성과 유지보수성 향상.

    • 유연성
      특정 조건에 따라 부가 기능을 쉽게 추가/제거 가능.


AOP의 구성요소

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service..*(..))")
    public void logBefore() {
        System.out.println("메서드 실행 전 로그 출력");
    }
}
  • Aspect

    • 공통 관심사(부가기능, Advice)와 적용 대상(Pointcut)을 묶어 놓은 모듈(클래스)이다.

    • 공통적인 기능(예: 로깅, 트랜잭션 처리 등)을 정의하고 이를 특정 지점에 적용한다.

    • @Aspect 어노테이션으로 정의한다.

    • @Component와 함께 사용하여 빈으로 등록한다.


@Before("execution(* com.example.service..*(..))")
public void logBefore() {
    System.out.println("메서드 실행 전 로그 출력");
}
  • Advice

    • 공통 관심사에서 실행될 실제 동작(부가기능) 코드이다.

    • 특정 시점에서 실행될 구체적인 로직을 정의한다.

      • Before
        대상 메서드 실행 전에 동작한다.

      • @After
        대상 메서드 실행 후에 동작한다.

      • @AfterReturning
        대상 메서드가 정상적으로 종료된 후 동작한다.

      • @AfterThrowing
        대상 메서드에서 예외 발생 시 동작한다.

      • @Around
        대상 메서드 실행 전후에 동작한다.


@Pointcut("execution(* com.example.service..*(..))")
public void serviceMethods() {}
  • Pointcut

    • 어드바이스가 적용될 조인 포인트(Join Point)를 지정하는 표현식이다.

    • 어드바이스를 실행할 메서드나 클래스 등 특정 위치를 정의한다.

    • 표현식을 사용하여 클래스, 메서드, 패키지 등을 선택한다.

    • @Pointcut 어노테이션을 사용하여 지정한다.


  • Joint Point

    • 애플리케이션 실행 중, 어드바이스가 적용될 수 있는 모든 실행 지점을 의미한다.

    • 메서드 호출, 생성자 호출, 예외 발생 등 실행 가능한 지점을 지정한다.


public class MyService {
    public void performTask() {
        System.out.println("실제 비즈니스 로직");
    }
}
  • Target

    • 실제로 어드바이스가 적용되는 대상 객체를 의미한다.

    • Joint Point에서 실행되는 구체적인 클래스나 메서드를 의미한다.

    • 위에서 MyService 클래스와 그 안의 performTask 메서드가 Target이다.


  • weaving

    • Aspect를 Target의 Join Point에 적용하는 과정을 의미한다.

    • 런타임, 컴파일타임, 또는 클래스 로드 시점에 Aspect와 Target을 결합하는 과정이다.

    • Weaving 방식

      • 런타임 위빙(Runtime Weaving)
        프록시 패턴을 사용해 동적으로 결합하는 방식이다.

      • 프록시 기반
        메서드 호출 시 프록시 객체가 실제 로직 전에 어드바이스를 실행하는 방식이다.


AOP 구성 요소 간 관계 정리

  • Aspect
    공통 관심사와 적용 위치를 정의한다.

  • Advice
    Aspect의 구체적인 실행 로직이다.

  • Pointcut
    Advice를 적용할 Join Point를 선택한다.

  • Join Point
    실행 가능한 모든 지점(메서드 호출 등)이다.

  • Target
    Aspect와 Advice가 실제로 적용되는 객체를 의미한다.

  • Weaving
    Aspect와 Target을 결합하는 과정을 의미한다.


IoC 컨테이너

출처: https://dotnettutorials.net/lesson/spring-framework-ioc-containers/

  • IoC 컨테이너의 역할

    • 스프링 프레임워크의 핵심은 IoC(Inversion of Control) 컨테이너로, 객체(Bean)의 생성, 조립, 관리를 담당.

    • 애플리케이션에서 필요한 여러 객체를 bean으로 등록하고, 이 객체 간의 의존성 주입(DI)을 관리함.

    • 스프링 컨텍스트에서 IoC는 DI(Dependency Injection)라고도 불림.


  • BeanFactory와 ApplicationContext

    • IoC 컨테이너는 스프링 패키지의 BeanFactory와 ApplicationContext 인터페이스로 구현됨.

    • BeanFactory: IoC 컨테이너의 핵심 인터페이스로, 빈 생성과 의존성 주입을 처리.

    • ApplicationContext: BeanFactory의 확장 버전으로, 엔터프라이즈급 기능을 제공.

      • 예: 국제화 지원, 이벤트 발행, 웹 애플리케이션 컨텍스트(WebApplicationContext) 등.

  • ApplicationContext의 주요 기능

    • 통합 라이프 사이클 관리.

    • BeanPostProcessor와 BeanFactoryPostProcessor 자동 등록.

    • 국제화 메시지 처리(MessageSource).

    • 이벤트 발행(ApplicationEvent).

    • 웹 애플리케이션을 위한 특화된 기능 제공.


  • 설정 메타데이터(Configuration Metadata)

    • IoC 컨테이너는 설정 메타데이터를 기반으로 객체를 생성하고 의존성을 주입.

    • 설정 방법:

      • XML 설정: 전통적인 방식.

      • 어노테이션: 현대적인 방식으로 사용 빈도 높음.

      • 자바 기반 설정: @Configuration 클래스 사용.


  • 스프링 컨테이너의 동작 방식

    • 개발자가 비즈니스 객체와 설정 메타데이터를 제공하면, IoC 컨테이너는 이를 조합하여 완전한 설정된 시스템을 생성하고 관리.

Bean과 Bean 범위 정의

  • Bean의 정의

    • Bean은 IoC 컨테이너가 관리하는 자바 객체로, 설정 메타데이터를 기반으로 생성 및 관리된다.

    • 각 Bean은 고유 식별자(Unique Identifier)를 가지며, 별칭(alias)으로 여러 이름을 가질 수 있다.

  • Bean 생성 방식

    • Bean은 XML, 자바 코드, 어노테이션을 통해 정의할 수 있다.

      • ex. 자바 기반으로 @Configuration 클래스 안에 @Bean 어노테이션을 사용하여 Bean을 생성.
  • 예시 코드

    @Configuration
    public class AppConfig {
      @Bean(initMethod = "init", destroyMethod = "destroy", name = {"sampleBean", "sb"})
      public SampleBean sampleBean() {
          return new SampleBean();
      }
    
      @Bean
      public BeanInterface beanInterface() {
          return new BeanInterfaceImpl();
      }
    }
    
    • bean 어노테이션 속성을 사용하면 bean의 초기화(init) 및 파괴(destruction) 수명 주기 메소드도 전달할 수 있다.
  • Bean 이름과 별칭

    • Bean의 기본 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 형태(SampleBean → sampleBean)이다.

    • name 속성을 사용하여 여러 별칭(예: sampleBean, sb)을 부여 가능하다.

  • Bean과 Component 어노테이션

    • @Bean은 @Component 어노테이션 내부에서 동작하며, @Configuration이 붙은 클래스의 메서드에서 Bean을 반환하도록 설정한다.

    • 추가 애노테이션:

      • @Controller, @Service, @Repository: 각각의 역할에 따라 자동으로 Bean으로 등록한다.
  • 설명 애노테이션

    • @Description: Bean을 설명하는 데 사용되며, 모니터링 도구에서 유용하게 활용 가능하다.

@ComponentScan 어노테이션

  • @ComponentScan의 역할

    • @ComponentScan은 Bean 자동 스캔을 허용하는 스프링 애노테이션이다.

    • 지정된 패키지 내에서 @Component, @Configuration, @Controller, @Service, @Repository 등의 애노테이션이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록한다.

  • 기본 스캔 동작

    • 스프링 부트는 기본적으로 @ComponentScan을 사용하여 애플리케이션의 기본 패키지와 하위 패키지를 스캔한다.

    • 스캔 범위를 설정하려면 basePackages 또는 basePackageClasses 속성을 사용한다.

  • 스캔 범위 지정

    • basePackages 속성: 특정 패키지를 문자열로 지정
      ex. @ComponentScan(basePackages = "com.example.package")

    • basePackageClasses 속성: 클래스 기준으로 스캔 범위를 지정
      ex. @ComponentScan(basePackageClasses = AppConfig.class)

  • 다중 패키지 스캔
    여러 패키지를 스캔하려면 @ComponentScans를 사용하여 스캔 대상 패키지를 나열.

    @ComponentScans({
      @ComponentScan(basePackages = "com.example.package1"),
      @ComponentScan(basePackageClasses = AppConfig.class)
    })
    public class AppConfig {
      // 코드
    }
    

Bean의 범위

스프링 컨테이너는 Bean 인스턴스를 생성할 때, 범위(scope)에 따라 인스턴스를 관리한다. 기본 범위는 싱글톤(singleton)이며, 필요에 따라 다른 범위를 설정할 수 있다.

  • singleton

    • IoC 컨테이너당 하나의 인스턴스만 생성한다.

    • 모든 요청에서 동일한 인스턴스를 사용한다.

    • ex.

      ```
      @Bean
      public SingletonBean singletonBean() {
        return new SingletonBean();
      }
      
      ```

  • Prototype

    • 요청할 때마다 새로운 인스턴스를 생성한다.

    • 상태를 공유하지 않는 객체에 적합하다.

    • ex.

      @Bean
      @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
        public PrototypeBean prototypeBean() {
          return new PrototypeBean();
      }
      

  • Request (웹 요청 범위)

    • HTTP 요청마다 새로운 인스턴스를 생성한다.

    • ex.

      @Bean
      @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
      public RequestScopedBean requestScopedBean() {
        return new RequestScopedBean();
      }
      
      또는
      
      @RequestScope
      public RequestScopedBean requestScopedBean() {
        return new RequestScopedBean();
      }

  • Session(세션 범위)

    • HTTP 세션마다 새로운 인스턴스를 생성한다.

    • ex.

      @SessionScope
      public SessionScopedBean sessionScopedBean() {
        return new SessionScopedBean();
      }
      

  • Application (서블릿 컨텍스트 범위)

    • 애플리케이션당 하나의 인스턴스를 생성한다.

    • ex.

      @ApplicationScope
      public ApplicationScopedBean applicationScopedBean() {
        return new ApplicationScopedBean();
      }
      

  • WebSocket

    • WebSocket 세션마다 새로운 인스턴스를 생성한다.

    • ex.

      @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
      public WebSocketScopedBean webSocketScopedBean() {
        return new WebSocketScopedBean();
      }

자바를 사용하여 bean 설정

@Import 어노테이션

  • 둘 이상의 설정 클래스가 있는 경우 설정을 모듈화 하는데 사용된다.

  • @Import 애노테이션은 설정된 클래스들에서 bean 정의를 가져와 컨텍스트(컨테이너)를 자동으로 인스턴스화할 때 사용된다.

  • 스프링 부트는 자동 설정을 제공하므로 일반적으로 @Import를 사용할 필요가 없다.

  • 수동으로 컨텍스트를 구성해야 하는 경우에는 @Import를 사용하여 설정을 모듈화할 수 있다.

  • ex.

    @Configuration
     public class FooConfig {
       @Bean
       public FooBean fooBean() {
           return new FooBean();
       }
     }
     
    @Configuration
    @Import(FooConfig.class)
    public class BarConfig {
      @Bean
      public BarBean barBean() {
          return new BarBean();
      }
    }
    
    public static void main(String[] args) {
      ApplicationContext appContext = new AnnotationConfigApplicationContext(BarConfig.class);
    
      // 컨테이너에서 FooBean과 BarBean 가져오기
      FooBean fooBean = appContext.getBean(FooBean.class);
      BarBean barBean = appContext.getBean(BarBean.class);
    
      System.out.println("FooBean: " + fooBean);
      System.out.println("BarBean: " + barBean);
    }
    • new AnnotationConfigApplicationContext(BarConfig.class)
      BarConfig를 기반으로 스프링 컨테이너 생성. @Import를 통해 FooConfig도 함께 로드되므로, FooBean과 BarBean 모두 컨테이너에 등록된다.

    • getBean(Class requiredType)
      스프링 컨테이너에서 지정된 타입의 Bean을 가져온다.

    • 컨테이너를 인스턴스화 하는 동안 BarConfig를 제공하여 위와 같이 스프링 컨테이너 안에서 FooBean과 BarBean 정의를 모두 가져올 수 있다.


@DependsOn 어노테이션

  • 스프링 컨테이너는 Bean 초기화 순서를 자동으로 관리하지만, 특정 Bean이 다른 Bean에 의존할 경우 명시적으로 초기화 순서를 정해야 할 때가 있다.

  • @DependsOn은 지정된 Bean이 먼저 초기화되도록 보장하여 의존성 문제를 방지한다.

  • 만약 초기화 순서가 올바르지 않으면 NoSuchBeanDefinitionException 예외가 발생할 수 있다.

  • ex.

    @Configuration 
    public class AppConfig {
    
      @Bean
      public FooBean fooBean() {
        return new FooBean();
      }
    
      @Bean
      public BarBean barBean() {
          return new BarBean();
      }
    
      @Bean
      @DependsOn({"fooBean", "barBean"})
      public BazBean bazBean() {
          return new BazBean();
      }
    }
    
    • fooBean()과 barBean() 메서드는 각각 FooBean과 BarBean 객체를 생성하고 컨테이너에 등록한다.

    • @DependsOn({"fooBean", "barBean"}): bazBean() 메서드가 실행되기 전에 fooBean과 barBean이 먼저 초기화되도록 지정.


DI 코딩 방법

  • 의존성 주입(DI)은 클래스 간의 결합도를 낮춰 유지보수성과 확장성을 높인다.

  • 스프링 컨테이너(ApplicationContext)는 Bean 설정 메타데이터를 기반으로 필요한 의존성을 자동으로 주입한다.

  • 생성자 주입 방식을 통해 의존성 분리를 구현하면 코드의 유연성과 재사용성이 크게 향상된다.

  • DI 적용 전

    public class CartService {
      private CartRepository repository;
      
      public CartService() {
          this.repository = new CartRepositoryImpl();
      }
    }
    • CartService는 CartRepository에 의존하며, 생성자 내에서 CartRepositoryImpl 객체를 직접 생성한다.

    • 해당 방식은 모듈가 결합도가 높아 테스트나 유지보수에 어려움을 줄 수 있다.


  • DI 적용 후

    public class CartService {
      private CartRepository repository;
    
      public CartService(CartRepository repository) {
          this.repository = repository;
      }
    }
    
    • 의존성을 분리하기 위해 생성자 주입 방식을 사용한다.

    • CartRepository 구현체는 외부에서 주입되므로 유연성과 테스트 용이성이 증가한다.

cf. Bean은 다른 Bean에 의존성을 가질 수 있다. 예를 들어, CartService는 CartRepository라는 객체를 필요로 하고,
이런 의존성은 생성자, 설정자 메서드(setter), 또는 클래스의 프로퍼티를 통해 정의할 수 있다.


생성자로 의존성 정의

  • 객체가 필요한 의존성(다른 객체)을 직접 생성하지 않고 생성자로 주입받는 것을 의미한다.

  • 이를 통해 객체 간 결합도를 낮추고, 유연하고 테스트하기 쉬운 코드를 작성할 수 있다.

  • ex.

    @Configuration
    public class AppConfig {
    
      @Bean
      public CartRepository cartRepository() {
          return new CartRepositoryImpl();  // CartRepository 구현체 생성 및 반환
      }
    
      @Bean
      public CartService cartService() {
          return new CartService(cartRepository());  // CartRepository를 생성자 주입
      }
    }

설정자 메소드로 의존성 정의

  • 생성자를 사용하는 대신, 설정자 메서드(Setter Method)를 통해 객체의 의존성을 주입하는 방식이다.

  • 객체 생성 후, setter 메서드를 호출하여 필요한 의존성을 설정한다.

  • 선택적 의존성 주입이나 객체 생성 후 의존성을 설정해야 할 때 유용하다.

  • ex.

    public class CartService {
      private CartRepository repository;  // CartRepository 의존성
    
      // Setter 메서드로 의존성 주입
      public void setCartRepository(CartRepository repository) {
          this.repository = repository;
      }
    }
    
    @Configuration
    public class AppConfig {
    
      @Bean
      public CartRepository cartRepository() {
          return new CartRepositoryImpl();  // CartRepository 구현체 반환
      }
    
      @Bean
      public CartService cartService() {
          CartService service = new CartService();  // CartService 인스턴스 생성
          service.setCartRepository(cartRepository());  // Setter로 CartRepository 주입
          return service;
      }
    }
    

클래스 프로퍼티를 사용한 의존성 정의

@Service
public class CartService {
    @Autowired
    private CartRepository repository; // CartRepository 의존성 주입
}
  • @Service: 해당 클래스가 비즈니스 로직을 수행하는 Service Layer임을 나타낸다.

  • @Autowired: Spring 컨테이너에서 CartRepository 타입의 Bean을 찾아 자동으로 주입한다.


어노테이션을 사용하여 bean의 메타데이터 설정

  • 스프링 프레임워크는 bean에 대한 메타데이터를 설정하기 위한 많은 어노테이션을 제공한다.

  • ex. @Autowired, @Qualifier, @ Inject, @Resource, @Primary, @Value


@Autowired

  • @Autowired는 Spring Framework에서 의존성을 자동으로 주입하기 위해 사용하는 어노테이션이다.

  • 개발자가 별도의 설정 클래스나 XML 파일을 작성하지 않아도, 필드, 생성자, 메서드에 Bean을 자동으로 주입할 수 있도록 도와준다.

  • 매칭 모호성을 제거하기 위해 일치하는 bean들은 타입별 일치(type matching), 한정자 일치(qualifier matching), 또는 이름 일치(name matching)를 사용하여 찾아내고 주입된다.

  • 사용 위치

    • 필드

      필드 주입은 테스트와 유지보수에 어려움이 있을 수 있으므로 생성자 주입을 권장한다.

    • 생성자

    • 메서드

  • ex.

    @Component
    public class CartService {
    
      // 필드 주입
      @Autowired
      private CartRepository repository;
    
      // 생성자 주입
      @Autowired
      public CartService(CartRepository cartRepository) {
          this.repository = cartRepository;
      }
    
      // 설정자 주입
      @Autowired
      public void setARepository(ARepository aRepository) {
          this.aRepository = aRepository;
      }
    
      // 임의 메서드 주입
      @Autowired
      public void xMethod(BRepository bRepository, CRepository cRepository) {
          this.bRepository = bRepository;
          this.cRepository = cRepository;
      }
    }
    

  • ex2.

    // CartRepository 인터페이스
    public interface CartRepository {
      void save(String item);
    }
    
    // CartRepository 구현체
    @Repository  // Spring 컨테이너에 Bean으로 등록
    public class CartRepositoryImpl implements CartRepository {
      @Override
      public void save(String item) {
          System.out.println("Item saved: " + item);
      }
    }
    
    // CartService 클래스
    @Service  // Spring 컨테이너에 Bean으로 등록
    public class CartService {
      @Autowired  // CartRepository Bean 자동 주입
      private CartRepository cartRepository;
    
      public void addItem(String item) {
          cartRepository.save(item);
          System.out.println("Item added: " + item);
      }
    }
    

한정자별 일치(Match by qualifier)

@Configuration
public class AppConfig {

    @Bean
    public CartService cartService1() {
        return new CartServiceImpl1();
    }

    @Bean
    public CartService cartService2() {
        return new CartServiceImpl2();
    }
}

@Controller
public class CartController {
    @Autowired
    private CartService service1;  // NoUniqueBeanDefinitionException 발생
    @Autowired
    private CartService service2;
}
  • 동일한 타입의 Bean이 여러 개 존재할 경우, Spring 컨테이너는 어떤 Bean을 주입해야 할지 결정하지 못한다. 이로 인해 NoUniqueBeanDefinitionException이 발생한다.

  • 위와 같이 CartService 타입의 Bean이 두 개(cartService1, cartService2)이므로 Spring이 어떤 Bean을 주입할지 결정하지 못한다.



@Configuration
public class AppConfig {

    @Bean
    public CartService cartService1() {
        return new CartServiceImpl1();
    }

    @Bean
    public CartService cartService2() {
        return new CartServiceImpl2();
    }
}

@Controller
public class CartController {

    @Autowired
    @Qualifier("cartService1")  // cartService1 Bean을 주입
    private CartService service1;

    @Autowired
    @Qualifier("cartService2")  // cartService2 Bean을 주입
    private CartService service2;
}
  • 위와 같이 @Qualifier 어노테이션을 사용하여 해결한다.

  • @Qualifier를 사용하면 주입받을 Bean의 이름을 명시적으로 지정할 수 있다.

    cf. @Bean으로 정의한 Bean의 이름은 메서드 이름과 동일하다.
    ex. cartService1() 메서드의 Bean 이름은 cartService1.


이름으로 일치(Match by name)

  • Spring은 기본적으로 타입(Type)을 기준으로 Bean을 주입한다.

  • 하지만, 필드 이름과 Bean 이름이 일치하면 이름 매칭으로도 주입이 가능하다.

  • 이때 @Service, @Component 등의 애노테이션에서 설정한 Bean 이름이 중요하다.


다음과 같이 bean을 등록할 때 이름을 지정할 수가 있다.

@Service(value = "cartServc")  // Bean 이름을 "cartServc"로 설정
public class CartService {
    // 서비스 로직
}

@Controller
public class CartController {
    @Autowired
    private CartService cartServc;  // 필드 이름과 Bean 이름이 일치하므로 자동 주입
}

  • @Autowired가 적용된 CartController의 cartServc 필드는 필드 이름과 동일한 Bean 이름(cartServc)을 가진 CartService Bean을 찾아 주입한다.

  • 만약, 필드 이름과 Bean 이름이 일치하지 않으면 NoUniqueBeanDefinitionException이 발생한다.

  • 유사 어노테이션 참고

    • @Autowired
      타입 매칭 우선, 이름 매칭은 필드 이름과 Bean 이름이 일치할 때만 동작. (타입 -> 한정자 -> 이름)

    • @Inject
      javax.inject 패키지에 속하며, Spring의 @Autowired와 거의 동일하게 동작. (타입 -> 한정자 -> 이름)

    • @Resource
      이름 매칭을 우선으로 함. 이름이 없으면 타입 매칭. (이름 -> 타입 -> 한정자)


@Primary 어노테이션

  • @Primary는 Spring Framework에서 제공하는 애노테이션으로, 동일한 타입의 Bean이 여러 개 존재할 때, 기본적으로 주입될 Bean을 지정하기 위해 사용한다.

  • @Qualifier와 유사한 역할을 하지만, 보다 간단하게 기본 Bean을 지정할 수 있다.

  • @Autowired를 사용할 때, @Primary가 지정된 Bean이 기본적으로 주입된다.


다음과 같이 @Autowired 어노테이션으로 주입될 기본 bean을 설정할 수가 있다.
@Configuration
public class AppConfig {

    @Bean
    @Primary  // 기본으로 주입될 Bean 설정
    public CartService cartService1() {
        return new CartServiceImpl1();
    }

    @Bean
    public CartService cartService2() {
        return new CartServiceImpl2();
    }
}

@Controller
public class CartController {

    @Autowired
    private CartService service;  // cartService1()이 기본적으로 주입됨
}

cf. @Primary vs @Qualifier

특징@Primary@Qualifier
용도기본으로 사용할 Bean 지정특정 Bean을 명시적으로 지정
적용 방식한 번 설정하면 기본값으로 동작주입 시마다 Bean 이름을 지정해야 함
사용 편리성간단하고 직관적더 세밀하게 Bean을 선택할 수 있음
우선 순위@Qualifier가 지정된 경우, @Primary는 무시됨@Primary보다 높은 우선순위

@Value 어노테이션

  • Spring Framework에서 제공하는 애노테이션으로, 외부 프로퍼티 파일에 정의된 값을 필드, 메서드 매개변수, 또는 생성자 매개변수에 주입할 때 사용한다.

  • 주로 application.properties 또는 application.yml 파일에 정의된 설정 값을 코드에 주입할 때 활용한다.


// application.properties
// default.currency=USD

@Configuration
@PropertySource("classpath:application.properties")  // 프로퍼티 파일을 로드
public class AppConfig {}

@Controller
public class CartController {

    @Value("${default.currency}")  // 프로퍼티 파일의 default.currency 값을 주입
    private String defaultCurrency;

    public void printCurrency() {
        System.out.println("Default Currency: " + defaultCurrency);
    }
}

cf. 스프링 부트를 사용할 경우 @PropertySource를 사용할 필요가 없고, src/main/resource 디렉토리 아래에 application.yml 또는 프로퍼티 파일을 두기만 하면된다.


AOP용 코드 작성

  • AOP(Aspect-Oriented Programming)는 횡단 관심사(Cross-Cutting Concerns)를 모듈화하여 코드 중복을 줄이고 핵심 로직과 부가 로직을 분리하는 것을 의미한다.

    횡단 관심사(Cross-Cutting Concerns): 애플리케이션 전반에 걸쳐 공통적으로 적용되어야 하는 로직(예: 로깅, 트랜잭션, 성능 모니터링 등)을 횡단 관심사라고 칭한다.

  • 코드 중복을 줄이고 핵심 비즈니스 로직과 부가적인 관심사를 분리하여 유지보수성을 향상시킨다.


예시: 로깅 모니터링 시간 측정 AOP

1. 커스텀 어노테이션 정의: @TimeMonitor

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

cf. public @interface TimeMonitor는 커스텀 애노테이션(Custom Annotation)을 정의하기 위한 구문이다.
스프링 부트에서는 기본적으로 제공되는 애노테이션(@Component, @Service, @RestController 등)을 많이 사용하지만, 특정 요구사항에 맞춘 사용자 정의 어노테이션을 만들어 사용할 수 있다.

  • @Target(ElementType.METHOD)
    어노테이션을 어디에 사용할 수 있는지 지정한다. 즉, 어노테이션의 적용 대상을 제한하는 역할을 한다.
    @Target에 설정 가능한 값은 java.lang.annotation.ElementType 열거형(enum)으로 정의되어 있다.

    주요 값은 다음과 같다:

    • ElementType.TYPE: 클래스, 인터페이스, 열거형, 애노테이션에 사용 가능하다.

    • ElementType.FIELD: 필드(멤버 변수)에 사용 가능하다.

    • ElementType.METHOD: 메서드에 사용 가능하다.

    • ElementType.PARAMETER: 메서드 매개변수에 사용 가능하다.

    • ElementType.CONSTRUCTOR: 생성자에 사용 가능하다.

    • ElementType.LOCAL_VARIABLE: 로컬 변수에 사용 가능하다.

    • ElementType.ANNOTATION_TYPE: 다른 애노테이션에 사용 가능하다.

    • ElementType.PACKAGE: 패키지 선언에 사용 가능하다.


  • @Retention(RetentionPolicy.RUNTIME)
    어노테이션의 유지 정책(Retention Policy)을 정의한다. 즉, 어노테이션이 얼마나 오래 유지되는지를 설정한다.
    어노테이션 정보를 런타임까지 유지하여 리플렉션이나 AOP에서 참조할 수 있도록 설정한다.

    리플렉션(Reflection): 프로그램이 실행 중에 클래스, 메서드, 필드, 어노테이션 등을 읽는 기술.

    주요 값은 다음과 같다. java.lang.annotation.RetentionPolicy 열거형(enum)으로 정의되어 있다:

    • RetentionPolicy.SOURCE:
      소스 코드에만 존재하고, 컴파일 후에는 사라진다. 컴파일러가 사용하는 어노테이션에 적합.
      ex. @Override

    • RetentionPolicy.CLASS:
      컴파일된 클래스 파일(.class)에는 남아 있지만, JVM 실행 시에는 읽을 수 없음.
      해당 값이 기본값으로 설정되어 있다.
      ex. 일부 내부 어노테이션.

    • RetentionPolicy.RUNTIME:
      런타임 동안 JVM에 의해 유지된다. 리플렉션을 사용하여 런타임에 어노테이션 정보에 접근 가능하다.
      ex. @Autowired, @Controller


2. Aspect 클래스: TimeMonitorAspect

@Aspect
@Component
public class TimeMonitorAspect {

    @Around("@annotation(com.packt.modern.api.TimeMonitor)")
    public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis(); // 시작 시간 기록
        Object proceed = joinPoint.proceed();    // 실제 메서드 실행
        long executionTime = System.currentTimeMillis() - start; // 실행 시간 계산
        System.out.println(joinPoint.getSignature() + " takes: " + executionTime + " ms");
        return proceed; // 메서드 결과 반환
    }
}
profile
IT, 개발 관련 정보들을 기록하는 장소입니다.

0개의 댓글