스프링을 이해하는 데는 POJO(Plain Old Java Object)를 기반으로 스프링 삼각형이라는 애칭을 가진 IoC/DI, AOP, PSA
라고 하는 스프링의 3대 프로그래밍에 대한 이해가 필수다.
IoC/DI : 의존성(new)을 주입을 통해 외부에서 처리하도록 한다.
AOP : 관심사를 분리한다. (핵심관심사 / 횡단관심사)
PSA : 확장성이 좋은 코드, 잘 만든 인터페이스 -> 스프링은 인터페이스 기반이고 어노테이션을 통해 PSA를 구현한다.
스프링의 IoC(Inversion of Control / 제어의 역전)라고도 하는 DI(Dependency Indection / 의존성 주입)을 알아보기 전에 프로그래밍에서 의존성이란 무엇인지 알아보자. 자바에서의 의존성은 무엇인지도 알아보자.
의사 코드
운전자가 자동차를 생산한다.
자동차는 내부적으로 타이어를 생산한다.
자바로 표현
new Car();
Car 객체 생성자에서 new Tire();
그리고 의존성을 단순하게 정의하면 아래와 같다.
의존성은 new다.
new를 실행하는 Car와 Tire 사이에서 Car가 Tire에 의존한다.
결론적으로 전체가 부분에 의존한다고 표현할 수 있다.
더 깊이 들어가면 의존하는 객체(전체)와 의존되는 객체(부분) 사이에 집합 관계(Aggregation)와 구성 관계(Composition)로 구분할 수도 있다고 말할 수 있다.
집합 관계 : 부분이 전체와 다른 생명 주기를 가질 수 있다.
예 : 집 vs 냉장고
구성 관계 : 부분은 전체와 같은 생명 주기를 가진다.
예 : 사람 vs 심장
먼저 스프링을 적용하지 않은 기존 방식으로 자바 코드를 작성해 보자. 그리고 점진적으로 스프링 Annotation 방식으로 변경해 보자.
인터페이스인 Tire.java
package expert001_01;
public interface Tire {
String getBrand();
}
Tire 인터페이스를 구현한 KoreTire.java
package expert001_01;
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
Tire 인터페이스를 구현한 AmericaTire.java
package expert001_01;
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
Tire를 생산(new)하고 사용할 Car.class
package expert001_01;
public class Car {
Tire tire;
public Car() {
tire = new KoreaTire();
// tire = new AmericaTire();
}
public String getTireBrand() {
return "장착된 타이어 : " + tire.getBrand();
}
}
Driver.java
package expert001_01;
public class Driver {
public static void main(String[] args) {
Car car = new Car();
System.out.println(car.getTireBrand());
}
}
여기서 주의 깊게 볼 부분은 Car.java의 new KoreaTire() 부분이다. 바로 자동차가 타이어를 생산(new)하는 부분, 즉 의존 관계가 일어나고 있는 부분이다.
지금까지 설명하고 작업한 내용을 한번 정리해보자.
자동차는 타이어에 의존한다.
운전자는 자동차를 사용한다.
운전자가 자동차에 의존한다고 봐도 된다.
자동차의 생성자 코드에서 tire 속성에 새로운 타이어를 생성해서 참조할 수 있게 해주었다.
여기에서는 의존이 일어나고 있는 두 객체 사이에 직접 의존성을 해결하는 코드를 작성해봤다.
의사 코드
운전자가 타이어를 생산한다.
운전자가 자동차를 생산하면서 타이어를 장착한다.
자바로 표현 - 생성자 인자 이용
Tire tire = new KoreaTire();
Car car = new Car(tire);
주입이란?
주입이란 말은 외부에서라는 뜻을 내포하고 있는 단어다.
결국 자동차 내부에서 타이어를 생성하는 것이 아니라 외부에서 생산된 타이어를 자동차에 장착하는 작업이 주입이다.
앞의 의존성을 직접 해결하는 예제에서는 Car 객체가 Tire를 직접 생성하는, Tire에 대한 의존성을 자체적으로 해결하는 방식이었다.
이번에는 외부에서 생성된 tire 객체를 Car 생성자의 인자로 주입(장착)하는 형태로 구현해보자.
위 클래스 다이어그램과 달리 Car 생성자에 인자가 생겼다. 나머지는 달라진 부분이 없다.
자바 코드는 다음과 같다.
Tire.java - 이전과 동일
package expert001_02;
public interface Tire {
String getBrand();
}
KoreaTire.java - 이전과 동일
package expert001_02;
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
AmericaTire.java - 이전과 동일
package expert001_02;
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
Car.java - 생성자 부분이 이전과 다름
package expert001_02;
public class Car {
Tire tire;
public Car(Tire tire) {
this.tire = tire;
}
public String getTireBrand() {
return "장착된 타이어 : " + tire.getBrand();
}
}
Car.java의 생성자 부분이 달라졌다. new가 사라지고 생성자에 인자가 추가된 것에 주목하자.
Driver.java - tire 생성 부분에 주목
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());
}
}
new를 통해 타이어를 생산하는 부분이 Car.java에서 Driver.java로 이동했다. 그리고 생상된 tire 객체 참조 변수를 Car 생성자의 인자로 전달했다.
이런 구현 방식에는 어떤 장점이 있을까? 기존 코드에서는 Car는 KoreaTire, AmericaTire에 정확히 알고 있어야만 그에 해당하는 객체를 생성할 수 있었다. 의존성 주입을 적용할 경우 Car는 그저 Tire 인터페이스를 구현한 어떤 객체가 들어오기만 하면 정상적으로 작동하게 된다. 의존성 주입을 하면 확장성도 좋아지는데, 나중에 ChinaTire, JapanTire, EnglandTire 등등 어떤 새로운 타이어 브랜드가 생겨도 각 타이어 브랜드들이 Tire 인터페이스를 구현한다면 Car.java 코드를 변경할 필요 없이 사용할 수 있기 때문이다(또한 다시 컴파일할 필요도 없다). 만약 이를 제품화한다면 Car.java, Tire.java를 하나의 모듈로, Driver.java와 KoreaTire.java, America.Tire를 각각 하나의 모듈로 만들면 나중에 새로운 China.Tire가 생겨도 Driver.java, China.Tire만 컴파일해서 배포하면 된다. 다른 코드는 재컴파일 및 배포할 필요가 없다. 이것은 인터페이스를 구현(준수)했기에 얻는 이점이라고 볼 수 있다.
앞에서 생성자를 통해 의존성을 주입해봤다. 이어서 속성을 통해 의존성을 주입해보자.
여기서는 디자인 패턴의 꽃이라고 하는 전략 패턴을 응용하고 있다. 전략 패턴의 3요소인 클라이언트, 전략, 컨텍스트에 해당하는 요소를 찾아보자.
전략 패턴의 3요소
- 전략 메서드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트
- 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제3자)
전략 : Tire를 구현한 KoreaTire, AmericaTire
컨텍스트 : Car의 getTireBrand() 메서드
클라이언트 : Driver의 main() 메서드
의사 코드
운전자가 타이어를 생산한다.
운전자가 자동차를 생산한다.
운전자가 자동차에 타이어를 장착한다.
자바로 표현
Tire tire = new KoreaTire();
Car car = new Car;
car.setTire(tire);
이번에는 속성을 통해 의존성을 주입해보자. 생성자를 통해 의존성을 주입하는 것을 다시 현실 세계의 예로 들어 생각해 보면 자동차를 생산(구입)할 때 한번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다면 문제가 생간다. 더 현실적인 방법은 운전자가 원할 때 Car의 Tire를 교체하는 것이다. 자바에서 이를 구현하려면 생성자가 아닌 속성을 통한 의존성 주입이 필요하다.
프로그래밍 세계에서는 생성자를 통해 의존성을 주입하는 방법과 속성을 통해 의존성을 주입하는 방법 중 어느 쪽이 더 좋은가에 대한 의견이 분분했었는데, 최근에는 속성을 통한 의존성 주입보다는 생성자를 통한 의존성 주입을 선호하는 사람들이 많다. 실세계에서라면 십년 가까이 타게 되는 차를 사고 타이어를 교체하는 일이 빈번할 수 있지만 프로그램에서는 한번 주입된 의존성을 계속 사용하는 경우가 더 일반적이기 때문이다.
Car 클래스에서 생성자가 사라졌다. 자바 컴파일러가 기본 생성자를 제공해 줄 것이다. 그리고 tire 속성의 get/set 속성 메서드가 보이는 것이 기존 코드와 달라진 부분이다.
예제 코드는 다음과 같다.
Tire.java - 이전과 동일
package expert001_03;
public interface Tire {
String getBrand();
}
KoreaTire.java - 이전과 동일
package expert001_03;
public class KoreaTire implements Tire {
public String getBrand() {
return "코리아 타이어";
}
}
AmericaTire.java - 이전과 동일
package expert001_03;
public class AmericaTire implements Tire {
public String getBrand() {
return "미국 타이어";
}
}
Car.java
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();
}
}
Car.java는 생성자가 없어졌고, tire 속성에 대한 접근자 및 설정자 메서드가 생겼다.
Driver.java
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());
}
}
참고