본문 바로가기

(10.13) Spring Boot - Spring Boot 개요, 프로젝트 생성 및 실행, 어노테이션, IOC / DI

@starweb2025. 10. 13. 18:34

[ 9주차 - 1013 ] 

    금일 커리큘럼
        ├ 09:00 ~ 12:00 backend 프로그래밍 (Spring Boot 개요, 프로젝트 생성 및 실행)
        └ 13:00 ~ 18:00 backend 프로그래밍 (Spring Boot 주요 어노테이션, IoC/DI 개념)

1. Spring Boot 개요

 

Spring과 Spring Boot란?

  • Spring : 자바 플랫폼을 위한 오픈소스 애플리케이션 프레임워크
    • 주요 특징으로는 IoC(제어의 역전), AOP(관점 지향 프로그래밍), 트랜잭션 관리 등을 제공하여, 엔터프라이즈급 애플리케이션 개발을 용이하게 함.
  • Spring Boot : 스프링기반으로 WAS개발 간소화하고 빠르게 시작할 수 있도록 도와주는 프레임워크
    • 복잡한 설정을 자동화하고, 내장 서버를 제공하여, 개발자가 빠르게 애플리케이션을 시작할 수 있도록 지원.

즉, 스프링은 핵심 모듈들 모아서 만든 것이고, 스프링 부트는 스프링을 더 쉽게 사용할 수 있도록 도와주는 도구

구분 Spring Spring Boot
설정 방식 개발자가 직접 설정 파일 작성 필요
(XML, Java Config)
자동 설정 제공
의존성 관리 필요한 라이브러리와 버전을 개발자가 직접 관리 Starter 의존성으로 관련 라이브러리 자동 관리
서버 배포 별도의 WAS 설치 및 설정 필요 내장 서버 제공으로 독립 실행 가능
실행 파일 WAR 파일로 패키징하여 서버에 배포 실행 가능한 JAR 파일 생성
개발 속도 초기 설정에 많은 시간 소요 빠른 프로젝트 시작 가능
사용 목적 세밀한 제어가 필요한 복잡한 애플리케이션 빠르고 간단한 애플리케이션 개발

 

Spring Boot의 핵심 가치

  • CoC(Convention over Configuration) : 일반적인 설정을 미리 정의하여, 개발자가 최소한의 설정으로도 애플리케이션을 시작할 수 있도록 함.
  • 자동 설정(Auto Configuration) : 개발자가 직접 설정 파일을 작성하지 않아도, Spring Boot가 프로젝트의 의존성에 따라 자동으로 필요한 설정을 적용.
  • 독립 실행형 애플리케이션 : 내장서버 (예: Tomcat, Jetty)를 포함하여, 별도의 서버 설치 없이도 애플리케이션을 실행할 수 있음.
  • 운영 환경 최적화 : Spring Boot는 운영 환경에서의 성능과 안정성을 고려하여 설계되었으며, 다양한 모니터링 및 관리 기능을 제공.

2. Spring Boot 프로젝트 시작하기

 

Spring Initializr를 이용한 프로젝트 생성

Spring Initializr : Spring Boot 프로젝트를 쉽게 생성할 수 있는 웹 기반 도구

  • 해당 URL 접속 : https://start.spring.io/
  • 혹은 IntelliJ 울티메이트에서 New Project -> Spring Initializr 선택

 

프로젝트 메타데이터

  • Project : gradle - Groovy
  • Language : Java
  • Spring Boot : 3.5.6
  • Project Metadata
    • Group : com.example (회사 도메인 역순)
    • Artifact : demo (프로젝트 이름)
    • Name : demo (프로젝트 이름)
    • Description : Demo project for Spring Boot (프로젝트 설명)
    • Package name : com.example.demo (기본 패키지 이름)
    • Packaging : Jar (실행 가능한 JAR 파일 생성)
    • Java : 21 (사용할 Java 버전)

Dependencies (의존성)

  • Spring Web : 웹 애플리케이션 개발을 위한 기본 의존성
  • Spring Boot DevTools : 개발 편의를 위한 도구 (자동 재시작, 라이브 리로드 등)

 

설정 완료 후 제너레이트 클릭하여 해당 프로젝트 다운로드 (zip 파일)


IntelliJ를 이용한 프로젝트 생성

  • 메타데이터 입력 후 -> [다음] -> 필요한 Dependencies 선택 -> [완료] 클릭

