LandvibeDev / 2024-spring5-programming-introduction

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

[4주차] 이태균, 박해원, 박수지, 인수빈 #1

Open d11210920 opened 1 month ago

d11210920 commented 1 month ago
haewonee commented 1 month ago

4단원

@Autowired 애노테이션을 사용한 의존 자동 주입

@Configuration
public class AppCtx{
    @Bean
    public MemberDao memberDao(){
        return new MemberDao(): 
        }
    @Bean
    public ChangePasswordService changePwdSvc(){
    ChangePasswordService pwdSvc = new ChangePasswordService();
    pwdSvc.setMemberDao(memberDao()); //세터를 사용한 의존 객체 주입
    return pwdSvc;
}
}

위의 코드는 의존 대상을 설정 코드에서 직접 주입하는 코드이다. 하지만 우리가 배우게 될 @Autowired를 사용하면 의존 대상을 설정 코드에서 직접 주입하지 않고 스프링이 자동으로 해당 타입의 빈을 찾아 객체를 주입해줄 수 있다. 이를 자동주입이라고 한다.

그렇다면 자동의존주입을 사용하는 방법은 무엇이냐 ->의존을 주입할 대상에 @Autowired를 붙이는 것이다.

public class ChangePasswordService{ //일부 생략 코드

    @Autowired
    private MemberDao memberDao;

    ~~~~~코드~~~~~~
    /*public void setMemberDao(MemberDao memberDao){
        this.memberDao = memberDao;
    }*/ 자동주입기능을 써서 memberDao에 이미 객체를 할당해줬기에 이 코드는 필요가없음
}

이 코드는 @Autowired를 써서 자동주입 기능을 사용한 코드이다. memberDao 필드에 자동주입 기능을 썼으므로 설정 클래스에서 따로 의존을 주입하지 않아도 스프링에서 자동으로 MemberDao타입의 빈 객체를 찾아서 memberDao필드에 할당해주게 된다. 이로 인해 처음에 봤던 설정 파일은 밑의 코드처럼 바뀔 수 있다.

@Configuration
public class AppCtx{
    @Bean
    public MemberDao memberDao(){
        return new MemberDao(): 
        }
    @Bean
    public ChangePasswordService changePwdSvc(){
    ChangePasswordService pwdSvc = new ChangePasswordService();
    //pwdSvc.setMemberDao(memberDao()); 
    return pwdSvc;
}
}

위의 저 코드를 없애고도 ChangePasswordService 클래스에서는 memberDao클래스의 기능을 사용할 수 있게 되었다. @Bean 메서드에서 따로 의존주입을 하지 않아도 스프링이 자동으로 @Autowired가 붙은 필드에 MemberDao 타입의 빈 객체를 찾아 주입해준 것이다.

@Autowired는 필드 뿐만 아니라 메서드에도 붙일 수 있다.

public class ChangePasswordService{
    private MemberDao memberDao;

    ~~~~~코드~~~~~~
    @Autowired
    public void setMemberDao(MemberDao memberDao){
        this.memberDao = memberDao;
    }
 }

위 코드처럼 메서드 자동주입기능을 사용하게되면 스프링은 메서드의 파라미터 타입에 해당하는 빈 객체를 찾아 인자로 주입하게된다. -> 이는 설정 파일에서 setter를 사용한 의존주입을 하지 않아도 된다는 것을 의미한다.

일치하는 빈이 없을 때

@Autowired 애노테이션을 적용한 대상에 일치하는 빈이 없다면? ->에러 발생 if) 위의 MemberDao를 주석처리 -> setThird 메서드에서 memberDao필드에 주입할 MemberDao타입의 빈을 발견하지 못함 ->에러발생 @Autowired 애노테이션을 붙인 주입 대상에 일치하는 빈이 두개 이상이라면? ->에러 발생 if) 위의 MemberDao 타입인 빈이 두개 설정되어있다고 치자. ->MemberDao타입의 빈중 어떤 빈을 자동 주입 대상으로 선택해야 할지 한정할 수 없게됨 ->에러발생

