Search
📒

2-3. Spring IoC

Spring Framework는 Inversion of Control(제어 반전)이라고 하는 프로그래밍 원리가 구현되어 있습니다. 이는 간단하게 설명하면 ‘프레임워크가 사용자의 소스 코드를 사용한다’라는 의미입니다. 이 말을 살펴보기 위해 Java에서 인터페이스와 추상클래스를 사용하는 법을 간단히 살펴보도록 합시다.

Java의 Interface

자동차의 기능을 생각해봅시다. 일반적으로 통용되는 기능으로는 가속, 감속의 기본적인 기능이 있을것이며, 운전자가 있어야 합니다. 이를 일반적인 Java class로 정의해 봅시다.
public class SmallCar { private Driver driver; public void setDriver(Driver driver) { if (driver.getLicense() < 1){ throw new RuntimeException("insufficient license"); } this.driver = driver; } public void accelerate() { this.velocity += 10; } public void brake() { this.velocity -= 10; } }
Java
복사
SmallCar 클래스를 활용하는 Java 소스코드에서는 무조건 해당 클래스 객체를 활용하여야 합니다. 살펴보면 SmallCar 클래스의 가속 또는 감속 기능은 함수가 한번 호출될때 10 씩 수치의 변화가 생깁니다.
public class Road { private List<SmallCar> carsOnRoad; public Road() { this.carsOnRoad = new ArrayList<>(); } public Road(List<SmallCar> carsOnRoad) { this.carsOnRoad = carsOnRoad; } public void addCar(SmallCar car) { this.carsOnRoad.add(car); } }
Java
복사
자동차가 운행될 수 있는 길 클래스를 생각해봅시다. 지금 저희의 코드 기준에서는 SmallCar 객체 외에 자동차와 유사한 기능을 가진 객체는 추가될 수 없습니다.
Java의 Interface는 같은 기능을 하는 객체들의 형태를 정의합니다. 위의 상황에서 자동차 객체는 가속, 감속의 기능이 분명히 존재하여야 하고, 운전자 설정이 가능합니다.
public interface CarInterface { void setDriver(Driver driver); void accelerate(); void brake(); }
Java
복사
이제 RoadCarInterface 를 사용하도록 바꾸어 봅시다.
public class Road { private List<CarInterface> carsOnRoad; public Road() { this.carsOnRoad = new ArrayList<>(); } public Road(List<CarInterface> carsOnRoad) { this.carsOnRoad = carsOnRoad; } public void addCar(CarInterface car) { this.carsOnRoad.add(car); } }
Java
복사
이렇게 정의된 Road에는 다양한 형태의 자동차 객체가 들어갈 수 있습니다. 기본적으로 CarInterface 의 구현체(Implementation)이기만 하면 됩니다.
public class SmallCar implements CarInterface { ... } public class GoCart implements CarInterface { ... } ... Road road = new Road(); road.addCar(new SmallCar()); road.addCar(new GoCart());
Java
복사
인터페이스는 상속을 통해 하위 객체가 반드시 갖춰야할 기능을 정의를 해둠으로서, 실제로 해당 객체를 사용할때, 사용되어야 할 객체가 바뀌어야 하는 상황(새로운 기술 또는 자원의 등장)에서 코드의 유지보수성을 높입니다. Java 언어의 다향성을 보여주는 기능이라고 볼 수 있겠습니다. 만일 같은 인터페이스들이 같은 기능 또는 같은 변수를 공유하게 하고 싶다면, 추상 클래스를 만들어 줍니다.
public abstract class AbstractCar implements CarInterface{ protected int velocity = 0; @Override public void brake(){ if(this.velocity < 0) this.velocity = 0; } }
Java
복사
추상 클래스를 이용하면, 추상 클래스를 상속받는 다양한 클래스가 공통으로 가져야할 기능과 멤버 변수(field)를 설정할 수 있습니다. 여기에 추상 클래스가 인터페이스의 구현체라면, 위의 Road 를 사용한 예제에서 CarInterface 를 상속받은 객체 대신 AbstractCar 의 객체를 추가할 수도 있습니다.
이러한 Java의 interface는 이미 Java 언어 내부에서 많이 사용하고 있습니다. 대표적으로 ListArrayList 클래스를 살펴봅시다.
public interface List<E> extends Collection<E> { ... } public class ArrayList<E> extends AbstractList<E> implements List<E>, ... { ... }
Java
복사
그 구조를 살펴보면 List 는 인터페이스이고, Collection 을 상속받고 있습니다. 한편 ArrayListList 의 구현체이며, AbstractList 를 상속받고 있습니다. ArrayListList 의 구현체이므로, 기본적으로 List 인터페이스에서 요구하는 모든 기능을 활용할 수 있다고 볼수 있습니다.
어떤 함수를 만들때, 매개변수로 사용할 변수의 class를 고를때 ListArrayList 중 하나를 골라야 한다면,
public void withList(List<E> list){}; public void withArrayList(ArrayList<E> arrayList){};
Java
복사
위의 함수를 선택하여야 합니다. 언뜻 보면 기능적으로는 문제가 없어 보이지만, List 의 구현체는 ArrayList 만 존재하는것이 아니며, List 로서 기능한다면 함수를 사용할 수 있어야 합니다.
하지만 아래의 예시를 사용하게 된다면 List 의 구현체로서 함수 내부에서 기능할 수 있는 객체를 사용하더라도, 요구하는 인자의 클래스는 ArrayList 로 고정됩니다. 이 함수를 사용하려면 ArrayList 클래스를 사용할 수 밖에 없습니다.

Spring IoC Container와 DI

