Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
d11210920 opened this issue Jul 21, 2024 · 6 comments
Open

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

d11210920 opened this issue Jul 21, 2024 · 6 comments

Comments

@d11210920
Copy link
Collaborator

d11210920 commented Jul 21, 2024

  • 4주차
    • 3장
    • 4~5장
    • 6~7장 (2명)
@d11210920 d11210920 changed the title [4주차] (이태균, 박해원, 박수지, 인수빈) [4주차] 이태균, 박해원, 박수지, 인수빈 Jul 21, 2024
@haewonee
Copy link

haewonee commented Jul 26, 2024

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
Copy link

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 애노테이션을 붙인 클래스만 되는게 아니다.

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

  1. 빈 이름 충돌
설정파일
@Configuration
@ComponentScan(backPackages = {spring, spring2})
public class 설정{
}

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

  1. 수동 등록한 빈과 충돌

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

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

@io-uty
Copy link

io-uty commented Jul 26, 2024

[Chapter 6]

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

  • 스프링 컨테이너의 라이프사이클
    • 초기화

      AnnotationConfigApplicationContext ctx = 
      	new AnnotationConfigApplicationContext(AppContext.class);
      • 빈 객체의 생성, 의존주입, 초기화 ⇒ 컨테이너 사용 가능
      • 컨테이너 사용이란? : getBean()과 같은 메서드를 이용해 컨테이너에 보관된 빈 객체를 구하는 것
    • 종료

      ctx.close();
      • 빈 객체의 소멸

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

  • 빈 객체의 라이프사이클 : 객체생성 → 의존 설정 → 초기화 → 소멸 (컨테이너에 의해 관리됨)

    • 의존 설정 - 의존 자동 주입을 통한 의존설정 수행
    • 초기화와 소멸은 위에서 언급한 바와 같음
  • 2.1) 빈 객체의 초기화와 소멸 : 스프링 인터페이스

    • 컨테이너는 빈 객체를 초기화하고 소멸하기 위해 빈 객체의 메서드 호출

      → org.springframework.beans.factory.InitializingBean

      → org.springframework.beans.factory.DisposableBean

      ** 메서드는 위의 두 인터페이스에 정의됨 **

    //인터페이스
    public interface InitializingBean{
    	void afterPropertiesSet() throws Exception;
    }
    public interface DisposableBean{
    	void destroy() throws Exception;
    }
    1. InitializingBean : 초기화과정

      • afterPropertiesSet() 메서드 실행

      • 초기화가 필요하면 1번 인터페이스를 상속하고 afterPropertiesSet() 메서드 적당히 구현

    2. DisposableBean : 소멸과정

      • destroy() 메서드 실행

      • 소멸이 필요하면 2번 인터페이스를 상속하고 destroy() 메서드 적당히 구현

    [Ex_1] 데이터베이스 커넥션 풀

    커넥션 풀을 위해 빈 객체는

    • 초기화과정 : DB 연결 생성 → 사용과정 : 연결유지→ 소멸 : DB 연결 끊기

    [Ex_2] 채팅 클라이언트

    • (초기화 과정) 시작 : 서버 연결 → (소멸 과정) 종료 : 연결 끊기
    //.../spring/Client.java
    package spring;
    ...
    public class Client implements InitailizingBean, DisposableBean{
    	...
    	@Override
    	public void afterPropertiesSet()throws Exception{... //여기서 적당히 구현};
    	...
    	@Override
    	public void destroy() throws Exception{... //여기서 적당히 구현};
    }
    
    // .../config/AppCtx.java
    package config;
    ...
    import spring.Client;
    
    @Configuration
    public class AppCtx{ ... //의존객체 주입 }
    
    // ...main/Main.java
    package main;
    ...
    import config.AppCtx;
    import spring.Client;
    
    public class Main{
    	public static void main(String[] args) throws IOException{
    		AbstactApplicationContext ctx = 
    			new AnnotationConfigApplicationContext(AppCtx.class);
    			
    		Client client = ctx.getBean(Client.class);
    		...
    	}
    }
  • 2.2) 빈 객체의 초기화와 소멸 : 커스텀 메서드

    • 모든 클래스가 스프링 인터페이스로 구현 가능한 것은 아님 ⇒ 직접 메서드 지정

    • 방법

      : @bean 태그에서 initMethod와 destroyMethod 속성을 사용해

      초기화, 소멸 메서드 이름 지정하기

    @Bean(initMethod = "connect", destroyMethod = "close")
    public Client2 client2(){
    	Client2 client = new Client2();
    	//set ... => AAA라 하겠음
    	AAA();
    	return client;
    }
    @Bean(destroyMethod = "close")
    public Client2 client2(){
    	Client2 client = new Client2();
    	AAA()
    	client.connect();
    	return client;
    }

    → 둘 다 가능

    • 초기화메서드가 두 번 불리는 건 불가능
    @Bean
    public Client client2(){
    	Client client = new Client2();
    	AAA()
    	client.afterPropertiesSet();
    	return client;
    }
    // 위에서 InitializingBean 인터페이스를 구현했기 때문에 불가능

3. 빈 객체의 생성과 관리

  • 스프링 컨테이너는 빈 객체를 한 개만 생성 ⇒ Client client1과 client2는 동일한 빈 객체 참조

    ⇒ 싱글톤 범위를 가짐 (default)

    ⇒ 프로토타입 범위의 빈 설정 가능 → 빈 객체를 구할 때마다 새로운 객체 생성!

    : 프로토타입 범위로 지정하려면 @scope 애노테이션을 @bean과 함께 사용

    import ...;
    
    @Configuration
    public class ...{
    	@Bean
    	@Scope("prototype")
    	...
    }

    c.f) 프로토타입 범위를 갖는 빈은 완전한 라이프사이클을 따르지 않는다!

    → 초기화는 수행하지만 소멸은 수행하지 않기 때문에 코드에서 처리 필수

@taekyun0219
Copy link

taekyun0219 commented Jul 26, 2024

빈 라이프사이클과 범위

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

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

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

//컨테이너 종료
ctx.close();
  • 컨테이너 초기화 → 빈 객체의 생성, 의존 주입, 초기화
    • 스프링 컨테이너는 클래스에서 정보를 읽어와 알맞은 빈 객체를 생성하고 각 빈을 연결(의존 주입)
  • 컨테이너 초기화가 되면 컨테이너에 보관된 빈 객체를 구한다는 것을 뜻함((getBean()) 같은 메서드 이용)
  • close() 메서드를 이용해서 컨테이너를 종료함 → 빈 객체의 소멸

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

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

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

  • org.springframework.beans.factory.InitializingBean
  • org.springframework.beans.factory.DisposableBean

이 두 인터페이스에 각각 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라고 함

  • Spring

    • Aspect:
      • 흩어진 관심사를 묶어서 모듈화 한 것. (핵심 기능에 부가되어 의미를 갖는 모듈).
      • 부가된 기능을 정의한 Advice와 Advice를 어디에 적용할지 결정하는 Pointcut을 갖고 있음
      • 부가적인 기능을 분리해서 Aspect라는 모듈로 만들어 설계하고 개발하는 방법
    • Advice:
      • 실직적으로 부가기능을 담은 구현체
      • Aspect가 무엇을 언제 할지를 정의
    • Join Point:
      • Advice 가 적용될 위치, 끼어들 수 있는 지점, 메서드 진입 지점 등 다양한 시점 등 어디에 적용해야하는지에 대한 정보
      • 타겟 객체가 구현한 모든 메소드는 조인포인트가 된다
    • Point Cut:
      • JointPoint의 상세한 스펙을 정의한 것. 구체적으로 Advice가 실행될 시점
      • Advice를 적용할 joinpoint를 선별하는 기능을 정의한 모듈
    • Target:
      • Advice가 적용되는 대상(클래스, 메서드 등)
      • =부가기능을 부여할 대상(핵심기능을 담고 있는 모듈)

    스프링은 프록시를 이용해 메서드 호출 시점에 Aspect를 적용

    • Proxy

      • Target을 감싸서 Target의 요청을 대신 받아주는 랩핑 오브젝트
      • 클라이언트에서 Target을 호출하게 되면, 타겟이 아닌 타겟을 감싸고 있는 proxy가 호출되어 타겟 메서도 실행전에 선처리, 후처리 실행

      → 근데 이 책에선 대상객체의 메서드를 실행하기 전/후, 익셉션 발생시점 등 다양한 시점에 원하는 기능을 삽입하기 위해 Around Advice를 사용함

스프링 AOP 구현

  • Aspect로 사용할 클래스에 @aspect 애노테이션을 붙임
  • @pointcut 애노테이션으로 공통 기능을 적용할 Pointcut을 정의
  • 공통 기능을 구현한 메서드에 @around 애노테이션을 적용

공통기능을 제공하는 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
Copy link

InSooBeen commented Jul 26, 2024

3장

의존 관계란?


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

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

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


  • 클래스 내부에서 의존하는 객체를 직접 생성하기

    프로그래밍 하는 과정에서는 쉽지만, 유지보수 관점에서 본다면 문제를 유발할 수 있다.

  • 외부에서 의존 객체를 생성하고, 생성된 의존객체를 전달받기 (DI)

    의존성 주입 방식을 이용하면, ‘변경의 유연함’이라는 장점이 생긴다.

변경의 유연함이란?


  • 클래스 내에서 직접 의존하는 객체를 생성하는 경우

    의존 객체가 수정되면 의존 객체를 생성하는 모든 클래스에서 수정이 필요하다.

  • 의존 객체를 전달받는 경우

    의존 객체가 수정되더라도, 변경할 곳은 의존 객체를 생성하는 부분으로만 집중된다.

    이러한 특징을 ‘변경의 유연함’이라고 할 수 있다.

의존성을 주입하는 방식


  • 생성자 방식

    1. 객체의 생성자에서 의존할 대상을 주입 받는다.

    2. 객체의 생성된 후에 의존성은 변경되지 않는다.

  • setter 메서드 방식

    1. 객체가 생성된 후, setter 메서드를 통해 의존할 대상을 주입 받는다.

    2. 객체가 생성된 후에 의존성은 변경될 수 있다.

각 방식의 장단점은?

생성자방식

  • 장점

    • 객체를 생성하는 시점에 모든 의존 객체가 주입되므로 완전한 상태로 사용할 수 있다.

    • 객체의 의존성이 변경되지 않으므로 객체의 상태가 일관되게 유지된다.

  • 단점

    • 생성자의 코드를 확인해야 의존 객체를 알 수 있다.

    • 의존 객체가 많아지면, 생성자 코드가 복잡해진다.

setter 메서드 방식

  • 장점

    • 객체 생성 후에도 의존성의 설정 및 변경이 가능하므로 유연한 객체 설정이 가능하다.

    • 메서드 이름을 통해 주입되는 의존성을 확인할 수 있다.

  • 단점

    • 완전하지 않은 객체가 생성된 후 setter를 통해 의존성이 주입되므로 객체를 사용하는 시점에서 예외가 발생할 수 있다.

어떤 방식이 더 권장될까?

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

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

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

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

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

객체 조립기


객체의 조립

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

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

  • Main 메서드
    Main 메서드에서도 전달하려는 객체를 생성하고 주입할 수 있다.
  • 해당 기능을 전담하는 클래스
    전달 객체를 생성하고 주입하는 클래스를 만들어 사용할 수 있다.

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

객체 조립기

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

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


스프링의 정보 설정

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

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

  • @configuration

    위의 애노테이션이 클래스에 붙으면, 해당 클래스는 스프링 설정 클래스로 사용된다.

  • @bean

    위의 애노테이션이 메서드에 붙으면, 해당 메서드가 생성한 객체는 스프링 Bean으로 등록된다.
    이 애노테이션이 붙은 메서드는 1개의 Bean 객체를 생성하며, 따로 지정을 하지 않으면 메서드의 이름이 등록되는 Bean 객체의 이름으로 사용된다.

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

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

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


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

  • BeanFactory
    스프링 컨테이너의 최상위 인터페이스이다.
    Bean의 등록, 생성, 조회,소멸 등 빈을 관리하는 역할을 한다.
    getBean() 메서드를 통한 인스턴스화, @bean 애노테이션이 붙은 메서드의 Bean 등록 기능 모두 BeanFactory가 지원하는 것이다.

  • ApplicationContext
    BeanFactory에서 상속받은 Bean의 관리 및 검색 기능에 부가기능을 추가한 것이다.
    BeanFactory의 기능을 기반으로 다양한 기능을 제공하기 때문에, ApplicationContext가 컨테이너로 사용되는 경우가 많다.

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

  • ClassPathXmlApplicationContext

    클래스패스 내의 XML 설정 파일을 읽어들인다.

  • FileSystemXmlApplicationContext
    파일 시스템 경로에 있는 XML 설정 파일을 읽어들인다.

  • AnnotationConfigApplicationContext
    자바 기반 설정 클래스에 정의된 빈 설정을 읽어들인다.
    이 책에서 사용하고 있다.

스프링 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

  • Bean 이름 및 타입을 모두 전달할 때

    1. 존재하지 않는 Bean의 이름을 전달하는 경우
      NoSuchBeanDefinitionException

    2. Bean의 실제 타입과 전달한 타입이 다른 경우
      BeanNotOfRequiredTypeException

  • Bean 타입만 전달할 때

    1. 해당 타입의 Bean이 존재하지 않는 경우

      NoSuchBeanDefinitionException

    2. 해당 타입의 Bean이 2개 이상 존재하는 경우

      NoUniqueBeanDefinitionExcetpion

@Autowired


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

  • Ex) 설정 클래스에서의 사용
    스프링 컨테이너는 설정클래스에서 사용한 @Autowired에 대해 자동주입 처리를 한다.
    실제로 스프링은 설정 클래스를 내부적으로 스프링 Bean으로 등록한다. 그리고, @Autowired가 붙은 대상에 알맞은 Bean을 주입한다.

  • Ex)설정 클래스가 아닌 클래스에서의 사용
    스프링 컨테이너는 @Autowired가 붙은 필드에 알맞은 객체를 자동주입한다.
    필드에 @Autowired를 붙인 경우 스프링 설정 클래스의 @bean 메서드에서 의존 주입을 위한 코드를 작성하지 않아도 된다.

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

