스프링 삼각형
-스프링 삼각형은 스프링의 3대 프로그래밍 모델(IoC/DI, AOP, PSA)을 의미합니다.
-스프링을 이해하기 위해서는 POJO(Plain Old Java Object)를 기반으로 스프링 삼각형에 대한 이해가 필수입니다.
1. IoC(Inversion of Control)/DI(Dependency Injection)-제어의 역전/의존성 주입
2. AOP(Aspect-Oriented Programming)-관점 지향 프로그래밍
3. PSA(Portable Service Abstraction)-일관성 있는 서비스 추상화
IoC/DI-제어의 역전/의존성 주입
프로그래밍에서의 의존성이란?
new Car();
Car 객체 생성자에서 new Tire();
의존성은 new이다.
new를 실행하는 Car와 Tire 사이에서 Car가 Tire에 의존한다.
위의 예시에서 알 수 있듯이 '전체가 부분에 의존한다'고 표현할 수 있습니다.
-의존하는 객체(전체)와 의존되는 객체(부분) 사이에 집합 관계(Aggregation)와 구성 관계(Composition)로 구분할 수 있습니다.
- 집합 관계: 부분이 전체와 다른 생명 주기를 가질 수 있습니다. ex) 집 vs 냉장고
- 구성 관계: 부분은 전체와 같은 생명 주기를 가집니다. ex) 사람 vs 심장
-전체가 부분에 의존합니다.
-프로그래밍에서 의존 관계는 new로 표현됩니다.
1. 의존 관계를 직접 해결
//Tire.java
interface Tire {
String getBrand();
}
//KoreaTire.java
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
//AmericaTire.java
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
Tire 인터페이스를 구현한 KoreaTire와 America 클래스
//Car.java
public class Car {
Tire tire;
public Car() {
tire=new KoreaTire();
//tire=new AmericaTire();
}
public String getTireBrand() {
return "장착된 타이어: "+tire.getBrand();
}
}
Tire를 생산(new)하고 사용할 Car 클래스
new KoreaTire(): 자동차가 타이어를 생산(new)하는 부분, 즉 의존 관계가 일어나고 있는 부분입니다.
//Driver.java
public class Driver {
public static void main(String[] args) {
Car car=new Car();
System.out.println(car.getTireBrand());
}
}
출력 결과
->장착된 타이어: 코리아 타이어
//테스트 코드
package expert001_01;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CarTest {
@Test
public void 자동차_장착_타이어브랜드_테스트() {
Car car=new Car();
assertEquals("장착된 타이어: 미국 타이어", car.getTireBrand());
}
}
2. 스프링 없이 의존성 주입하기1-생성자를 통한 의존성 주입
Tire tire=new KoreaTire(); //운전자가 타이어를 생산합니다.
Car car=new Car(tire); //운전자가 자동차를 생산하면서 타이어를 장착합니다.
-주입: '외부에서'라는 뜻을 내포하고 있습니다. 결국 자동차 내부에서 타이어를 생산하는 것이 아니라 외부에서 생산된 타이어를 자동차에 장착하는 작업입니다.
-외부에서 생산된 tire객체를 Car의 생성자의 인자로 주입(장착)하는 형태로 구현해보겠습니다.
이전 클래스 다이어그램과 다르게 Car의 생성자에 인자가 생겼습니다.
//Tire.java
public interface Tire {
String getBrand();
}
//KoreaTire.java
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
//AmericaTire.java
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
Tire 인터페이스를 구현한 KoreaTire와 America 클래스
//Car.java
public class Car {
Tire tire;
public Car(Tire tire) {
this.tire=tire;
}
public String getTireBrand() {
return "장착된 타이어: "+tire.getBrand();
}
}
Car의 생성자 부분에 new가 사라지고, 생성자에 인자가 추가되었습니다.
//Driver.java
public class Driver {
public static void main(String[] args) {
Tire tire=new KoreaTire();
//Tire tire=new AmericaTire();
Car car=new Car(tire);
System.out.println(car.getTireBrand());
}
}
new를 통해 타이어를 생산하는 부분이 Car 클래스에서 Driver 클래스로 이동했습니다. 그리고 생산된 tire 객체 참조 변수를 Car 생성자의 인자로 전달합니다.
이전 코드와의 차이점
- 기존 코드에서는 Car가 구체적으로 KoreaTire를 생산할지 AmericaTire를 생산할지를 결정했습니다. 이러한 코드는 유연성이 떨어집니다.
- 변경된 코드는 자동차가 생산될 때 어떤 타이어를 생산해서 장착할까를 자동차가 스스로 고민하지 않고, 운전자가 차량을 생산할 때 운전자가 어떤 타이어를 장착할까 고민하게 하는 것입니다. 자동차는 어떤 타이어를 장착할까를 더이상 고민하지 않아도 됩니다.
의존성 주입을 적용할 경우의 특징
- 기존 방식에서 Car는 KoreaTire, AmericaTire에 대해 정확히 알고 있어야만 그에 해당하는 객체를 생성할 수 있었습니다. 그러나 의존성 주입을 적용하면 Car는 그저 Tire 인터페이스를 구현한 어떤 객체가 들어오기만 하면 정상적으로 작동하게 됩니다.
- 의존성 주입을 하면 확장성도 좋아집니다. 나중에 JapanTire, ChinaTire, EnglandTire 등 어떤 새로운 타이어가 생겨도 각 타이어 브랜드들이 Tire 인터페이스를 구현한다면 Car 클래스 코드를 변경할 필요 없이 사용할 수 있습니다.
- 코드 재컴파일과 재배포에 대한 부담도 덜 수 있습니다.
- 현실 세계의 표준 규격 준수=프로그래밍 세계의 인터페이스 구현
- 전략 패턴을 응용하고 있습니다.
*전략 패턴*
전략: Tire를 구현한 KoreaTire, AmericaTire
컨텍스트: Car의 getTireBrand() 메소드
클라이언트: Driver의 main() 메소드
//테스트 코드
package expert001_02;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CarTest {
@Test
public void 자동차_코리아타이어_장착_타이어브랜드_테스트() {
Tire tire1=new KoreaTire();
Car car1=new Car(tire1);
assertEquals("장착된 타이어: 코리아 타이어", car1.getTireBrand());
}
@Test
public void 자동차_미국타이어_장착_타이어브랜드_테스트() {
Tire tire2=new AmericaTire();
Car car2=new Car(tire2);
assertEquals("장착된 타이어: 미국 타이어", car2.getTireBrand());
}
}
3. 스프링 없이 의존성 주입하기2-속성을 통한 의존성 주입
Tire tire=new KoreaTire(); //운전자가 타이어를 생산합니다.
Car car=new Car(); //운전자가 자동차를 생산합니다.
car.setTire(tire); //운전자가 자동차에 타이어를 장착합니다.
-생성자를 통해 의존성을 주입하면, 자동차를 생산(구입)할 때 한번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다는 문제가 생깁니다. 속성을 통해 의존성을 주입하면, 운전자가 원할 때 Car의 Tire를 교체할 수 있습니다.
Car 클래스에서 생성자가 사라졌습니다. 자바 컴파일러가 기본 생성자를 제공해 줄 것입니다. 또한 tire 속성에 get/set 메소드가 추가되었습니다.
Tire, KoreaTire, AmericaTire 클래스는 이전과 동일합니다.
//Car.java
public class Car {
Tire tire;
public Tire getTire() {
return tire;
}
public void setTire(Tire tire) {
this.tire=tire;
}
public String getTireBrand() {
return "장착된 타이어: "+tire.getBrand();
}
}
Car 클래스에는 생성자가 없어지고, tire 속성에 대한 접근자 및 설정자 메소드가 생겼습니다.
//Driver.java
public class Driver {
public static void main(String[] args) {
Tire tire=new KoreaTire();
Car car=new Car();
car.setTire(tire);
System.out.println(car.getTireBrand());
}
}
//테스트 코드
package expert001_03;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CarTest {
@Test
public void 자동차_코리아타이어_장착_타이어브랜드_테스트() {
Tire tire1=new KoreaTire();
Car car1=new Car();
car1.setTire(tire1);
assertEquals("장착된 타이어: 코리아 타이어", car1.getTireBrand());
}
@Test
public void 자동차_미국타이어_장착_타이어브랜드_테스트() {
Tire tire2=new AmericaTire();
Car car2=new Car();
car2.setTire(tire2);
assertEquals("장착된 타이어: 미국 타이어", car2.getTireBrand());
}
}
4. 스프링을 통한 의존성 주입-XML 파일 사용
ApplicationContext context=new ClassPathXmlApplicationContext("expert002.xml", Driver.class);
Tire tire=(Tire)context.getBean("tire"); //운전자가 종합 쇼핑몰에서 타이어를 구매합니다.
Car car=(Car)context.getBean("car"); //운전자가 종합 쇼핑몰에서 자동차를 구매합니다.
car.setTire(tire); //운전자가 자동차에 타이어를 장착합니다.
'종합 쇼핑몰'의 역할을 하는 것이 스프링 프레임워크입니다.
기존 시스템에서 운전자가 타이어와 자동차를 직접 생산했다면, 스프링 도입 후에는 종합 쇼핑몰을 통해 타이어와 자동차를 구매하는 형태로 바뀌었습니다.
Tire, KoreaTire, AmericaTire, Car 클래스는 이전과 동일합니다.
//Driver.java
import org.springframework.context.ApplicationContext;
import org.springframework.content.support.ClassPathXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ApplicationContext context=new ClassPathXmlApplicationContext("expert002/expert002.xml");
Car car=context.getBean("car", Car.class);
Tire tire=context.getBean("tire", Tire.class);
cat.setTire(tire);
System.out.println(car.getTireBrand());
}
}
1. 종합 쇼핑몰에 대한 정보가 필요하므로, xml 파일 경로(입점된 상품의 정보)를 전달받은 context 객체를 만들어줍니다.
2. car를 구매하는 코드와 tire를 구매하는 코드를 작성합니다.
//XML 파일
.....
<bean id="tire" class="expert002.KoreaTire"></bean>
<bean id="americatire" class="expert002.AmericaTire"></bean>
<bean id="car" class="expert002.Car"></bean>
...
XML 파일 안에 쇼핑몰에서 구매 가능한 상품 목록이 등록돼 있어야 합니다. 상품을 등록할 때는 bean 태그를 이용해 등록합니다. 이때 각 상품을 구분하기 위한 id 속성과 그 상품을 어떤 클래스를 통해 생산(인스턴스화)해야 할지 나타내는 class 속성을 지정하면 됩니다.
스프링을 도입해서 얻게 되는 이득은 자동차의 타이어 브랜드를 변경할 때 그 무엇도 재컴파일/재배포하지 않아도 XML 파일만 수정하면 프로그램의 실행 결과를 바꿀 수 있다는 것입니다.
//테스트 코드
package expert002_01;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CarTest {
@Test
public void 자동차_코리아타이어_장착_타이어브랜드_테스트() {
Tire tire1=new KoreaTire();
Car car1=new Car();
car1.setTire(tire1);
assertEquals("장착된 타이어: 코리아 타이어", car1.getTireBrand());
}
@Test
public void 자동차_미국타이어_장착_타이어브랜드_테스트() {
Tire tire2=new AmericaTire();
Car car2=new Car();
car2.setTire(tire2);
assertEquals("장착된 타이어: 미국 타이어", car2.getTireBrand());
}
}
5. 스프링을 통한 의존성 주입-스프링 설정 파일(XML)에서 속성 주입
//의사 코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청합니다.
종합 쇼핑몰은 자동차를 생산합니다.
종합 쇼핑몰은 타이어를 생산합니다.
종합 쇼핑몰은 자동차에 타이어를 장착합니다.
종합 쇼핑몰은 운전자에게 자동차를 전달합니다.
//자바로 표현
ApplicationContext context=new ClassPathXmlApplicationContext("expert003/expert003.xml");
Car car=context.getBean("car", Car.class);
//XML로 표현
<bean id="koreaTire" class="expert003.KoreaTire"></bean>
<bean id="americaTire" class="expert003.AmericaTire"></bean>
<bean id="car" class="expert003.Car">
<property name="tire" ref="koreaTire"></property>
</bean>
Driver 클래스에서 car.setTire(tire)라고 하던 부분을 XML 파일의 property 태크를 이용하여 대체합니다.
클래스 다이어그램은 이전과 동일합니다.
Tire, KoreaTire, AmericaTire, Car 클래스는 이전과 동일합니다.
//Driver.java
public class Driver {
public static void main(String[] args) {
ApplicationContext context=new ClassPathXmlApplicationContext("expert003/expert003.xml");
Car car=context.getBean("car", Car.class);
System.out.println(car.getTireBrand());
}
}
기존 코드에서
Tire tire=context.getBean("tire", Tire.class);
car.setTire(tire);
두 줄이 삭제되었습니다.
//expert003.xml
.....
<bean id="koreaTire" class="expert003.KoreaTire"></bean>
<bean id="americaTire" class="expert003.AmericaTire"></bean>
<bean id="car" class="expert003.Car">
<property name="tire" ref="koreaTire"></property>
</bean>
...
Driver 클래스에서 삭제된 두 줄이 xml 파일에서 구현된 것을 볼 수 있습니다.
1. Car를 구매하는 부분으로, 자바 코드 파일에 그대로 남겨 둬야 합니다.
2. 타이어를 구매하는 부분입니다. XML 설정만으로 완성되므로 자바 코드에서 사라집니다.
3. 코리아 타이어를 자동차의 타이어 속성에 결합하는 부분입니다. XML 설정만으로 완성되므로 자바 코드에서 사라집니다.
4. car의 tire 속성을 설정하는 부분으로, XML 설정만으로 완성되므로 자바 코드에서 사라집니다.
//테스트 코드
package expert003;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.jupiter.api.Assertions.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("expert003.xml")
public class CarTest {
@Autowired
Car car;
@Test
public void 자동차_코리아타이어_장착_타이어브랜드_테스트() {
assertEquals("장착된 타이어: 코리아 타이어", car.getTireBrand());
}
}
6. 스프링을 통한 의존성 주입-@Autowired를 통한 속성 주입
@Autowired: 스프링 설정 파일을 보고 자동으로 속성의 설정자 메소드에 해당하는 역할을 해주겠다는 의미입니다.
//expert004.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmls:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<context:annotation-config />
<bean id="tire" class="expert004.KoreaTire"></bean>
<bean id="americaTire" class="expert004.AmericaTire"></bean>
<bean id="car" class="expert004.Car"></bean>
</beans>
-기존의 XML 설정 파일에서 property 태그가 사라진 것을 볼 수 있습니다. @Autowired를 통해 car의 property를 자동으로 엮어줄 수 있으므로(자동 의존성 주입) 생략이 가능해진 것입니다.
-@Autowired는 type 기준으로 매칭하기 때문에, 같은 타입을 구현한 클래스가 여러 개 있다면 그때 bean 태그의 id로 구분해서 매칭하게 되고, 같은 타입을 구현한 클래스가 하나뿐이라면 id가 존재하지 않더라도 매칭이 가능합니다.
Car.java | @Autowired Tire tire; |
expert.xml | <bean id="usaTire" class="expert004.AmericaTire"></bean> |
정상적으로 구동됩니다.
Car.java | @Autowired Tire tire; |
expert.xml | <bean class="expert004.KoreaTire"></bean> <bean class="expert004.AmericaTire"></bean> |
둘 다 똑같은 인터페이스 타입(Tire.java)을 구현하고 있는데 id로도 구분할 수 없으므로 실행 시 에러가 발생합니다.
Car.java | @Autowired Tire tire; |
expert.xml | <bean id="tire" class="expert004.KoreaTire"></bean> <bean class="expert004.AmericaTire"></bean> |
KoreaTire로 잘 작동합니다.
Car.java | @Autowired Tire tire; |
expert.xml | <bean id="wheel" class="expert004.KoreaTire"></bean> |
스프링 @Autowired는 id 매칭보다 type 매칭이 우선이기 때문에 정상적으로 구동됩니다.
Door.java | package expert004; public class Door { } |
Car.java | @Autowired Tire tire; |
expert.xml | <bean class="expert004.KoreaTire"></bean> <bean id="tire" class="expert004.Door"></bean> |
스프링 @Autowired는 id와 type 중 type 구현에 우선순위가 있기 때문에 KoreaTire로 잘 작동합니다.
Tire, KoreaTire, AmericaTire 클래스는 기존과 동일합니다.
//Car.java
public class Car {
@Autowired
Tire tire;
public String getTireBrand() {
return "장착된 타이어: "+tire.getBrand();
}
}
-Car 클래스가 @Autowired를 사용하도록 바뀐 것을 확인할 수 있습니다.
-@Autowired 어노테이션을 이용하면 설정자 메소드를 이용하지 않고도 종합쇼핑몰인 스프링 프레임워크가 설정 파일을 통해 설정자 메소드 대신 속성을 주입해줍니다.
-Driver 클래스도 변경된 부분이 없지만, 필요하다면 Car 클래스도 Driver 클래스의 속성으로 뽑아낸 후 @Autowired를 이용하도록 바꿀 수 있습니다.
7. 스프링을 통한 의존성 주입-@Resource를 통한 속성 주입
@Autowired와 @Resource 비교
@Autowired | @Resource | |
출처 | 스프링 프레임워크 | 표준 자바 |
소속 패키지 | org.springframework.beans.factory.annotation.Autowired | javax.annotation.Resource |
빈 검색 방식 | byType 먼저, 못 찾으면 byName | byName 먼저, 못 찾으면 byType |
특이사항 | @Qualifier("") 협업 | name 어트리뷰트 |
byName 강제하기 | @Autowired @Qualifier("tire1") |
@Resource(name="tire1") |
type과 id 가운데 매칭 우선순위 | type | id |
공통점 | 두 객체 사이에 의존성을 해결해줍니다. |
@Resource는 자바 표준 어노테이션이고 @Autowired는 스프링의 어노테이션입니다. 스프링 프레임워크를 사용하지 않는다면 @Autowired는 사용할 수 없고 오직 @Resource만을 사용해야 합니다.
8. 스프링을 통한 의존성 주입-@Autowired vs. @Resource vs. <property> 태그
마무리
-변수에 값을 할당하는 모든 곳에 의존 관계가 생깁니다. 즉, 대입 연산자(=)에 의해 변수에 값이 할당되는 순간에 의존이 생깁니다.
-변수가 지역 변수이건, 속성이건, 할당되는 값이 리터럴이건, 객체이건 의존은 발생합니다.
-의존 대상을 구현하고 배치할 때 SOLID와 응집도는 높이고 결합도는 낮추라는 기본 원칙에 충실해야 합니다.
'Spring > additional' 카테고리의 다른 글
[Spring] 스프링 삼각형2_AOP, PSA (0) | 2021.02.28 |
---|---|
[Spring] 스프링 AOP 5대 용어 (0) | 2021.02.28 |
[Spring] 스프링을 통한 의존성 주입 (0) | 2021.02.28 |
[디자인 패턴] Decorator Pattern(데코레이터 패턴) (0) | 2021.02.21 |
[디자인 패턴] Proxy Pattern(프록시 패턴) (0) | 2021.02.21 |