Spring Framwork

  • “테스트의 용이성”, “느슨한 결합”에 중점을 두고 개발

  • 가운데 POJO를 중심에 두고, 3가지 특징을 중심으로 개발하도록 설계된 프레임워크이다.

IoC / DI

IoC (Inversion of Control)

  • 제어의 역전이라는 뜻이다. 스프링에서는 일반적인 Java 객체를 new로 생성하여 개발자가 관리하는 것이 아 니라, Spring Container에게 맡긴다. 객체 제어의 권한을 프레임워크가 갖고 있음을 뜻한다.

  • 프레임워크의 Spring Container안에서 싱글톤 객체로 관리된다.

DI (Dependency Injection)

  • 의존성주입이란 뜻으로, Spring Container로부터 사용할 객체를 주입받아 사용한다.
  • 코드 테스트에 용이하다.
  • DI를 통하여, Mock을통해 안정적 테스트가 가능하다.
  • 코드를 확장하거나 변경 할 때 영향을 최소화 한다.
  • 순환참조를 막을 수 있다.

DI 코드 예제

  • IoC/DI 적용 전
public static void main(String[] args) {
    String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

    // Base 64 encoding
    Base64Encoder encoder = new Base64Encoder();
    String result = encoder.encode(url);
    System.out.println(result);
}
public class Base64Encoder {
    public String encode(String message){
        return Base64.getEncoder().encodeToString(message.getBytes());
    }
}

위는 그저 Encoder라는 객체를 통해 url을 인코딩했다.
그런데 여기서 여러개의 Encoder가 늘어난다면 어떻게 될까? URLEncoder를 추가하면 아래와 같다.

public static void main(String[] args) {
    String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

    // Base 64 encoding
    Encoder encoder = new Encoder();
    String result = encoder.encode(url);
    System.out.println(result);

    // URL encoding
    UrlEncoder urlEncoder = new UrlEncoder();
    String urlResult = urlEncoder.encode(url);
    System.out.println(urlResult);
}
public class UrlEncoder {
    public String encode(String message){
        try {
            return URLEncoder.encode(message, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

이제 여기에서 추상화를 들어간다.

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

encode메소드를 정의하는 인터페이스를 하나 만들고, 기존 Base64Encoder, UrlEncoder가 이를 상속받 도록 한다. 그러면 메인함수에서 다음과 같이 쓸수있다.

public static void main(String[] args) {
    String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";
    // 추상화 후
    IEncoder encoder = new Base64Encoder();
    String iResult = encoder.encode(url);
    System.out.println(iResult);

}

이제 DI가 들어가는 부분이 나온다. 매번 IEncoder를 new해서 써야하는 불편함을 제거하기 위해서, 다음과 같이 한다.

IEncoder를 멤버변수로 받고, encode를 정의한 Encoder클래스를 하나 정의한다.

public class Encoder {

    private IEncoder iEncoder;

    public Encoder(){
        this.iEncoder = new Base64Encoder();
    }

    public String encode(String message){
        return iEncoder.encode(message);
    }
}

이렇게 하면 main이 다음과 같이 바뀐다.

public static void main(String[] args) {
    String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

    Encoder encoder = new Encoder();
    String result = encoder.encode(url);
    System.out.println(result);
}

매우 간단해진것을 알 수 있다.
그런데 지금은 Base64Encoder로 인코딩하는 encoder이기때문에 여러 인코더를 사용하려며 어떻게 하느냐? 이것이 DI의 핵심이다.

DI는 외부에서 내가 사용하는 객체를 주입받는 것이다.
방법은 간단하다. 첫째로 Encoder객체의 생성자에서 어떤 인코더를 사용할것인지를 주입받는다.

public class Encoder {

    private IEncoder iEncoder;

    public Encoder(IEncoder iEncoder){
        this.iEncoder = iEncoder;
    }

    public String encode(String message){
        return iEncoder.encode(message);
    }
}

그 후, main에서 사용할 때에는 인코더를 new해서 넘겨주면 된다.

public static void main(String[] args) {
    String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";

    // Encoder encoder = new Encoder(new Base64Encoder()); 사용하고 싶은 인코더를
    Encoder encoder = new Encoder(new UrlEncoder()); // 주입시켜 주면 된다.
    String result = encoder.encode(url);
    System.out.println(result);
}

그렇다면 IoC라 함은, 위와같은 의존 주입을 Spring Container가 해준다고 이해할 수 있다.

IoC 코드 예제

일단 Spring Application 프로젝트를 하나 만든다. Spring Initializer로 간단한게 만들 수 있다.

이전 포스트에서 만들었던 클래스 및 인터페이스를 모조리 복사해 온 후, 패키지 명을 맞춰준다.

DI예제에서는 주입하는 객체를 개발자가 직접 new키워드를 통해 만들어서 객체를 직접 주입하고 있다.

Spring Framework에서는 IoC특성상 주입되는 객체를 Spring Container가 관리해주도록 할 수 있다.

어떻게 하느냐? 클래스위에 @Component라는 어노테이션을 붙이면 스프링이 해당 클래스의 인스턴스를 객체로써 등록을 해 주게 된다. 객체는 싱글톤형태로 관리되게 된다.

그러면, 우리는 등록된 객체에 어떻게 접근하고 사용할 수 있는가?
ApplicationContextAware를 스프링으로부터 상속받아서 정의하고 사용가능하다.

@Component
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getContext(){
        return context;
    }
}

그리고, 기존 Encoder에 IEncoder를 바꾸는 set메소드를 하나 만들어주자.

public class Encoder {

    private IEncoder iEncoder;

    public Encoder(IEncoder iEncoder){
        this.iEncoder = iEncoder;
    }

    public void setIEncoder(IEncoder iEncoder){
        this.iEncoder = iEncoder;
    }

    public String encode(String message){
        return iEncoder.encode(message);
    }
}

이러면 다음과 같이 메인에서 사용 가능하다.

@SpringBootApplication
public class IocApplication {

    public static void main(String[] args) {
        SpringApplication.run(IocApplication.class, args);

        ApplicationContext context = ApplicationContextProvider.getContext();

        Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
        UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);

        Encoder encoder = new Encoder(base64Encoder);

        String url = "www.naver.com/boooks/it?page=10&size=20&name=spring-boot";
        String result = encoder.encode(url);
        System.out.println(result);

        encoder.setIEncoder(urlEncoder);
        result = encoder.encode(url);
        System.out.println(result);
    }
}

아직까지는 Encoder도 직접 관리하고 있는데, 이또한 스프링에게 제어하도록 할 것이다. 일단 Encoder에 @Component어노테이션을 붙여보자.

@Component
public class Encoder {

    private IEncoder iEncoder;

    public Encoder(@Qualifier("base64Encoder") IEncoder iEncoder){
        this.iEncoder = iEncoder;
    }

    public void setIEncoder(IEncoder iEncoder){
        this.iEncoder = iEncoder;
    }

    public String encode(String message){
        return iEncoder.encode(message);
    }
}

그러면, 생성자쪽에서 에러가 나는것을 알 수 있는데 이는 Spring이 등록된 Bean중에 어떤 녀석을 붙여야 할 지 알수 없기 때문에 생기는 에러이다. @Qualifier어노테이션을 통해 명시해주도록 하자.

객체의 이름은 기본적으로 클래스명 맨앞글자만 소문자로 바꿔주면 되는데, 혹시 따로 빈객체의 이름을 명명 하고 싶으면 @Component("명명할이름")으로 어노테이션을 붙여주면 된다.

이제 Encoder도 빈객체로 등록했기 때문에 메인을 정리해보자.

@SpringBootApplication
public class IocApplication {

    public static void main(String[] args) {
        SpringApplication.run(IocApplication.class, args);

        ApplicationContext context = ApplicationContextProvider.getContext();

//        Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
//        UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);

        Encoder encoder = context.getBean(Encoder.class);

        String url = "www.naver.com/boooks/it?page=10&size=20&name=spring-boot";
        String result = encoder.encode(url);
        System.out.println(result);

        encoder.setIEncoder(context.getBean(UrlEncoder.class));
        result = encoder.encode(url);
        System.out.println(result);
    }
}

한클래스내에서 여러개의 빈을 등록하고 싶을때에는 다음과 같이 한다.

@Configuration
class AppConfig{

    //Base64Encoder, UrlEncoder가 빈으로 등록되어있기때문에 아래 주입받는 곳에 자동으로 매칭됨.

    @Bean("base64Encode")
    public Encoder encoder(Base64Encoder base64Encoder){
        return new Encoder(base64Encoder);
    }

    @Bean("urlEncode")
    public Encoder encoder(UrlEncoder urlEncoder){
        return new Encoder(urlEncoder);
    }
}

사용할때는 다음과 같이 한다.

@SpringBootApplication
public class IocApplication {

    public static void main(String[] args) {
        SpringApplication.run(IocApplication.class, args);

        ApplicationContext context = ApplicationContextProvider.getContext();

        Encoder encoder = context.getBean("base64Encode", Encoder.class);
        String url = "www.naver.com/boooks/it?page=10&size=20&name=spring-boot";
        String result = encoder.encode(url);
        System.out.println(result);
    }
}

이제 개발자가 관리하는 객체는 없고 전부 Spring이 객체를 관리하고 있다. IoC특징이 적용된 것이다. 등록된 빈의 생명주기를 스프링이 관리를 해준다.