IntelliJ에서 Spring Boot 프로젝트 열기

  • File -> Open -> 해당 폴더 선택
  • 외부 라이브러리 다운로드 및 인덱싱 진행 (잠시 대기)
    • Gradle 프로젝트이므로, IntelliJ가 자동으로 build.gradle 파일을 인식하고 필요한 라이브러리를 다운로드하게 됨.
    • external Libraries에 그래들 Spring Boot, Spring Web 등이 추가된 것을 확인할 수 있음.

스프링 부트 구조

demo
 ├─ .idea
 ├─ src
 │   ├─ main
 │   │   ├─ java/com/example/demo       # 기본 패키지 경로
 │   │   │   └─ DemoApplication.java    # 메인 애플리케이션 클래스
 │   │   │
 │   │   └─ resources                   # 리소스 파일 경로
 │   │       ├─ static                   # 정적 자원 (CSS, JS, 이미지 등)
 │   │       ├─ templates                # 템플릿 파일 (Thymeleaf 등)
 │   │       ├─ application.properties   # 애플리케이션 설정 파일
 │   │       └─ ...
 │   │
 │   └─ test # 테스트 소스 경로
 │       └─ java/com/example/demo
 │           └─ DemoApplicationTests.java
 ├─ .gitignore
 ├─ build.gradle    # 그래들 빌드 설정 파일
 ├─ gradle          # 그래들 래퍼 파일
 ├─ gradlew         # 그래들 래퍼 실행 파일
 ├─ gradlew.bat     # 그래들 래퍼 배치 파일
 └─ settings.gradle # 그래들 설정 파일

3. Spring Boot 애플리케이션 실행하기

DemoApplication.java

  • 해당 자바 파일이 스프링 부트 애플리케이션의 진입점
  • @SpringBootApplication 어노테이션이 붙어있으며, main 메서드에서 SpringApplication.run() 메서드를 호출하여 애플리케이션을 시작되도록 함
  • @GetMapping 어노테이션을 사용하여 HTTP GET 요청을 처리하는 메서드를 정의할 수 있음
// ...
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication  // 스프링 부트 애플리케이션임을 나타내는 어노테이션
@RestController         // RESTful 웹 서비스의 컨트롤러 역할을 하는 클래스임을 나타냄
public class HellospringApplication {

    // HTTP GET 요청을 처리하는 메서드 매핑
    @GetMapping("/star1431") // localhost:8080/star1431 접속하면 나타남
    public String star1431() {
        return "Hello Spring Boot!";
    }

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

로컬포트 변경 방법

  • 기본적으로 스프링 부트 애플리케이션은 8080 포트를 사용
  • src/main/resources/application.properties 파일에 다음 설정 추가
# 서버 포트 설정 (기본값: 8080)
server.port=8088

# 개발 환경에서 자동 재시작 활성화
spring.devtools.restart.enabled=true
  • application.yml 파일로도 설정 가능
server:
  port: 8088

4. build.gradle 상세 분석

Spring Boot 3.x의 build.gradle은 다음과 같은 주요 섹션으로 구성됨

plugins { // 플러그인 섹션
    id 'java'
    id 'org.springframework.boot' version '3.5.6'
    id 'io.spring.dependency-management' version '1.1.7'
}

// 프로젝트 메타데이터
group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'

java { // 자바 섹션 (컴파일러)
    toolchain { // 자바 툴체인 설정
        languageVersion = JavaLanguageVersion.of(21) // 자바 21 버전 사용
    }
}

repositories { // 의존성 저장소 섹션
    mavenCentral()
}

dependencies { // 의존성 섹션
    implementation 'org.springframework.boot:spring-boot-starter-web' // starter-web : 웹 개발용
    // implementation 'org.springframework.boot:spring-boot-devtools' // 개발 편의를 위한 도구
    testImplementation 'org.springframework.boot:spring-boot-starter-test' // stater-test : 테스트용
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') { // 테스트 태스크 설정
    useJUnitPlatform()
}
  • plugins : 프로젝트에 적용할 플러그인 정의
    • java : 자바 프로젝트용 플러그인
    • org.springframework.boot : 스프링 부트 플러그인
    • io.spring.dependency-management : 의존성 관리 플러그인
  • dependencies : 프로젝트에 필요한 라이브러리 의존성 정의
    • implementation : 컴파일 및 런타임에 필요한 의존성 (Spring Boot Starter 등)
    • testImplementation : 테스트 코드 컴파일 및 런타임에 필요한 의존성 (JUnit, Mockito 등)
    • testRuntimeOnly : 테스트 실행 시에만 필요한 의존성 (JUnit 플랫폼 런처 등)
    • runtimeOnly : 런타임에만 필요한 의존성 (예: JDBC 드라이버)
    • compileOnly : 컴파일 시에만 필요한 의존성 (예: Lombok)
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web' // 웹 개발용
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JPA 사용용
    implementation 'com.h2database:h2' // H2 데이터베이스
    testImplementation 'org.springframework.boot:spring-boot-starter-test' // 테스트용
}

TIP - 터미널 그래들 명령어 모음

# 애플리케이션 실행 (개발 모드)
./gradlew bootRun

# 전체 빌드 (컴파일 + 테스트 + JAR 생성)
./gradlew build

# 테스트 실행
./gradlew test

# 실행 가능한 JAR 파일 생성
./gradlew bootJar

# 빌드 결과물 삭제 (clean build 시 유용)
./gradlew clean

# clean + build 한 번에 실행
./gradlew clean build

# 의존성 트리 확인
./gradlew dependencies

# 프로젝트 정보 확인
./gradlew projects

# 사용 가능한 모든 Task 확인
./gradlew tasks

# 특정 Task 상세 정보
./gradlew help --task bootRun

# 테스트 스킵하고 빌드
./gradlew build -x test

# 병렬 빌드 (멀티 모듈 프로젝트)
./gradlew build --parallel

# 빌드 캐시 사용
./gradlew build --build-cache

# 의존성 새로 다운로드 (캐시 무시)
./gradlew build --refresh-dependencies

# 디버그 모드로 실행
./gradlew bootRun --debug-jvm

5. 스트링부트 어노테이션 이해

주요 어노테이션

  • @SpringBootApplication : 스프링 부트 애플리케이션의 시작점임을 나타냄
    • 딱 한 개의 클래스에만 붙여야 함 (보통 메인 클래스)
    • 내부적으로 @Configuration, @EnableAutoConfiguration, @ComponentScan을 포함되어 있음.
    • 컴포넌트스캔 ? 메인에서 객체 생성 안하고도 자동으로 스캔하여 Bean으로 등록 (DI 가능)
  • @RestController : RESTful 웹 서비스의 컨트롤러 역할을 하는 클래스임을 나타냄
    • @Controller@ResponseBody가 결합된 형태
    • 메서드가 반환하는 값이 HTTP 응답 본문에 직접 쓰여짐 (뷰 리졸버 사용 안함)
  • @GetMapping : HTTP GET 요청을 처리하는 메서드를 매핑
    • @RequestMapping(method = RequestMethod.GET)의 축약형
    • 예: @GetMapping("/hello")
@SpringBootApplication
@RestController
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
//order source...
@RestController  // 스프링부트어플리케이션에서 컴포넌트 스캔에 탐지되어 "/json01" 접속됨
class TestController {
    @GetMapping("/json01")
    public Map<String, Object> json01() {
        Map<String, Object> map = new HashMap<>();
        map.put("key01", "value01");
        map.put("key02", 1);
        map.put("time", LocalDateTime.now().toString());
        return map;
    }
}

6. 스프링 코어 - IoC / DI

IoC (Inversion of Control, 제어의 역전)

  • 객체의 생성과 생명주기 관리를 개발자가 아닌 프레임워크가 담당
  • 객체의 의존성을 외부에서 주입받아 사용 (DI)

DI (Dependency Injection, 의존성 주입)

  • 객체가 필요로 하는 의존 객체를 외부에서 주입받는 방식
  • 생성자 주입, 세터 주입, 필드 주입 방식이 있음
  • 스프링에서는 주로 생성자 주입을 권장 (불변성 유지, 테스트 용이)

스프링 Bean

  • 빈은 인스턴스화된 객체를 의미하며, 스프링 IoC 컨테이너에 등록된 객체를 스프링 빈이라고 함.
    • 즉, 자바의 new 키워드로 생성한 객체 대신 스프링이 관리하는 객체를 사용

Bean 등록 방법

  • @Component, @Service, @Repository, @Controller 등 사용하여 등록
  • @Configuration 클래스 내에서 @Bean 사용하여 등록
  • XML 설정 파일을 통해 등록 (구식 방법)
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

// 0. Component 사용한 Bean 등록 (일반 컴포넌트)
@Component
class MyComponent {
    public void message() {
        System.out.println("안녕하세요 Component 정상 등록되었습니다!");
    }
}

