스프링의 삼각형 이해하기!

maketheworldwise·2022년 3월 29일
0


이 글의 목적?

스프링의 핵심인 삼각형 - IoC/DI, AOP, PSA에 대해서 이해해보자.

IoC vs DI

IoC(Inversion of Control, 제어의 역전)와 DI(Dependency Injection, 의존성 주입)는 하나로 묶여 표현했지만 다른 의미를 가지고 있다.

IoC는 코드의 흐름을 제어하는 것을 개발자가 아닌 스프링이 해주는 개념이다. 하단의 코드는 개발자가 직접 코드의 흐름을 제어하는 예시다.

public class A {
	private B b;
    
    public A() {
    	b = new B();
    }
}

반대로 객체가 스프링 컨테이너에게 관리되고 있는 빈이라면 @Autowired 어노테이션을 이용하여 객체를 주입받을 수 있다. 즉, 개발자가 직접 객체를 관리하지 않고 스프링 컨테이너에서 직접 객체를 생성하여 해당 객체에 주입시켜준다는 의미다.

public class A {
	@Autowired
	private B b;
}

따라서 IoC는 개발자가 아닌 스프링이 코드의 흐름을 역으로 제어해준다고 하여 제어의 역전이라고 한다. 스프링이 제어해주기 때문에 개발자의 부담은 적어지고 SOLID 원칙을 보다 쉽게 지키면서 변경에 유연한 코드 구조로 개발을 할 수 있다는 장점을 가지고 있다.

그럼 DI는 뭘까? IoC는 스프링에서만 사용하는 개념이 아닌 폭넓게 사용되는 용어이기에, 스프링을 IoC 컨테이너라고 정의를 해도 스프링이 제공하는 기능을 명확하게 설명할 수 없다. 따라서 조금 더 핵심적인 의미를 가르키기 위해 DI라는 개념을 적용했다. 쉽게 말하자면, DI는 IoC 프로그래밍 모델을 구현하는 방식중에 하나라는 의미다.

IoC와 DI의 관계를 정리하자면 - "스프링에서는 IoC를 구체적으로 DI라는 방식을 통해서 의존성 역전 제어를 하고 있다!"

의존성 주입 방법

의존성 주입 방법은 총 3가지가 있다. 생성자와 속성을 이용한 의존성 주입은 간단하게 코드로만 살펴보고 XML을 이용한 의존성 주입 방법을 더 자세하게 정리해보자.

  • 생성자를 이용한 의존성 주입
  • 속성을 이용한 의존성 주입 (Getter/Setter)
  • XML을 이용한 의존성 주입 (생성자, 속성을 통한 주입 모두 지원)

생성자를 이용한 의존성 주입

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());
    }
}

속성을 이용한 의존성 주입

생성자를 이용한 의존성 주입은 한번 타이어를 장착하면 더 이상 타이어를 교체 장착할 방법이 없다. 더 현실적으로 운전자가 원할 때 차의 타이어를 교체하도록 하기 위해서는 속성을 이용한 의존성 주입을 사용하면 된다. 생성자를 이용한 의존성 주입 코드에서 일부만 수정해보자.

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());
    }
}

💡 속성을 통한 의존성 주입보다는 생성자를 이용한 의존성 주입이 더 인기가 많다?

위의 타이어 예제를 가지고와서 생각해보자. 실제로 우리는 몇십년 동안 차를 몰면서 타이어를 교체하는 일이 일반적인 운전자라면 거의 없다. 즉, 현실에서 빈번하게 변경이 일어나는 일이 아니라면 생성자를 이용한 의존성 주입을 많이 사용하는 추세라는 것이다!

XML을 이용한 의존성 주입

책에서는 XML을 종합 쇼핑몰의 역할로 표현했다. 위의 생성자 및 속성을 이용한 의존성 주입의 예에서는 운전자가 직접 차와 타이어를 만들었다면, XML을 이용한 의존성 주입에서는 운전자가 XML에 등록된 차와 타이어를 선택하는 개념이다. 따라서 XML을 이용하면 재컴파일 및 재배포를 하지 않아도 XML 파일만 수정하면 프로그램의 실행 결과를 변경할 수 있다.

XML 기본 주입

<!-- xml 파일 위치 : src/main/java/expert002/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="mainTire" class="expert002.KoreaTire"></bean>
    <bean id="americaTire" class="expert002.AmericaTire"></bean>

  	<bean id="car" class="expert002.Car"></bean>

</beans>
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("mainTire", Tire.class);
		car.setTire(tire);
		System.out.println(car.getTireBrand());
	}
}

XML 속성 주입

<!-- xml 파일 위치 : src/main/java/expert002/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="mainTire" class="expert002.KoreaTire"></bean>
    <bean id="americaTire" class="expert002.AmericaTire"></bean>

    <bean id="car" class="expert002.Car">
      	<property name="tire" ref="mainTire"></property>
  	</bean>

</beans>
public class Driver {
	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("expert002/expert002.xml");
        
		Car car = context.getBean("car", Car.class);
		System.out.println(car.getTireBrand());
	}
}

@Autowired 주입

@Autowired 어노테이션을 이용하면 설정자 메서드를 이용하지 않아도 스프링 프레임워크가 설정 파일을 통해 생성자 메서드 대신 속성을 주입해준다.

<!-- xml 파일 위치 : src/main/java/expert002/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
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/spring-context-3.1.xsd">
 
  	<context:annotation-config />
  
    <bean id="mainTire" class="expert002.KoreaTire"></bean>
    <bean id="americaTire" class="expert002.AmericaTire"></bean>

  	<bean id="car" class="expert002.Car"></bean>

</beans>
public class Car {
	@Autowired
	Tire mainTire;
    
    public String getTireBrand() {
    	return "장착된 타이어: " + mainTire.getBrand();
    }
}

만약 메인 타이어를 미국 타이어로 설정하길 원한다면, XML 파일에서 미국 타이어에 해당하는 빈을 찾아 id만 mainTire로 변경해주면 된다.

<!-- xml 파일 위치 : src/main/java/expert002/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
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/spring-context-3.1.xsd">
 
  	<context:annotation-config />
  
    <bean id="koreaTire" class="expert002.KoreaTire"></bean>
    <bean id="mainTire" class="expert002.AmericaTire"></bean>

  	<bean id="car" class="expert002.Car"></bean>

</beans>

다양한 경우를 살펴보자 🤓

@Autowired - XML의 id 속성이 없을 경우

하단의 코드의 결과는 @Autowired가 제대로 동작한다. 그 이유는 KoreaTire 클래스가 Tire 클래스를 상속 받았기 때문이다.

@Autowired
Tire tire
<bean class="expert002.KoreaTire"></bean>
<bean id="car" class="expert002.Car"></bean>

@Autowired - XML의 id 속성과 일치하지 않을 경우

앞에서 설명했듯이 @Autowired는 type속성을 우선시한다. 따라서 id가 달라도 정상적으로 동작한다.

@Autowired
Tire tire
<bean id="differentTireName" class="expert002.KoreaTire"></bean>
<bean id="car" class="expert002.Car"></bean>

@Autowired - XML의 id 속성이 없고 type이 동일한 bean이 2개일 때

결과는 실패다. 그 이유는 당연하다. id 속성이 없고 type이 동일한 빈이 2개일 경우에는 어떤 빈을 매칭시켜야할지에 대한 정보를 스프링에서는 알 수 없기 때문이다.

@Autowired
Tire tire
<bean class="expert002.KoreaTire"></bean>
<bean class="expert002.AmericaTire"></bean>
<bean id="car" class="expert002.Car"></bean>

@Autowired - XML의 다른 type이지만 id가 매칭되는 경우가 있을 때

이 경우에는 다른 type이지만 id가 매칭되어도 type이 매칭되는 빈과 매칭해준다.

@Autowired
Tire tire
<bean class="expert002.KoreaTire"></bean> <!-- 매칭 -->
<bean id="tire" class="expert002.Door"></bean>
<bean id="car" class="expert002.Car"></bean>

@Resource 주입

@Resource는 자바 표준 어노테이션으로 스프링 프레임워크를 사용하지 않을 경우에도 사용이 가능하다. 책의 필자는 @Autowired 보다는 @Resource 어노테이션을 이용을 하는 것이 더 적절하다고 했다. 차의 입장에서 타이어는 자원인 것처럼 더 직관적으로 표현할 수 있기 때문이다.

단, 각 어노테이션 별로 빈 속성 우선순위가 다르기에 주의해야한다.

  • @Autowired의 우선순위는 type(class) 다음 id
  • @Resource의 우선순위는 id 다음 type(class)

💡 @Resource보다 XML 속성 주입(property)을 이용하는 것이 좋다?

책에서는 @Resource이 개발 생산성에서는 더 좋지만, XML 파일만 보아도 DI 관계를 쉽게 확인할 수 있고 유지보수성이 뛰어나며 XML 파일도 용도별로 분리할 수 있어서 XML 속성 주입(property)을 이용하는 방법을 더 추천했다.

@Qualifier(""), @Resource(name="")

다음과 같이 XML 파일이 작성되어있을 때, 특정 id를 사용하되 클래스의 속성 이름과 일치하지 않도록 구성하고 싶을 경우에는 @Autowired의 경우 @Qualifier를, @Resource는 name 속성을 이용하면 된다.

<bean id="differentTireName" class="expert002.KoreaTire"></bean>
<bean id="car" class="expert002.Car"></bean>
@Autowired
@Qualifier("differentTireName")
Tire tire
@Resource(name = "differentTireName")
Tire tire

AOP

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)은 로깅, 보안, 트랜잭션 등과 같이 반복적으로 나타나는 횡단 관심사들과 핵심 관심사들을 분리하고 한 곳에 모아 관리하는 방법이다. AOP로 구현한 코드들은 추가 개발과 유지보수 관점에서 유리해지고 단일 책임 원칙을 적용할 수 있게 된다. 여기서는 사용 방법보다는 특징들만 정리해보자.

AOP에서 기억해야할 점은 크게 3가지다.

  • 프록시를 사용한다.
  • 인터페이스 기반이다.
  • 런타임 기반이다.

AOP 용어에는 Pointcut, JoinPoint, Advice, Aspect, Advisor가 있다. 특히 Aspect 용어를 해석해보면 AOP를 더 쉽게 이해할 수 있다.

  • Aspect = Advice(언제, 무엇을)들 + Pointcut(어디에)들 = 언제 무엇을 어디에

💡 Advisor?

Advisor는 스프링 AOP에서만 사용하는 용어다. Aspect 처럼 Advisor를 공식으로 표현하면 다음과 같다.

  • Advisor = 하나의 Advice + 하나의 Pointcut

하지만 이 기능은 스프링 버전이 올라가면서 더 이상 사용하지 말라고 권고한다. 그 이유는 스프링이 발전하면서 다수의 Advice와 다수의 Pointcut을 다양하게 조합하여 사용할 수 있는 방법으로 Aspect가 나왔기 때문이다.

즉, AOP는 언제 무엇을 어디에 횡단 관심사를 적용할지에 대해 정의하는 프로그래밍 기법이라고 생각하면 쉽다.

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
                           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 
  	<aop:asepctj-autoproxy />
  
    <bean id="myAspect" class="aop002.MyAspect"></bean>
  	<bean id="boy" class="aop002.Boy"></bean>
  	<bean id="girl" class="aop002.Girl"></bean>

</beans>
@Aspect
public class MyAspect {
	@Pointcut("execution(* runSomething())")
    private void iampc() {
    	// 의미 없는 코드 블록
    }

	// @Before("execution(* runSomething())")
    @Before("iampc()")
    public void before(JoinPoint joinPoint) {
    	System.out.println("AOP 적용 (before)");
    }
    
    @After("iampc()")
    public void lockDoor(JoinPoint joinPoint) {
    	System.out.println("AOP 적용 (after)");
    }
}

상단의 코드는 어노테이션 기반으로 AOP를 적용했다면, POJO와 XML 기반의 AOP는 다음과 같다.

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
                           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 
  	<aop:asepctj-autoproxy />
  
    <bean id="myAspect" class="aop002.MyAspect"></bean>
  	<bean id="boy" class="aop002.Boy"></bean>
  	<bean id="girl" class="aop002.Girl"></bean>

  	<aop:config>
      	<aop:pointcut expression="execution(* runSomething())" id="iampc" />
    	<aop:aspect ref="myAspect">
          	<!--
        	<aop:before method="before" pointcut="execution(* runSomething())" /> 
        	<aop:after method="lockDoor" pointcut="execution(* runSomething())" /> 
			-->
          	<aop:before method="before" pointcut-ref="iampc" />
          	<aop:before method="lockDoor" pointcut-ref="iampc" />
      	</aop:aspect>
  	</aop:config>
</beans>
public class MyAspect {
    public void before(JoinPoint joinPoint) {
    	System.out.println("AOP 적용 (before)");
    }
    
    public void lockDoor(JoinPoint joinPoint) {
    	System.out.println("AOP 적용 (after)");
    }
}

PSA

PSA(Portable Service Abstraction)는 일관성 있는 서비스 추상화라고 한다. 서비스 추상화는 JDBC 처럼 어댑터 패턴을 활용하여 같은 일을 하는 다수의 기술을 공통의 인터페이스로 제어할 수 있게 한 것을 서비스 추상화라고 한다. 스프링은 OXM(Object XML Mapping - 객체와 xml 매핑),ORM, 캐시, 트랜잭션, 등 다양한 기술에 대한 PSA를 제공한다.

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글