LandvibeDev / 2024-spring5-programming-introduction

스프링5 프로그래밍 입문 레포지토리입니다. (2024 썸머코딩 서버)
0 stars 0 forks source link

[4주차] 정건우 박지현 홍수진 #2

Open d11210920 opened 1 month ago

d11210920 commented 1 month ago
jgw1202 commented 1 month ago

1. 의존과 DI를 통한 의존처리

DI는 'Dependency Injection"의 약자로 의존성 주입이라고 합니다.

그렇다면 의존이란 무엇일까요? 간단한 코드로 의존을 설명할 수 있습니다.

public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}
public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // 직접 엔진 객체를 생성
    }

    public void start() {
        engine.start();
        System.out.println("Car started");
    }

    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

위 코드는 Car 클래스가 Engine 클래스에 강하게 결합되어 있습니다. Car 클래스의 생성자에서 Engine 객체를 직접 생성하고 있기 때문에, Engine 클래스의 변경이나 다른 Engine 구현체로의 변경이 필요할 때 Car 클래스도 변경을 해야 합니다. 이는 코드의 유연성과 테스트 가능성을 저하시킵니다.

따라서 의존이란, 변경에 따른 영향이 전파되는 관계를 의미합니다. 클래스 내부에서 직접 의존 객체를 생성하는 것은 프로그래밍을 할 때 매우 쉽지만, 유지보수적 관점에서 바라볼 때, 문제점을 야기하기엔 충분합니다. 변경에 따른 영향이 전파되기 때문이죠. 그렇다면 이를 어떻게 해결해야 할까요?

public interface Engine {
    void start();
}

public class GasolineEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gasoline Engine started");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric Engine started");
    }
}
public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine; // 의존성 주입
    }

    public void start() {
        engine.start();
        System.out.println("Car started");
    }

    public static void main(String[] args) {
        Engine gasolineEngine = new GasolineEngine();
        Car carWithGasolineEngine = new Car(gasolineEngine);
        carWithGasolineEngine.start();

        Engine electricEngine = new ElectricEngine();
        Car carWithElectricEngine = new Car(electricEngine);
        carWithElectricEngine.start();
    }
}

위의 코드에서 Car 클래스는 이제 Engine 인터페이스에 의존하고, 구체적인 Engine 구현체(GasolineEngine 또는 ElectricEngine)는 외부에서 주입됩니다.

이렇게 하면 Car 클래스는 특정 Engine 구현체에 강하게 결합되지 않으며, 필요에 따라 다양한 Engine 구현체를 사용할 수 있습니다.

2. 객체 조립기

main 메소드에서 의존 대상 객체를 생성하고 주입하는 방법이 나쁘지는 않습니다. 하지만 좀 더 나은 방법은 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것 입니다. 이러한 클래스를 객체 조립기라고도 표현합니다.

위에서 사용한 코드에서 객체 조립기를 활용하여 코드를 확장해보았습니다.

public interface Engine {
    void start();
}

public class GasolineEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gasoline Engine started");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric Engine started");
    }
}
public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine; // 의존성 주입
    }

    public void start() {
        engine.start();
        System.out.println("Car started");
    }
}
public class CarAssembler {  // 객체 조립기
    public Car assembleCar(String engineType) {
        Engine engine = createEngine(engineType);
        return new Car(engine);
    }

    private Engine createEngine(String engineType) {
        switch (engineType) {
            case "Gasoline":
                return new GasolineEngine();
            case "Electric":
                return new ElectricEngine();
            default:
                throw new IllegalArgumentException("Unknown engine type: " + engineType);
        }
    }
}
public class Main { 
    public static void main(String[] args) {
        CarAssembler assembler = new CarAssembler();

        Car carWithGasolineEngine = assembler.assembleCar("Gasoline");
        carWithGasolineEngine.start();

        Car carWithElectricEngine = assembler.assembleCar("Electric");
        carWithElectricEngine.start();
    }
}

Engine 인터페이스 및 구현 클래스는 Engine 인터페이스와 두 가지 구현체(GasolineEngine 및 ElectricEngine)를 정의합니다.

Car 클래스는 Engine 인터페이스를 의존성으로 받아서 사용합니다. 이를 통해 Car 클래스는 특정 엔진 구현체에 의존하지 않습니다.

CarAssembler 클래스는 Car 객체를 조립하고 필요한 Engine 객체를 생성합니다.

assembleCar 메서드는 주어진 엔진 타입에 따라 적절한 Engine 구현체를 생성하고, 이를 Car 객체에 주입합니다.

Main 클래스에서는 CarAssembler를 사용하여 Car 객체를 생성합니다.

엔진 타입을 문자열로 전달하여 다양한 조합의 Car 객체를 쉽게 생성할 수 있습니다.

이렇게 DI와 객체 조립기를 사용하면 코드의 유연성과 재사용성을 크게 향상시킬 수 있습니다.

또한, 객체 생성과 의존성 주입 로직을 분리함으로써 코드의 가독성과 유지보수성을 높일 수 있습니다.

3. 스프링의 DI 설정

스프링은 앞서 구현한 객체 조립기와 유사한 기능을 제공합니다. 즉 스플링은 Assembler 클래스의 생성자 코드처럼 필요한 객체를 생성하고 생성한 객체에 의존성을 주입합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public Engine gasolineEngine() {
        return new GasolineEngine();
    }

    @Bean
    public Engine electricEngine() {
        return new ElectricEngine();
    }

    @Bean
    public Car carWithGasolineEngine() {
        return new Car(gasolineEngine());
    }

    @Bean
    public Car carWithElectricEngine() {
        return new Car(electricEngine());
    }
}

위 코드는 스프링 설정 클래스입니다. @Configuration 어노테이션은 스프링의 설정 클래스임을 의미하는 어노테이션입니다. @Bean은 해당 메서드가 생성한 객체를 스프링 빈으로 등록합니다. 위 코드에선 gasolineEngine, electricEngine, carWithGasolineEngine, carWithElectricEngine 빈을 정의합니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Car carWithGasolineEngine = context.getBean("carWithGasolineEngine", Car.class);
        carWithGasolineEngine.start();

        Car carWithElectricEngine = context.getBean("carWithElectricEngine", Car.class);
        carWithElectricEngine.start();
    }
}

위 메인 클래스에서 AnnotationConfigApplicationContext를 사용하여 스프링 컨텍스트를 초기화하고 AppConfig 클래스를 설정으로 로드합니다. context.getBean 메서드를 사용하여 스프링 컨테이너로부터 Car 빈을 가져와서 사용할 수 있습니다.

DI 방식 1 : 생성자 주입 방식

  1. 불변성 보장: 의존성이 객체 생성 시에 주입되므로, 주입된 이후에는 변경될 수 없어서 객체의 상태가 일관되게 유지됩니다.
  2. 필수 의존성 보장: 생성자를 통해 모든 필수 의존성을 주입받으므로, 객체가 생성될 때 필요한 모든 의존성을 갖추게 됩니다.
  3. 불변 객체 지원: 의존성이 변하지 않기 때문에 불변 객체를 만들기 쉽습니다.

DI 방식 2 : 세터 주입 방식

  1. 유연성: 의존성을 나중에 변경할 수 있으므로 더 유연한 객체 구성을 지원합니다.
  2. 선택적 의존성: 필수가 아닌 선택적인 의존성을 쉽게 주입할 수 있습니다.
  3. 가독성: 생성자 매개변수가 많지 않아서 코드가 간결해질 수 있습니다.

뭐가 더 낫나?

일반적으로는 생성자 주입 방식 이 더 권장됩니다. 그 이유는 객체 상태의 불변인 불변성, 모든 의존성을 생성 시점에 주입받으므로 일관성, 생성자 주입은 의존성을 명시적으로 주입하므로 명시적 주입이 가능하기 때문입니다.

4. @Configuration 설정 클래스의 @Bean 설정과 싱글톤

스프링 컨테이너가 생성한 빈은 싱글톤 객체입니다. 스프링 컨테이너는 @Bean 이 붙은 메소드에 대해 단 1개의 객체만을 생성합니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Car carWithGasolineEngine1 = context.getBean("carWithGasolineEngine", Car.class);
        Car carWithGasolineEngine2 = context.getBean("carWithGasolineEngine", Car.class);

        System.out.println("carWithGasolineEngine1 == carWithGasolineEngine2: " + (carWithGasolineEngine1 == carWithGasolineEngine2));

        Engine gasolineEngine1 = context.getBean("gasolineEngine", Engine.class);
        Engine gasolineEngine2 = context.getBean("gasolineEngine", Engine.class);

        System.out.println("gasolineEngine1 == gasolineEngine2: " + (gasolineEngine1 == gasolineEngine2));
    }
}

