디자인 패턴은 읽어도 아직은 이해가 안 되는 부분이 많고 마침 오늘 스프링 삼각형을 배웠기에 건너 뛰고 7장을 먼저 읽기로 결정했다😂
1. IoC/DI - 제어의 역전/의존성 주입
1-1. 프로그래밍에서 의존성이란?
- 의사 코드
운전자가 자동차를 생산한다.
자동차는 내부적으로 타이어를 생산한다. - 자바로 표현
new Car();
Car 객체 생성자에서 new Tire(); - 의존성을 단순하게 정의하면 다음과 같다.
의존성은 new다.
new를 실행하는 Car와 Tire 사이에서 Car가 Tire에 의존한다.
먼저 스프링을 적용하지 않은 기존 방식으로 자바 코드를 작성한 예제다.
public interface Tire {
String getBrand();
}
public class KoreaTire implements Tire{
public String getBrand() {
return "코리아 타이어";
}
}
public class AmericaTire implements Tire{
public String getBrand() {
return "미국 타이어";
}
}
public class Car {
Tire tire;
public Car() {
tire = new KoreaTire();
}
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
public class Driver {
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.getTireBrand());
}
}
이제부터 의존성을 주입해보자.
1-2. 스프링 없이 의존성 주입하기1 - 생성자를 통한 의존성 주입
- 의사 코드
운전자가 타이어를 생산한다.
운전자가 자동차를 생산하면서 타이어를 장착한다. - 자바로 표현
Tire tire = new KoreaTire();
Car car = new Car(Tire); - 주입이란?
주입이란 말은 외부에서라는 뜻을 내포하고 있는 단어다.
결국 자동차 내부에서 타이어를 생산하는 것이 아니라 외부에서 생산된 타이어를 자동차에 장착하는 작업이 주입이다.
public interface Tire {
String getBrand();
}
public class KoreaTire implements Tire{
public String getBrand() {
return "코리아 타이어";
}
}
public class AmericaTire implements Tire{
public String getBrand() {
return "미국 타이어";
}
}
public class Car {
Tire tire;
**public Car(Tire tire) {
this.tire = tire;
}**
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
public class Driver {
public static void main(String[] args) {
**Tire tire = new KoreaTire();
Car car = new Car(tire);**
System.out.println(car.getTireBrand());
}
}
기존 코드에서는 Car가 구체적으로 어떤 Tire를 생산할지 결정했지만 지금의 코드에서는 Tire를 Car에 주입하고 있다.
비유하자면 자동차가 생산될 때 어떤 타이어를 생산해서 장착할까를 자동차가 스스로 고민하지 않고, 운전자가 차량을 생산할 때 고민하게 하는 것이다.
이런 방식의 코드 작성은 어떤 이점이 있을까?
기존 방식에서라면 Car는 KoreaTire, AmericaTire에 대해 정확히 알고 있어야만 그에 해당하는 객체를 생성할 수 있었다.
의존성 주입을 적용할 경우 Car는 그저 Tire 인터페이스를 구현한 어떤 객체가 들어오기만 하면 정상적으로 작동하게 된다.
이렇게 의존성 주입을 하면 확장성도 좋아지는데 나중에 어떤 새로운 타이어 브랜드가 생겨도 각 타이어 브랜드들이 Tire 인터페이스를 구현한다면 Car.java 코드를 변경할 필요 없이 사용할 수 있기 때문이다.
만약 이를 제품화한다면 Car.java, Tire.java를 하나의 모듈로, Driver.java와 KoreaTire.java, AmericaTire.java를 각각 하나의 모듈로 만들면 나중에 새로운 ChinaTire.java가 생겨도 Driver.java, ChinaTire.java만 컴파일해서 배포하면 된다. 이것은 인터페이스를 구현했기에 얻는 이점이라고 볼 수 있다.
현실 세계의 표준 규격 준수 = 프로그래밍 세계의 인터페이스 구현
1-3. 스프링 없이 의존성 주입하기2 - 속성을 통한 의존성 주입
- 의사 코드
운전자가 타이어를 생산한다.
운전자가 자동차를 생산한다.
운전자가 자동차에 타이어를 장착한다. - 자바로 표현 - 속성 접근자 메서드 사용
Tire tire = new KoreaTire();
Car car = new Car();
car.setTire(tire);
앞서 생성자를 통해 의존성을 주입했는데 현실 세계의 예를 들어 생각해보면 자동차를 생산할 때 한 번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다는 문제가 생긴다.
더 현실적인 방법은 운전자가 원할 때 Car의 Tire를 교체하는 것이다.
이를 구현하는 방법이 속성을 통한 의존성 주입이다.
💡 최근에는 속성을 통한 의존성 주입보다는 생성자를 통한 의존성 주입을 선호하는 사람들이 많다. 한 번 주입된 의존성을 계속 사용하는 경우가 더 일반적이기 때문이다.
public interface Tire {
String getBrand();
}
public class KoreaTire implements Tire{
public String getBrand() {
return "코리아 타이어";
}
}
public class AmericaTire implements Tire{
public String getBrand() {
return "미국 타이어";
}
}
public class Car {
Tire tire;
**public Tire getTire() {
return tire;
}
public void setTire(Tire tire) {
this.tire = tire;
}**
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
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());
}
}
Tire와 Car의 객체를 각각 생성하고 Car 클래스의 setter를 이용해서 타이어를 주입해주고 있다.
이제 스프링을 이용해 의존성을 주입해보자.
1-4. 스프링을 통한 의존성 주입 - XML 파일 사용
- 의사 코드
운전자가 종합 쇼핑몰에서 타이어를 구매한다.
|운전자가 종합 쇼핑몰에서 자동차를 구매한다.
운전자가 자동차에 타이어를 장착한다. - 자바로 표현 - 속성 메서드 사용
ApplicationContext context = new ClassPathXmlApplicationContext(”practice.xml”, Driver.class);
Tire tire = (Tire)context.getBean(”tire”);
Car car = (Car)context.getBean(”car”);
car.setTire(tire);
운전자가 타이어, 자동차를 직접 생산하던 시스템에서 종합 쇼핑몰을 통해 구매하는 형태로 바뀌었다.
종합 쇼핑몰 스프링 프레임워크가 들어오면서 달라지는 코드를 살펴보자.
public interface Tire {
String getBrand();
}
public class KoreaTire implements Tire{
public String getBrand() {
return "코리아 타이어";
}
}
public class AmericaTire implements Tire{
public String getBrand() {
return "미국 타이어";
}
}
public class Car {
Tire tire;
public Tire getTire() {
return tire;
}
public void setTire(Tire tire) {
this.tire = tire;
}
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
public class Driver {
public static void main(String[] args) {
**ApplicationContext context = new ClassPathXmlApplicationContext("practice.xml");**
Car car = context.getBean("car", Car.class);
Tire tire = context.getBean("tire", Tire.class);
car.setTire(tire);
System.out.println(car.getTireBrand());
}
}
<?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="practice.KoreaTire"></bean>
<bean id="americaTire" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
</beans>
스프링을 이용해서 달라진 점은 Driver 클래스의 수정과 xml 파일이 추가되었다는 것이다.
우선 Driver 클래스부터 살펴보자.
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("practice.xml");
이 부분이 종합 쇼핑몰에 대한 정보라고 할 수 있다.
Car car = context.getBean("car", Car.class); Tire tire = context.getBean("tire", Tire.class);
이렇게 두 개의 문장이 상품에 해당하는 Car와 Tire를 구매하는 코드다.
마지막으로 종합 쇼핑몰에 입점된 상품에 대한 정보가 필요한데 그게 XML 파일이다.
상품을 등록할 때는 bean 태그를 이용해 등록하고 각 상품을 구분하기 위한 id 속성과 어떤 클래스를 통해 인스턴스화할지 나타내는 class 속성을 함께 지정하면 된다.
이렇게 스프링을 도입해서 얻는 이득은 무엇일까?
가장 큰 이득을 꼽으라면 자동차의 타이어를 변경할 때 그 무엇도 재컴파일/재배포하지 않아도 XML 파일만 수정하면 프로그램의 실행 결과를 바꿀 수 있다는 것이다.
1-5. 스프링을 통한 의존성 주입 - 스프링 설정 파일(XML)에서 속성 주입
- 의사 코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 전달한다. - 자바로 표현 - 속성 메서드 사용
ApplicationContext context = new ClassPathXmlApplicationContext(”practice.xml”)
Car car = context.getBean(”car”, Car.class); - XML로 표현
<bean id=”KoreaTire" class="practice.KoreaTire"></bean>
<bean id="americaTire" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
<property name=”tire” ref=”koreaTire”></property>
</bean>
import org.apache.catalina.core.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("practice.xml");
Car car = context.getBean("car", Car.class);
System.out.println(car.getTireBrand());
}
}
<?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="practice.KoreaTire"></bean>
<bean id="americaTire" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car">
<property name="tire" ref="koreaTire"></property>
</bean>
</beans>
달라진 두 개의 파일만 살펴보자.
기존에 타이어를 생산하고 차에 결합하는 부분이 사라졌다. XML 설정으로 이 부분을 완성할 수 있기 때문이다.
이제는 타이어의 변경 사항이 생기면 XML 파일의 property ref 값만 변경해주면 된다.
1-6. 스프링을 통한 의존성 주입 - @Autowired를 통한 속성 주입
- 의사 코드
운전자가 종합 쇼핑몰에서 자동차를 구매 요청한다.
종합 쇼핑몰은 자동차를 생산한다.
종합 쇼핑몰은 타이어를 생산한다.
종합 쇼핑몰은 자동차에 타이어를 장착한다.
종합 쇼핑몰은 운전자에게 자동차를 전달한다.
의사 코드는 이전과 동일하다.
여기서 잠깐 프로그래머의 3대 스킬을 알아보자.
- C&P: Copy & Paste / 복사 & 붙여넣기
- D&C: Divide & Conquer / 분할 & 정복
- C&I: Creative Idleness / 창조적 게으름
스프링 프레임워크 개발팀은 어떤 창조적 게으름을 발휘했을까?
Car 클래스에 tire라는 속성을 만들고 setter를 만든다고 하면 대부분 아래와 같이 코드를 작성한다.
Tire tire;
public void setTire(Tire tire) {
this.tire = tire;
}
여기서 창조적 게으름을 발휘해보자. 스프링의 속성 주입 방법 가운데 @Autowired를 이용하는 것이다.
import org.springframework.beans.factory.annotation.Autowired;
@Autowired
Tire tire;
import문 하나와 @Autowired 애노테이션을 이용하면 스프링 프레임워크가 설정 파일을 통해 setter 대신 속성을 주입해준다.
아래는 변경된 설정 파일이다.
<?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
http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<context:annotation-config />
<bean id="tire" class="practice.KoreaTire"></bean>
<bean id="americaTire" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
</beans>
@Autowired의 의미를 이해해보자. 이것은 스프링 설정 파일을 보고 자동으로 속성의 setter 메서드 역할을 해주겠다는 의미다. 그렇기 때문에 XML 파일의 property 태그도 없어진 것이다.
import org.springframework.beans.factory.annotation.Autowired;
public class Car {
@Autowired
Tire tire;
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
@Autowired가 tire에 해당하는 bean을 찾아 주입해준다.
이제는 타이어를 다른 타이어로 변경하고 싶다면 XML 파일에서 bean id만 수정해주면 된다.
//변경 전
<bean id="**tire**" class="practice.KoreaTire"></bean>
<bean id="americaTire" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
//변경 후
<bean id="koreaTire" class="practice.KoreaTire"></bean>
<bean id="tire" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
만약 koreaTire 부분을 완전히 삭제하고 AmericaTire의 id 속성을 삭제한다면 실행이 될까?
놀랍게도 된다.
<bean class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
기존에는 지정된 tire 속성과 bean의 id 속성이 일치하는 것을 찾아 매칭시킨 것 같았는데 어떻게 되는 것일까?
인터페이스의 구현 여부가 그 답이다.
같은 타입을 구현한 클래스가 여러 개 있다면 그 때 bean 태그의 id로 구분해서 매칭하는 것이다.
//1.
<bean id="wheel" class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
//2.
<bean id="tire" class="practice.KoreaTire"></bean>
<bean class="practice.AmericaTire"></bean>
<bean id="car" class="practice.Car"></bean>
또한 위와 같은 경우에서도 작동은 잘 된다.
따라서 id와 type 중 type 구현에 우선순위가 있음을 알 수 있다.
1-7. 스프링을 통한 의존성 주입 - @Resource를 통한 속성 주입
@Autowired와 동일한 기능을 하는 어노테이션이다.
차이점은 @Autowired는 스프링의 어노테이션이고 @Resource는 자바 표준 어노테이션이다.
@Autowired는 type이 id보다 우선순위지만 @Resource는 반대로 id가 type보다 우선순위다.
1-8. 스프링을 통한 의존성 주입 - @Autowired vs. @Resource vs. <property> 태그
@Autowired와 @Resource를 바꿔서 사용하는 데 크게 차이가 없다.
하지만 나중에 스프링이 아닌 다른 프레임워크로 교체되는 경우를 대비하면 자바 표준인 @Resource를 쓰는 것이 유리하다.
여기서 또 하나의 고민이 생긴다. @Resource는 <property> 태그로 해결될 수 있다. 어떤 게 더 나을까?
@Resource는 개발 생산성이 더 낫고 <property>는 유지보수성이 좋다.
프로젝트의 규모가 커지면 XML 파일의 규모가 커지기 마련인데 XML 파일도 용도별로 분리할 수 있기에 더더욱 <property>에 한 표를 던진다.
다만 유지보수에 무관한 관계라면 @Resource를, 유지보수와 밀접하거나 자주 변경되는 관계라면 <property> 태그를 사용하는 것이 유리하니 프로젝트 규모와 팀의 성향에 따라 유연하게 사용하면 된다.
DI를 마무리하기 전에 마지막으로 언급할 사항이 하나 있다.
의존 관계가 new 라고 단순화했던 부분이다.
사실 변수에 값을 할당하는 모든 곳에 의존 관계가 생긴다. 즉, 대입 연산자(=)에 의해 변수에 값이 할당되는 순간에 의존이 생긴다.
DI는 외부에 있는 의존 대상을 주입하는 것을 말한다. 의존 대상을 구현하고 배치할 때 SOLID와 응집도는 높이고 결합도는 낮추라는 기본 원칙에 충실해야 한다. 그래야 프로젝트의 구현과 유지보수가 수월해진다.
'Reading > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
7. 스프링 삼각형과 설정 정보_AOP, PSA (0) | 2023.04.04 |
---|---|
5. 객체 지향 설계 5원칙 - SOLID (0) | 2023.03.29 |
4. 자바가 확장한 객체 지향 (0) | 2023.03.23 |
3. 자바와 객체 지향 (0) | 2023.03.23 |
2. 자바와 절차적/구조적 프로그래밍 (0) | 2023.03.21 |