728x90
반응형

1. Proxy

Java에서 프록시(Proxy)는 대리자를 의미한다. 프록시는 기존의 객체를 감싸서 그 객체의 기능을 확장하거나 변경할 수 있게 해준다. 예를 들어, 프록시 객체를 사용하면 객체에 대한 접근을 제어하거나, 객체의 메소드 호출 전후에 로깅 작업 등을 수행할 수 있다. 또한, 프록시 객체를 사용하여 원격으로 실행되는 객체를 호출할 수도 있다. 프록시는 주로 AOP(Aspect Oriented Programming)에서 사용된다.
  • 프록시 생성은 크게 두 가지 방식이 제공된다.
  1. 1. JDK Dynamic Proxy 방식
    • 리플렉션을 이용해서 proxy 클래스를 동적으로 생성해주는 방식으로, 타겟의 인터페이스를 기준으로 proxy를 생성해준다. 사용자의 요청이 타겟을 바라보고 실행될 수 있도록 타겟 자체에 대한 코드 수정이 아닌 리플렉션을 이용한 방식으로, 타겟의 위임 코드를InvocationHandler를 이용하여 작성하게 된다. 하지만 사용자가 타겟에 대한 정보를 잘못 주입하는 경우가 발생할 수 있기 때문에 내부적으로 주입된 타겟에 대한 검증 코드를 거친 후 invoke가 동작하게 된다.
  2. CGLib 방식
    • 동적으로 Proxy를 생성하지만 바이트코드를 조작하여 프록시를 생성해주는 방식이다. 인터페이스 뿐 아니라 타겟의 클래스가 인터페이스를 구현하지 않아도 프록시를 생성해준다. CGLib(Code Generator Library)의 경우에는 처음 메소드가 호출된 당시 동적으로 타켓 클래스의 바이트 코드를 조작하게 되고, 그 이후 호출 시부터는 변경된 코드를 재사용한다. 따라서 매번 검증 코드를 거치는 1번 방식보다는 invoke시 더 빠르게 된다. 또한 리플렉션에 의한 것이 아닌 바이트코드를 조작하는 방식이기 때문에 성능면에서는 더 우수하다.

하지만 CGLib 방식은 스프링에서 기본적으로 제공되는 방식은 아니었기에 별도로 의존성을 추가하여 개발해야 했고, 파라미터가 없는 default 생성자가 반드시 필요했으며, 생성된 프록시의 메소드를 호출하면 타겟의 생성자가 2번 호출되는 등의 문제점들이 있었다.

스프링 4.3, 스프링부트 1.3 이후부터 CGLib의 문제가 된 부분이 개선되어 기본 core 패키지에 포함되게 되었고, 스프링에서 기본적으로 사용하는 프록시 방식이 CGLib 방식이 되었다.

 

1. 로직을 포함하는 코드 작성

  • Student 인터페이스 작성
public interface Student {
	
	void study(int hours);
}

 

  • Student 클래스 작성 (Student인터페이스 구현)
public class Student implements Student {
	
	 @Override
     public void study(int hours) {
        System.out.println(hours + "시간 동안 열심히 공부합니다.");
     }
}

 

2. dynamic

1. Handler 클래스 작성

java.lang.reflect.InvocationHandler를 구현한 클래스를 작성한다.

Student 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.

public class Handler implements InvocationHandler {
	
	/* 메소드 호출을 위해 타겟 인스턴스가 필요하다 */
	private final Student student;
     
  public Handler(Student student) {
    this.student = student;
  }
    
  /* 생성된 proxy 인스턴스와 타겟 메소드, 전달받은 인자를 매개변수로 한다. */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) 
		throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    	
    System.out.println("============ 공부가 너무 하고 싶습니다. ==============");
    System.out.println("호출 대상 메소드 : " + method);
    for(Object arg : args) {
    	System.out.println("전달된 인자 : " + arg);
    }
    	
   /* 타켓 메소드를 호출한다. 타겟 Object와 인자를 매개변수로 전달한다. 
    * 여기서 프록시를 전달하면 다시 타겟을 호출할 때 다시 프록시를 생성하고 다시 또 전달하는 무한 루프에 빠지게 된다.
    * */
   method.invoke(student, args);
    	 
   System.out.println("============ 공부를 마치고 수면 학습을 시작합니다. ============");
    	 
   return proxy;
    	 
  }
}

 

2. Application 실행 클래스 작성

Student student = new Student();
Handler handler = new Handler(student);
		
/* 클래스로더, 프록시를 만들 클래스 메타 정보(인터페이스만 가능), 프록시 동작할 때 적용될 핸들러 */
Student proxy 
= (Student) Proxy.newProxyInstance(Student.class.getClassLoader(), new Class[] {Student.class}, handler);
	    
/* 프록시로 감싸진 인스턴스의 메소드를 호출하게 되면 핸들러에 정의한 메소드가 호출된다. */
proxy.study(16);

/*
============ 공부가 너무 하고 싶습니다. ==============
호출 대상 메소드 : public abstract void Student.study(int)
전달된 인자 : 16
16시간 동안 열심히 공부합니다.
============ 공부를 마치고 수면 학습을 시작합니다. ============
*/

 

  • study 메소드 호출 시 proxy 객체의 동작을 확인할 수 있다.

 

3. cglib

1. Handler 클래스 작성

org.springframework.cglib.proxy.InvocationHandler를 구현한 클래스를 작성한다.

Student 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.

public class Handler implements InvocationHandler {
	
	private final Student student;
     
  public Handler(Student student) {
   this.student = student;
  }
    
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) 
		throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    	
  	System.out.println("============ 공부가 너무 하고 싶습니다. ==============");
   	System.out.println("호출 대상 메소드 : " + method);
   	for(Object arg : args) {
   		System.out.println("전달된 인자 : " + arg);
   	}
    	
   	method.invoke(student, args);
    	 
   	System.out.println("============ 공부를 마치고 수면 학습을 시작합니다. ============");
    	 
   	return proxy;
   	 
  }
}

 

2. Application 실행 클래스 작성

Student student = new Student();
Handler handler = new Handler(student);
		
/* Enhancer 클래스의 create static 메소드는 타겟 클래스의 메타정보와 핸들러를 전달하면 proxy를 생성해서 반환해준다. */
Student proxy 
= (Student) Enhancer.create(Student.class, new Handler(new Student()));

proxy.study(20);

/*
============ 공부가 너무 하고 싶습니다. ==============
호출 대상 메소드 : public void Student.study(int)
전달된 인자 : 20
20시간 동안 열심히 공부합니다.
============ 공부를 마치고 수면 학습을 시작합니다. ============
*/
  • study 메소드 호출 시 proxy 객체의 동작을 확인할 수 있다.
728x90
반응형
728x90
반응형

[Spring Framework] Spring AOP 구현하기 | 예제 포함

 

[Spring Framework] Spring AOP 구현하기 | 예제 포함

[Spring Framework] [Spring AOP] 종류와 특징 총정리 – 입문자를 위한 기초 가이드 [Spring Framework] [Spring AOP] 종류와 특징 총정리 – 입문자를 위한 기초 가이드[Spring Framework] 초보자를 위한 Spring Bean Proper

crushed-taro.tistory.com

1. Reflection

Java 리플렉션(Reflection)은 실행 중인 자바 프로그램 내부의 클래스, 메소드, 필드 등의 정보를 분석하여 다루는 기법을 말한다. 이를 통해 프로그램의 동적인 특성을 구현할 수 있다. 예를 들어, 리플렉션을 이용하면 실행 중인 객체의 클래스 정보를 얻어오거나, 클래스 내부의 필드나 메소드에 접근할 수 있다. 이러한 기능들은 프레임워크, 라이브러리, 테스트 코드 등에서 유용하게 활용된다.

⇒ 스프링에서는 이 Reflection 기술을 사용해 런타임 시 등록한 빈을 애플리케이션 내에서 사용할 수 있게 한다.

 

1. 로직을 포함하는 코드 작성

플렉션 테스트의 대상이 될Account클래스를 생성한다.

public class Account {
	
	private String bankCode;
	private String accNo;
	private String accPwd;
	private int balance;
	
	public Account() {}
	
	public Account(String bankCode, String accNo, String accPwd) {
		this.backCode = bankCode;
		this.accNo = accNo;
		this.accPwd = accPwd;
	}
	
	public Account(String bankCode, String accNo, String accPwd, int balance) {
		this(bankCode, accNo, accPwd);
		this.balance = balance;
	}
	
	/* 현재 잔액을 출력해주는 메소드 */
	public String getBalance() {
		
		return this.accNo + " 계좌의 현재 잔액은 " + this.balance + "원 입니다.";
	}
	