@Bean 애노테이션이 붙은 메소드는 기본적으로 싱글톤 스코프를 가집니다. 이는 스프링 컨테이너가 해당 메소드에 대해 단 한 번만 호출되어 객체를 생성하고, 그 후에는 동일한 인스턴스를 반환하는 것을 의미합니다.

<<실행결과>> Gasoline Engine started Car started Gasoline Engine started Car started carWithGasolineEngine1 == carWithGasolineEngine2: true gasolineEngine1 == gasolineEngine2: true

실행결과를 보면 carWithGasolineEngine1과 carWithGasolineEngine2는 동일한 인스턴스임을 확인할 수 있습니다. gasolineEngine1과 gasolineEngine2 또한 동일한 인스턴스임을 확인할 수 있습니다. 이를 통해 스프링 컨테이너가 @Bean 애노테이션이 붙은 메소드에 대해 단 한 번만 객체를 생성하여 싱글톤으로 관리한다는 것을 알 수 있습니다.

@Autowired ?

스프링 컨테이너는 설정 클래스에서 사용한 @Autowired에 대해 자동 주입 처리를 해줍니다. 스프링은 @Configuration 어노테이션이 붙은 설정 클래스를 내부적으로 스프링 빈으로 등록합니다. 그리고 다른 빈과 마찬가지로 @Autowired가 붙은 대상에 대해 알맞은 빈을 자동으로 주입해줍니다.

간단한 예시 코드를 살펴봅시다.

public class MessageService {
    public String getMessage() {
        return "Hello, World!";
    }
}

MessageService 클래스: 단순히 메시지를 반환하는 메소드를 가진 서비스 클래스입니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Printer {
    private MessageService messageService;

    // 필드 주입
    @Autowired
    private MessageService messageService;

    public void printMessage() {
        System.out.println(messageService.getMessage());
    }
}

Printer 클래스: MessageService에 의존하는 클래스입니다. @Autowired 애노테이션을 사용하여 MessageService 빈을 간단하게 필드 주입 방식으로 주입받습니다. printMessage 메소드는 주입된 MessageService의 getMessage 메소드를 호출하여 메시지를 출력합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {

    @Bean
    public MessageService messageService() {
        return new MessageService();
    }
}

Spring Configuration 클래스 (AppConfig): @Configuration 애노테이션을 사용하여 스프링 설정 클래스를 정의합니다. @Bean 애노테이션을 사용하여 MessageService 빈을 정의합니다. @ComponentScan 애노테이션을 사용하여 Printer 클래스를 자동으로 스캔하고 빈으로 등록합니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Printer printer = context.getBean(Printer.class);
        printer.printMessage();
    }
}

Main 클래스: AnnotationConfigApplicationContext를 사용하여 스프링 컨텍스트를 초기화하고 AppConfig 클래스를 설정으로 로드합니다. context.getBean 메소드를 사용하여 Printer 빈을 가져와서 사용합니다.

jlhyunii commented 1 month ago

Chapter 6. 빈 라이프사이클과 범위



1. 컨테이너 초기화와 종료


// 1. 컨테이너 초기화
AnnotationConfigApplicationContext ctx =
               new AnnotationConfigApplicationContext(AppContext.class);

// 2. 컨테이너에서 빈 객체를 구해서 사용
Greeter g = ctx.getBeen("greeter", Greeter.class);
String msg = g.greet("스프링");
System.out.println(msg);

// 3. 컨테이너 종료
ctx.close();


컨테이너 초기화 -> 빈 객체의 생성, 의존 주입, 초기화 컨테이너 종료 -> 빈 객체의 소멸



2. 스프링 빈 객체의 라이프사이클


스프링 컨테이너는 빈 객체의 라이프사이클을 관리한다.

빈 객체의 초기화와 소멸 : 스프링 인터페이스


public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

public interface DisposableBean {
    void destroy() throws Exception;
}


스프링에서 두 인터페이스에 이 메서드를 정의하고 있다면?

예시

  • 데이터베이스 커넥션 풀 : 초기화 과정에서 데이터베이스 연결 생성하고, 소멸 과정에서 사용중인 데이터베이스 연결 끊는다.
  • 채팅 클라이언트 : 초기화 과정에서 서버와의 연결을 생성하고, 소멸 과정에서 끊는 작업을 수행한다.


빈 객체의 초기화와 소멸 : 커스텀 메서드


만약 InitializingBean, DisposableBean 인터페이스를 구현할 수 없거나 사용하고 싶지 않을 때는 어떻게 해야할까?

-> @Bean 태그에서 initMethod 속성과 destroyMethod 속성을 사용해서 초기화 메서드와 소멸 메서드의 이름을 지정하면 된다!


// Client2 클래스를 빈으로 사용하는 경우
@Bean(initMethod = "connect", destroyMethod = "close")
public Client2 client2() {
    Client2 client = new Client2();
    client.setHost("host");
    return client;
}


그럼 initMethod 속성을 사용하는 대신 빈 설정 메서드에서 직접 초기화를 해도 될까?

// 설정 코드에서 초기화 메서드를 직접 실행한 경우
@Bean(destroyMethod = "close")
public Client2 client2() {
    Client2 client = new Client2();
    client.setHost("host");
    client.connect();
    return client;
}


! 주의해야할 점 !


" 초기화 메서드가 두 번 호출되지 않도록 해야한다. "

@Bean
public Client client() {
    Client client = new Client();
    client.setHost("host");
    client.afterPropertiesSet();
    return client;
}

초기화 관련 메서드를 빈 설정 코드에서 직접 실행할 때, 초기화 메서드가 두 번 호출되지 않도록 주의해야한다.



3. 빈 객체의 생성과 관리 범위


싱글톤


한 식별자에 대해 한 개의 객체만 존재하는 빈의 범위를 말한다.

Client client1 = ctx.getBean("client", Client.class);
Client client2 = ctx.getBean("client", Client.class);
// client1 == client2 -> true


@Scope


특정 빈을 프로토타입 범위로 지정하려면 이 애노테이션을 사용해야 한다.

(생략)
.
.
@Bean
@Scope("prototype")
public Client client() {
    Client client = new Client();
    client.setHost("host");
    return client;
}


즉, 프로토타입 범위의 빈을 설정하면 빈 객체를 구할 때마다 매번 새로운 객체를 생성한다.

Client client1 = ctx.getBean("client", Client.class);
Client client2 = ctx.getBean("client", Client.class);
// client1 != client2 -> true


한계






Chapter 7. AOP 프로그래밍



1. AOP를 구현하기 위한 build.gradle


dependencies {
    (생략)
    .
    .
    implementation 'org.springframework:spring-context:5.3.9' // 버전은 필요에 따라 조정
    implementation 'org.aspectj:aspectjweaver:1.9.6' // 버전은 필요에 따라 조정
    .
    .
}

스프링이 AOP를 구현할 때 사용하는 모듈이다.



2. 프록시 (proxy)


기존 코드를 수정하지 않고 코드 중복도 피할 수 있는 방법이다.


이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다!



3. AOP (Aspect Oriented Programming)


여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법이다.

-> 분리를 통해, 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 추가할 수 있게 한다.


핵심 기능에 공통 기능을 삽입하는 방법

  • 컴파일 시점에 코드에 공통 기능을 삽입
  • 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입
  • 런타임에 프록시 객체를 생성해서 공통 기능을 삽입


프록시 기반의 AOP



AOP 주요 용어


용어 의미 예시
Advice 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의
Joinpoint Advice를 적용 가능한 지점
Pointcut Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 의미
Weaving Advice를 핵심 로직 코드에 적용
Aspect 여러 객체에 공통으로 적용되는 기능 트랙젝션, 보안 등


Advice의 종류

스프링은 프록시를 이용해서 메서드 호출 시점에 Aspect를 적용하기 때문에 구현 가능한 Advice가 여러 개 존재한다.


