BlueNyang
[멀티캠퍼스] 풀스택 개발자 아카데미 (21) - Spring Framework(1)
Java 풀스택 아카데미
· 멀티캠퍼스 JAVA 풀스택 개발자 아카데미 6회차 (21편)

[멀티캠퍼스] 풀스택 개발자 아카데미 (21) - Spring Framework(1)

BlueNyangBlueNyang
·
·
약 10분
·
# 부트캠프후기# 멀티캠퍼스it부트캠프# [현대이지웰] JAVA 풀스택 개발자 아카데미 6회차# spring-framework# pojo# ioc# dependency-injection
시리즈·멀티캠퍼스 JAVA 풀스택 개발자 아카데미 6회차(30개의 글)
  • ···
  • 20.[멀티캠퍼스] 풀스택 개발자 아카데미 (20) - Servlet/JSP(4)
  • 21.[멀티캠퍼스] 풀스택 개발자 아카데미 (21) - Spring Framework(1)현재
  • 22.[멀티캠퍼스] 풀스택 개발자 아카데미 (22) - Spring Framework(2)
  • ···

1. EJB(Enterprise Java Beans)를 넘어서기

앞선 글을 통해 우리는 순수 서블릿/JSP 기반의 웹 애플리케이션 개발 방법과 MVC Model 2 아키텍처, 그리고 커넥션 풀을 이용한 성능 개선까지 자바 웹 개발의 기초를 다뤘다. 하지만 기능 구현 외에도 신경 써야 할 기반 설정과 반복적인 코드가 많다는 것을 느꼈을 것이다. 특히 과거 J2EE(Java 2 Platform, Enterprise Edition) 시절에는 EJB와 같은 기술들이 강력한 기능을 제공했지만, 동시에 과도한 복잡성, 느린 개발 속도, 어려운 테스트 등의 문제점을 안고 있었다.

이러한 배경 속에서 스프링 프레임워크(Spring Framework) 가 등장했다. 스프링은 EJB의 문제점을 해결하고, 평범한 자바 객체(POJO) 를 기반으로 하면서도 엔터프라이즈급 애플리케이션 개발에 필요한 강력한 기능들을 제공하는 것을 목표로 탄생했다. 가볍고 유연하며, 생산성을 극대화하는 스프링의 철학은 곧 많은 개발자들의 지지를 얻으며 사실상의 표준으로 자리 잡았다.

이번 글에서는 스프링 프레임워크가 왜 등장했으며, 그 핵심 철학은 무엇인지 알아본다. 특히 스프링의 기둥인 제어의 역전(IoC)의존성 주입(DI) 개념을 심층적으로 파헤치고, 전통적인 XML 설정을 통해 이를 구현하는 방법을 살펴본다.

2. POJO

스프링의 핵심 철학 중 하나는 POJO(Plain Old Java Object) 기반 프로그래밍이다. POJO는 말 그대로 '오래된 방식의 평범한 자바 객체'를 의미한다. 조금 다르게 표현하자면, 특정 기술이나 규약에 종속되지 않은 순수한 자바 클래스를 말한다 .

예를 들어, HttpServlet을 상속받아야 하는 서블릿 클래스와 달리, POJO는 특정 클래스를 상속하거나 인터페이스를 반드시 구현할 필요가 없다. 우리가 흔히 만드는 데이터 전달 객체(DTO/VO)나 자바빈(JavaBean)이 대표적인 POJO의 예이다.

POJO의 중요성

  • 단순함: 특정 기술에 얽매이지 않아 코드가 간결하고 이해하기 쉬움
  • 유연성: 특정 환경에 종속되지 않으므로 다른 환경이나 기술에서도 재사용하기 용이
  • 테스트 용이성: 복잡한 컨테이너 환경 없이도 간단한 단위 테스트(Unit Test) 작성이 가능

스프링은 이러한 POJO의 장점을 극대화하면서, 동시에 엔터프라이즈 환경에 필요한 트랜잭션 관리, 보안 등의 서비스를 POJO에 적용할 수 있도록 지원한다. 이 덕분에, 개발자는 복잡한 기술 규약 대신 비즈니스 로직 자체에 집중할 수 있게 된다.

3. 제어권의 역전(IoC)

