[Spring] Spring에서의 IoC와 DI

p1atin0uss·2022년 6월 8일

Spring

목록 보기
3/4
post-thumbnail

📘 사전지식

Bean

  • Spring Container(IoC Container)가 관리하는 자바객체를 Bean이라고 부른다.
  • Spring에서 직접 객체를 싱글톤 형태로 만들어서 Container에서 관리한다.
  • new 연산자로 생성된 객체를 뜻하는 것이 아닌, ApplicationContext.getBean()으로 얻어질 수 있는 객체를 말한다.
  • 즉, Spring에서의 Bean은 ApplicationContext가 만들어서 그 안에 담겨진 객체를 의미한다.



📌 DI (Dependency Injection)

개념

  • 의존 관계 주입이라고도 하며, 어떤 객체가 사용하는 의존 객체를 직접 생성하여 사용하는 것이 아닌, 주입받아 사용하는 것을 뜻한다.

예시

간단한 Base32인코더와 Base64인코더를 제작한다고 가정해보고, 차근차근 과정을 이해해보자.

// Base32Encoder 클래스
public class Base32Encoder {
	public String encode(String message) {
    	...
        
        return Base32.getEncoder().encodeToString(message.getBytes());
    }
}

// Base64Encoder 클래스
public class Base64Encoder {
	public String encode(String message) {
    	...
        
        return Base64.getEncoder().encodeToString(message.getBytes());
    }
}

// Main 클래스
public class Main {
	public static void main(String[] args) {
    	String url = "en.dict.naver.com/#/search?query=spring";
        
        // Base32Encode
        Base32Encoder base32Encoder = new Base32Encoder();
        String base32Result = base32Encoder.encode(url);
        
        // Base64Encode
        Base64Encoder base64Encoder = new Base64Encoder();
        String base64Result = base64Encoder.encode(url);
    }
}

추상화를 적용하지 않고는 위와 같이 작성할 수 있을 것이다.
여기서 인터페이스를 추가하고 DI를 적용하기 전까지 구현하면 다음과 같이 구현할 수 있을 것이다.


// 인터페이스 추가
public interface IEncoder {
	String encode(String message);
}

// IEncoder 상속받음
public class Base32Encoder implements IEncoder {
	public String encode(String message) {
    	...
        
        return Base32.getEncoder().encodeToString(message.getBytes());
    }
}

// IEncoder 상속받음
public class Base64Encoder implements IEncoder {
	public String encode(String message) {
    	...
        
        return Base64.getEncoder().encodeToString(message.getBytes());
    }
}

// Encoder 클래스 수정
public class Encoder {
	private IEncoder iEncoder;
    
    // 문제점 : 내부 코드를 수정해야 하는 과정이 필요하다
    public Encoder() {
    	// this.iEncoder = new Base64Encoder();
        this.iEncoder = new Base32Encoder();
    }
    
    public String encode(String message) {
    	return iEncoder.encode(message);
    }
}

// main 메소드 수정
public class Main {
	public static void main(String[] args) {
    	String url = "en.dict.naver.com/#/search?query=spring";
        
        // 다음과 같이 작성할 수 있음
        Encoder encoder = new Encoder();
        String result = encoder.encode(url);
    }
}

위의 코드를 살펴보면, DI가 적용되어 있지 않아 내가 어떠한 인코더를 사용하느냐에 따라 Encoder의 iEncoder에 Base32Encoder를 생성할 것인지, Base64Encoder를 생성할 것인지 내부 Encoder 클래스를 수정 할 필요성이 생기게 된다.

그렇지만 여기서 DI 개념을 사용해 외부에서 내가 사용하는 객체를 주입시켜보자.


public interface IEncoder {
	String encode(String message);
}

public class Base32Encoder implements IEncoder {
	public String encode(String message) {
    	...
        
        return Base32.getEncoder().encodeToString(message.getBytes());
    }
}

public class Base64Encoder implements IEncoder {
	public String encode(String message) {
    	...
        
        return Base64.getEncoder().encodeToString(message.getBytes());
    }
}

// Encoder 클래스 수정
public class Encoder {
	private IEncoder iEncoder;
    
    public Encoder(IEncoder iEncoder) {
    	this.iEncoder = iEncoder;
    }
    
    public String encode(String message) {
    	return iEncoder.encode(message);
    }
}

// main 메소드 수정
public class Main {
	public static void main(String[] args) {
    	String url = "en.dict.naver.com/#/search?query=spring";
        
        // 의존 객체 주입 (DI)
        Encoder encoder = new Encoder(new Base64Encoder());
        String result = encoder.encode(url);
    }
}

위와 같이 외부에서, 사용하는 객체를 주입받는 형태로 작성하게 되면,
Encoder 클래스는 수정할 필요없이 main메서드 내에서만 필요로 하는 인코더만 주입시켜주면 된다.

추가적으로 다른 인코더를 적용할 때도, IEncoder를 상속받고 새로운 Encoder 클래스를 작성시켜, 다른 인코더 클래스를 수정할 필요 없이 main메소드 내에서 객체를 주입시켜주기만 하면 된다.


Spring에서의 적용

사전지식

외부에서 의존 관계를 결정하는 것을 뜻하는 DI(Dependency Injection)는,
스프링에서는 외부의 대상이 Spring Container(IoC Container)가 되고, Spring Container는 Bean(Spring Container가 관리하는 오브젝트)을 대신 주입해준다.

3가지 의존성 주입 방식

1) 필드 주입

@Controller
public class BookController {
    
    @Autowired
    private BookService bookService;
}
  • 가장 일반적이고 쉬운 방법이다.
  • 의존 관계가 눈에 잘 보이지 않아 추상적이고, 의존성 관계가 복잡해질 수 있다.
  • SRP(단일 책임 원칙)에 반하는 패턴이다.
  • 의존성 주입 대상 필드가 final로 선언 불가하다.
  • 현재 deprecated한 방식이다.

2) setter 주입

@Controller
public class BookController {

    private BookService bookService;

    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }
}
  • 생성 당시 주입 필요없이, 필요시 setter 메서드를 호출하여 주입할 수 있다.
  • 의존성 주입 대상 필드를 final로 선언할 수 없다.
  • A와 B 서로가 참조하고 있을 경우, 서로를 호출하는 경우가 생기므로, 순환참조를 방지하지 못한다.

3) 생성자 주입

@Controller
public class BookController {

    private final BookService bookService;
    
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
}
  • 의존성 대상 주입 필드를 final로 선언할 수 있다.
  • 테스트 코드 작성시 생성자를 통해 의존성 주입이 용이하다.



📌 IoC (Inversion of Control)

개념

  • Spring에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리하는 것이 아닌, Spring Container에게 모두 맡긴다.
  • 즉, 개발자에서 Framework로 제어의 객체 관리 권한이 넘어갔으므로 "제어의 역전"을 뜻한다.

Spring에서의 적용

@Component 애너테이션을 클래스에 붙임으로써, Spring Container가 알아서 Spring Bean 객체로 등록하고 생성한다.
이렇게 생성된 Bean은 위에서 소개한 3가지 의존성 주입 방식(생성자 주입, setter 주입, 필드 주입)을 통해 필요한 곳에 객체를 주입하게 된다.

profile
지식의 깊이는 곧 이해의 넓이 📚

0개의 댓글