그렇다면 자동 주입 가능한 빈이 두개 이상일 때 자동 주입할 빈을 지정하는 방법은 없을까? ->@Qualifier 애노테이션을 사용하자 @Qualifier 애노테이션을 사용하면 자동 주입 대상 빈을 한정할 수 있다. 아래 예시를 보자. @Qualifier 애노테이션은 두 위치에서 사용 가능하다. 첫번째 위치는 빈 설정 메서드이다. 설정파일에서 first1 메서드에 "first"값을 갖는 @Qualifier 애노테이션을 붙였다. 이 설정은 해당 빈의 한정 값으로 "first"를 지정한다. 오른쪽 그림이 두번째 위치이다. @Autowired를 붙였으므로 First타입의 빈을 자동 주입한다. 이때 Qualifier 애노테이션 값이 "first"이므로 한정값이 "first"인 First타입의 빈(first1)을 자동 주입 대상으로 사용한다.

만약 @Qualifier 애노테이션이 없다면? ->빈의 이름을 한정자로 지정한다. 위 그림에서 first1()메서드로 정의한 빈의 한정자는 빈 이름인 first1이 되는것이다.

상속관계에서의 자동 주입

Child라는 클래스가 Parent라는 클래스를 상속받는다고 가정해보자.

설정파일
@Bean
public Child child(){
     return new Child():
}
@Bean
public Parent parent(){
    return new Parent():
    }

이 상태로 main을 실행하면 동일한 타입의 빈을 두개 설정하고 @Qualifier 애노테이션을 붙이지 않았을 때와 동일한 exception이 발생한다. ->동일한 타입이 아님에도 같은 에러가 발생한 이유는 Child 클래스가 Parent 클래스를 상속했기 때문이다. 다형성에서 배웠듯이 Child 클래스는 Parent 타입에도 할당할 수 있으므로(부모는 자식을 품을 수 있다), 스프링 컨테이너는 Parent 타입 빈을 자동 주입 해야하는 @Autowired를 만나면 child빈과 parent빈 중 어떤 빈을 주입해야 할지 알 수 없다.

->위에 배웠던 것처럼 설정 클래스와 @Autowired 애노테이션을 붙인 곳에 동일한 @Qualifier를 붙여서 주입할 빈을 한정하면 해결할 수 있다. ->@Autowired가 붙은 메서드 파라미터 타입이나, 필드 타입이 만약 Parent였다면 Child로 바꿔주는 방법도 있다.(자식은 부모를 품을 수 없음->Child타입 빈은 한개만 존재)

public class Printer{
    private Date date;
    public void print(Member member){
        if(date==null){
            System.out.println("null값입니다");
        }
        else{
            System.out.println("null값이 아닙니다",date.함수)

        }
    }
    @Autowired
    public void setDate(Date date){
        this.date = date;
    }
}

위 코드에서 print()메서드는 date가 null인 경우에도 null이라는 출력을 한다. ->즉, 반드시 setDate를 통해서 의존 객체를 주입할 필요가 없다는 뜻이다. 하지만 @Autowired 애노테이션은 기본적으로 @Autowired를 붙인 타입에 해당하는 빈이 존재하지 않으면 exception을 발생시킨다. 이렇게 자동 주입할 대상이 필수가 아닌 경우에 필수 여부를 지정할 수 있는 세가지 방법이 있다.

  1. @Autowired 애노테이션의 required 속성을 false로 지정
  2. Optional 사용
  3. @Nullable 애노테이션 사용 이 세가지 방식은 필드에도 그대로 적용되나 예시는 메서드로 하겠다.

1번의 사용 방법은 아래 코드와 같다.

public class Printer{
    private Date date;
    public void print(Member member){
        생략
    }
    @Autowired(required = false)
    public void setDate(Date date){
        this.date = date;
    }
}

@Autowired의 애노테이션의 required 속성을 false로 지정하면 Date타입의 빈이 없어도 exception이 발생하지 않고 setDate() 메서드를 실행하지 않는다.

2번의 사용 방법은 아래와 같다.

public class Printer{
    private Date date;
    public void print(Member member){
        생략
    }
    @Autowired
    public void setDate(Optional<Date> dateOpt){
        if(dateOpt.isPresent()){
            this.date = dateOpt.get();
        }else{
            this.date = null
        }
    }
}