객체 지향 프로그래밍에서 객체 간의 관계, 즉 의존성을 설정하는 것은 매우 중요하다. 전통적인 방식에서는 객체를 사용하는 쪽(클라이언트)이 자신이 사용할 객체(서비스)를 직접 생성하고 관리했다. 예를 들어 ControllerService 객체를 사용해야 한다면, Controller 내부에서 new Service() 코드를 통해 직접 객체를 생성했다. 이 경우 모든 제어권은 개발자(개발자가 작성한 코드)에게 있다.

제어의 역전(Inversion of Control, IoC) 은 이러한 전통적인 제어 흐름을 뒤집는 개념이다. 객체의 생성부터 생명주기 관리까지, 객체에 대한 모든 제어권이 개발자에게서 IoC 컨테이너(스프링에서는 스프링 컨테이너 또는 ApplicationContext라고 부름)로 넘어가게 된다.

IoC-개념-설명도-(전통방식-vs-IoC)
IoC-개념-설명도-(전통방식-vs-IoC)

마치 내가 필요한 도구를 직접 만드는 대신, 전문 공방(컨테이너)에 요청해서 이미 만들어진 도구를 받아 사용하는 것과 같다. 개발자는 더 이상 new 연산자를 통해 객체를 직접 생성할 필요 없이, 컨테이너에게 "이 객체가 필요하다"라고 요청하기만 하면 된다. 스프링 컨테이너는 설정 정보(예: XML 파일)를 바탕으로 필요한 객체들을 미리 생성(Pre-loading)해두고, 요청이 오면 해당 객체를 제공해준다.

IoC를 통해 얻는 가장 큰 이점은 느슨한 결합(Loose Coupling) 이다. 객체들이 서로 직접적으로 얽혀있지 않으므로, 하나의 모듈 변경이 다른 모듈에 미치는 영향을 최소화할 수 있어 유연하고 확장 가능한 시스템을 만들 수 있다.

4. 의존성 주입(DI)

IoC라는 개념을 실제로 구현하는 기술적인 방법론이 바로 의존성 주입(Dependency Injection, DI) 이다. DI는 클래스 간의 의존 관계(한 클래스가 다른 클래스의 기능을 사용하는 관계)를 코드 내부가 아닌, 외부(컨테이너) 에서 설정(주입)해주는 디자인 패턴이다.

DI-개념-설명도-(의존성-개념과-주입)
DI-개념-설명도-(의존성-개념과-주입)

예를 들어, NameController 클래스가 NameService 클래스에 의존한다고 가정하면,

  • 기존 방식 (Tight Coupling): NameController 내부에서 private NameService service = new NameService(); 와 같이 직접 객체를 생성한다. 만약 NameService 대신 다른 구현체(예: AnotherNameService)를 사용하려면 NameController의 코드를 직접 수정해야 한다.
  • DI 방식 (Loose Coupling): NameControllerNameService 타입의 객체가 필요하다는 사실만 알고 있고, 실제 어떤 구현체 객체가 들어올지는 신경 쓰지 않는다. 필요한 NameService 객체는 외부(스프링 컨테이너)에서 생성되어 NameController주입된다. 주입하는 방식은 크게 생성자를 통해 주입받거나 Setter 메소드를 통해 이루어진다.
생성자-주입과-Setter-주입-개념-비교
생성자-주입과-Setter-주입-개념-비교

DI를 사용하면, NameService의 구현체가 변경되더라도 NameController의 코드를 수정할 필요 없이, 외부 설정(XML 또는 어노테이션)만 변경하면 된다. 이는 코드의 유연성과 재사용성을 크게 높이고, 단위 테스트를 훨씬 용이하게 만든다.

5. 스프링 DI 구현

스프링에서 DI를 설정하는 방법은 크게 XML 파일을 이용하는 방식과 어노테이션(@)을 이용하는 방식이 있다. 여기서는 전통적인 XML 설정 방식을 먼저 알아본다.

5.1. 스프링 컨테이너와 빈(Bean)

스프링 IoC 컨테이너는 XML 설정 파일을 읽어 객체(스프링에서는 이를 Bean이라고 부른다)를 생성하고 관리한다. XML 파일에는 어떤 클래스를 객체로 만들고(빈 정의), 객체들 간의 의존 관계를 어떻게 설정할지(의존성 주입) 명시한다.

주요 컨테이너 인터페이스는 ApplicationContext이며, 클래스패스 상의 XML 파일을 읽는 구현체는 GenericXmlApplicationContext 등이 있다.

5.2. XML 파일 작성 (application-context.xml)

보통 src/main/resources(또는 src/main/resources/spring) 폴더 아래에 XML 설정 파일을 위치시킨다. 파일명은 관례적으로 application-context.xml 등으로 지정한다.

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="nameService" class="com.example.practice.spring_di.service.NameService" />

    </beans>

<bean> 태그는 스프링 컨테이너가 관리할 객체를 정의한다.

  • id: 빈의 고유한 식별자 (이름)
  • class: 빈으로 생성할 클래스의 전체 경로

5.3. 생성자 주입 (Constructor Injection)

클래스의 생성자를 통해 의존성을 주입하는 방식이다. XML 설정 파일에서 <constructor-arg> 태그를 사용한다.

예시 코드 (NameController.java):

java
package com.example.practice.spring_di.controller;

public class NameController {
    private NameService nameService; // 의존 객체

    // 생성자를 통해 NameService 객체를 주입받음
    public NameController(NameService nameService) {
        System.out.println("NameController 생성자 호출 (NameService 주입)");
        this.nameService = nameService;
    }

    public void showName(String name) {
        System.out.println("Controller: " + nameService.showName(name));
    }
}

XML 설정 (application-context.xml):

xml
<bean id="nameService" class="com.example.practice.spring_di.service.NameService" />
<bean id="nameController" class="com.example.practice.spring_di.controller.NameController">
    <constructor-arg ref="nameService" />
</bean>
XML-생성자-주입-설정-예시
XML-생성자-주입-설정-예시

컨테이너는 nameController 빈을 생성할 때, nameService 빈을 먼저 생성하고 이를 NameController의 생성자 인자로 전달하여 주입한다.

5.4. Setter 주입 (Setter Injection)

클래스의 Setter 메소드를 통해 의존성을 주입하는 방식이다. XML 설정 파일에서 <property> 태그를 사용한다.

예시 코드:

java
package com.example.practice.spring_di.controller;

public class NameController {
    private NameService nameService; // 의존 객체

    // 기본 생성자
    public NameController() {
        System.out.println("NameController 기본 생성자 호출");
    }

    // Setter 메소드를 통해 NameService 객체를 주입받음
    public void setNameService(NameService nameService) {
        System.out.println("setNameService() 호출 (NameService 주입)");
        this.nameService = nameService;
    }

    public void showName(String name) {
        System.out.println("Controller: " + nameService.showName(name));
    }
}

XML 설정 (application-context2.xml):

xml
<bean id="nameService" class="com.example.practice.spring_di.service.NameService" />
<bean id="nameController" class="com.example.practice.spring_di.controller.NameController">
    <property name="nameService" ref="nameService" />
</bean>
XML-Setter-주입-설정-예시
XML-Setter-주입-설정-예시

컨테이너는 먼저 NameController의 기본 생성자를 호출하여 빈을 생성한 후, setNameService() 메소드를 찾아 nameService 빈을 인자로 전달하여 호출함으로써 의존성을 주입한다.

5.5. 부록:Lombok 사용하기

이 부록은 본 교육과정이 아니라 스스로 찾아보며 공부한 내용이다.

어노테이션에 대해 찾아보고 공부하다보면 Lombok이라는 라이브러리를 마주할 수 있다. Lombok을 사용한다면 Getter나 Setter, 생성자를 쉽게 생성할 수 있다.

  • pom.xml에 의존성 추가하기:
xml
	<!-- Lombok -->
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	    <version>1.18.30</version>
	    <scope>provided</scope>
	</dependency>

생성자를 생성하는 어노테이션에는 @RequiredArgsConstructor라는 것이 있는데, 해당 어노테이션을 클래스에 적용하는 경우, 클래스의 필드 중 private final로 선언된 필드를 파라미터로 갖는 생성자가 생성된다.

예를 들어, 다음과 같은 클래스와 생성자가 있다고 하자.

java
@Controller
public class MyContoller {
	private Service service;

	public MyController(Service service) {
		this.service = service;
	}
}

해당 클래스는 어노테이션을 통해 다음과 같이 바꿀 수 있다.

java
@Controller
@RequiredArgsConstructor
public class MyController {
	private final Service service;
}

대부분의 경우, Service는 여러 객체를 생성하지 않는 싱글톤과 같이 관리되며 변경되는 일이 없기 때문에, final로 선언하더라도 문제가 없다.

이 외에도 Lombok은 VO나, DTO도 빠르게 선언할 수 있다.

  • Lombok을 사용하지 않은 VO:
java
public class MyDataVO {
	private int dataId;
	private String dataName;
	private String dataAuthor;

	public MyDataVO(){
	}