public class ComponentExample {
    public static void main(String[] args) {
        // ApplicationContext context = new AnnotationConfigApplicationContext(MyComponent.class);
        // 현재 클래스가 속한 패키지를 기준으로 컴포넌트 스캔
        ApplicationContext context =
                new AnnotationConfigApplicationContext(ComponentExample.class.getPackageName());


        // 등록된 Bean 가져오기
        MyComponent myComponent = context.getBean(MyComponent.class);
        myComponent.message(); // "안녕하세요 Component 정상 등록되었습니다!" 출력

        context.close();
    }
}
import org.springframework.web.bind.annotation.Service;

// 1. @Service를 사용한 Bean 등록 (비즈니스 로직 담당)
@Service
public class UserService {
    private final UserRepository userRepository;

    // 생성자 주입 (권장 방식)
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserById(Long id) {
        return userRepository.findById(id);
    }

    public User createUser(String name, String email) {
        User newUser = new User(name, email);
        return userRepository.save(newUser);
    }
}
import org.springframework.web.bind.annotation.Repository;

// 2. @Repository를 사용한 Bean 등록 (데이터 액세스 담당)
@Repository
public class UserRepository {
    private final List<User> users = new ArrayList<>();

    public User findById(Long id) {
        return users.stream()
                .filter(user -> user.getId().equals(id))
                .findFirst()
                .orElse(null);
    }

    public User save(User user) {
        users.add(user);
        return user;
    }
}
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping; 

// 3. @Controller를 사용한 Bean 등록 (웹 요청 처리 담당)
@RestController
public class UserController {
    private final UserService userService;

    // 생성자 주입으로 UserService 의존성 주입
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findUserById(id);
    }

    @PostMapping("/users")
    public User createUser(@RequestBody CreateUserRequest request) {
        return userService.createUser(request.getName(), request.getEmail());
    }
}
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;

// 4. @Configuration과 @Bean을 사용한 Bean 등록
@Configuration
public class AppConfig {

    // 애플리케이션 설정 Bean
    @Bean
    public AppProperties appProperties() {
        AppProperties properties = new AppProperties();
        properties.setName("Spring Boot Demo");
        properties.setVersion("1.0.0");
        return properties;
    }
}

// 설정 클래스 예시
class AppProperties {
    private String name;
    private String version;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getVersion() { return version; }
    public void setVersion(String version) { this.version = version; }
}

7. Bean 관리 실습

IoC 방식

스프링 컨테이너가 객체의 생성과 생명주기를 관리하는 방식

  • context.getBean("빈이름", 클래스명.class) 으로 주입
  • @Scope("prototype") : 매번 새로운 인스턴스 생성 (기본값은 싱글톤)
package sample.bean;

public class MyBean {
    private String name;
    private int age;

    public MyBean() {
        System.out.println("마이빈 생성!");
    }