자동 주입 대상 타입이 Optional인 경우, 일치하는 빈이 존재하지 않으면 값이 없는 Optional을 인자로 전달하고, 일치하는 빈이 존재하면 해당 빈을 값으로 갖는 Optional을 인자로 전달한다. 만약 값이 있다면 Date 타입의 빈을 주입받아 date 필드에 할당한다. 값이 존재하지 않는다면 주입받은 빈 객체가 없으므로 date 필드에 null을 할당한다.

3번의 사용 방법은 아래와 같다.

public class Printer{
    private Date date;
    public void print(Member member){
        생략
    }
    @Autowired
    public void setDate(@Nullable Date date){
        this.date = date;
    }
}

@Nullable 애노테이션을 의존 주입 대상 파라미터에 붙이면, 스프링 컨테이너는 세터 메서드를 호출할 때 자동주입할 빈이 존재하면 해당 빈을 인자로 전달하고, 존재하지 않으면 인자로 null을 전달한다.

@Autowired(required==false) vs @Nullable 차이 @Nullable 애노테이션의 경우 자동 주입할 빈이 존재하지 않아도 메서드가 호출되나, @Autowired의 required = false 를 사용한 경우 대상 빈이 존재하지 않으면 세터 메서드를 호출하지 않는다.'

**정리 @Autowired(required==false): 일치하는 빈이 없으면 값 할당 자체를 하지 않는다 Optional타입 : 매칭되는 빈이 없으면 값이 없는 Optional을 할당 @Nullable 애노테이션 사용: 일치하는 빈이 없으면 null값을 할당

if)설정 클래스에서 의존을 주입했는데 자동 주입 대상이면 어떻게 될까?

위 코드에서는 설정 클래스에서 다른 빈을 Haewon에 주입하고 있으나 자동 주입 한정자는 "printer"인 것을 확인할 수 있다. -> 결론적으로 자동 주입을 통해 Printer 타입인 printer라는 빈을 사용한다.

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

haewonee commented 1 month ago

5단원 컴포넌트 스캔이란?

@Component 애노테이션은 해당 클래스를 스캔 대상으로 표시한다. 이를 통해 스프링이 검색해서 빈으로 등록하는게 가능하다.

@Component
public class FirstDao{ //first 빈
        내용
}
@Component("printer")
public class Second{ //printer 빈
        내용
}

@Component 애노테이션에 값을 주었는지에 따라 빈으로 등록할 때 사용할 이름이 결정된다. 값을 주지 않았다면 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 빈 이름으로 사용한다. 위 코드에서 FirstDao클래스는 빈 이름으로 "firstDao"를 사용하고 Second클래스는 빈 이름으로 "printer"를 사용한다.

설정 클래스
@ComponentScan(backPackages = {"spring"})
public class Option{
    FirstDap, Second빈 코드 삭제
    코드 양 감소
}

이 방식을 사용하면 설정코드에서 FirstDao와 Second에 대한 코드가 없어지기에 설정 클래스 코드가 줄어든다.

위에서 @ComponentScan(backPackages = {"spring"}) 이 코드는 spring 패키지와 그 하위 패키지에 속한 클래스를 스캔 대상으로 설정하는 코드이다. 스캔 대상에 해당하는 클래스 중에서 @Component 애노테이션이 붙은 클래스의 객체를 생성해서 빈으로 등록한다.

스캔 대상에서 제외하기

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

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

이 코드는 @Filter 애노테이션의 type 속성값으로 FilterType.REGEX를 주었다. 이는 정규표현식을 사용해서 제외 대상을 지정한다는 것을 의미한다. pattern 속성은 FilterType에 적용할 값을 설정한다. 위 설정에서는 "spring."으로 시작하고 Dao로 끝나는 정규표현식을 지정했으므로 spring.FirstDao 클래스를 컴포넌트 스캔 대상에서 제외한다.

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

이 코드는 정규표현식 대신 AspectJ 패턴을 사용해서 대상을 지정한다. 위 설정에서는 spring 패키지의 Dao로 끝나는 클래스를 컴포넌트 스캔 대상에서 제외한다.

@ComponentScan(backPackages = {"spring"}, 
excludeFilteers = @Filteer(type = FilterType.ANNOTATION,classes =
{NoProduct.class,ManualBean.class}))

이 코드는 특정 애노테이션을 붙인 타입을 컴포넌트 타입에서 제외하는 코드이다. 코드에선 @NoProduct나 @ManualBean 애노테이션을 붙인 클래스를 스캔 대상에서 제외하고 있다.

++ 특정 타입이나 그 하위 타입을 컴포넌트 스캔 대상에서 제외하려면 ASSIGNABLE_TYPE을 FilterType으로 사용하면 된다.

컴포넌트 스캔 대상은 @Component 애노테이션을 붙인 클래스만 되는게 아니다.

  • @Component
  • @Controller
  • @Service
  • @Repository
  • @Aspect
  • @Configuration 다음 애노테이션을 붙인 클래스도 컴포넌트 스캔 대상에 포함된다.

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

  1. 빈 이름 충돌

    설정파일
    @Configuration
    @ComponentScan(backPackages = {spring, spring2})
    public class 설정{
    }

    이런 설정 파일이 있다. spring과 spring2에 Haewon이라는 클래스가 존재하고 두 클래스 모두 @Component 애노테이션을 붙였다고 하자. 이 상태에서 @ComponentScan을 사용하게 되면 -> exception이 발생한다. 이는 서로 다른 타입인데 같은 빈 이름을 사용했기에 발생한 문제다. -> 둘 중 하나를 명시적으로 빈 이름을 지정해서 충돌을 피해야한다. ex) spring2의 Haewon 클래스에서 Component("haha")로 변경

  2. 수동 등록한 빈과 충돌 @Component에 의해 자동 등록된 빈의 이름은 Haewon의 첫글자를 소문자로 바꾼 "haewon"이다. 그런데 다음과 같이 설정 클래스에 직접 Haewon 클래스를 "haewon"이라는 이름의 빈으로 등록한 상황이다. -> 수동 등록한 빈이 우선한다. 즉 Haewon 타입 빈은 설정파일에서 수동 등록한 한개만 존재한다.

만약 여기서 설정파일의 haewon 이름을 haewon2로 변경하면 자동등록한 haewon빈과 수동등록한 haewon2빈 모두 존재하게 된다. 이는 @Qualifier 애노테이션을 사용해서 알맞은 빈을 선택하도록 하면 된다.

io-uty commented 1 month ago

[Chapter 6]

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

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

3. 빈 객체의 생성과 관리

taekyun0219 commented 1 month ago

빈 라이프사이클과 범위

스프링 컨테이너는 초기화종료라는 라이프사이클을 갖는다.

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

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

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

컨테이너가 관리하는 빈 객체의 라이프사이클

객체 생성 → 의존 설정 → 초기화 → 소멸

스프링 컨테이너는 빈 객체를 초기화하고 소멸하기 위해 빈 객체의 지정한 메서드를 시용

이 두 인터페이스에 각각 afterPropertiesSet() 메서드와 destroy() 메서드르 저장하고 있음

스프링 컨테이너는 초기화 과정에서 afterPropertiesSet() 메서드를 실행,

소멸 과정에서 빈 객체의 destroy() 메서드를 실행함

이때 빈 객체를 소멸할 때 사용중인 데이터베이스 연결을 끊어야함

ex)채팅 클라이언트

: 시작할 때 서버와 연결을 생성하고 종료할 때 연결을 끊음

코드를 보면 destroy()를 통해 컨테이너를 종료할 때 호출한다는 것을 알 수 있음

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

모든 클래스가 InitializingBean 인터페이스와 DisposalbleBean 인터페이스를 상속받아 구혈할 수 있는 것은 아님 → 직접 구현한게 아닌 외부에서 제공받은 클래스를 스프링 빈 객체로 설정하고 싶을 떄도 있음

→@Bean 태그에서 initMethod 속성과 destroyMethod 속성을 사용해서 초기화, 소멸 메서드 이름을 지정하면 됌

ex)

‘’’
public class Client2{
    private String host;