@io-uty
Copy link

io-uty commented Jul 29, 2024

[Chapter 7]

1. 프로젝트 준비

  • pom.xml(build.gradle)에 aspectjweaver 의존 추가 : AOP 설정하는데 필요한 애노테이션 제공
  • 프로젝트 생성 후 import → 코드작성

[리스트 7.1] factorial

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

2. 프록시와 AOP

  • tutorial) 앞에서 구현한 계승 구현 클래스 실행 시간 출력 방법은?

    메서드 시작과 끝의 시간을 구하고 시간의 차이 출력(기존코드 수정), 재귀 사용 (코드중복 문제발생)

    ⇒ 기존 코드를 수정하지 않고 코드 중복을 피할 수 있는 방법은 ⇒ 프록시 객체

  • 프록시 객체 구현 : 기능 자체 구현 x 다른 객체에 기능 실행 위임, 계산기능 외 다른 부가적 기능 실행

    ⇒ 코드 수정하지 않고 기능 수행 가능, 코드의 중복 제거

    → 프록시의 특징은 핵심 기능을 구현하지 않는다는 것 : 공통 기능과 핵심기능 구현을 분리!

  • 2.1 AOP

    • AOP(Aspect Oriented Programming) : 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법 ⇒ 핵심기능 코드 수정 없이 공통기능 적용

      → 핵심 기능에 공통 기능을 삽입

      • 방법(스프링에선 방법 3을 채택)
        1. 컴파일 시점에 코드에 공통 기능 삽입
        2. 클래스 로딩 시점에 바이트 코드에 공통 기능 삽입
        3. 런타임에 프록시 객체 생성, 공통 기능 삽입

      → 스프링 AOP는 프록시 객체를 자동으로 생성해주기 때문에 프록시 클래스 구현x, 공통기능 구현 클래스만 구현하면 됨!

    • 주요 용어 정리

      • Advice
        • 언제 공통 관시 기능을 핵심 로직에 적용할 것인지 정의
        • Aspect가 무엇을 언제 할지를 정의
      • Join Point
        • Advice 를 적용 가능한 지점 (메서드 호출, 필드 값 변경 등)
      • Point Cut : Join Point의 부분집합
        • 구체적으로 Advice가 적용되는 joinpoint
      • Weaving
        • Advice를 핵심 로직 코드에 적용하는 것
      • Aspect
        • AOP의 공통 기능
  • 2.2 Advice 종류

    • Around Advice : 대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는 데 사용