종류 설명
Before Advice 대상 객체의 메서드 호출 전에 공통 기능 실행
After Returning Advice 대상 객체의 메서드가 익셉션 없이 실행된 이후에 공통 기능 실행
After Throwing Advice 대상 객체의 메서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능 실행
After Advice 익셉션 발생 여부에 상관없이 대상 객체의 메서드 실행 후 공통 기능 실행
Around Advice 대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능 실행



4. 스프링 AOP 구현



(생략)
.
.

@Aspect
public class ExeTimeAspect {

    @Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }

    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long finish = System.nanoTime();
            .
            .
            (생략)
            .
            .
        }
    }
}

즉, @Around 애노테이션의 Pointcut으로 설정한 publicTarget() 메서드의 Pointcut을 확인하고, 그 위치에 속하는 타입에 @Around 애노테이션이 적용된 공통 기능인 measure() 메서드를 이전과 이후에 위치시키면 된다.


@EnableAspect


@Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용하기 위해 설정 클래스에 추가해야 하는 애노테이션이다.



5. 프록시 생성 방식


스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다.


// 수정 전
Calculator cal = ctx.getBean("calculator", Calculator.class);

// 수정 후
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);

// 자바 설정 파일
(생략)
.
.
@Bean
public Calculator calculator() {
    return new RecCalculator();
}
.
.

getBean() 메서드에 Calculator 타입 대신에 RecCalculator 타입을 사용하도록 수정한 이후에 메인 클래스를 실행했다고 하자. 과연 정상 실행될까?

-> getBean() 메서드에 사용한 타입이 RecCalculator인데 반해 실제 타입은 $Proxy17. 라는 익셉션이 발생한다.


그럼 빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성할 수 있을까?

@EnableAspectJAutoProxy


@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx {
    .
    .
}


execution 명시자 표현식


excution 명시자는 Aspect를 적용할 메서드를 지정할 때 사용된다.

기본형식 : execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))


예 (execution 생략) 설명
(public void set*(..)) 리턴 타입이 void이고, 메서드 이름이 set으로 시작하는 파라미터가 0개 이상인 메서드 호출
(Long chap07.Calculator.factorial(..)) 리턴 타입이 Long인 calculator 타입의 factorial() 메서드 호출


Advice 적용 순서



@Order


import org.springframework.core.annotation.Order;

@Aspect
@Order(1)
public class ExeTimeAspect {
    ...
}

@Aspect
@Order(2)
public class CacheAspect {
    ...
}

ExeTimeAspect 프록시 객체의 대상 객체는 CacheAspect 프록시 객체. CacheAspect 프록시 객체의 대상 객체는 실제 대상 객체.


@Around의 Pointcut 설정과 @Pointcut 재사용


@Around 애노테이션 역시 execution 명시자를 직접 지정할 수 있다.

@Aspect
public class CacheAspect {

    @Around("execution(public*chap07..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
    }

}
@Aspect
public class ExeTimeAspect {

    @Pointcut("execution(public*chap07..*(..))")
    private void publicTarget() {
        ...
    }

    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
    }

}


만약, 이 상황에서 CacheAspect 클래스에서 ExeTimeAspect 클래스에 위치한 publicTarget() 메서드의 Pointcut을 사용하고 싶다면? (두 클래스는 같은 패키지에 위치한다고 하자.)

@Aspect
public class CacheAspect {

    @Around("ExeTimeAspect.publicTarget()")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
    }

}


여러 Aspect에서 공통으로 사용하는 Pointcut이 있다면 별도 클래스에 Pointcut을 정의하고, 각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성하자!

hongsujin2eeZyo commented 1 month ago

제 4장 - 의존 자동 주입

직접 주입 : 설정 클래스에서 의존 대상을 생성자나 메서드를 이용해서 주입

@Bean
public ChangePasswordService changePwdSvc(){
    ChangePasswordService pwdSvc = new ChangePasswordService();
    psdSvc.setMemberDao(memberDao()); //의존 주입
    return pwdSvc;
}

1. @Autowired를 이용한 의존 자동 주입

자동주입기능을 사용하면 @Bean 메서드에서 의존을 주입하지 않아도(의존 객체를 직접 명시하지 않아도) 의존 객체가 주입됨!!

사용 방법 : 의존을 주입할 대상에 @Autowired 애노테이션을 붙이기만 하면 됨

1) 필드에 붙이기

필드에 @Autowired애노테이션이 붙어 있으면 스프링이 해당 타입의 빈 객체를 찾아서 필드에 할당한다

2)세터 메서드에 붙이기

클래스의 세터메서드에 @Autowired를 붙이면 다른 설정클래스에서 세터 메서드를 호출하지 않아도 정상동작이 된다 (빈 객체의 메서드에 @Autowired 애노테이션을 붙이면 스프링은 해당 메서드를 호출한다. 이때 메서드 파라미터 타입에 해당하는 빈 객체를 찾아 인자로 주입한다.)

정리 : @Autowired 애노테이션을 필드나 세터 메서드에 붙이면 스프링은 타입이 일치하는 빈 객체를 찾아서 주입한다.

@Configuration
public class AppCtx{
    @Bean
    public MemberDao memberDao(){
        return new MemberDao();
    }

    @Bean
    public ChangePasswordService changePwdSvc(){
        ChangePasswordService pwdSvc = 
            new ChangePasswordService();
        return pwdSvc
    }

    @Bean
    public MemberPrinter memberPrinter(){
        return new MemverPrinter();
    }
    >
    @Bean
    public MemberInfoPrinter infoPrinter(){
        MemberInfoPrinter infoPrinter = 
            new MemberInfoPrinter();
        return infoPrinter;
    }
}
public class ChangePasswordService{
    //필드타입 -> MemberDao이므로 일치하는 타입을 가진 memberDao 빈이 주입됨
    @Autowired
    private MemberDao memberDao;

  --생략

public class MemberInfoPrinter{
    private MemberDao memDao;
    private MemberPrinter printer;

    public void printMemberInfo(String email){
    --생략
    }

    //파라미터타입 -> MemberDao이므로 일치하는 타입을 가진 memberDao 빈이 주입됨
    @Autowired
    public void setMemberDao(MemberDao memberDao){
        this.memDao = memberDao;
    }

    //파라미터타입 -> MemberPrinter이므로 일치하는 타입을 가진 memberPrinter 빈이 주입됨
    @Autowired
    public void setPrinter(MemeberPrinter printer){
        this.printer = printer;
    }
 }

만약 일치하는 빈이 없는 경우에는??

익셉션이 발생하면서 제대로 실행되지 않는다. 콘솔에는 에러메시지가 출력됨 -> 'ㅇㅇ'빈을 생성하는데 에러가 발생했다, 'ㅇㅇㅇ'필드에 대한 의존을 충족하지 않는다, 'ㅇㅇㅇ' 타입의 빈이 없다 라는 내용이 나온다.

주입 대상에 일치하는 빈이 두 개 이상이면??

이 경우에도 스프링은 자동 주입에 실패하고 익셉션을 발생시킴 이유는 자동주입을 하려면 해당 타입을 가진 빈이 어떤 빈인지 정확하게 한정할 수 있어야 하는데 해당 타입의 빈이 2개 이상이면 어떤 빈을 자동 주입 대상으로 선택해야할지 한정할 수 없다.

2. @Qualifier 애노테이션을 이용한 의존 객체 선택

위의 경우 처럼 자동 주입 가능한 빈이 두 개 이상일때 빈을 지정할 수 있는 애노테이션이다.

(이 애노테이션은 두 위치에서 사용 가능하다)

  1. @Bean 애노테이션을 붙인 빈 설정 메서드
  2. @Autowired 애노테이션에서 자동 주입할 빈을 한정할때
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter1(){
    return new MemberPrinter();
}

@Bean
public MemberPrinter memberPrinter2(){
    return new MemberPrinter();
}
@Autowired
@Qualifier("printer")
public void setMemberPrint(MemberPrinter printer){
    this.printer = printer;
}

setMemberPrinter()메서드에 @Autowired 애노테이션을 붙였으므로 MemberPrinter타입의 빈을 자동 주입한다. 이때 @Qualifier 애노테이션 값이 "printer"이므로 한정 값이 "printer"인 빈을 의존 주입 후보로 사용한다. 그러므로 프링 설정 클래스에서 @Qualifier 애노테이션의 값으로 "printer"를 준 MemberPrinter 타입의 빈(memberPrinter1)을 자동 주입 대상으로 사용한다.

@Autowired 애노테이션을 필드와 메서드에 모두 적용할 수 있으므로 @Qualifier 애노테이션도 필드와 메서드에 적용할 수 있다.

빈 이름과 기본 한정자

빈 설정에 @Qualifier 애노테이션이 없으면 빈의 이름을 한정자로 지정한다.

빈이름 ----------@Qualifier--------한정자 printer ---------------------------- printer printer2 ----------mprinter--------mprinter infoPrinter -------------------------infoPrinter

3. 상위/하위 타입 관계와 자동 주입

상속관계일 경우에는 빈의 타입을 변경해도 에러가 발생한다 @Autowired 애노테이션 태그를 만나면 두개의 빈 중에서 어떤 빈을 주입해야 할지 알 수 없다. 익셉션 발생

이를 해결할 수 있는 두 가지 방법

  1. @Qualifier애노테이션을 붙여서 주입할 빈을 한정한다.
  2. 파라미터 타입을 변경한다.
    
    @Bean
    @Qualifier("summaryPrinter")
    public MemberSummaryPrinter memberPrinter2(){
    return new MemberSummaryPrinter();
    }
  3. @Autowired @Qualifier("summaryPrinter") //애노테이션 붙이기 public void setMemberPrinter(MemberPrinter printer){ this.printer = printer; }

  4. @Autowired //파라미터 타입 변경 public void setMemberPrinter(MemberSummaryPrinter printer){ this.printer = printer; }

4. @Autowired 애노테이션의 필수 여부

1) 자동 주입할 대상이 필수가 아닌 경우에는 @Autowired 애노테이션의 required 속성을 false로 지정함

@Autowired(required = false)
public void 블라블라....

이렇게 하면 매칭되는 빈이 없어도 익셉션이 발생하지 않으며 자동 주입을 수행하지않는다.

2) 자동 주입 대상 타입이 Optional인 경우 일치하는 빈이 존재하지 않으면 값이없는 Optional을 인자로 전달하고, 일치하는 빈이 존재하면 해당 빈을 값으로 갖는 Optional을 인자로 전달한다.

@Autowired
public void setDateFormatter(Optional</DateTimeFormatter>formatterOpt){
    if(formatterOpt.isPresent()){
        this.dateTimeFormatter = formatterOpt.get();
  } else{
        this.dateTimeFormatter = null;
  }
} 

3) @Nullable 애노테이션 사용 새터 메서드를 호출할 때 자동 주입할 빈이 존재하면 해당 빈을 인자로 전달하고, 존재하지 않으면 인자로 null을 전달한다.

@Autowired
  public void setDateFormatter(@Nullable DateTimeFormatter dateTimeFormatter){
        this.dateTimeFormatter = dateTimeFormatter;
  }

1번과 3번의 차이는 @Nullable을 사용하면 자동 주입할 빈이 존재하지 않아도 메서드가 호출된다는 점이다. required 속성이 false 인데 대상 빈이 존재하지 않으면 세터 메서드를 호출하지 않는다.

5. 자동 주입과 명시적 의존 주입 간의 관계

설정 클래스에서 세터 메서드를 통해 의존을 주입해도 해당 세터 메서드에 @Autowired 애노테이션이 붙어 있으면 자동 주입을 통해 일치하는 빈을 주입한다. -> @Autowired 애노테이션을 사용했다면 설정 클래스에서 객체를 주입하기보다는 스프링이 제공하는 자동 주입 기능을 사용하는게 나음

제 5장 - 컴포넌트 스캔

컴포넌트 스캔이란? 스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능 (설정 클래스에 빈으로 등록하지않아도 원하는 클래스를 빈으로 등록할 수 있으므로 컴포넌트 스캔 기능을 사용하면 설정 코드가 크게 줄어든다)

1. @Component 애노테이션으로 스캔 대상 지정

클래스에 붙여야함 @Component 애노테이션은 해당 클래스를 스캔 대상으로 표시한다.

빈 이름 설정

애노테이션에 값을 주지 않았다면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용한다 클래스이름 : MemberDao ---> 빈 이름 : memberDao

값을 주면 그 값을 빈 이름으로 사용한다. @Component("listPrinter") ---> 빈 이름 : listPrinter

@Component
public class MemberDao{
    블라블라,.,.
}
@Component("listPrinter")
public class MemberInfoPrinter{
    블라블라,.,.
}

2. @ComponentScan 애노테이션으로 스캔 설정

@Component 애노테이션을 붙인 클래스를 스캔에서 스프링 빈으로 등록하려면 설정 클래스에 @ComponentScan 애노테이션을 적용해야한다.

@ComponentScan(basePackages = {"클래스가 있는 패키지 이름"})

3. 스캔 대상에서 제외하거나 포함하기

excludeFilters 속성을 사용하면 스캔할 때 특정 대상을 자동 등록 대상에서 제외할 수 있다.

1) 정규표현식을 사용해서 제외 대상을 지정

@ComponentScan(basePackages = {"spring"},
 excludeFilters = @Fiter(type = FilterType.REGEX,pattern = "spring\\..*Dao"))

해석 -> spring으로 시작하고 Dao로 끝나는 정규표현식을 지정했으므로 spring.MemberDao 클래스를 컴포넌트 스캔 대상에서 제외한다.

2)특정 애노테이션을 붙인 타입을 컴포넌트 대상에서 제외할 수도 있다.

@ComponentScan(basePackages = {"spring","spring2"},
 excludeFilters = @Fiter(type = FilterType.ANNOTATION, classes = {NoProdeuct.class, ManualBean.class }))

해석->FilterTypeANNOTATION을 사용하면 애노테이션을 붙인 클래스를 스캔대상에서 제외함

3)특정 타입이나 그 하위 타입을 컴포넌트 스캔 대상에서 제외하려면 ASSIGNALBLE_TYPE을 FilterType으로 사용한다

@ComponentScan(basePackages = {"spring"},
 excludeFilters = @Fiter(type = FilterType.ASSIGNALBLE_TYPE, classes = MemberDao.class ))

4) 설정할 필터가 두개 이상이면 @ComponentScan의 excludeFilters 속성에 배열의 사용해서 @Filter 목록을 전달하면된다

@ComponentScan(basePackages = ({"spring"},
    excludeFilters = {
    @Fiter(type = FilterType.ANNOTATION, classes = ManualBean.class),
    @Fiter(type = FilterType.REGEX,pattern = "spring\\..*") })

@Component 뿐만아니라 @Controller, @Service, @Repository, @Aspect, @Configuration 을 붙인 클래스가 컴포넌트 스캔 대상에 포함된다.

4. 컴포넌트 스캔에 따른 충돌 처리

컴포넌트 스캔 기능을 사용해서 자동으로 빈을 등록할 때는 충돌에 주의해야 한다.

빈 이름 충돌

서로 다른 패키지에 동일한 이름의 클래스가 존재하고 두 클래스에 모두 @Component 애노테이션을 붙이면 익셉션이 발생함 --> 서로 다른 타입인데 같은 빈 이름을 사용하는 경우가 있다면 둘 중 하나에 명시적으로 빈 이름을 지정해서 이름 충돌을 피해야 한다.

수동 등록한 빈과 충돌

스캔할 때 사용하는 빈 이름과 수동 등록한 빈 이름이 같은 경우 수동 등록한 빈이 우선이다. 즉 수동 등록한 빈 하나만 존재한다고 볼 수 있다. 다른이름을 사용한다면. 스캔을 통한 빈1 , 수동 등록한 빈2 모두 존재한다. 같은 타입의 빈이 두개가 생성되므로 자동 주입하는 코드는 @Qualifier 애노테이션을 사용해서 알맞은 빈을 선택해야한다.

jgw1202 commented 1 month ago

3장. 스프링 DI

1. 의존과 DI를 통한 의존처리

DI는 'Dependency Injection"의 약자로 의존성 주입이라고 합니다.

그렇다면 의존이란 무엇일까요? 간단한 코드로 의존을 설명할 수 있습니다.

public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}
public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // 직접 엔진 객체를 생성
    }

    public void start() {
        engine.start();
        System.out.println("Car started");
    }

    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

위 코드는 Car 클래스가 Engine 클래스에 강하게 결합되어 있습니다. Car 클래스의 생성자에서 Engine 객체를 직접 생성하고 있기 때문에, Engine 클래스의 변경이나 다른 Engine 구현체로의 변경이 필요할 때 Car 클래스도 변경을 해야 합니다. 이는 코드의 유연성과 테스트 가능성을 저하시킵니다.

따라서 의존이란, 변경에 따른 영향이 전파되는 관계를 의미합니다. 클래스 내부에서 직접 의존 객체를 생성하는 것은 프로그래밍을 할 때 매우 쉽지만, 유지보수적 관점에서 바라볼 때, 문제점을 야기하기엔 충분합니다. 변경에 따른 영향이 전파되기 때문이죠. 그렇다면 이를 어떻게 해결해야 할까요?

public interface Engine {
    void start();
}

public class GasolineEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gasoline Engine started");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric Engine started");
    }
}
public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine; // 의존성 주입
    }

    public void start() {
        engine.start();
        System.out.println("Car started");
    }

    public static void main(String[] args) {
        Engine gasolineEngine = new GasolineEngine();
        Car carWithGasolineEngine = new Car(gasolineEngine);
        carWithGasolineEngine.start();

        Engine electricEngine = new ElectricEngine();
        Car carWithElectricEngine = new Car(electricEngine);
        carWithElectricEngine.start();
    }
}

위의 코드에서 Car 클래스는 이제 Engine 인터페이스에 의존하고, 구체적인 Engine 구현체(GasolineEngine 또는 ElectricEngine)는 외부에서 주입됩니다.

이렇게 하면 Car 클래스는 특정 Engine 구현체에 강하게 결합되지 않으며, 필요에 따라 다양한 Engine 구현체를 사용할 수 있습니다.

2. 객체 조립기

main 메소드에서 의존 대상 객체를 생성하고 주입하는 방법이 나쁘지는 않습니다. 하지만 좀 더 나은 방법은 객체를 생성하고 의존 객체를 주입해주는 클래스를 따로 작성하는 것 입니다. 이러한 클래스를 객체 조립기라고도 표현합니다.

위에서 사용한 코드에서 객체 조립기를 활용하여 코드를 확장해보았습니다.

public interface Engine {
    void start();
}

public class GasolineEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gasoline Engine started");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric Engine started");
    }
}
public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine; // 의존성 주입
    }

    public void start() {
        engine.start();
        System.out.println("Car started");
    }
}
public class CarAssembler {  // 객체 조립기
    public Car assembleCar(String engineType) {
        Engine engine = createEngine(engineType);
        return new Car(engine);
    }

    private Engine createEngine(String engineType) {
        switch (engineType) {
            case "Gasoline":
                return new GasolineEngine();
            case "Electric":
                return new ElectricEngine();
            default:
                throw new IllegalArgumentException("Unknown engine type: " + engineType);
        }
    }
}
public class Main { 
    public static void main(String[] args) {
        CarAssembler assembler = new CarAssembler();

        Car carWithGasolineEngine = assembler.assembleCar("Gasoline");
        carWithGasolineEngine.start();

        Car carWithElectricEngine = assembler.assembleCar("Electric");
        carWithElectricEngine.start();
    }
}

Engine 인터페이스 및 구현 클래스는 Engine 인터페이스와 두 가지 구현체(GasolineEngine 및 ElectricEngine)를 정의합니다.

Car 클래스는 Engine 인터페이스를 의존성으로 받아서 사용합니다. 이를 통해 Car 클래스는 특정 엔진 구현체에 의존하지 않습니다.

CarAssembler 클래스는 Car 객체를 조립하고 필요한 Engine 객체를 생성합니다.

assembleCar 메서드는 주어진 엔진 타입에 따라 적절한 Engine 구현체를 생성하고, 이를 Car 객체에 주입합니다.

Main 클래스에서는 CarAssembler를 사용하여 Car 객체를 생성합니다.

엔진 타입을 문자열로 전달하여 다양한 조합의 Car 객체를 쉽게 생성할 수 있습니다.

이렇게 DI와 객체 조립기를 사용하면 코드의 유연성과 재사용성을 크게 향상시킬 수 있습니다.

또한, 객체 생성과 의존성 주입 로직을 분리함으로써 코드의 가독성과 유지보수성을 높일 수 있습니다.

3. 스프링의 DI 설정

스프링은 앞서 구현한 객체 조립기와 유사한 기능을 제공합니다. 즉 스플링은 Assembler 클래스의 생성자 코드처럼 필요한 객체를 생성하고 생성한 객체에 의존성을 주입합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public Engine gasolineEngine() {
        return new GasolineEngine();
    }

    @Bean
    public Engine electricEngine() {
        return new ElectricEngine();
    }

    @Bean
    public Car carWithGasolineEngine() {
        return new Car(gasolineEngine());
    }

    @Bean
    public Car carWithElectricEngine() {
        return new Car(electricEngine());
    }
}

위 코드는 스프링 설정 클래스입니다. @Configuration 어노테이션은 스프링의 설정 클래스임을 의미하는 어노테이션입니다. @Bean은 해당 메서드가 생성한 객체를 스프링 빈으로 등록합니다. 위 코드에선 gasolineEngine, electricEngine, carWithGasolineEngine, carWithElectricEngine 빈을 정의합니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Car carWithGasolineEngine = context.getBean("carWithGasolineEngine", Car.class);
        carWithGasolineEngine.start();

        Car carWithElectricEngine = context.getBean("carWithElectricEngine", Car.class);
        carWithElectricEngine.start();
    }
}

위 메인 클래스에서 AnnotationConfigApplicationContext를 사용하여 스프링 컨텍스트를 초기화하고 AppConfig 클래스를 설정으로 로드합니다. context.getBean 메서드를 사용하여 스프링 컨테이너로부터 Car 빈을 가져와서 사용할 수 있습니다.

DI 방식 1 : 생성자 주입 방식

  1. 불변성 보장: 의존성이 객체 생성 시에 주입되므로, 주입된 이후에는 변경될 수 없어서 객체의 상태가 일관되게 유지됩니다.
  2. 필수 의존성 보장: 생성자를 통해 모든 필수 의존성을 주입받으므로, 객체가 생성될 때 필요한 모든 의존성을 갖추게 됩니다.
  3. 불변 객체 지원: 의존성이 변하지 않기 때문에 불변 객체를 만들기 쉽습니다.

DI 방식 2 : 세터 주입 방식

  1. 유연성: 의존성을 나중에 변경할 수 있으므로 더 유연한 객체 구성을 지원합니다.
  2. 선택적 의존성: 필수가 아닌 선택적인 의존성을 쉽게 주입할 수 있습니다.
  3. 가독성: 생성자 매개변수가 많지 않아서 코드가 간결해질 수 있습니다.

뭐가 더 낫나?

일반적으로는 생성자 주입 방식 이 더 권장됩니다. 그 이유는 객체 상태의 불변인 불변성, 모든 의존성을 생성 시점에 주입받으므로 일관성, 생성자 주입은 의존성을 명시적으로 주입하므로 명시적 주입이 가능하기 때문입니다.

4. @Configuration 설정 클래스의 @Bean 설정과 싱글톤

스프링 컨테이너가 생성한 빈은 싱글톤 객체입니다. 스프링 컨테이너는 @Bean 이 붙은 메소드에 대해 단 1개의 객체만을 생성합니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Car carWithGasolineEngine1 = context.getBean("carWithGasolineEngine", Car.class);
        Car carWithGasolineEngine2 = context.getBean("carWithGasolineEngine", Car.class);

        System.out.println("carWithGasolineEngine1 == carWithGasolineEngine2: " + (carWithGasolineEngine1 == carWithGasolineEngine2));

        Engine gasolineEngine1 = context.getBean("gasolineEngine", Engine.class);
        Engine gasolineEngine2 = context.getBean("gasolineEngine", Engine.class);

        System.out.println("gasolineEngine1 == gasolineEngine2: " + (gasolineEngine1 == gasolineEngine2));
    }
}

@Bean 애노테이션이 붙은 메소드는 기본적으로 싱글톤 스코프를 가집니다. 이는 스프링 컨테이너가 해당 메소드에 대해 단 한 번만 호출되어 객체를 생성하고, 그 후에는 동일한 인스턴스를 반환하는 것을 의미합니다.

<<실행결과>> Gasoline Engine started Car started Gasoline Engine started Car started carWithGasolineEngine1 == carWithGasolineEngine2: true gasolineEngine1 == gasolineEngine2: true

실행결과를 보면 carWithGasolineEngine1과 carWithGasolineEngine2는 동일한 인스턴스임을 확인할 수 있습니다. gasolineEngine1과 gasolineEngine2 또한 동일한 인스턴스임을 확인할 수 있습니다. 이를 통해 스프링 컨테이너가 @Bean 애노테이션이 붙은 메소드에 대해 단 한 번만 객체를 생성하여 싱글톤으로 관리한다는 것을 알 수 있습니다.

@Autowired ?

스프링 컨테이너는 설정 클래스에서 사용한 @Autowired에 대해 자동 주입 처리를 해줍니다. 스프링은 @Configuration 어노테이션이 붙은 설정 클래스를 내부적으로 스프링 빈으로 등록합니다. 그리고 다른 빈과 마찬가지로 @Autowired가 붙은 대상에 대해 알맞은 빈을 자동으로 주입해줍니다.

간단한 예시 코드를 살펴봅시다.

public class MessageService {
    public String getMessage() {
        return "Hello, World!";
    }
}

MessageService 클래스: 단순히 메시지를 반환하는 메소드를 가진 서비스 클래스입니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Printer {
    private MessageService messageService;

    // 필드 주입
    @Autowired
    private MessageService messageService;

    public void printMessage() {
        System.out.println(messageService.getMessage());
    }
}

Printer 클래스: MessageService에 의존하는 클래스입니다. @Autowired 애노테이션을 사용하여 MessageService 빈을 간단하게 필드 주입 방식으로 주입받습니다. printMessage 메소드는 주입된 MessageService의 getMessage 메소드를 호출하여 메시지를 출력합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {

    @Bean
    public MessageService messageService() {
        return new MessageService();
    }
}

Spring Configuration 클래스 (AppConfig): @Configuration 애노테이션을 사용하여 스프링 설정 클래스를 정의합니다. @Bean 애노테이션을 사용하여 MessageService 빈을 정의합니다. @ComponentScan 애노테이션을 사용하여 Printer 클래스를 자동으로 스캔하고 빈으로 등록합니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        Printer printer = context.getBean(Printer.class);
        printer.printMessage();
    }
}

Main 클래스: AnnotationConfigApplicationContext를 사용하여 스프링 컨텍스트를 초기화하고 AppConfig 클래스를 설정으로 로드합니다. context.getBean 메소드를 사용하여 Printer 빈을 가져와서 사용합니다.

4장. 의존 자동 주입

자동 주입이 아닌 의존주입 방법:

@Bean
public ChangePasswordService changePwdSvc(){
    ChangePasswordService pwdSvc = new ChangePasswordService();
    psdSvc.setMemberDao(memberDao()); //의존 주입
    return pwdSvc;
}

설정 클래스에서 의존 대상을 생성자나 메서드를 이용해서 주입

1. @Autowired를 이용한 의존 자동 주입

자동주입기능을 사용하면 @Bean 메서드에서 의존을 주입하지 않아도(의존 객체를 직접 명시하지 않아도) 의존 객체가 주입됨!!

사용 방법 : 의존을 주입할 대상에 @Autowired 애노테이션을 붙이기만 하면 됨

1) 필드에 붙이기

필드에 @Autowired애노테이션이 붙어 있으면 스프링이 해당 타입의 빈 객체를 찾아서 필드에 할당한다

2)세터 메서드에 붙이기

빈 객체의 메서드에 @Autowired 애노테이션을 붙이면 스프링은 해당 메서드를 호출한다. 이때 메서드 파라미터 타입에 해당하는 빈 객체를 찾아 인자로 주입한다.

예시를 보자.

@Configuration
public class AppCtx{
    @Bean
    public MemberDao memberDao(){
        return new MemberDao();
    }

    @Bean
    public ChangePasswordService changePwdSvc(){
        ChangePasswordService pwdSvc = 
            new ChangePasswordService();
        return pwdSvc
    }

    @Bean
    public MemberPrinter memberPrinter(){
        return new MemverPrinter();
    }
    >
    @Bean
    public MemberInfoPrinter infoPrinter(){
        MemberInfoPrinter infoPrinter = 
            new MemberInfoPrinter();
        return infoPrinter;
    }
}
public class ChangePasswordService{
    //필드타입 -> MemberDao이므로 일치하는 타입을 가진 memberDao 빈이 주입됨
    @Autowired
    private MemberDao memberDao;

  --생략

public class MemberInfoPrinter{
    private MemberDao memDao;
    private MemberPrinter printer;

    public void printMemberInfo(String email){
    --생략
    }

    //파라미터타입 -> MemberDao이므로 일치하는 타입을 가진 memberDao 빈이 주입됨
    @Autowired
    public void setMemberDao(MemberDao memberDao){
        this.memDao = memberDao;
    }

    //파라미터타입 -> MemberPrinter이므로 일치하는 타입을 가진 memberPrinter 빈이 주입됨
    @Autowired
    public void setPrinter(MemeberPrinter printer){
        this.printer = printer;
    }
 }

정리 : @Autowired 애노테이션을 필드나 세터 메서드에 붙이면 스프링은 타입이 일치하는 빈 객체를 찾아서 주입한다.

만약 일치하는 빈이 없는 경우에는??

익셉션이 발생하면서 제대로 실행되지 않는다. 콘솔에는 에러메시지가 출력됨 -> 'ㅇㅇㅇ'빈을 생성하는데 에러가 발생했다, 'ㅇㅇㅇ'필드에 대한 의존을 충족하지 않는다, 'ㅇㅇㅇ' 타입의 빈이 없다 라는 내용이 나온다.

주입 대상에 일치하는 빈이 두 개 이상이면??

이 경우에도 스프링은 자동 주입에 실패하고 익셉션을 발생시킴 이유 : 자동주입을 하려면 해당 타입을 가진 빈이 어떤 빈인지 정확하게 한정할 수 있어야 하는데 해당 타입의 빈이 2개 이상이면 어떤 빈을 자동 주입 대상으로 선택해야할지 한정할 수 없다.

2. @Qualifier 애노테이션을 이용한 의존 객체 선택

위의 경우 처럼 자동 주입 가능한 빈이 두 개 이상일때 빈을 지정할 수 있는 애노테이션이다.

(이 애노테이션은 두 위치에서 사용 가능하다)

  1. @Bean 애노테이션을 붙인 빈 설정 메서드
  2. @Autowired 애노테이션에서 자동 주입할 빈을 한정할때
@Bean
@Qualifier("printer") <- 이부분
public MemberPrinter memberPrinter1(){
    return new MemberPrinter();
}
@Bean
public MemberPrinter memberPrinter2(){
    return new MemberPrinter();
}
@Autowired
@Qualifier("printer") <- 이부분
public void setMemberPrint(MemberPrinter printer){
    this.printer = printer;
}

동작과정: setMemberPrinter()메서드에 @Autowired 애노테이션을 붙였으므로 MemberPrinter타입의 빈을 자동 주입한다. 이때 @Qualifier 애노테이션 값이 "printer"이므로 한정 값이 "printer"인 빈을 의존 주입 후보로 사용한다. 그러므로 스프링 설정 클래스에서 @Qualifier 애노테이션의 값으로 "printer"를 준 MemberPrinter 타입의 빈(memberPrinter1)을 자동 주입 대상으로 사용한다.

@Autowired 애노테이션을 필드와 메서드에 모두 적용할 수 있으므로 @Qualifier 애노테이션도 필드와 메서드에 적용할 수 있다.

빈 이름과 기본 한정자

빈 설정에 @Qualifier 애노테이션이 없으면 빈의 이름을 한정자로 지정한다.

빈이름 ----------@Qualifier--------한정자 printer ---------------------------- printer printer2 ----------mprinter---------mprinter infoPrinter -------------------------infoPrinter

3. 상위/하위 타입 관계와 자동 주입

상속관계일 경우에는 빈의 타입을 변경해도 에러가 발생한다 @Autowired 애노테이션 태그를 만나면 두개의 빈 중에서 어떤 빈을 주입해야 할지 알 수 없다. 익셉션 발생

이를 해결할 수 있는 두 가지 방법

  1. @Qualifier애노테이션을 붙여서 주입할 빈을 한정한다.
  2. 파라미터 타입을 변경한다.
    