    public void setHost(String host){
        [this.host](http://this.host) = host;
    }

    public void connect(){
        ‘’
    }

    public void send(){
        ‘’
    }

    public void close(){
        ‘'
    }

}

일 때, 초기화 과정에서 connect() 메서드를 실행하고 소멸과정에서 close() 메서드를 실행하고 싶다?

@Bean(initMethod = “connect”, destroyMethod=”close”)
public Client2 client2(){
    Client2 client = new Client2();
    client.setHost(”host”);
    return client;
}

이렇게 할 수 있음

근데 주의할 점은 설정 코드에서 초기화 메서드를 직접 실행할 때 초기화 메서드가 두번 불리면 안됌!

빈 객체의 생성과 관리 범위

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

한 식별자에 대해 한 개의 객체만 존재하는 빈은 싱글톤 범위를 갖는다. (별도 설정 안하면 빈은 싱글톤임)

빈 객체를 구할 때마다 매번 새로운 객체를 생성하려면 프로토타입 빈으로 설정하면됌

@Bean어노테이션 밑에

@ Scope(”prototype”)만 설정해주면 끝

그리고 만약 싱글톤 범위를 명시적으로 지정하고 싶다면 @Scope(”singleton”) 하면됌

→ 프로토타입으로 설정하면 아까 client1 ≠ client2 가 됌

스프링 컨테이너는 프로토타입의 빈 객체를 생성하고 프로퍼티 설정, 초기화는 하지만 생성한 프로토타입 빈 객체의 소멸 메서드는 실행x → 빈 객체의 소멸 처리를 코드에서 직접 해야한다.

AOP 프로그래밍

AOP란? (Aspect Oriented Programming) = 여러 객체에 공통으로 적용할 수 있는 기능을 분리하여 재사용성을 높여주는 프로그래밍 기법

일단 프록시, 대상 객체에 대해 알아보자

프록시는 기본적으로 다른 객체에 대한 접근을 제어하거나 기능을 추가하기 위해 중간에 위치한 대리 객체를 말한다

이렇게 핵심 기능의 실행은 다른 객체에 위임하고, 부가적인 기능을 제공하는 객체를 프록시이고, 실제 핵심 기능을 실행하는 객체는 대상 객체이다.

스프링의 AOP구현

스프링도 프록시를 이용해서 AOP를 구현함

기본적으로 핵심 기능에 공통 기능을 삽입한다. 이때 공통 기능을 Aspect라고 함

스프링 AOP 구현

공통기능을 제공하는 Aspect 구현 메서드를 만들고, @Aspect 애노테이션을 이용해서 Aspect를 구현하면 스프링 프레임워크가 알아서 프록시를 만들어줌.

  1. spring-boot-starter-aop dependency 적용

build.gradle에

추가하고 AOP 의존성을 추가하고 빌드를 하였으면 AOP를 활성화하겠다는 어노테이션을 추가해야함

implementation 'org.springframework.boot:spring-boot-starter-aop'
  1. @EnableAspectJAutoProxy 애노테이션 추가
@EnableAspectJAutoProxy
@SpringBootApplication
public class AopApplication {
     public static void main(String[] args) {
             SpringApplication.run(AopApplication.class, args);    
     }
 }

@EnableAspectJAutoProxy 애노테이션은 스프링 컨텍스트 내에서 AspectJ AOP 프레임워크를 사용할 수 있도록 함. 해당 애노테이션을 사용하면 AOP 프록시 빈을 자동으로 등록하고 AOP를 사용할 수 있게 됌

근데 스프링부트에서는 자동으로 AOP 프록시 빈을 등록하고 AOP를 사용할 수 있게 해줌

  1. 실제 AOP 로직을 작성 ( 부가기능을 정의하고 부가기능이 사용될 시점을 정의)

모든 API에 비즈니스 로직의 실행시간을 측정해야 한다고 가정

@Aspect
@Component
public class LogAspect {
    Logger logger =  LoggerFactory.getLogger(LogAspect.class);        

    //모든 패키지 내의 aop package에 존재하는 클래스    
    @Around("execution(**..aop.*.*(..))")    
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
        //해당 클래스 처리 전의 시간    
        StopWatch sw = new StopWatch();    
        sw.start();        

        //해당하는 클래스의 메소드 핵심기능을 실행    
        Object result = pjp.proceed();        

        //해당 클래스 처리 후의 시간    
        sw.stop();    
        long executionTime = sw.getTotalTimeMillis();        

        String className = pjp.getTarget().getClass().getName();    
        String methodName = pjp.getSignature().getName();    
        String task = className + ". " + methodName;        

        log debug("[ExecutionTime] " + task + "-->" + executionTime + "(ms)");        

        return result;    
    }        
}

AOP 클래스로 설정하기 위해 @Aspect 애노테이션을 추가해주고, Spring 빈으로 등록하기 위해 @Component 애노테이션을 추가 ( AOP 사용시 빈 등록을 꼭 해야함)

우리가 하고자하는건 모든 API 실행 시간을 측정하는 것이므로,

@Around 애노테이션을 통해 aop 패키지에 존재하는 모든 클래스에 해당 AOP를 적용하겠다고 설정 → @Around("execution(*..aop..*(..))")

실행 시간 측정을 위해 StopWatch를 생성하여 측정을 시작하고, pjp (우리 책에선 joinPoint) 를 통해 실제 핵심 로직을 실행하여 Object 클래스로 결과를 받음 (Object로 결과를 받아야함)

이후에 StopWatch를 중단하여 실행 시간을 밀리세컨드로 계산해 로그를 출력하고 함수를 종료시킴.

만약 실행 실행시간 측정을 밀리세컨트가 아닌 세컨드로 변경한다고 했을때 AOP를 적용하지 않았다면 관련 로직의 모든 코드를 수정하는것이 아닌 AOP를 적용함으로써 핵심 로직에 대한 수정 없이 쉽게 처리 가능!

만약 사용자가 직접 Aspect를 적용하고 싶다면?

// 이 어노테이션을 부여하면 해당 메소드의 성능을 로깅합니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
@Aspect
@Component
public class LogAspect {
    Logger logger =  LoggerFactory.getLogger(LogAspect.class);        

    @Around("@annotation(LogExecutionTime)")    
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
        //해당 클래스 처리 전의 시간    
        StopWatch sw = new StopWatch();    
        sw.start();        

        //해당하는 클래스의 메소드 핵심기능을 실행    
        Object result = pjp.proceed();        

        //해당 클래스 처리 후의 시간    
        sw.stop();    
        long executionTime = sw.getTotalTimeMillis();        

        String className = pjp.getTarget().getClass().getName();    
        String methodName = pjp.getSignature().getName();    
        String task = className + ". " + methodName;        

        log debug("[ExecutionTime] " + task + "-->" + executionTime + "(ms)");        
        return result;    
     }
}

Around를 execution에서 @annotation으로 변경하고 시간측정 AOP를 적용하고 싶은 클래스에 가서 @LogExecution 애노테이션을 붙이면됌

InSooBeen commented 1 month ago

3장

의존 관계란?


한 객체에서 다른 객체를 생성하거나, 다른 객체의 메서드를 호출한다면, 이를 의존한다고 표현한다. 즉, 의존은 변경에 의해 영향을 받는 관계를 뜻한다.

EX) A클래스에서 B클래스의 use()메서드를 사용하는 경우 B클래스의 use()가 useB()로 변경되면 A클래스에서도 useB()로 변경되어야 한다. 이를, 변경에 의해 영향을 받는 관계라고 한다.

의존하는 객체를 구하는 방법


변경의 유연함이란?


의존성을 주입하는 방식


각 방식의 장단점은?

생성자방식

setter 메서드 방식

어떤 방식이 더 권장될까?

최근, 스프링을 포함한 DI 프레임워크에서는 생성자를 이용한 주입 방식이 권장된다.

불변성 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없고, 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된다. setter 방식을 사용하면 해당 메서드를 public으로 설정해야 하므로, 변경이 가능해져 안전성이 낮다. 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없어 안전하다.

누락 프레임워크를 사용할 때는 의존성 주입에 누락이 생겨도 오류가 발생하지만, 순수 자바 단위 코드로 테스트를 하면 누락이 생겨도 오류가 발생하지 않고, Exception이 발생한다. 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생한다. 그리고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다. (생성자 주입 방식에만 해당)

final 키워드 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있어서, 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 막아준다. 컴파일 오류는 가장 빠르고 좋은 오류이다.

이러한 이유로 생성자 주입 방식이 권장된다. 따라서 생성자 주입을 선택하되, 가끔 옵션이 필요하면 setter 메서드 주입을 선택하는 것이 좋다.

객체 조립기


객체의 조립

앞에서 계속 다루었던 의존성 주입을 또다른 관점으로 바라본다면, ”의존 객체의 주입 = 서로 다른 두 객체를 조립”으로도 볼 수 있다.