	/* 금액을 매개변수로 전달 받아 잔액을 증가(입금) 시켜주는 메소드 */
	public String deposit(int money) {
		
		String str = "";
		
		if(money >= 0) {
			this.balance += money;
			str = money + "원이 입급되었습니다.";
		}else {
			str = "금액을 잘못 입력하셨습니다.";
		}
		
		return str;
	}
	
	/* 금액을 매개변수로 받아 잔액을 감소(출금) 시켜주는 메소드 */
	public String withDraw(int money) {
		
		String str = "";
		
		if(this.balance >= money) {
			this.balance -= money;
			str = money + "원이 출금되었습니다.";
		}else {
			str = "잔액이 부족합니다. 잔액을 확인해주세요.";
		}

		return str;
	}
}

 

2. 리플렉션 테스트

1. Class

Class타입의 인스턴스는 해당 클래스의 메타정보를 가지고 있는 클래스이다.

/* .class 문법을 이용하여 Class 타입의 인스턴스를 생성할 수 있다. */
Class class1 = Account.class;
System.out.println("class1 : " + class1);
		
/* Object 클래스의 getClass() 메소드를 이용하면 Class 타입으로 리턴받아 이용할 수 있다. */
Class class2 = new Account().getClass();
System.out.println("class2 : " + class2);

/* Class.forName() 메소드를 이용하여 런타임시 로딩을 하고 그 클래스 메타정보를 Class 타입으로 반환받을 수 있다. */
try {
	Class class3 = Class.forName("project.reflection.Account");
	System.out.println("class3 : " + class3);
			
	/* Double자료형 배열을 로드할 수 있다. */
	Class class4 = Class.forName("[D");
	Class class5 = double[].class;
			
	System.out.println("class4 : " + class4);
	System.out.println("class5 : " + class5);
			
	/* String자료형 배열을 로드할 수 있다. */
	Class class6 = Class.forName("[Ljava.lang.String;");
	Class class7 = String[].class;
	System.out.println("class6 : " + class6);
	System.out.println("class7 : " + class7);
			
} catch (ClassNotFoundException e) {
	e.printStackTrace();
}
		
/* 원시 자료형을 사용하면 컴파일 에러 발생 */
//		double d = 1.0;
//		Class class8 = d.getClass();
		
/* TYPE 필드를 이용하여 원시형 클래스를 반환받을 수 있다. */
Class class8 = Double.TYPE;
System.out.println("class8 : " + class8);
		
Class class9 = Void.TYPE;
System.out.println("class9 : " + class9);
		
/* 클래스의 메타 정보를 이용하여 여러 가지 정보를 반환받는 메소드를 제공한다. */
/* 상속된 부모 클래스를 반환한다. */
Class superClass = class1.getSuperclass();
System.out.println("superClass : " + superClass);

/*
class1 : class project.reflection.Account
class2 : class project.reflection.Account
class3 : class project.reflection.Account
class4 : class [D
class5 : class [D
class6 : class [Ljava.lang.String;
class7 : class [Ljava.lang.String;
class8 : double
class9 : void
superClass : class java.lang.Object
*/

 

2. field

field 정보에 접근할 수 있다.

Field[] fields = Account.class.getDeclaredFields();
for(Field field : fields) {
	System.out.println("modifiers : " + Modifier.toString(field.getModifiers()) + 
			", type : " + field.getType() + 
			", name : " + field.getName() );
}

/*
modifiers : private, type : class java.lang.String, name : backCode
modifiers : private, type : class java.lang.String, name : accNo
modifiers : private, type : class java.lang.String, name : accPwd
modifiers : private, type : int, name : balance
*/

 

3. 생성자

생성자 정보에 접근할 수 있다.

Constructor[] constructors = Account.class.getConstructors();
for(Constructor con : constructors) {
	System.out.println("name : " + con.getName());
			
	Class[] params = con.getParameterTypes();
	for(Class param : params) {
		System.out.println("paramType : " + param.getTypeName());
	}
}

/*
name : project.reflection.Account
paramType : java.lang.String
paramType : java.lang.String
paramType : java.lang.String
paramType : int
name : project.reflection.Account
paramType : java.lang.String
paramType : java.lang.String
paramType : java.lang.String
name : project.reflection.Account
*/

 

생성자를 이용하여 인스턴스를 생성할 수 있다.

try {
	Account acc = (Account) constructors[0].newInstance("20", "110-223-123456", "1234", 10000);
	System.out.println(acc.getBalance());
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
		| InvocationTargetException e) {
		e.printStackTrace();
}

/*
110-223-123456 계좌의 현재 잔액은 10000원 입니다.
*/

 

4. 생성자

메소드 정보에 접근할 수 있다.

Method[] methods = Account.class.getMethods();
Method getBalanceMethod = null;
for(Method method : methods) {
	System.out.println(Modifier.toString(method.getModifiers()) + " " + 
					method.getReturnType().getSimpleName() + " " + 
					method.getName());
			
	if("getBalance".equals(method.getName())) {
		getBalanceMethod = method;
	}
}

/*
public String getBalance
public String withDraw
public String deposit
public final native void wait
public final void wait
public final void wait
public boolean equals
public String toString
public native int hashCode
public final native Class getClass
public final native void notify
public final native void notifyAll
*/

 

invoke 메소드로 메소드를 호출할 수 있다.

try {
	System.out.println(getBalanceMethod.invoke(((Account) constructors[2].newInstance())));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
	e.printStackTrace();
} catch (InstantiationException e) {
	e.printStackTrace();
}

/*
null 계좌의 현재 잔액은 0원 입니다.
*/
728x90
반응형
728x90
반응형

[Spring Framework] 초보자를 위한 Spring Bean Properties 가이드

 

[Spring Framework] 초보자를 위한 Spring Bean Properties 가이드