    @Bean
    @Qualifier("summaryPrinter")
    public MemberSummaryPrinter memberPrinter2(){
    return new MemberSummaryPrinter();
    }
  3. @Autowired @Qualifier("summaryPrinter") //애노테이션 붙이기 public void setMemberPrinter(MemberPrinter printer){ this.printer = printer; }

  4. @Autowired //파라미터 타입 변경 public void setMemberPrinter(MemberSummaryPrinter printer){ this.printer = printer; }

4. @Autowired 애노테이션의 필수 여부

1) 자동 주입할 대상이 필수가 아닌 경우에는 @Autowired 애노테이션의 required 속성을 false로 지정함

@Autowired(required = false)
public void 블라블라....

이렇게 하면 매칭되는 빈이 없어도 익셉션이 발생하지 않으며 자동 주입을 수행하지않는다.

2) 자동 주입 대상 타입이 Optional인 경우 일치하는 빈이 존재하지 않으면 값이없는 Optional을 인자로 전달하고, 일치하는 빈이 존재하면 해당 빈을 값으로 갖는 Optional을 인자로 전달한다.

@Autowired
public void setDateFormatter(Optional</DateTimeFormatter>formatterOpt){
    if(formatterOpt.isPresent()){
        this.dateTimeFormatter = formatterOpt.get();
  } else{
        this.dateTimeFormatter = null;
  }
} 

3) @Nullable 애노테이션 사용 새터 메서드를 호출할 때 자동 주입할 빈이 존재하면 해당 빈을 인자로 전달하고, 존재하지 않으면 인자로 null을 전달한다.

@Autowired
  public void setDateFormatter(@Nullable DateTimeFormatter dateTimeFormatter){
        this.dateTimeFormatter = dateTimeFormatter;
  }

정리: @Autowired(required=false) : 대상 빈이 존재하지않으면 값 할당을 안함 Optional : 일치하는 빈이 존재하지 않으면 값이 없는 Opional을 인자로 전달 @Nullable : 일치하는 빈이 존재하지 않으면 Null값을 인자로 전달

1번과 3번의 차이는 @Nullable을 사용하면 자동 주입할 빈이 존재하지 않아도 메서드가 호출된다는 점이다. required 속성이 false 인데 대상 빈이 존재하지 않으면 세터 메서드를 호출하지 않는다.

5. 자동 주입과 명시적 의존 주입 간의 관계

설정 클래스에서 세터 메서드를 통해 의존을 주입해도 해당 세터 메서드에 @Autowired 애노테이션이 붙어 있으면 자동 주입을 통해 일치하는 빈을 주입한다.

따라서 @Autowired 애노테이션을 사용했다면 설정 클래스에서 객체를 주입하기보다는 스프링이 제공하는 자동 주입 기능을 사용하는게 낫다!!

5장. 컴포넌트 스캔

컴포넌트 스캔이란?

스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능 : 설정 클래스에 빈으로 등록하지않아도 원하는 클래스를 빈으로 등록할 수 있으므로 컴포넌트 스캔 기능을 사용하면 설정 코드가 크게 줄어든다!!

1. @Component 애노테이션으로 스캔 대상 지정

클래스에 붙여야함 @Component 애노테이션은 해당 클래스를 스캔 대상으로 표시한다.

빈 이름 설정

애노테이션에 값을 주지 않았다면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용한다 클래스이름 : MemberDao ---> 빈 이름 : memberDao

값을 주면 그 값을 빈 이름으로 사용한다. @Component("listPrinter") ---> 빈 이름 : listPrinter

@Component
public class MemberDao{
    블라블라,.,.
}
@Component("listPrinter")
public class MemberInfoPrinter{
    블라블라,.,.
}

2. @ComponentScan 애노테이션으로 스캔 설정

@Component 애노테이션을 붙인 클래스를 스캔에서 스프링 빈으로 등록하려면 설정 클래스에 @ComponentScan 애노테이션을 적용해야한다.

@ComponentScan(basePackages = {"클래스가 있는 패키지 이름"})

-> 스캔 대상 패키지 목록을 지정하는 것 , 해당 패키지와 그 하위 패키지에 속한 클래스를 스캔 대상으로 설정하는 것이다.

스캔 대상에 해당하는 클래스 중에서 @Component 애노테이션이 붙은 클래스의 객체를 생성해서 빈으로 등록한다.

3. 스캔 대상에서 제외하거나 포함하기

excludeFilters 속성을 사용하면 스캔할 때 특정 대상을 자동 등록 대상에서 제외할 수 있다.

1) 정규표현식을 사용해서 제외 대상을 지정

@ComponentScan(basePackages = {"spring"},
 excludeFilters = @Fiter(type = FilterType.REGEX,pattern = "spring\\..*Dao"))

@Fiter 애노테이션의 type 속성값으로 FilterType.REGEX가 주어졌다 이는 정규표현식을 사용해서 제외대상을 지정하는것을 의미함. spring으로 시작하고 Dao로 끝나는 정규표현식을 지정했으므로 spring.MemberDao 클래스를 컴포넌트 스캔 대상에서 제외한다.

2)AspectJ 패턴을 사용해서 대상을 지정

@ComponentScan(backPackages = {"spring"}, 
excludeFilteers = @Filteer(type = FilterType.ASPECTJ, pattern 
= "spring.*Dao"))

spring 패키지의 Dao로 끝나는 클래스를 컴포넌트 스캔 대상에서 제외한다

3)특정 애노테이션을 붙인 타입을 컴포넌트 대상에서 제외

@ComponentScan(basePackages = {"spring","spring2"},
 excludeFilters = @Fiter(type = FilterType.ANNOTATION, classes = {NoProdeuct.class, ManualBean.class }))

FilterTypeANNOTATION을 사용하면 애노테이션을 붙인 클래스를 스캔대상에서 제외함 @Noproduct @ManualBean 애노테이션을 붙인 클래스를 스캔대상에서 제외

4)특정 타입이나 그 하위 타입을 컴포넌트 스캔 대상에서 제외하려면 ASSIGNALBLE_TYPE을 FilterType으로 사용한다

@ComponentScan(basePackages = {"spring"},
 excludeFilters = @Fiter(type = FilterType.ASSIGNALBLE_TYPE, classes = MemberDao.class ))

설정할 필터가 두개 이상이면 @ComponentScan의 excludeFilters 속성에 배열의 사용해서 @Filter 목록을 전달하면된다