    public MyBean(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name;}
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }

    @Override
    public String toString() {
        return "MyBean{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package sample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import sample.bean.MyBean;

@Configuration
public class MyBeanConfig {

    // XML
    // <bean id="myBean" class="sample.bean.MyBean"/>

    @Bean
    public MyBean myBean() { return new MyBean(); }

    @Bean
    @Scope("prototype") // 프로토타입 스코프 설정
    public MyBean myBean1() { return new MyBean(); }

    @Bean
    public MyBean myBean2() { return new MyBean(); }

    @Bean
    public MyBean myBean3() {
        MyBean myBean = new MyBean();
        myBean.setName("myBean3");
        myBean.setAge(88);
        return myBean;
    }
}
package sample.run;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import sample.bean.MyBean;
import sample.config.MyBeanConfig;

public class RunExam01 {
    public static void main(String[] args) {
        System.out.println("스프링 빈 등록 전 ----");
        ApplicationContext context = new AnnotationConfigApplicationContext(MyBeanConfig.class);

        System.out.println("스프링 빈 등록 후 ----");

        // 등록된 빈
        MyBean bean = (MyBean) context.getBean("myBean");
        bean.setName("star1431");
        bean.setAge(99);
        System.out.println(bean.toString());

        MyBean myBean = (MyBean) context.getBean("myBean");
        System.out.println(bean == myBean); // true (@Scope 아닌 경우 같은 인스턴스)

        // 타입을 통해서 주입
        MyBean bean1 = context.getBean("myBean1", MyBean.class); // @Scope
        bean1.setName("star1431");
        bean1.setAge(88);
        System.out.println(bean1.toString());

        MyBean bean2 = context.getBean("myBean2", MyBean.class); // 다른 네임
        System.out.println(bean2.toString());

        MyBean bean3 = context.getBean("myBean3", MyBean.class); // 컨피그에서 값 세팅
        System.out.println(bean3.toString());
    }
}
# 실행 결과
스프링 빈 등록 전 ----
마이빈 생성!
마이빈 생성!
마이빈 생성!
스프링 빈 등록 후 ----
MyBean{name='star1431', age=99}
true
마이빈 생성!
MyBean{name='star1431', age=88}
MyBean{name='null', age=0}
MyBean{name='myBean3', age=88}

DI 방식

의존성 주입하여 객체를 사용하는 방식

  • 생성자 주입, 세터 주입 방식이 있음 (하단 예시 Dice.java)
    • 생성자 주입 : 불변성 유지, 필수 의존성 주입에 적합
    • 세터 주입 : 선택적 의존성 주입에 적합
package sample.bean;

public class Dice {
    private int face;

    public Dice() { System.out.println("Dice() 생성!"); }

    public Dice(int face) {
        this.face = face;
        System.out.println("Dice(int) 생성!");
    }

    public int getFace() { return face; }
    public int runDice() { return (int) (Math.random() * face) + 1; }
}
package sample.bean;

public class Player {
    private String name;
    private Dice dice; // 의존성 주입 받음 (DI)

    public Player() {}

    // 생성자로 주입
    public Player(Dice dice) { this.dice = dice; }

    public void setName(String name) { this.name = name; }

    // 세터로 주입
    public void setDice(Dice dice) { this.dice = dice; }

    public void play() {
        System.out.printf("[%s]님이 주사위 던져서 [%d] 나왔습니다. (주사위면: %d)%n", name, dice.runDice(), dice.getFace());
    }
}
package sample.bean;

import java.util.List;

public class Game {
    private List<Player> players; // 의존성 주입

    public Game() {}
    // 생성자로 주입 전달
    public Game(List<Player> players) { this.players = players; }

    public void play() {
        for (Player player : players) {
            player.play();
        }
    }
}
package sample.config;

import org.springframework.context.annotation.Bean;
import sample.bean.Dice;
import sample.bean.Game;
import sample.bean.Player;

import java.util.List;

public class GameConfig {

    @Bean
    public Dice dice() {
        return new Dice(6);
    }

    @Bean
    public Player kim(Dice dice) {
        Player player = new Player(dice);
        player.setName("kim");
        return player;
    }

    @Bean
    public Player player1(Dice dice) {
        Player player = new Player(dice);
        player.setName("player1");
        return player;
    }

    @Bean
    public Player player2(Dice dice) {
        Player player = new Player(dice);
        player.setName("player2");
        return player;
    }

    @Bean
    public Game game(List<Player> players) {
        return new Game(players);
    }
}
package sample.run;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import sample.bean.Game;
import sample.bean.Player;
import sample.config.GameConfig;

public class RunExam02 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(GameConfig.class);

        Player kim = context.getBean("kim", Player.class);
        kim.play();

        System.out.println("Bean 등록된 플레이어들 ---");
        Game game = context.getBean("game", Game.class);
        game.play();

    }
}
# 실행 결과
Dice(int) 생성!
[kim]님이 주사위 던져서 [4] 나왔습니다. (주사위면: 6)
Bean 등록된 플레이어들 ---
[kim]님이 주사위 던져서 [1] 나왔습니다. (주사위면: 6)
[player1]님이 주사위 던져서 [3] 나왔습니다. (주사위면: 6)
[player2]님이 주사위 던져서 [5] 나왔습니다. (주사위면: 6)

etc.

포트 강제 종료 방법 (cmd)

Windows

# 포트 사용 프로세스 찾기
netstat -ano | findstr :8080

# 실행결과
# TCP   0.0.0.0:8080  0.0.0.0:0   LISTENING   1160
# TCP   [::]:8080     [::]:0      LISTENING   1160

# 프로세스 종료 (LISTENING 옆에 있는 PID 번호 입력)
taskkill /PID [PID번호] /F

Mac / Linux

# 포트 사용 프로세스 찾기
lsof -i :8080

# 프로세스 종료
kill -9 [PID번호]
starweb
@starweb :: starweb 님의 블로그

starweb 님의 블로그 입니다.

공감하셨다면 구독도 환영합니다!

목차