그렇다면 전달하는 객체를 생성하는 코드는 어디에 위치하면 될까?

관심사의 분리, 유지보수 관점에서 생각한다면 후자가 더 나은 방법이라고 판단된다.

객체 조립기

앞서 언급한 객체의 조립 관점에서 생각해보면, 전달 객체를 생성하고 주입하는 기능을 전담하는 클래스는 객체 조립기로 표현할 수 있다. 참고로, setter 메서드에 의한 의존성 주입의 경우, 주입을 받는 객체를 불러와야 한다. 따라서, 해당 객체를 제공하는 기능도 객체 조립기에 포함된다.

스프링을 이용한 객체 조립 및 사용


스프링의 정보 설정

스프링에서 조립기에 해당하는 클래스는 스프링이 어떤 객체를 사용하며, 어떻게 의존을 주입할 지에 대한 정보를 설정한다. 이를 Configuration이라고 한다.

스프링의 정보 설정에 사용되는 애노테이션을 확인해보자.

설정 클래스를 통한 정보 설정이 끝났다면, 이 정보를 바탕으로 실제로 객체를 생성하고 의존 객체를 주입하는 기능을 할 곳이 필요하다. 바로 이 역할을 하는 것이 Container이다.

앞서 언급한 객체 조립기에서는 해당 기능을 전담하는 클래스를 만들어 사용한다고 했다. 스프링에서는 이 기능을 전담하는 Container가 존재한다.

스프링 Container 인터페이스 및 구현체 종류


스프링 컨테이너는 BeanFactory와 ApplicationContext 라는 두 종류의 인터페이스로 구현되어 있다.

스프링 컨테이너의 구현체 종류는 다음과 같다.

스프링 Container의 기능 및 생성


스프링 Container의 기능

스프링 컨테이너는 Bean의 인스턴스화, 구성, 전체 생명 주기, 제거를 관리하는 기능을 가진다. 스프링 컨테이너는 런타임에 설정 클래스를 상속한 새로운 설정 클래스를 만들어 사용하고, 런타임 과정에서 @Bean 애노테이션이 붙은 메서드에 대해 단 1개의 객체만 생성하여, 보관했다가 사용하는 방식으로 작동한다. 즉, 싱글톤 개념으로 관리한다.

스프링 Container의 생성

스프링 설정 정보를 다 작성한 후에, 사용할 컨테이너 종류와 구현체를 이용하여 생성할 수 있다. 이 책에서는 ApplicationContext 컨테이너를 AnnotationConfigApplicationContext 구현체에 AppCtx.class 정보를 전달해서 생성한다.

ApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

스프링 Container를 사용하는 이유는?


스프링 컨테이너를 사용하지 않고 객체를 생성한다면, new 생성자를 사용해야 한다. 그 결과, 애플리케이션에서는 수많은 객체가 존재하고, 서로를 참조하게 된다.

객체 간의 참조가 많아진다 = 의존성이 높아진다 = 객체 지향의 개념에서 멀어진다.

스프링 컨테이너는 구현 클래스의 의존성을 제거하고 인터페이스에만 의존하는 설계를 가능하게 하므로, 객체 지향적 설계에 가까워진다.

스프링 Container의 getBean()


컨테이너에서 제공하는 getBean() 메서드를 이용하면 등록된 Bean() 객체를 구할 수 있다.

메서드의 사용 방식은 다음과 같다.

//Bean 이름 및 타입 전달
Bean타입 변수명 = 컨테이너.getBean("Bean이름", "Bean 타입");

//Bean 타입 전달
Bean타입 변수명 = 컨테이너.getBean("Bean 타입");

getBean() 메서드 사용 중 발생할 수 있는 Exception

@Autowired


스프링 Bean에 의존하는 다른 Bean을 자동으로 주입하고 싶을 때 사용한다.

자세한 내용은 4장에서 다루므로 책에 나온 예제 case만 간단하게 설명했다.

io-uty commented 1 month ago

[Chapter 7]

1. 프로젝트 준비

[리스트 7.1] factorial

→ [리스트 7.2], [리스트 7.3] @Override (각각 for문, 재귀를 이용해 인터페이스 구현)

2. 프록시와 AOP

3. 스프링 AOP 구현

4. 프록시 생성 방식