@ComponentScan(basePackages = ({"spring"},
    excludeFilters = {
    @Fiter(type = FilterType.ANNOTATION, classes = ManualBean.class),
    @Fiter(type = FilterType.REGEX,pattern = "spring\\..*") })

컴포넌트 스캔 대상은 @Component이 붙은 클래스 뿐만아니라 @Controller, @Service, @Repository, @Aspect, @Configuration 을 붙인 클래스가 컴포넌트 스캔 대상에 포함된다.

이런식으로 @Controller가 붙은 클래스에서 @Controller 애노테이션을 누르게되면 위에 @Component가 사용이 되는걸 보아 @Controller도 스캔대상에 포함이 된다고 볼수 있다

4. 컴포넌트 스캔에 따른 충돌 처리

컴포넌트 스캔 기능을 사용해서 자동으로 빈을 등록할 때는 충돌에 주의해야 한다.

빈 이름 충돌

서로 다른 패키지에 동일한 이름의 클래스가 존재하고 두 클래스에 모두 @Component 애노테이션을 붙이면 익셉션이 발생함 따라서 서로 다른 타입인데 같은 빈 이름을 사용하는 경우가 있다면 둘 중 하나에 명시적으로 빈 이름을 지정해서 이름 충돌을 피해야 한다.

수동 등록한 빈과 충돌

스캔할 때 사용하는 빈 이름과 수동 등록한 빈 이름이 같은 경우 수동 등록한 빈이 우선이다. 즉, 수동 등록한 빈 하나만 존재한다고 볼 수 있다.

다른이름을 사용한다면. 스캔을 통한 빈1 , 수동 등록한 빈2 모두 존재한다. 같은 타입의 빈이 두개가 생성되므로 자동 주입하는 코드는 @Qualifier 애노테이션을 사용해서 알맞은 빈을 선택해야한다.

6장. 빈 라이프사이클과 범위



1. 컨테이너 초기화와 종료


// 1. 컨테이너 초기화
AnnotationConfigApplicationContext ctx =
               new AnnotationConfigApplicationContext(AppContext.class);

// 2. 컨테이너에서 빈 객체를 구해서 사용
Greeter g = ctx.getBeen("greeter", Greeter.class);
String msg = g.greet("스프링");
System.out.println(msg);

// 3. 컨테이너 종료
ctx.close();


컨테이너 초기화 -> 빈 객체의 생성, 의존 주입, 초기화 컨테이너 종료 -> 빈 객체의 소멸



2. 스프링 빈 객체의 라이프사이클


스프링 컨테이너는 빈 객체의 라이프사이클을 관리한다.

빈 객체의 초기화와 소멸 : 스프링 인터페이스


public interface InitializingBean {
   void afterPropertiesSet() throws Exception;
}

public interface DisposableBean {
   void destroy() throws Exception;
}


스프링에서 두 인터페이스에 이 메서드를 정의하고 있다면?

예시

  • 데이터베이스 커넥션 풀 : 초기화 과정에서 데이터베이스 연결 생성하고, 소멸 과정에서 사용중인 데이터베이스 연결 끊는다.
  • 채팅 클라이언트 : 초기화 과정에서 서버와의 연결을 생성하고, 소멸 과정에서 끊는 작업을 수행한다.


빈 객체의 초기화와 소멸 : 커스텀 메서드


만약 InitializingBean, DisposableBean 인터페이스를 구현할 수 없거나 사용하고 싶지 않을 때는 어떻게 해야할까?

-> @Bean 태그에서 initMethod 속성과 destroyMethod 속성을 사용해서 초기화 메서드와 소멸 메서드의 이름을 지정하면 된다!


// Client2 클래스를 빈으로 사용하는 경우
@Bean(initMethod = "connect", destroyMethod = "close")
public Client2 client2() {
   Client2 client = new Client2();
    client.setHost("host");
    return client;
}


그럼 initMethod 속성을 사용하는 대신 빈 설정 메서드에서 직접 초기화를 해도 될까?

// 설정 코드에서 초기화 메서드를 직접 실행한 경우
@Bean(destroyMethod = "close")
public Client2 client2() {
   Client2 client = new Client2();
    client.setHost("host");
    client.connect();
    return client;
}


! 주의해야할 점 !


" 초기화 메서드가 두 번 호출되지 않도록 해야한다. "

@Bean
public Client client() {
   Client client = new Client();
    client.setHost("host");
    client.afterPropertiesSet();
    return client;
}

초기화 관련 메서드를 빈 설정 코드에서 직접 실행할 때, 초기화 메서드가 두 번 호출되지 않도록 주의해야한다.



3. 빈 객체의 생성과 관리 범위


싱글톤


한 식별자에 대해 한 개의 객체만 존재하는 빈의 범위를 말한다.

Client client1 = ctx.getBean("client", Client.class);
Client client2 = ctx.getBean("client", Client.class);
// client1 == client2 -> true


@Scope


특정 빈을 프로토타입 범위로 지정하려면 이 애노테이션을 사용해야 한다.

(생략)
.
.
@Bean
@Scope("prototype")
public Client client() {
   Client client = new Client();
    client.setHost("host");
    return client;
}


즉, 프로토타입 범위의 빈을 설정하면 빈 객체를 구할 때마다 매번 새로운 객체를 생성한다.

Client client1 = ctx.getBean("client", Client.class);
Client client2 = ctx.getBean("client", Client.class);
// client1 != client2 -> true


한계






7장. AOP 프로그래밍



1. AOP를 구현하기 위한 build.gradle


dependencies {
   (생략)
    .
    .
    implementation 'org.springframework:spring-context:5.3.9' // 버전은 필요에 따라 조정
    implementation 'org.aspectj:aspectjweaver:1.9.6' // 버전은 필요에 따라 조정
    .
    .
}

스프링이 AOP를 구현할 때 사용하는 모듈이다.



2. 프록시 (proxy)


기존 코드를 수정하지 않고 코드 중복도 피할 수 있는 방법이다.


이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다!



3. AOP (Aspect Oriented Programming)


여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법이다.

-> 분리를 통해, 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 추가할 수 있게 한다.


핵심 기능에 공통 기능을 삽입하는 방법

  • 컴파일 시점에 코드에 공통 기능을 삽입
  • 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입
  • 런타임에 프록시 객체를 생성해서 공통 기능을 삽입


프록시 기반의 AOP



AOP 주요 용어


용어 의미 예시
Advice 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의
Joinpoint Advice를 적용 가능한 지점
Pointcut Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 의미
Weaving Advice를 핵심 로직 코드에 적용
Aspect 여러 객체에 공통으로 적용되는 기능 트랙젝션, 보안 등


Advice의 종류

스프링은 프록시를 이용해서 메서드 호출 시점에 Aspect를 적용하기 때문에 구현 가능한 Advice가 여러 개 존재한다.


종류 설명
Before Advice 대상 객체의 메서드 호출 전에 공통 기능 실행
After Returning Advice 대상 객체의 메서드가 익셉션 없이 실행된 이후에 공통 기능 실행
After Throwing Advice 대상 객체의 메서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능 실행
After Advice 익셉션 발생 여부에 상관없이 대상 객체의 메서드 실행 후 공통 기능 실행
Around Advice 대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능 실행



4. 스프링 AOP 구현



(생략)
.
.

@Aspect
public class ExeTimeAspect {

    @Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }

    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
       long start = System.nanoTime();
        try {
           Object result = joinPoint.proceed();
            return result;
        } finally {
           long finish = System.nanoTime();
            .
            .
            (생략)
            .
            .
        }
    }
}

즉, @Around 애노테이션의 Pointcut으로 설정한 publicTarget() 메서드의 Pointcut을 확인하고, 그 위치에 속하는 타입에 @Around 애노테이션이 적용된 공통 기능인 measure() 메서드를 이전과 이후에 위치시키면 된다.


@EnableAspect


@Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용하기 위해 설정 클래스에 추가해야 하는 애노테이션이다.



5. 프록시 생성 방식


스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다.


// 수정 전
Calculator cal = ctx.getBean("calculator", Calculator.class);

// 수정 후
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);

// 자바 설정 파일
(생략)
.
.
@Bean
public Calculator calculator() {
   return new RecCalculator();
}
.
.

getBean() 메서드에 Calculator 타입 대신에 RecCalculator 타입을 사용하도록 수정한 이후에 메인 클래스를 실행했다고 하자. 과연 정상 실행될까?

-> getBean() 메서드에 사용한 타입이 RecCalculator인데 반해 실제 타입은 $Proxy17. 라는 익셉션이 발생한다.


그럼 빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성할 수 있을까?

@EnableAspectJAutoProxy


@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx {
   .
    .
}


execution 명시자 표현식


excution 명시자는 Aspect를 적용할 메서드를 지정할 때 사용된다.

기본형식 : execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))


예 (execution 생략) 설명
(public void set*(..)) 리턴 타입이 void이고, 메서드 이름이 set으로 시작하는 파라미터가 0개 이상인 메서드 호출
(Long chap07.Calculator.factorial(..)) 리턴 타입이 Long인 calculator 타입의 factorial() 메서드 호출


Advice 적용 순서



@Order


import org.springframework.core.annotation.Order;

@Aspect
@Order(1)
public class ExeTimeAspect {
   ...
}

@Aspect
@Order(2)
public class CacheAspect {
   ...
}

ExeTimeAspect 프록시 객체의 대상 객체는 CacheAspect 프록시 객체. CacheAspect 프록시 객체의 대상 객체는 실제 대상 객체.


@Around의 Pointcut 설정과 @Pointcut 재사용


@Around 애노테이션 역시 execution 명시자를 직접 지정할 수 있다.

@Aspect
public class CacheAspect {

   @Around("execution(public*chap07..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
       ...
    }

}
@Aspect
public class ExeTimeAspect {

   @Pointcut("execution(public*chap07..*(..))")
    private void publicTarget() {
       ...
    }

    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
       ...
    }

}


만약, 이 상황에서 CacheAspect 클래스에서 ExeTimeAspect 클래스에 위치한 publicTarget() 메서드의 Pointcut을 사용하고 싶다면? (두 클래스는 같은 패키지에 위치한다고 하자.)

@Aspect
public class CacheAspect {

   @Around("ExeTimeAspect.publicTarget()")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
       ...
    }

}


여러 Aspect에서 공통으로 사용하는 Pointcut이 있다면 별도 클래스에 Pointcut을 정의하고, 각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성하자!