[Spring Framework] Spring Boot Bean 초기화(init)와 소멸(destroy) 메서드 완전 정리 [Spring Framework] Spring Boot Bean 초기화(init)와 소멸(destroy) 메서드 완전 정리[Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton

crushed-taro.tistory.com

1. AOP

1. AOP란?

AOP는 관점 지향 프로그래밍(Aspect Oriented Programming)의 약자이다. 중복되는 공통 코드를 분리하고 코드 실행 전이나 후의 시점에 해당 코드를 삽입함으로써 소스 코드의 중복을 줄이고, 필요할 때마다 가져다 쓸 수 있게 객체화하는 기술을 말한다.

AOP 사진 1

 

2. AOP 핵심 용어

용어 설명
Aspect 핵심 비즈니스 로직과는 별도로 수행되는 횡단 관심사를 말한다.
Advice Aspect의 기능 자체를 말한다.
Join point Advice가 적용될 수 있는 위치를 말한다.
Pointcut Join point 중에서 Advice가 적용될 가능성이 있는 부분을 선별한 것을 말한다.
Weaving Advice를 핵심 비즈니스 로직에 적용하는 것을 말한다.

AOP 사진 2

 

3. Advice의 종류

종류 설명
Before 대상 메소드가 실행되기 이전에 실행되는 어드바이스
After-returning 대상 메소드가 정상적으로 실행된 이후에 실행되는 어드바이스
After-throwing 예외가 발생했을 때 실행되는 어드바이스
After 대상 메소드가 실행된 이후에(정상, 예외 관계없이) 실행되는 어드바이스
Around 대상 메소드 실행 전/후에 적용되는 어드바이스

 

4. Spring AOP

스프링 프레임워크에서 제공하는 AOP는 다음과 같은 특징을 가진다.

  • 프록시 기반의 AOP 구현체 : 대상 객체(Target Object)에 대한 프록시를 만들어 제공하며, 타겟을 감싸는 프록시는 서버 Runtime 시에 생성된다.
  • 메서드 조인 포인트만 제공 : 핵심기능(대상 객체)의 메소드가 호출되는 런타임 시점에만 부가기능(어드바이스)을 적용할 수 있다.

AOP 사진 3

728x90
반응형
728x90
반응형

[Spring Framework] Spring Boot Bean 초기화(init)와 소멸(destroy) 메서드 완전 정리

 

[Spring Framework] Spring Boot Bean 초기화(init)와 소멸(destroy) 메서드 완전 정리

[Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton부터 Prototype까지 [Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton부터 Prototype까지[Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이

crushed-taro.tistory.com

1. Bean

1. Properties

1. Properties

Properties는 키/값 쌍으로 이루어진 간단한 파일이다. 보통 소프트웨어 설정 정보를 저장할 때 사용된다. 스프링에서는 Properties를 이용하여 빈의 속성 값을 저장하고 읽어올 수 있다.

Properties 파일의 각 줄은 다음과 같은 형식으로 구성된다. 주석은#으로 시작하며, 빈 줄은 무시된다.

# 주석
key=value

product-info.properties라는 이름의 파일을 생성하고 Product 타입의 값이 될 value를 적절한 key를 설정하여 정의한다.

bread.carpbread.name=붕어빵
bread.carpbread.price=1000
beverage.milk.name=딸기우유
beverage.milk.price=1500
beverage.milk.capacity=500
beverage.water.name=지리산암반수
beverage.water.price=3000
beverage.water.capacity=500
bread.carpBread.name=\uBD95\uC5B4\uBE75
bread.carpBread.price=800
beverage.milk.name=\uC544\uBAAC\uB4DC\uC6B0\uC720
beverage.milk.price=2800
beverage.milk.capacity=950
beverage.water.name=\uC0BC\uB2E4\uC218
beverage.water.price=1000
beverage.water.capacity=2000

상품들을 빈으로 등록할 설정 파일을 작성한다. 이 때product-info.properties 파일에 기재한 값으로 상품들의 값을 초기화 하려고 한다. properties 파일을 읽어올 때 @PropertySource 어노테이션에 경로를 기재하여 읽어올 수 있으므로 읽어올 properties 파일의 경로를 작성한다.

@Configuration
/* resources 폴더 하위 경로를 기술한다. 폴더의 구분은 슬러쉬(/) 혹은 역슬러쉬(\\)로 한다. */
@PropertySource("properties/product-info.properties")
public class ContextConfiguration {

}

빈 설정 파일 내부에@Value 어노테이션을 사용하여 properties의 값을 읽어온다. @Value어노테이션은 빈의 속성 값을 자동으로 주입받을 수 있는 어노테이션이다.

@Configuration
/* resources 폴더 하위 경로를 기술한다. 폴더의 구분은 슬러쉬(/) 혹은 역슬러쉬(\\)로 한다. */
@PropertySource("properties/product-info.properties")
public class ContextConfiguration {

	/* 치환자(placeholder) 문법을 이용하여 properties에 저장된 key를 입력하면 value에 해당하는 값을 꺼내온다.
   * 공백을 사용하면 값을 읽어오지 못하니 주의한다.
   * : 을 사용하면 값을 읽어오지 못하는 경우 사용할 대체 값을 작성할 수 있다.
   **/
	@Value("${bread.carpbread.name:팥붕어빵}")
	private String carpBreadName;

	/* 값은 여러 번 불러올 수 있다. */
//	@Value("${bread.carpbread.name:슈크림붕어빵}")
//	private String carpBreadName2;

  @Value("${bread.carpbread.price:0}")
  private int carpBreadPrice;

  @Value("${beverage.milk.name:}")
  private String milkName;

  @Value("${beverage.milk.price:0}")
  private int milkPrice;

  @Value("${beverage.milk.capacity:0}")
  private int milkCapacity;

  @Bean
  public Product carpBread() {

      return new Bread(carpBreadName, carpBreadPrice, new java.util.Date());
  }

  @Bean
  public Product milk() {

      return new Beverage(milkName, milkPrice, milkCapacity);
  }

  @Bean
  public Product water(@Value("${beverage.water.name:}") String waterName,
                       @Value("${beverage.water.price:0}") int waterPrice,
                       @Value("${beverage.water.capacity:0}") int waterCapacity) {

      return new Beverage(waterName, waterPrice, waterCapacity);
  }

  @Bean
  @Scope("prototype")
  public ShoppingCart cart() {

      return new ShoppingCart();
  }
}

필드 또는 파라미터에@Value 어노테이션을 사용할 수 있으며 해당 어노테이션에 ${key} 와 같이 치환자(placeholder) 문법을 이용하여 properties에 저장된 key를 입력하면 value에 해당하는 값을 꺼내와 필드 또는 파라미터에 주입한다. key 뒤에 :을 이용하여 해당 key 값이 없을 경우에는 주입할 기본 값을 입력할 수 있다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

...생략

/*
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 23:07:58 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
cart1의 hashcode : 716487794
cart2의 hashcode : 987249254
*/

해당 빈 설정 파일을 읽어 IoC 컨테이너를 생성하고 컨테이너에서 상품 객체들을 꺼내 쇼핑 카트에 담아 출력하는 코드를 실행해보면Properties파일의 값이 잘 주입 된 것을 확인할 수 있다.

 

2. 국제화

국제화(Internationalization 또는 i18n)란, 소프트웨어를 다양한 언어와 문화권에 맞게 번역할 수 있도록 디자인하는 과정이다. Spring Framework에서 i18n은 MessageSource 인터페이스와 property 파일을 이용하여 구현된다. 각 언어별로 property 파일을 정의하면, Spring은 사용자의 로케일에 맞게 적절한 파일을 선택하여 애플리케이션 텍스트를 올바르게 번역할 수 있다.

먼저 resources 폴더에 한국어 와 영어 버전의 에러 메세지를 properties 파일로 정의한다. properties 파일에 {인덱스} 의 형식으로 문자를 입력하면 값을 전달하면서 value를 불러올 수 있다. 각 파일의 끝에는 Locale 이 적절하게 입력 되어야 한다. 다음은 일반적으로 사용되는 Locale코드들의 목록이다.

언어 국가 코드
한국어 대한민국 ko_KR
영어 미국 en_US
영어 영국 en_UK
일본어 일본 ja_JP
스페인어 스페인 es_ES
# 한국어 버전
# message_ko_KR.properties
error.404=페이지를 찾을 수 없습니다!
error.500=개발자의 잘못입니다. 개발자는 누구? {0} 입니다. 현재시간 {1}
# 영어 버전
# message_en_US.properties
error.404=Page Not Found!!
error.500=something wrong! The developer''s fault. who is devloper? It''s {0} at {1}

빈 설정 파일에MessageSource 타입의 빈을 등록한다. 지정 된 명칭이므로 컨테이너에 등록 되는 빈 이름(메소드명)을 정확하게 작성하도록 한다.

@Configuration
public class ContextConfiguration {
	
	@Bean
	public ReloadableResourceBundleMessageSource messageSource() {
		
		/* 접속하는 세션의 로케일에 따라 자동 재로딩하는 용도의 MessageSource 구현체 */
		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
		
		/* 다국어메세지를 읽어올 properties 파일의 파일 이름을 설정한다. */
		messageSource.setBasename("properties/subsection02/i18n/message");
		/* 불러온 메세지를 해당 시간 동안 캐싱한다. */
		messageSource.setCacheSeconds(10);
		/* 기본 인코딩 셋을 설정할 수 있다. */
		messageSource.setDefaultEncoding("UTF-8");
		
		return messageSource;
	}
}

ReloadableResourceBundleMessageSource 은 MessageSource 인터페이스의 구현체 중 한 종류로 접속하는 세션의 로케일에 따라 자동 재로딩하는 기능을 가지고 있다. basename 속성에 다국어 메세지를 읽어올 properties 파일의 이름을 설정한다. 그 외에도 MessageSource에 대해 다양한 설정을 할 수 있다.

ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

String error404MessageKR = context.getMessage("error.404", null, Locale.KOREA);
String error500MessageKR = context.getMessage("error.500", 
	new Object[] {"여러분", new Date()}, Locale.KOREA);

System.out.println("I18N error.404 메세지 : " + error404MessageKR);
System.out.println("I18N error.500 메세지  : " + error500MessageKR);

/*
I18N error.404 메세지 : 페이지를 찾을 수 없습니다!
I18N error.500 메세지 : 개발자의 잘못입니다. 개발자는 누구? 여러분 입니다. 현재시간 23. 6. 8. 오후 11:31
*/

빈 설정 파일을 읽어와 IoC 컨테이너를 구동시키고getMessage 메소드를 통해 읽어올 메세지의 key 값과 Locale.KOREA 라고 하는 Locale을 전달한다. ReloadableResourceBundleMessageSource 가 기능하여 한국어 메세지를 로드해오는 것을 출력을 통해 확인할 수 있다. 이 때 getMessage 메소드의 두 번째 인자는 전달 값으로 배열로 전달 시 properties 파일에 {인덱스} 로 작성했던 영역에 인덱스 순서대로 채워지는 것을 확인할 수 있다.

ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

String error404MessageUS = context.getMessage("error.404", null, Locale.US);
String error500MessageUS = context.getMessage("error.500", 
	new Object[] {"you", new Date()}, Locale.US);
		
System.out.println("The I18N message for error.404 : " + error404MessageUS);
System.out.println("The I18N message for error.500 : " + error500MessageUS);

/*
The I18N message for error.404 : Page Not Found!!
The I18N message for error.500 : something wrong! The developer's fault. who is devloper? 
It's you at 6/8/23, 11:31 PM
*/

다시 한 번 빈 설정 파일을 읽어와 IoC 컨테이너를 구동시키고getMessage 메소드를 통해 읽어올 메세지의 key 값과  Locale.US 라고 하는 Locale을 전달한다. ReloadableResourceBundleMessageSource 가 기능하여 영어 메세지를 로드해오는 것을 출력을 통해 확인할 수 있다. getMessage메소드의 두 번째 인자로 전달 된 값도 함께 잘 출력 되는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

[Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton부터 Prototype까지

 

[Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton부터 Prototype까지

[Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이드 [Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이드[Spring Framework] Spring Boot DI 완벽 정리 | @Resource 활용법 [Spring Framework] Sp

crushed-taro.tistory.com

1. Bean

1. init, destroy method

스프링 빈은 초기화(init)와 소멸화(destroy)의 라이프 사이클을 가지고 있다. 이 라이프 사이클을 이해하면 빈 객체가 생성되고 소멸될 때 추가적인 작업을 수행할 수 있다. 

init-method 속성을 사용하면 스프링이 빈 객체를 생성한 다음 초기화 작업을 수행할 메소드를 지정할 수 있다. 이 메소드는 빈 객체 생성자가 완료된 이후에 호출된다. init-method 속성으로 지정된 메소드는 일반적으로 빈의 초기화를 위해 사용된다.

destroy-method 속성을 사용하면 빈 객체가 소멸될 때 호출할 메소드를 지정할 수 있다. 이 메소드는 ApplicationContext의 close() 메소드가 호출되기 전에 빈 객체가 소멸될 때 호출된다. destroy-method 속성으로 지정 된 메소드는 일반적으로 사용하던 리소스를 반환하기 위해 사용된다.

 

1. java

java 설정 방식으로init-methoddestroy-method 를 테스트 하기 위해 Owner라는 클래스를 추가한다.

public class Owner {
	
	public void openShop() {
		System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
	}
	
	public void closeShop() {
		System.out.println("사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
	}
	
}

 

Owner 타입의 빈 객체를 설정 파일에 등록한다. @Bean 어노테이션에는 initMethoddestoryMethod 라는 속성이 있는데 해당 속성을 Owner 클래스에 정의했던 openShopcloseShop메소드로 설정한다.

@Configuration
public class ContextConfiguration {
	
	...생략
	
	/* init-method로 openShop 메소드를 설정하고 destory-method로 closeShope 메소드를 설정한다. */
	@Bean(initMethod = "openShop", destroyMethod="closeShop")
	public Owner owner() {
		
		return new Owner()
	}

}

 

실행 파일에서 빈 설정 파일을 기반으로 IoC 컨테이너를 생성하고 쇼핑 카트에 상품을 담는 동일한 코드의 끝에 컨테이너를 종료하는 코드를 추가하여 실행해본다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

...생략

/* init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만
 * destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
 * 가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 
 * 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
 * 따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
 * */
((AnnotationConfigApplicationContext) context).close();

/*
사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:58:53 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.
*/

 

openShop 메소드가 Owner 객체의 생성 시점에 호출 되고, closeShop 메소드가 Owner객체의 소멸 시점에 호출 되었음을 확인할 수 있다. init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만 destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다. 따라서 컨테이너를 종료시키면 destroy 메소드의 동작까지 확인 할 수 있다.

 

2. annotation

annotation 방식으로init-method,destroy-method를 테스트 하기 위해Owner라는 클래스를 추가한다.

@Component
public class Owner {
	
	/* init-method와 같은 설정 어노테이션이다. */
	@PostConstruct
	public void openShop() {
		System.out.println("사장님이 가게 문을 오픈하셨습니다. 이제 쇼핑을 하실 수 있습니다.");
	}
	
	/* destroy-method와 같은 설정 어노테이션이다. */
	@PreDestroy
	public void closeShop() {
		System.out.println("사장님이 가게 문을 닫으셨습니다. 이제 쇼핑을 하실 수 없습니다.");
	}
	
}

컴포넌트 스캔을 통해 빈 등록 하기 위해 @Componet 어노테이션을 클래스 위에 설정한다. javax.annotation의 @PostConstruct , @PreDestroy 어노테이션은 @Bean 어노테이션에 init-methoddestroy-method 속성을 설정하는 것과 같은 역할을 한다. 단, 해당 어노테이션을 사용할 수 있도록 라이브러리 의존성이 build.gradle.kts파일에 추가 되어 있어야 한다.

dependencies {
	...생략
	implementation("javax.annotation:javax.annotation-api:1.3.2")
}

Owner타입의 빈 객체를 컴포넌트 스캔을 통해 읽어 등록할 수 있도록 빈 설정 파일에 컴포넌트 스캔 경로를 설정한다. 그 외의 상품과 쇼핑 카트의 빈 등록 코드는 그대로 사용한다.

@Configuration
@ComponentScan("project.initdestroy.subsection02.annotation")
public class ContextConfiguration {
	...생략
}

실행 파일에서 빈 설정 파일을 기반으로 IoC 컨테이너를 생성하고 쇼핑 카트에 상품을 담는 동일한 코드의 끝에 컨테이너를 종료하는 코드를 추가하여 실행해본다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

...생략

/* init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만
 * destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
 * 가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
 * 따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
 * */
((AnnotationConfigApplicationContext) context).close();

/*
사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:59:07 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.
*/

openShop 메소드가 Owner 객체의 생성 시점에 호출 되고, closeShop 메소드가 Owner 객체의 소멸 시점에 호출 되었음을 확인할 수 있다. 

 

3. xml

xml 설정 방식으init-methoddestroy-method 를 테스트 하기 위해 Owner라는 클래스를 추가한다.

public class Owner {
	
	public void openShop() {
		System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
	}
	
	public void closeShop() {
		System.out.println("사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
	}
	
}

bean configuration file에<bean> 태그를 통해 상품, 쇼핑카트 그리고 Owener 에 대한 빈 등록을 설정한다.Owener 에는 init-methoddestroy-method 설정한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
											http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<bean id="carpBread" class="project.common.Bread">
		<constructor-arg name="name" value="붕어빵"/>
		<constructor-arg name="price" value="1000"/>
		<constructor-arg name="bakedDate" ref="today"/>
	</bean>
	
	<bean id="today" class="java.util.Date" scope="prototype"/>
	
	<bean id="milk" class="project.common.Beverage">
		<constructor-arg name="name" value="딸기우유"/>
		<constructor-arg name="price" value="1500"/>
		<constructor-arg name="capacity" value="500"/>
	</bean>
	
	<bean id="water" class="project.common.Beverage">
		<constructor-arg name="name" value="지리산암반수"/>
		<constructor-arg name="price" value="3000"/>
		<constructor-arg name="capacity" value="500"/>
	</bean>
	
	<bean id="cart" class="project.common.ShoppingCart" scope="prototype"/>

	<bean id="owner" class="project.section02.initdestroy.subsection03.xml.Owner" 
			  init-method="openShop" destroy-method="closeShop"/>
</beans>

실행 파일에서 빈 설정 파일을 기반으로 IoC 컨테이너를 생성하고 쇼핑 카트에 상품을 담는 동일한 코드의 끝에 컨테이너를 종료하는 코드를 추가하여 실행해본다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context 
	= new GenericXmlApplicationContext("section02/initdestroy/subsection03/xml/spring-context.xml");

...생략

/* init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만
 * destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
 * 가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 
 * 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
 * 따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
 * */
((GenericXmlApplicationContext) context).close();

/*
사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 22:00:09 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.
*/

openShop 메소드가 Owner 객체의 생성 시점에 호출 되고, closeShop 메소드가 Owner 객체의 소멸 시점에 호출 되었음을 확인할 수 있다. 

728x90
반응형
728x90
반응형

[Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이드

 

[Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이드

[Spring Framework] Spring Boot DI 완벽 정리 | @Resource 활용법 [Spring Framework] Spring Boot DI 완벽 정리 | @Resource 활용법[Spring Framework] Spring Boot DI 핵심 가이드 | @Primary와 @Qualifier 제대로 이해하기 [Spring Framework]

crushed-taro.tistory.com

1. Bean

아래 코드는 이번 차시의 테스트에 공통적으로 사용할 Product , Beverage , Bread , ShoppingCart 클래스이다.

 

  • Product(abstract class)
public abstract class Product {
	
	private String name;	//상품명
	private int price;		//상품가격
	
	public Product() {}

	public Product(String name, int price) {
		super();
		this.name = name;
		this.price = price;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

 

  • Beverage
public class Beverage extends Product {
	
	private int capacity;	//용량
	
	public Beverage() {
		super();
	}
	
	public Beverage(String name, int price, int capacity) {
		super(name, price);
		this.capacity = capacity;
	}
	
	public int getCapacity() {
		return this.capacity;
	}
	
	public void setCapacity(int capacity) {
		this.capacity = capacity;
	}
	
	@Override
	public String toString() {
		return super.toString() + " " + this.capacity;
	}
}

 

  • Bread
public class Bread extends Product {
	
	private java.util.Date bakedDate;	//생산시간
	
	public Bread() {
		super();
	}
	
	public Bread(String name, int price, java.util.Date bakedDate) {
		super(name, price);
		this.bakedDate = bakedDate;
	}
	
	public java.util.Date getBakedDate() {
		return this.bakedDate;
	}
	
	public void setBakedDate(java.util.Date bakedDate) {
		this.bakedDate = bakedDate;
	}
	
	@Override
	public String toString() {
		return super.toString() + " " + this.bakedDate;
	}
}

 

  • ShoppingCart
public class ShoppingCart {
	
	private final List<Product> items;	//쇼핑카트에 담긴 상품들
	
	public ShoppingCart() {
		items = new ArrayList<>();
	}
	
	public void addItem(Product item) {
		items.add(item);
	}
	
	public List<Product> getItem() {
		return items;
	}
}

 

1. Bean Scope

bean scope란, 스프링 빈이 생성될 때 생성되는 인스턴스의 범위를 의미한다. 스프링에서는 다양한 bean scope를 제공한다.

Bean Scope Description
Singleton 하나의 인스턴스만을 생성하고, 모든 빈이 해당 인스턴스를 공유하여 사용한다.
Prototype 매번 새로운 인스턴스를 생성한다.
Request HTTP 요청을 처리할 때마다 새로운 인스턴스를 생성하고, 요청 처리가 끝나면 인스턴스를 폐기한다. 웹 애플리케이션 컨텍스트에만 해당된다.
Session HTTP 세션 당 하나의 인스턴스를 생성하고, 세션이 종료되면 인스턴스를 폐기한다. 웹 애플리케이션 컨텍스트에만 해당된다.

 

1. singleton

Spring Framework에서 Bean의 기본 스코프는 singleton이다. singleton은 애플리케이션 내에서 하나의 인스턴스만을 생성하고, 모든 빈이 해당 인스턴스를 공유하여 사용한다. 이를 통해 메모리 사용량을 줄일 수 있으며, 성능 향상을 기대할 수 있다.

 

@Configuration
public class ContextConfiguration {
	
	@Bean
	public Product carpBread() {
		
		return new Bread("붕어빵", 1000, new java.util.Date());
	}
	
	@Bean
	public Product milk() {
		
		return new Beverage("딸기우유", 1500, 500);
	}
	
	@Bean
	public Product water() {
		
		return new Beverage("지리산암반수", 3000, 500);
	}
	
	@Bean
	@Scope("singleton")		//기본값
	public ShoppingCart cart() {
		
		return new ShoppingCart();
	}
}

ContextConfiguration 설정 파일에Bread 타입의 붕어빵과 Beaverage 타입의 딸기우유와 지리산암반수, ShoppingCart 타입의 쇼핑카트를 bean으로 등록 한다. @Scope 어노테이션에 기본 값에 해당하는 singleton설정도 기재하였다.

 

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

/* 붕어빵, 딸기우유, 지리산 암반수 등의 빈 객체를 반환 받는다. */
Product carpBread = context.getBean("carpBread", Bread.class);
Product milk = context.getBean("milk", Beverage.class);
Product water = context.getBean("water", Beverage.class);

/* 첫 번째 손님이 쇼핑 카트를 꺼낸다. */
ShoppingCart cart1 = context.getBean("cart", ShoppingCart.class);
cart1.addItem(carpBread);
cart1.addItem(milk);

/* 붕어빵과 딸기우유가 담겨있다. */
System.out.println("cart1에 담긴 내용 : " + cart1.getItem());

/* 두 번째 손님이 쇼핑 카트를 꺼낸다. */		
ShoppingCart cart2 = context.getBean("cart", ShoppingCart.class);
cart2.addItem(water);

/* 붕어빵과 딸기우유와 지리산암반수가 담겨있다. */
System.out.println("cart2에 담긴 내용 : " + cart2.getItem());
		
/* 두 카드의 hashcode를 출력해보면 동일한 것을 볼 수 있다. */
System.out.println("cart1의 hashcode : " + cart1.hashCode());
System.out.println("cart2의 hashcode : " + cart2.hashCode());

/*
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:58:27 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:58:27 KST 2023, 딸기우유 1500 500, 지리산암반수 3000 500]
cart1의 hashcode : 696933920
cart2의 hashcode : 696933920
*/

이 예제에서 손님 두 명이 각각 쇼핑 카트를 이용해 상품을 담는다고 가정했지만singleton으로 관리되는 cart는 사실 하나의 객체이므로 두 손님이 동일한 카트에 물건을 담는 상황이 발생한다. 상황에 따라서 기본 값인 singleton 스코프가 아닌 prototype 스코프가 필요할 수 있다. 

 

2. prototype

prototype 스코프를 갖는 Bean은 매번 새로운 인스턴스를 생성한다. 이를 통해 의존성 주입 등의 작업에서 객체 생성에 대한 부담을 줄일 수 있다.
@Configuration
public class ContextConfiguration {
	
	@Bean
	public Product carpBread() {
		
		return new Bread("붕어빵", 1000, new java.util.Date());
	}
	
	@Bean
	public Product milk() {
		
		return new Beverage("딸기우유", 1500, 500);
	}
	
	@Bean
	public Product water() {
		
		return new Beverage("지리산암반수", 3000, 500);
	}
	
	@Bean
	@Scope("prototype")		//기본 값에서 변경
	public ShoppingCart cart() {
		
		return new ShoppingCart();
	}

}

이전 예제와 동일하게 ContextConfiguration 설정 파일에 Bread 타입의 붕어빵과 Beaverage 타입의 딸기우유와 지리산암반수, ShoppingCart 타입의 쇼핑카트를 bean으로 등록 한다. 단, ShoppingCart의 경우 @Scope 어노테이션에 기본 값이 아닌 해당하는 prototype설정도 기재하였다.

 

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

/* 붕어빵, 딸기우유, 지리산 암반수 등의 빈 객체를 반환 받는다. */
Product carpBread = context.getBean("carpBread", Bread.class);
Product milk = context.getBean("milk", Beverage.class);
Product water = context.getBean("water", Beverage.class);

/* 첫 번째 손님이 쇼핑 카트를 꺼낸다. */
ShoppingCart cart1 = context.getBean("cart", ShoppingCart.class);
cart1.addItem(carpBread);
cart1.addItem(milk);

/* 붕어빵과 딸기우유가 담겨있다. */
System.out.println("cart1에 담긴 내용 : " + cart1.getItem());

/* 두 번째 손님이 쇼핑 카트를 꺼낸다. */		
ShoppingCart cart2 = context.getBean("cart", ShoppingCart.class);
cart2.addItem(water);

/* 지리산암반수가 담겨있다. */
System.out.println("cart2에 담긴 내용 : " + cart2.getItem());
		
/* 두 카드의 hashcode를 출력해보면 다른 것을 볼 수 있다. */
System.out.println("cart1의 hashcode : " + cart1.hashCode());
System.out.println("cart2의 hashcode : " + cart2.hashCode());

/*
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:58:40 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
cart1의 hashcode : 1946988038
cart2의 hashcode : 1990519794
*/

ShoppingCart의 bean scope를prototype으로 설정하자 getBean으로 인스턴스를 꺼내올 때 마다 새로운 인스턴스를 생성하게 된다. 따라서 이번 예제에서는 손님 두 명이 각각 쇼핑 카트를 이용해 상품을 담는 상황이 잘 연출되었다.

 

3. xml 설정

의 예제에서는 모두 Java 빈 객체 설정을 통해 확인해보았다. 만약 XML 파일에<bean> 태그를 이용한다면 다음과 같이 속성을 기재할 수 있다.

<!-- singleton 설정 --> 
<bean id="cart" class="패키지명.ShoppingCart" scope="singleton"/>

<!-- prototype 설정 -->
<bean id="cart" class="패키지명.ShoppingCart" scope="prototype"/>

XML 설정에 대한 별도의 실행 예제는 생략한다.

728x90
반응형
728x90
반응형

[Spring Framework] Spring Boot DI 완벽 정리 | @Resource 활용법

 

[Spring Framework] Spring Boot DI 완벽 정리 | @Resource 활용법

[Spring Framework] Spring Boot DI 핵심 가이드 | @Primary와 @Qualifier 제대로 이해하기 [Spring Framework] Spring Boot DI 핵심 가이드 | @Primary와 @Qualifier 제대로 이해하기[Spring Framework] Spring Framework 의존성 주입(DI)

crushed-taro.tistory.com

1. DI Annotation

1. @Inject

@Inject 어노테이션은 자바에서 제공하는 기본 어노테이션이다. @Autowired 어노테이션과 같이 Type으로 빈을 의존성 주입한다.

당 어노테이션은 사용하기 전 라이브러리 의존성 추가가 필요하므로 Maven Repository에서javax inject을 검색하여 build.gradle.kts파일에 아래와 같은 구문을 추가한다.

dependencies {
	implementation("javax.inject:javax.inject:1")
	...생략
}

 

1. 필드 주입

드로Pokemon 타입의 객체를 의존성 주입 받는 PokemonService 클래스를 선언한다. @Inject 어노테이션은 Type 으로 의존성 주입하므로 3개의 동일한 타입의 빈이 있는 현재 상황에서는 오류가 발생한다. 따라서 @Named어노테이션을 함께 사용해서 빈의 이름을 지정하면 해당 빈을 의존성 주입할 수 있다.

@Service("pokemonServiceInject")
public class PokemonService {

	/* 1. 필드 주입 */
	@Inject
	@Named("pikachu")
	private Pokemon pokemon;

	public void pokemonAttack() {
		pokemon.attack();
	}
}

CharmanderPikachuSquirtlePokemonService 를 빈 스캐닝 할 수 있는 basePackages를 설정하여 스프링 컨테이너를 생성한다.

ApplicationContext context = new AnnotationConfigApplicationContext("project");

PokemonService pokemonService = context.getBean("pokemonServiceInject", PokemonService.class);
		
pokemonService.pokemonAttack();

/*
피카츄 백만볼트⚡
*/

 

2. 생성자 주입

성자로Pokemon 타입의 객체를 의존성 주입 받는 PokemonService클래스를 선언한다.

@Service("pokemonServiceInject")
public class PokemonService {

	private Pokemon pokemon;

	/* 2. 생성자 주입 */
	@Inject
	public PokemonService(@Named("pikachu") Pokemon pokemon) {
		this.pokemon = pokemon;
	}

	public void pokemonAttack() {
		pokemon.attack();
	}
}

/*
피카츄 백만볼트⚡
*/

@Named어노테이션의 경우 메소드 레벨, 파라미터 레벨에서 둘 다 사용 가능하다.

 

3. 세터 주입

터로Pokemon타입의 객체를 의존성 주입 받는PokemonService클래스를 선언한다.

@Service("pokemonServiceInject")
public class PokemonService {

	private Pokemon pokemon;

	/* 3. 세터 주입 */
	@Inject
	public void setPokemon(@Named("pikachu") Pokemon pokemon) {
		this.pokemon = pokemon;
	}

	public void pokemonAttack() {
		pokemon.attack();
	}
}

/*
피카츄 백만볼트⚡
*/

@Inject는 필드 주입생성자 주입세터 주입모두 가능하다.

 

정리

DI는 스프링 프레임워크에서 매우 중요한 개념 중 하나로, 개발자는 객체 간의 의존성을 직접 관리하지 않고 스프링 컨테이너가 객체 간의 의존성을 주입해주는 방식으로 관리할 수 있다.

다양한 DI 어노테이션이 있는데 각각의 특징과 사용 방식이 다르다.

  @Autowried @Resource @Inject
제공 Spring Java Java
지원 방식 필드, 생성자, 세터 필드, 세터 필드, 생성자, 세터
빈 검색 우선 순위 타입 → 이름 이름 → 타입 타입 → 이름
빈 지정 문법 @Autowired
@Qualifier(”name”)
@Resource(name=”name”) @Inject
@Named(”name”)
728x90
반응형
728x90
반응형

[Spring Framework] Spring Boot DI 핵심 가이드 | @Primary와 @Qualifier 제대로 이해하기

 

[Spring Framework] Spring Boot DI 핵심 가이드 | @Primary와 @Qualifier 제대로 이해하기

[Spring Framework] Spring Framework 의존성 주입(DI) 어노테이션 총정리 [Spring Framework] Spring Framework 의존성 주입(DI) 어노테이션 총정리[Spring Framework] Spring Boot에서 Dependency Injection 완벽 가이드 | 개념부터

crushed-taro.tistory.com

1. Collection

  • 같은 타입의 빈을 여러 개 주입 받고 싶다면Collection타입을 활용할 수 있다.

 

1. List 타입

List<Pokemon>타입의 객체를 의존성 주입 받는PokemonService래스를 선언한다.

@Service("pokemonServiceCollection")
public class PokemonService {
	
	/* 1. List 타입으로 주입 */
	private List<Pokemon> pokemonList;

	@Autowired
	public PokemonService(List<Pokemon> pokemonList) {
		this.pokemonList = pokemonList;
	}

	public void pokemonAttack() {
		pokemonList.forEach(Pokemon::attack);
	}
}

CharmanderPikachuSquirtlePokemonService 를 빈 스캐닝 할 수 있는 basePackages를 설정하여 스프링 컨테이너를 생성한다.

ApplicationContext context = new AnnotationConfigApplicationContext("project");

PokemonService pokemonService = context.getBean("pokemonServiceCollection", PokemonService.class);
		
pokemonService.pokemonAttack();

/*
파이리 불꽃 공격🔥
피카츄 백만볼트⚡
꼬부기 물대포 발사🌊
*/

bean 이름의 사전순으로 List에 추가 되어 모든 Pokemon 타입의 빈이 주입 된다.

 

2. Map 타입

Map<String, Pokemon>타입의 객체를 의존성 주입 받는PokemonService클래스를 선언한다.

@Service("pokemonServiceCollection")
public class PokemonService {
	
	/* 2. Map 타입으로 주입 */
	private Map<String, Pokemon> pokemonMap;
	
	@Autowired
	public PokemonService(Map<String, Pokemon> pokemonMap) {
		this.pokemonMap = pokemonMap;
	}
	
	public void pokemonAttack() {
      pokemonMap.forEach((k, v) -> {
          System.out.println("key : " + k);
          System.out.print("공격 : ");
          v.attack();
      });
  }
}

/*
key : charmander
공격 : 파이리 불꽃 공격🔥
key : pikachu
공격 : 피카츄 백만볼트⚡
key : squirtle
공격 : 꼬부기 물대포 발사🌊
*/

bean 이름의 사전순으로 Map에 추가 되어 모든 Pokemon 타입의 빈이 주입 된다.

 

2. Resource

@Resource 어노테이션은 자바에서 제공하는 기본 어노테이션이다. @Autowired와 같은 스프링 어노테이션과 다르게 
name 속성 값으로 의존성 주입을 할 수 있다.

해당 어노테이션은 사용하기 전 라이브러리 의존성 추가가 필요하므로 Maven Repository에서javax annoataion을 검색하여 build.gradle.kts파일에 아래와 같은 구문을 추가한다.

dependencies {
	implementation("javax.annotation:javax.annotation-api:1.3.2")
	...생략
}

 

1. 이름으로 주입

드로Pokemon 타입의 객체를 의존성 주입 받는 PokemonService 클래스를 선언한다. @Resource 어노테이션의 name속성에 주입할 빈 객체의 이름을 지정한다.

@Service("pokemonServiceResource")
public class PokemonService {
  
  /* pikachu 이름의 빈 지정 */
	@Resource(name = "pikachu")
	private Pokemon pokemon;

	public void pokemonAttack() {
		pokemon.attack();
	}
}

CharmanderPikachuSquirtlePokemonService 를 빈 스캐닝 할 수 있는 basePackages를 설정하여 스프링 컨테이너를 생성한다.

ApplicationContext context = new AnnotationConfigApplicationContext("project");

PokemonService pokemonService = context.getBean("pokemonServiceResour", PokemonService.class);
		
pokemonService.pokemonAttack();

/*
피카츄 백만볼트⚡
*/

 

2. 타입으로 주입

List<Pokemon> 타입으로 변경한 뒤 name 속성을 따로 기재하지 않고 동작시킬 수 있다. 기본적으로는 name속성을 통해 주입하지만 name 속성이 없을 경우 Type을 통해 의존성 주입한다.

@Service("pokemonServiceResource")
public class PokemonService {
  
  @Resource
	private List<Pokemon> pokemonList;

	public void pokemonAttack() {
		pokemonList.forEach(Pokemon::attack);
	}
}

/*
파이리 불꽃 공격🔥
피카츄 백만볼트⚡
꼬부기 물대포 발사🌊
*/

bean 이름의 사전순으로 List에 추가 되어 모든 Pokemon 타입의 빈이 주입 된다.

728x90
반응형
728x90
반응형

[Spring Framework] Spring Framework 의존성 주입(DI) 어노테이션 총정리

 

[Spring Framework] Spring Framework 의존성 주입(DI) 어노테이션 총정리

[Spring Framework] Spring Boot에서 Dependency Injection 완벽 가이드 | 개념부터 실습까지 [Spring Framework] Spring Boot에서 Dependency Injection 완벽 가이드 | 개념부터 실습까지[Spring Framework] Annotation-based Configuration

crushed-taro.tistory.com

1. DI Annotation

  • @Autowired 어노테이션은 가장 보편적으로 사용 되는 의존성 주입 Annotation이다. @Autowired 와 함께 사용하거나 또는 대체해서 사용할 수 있는 어노테이션을 학습한다.
  • 아래 코드는 테스트에 공통적으로 사용 할 Pokemon, Charmander, Pikachu, Squirtle 클래스이다.

 

  • Pokemon
public interface Pokemon {
	
	/* 공격하다 */
	void attack();
}
  • Charmander
@Component
public class Charmander implements Pokemon {

	@Override
	public void attack() {
		System.out.println("파이리 불꽃 공격🔥");
	}
}
  • Pikachu
@Component
public class Pikachu implements Pokemon {

	@Override
	public void attack() {
		System.out.println("피카츄 백만볼트⚡");
	}
}
  • Squirtle
@Component
public class Squirtle implements Pokemon {

	@Override
	public void attack() {
		System.out.println("꼬부기 물대포 발사🌊");
	}
}

 

1. @Primary

@Primary 어노테이션은 여러 개의 빈 객체 중에서 우선순위가 가장 높은 빈 객체를 지정하는 어노테이션이다.

생성자로 Pokemon 타입의 객체를 의존성 주입 받는 PokemonService클래스를 선언한다.

@Service("pokemonServicePrimary")
public class PokemonService {
	
	private Pokemon pokemon;
	
	@Autowired
	public PokemonService(Pokemon pokemon) {
		this.pokemon = pokemon;
	}
	
	public void pokemonAttack() {
		pokemon.attack();
	}

}

CharmanderPikachuSquirtlePokemonService 를 빈 스캐닝 할 수 있는 basePackages를 설정하여 스프링 컨테이너를 생성한다.

ApplicationContext context = new AnnotationConfigApplicationContext("project");

PokemonService pokemonService = context.getBean("pokemonServicePrimary", PokemonService.class);
		
pokemonService.pokemonAttack();

/*
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'pokemonServicePrimary' defined in file 파일 경로 : 
Unsatisfied dependency expressed through constructor parameter 0; 
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type 'project.common.Pokemon' available: 
expected single matching bean but found 3: charmander,pikachu,squirtle
...생략
*/

스프링 컨테이너 내부에 Pokemon 타입의 빈 객체가 charmander,pikachu,squirtle 3개가 있어 1개의 객체를 PokemonService의 생성자로 전달할 수 없어 오류가 발생했음을 확인할 수 있다.

 

Charmander,Pikachu,Squirtle중에서Charmander빈 객체를 우선적으로 주입받도록@Primary어노테이션을 설정한다.

@Component
@Primary
public class Charmander implements Pokemon {

    @Override
    public void attack() {
        System.out.println("파이리 불꽃 공격🔥");
    }
}

/*
파이리 불꽃 공격🔥
*/

@Primary 어노테이션을 설정하면 @Autowired로 동일한 타입의 여러 빈을 찾게 되는 경우 자동으로 연결 우선 시 할 타입으로 설정 된다.

동일한 타입의 클래스 중 한 개만 @Primary 어노테이션을 사용할 수 있다.

Charmander 빈 객체에 @Primary 어노테이션이 설정되어 있으므로, PokemonService의 생성자로 Pokemon 객체를 주입받으면 Charmander 빈 객체가 우선적으로 주입된다.

 

2. @Qualifier

@Qualifierb어노테이션은 여러 개의 빈 객체 중에서 특정 빈 객체를 이름으로 지정하는 어노테이션이다. 

 

1. 필드 주입

드로Pokemon 타입의 객체를 의존성 주입 받는 PokemonService 클래스를 선언한다. @Autowired 어노테이션과 함께 @Qualifier 어노테이션을 사용하여 빈 이름을 통해 주입할 빈 객체를 지정한다.

@Service("pokemonServiceQualifier")
public class PokemonService {
	
  /* @Qualifier 어노테이션을 사용하여 pikachu 빈 객체를 지정한다. */
	@Autowired
	@Qualifier("pikachu")
	private Pokemon pokemon;
	
	public void pokemonAttack() {
		pokemon.attack();
	
}

CharmanderPikachuSquirtlePokemonService 를 빈 스캐닝 할 수 있는 basePackages를 설정하여 스프링 컨테이너를 생성한다.

ApplicationContext context = new AnnotationConfigApplicationContext("project");

PokemonService pokemonService = context.getBean("pokemonServiceQualifier", PokemonService.class);
		
pokemonService.pokemonAttack();

/*
피카츄 백만볼트⚡
*/

@Primary 어노테이션과 @Qualifier 어노테이션이 함께 쓰였을 때 @Qualifier 우선한다는 것도 결과를 통해 확인할 수 있다.

 

2. 생성자 주입

생성자 주입의 경우 @Qualifier 어노테이션은 메소드의 파라미터 앞에 기재한다. 역시 빈 이름을 통해 주입할 빈 객체를 지정한다.

@Service("pokemonServiceQualifier")
public class PokemonService {
	
	private Pokemon pokemon;

  /* @Qualifier 어노테이션을 사용하여 squirtle 빈 객체를 지정한다. */
	@Autowired
	public PokemonService(@Qualifier("squirtle") Pokemon pokemon) {
		this.pokemon = pokemon;
	}
	
	public void pokemonAttack() {
		pokemon.attack();
	}
}

/*
꼬부기 물대포 발사🌊
*/
728x90
반응형
728x90
반응형

[Spring Framework] Spring Boot에서 Dependency Injection 완벽 가이드 | 개념부터 실습까지

 

[Spring Framework] Spring Boot에서 Dependency Injection 완벽 가이드 | 개념부터 실습까지

[Spring Framework] Annotation-based Configuration이란? Spring 설정을 더 간결하게! [Spring Framework] Annotation-based Configuration이란? Spring 설정을 더 간결하게![Spring Framework] Spring IoC 컨테이너 사용법 완벽 정리 | 실

crushed-taro.tistory.com

1. DI Annotation (@Autowired)

@Autowired 어노테이션은 Type을 통한 DI를 할 때 사용한다. 스프링 컨테이너가 알아서 해당 타입의 Bean을 찾아서 주입해준다.

아래 코드는 테스트에 공통적으로 사용 할 BookDTOBookDAOBookDAOImpl 클래스이다.

 

  • BookDTO
@Data
@AllArgsConstructor
public class BookDTO {

    private int sequence;        //도서번호
    private int isbn;            //isbn
    private String title;        //제목
    private String author;       //저자
    private String publisher;    //출판사
    private Date createdDate;    //출판일

}

 

  • BookDAO
public interface BookDAO {

    /* 도서 목록 전체 조회 */
    List<BookDTO> selectBookList();

    /* 도서 번호로 도서 조회 */
    BookDTO selectOneBook(int sequence);
}

 

  • BookDAOImpl
/* @Repository : @Component의 세분화 어노테이션의 한 종류로 DAO 타입의 객체에 사용한다. */
@Repository("bookDAO")
public class BookDAOImpl implements BookDAO {

    private Map<Integer, BookDTO> bookList;

    public BookDAOImpl() {
        bookList = new HashMap<>();
        bookList.put(1, new BookDTO(1, 123456, "자바의 정석", "남궁성", "도우출판", new Date()));
        bookList.put(2, 
					new BookDTO(2, 654321, "칭찬은 고래도 춤추게 한다", "고래", "고래출판", new Date()));
    }

    @Override
    public List<BookDTO> selectBookList() {
        return new ArrayList<>(bookList.values());
    }

    @Override
    public BookDTO selectOneBook(int sequence) {
        return bookList.get(sequence);
    }
}

 

1. 필드(field) 주입

/* @Service : @Component의 세분화 어노테이션의 한 종류로 Service 계층에서 사용한다. */
@Service("bookServiceField")
public class BookService {

    /* BookDAO 타입의 빈 객체를 이 프로퍼티에 자동으로 주입해준다. */
    @Autowired
    private BookDAO bookDAO;

		/* 도서 목록 전체 조회 */
    public List<BookDTO> selectAllBooks(){

        return bookDAO.selectBookList();
    }

		/* 도서 번호로 도서 조회 */
    public BookDTO searchBookBySequence(int sequence) {

        return bookDAO.selectOneBook(sequence);
    }
}

private BookDAO bookDAO = new BookDAOImpl(); 와 같이 필드를 선언한다면 BookService 클래스는 BookDAOImpl 클래스의 변경에 직접적으로 영향을 받는 강한 결합을 가지게 된다. 객체간의 결합을 느슨하게 하기 위해 new BookDAOImpl() 와 같은 직접적으로 객체를 생성하는 생성자 구문을 제거하고 필드에 @Autowired 어노테이션을 작성할 수 있다. 그러면 스프링 컨테이너는 BookService 빈 객체 생성 시 BookDAO 타입의 빈 객체를 찾아 의존성을 주입해준다.

 

스프링 컨테이너를 생성하여 @Repository@Service 등의 어노테이션이 작성 된 클래스가 빈 스캐닝을 통해 잘 등록 되었는지, 또한 객체의 의존 관계에 따라 @Autowired어노테이션을 통해 의존성 주입이 되었는지를 테스트한다.

/* AnnotationConfigApplicationContext 생성자에 basePackages 문자열을 전달하며 ApplicationContext 생성한다. */
ApplicationContext context = new AnnotationConfigApplicationContext("project");

BookService bookService = context.getBean("bookServiceField", BookService.class);

/* 전체 도서 목록 조회 후 출력 확인 */
bookService.selectAllBooks().forEach(System.out::println);

/* 도서번호로 검색 후 출력 확인*/
System.out.println(bookService.searchBookBySequence(1));
System.out.println(bookService.searchBookBySequence(2));

/*
BookDTO(sequence=1, isbn=123456, title=자바의 정석, author=남궁성, publisher=도우출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=2, isbn=654321, title=칭찬은 고래도 춤추게 한다, author=고래, publisher=고래출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=1, isbn=123456, title=자바의 정석, author=남궁성, publisher=도우출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=2, isbn=654321, title=칭찬은 고래도 춤추게 한다, author=고래, publisher=고래출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
*/

 

2. 생성자(constructor) 주입

/* @Service : @Component의 세분화 어노테이션의 한 종류로 Service 계층에서 사용한다. */
@Service("bookServiceConstructor")
public class BookService {

    private final BookDAO bookDAO;

		/* BookDAO 타입의 빈 객체를 생성자에 자동으로 주입해준다. */
    @Autowired
    public BookService(BookDAO bookDAO) {
        this.bookDAO = bookDAO;
    }

    public List<BookDTO> selectAllBooks(){

        return bookDAO.selectBookList();
    }

    public BookDTO searchBookBySequence(int sequence) {

        return bookDAO.selectOneBook(sequence);
    }

}

생성자에도 @Autowired 어노테이션을 작성할 수 있다. 그러면 스프링 컨테이너는 BookService 빈 객체 생성 시 BookDAO 타입의 빈 객체를 찾아 의존성을 주입해준다.

Spring 4.3 버전 이후로는 생성자가 한 개 뿐이라면 @Autowired 어노테이션을 생략해도 자동으로 생성자 주입이 동작한다. 단, 생성자가 1개 이상일 경우에는 명시적으로 작성을 해주어야 한다. 위의 코드에 기본 생성자를 추가로 작성하고 매개변수 생성자에 @Autowired 어노테이션을 생략하게 되면 생성자 주입이 동작하지 않아 오류가 발생한다.

 

생성자 주입의 장점

  • 객체가 생성 될 때 모든 의존성이 주입 되므로 의존성을 보장할 수 있다.
    • 필드 주입/세터 주입은 의존성이 있는 객체가 생성되지 않아도 객체 생성은 가능하여 메소드가 호출 되면(런타임) 오류가 발생한다.
    • 생성자 주입은 의존성이 있는 객체가 생성되지 않으면 객체 생성이 불가능하여 어플리케이션 실행 시점에 오류가 발생한다.
  • 객체의 불변성을 보장할 수 있다.
    • 필드에 final 키워드를 사용 할 수 있고 객체 생성 이후 의존성을 변경할 수 없어 안정성이 보장 된다.
  • 코드 가독성이 좋다.
    • 해당 객체가 어떤 의존성을 가지고 있는지 명확히 알 수 있다.
  • DI 컨테이너와의 결합도가 낮기 때문에 테스트 하기 좋다.
    • 스프링 컨테이너 없이 테스트를 할 수 있다.

 

프링 컨테이너를 생성하여@Repository@Service 등의 어노테이션이 작성 된 클래스가 빈 스캐닝을 통해 잘 등록 되었는지, 또한 객체의 의존 관계에 따라 @Autowired어노테이션을 통해 의존성 주입이 되었는지를 테스트한다.

/* AnnotationConfigApplicationContext 생성자에 basePackages 문자열을 전달하며 ApplicationContext 생성한다. */
ApplicationContext context = new AnnotationConfigApplicationContext("project");

BookService bookService = context.getBean("bookServiceConstructor", BookService.class);

/* 전체 도서 목록 조회 후 출력 확인 */
bookService.selectAllBooks().forEach(System.out::println);

/* 도서번호로 검색 후 출력 확인*/
System.out.println(bookService.searchBookBySequence(1));
System.out.println(bookService.searchBookBySequence(2));

/*
BookDTO(sequence=1, isbn=123456, title=자바의 정석, author=남궁성, publisher=도우출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=2, isbn=654321, title=칭찬은 고래도 춤추게 한다, author=고래, publisher=고래출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=1, isbn=123456, title=자바의 정석, author=남궁성, publisher=도우출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=2, isbn=654321, title=칭찬은 고래도 춤추게 한다, author=고래, publisher=고래출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
*/

 

3. 세터(setter) 주입

/* @Service : @Component의 세분화 어노테이션의 한 종류로 Service 계층에서 사용한다. */
@Service("bookServiceSetter")
public class BookService {

    private BookDAO bookDAO;

    /* BookDAO 타입의 빈 객체를 setter에 자동으로 주입해준다. */
    @Autowired
    public void setBookDAO(BookDAO bookDAO) {
        this.bookDAO = bookDAO;
    }

    public List<BookDTO> selectAllBooks(){

        return bookDAO.selectBookList();
    }

    public BookDTO searchBookBySequence(int sequence) {

        return bookDAO.selectOneBook(sequence);
    }
}

setter 메소드에도 @Autowired 어노테이션을 작성할 수 있다. 그러면 스프링 컨테이너는 BookService 빈 객체 생성 시 BookDAO 타입의 빈 객체를 찾아 의존성을 주입해준다. 

 

스프링 컨테이너를 생성하여 @Repository@Service 등의 어노테이션이 작성 된 클래스가 빈 스캐닝을 통해 잘 등록 되었는지, 또한 객체의 의존 관계에 따라 @Autowired어노테이션을 통해 의존성 주입이 되었는지를 테스트한다.

/* AnnotationConfigApplicationContext 생성자에 basePackages 문자열을 전달하며 ApplicationContext 생성한다. */
ApplicationContext context = new AnnotationConfigApplicationContext("project");

BookService bookService = context.getBean("bookServiceSetter", BookService.class);

/* 전체 도서 목록 조회 후 출력 확인 */
bookService.selectAllBooks().forEach(System.out::println);

/* 도서번호로 검색 후 출력 확인*/
System.out.println(bookService.searchBookBySequence(1));
System.out.println(bookService.searchBookBySequence(2));

/*
BookDTO(sequence=1, isbn=123456, title=자바의 정석, author=남궁성, publisher=도우출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=2, isbn=654321, title=칭찬은 고래도 춤추게 한다, author=고래, publisher=고래출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=1, isbn=123456, title=자바의 정석, author=남궁성, publisher=도우출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
BookDTO(sequence=2, isbn=654321, title=칭찬은 고래도 춤추게 한다, author=고래, publisher=고래출판, 
	createdDate=Sun May 28 20:19:12 KST 2023)
*/
728x90
반응형

+ Recent posts