앞서 말씀드린 것처럼 IoC의 기본 개념은, 개발자가 프레임워크의 소스 코드를 사용하는 것이 아닌, 프레임워크가 상황에 맞는 개발자의 코드를 사용하는 것을 의미합니다.
앞서 살펴봤던 그림에서, DispatcherServlet 이라는 Spring의 일부가 사용자의 요청이 어디로 전달되어야 할지 정해서 전달한다고 이야기 하였습니다. 이때 실제 그 요청을 전달받는 Controller 와 그 안쪽의 RequestMapping (URL 경로에 연결하는 함수)으로 요청을 전달하게 되는데, 이 부분은 개발자가 작성하게 됩니다. 즉, Spring을 이용한 개발에서는, 추상화된 사용자의 요청을 처리하는 클래스와 함수를 정의하면 이미 만들어진 Spring Framework에서 해당 코드를 가져다 사용한다고 볼 수 있습니다.
그 외에 다른 작성된 코드도 Spring Framework에서 관리하게 됩니다. 뒤에서 확인하게 될 Spring에서 제공하는 특정 어노테이션 (@interface)이 붙은 클래스들을 관리합니다. 저희는 Spring을 사용하면서 저희가 작성하는 코드를 Spring에 위임하게 되며, 이때 이 코드를 관리하는 부분을 Spring IoC Container라 부르게 됩니다. 대표적으로 위에 그림의 Controller , Service , Repository 와 같은 어노테이션들을 확인하여 IoC Container에 등록하게 됩니다. Spring Container는 이 정의한 클래스를 확인하고, 객체로 생성, 관리합니다. 이때 Container에서 생성한 객체를 Bean 객체라고 부르게 됩니다.
개발자가 정의한 클래스들이 Spring Container에 Bean의 형태로 등록되면, 다시 Spring Application에 해당 클래스가 필요한 시점에 만들어진 Bean 객체를 전달하게 됩니다.
이때 전달하게 되는 Bean 객체는, 당연하지만 필요한 클래스를 그대로 전달을 하는데, 이때 클래스가 아니라 인터페이스를 요구하도록 할 수 있습니다.
public PostRestController( @Autowired PostService postService ){ this.postService = postService; }
Java
복사
PostRestController 라는 클래스는 PostService 객체가 있어야 인스턴스화 될 수 있습니다. 이때 개발자가 작성한 PostService 클래스 중 Spring 어노테이션이 붙은 클래스를, 생성자의 인자로 자동으로 전달하게 됩니다. 이 PostService 는 interface 일수도 있는데, Spring IoC Container에서는 요구하는 interface의 구현체를 감지하여 전달합니다.
// PostRestController에서 필요로 하는 PostService 인터페이스 public interface PostService { ... } // PostService의 구현체, IoC에 등록된 클래스 @Service public class PostServiceSimple implements PostService { ... }
Java
복사
위의 생성자는 PostService라는 인터페이스를 요구하고 있습니다. 이때 이 인터페이스의 구현체인 PostServiceSimple 이라는 객체가 자동으로 주입되게 됩니다. 이렇게 특정 클래스가 필요로하는 의존성(dependency)을 자동으로 주입하는 행동을 의존성 주입(Dependency Injection)이라고 합니다.
이 과정은 전부 Spring Framework가 진행을 위임하게 됩니다. Java의 Interface의 다향성과 더해 Spring Framework가 오랜 기간 사용되는 이유이기도 합니다. Spring IoC와 DI는 전체적인 코드간의 결합력을 줄여주면서, 개발자가 해야하는 작업을 줄여줍니다.

Spring과 Spring Boot의 차이

기본적으로 Spring Boot는 Spring의 하위 프로젝트 입니다. Spring Boot 실행하기에서도 확인하였듯이, Spring Boot는 산출물이 JAR 파일로 나오게 됩니다. Spring Framework 같은 경우는, 기본적으로 WAR 파일로 나오게 됩니다.
WAR파일은 Web Application Resource 또는 Web application ARchive의 약자입니다. WAR는 기본적인 Java에서 요구하는 정의사항을 기준으로 만들어진 웹 서버에서 사용자에게 전달한 HTML(JSP), 비즈니스 로직이 구현된 클래스 정의 등이 한곳에 압축된 파일입니다. 즉 자체적으로 실행되기 위한 파일이 아닌, Tomcat을 비롯한 다른 Web Server에서 활용하여 사용자에게 기능을 제공하기 위한 파일입니다.
반면 Spring Boot 프로젝트는 본래 실행을 위해 필요하던 Tomcat을 비롯한 Web Server를 JAR파일에 내장시켜, 관리와 실행을 간소화 하기 위한 프로젝트 입니다. 그래서 앞서 살펴봤듯이, 산출물은 JAR파일로 나오게 됩니다.
또한 Spring Framework 같은 경우 본래 개발자가 정상적으로 웹 서버를 실행하기 위해 필요한 설정들을 web.xml 로 정의하도록 요구하였습니다.
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
XML
복사
한편 Spring Boot 에서는 이러한 설정들을 일반적으로 통용되는 기본값으로 사용하여, 쉽게 실행할 수 있도록 미리 정의된 설정들이 다수 존재합니다. 그래서 큰 걱정없이 JAR파일을 실행하면, 기본적인 웹 서비스가 실행되게 됩니다. 이런 기본 설정들은 spring-boot-starter 에 정의되어 있습니다.
dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
Groovy
복사
Spring Boot는 Spring을 하기 위해서 필요하였던 많은 설정들을 간소화 하였고, Tomcat의 설정등 필요한 작업을 줄여주었습니다. 그래서 저희는 큰 걱정 없이 Spring Framework를 Spring Boot를 통해 공부할 수 있게 되었습니다.