	public MyDataVO(int dataId, String dataName, String dataAuthor) {
		this.dataId = dataId;
		this.dataName = dataName;
		this.dataAuthor = dataAuthor;
	}

	public int getDataId() { return dataId; }
	public void setDataId(int dataId) { this.dataId = dataId; }

	public String getDataName() { return dataName; }
	public void setDataName(String dataName) { this.dataName = dataName; }

	public String getDataAuthor() { return dataAuthor; }
	public void setDataAuthor(String dataAuthor) { this.dataAuthor = dataAuthor; }
}
  • Lombok을 사용한 VO:
java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MyDataVO {
	private int dataId;
	private String dataName;
	private String dataAuthor;
}

6. 개발 환경 설정 및 레거시 프로젝트 생성

6.1. STS 3.9

이제 실제 스프링 프로젝트를 만들어보자. 교육 과정에서 사용하는 STS(Spring Tool Suite) 3.9 버전을 기준으로 설명하고, IntelliJ에서 구성하는 방법도 설명한다. (최신 환경은 STS4 또는 IntelliJ IDEA + Spring Initializr를 사용한다.)

Warning
단, 교육과정과 달리 IntelliJ를 사용하는 경우, 문제가 생기면, 문제를 직접 해결해야 하므로, 주의하길 바란다.
  1. STS 3.9 설치: 공식 사이트 또는 제공된 파일을 통해 설치한다. JDK 11 환경이 필요하다.
  2. Spring Legacy Project 생성: STS 메뉴에서 File > New > Spring Legacy Project를 선택한다.
  3. 프로젝트명 입력: 예: spring_di
  4. 템플릿 선택: 여러 템플릿 중, DI/IoC 개념 학습에는 Simple Spring Maven이 적합할 수 있다. 웹 애플리케이션 구조를 보려면 Spring MVC Project를 선택한다.
  5. 패키지명 입력: 예: com.example.practice
  6. pom.xml 확인 및 의존성 추가: 프로젝트가 생성되면 pom.xml 파일이 자동으로 만들어진다. 스프링 컨테이너(IoC/DI) 기능을 사용하려면 spring-context 라이브러리 의존성을 추가해야 한다. <dependencies> 태그 안에 아래 내용을 추가하거나, STS의 pom.xml 편집기 UI를 이용해 추가할 수 있다.

    XML

xml
	<dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context</artifactId>
       <version>5.2.25.RELEASE</version>
	</dependency>
  1. src/main/resources에 XML 설정 파일 생성: 위에서 설명한 application-context.xml 파일을 생성하고 빈과 의존성을 정의한다.
  2. Main 클래스 작성 및 실행: 컨테이너를 구동하고 빈을 사용하는 간단한 자바 애플리케이션을 만들어 테스트한다.

6.2. IntelliJ IDEA

STS 3.9는 Spring Framework 4.3.x를 지원하는 마지막 주요 STS 버전이다. 최근 Java 웹 프로젝트는 Spring Boot가 사실상 표준이 되면서, 최신 IDE에서는 레거시 Spring MVC 프로젝트 생성을 제공하지 않는다. 레거시 Spring MVC를 구성하려면, 직접 구성하거나 STS 3.9를 사용해야 하는데, 필자는 미리 세팅한 Maven Archetype 템플릿을 공유하고자 한다.

6.2.1. Archetype 템플릿 다운로드

다음 URL을 사용하거나, 본 블로그의 사이드바 최하단에서 File Sharing을 통해 파일 공유 서비스에 접속한 뒤, multicampus-java-fullstack > legacy-spring-mvc로 들어 간다.

해당 파일 공유 시스템에 접속했다면, kr.croffledev.legacy_mvc_archetype.zip (또는 .tar.gz/.tar.xz)을 내려받는다.

Tip
해당 시스템은 curl이나 wget으로도 동작하기 떄문에, 다음 명령어로도 다운로드할 수 있다. curl -L -O https://dl.bluenyang.kr/public/multicampus-java-fullstack/legacy-spring-mvc/[파일명(위 참고)] wget https://dl.bluenyang.kr/public/multicampus-java-fullstack/legacy-spring-mvc/[파일명(위 참고)]

6.2.2. Archetype 템플릿 적용

해당 zip(또는 tar.gz/tar.xz)파일을 압축 해제하면, kr 폴더가 나올 것이다. 폴더를 열어 봤다면, kr/croffledev/springmvc-archetype내에 파일들이 있지만, 다음 경로에 kr폴더 전체를 옮길 것이다.

  • Window: C:/Users/<사용자이름>/.m2/repository
  • MacOS/Linux: ~/.m2/repository