3. 스프링 AOP 구현

  • 구현방법
    1. Aspect로 사용할 클래스에 @aspect 애노테이션 붙이기
    2. @pointcut 애노테이션으로 공통 기능을 적용할 Pointcut 정의
    3. 공통 기능 구현한 메서드에 @around 애노테이션 적용
  • 3.1 @aspect, @pointcut, @around 를 이용한 AOP 구현
    • 공통 기능 제공하는 Aspect 구현 클래스를 만들고 자바 설정을 이용해 Aspect 적용 위치 설정
    • @pointcut - 공통 기능을 제공할 대상 설정 / @around - Around Advice 설정
  • 3.2 ProceedingJoinPoint의 메서드

4. 프록시 생성 방식

  • 4.1 execution 명시자 표현식
    • 위 명시자는 Advice 적용할 메서드 지정할 떄 사용
    • execution(수식어패턴?리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴)) 형식
  • 4.2 Advice 적용 순서
    • 한 Pointcut에 여러 Advice 적용 가능
    • @order 애노테이션에 지정한 값에 따라 적용 순서 결정
  • 4.3 @around의 Pointcut 설정과 @pointcut 재사용
    • @around 애노테이션에 execution 명시자를 직접 지정 가능

    • 같은 Pointcut을 여러 Advice가 함께 사용한다면 재사용 o

      → 다른 클래스에 위치한 @around 애노테이션에서 Pointcut 사용하고싶다면 해당 메서드를 public으로 변경

      → 해당 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 @around 애노테이션에서 사용

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants