본문으로 바로가기

Spring Triangle

1. 제어 역전 (IoC, Inversion of Control)

1.1 제어 역전(IoC) 이란?

객체 지향 프로그래밍인 자바에서의 클래스는 일반적인 경우 클래스 내에서 필요로 하는 의존성을 스스로 만들어 사용했다.

class OwnerController {
	private OwnerRepository repo = new OwnerRepository();
}

위의 예시에서 스스로 등록했다는 것은 OwnerController 클래스의 멤버 변수(객체)인 repo 변수에 할당될 객체를  new OwnerRepository(); 라는 구문을 통해 직접 생성했다는 것을 뜻한다.

class OwnerController {
	//오너 레포지스트리를 사용하긴 하지만 직접 만들지는 않는다.
	private OwnerRepository repo;
	//생성자를 통해서 받아온다 -> 즉, 오너 컨트롤러가 하는 일이 아님.
	public OwnerController(OwnerRepository repo){
		this.repo = repo;
	}
}

class OwnerControllerTest {
	@Test
	public void create() {
		OwnerRepository repo = new OwnerRepository();
		OwnerController controller = new OwnerController(repo);
	}
}

위 코드의 주석에서 설명했듯, 초기 코드에서는 OwnerController가 자신이 필요한 의존성인 OwnerRepository를 직접 생성했지만 위의 코드는 생성자로부터 OwnerRepository를 외부로부터 전달받아 멤버 변수(객체)로 주입되는 것을 알 수 있다.

 

그런데 이것을 제어 역전이라고 하는 이유와 장점에는 어떤 것이 있는것인가?

우리가 new OwnerRepository(); 를 통해 인스턴스를 직접 생성했다고 가정해보자.

그런데 만약 인스턴스의 생성비용이 다소 크다거나 (일일이 이렇게 생성 및 주입을 해야하나?), Repository 의 변경이 있을 경우 우리는 해당 멤버객체에 주입되는 구현체를 변경해주어야 한다.

 

하지만 관리 주체가 외부에 있는 경우, OwnerController 를 주입받는 외부에서 생성자로 전달되는 객체를 통해 실제 주입(사용)될 객체를 결정할 수 있고, 해당 클래스에 주입되는 구현체의 제어권은 외부로부터 결정된다고 할 수 있다.

이것을 Spring 에서는 제어의 역전이라고 표현한다.

 

1.2 Bean 등록 방법

빈(Bean)은 스프링의 IoC 컨테이너가 관리하는 객체를 뜻한다. IoC 컨테이너에 Bean 으로 등록된 객체는 BeanFactory 를 상속(확장)받은 ApplicationContext 객체를 통해 다양한 패턴으로 어플리케이션 전역에 주입 또는 사용될 수 있다.

 

Bean 을 등록하는 방법에는 2가지가 있다.

첫번째는 어노테이션을 활용해 컴포넌트 스캔을 이용하는 것.

두번째는 XML 또는 자바의 설정파일을 활용해 직접 등록하는 방법이 있다.

아래는 컴포넌트 스캔을 통해 등록가능하며, 일반적으로 가장 많이 사용되는 어노테이션이다.

  • Component Scanning
    • @Component
      • @Controller
      • @Service
      • @Repository
      • @Configuration

SpringBootApplication 에서 Component Scan 을 통해 해당 어노테이션을 확인하면 자동으로 빈으로 등록해주게 된다.

OwnerRepository의 경우 spring-data-jpa 가 제공해주는 구현체를 빈으로 등록해준다.

특정 인터페이스를 상속받게 되면 해당 인터페이스의 구현체를 찾아 주입해준다.

/*SampleController.java 파일*/
package org.springframework.samples.petclinic.sample;

import org.springframework.stereotype.Controller;

@Controller
public class SampleController {

}
/*SampleControllerTest.java 파일*/
package org.springframework.samples.petclinic.sample;

import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class sampleControllerTest {
    @Autowired
    ApplicationContext applicationContext;

    @Test
    public void testDI(){
        SampleController bean = applicationContext.getBean(SampleController.class);
        AssertionsForClassTypes.assertThat(bean).isNotNull();
    }
}

위와 같이 @Controller 어노테이션을 통해 Bean 으로 등록이 되며, 사용하는 클래스에서는 @Autowired (또는 생성자) 를 통해 간편하게 주입할 수가 있다.

 

두번째 방법으로 XML 혹은 Java 설정 파일을 통해 등록하는 방법이 있다.

(XML을 사용하는 방법은 Spring Boot 가 나오기 전에 사용하는 방법으로 현재는 많이 사용하지 않으므로 설명은 생략한다.)

/*SampleConfig.java 파일*/
package org.springframework.samples.petclinic.sample;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration //설정 관련 애노테이션
public class SampleConfig {
    @Bean // Bean으로 아래 객체 직접등록
    public SampleController sampleController(){
        return new SampleController();
    }
}

/*SampleController.java 파일*/
package org.springframework.samples.petclinic.sample;

import org.springframework.stereotype.Controller;

//@Controller 생략을 해도 빈으로 등록이 된다. (위에서 직접 등록해놨기 때문에)
public class SampleController {

}
  1. @Configuration
    @Configuration 또한 , @Component 안에 속한 어노테이션으로, Component Scan 에 포함된다.
  2. @Bean
    컴포넌트 스캔을 하면서, @Bean 어노테이션이 포함된 메소드를 IoC 컨테이너로 등록한다.

마찬가지로 IoC 컨테이너에 등록된 Bean 을 @Autowired 를 통해 사용하고자 하는 클래스로 주입할 수 있다.

1.3 의존성 주입

@Autowired
public OwnerController(OwnerRepository clinicService) {
    this.owners = clinicService;
}

생성자에도 원래 @Autowired라는 어노테이션을 통해 의존성 주입이 가능했으나, 스프링 4.3 부터는 어떠한 클래스의 생성자가 하나뿐이고, 생성자로 주입받는 매개변수가 Bean으로 등록되어 있다면, Bean으로 자동 주입하도록 하는 기능이 있으므로 @Autowired 어노테이션은 생략이 가능하다.

 

따라서 필드로 직접 주입을 하고자 하는 경우 아래와 같이 사용이 가능하다.

@Autowired
public OwnerController(OwnerRepository clinicService) {
    this.owners = clinicService;
}

아래와 같이 Setter 메소드를 이용한 주입 또한 가능하다.

private OwnerRepository owners;

@Autowired
public void setOwnsers(OwnerRepository owners) {
	this.owner = owners;
}

하지만 위 2가지 방법에는 필히 사용해야하는 인스턴스가 없더라도 클래스 생성이 가능하다는 단점이 있고,

스프링에서는 아래와 같이 생성자를 통해 레퍼런스가 존재하는 경우에만 인스턴스를 생성할 수 있는 방법을 권장한다.

private final OwnerRepository owners;


public OwnerController(OwnerRepository clinicService) {
    this.owners = clinicService;
}

2. 관점 지향 프로그래밍 (Aspect Oriented Programming)

스프링에서 지원하는 AOP에 대한 개념을 이해하기 위해서는 AOP 를 구성하는 요소들의 개념에 대한 설명, 그리고 AOP 구현을 위한 디자인 패턴(Proxy Pattern) 에 대한 이해가 선행되어야 합니다. 하지만 이러한 개념들을 하나씩 짚고 넘어가기엔 설명이 길어질 수 있으므로 해당 포스트 에서는 AOP 의 기본 개념과 샘플코드를 통한 설명을 통해 전반적인 이해를 돕는 것을 목적으로 합니다.

2.1 관점 지향 프로그래밍(AOP) 이란?

객체 지향 프로그래밍(OOP)은 관심사에 따라 클래스를 분리합니다. 하지만 OOP의 설계방식으로는 다 해결할 수 없는 아쉬운점이 존재하는데 위 이미지와 같이 로깅이나 보안 및 트랜잭션 등 공통된 기능들이 흩어져 존재한다는 점 입니다.

AOP는 서비스 전역에 흩어진 부분적인 관심사를 비즈니스 로직으로 부터 분리(모듈화)하여 보다 깨끗한 코드를 작성하는 것을 목적으로 합니다.

2.2 AOP 적용 예제

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;
    private final PlatformTransactionManager transactionManager;

    public void sendMoneyToAnotherUser(Long senderId, Long receiverId, Long money) {
        TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            //로깅 관련 로직 추가
            //보안 관련 로직 추가
            Account senderAccount = userDao.findAccountById(senderId);
            Account receiverAccount = userDao.findAccountById(receiverId);
            userDao.updateMoney(senderId, senderAccount.withdraw(money));
            userDao.updateMoney(receiverId, receiverAccount.add(money));
            transactionManager.commit(transaction);
        } catch (RuntimeException runtimeException) {
            transactionManager.rollback(transaction);
            throw runtimeException;
        }
    }

    public void withdrawMoney(Long id, Long money) {
        TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            //로깅 관련 로직 추가
            //보안 관련 로직 추가
            Account account = userDao.findAccountById(senderId);
            userDao.updateMoney(senderId, account.withdraw(money));
            transactionManager.commit(transaction);
        } catch (RuntimeException runtimeException) {
            transactionManager.rollback(transaction);
            throw runtimeException;
        }
    }
}

위의 예제 코드에서 부가적인 관심사는 트랜잭션 하나 뿐이지만, 로깅이나 보안등의 관심사가 추가되면 어떻게 될까요?

sendMoneyToAnotherUser() 메소드가 더욱 비대해질 것입니다. 더 큰 문제는 이러한 부가적 관심사를 가지는 클래스가 UserService에 국한되지 않는다는 점 입니다.

만약 트랜잭션이나 로깅 및 보안 등의 부가기능 정책이 변경되거나 API가 변경된다면?

이를 사용하는 수많은 클래스가 모두 함꼐 수정되어야 합니다.

 

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserDao userDao;

    @Transactional
    public void sendMoneyToAnotherUser(Long senderId, Long receiverId, Long money) {
        Account senderAccount = userDao.findAccountById(senderId);
        Account receiverAccount = userDao.findAccountById(receiverId);
        userDao.updateMoney(senderId, senderAccount.withdraw(money));
        userDao.updateMoney(receiverId, receiverAccount.add(money));
    }
}

스프링에서 흔히 보는 @Transactional 어노테이션 또한 AOP가 적용된 대표 사례로 예외 발생 여부에 따라 트랜잭션을 커밋하거나 롤백하는 기능을 합니다.

해당 포스트에서는 생략되었으나 내부적으로는 해당 오브젝트에 대한 프록시 객체를 생성하고 Pointcut과 Advice를 통한 정보를 바탕으로 부가기능에 대한 관심사를 추가합니다.

 

AOP는 이해하기 어렵고, 제대로 사용하기 위해서는 꾸준한 학습이 필요한 부분으로 핵심이 되는 기능을 이해하거나 직접 구현하기 위해서는 AspectJ 패키지를 추가해 코드를 분석하고 직접 구현해보기를 권장합니다.

3. 서비스 추상화 (Portable Service Abstraction)

3.1 서비스 추상화(PSA) 란?

위의 AOP 섹션에서는 Spring의 AOP가 Proxy 패턴을 발전시켜 만들어졌다는 것을 설명했습니다.

여기에 우리가 간과하고 있던 사실이 있습니다. @Transactional 어노테이션을 사용하는 것 만으로 별도의 코드 추가 없이 트랜잭션 서비스를 사용할 수 있다는 사실 입니다. 그리고 내부적으로 트랜잭션 코드가 추상화 되어 숨겨져 있는 것입니다. 이렇게 추상화 계층을 사용하면 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공해주는 것이 서비스 추상화 (Service Abstraction) 입니다.

3.2 스프링 트랜잭션

Transaction 추상화 계층 

위 그림에서 알 수 있듯이 Spring 의 Transactional 은 최상위에 존재하는 PlatformTransactionManager의 하위로 실제 구현체인 각각의 TransactionManager 가 존재한다는 것을 알 수 있습니다.

public interface PlatformTransactionManager extends TransactionManager {

  TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

  void commit(TransactionStatus status) throws TransactionException;

  void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager의 구현 클래스

위 코드와 이미지에서 알 수 있듯이 PlatformTransactionManager의 하위로 다양한 TransactionManager가 구현되어 있음을 확인할 수 있습니다. 이는 Transaction을 제어함에 있어 하나의 기술에 국한되지 않고 트랜잭션 관리의 모든 기술을 아우를 수 있는 Spring의 PSA(Portable Service Abstraction)에 맞는 코드라 볼 수 있습니다.

'Framework > Spring' 카테고리의 다른 글

[Spring] Lifecycle  (0) 2022.01.15
[Spring] Security 가 적용된 Test  (0) 2022.01.14
[Spring] Open API 3 (with Swagger3)  (0) 2022.01.11
[Spring] Swagger 3  (0) 2022.01.11
[Spring] Filter & Interceptor  (0) 2021.12.22