스프링을 이해하는 데는 POJO(Plain Old Java Object) 를 기반으로 스프링 삼각형이라는 애칭을 가진 IoC/DI, AOP, PSA라고 하는 스프링의 3대 프로그래밍 모델에 대한 이해가 필수다.
의존성이란?
실제 배포용 코드를 포함한 폴더의 소스에는 한글로 된 메서드명을 권장하지 않지만 배포하지 않는 테스트 코드를 포함한 폴더에서는 영어보다는 한글로 정확히 무슨 테스트를 하고 있는지 적어주는 것을 권장
package expert001_01;
interface Tire {
String getBrand();
}
package expert001_01;
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
package expert001_01;
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
package expert001_01;
public class Car {
Tire tire;
public Car() {
tire = new KoreaTire();
// tire = new AmericaTire();
}
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
package expert001_01;
public class Driver {
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.getTireBrand());
}
}
자동차는 타이어에 의존한다. 운전자는 자동차를 사용한다. 운전자가 자동차에 의존한다고 봐도 된다. 자동차의 생성자 코드에서 tire 속성에 새로운 타이어를 생성해서 참조할 수 있게 해주었다.
👨🏻💻 의사코드
운전자가 타이어를 생산한다.
운전자가 자동차를 생산하면서 타이어를 장착한다.
package expert001_02;
interface Tire {
String getBrand();
}
package expert001_02;
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
package expert001_02;
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
package expert001_02;
public class Car {
Tire tire;
public Car(Tire tire) {
this.tire = tire;
}
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
package expert001_02;
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());
}
}
현실 세계에 비유해서 차량을 생산할 때 어떤 타이어를 장착할까에 대한 고민을 차량이 아닌 운전자가 하게 함.
의존성 주입을 적용할 경우 Car는 그저 Tire 인터페이스를 구현한 어떤 객체가 들어오기만 하면 정상적으로 작동하게 된다. 의존성 주입을 하면 확장성도 좋아지는데, 나중에 새로운 타이어 브랜드가 생겨도 각 타이어 브랜드들이 Tire 인터페이스를 구현한다면 Car.java 코드를 변경할 필요 없이 사용할 수 있다.
→ 코드 재컴파일과 재배포에 대한 부담을 덜 수 있다. (인터페이스를 구현했기에 얻은 이점)
→자동차를 생산할 때 한번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다는 문제 발생
👨🏻💻 의사코드
운전자가 타이어를 생산한다.
운전자가 자동차를 생산한다.
운전자가 자동차에 타이어를 장착한다.
package expert001_03;
interface Tire {
String getBrand();
}
package expert001_03;
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
package expert001_03;
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
package expert001_03;
public class Car {
Tire tire;
public Tire getTire() {
return tire;
}
public void setTire(Tire tire) {
this.tire = tire;
}
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
package expert001_03;
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());
}
}
👨🏻💻 의사코드
운전자가 종합 쇼핑몰에서 타이어를 구매한다.
운전자가 종합 쇼핑몰에서 자동차를 구매한다.
운전자가 자동차에 타이어를 장착한다.
종합 쇼핑몰의 역할을 하는 것이 바로 스프링 프레임워크다!
//Driver.java
package expert002;
//종합 쇼핑몰(스프링 프레임워크)에 대한 정보를 가지고 있는 패키지
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("expert002.xml");
Car car = context.getBean("car", Car.class);
Tire tire = context.getBean("tire", Tire.class);
car.setTire(tire);
System.out.println(car.getTireBrand());
}
}
//expert002.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="tire" class="expert002.KoreaTire"></bean>
<bean id="americaTire" class="expert002.AmericaTire"></bean>
<bean id="car" class="expert002.Car"></bean>
</beans>
기존 생산 과정이 종합 쇼핑몰(스프링 프레임워크)을 통해 구매하는 형태로 바뀌었다.
종합 쇼핑몰이라고 하지만 상품이 입점돼 있어야만 판매할 수 있다. 입점된 상품에 대한 정보는 XML 파일에 등록돼 있다.
상품을 등록할 때는 bean 태그를 이용해 등록한다. 이때 각 상품을 구분하기 위한 id 속성과 그 상품을 어떤 클래스를 통해 생산(인스턴스화)해야 할지 나타내는 class 속성을 함께 지정하면 된다.
getBean을 통해 구매한다고 생각하면 된다.
XML 파일을 변경하고 프로그램을 실행하면 바로 변경사항이 적용되기 때문에 자동차의 타이어 브랜드를 변경할 때 자바 코드를 변경/재컴파일/재배포할 필요가 없다.
👨🏻💻 의사코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합쇼핑몰은 운전자에게 자동차를 전달한다.
XML 파일에 새롭게 property라고 하는 부분 추가. XML 파일의 property 태그를 이용해 속성 메서드를 대체한다.
//Driver.java
package expert003;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
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());
}
}
//expert003.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<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>
</beans>
👨🏻💻 의사코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합쇼핑몰은 운전자에게 자동차를 전달한다.
@Autowired 애노테이션을 이용하면 설정자 메서드를 이용하지 않고도 종합쇼핑몰인 스프링 프레임워크가 설정 파일을 통해 설정자 메서드 대신 속성을 주입해 준다. (스프링 설정 파일을 보고 자동으로 속성의 설정자 메서드에 해당하는 역할을 해주겠다는 의미)
//Car.java
package expert004;
import org.springframework.beans.factory.annotation.Autowired;
public class Car {
@Autowired
Tire tire;
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
//Driver.java
package expert004;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("expert004/expert004.xml");
Car car = context.getBean("car", Car.class);
System.out.println(car.getTireBrand());
}
}
//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"
xmlns: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 https://www.springframework.org/schema/context/spring-context.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>
bean의 id 속성이 없는데 어떻게 매칭시킨 것일까?
@Autowired의 마법
@Autowired는 type 기준으로 매칭을 하기 때문에 같은 타입을 구현한 클래스가 여러 개 있다면 그때 bean 태그의 id로 구분해서 매칭한다. (id 매칭 보다 type 매칭이 우선이다)
그래도 항상 bean 태그의 id 속성을 작성하는 습관을 들이자.
👨🏻💻 의사코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 전달한다.
package expert005;
import javax.annotation.Resource;
public class Car {
@Resource
Tire tire;
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
책의 사례 연구를 살펴보면 @Autowired와 @Resource를 바꿔서 사용하는 데 크게 차이가 없다는 사실을 알 수 있다. 그럼 둘 중에 어느 것을 써야 할까?
필자의 경우 스프링이 제공하는 @Autowired와 자바가 제공하는 @Resource 중에서 손 들라고 하면 @Resource를 지지하는 입장이다. 자동으로 주입된다는 의미에서는 @Autowired가 명확해 보이지만 실제 Car 입장에서 보면 @Resource라는 표현이 더 어울린다. 그리고 나중에 스프링이 아닌 다른 프레임워크로 교체되는 경우를 대비하면 자바 표준인 @Resource를 쓰는 것이 유리하다.
그럼 여기서 또 하나의 고민이 생긴다. @Resource는 사실 태그의 자식 태그인 태그로 해결될 수 있다. 그럼 @Resource vs. 중에서는 누구의 손을 들어줘야 할까?
필자의 경우 의 손을 들어주겠다. XML 파일만 봐도 DI관계를 손쉽게 확인할 수 있기 때문이다. 그렇지만 개발 생산성은 @Resource가 더 나을 것이다. 반면 는 유지보수성이 좋다. 프로젝트의 규모가 커지면 XML 파일의 규모가 커지기 마련인데 XML 파일도 용도별로 분리할 수 있다. 그래서 더더욱 에 한 표를 던진다.
위의 질문에 대한 대답은 필자의 생각일 뿐 내가 경험하고 내 생각을 주입시켜보자!
필자의 생각..
다수의 클래스를 만들고 의존 관계를 지정하다 보면 유지보수에 무관한 관계도 있고, 유지보수와 밀접하거나 자주 변경되는 관계도 있다. 전자라면 @Resource를 후자라면 태그를 사용하는 것이 유리하다.
사례 연구 6. XML 설정 - 두 개의 빈이 tire 인터페이스를 구현하고 속성과 일치하는 id가 없지만 @Resource 어노테이션의 name 속성이 id와 일치하는 경우
@Resource(name="{id}")
@Autowired
@Qualifier("{id}")
설정 파일을 쓰는 가장 중요한 이유가 재컴파일/재배포 없이 프로그램의 실행 결과를 변경할 수 있기 때문이다.
DI는 외부에 있는 의존 대상을 주입하는 것을 말한다. 의존 대상을 구현하고 배치할 때 SOLID와 응집도는 높이고 결합도는 낮추라는 기본 원칙에 충실해야 한다. 그래야 프로젝트의 구현과 유지보수가 수월해진다.