6.2.3. 프로젝트 만들기

IntelliJ에서 Project > New Project를 누르고, 좌측의 Generator 중 Maven Archetype을 고른다. 이후, 다른 프로젝트와 동일하게 프로젝트 이름과, 경로, JDK버전 등을 지정한다.

중요한 부분은 ArchetypeVersion이다.

  1. Archetype의 옆에 있는 Add 버튼을 누른다.
  2. Group Id: kr.croffledev, Artifact Id: springmvc-archetype, Version: 1.0.0
  3. Add 버튼을 누른다.

이후 Create를 눌러 프로젝트를 생성하면, Spring MVC 패턴에 맞게 의존성 및 파일 구조가 생겨난다. Spring의 버전은 5.2.25 RELEASE로 되어있으며, JSP, JSTL이 포함되어 있어 따로 추가하지 않아도 된다.

IntelliJ-Archetype 추가하기
IntelliJ-Archetype 추가하기
IntelliJ-프로젝트-생성-최종-화면
IntelliJ-프로젝트-생성-최종-화면

6.3. Main 작성하기

  • Java:
java
// Main.java 예시
import org.springframework.context.support.GenericXmlApplicationContext;
// 사용하는 패키지에 맞게 변경
import com.example.practice.spring_di.controller.NameController;

public class Main {
    public static void main(String[] args) {
        // 1. 스프링 컨테이너 생성 (XML 설정 파일 로드)
        GenericXmlApplicationContext ctx =
                new GenericXmlApplicationContext("classpath:application-context.xml");

        // 2. 컨테이너로부터 빈(Bean) 객체 가져오기 (Lookup)
        NameController controller = ctx.getBean("nameController", NameController.class);

        // 3. 빈 객체 사용
        controller.showName("스프링");

        // 4. 컨테이너 종료
        ctx.close();
    }
}

이 Main 클래스를 실행하면, 스프링 컨테이너가 XML 설정을 읽어 NameServiceNameController Bean을 생성하고 의존성을 주입한 후, getBean()을 통해 얻어온 controller 객체의 메소드를 정상적으로 호출하는 것을 확인할 수 있다.

7. 마무리

이번 글에서는 스프링 프레임워크의 탄생 배경과 핵심 철학인 POJO 기반 프로그래밍, 그리고 IoC/DI 개념에 대해 알아보았다. IoC는 객체 관리의 제어권을 컨테이너에 넘기는 개념이며, DI는 이를 구현하기 위해 외부에서 의존 객체를 주입하는 기술임을 이해했다. 또한, 전통적인 XML 설정을 통해 생성자 주입과 Setter 주입을 구현하는 방법을 실습했다.

IoC와 DI는 단순히 기술적인 구현 방식을 넘어, 코드 간의 결합도를 낮추고 유연성과 확장성, 테스트 용이성을 높이는 스프링의 핵심 가치를 담고 있다. 이 개념을 확실히 이해하는 것이 앞으로 스프링 기반의 다양한 기술(MVC, AOP, Data 등)을 효과적으로 학습하고 활용하는 데 튼튼한 기초가 될 것이다.

XML 설정 방식은 빈과 의존 관계를 명확하게 보여준다는 장점이 있지만, 프로젝트 규모가 커지면 관리하기 번거로워질 수 있다. 이후에는 어노테이션 기반 설정이나 Java Config 방식 등 더 간결한 설정 방법들도 등장하게 된다.

8. 결론

스프링의 IoC/DI 개념은 처음 접하면 다소 추상적으로 느껴질 수 있지만, 객체 지향 설계의 원칙(특히 SOLID의 DIP - 의존관계 역전 원칙)과 깊은 관련이 있다. 이 개념들을 적용함으로써 얻는 느슨한 결합의 이점은 애플리케이션의 품질을 크게 향상시킨다. XML 기반 설정은 스프링 초기부터 사용된 방식으로, 그 구조를 이해하는 것은 스프링의 동작 방식을 파악하는 데 도움이 된다. 하지만 현대적인 스프링 개발에서는 어노테이션이나 Java Config 방식이 더 선호되는 경향이 있다.

다음 글에서는 스프링 MVC의 심장이라 할 수 있는 DispatcherServlet의 동작 원리와 요청 처리 과정에 대해 자세히 다뤄볼 예정이다.

BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus