Java Spring Framework 시작하기

Jeong-yun Lee·2025년 12월 23일

Spring이 필요한 이유?

Spring을 통해 다양한 애플리케이션(웹, REST API, Full Stack, Microservice) 빌드 가능.

쉽게 문제 디버깅할 수 있음.

Spring Boot에 대한 빠른 이해를 도움.

개발 생산성을 높이기 위해 사용!

Spring Framework: App 구축에 필요한 기능 제공 (“의존성 주입”과 “자동 연결”)

Spring Boot: Spring Framework를 쉽게 사용하기 위한 방법.

Coupling

  • 무언가를 변경하는데 얼마나 많은 작업이 관련되어 있는지에 대한 측정

Tight Coupling (iteration 1)

자동차-엔진: tight coupling

자동자-바퀴: loose coupling

코드는 지속적으로 변화하기 때문에, loose coupling을 사용하는 것이 바람직함.

가능한 한 코드 변경을 최소화하면서 기능을 변경할 수 있어야 함.

// Iteration 1. Tight Coupled Java Code
//     GameRuner class
//     Game classes: Mario, SuperContra, Pacman, etc.
package com.in28minutes.learn_spring_framework;

import com.in28minutes.learn_spring_framework.game.GameRunner;
import com.in28minutes.learn_spring_framework.game.MarioGame;
import com.in28minutes.learn_spring_framework.game.SuperContraGame;

public class AppGamingBasicJava {
    public static void main(String[] args) {

        var mariogame = new MarioGame();
        var marioGameRunner = new GameRunner(mariogame);
        marioGameRunner.run();

        var superContraGame = new SuperContraGame();
        var superContraGameRunner = new GameRunner(superContraGame);
        superContraGameRunner.run();
    }
}
  • 이 상태에서 실행하게 되면, GameRunner 생성자의 파라미터를 MarioGame으로 받고 있기 때문에 SuperContraGame 타입의 입력을 처리할 수 없어 컴파일 에러가 발생한다.
  • GameRunner 클래스가 특정 클래스(MarioGame)과 강하게 연결되어 결합되어 있어 다른 클래스(SuperContraGame)와는 호환되지 않는 상황을 Tight Coupling이라고 함.
package com.in28minutes.learn_spring_framework.game;

public class GameRunner {
    private MarioGame game;

		// MarioGame 타입의 인자만 받을 수 있는 상황
		// Tight Coupling 상황
    public GameRunner(MarioGame game) {
        this.game = game;
    }

    public void run() {
        System.out.println("Running game: " + game);
        game.up();
        game.down();
        game.left();
        game.right();
    }
}
  • SuperContraGame을 GameRunner에서 사용하기 위해서는 game 변수 선언을 변경해야함.
package com.in28minutes.learn_spring_framework.game;

public class MarioGame {
    public void up() {
        System.out.println("Jump");
    }
    public void down() {
        System.out.println("Go into a hole");
    }
    public void left() {
        System.out.println("Go back");
    }
    public void right() {
        System.out.println("Accelerate");
    }
}
package com.in28minutes.learn_spring_framework.game;

public class SuperContraGame {
    public void up() {
        System.out.println("up");
    }
    public void down() {
        System.out.println("Sit down");
    }
    public void left() {
        System.out.println("Go back");
    }
    public void right() {
        System.out.println("Shoot a bullet");
    }
}

Loose Coupling (iteration 2)

GameRunner 클래스가 GamingConsole 인터페이스와 상호작용하고 각 Game 클래스가 GamingConsole 인터페이스를 도입

// Iteration 2. Loose Coupling - Interfaces
//     GameRunner class
//     GamingConsole interface
//         - Game classes: Mario, SuperContra, Pacman, etc.
public class AppGamingBasicJava {
    public static void main(String[] args) {

        var mariogame = new MarioGame();
        var marioGameRunner = new GameRunner(mariogame);
        marioGameRunner.run();

        var superContraGame = new SuperContraGame();
        var superContraGameRunner = new GameRunner(superContraGame);
        superContraGameRunner.run();

        var pacmanGame = new PacmanGame();
        var pacmanGameRunner = new GameRunner(pacmanGame);
        pacmanGameRunner.run();
    }
}
  • Result
    Running game: com.in28minutes.learn_spring_framework.game.MarioGame@6d311334
    Jump
    Go into a hole
    Go back
    Accelerate
    Running game: com.in28minutes.learn_spring_framework.game.SuperContraGame@3d075dc0
    up
    Sit down
    Go back
    Shoot a bullet
    Running game: com.in28minutes.learn_spring_framework.game.PacmanGame@448139f0
    Up
    Down
    Left
    Right
    
    Process finished with exit code 0
    
package com.in28minutes.learn_spring_framework.game;

public class GameRunner {
    private GamingConsole game;

    // GamingConsole 인터페이스를 통해
    // GameRunner 코드를 변경하지 않고
    // Game 종류에 상관없이 유연하게 대처 가능.
    public GameRunner(GamingConsole game) {
        this.game = game;
    }

    public void run() {
        System.out.println("Running game: " + game);
        game.up();
        game.down();
        game.left();
        game.right();
    }
}
package com.in28minutes.learn_spring_framework.game;

// GameRunner가 Game 종류에 상관없이 일관된 작동할 수 있도록 함.
public interface GamingConsole {
    void up();
    void down();
    void left();
    void right();
}

각 Game 클래스는 GamingConsole 인터페이스를 구현하고 인터페이스의 추상 메소드를 실제 구현한다.

package com.in28minutes.learn_spring_framework.game;

// GamingConsole 인터페이스를 구현하고 추상 메소드 구현.
public class MarioGame implements GamingConsole {
    public void up() {
        System.out.println("Jump");
    }
    public void down() {
        System.out.println("Go into a hole");
    }
    public void left() {
        System.out.println("Go back");
    }
    public void right() {
        System.out.println("Accelerate");
    }
}
package com.in28minutes.learn_spring_framework.game;

// GamingConsole 인터페이스를 구현하고 추상 메소드 구현.
public class SuperContraGame implements GamingConsole {
    public void up() {
        System.out.println("up");
    }
    public void down() {
        System.out.println("Sit down");
    }
    public void left() {
        System.out.println("Go back");
    }
    public void right() {
        System.out.println("Shoot a bullet");
    }
}
package com.in28minutes.learn_spring_framework.game;

// GamingConsole 인터페이스를 구현하고 추상 메소드 구현.
public class PacmanGame implements GamingConsole {
    public void up() {
        System.out.println("Up");
    }
    public void down() {
        System.out.println("Down");
    }
    public void left() {
        System.out.println("Left");
    }
    public void right() {
        System.out.println("Right");
    }
}

Spring Bean

기존: JVM 내부에서 실행되는 객체 생성 코드를 사용자가 직접 관리

Spring: 프레임워크가 사용자 대신 객체 생성 및 결합할 수 있도록 함.

JVM에서 Spring context를 생성하고, Spring이 관리할 수 있는 Spring Bean을 생성. - @Configuration, @Bean 활용.

Spring이 관리하는 모든 것은 Spring Bean.

package com.in28minutes.learn_spring_framework;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Arrays;

// Spring & Spring Bean
public class App02HelloWorldSpring {
    public static void main(String[] args) {
        // 1. Launch a Spring context
        var context =
                new AnnotationConfigApplicationContext(HelloWorldConfiguration.class);

        // 2. Config things we want Spring to manage
        //     HelloWorldConfiguration - @Configuration
        //     name - @Bean

        // 3. Retrieving Beans managed by Spring
        System.out.println(context.getBean("name"));
        System.out.println(context.getBean("age"));
        System.out.println(context.getBean("person"));
        System.out.println(context.getBean("person2MethodCall"));
        System.out.println(context.getBean("person3Parameters"));
        System.out.println(context.getBean("customBean"));
            // getBean을 통해 Spring Bean의 이름을 통해 Bean 불러올 수 있음.

        System.out.println("\n" + context.getBean(Address.class));
            // getBean을 통해 Spring Bean의 타입을 지정해 Bean 불러올 수 있음.
        System.out.println("\n" + context.getBean(Person.class) + "\n");
            // 같은 타입의 객체가 여러개 발견되어 에러 발생. @Primary로 우선순위 부여해 해결.

        // get all names of beans by functional programming
        Arrays.stream(context.getBeanDefinitionNames())
                .forEach(System.out::println); // method reference

        // Qualifier로 특정
        System.out.println("\n" + context.getBean("person5Qualifier") + "\n");
    }
}
  • Java record immutable 데이터 객체를 간결하게 만들어 가독성 높임.
    필드 정의 시, 메소드 자동 생성(eqauls, hashCode, toString, ...)
    setter, getter, constructor 자동 생성.
    객체 간 불변 데이터 전달 목적. Data Transfer Object 표현에 적합.
package com.in28minutes.learn_spring_framework;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

// record?
// immutable 데이터 객체를 간결하게 만들어 가독성 높임.
// 필드 정의 시, 메소드 자동 생성(eqauls, hashCode, toString, ...)
// setter, getter, constructor 자동 생성.
// 객체 간 불변 데이터 전달 목적. Data Transfer Object 표현에 적합.
record Person (String name, int age, Address address) { }
record Address (String firstLine, String city) { }

@Configuration
public class HelloWorldConfiguration {

    // 2. Config things we want Spring to manage
    //     HelloWorldConfiguration - @Configuration
    //     name - @Bean
    //     1+ Bean method required
    @Bean
    public String name() {
        return "itisyijy";
    }

    @Bean
    public int age() {
        return 25;
    }

    @Bean
    @Primary
    public Person person() {
        var person = new Person("Cona", 8, new Address("Gangnam-gu", "Seoul"));
        return person;
    }

    // Bean 재활용해 새로운 Bean 생성 가능.
    @Bean
    public Person person2MethodCall() {
        // method call을 통해 Bean 활용하는 접근.
        var person = new Person(name(), age(), address());
        return person;
    }

    // Bean 재활용해 새로운 Bean 생성 가능.
    @Bean
    public Person person3Parameters(String name, int age, Address customBean) {
        // 파라미터로 Bean 활용하는 접근.
        // 파라미터가 자동으로 파라미터명에 해당하는 Bean을 자동 연결해 새로운 Bean 생성!
        var person = new Person(name, age, customBean);
        return person;
    }

    @Bean
    // "address"라는 이름의 Bean은 없으나, Primary가 있어 정상적으로 작동함.
    public Person person4Parameters(String name, int age, Address address) {
        var person = new Person(name, age, address);
        return person;
    }

    @Bean
    // 자동 주입 가능한 Bean이 2개 이상인 경우,
    // Qualifier로 Bean을 지정할 수 있다.
    public Person person5Qualifier(String name, int age, @Qualifier("address1qualifier") Address address) {
        var person = new Person(name, age, address);
        return person;
    }

    @Bean(name="customBean")
    @Primary
    public Address address() {
        var address = new Address("Nowon-gu", "Seoul");
        return address;
    }

    @Bean
    @Qualifier("address1qualifier")
    public Address address1() {
        var address = new Address("Yongsan-gu", "Seoul");
        return address;
    }
}
itisyijy
25
Person[name=Cona, age=8, address=Address[firstLine=Gangnam-gu, city=Seoul]]
Person[name=itisyijy, age=25, address=Address[firstLine=Nowon-gu, city=Seoul]]
Person[name=itisyijy, age=25, address=Address[firstLine=Nowon-gu, city=Seoul]]
Address[firstLine=Nowon-gu, city=Seoul]

Address[firstLine=Nowon-gu, city=Seoul]

Person[name=Cona, age=8, address=Address[firstLine=Gangnam-gu, city=Seoul]]

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
helloWorldConfiguration
name
age
person
person2MethodCall
person3Parameters
person4Parameters
person5Qualifier
customBean
address1

Person[name=itisyijy, age=25, address=Address[firstLine=Yongsan-gu, city=Seoul]]

Process finished with exit code 0

Spring Bean - Game Example (iteration 3)

package com.in28minutes.learn_spring_framework;
import com.in28minutes.learn_spring_framework.game.GameConfiguration;
import com.in28minutes.learn_spring_framework.game.GameRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

// Iteration 3. Loose Coupling - Spring Level 1.
//     Spring Beans
//     Spring framework will manage objects and wiring

public class App03GamingSpringBeans {
    public static void main(String[] args) {
        var context = new AnnotationConfigApplicationContext(GameConfiguration.class);
        var game1 = context.getBean("gameRunner"); // Object 타입을 리턴. 타입 캐스트 필요.
        ((GameRunner)game1) .run();
        var game2 = context.getBean(GameRunner.class); // Bean의 클래스를 함께 전달.
        game2 .run();
        var game3 = context.getBean("gameRunner", GameRunner.class);
            // Bean의 이름과 클래스를 함께 전달.
        game3 .run();
    }
}
package com.in28minutes.learn_spring_framework.game;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GameConfiguration {
    @Bean
    public GamingConsole marioGame() {
        return new MarioGame();
    }

    @Bean
    public GameRunner gameRunner(GamingConsole marioGame) {
        return new GameRunner(marioGame);
    }
}
Running game: com.in28minutes.learn_spring_framework.game.MarioGame@4e3958e7
Jump
Go into a hole
Go back
Accelerate
Running game: com.in28minutes.learn_spring_framework.game.MarioGame@4e3958e7
Jump
Go into a hole
Go back
Accelerate
Running game: com.in28minutes.learn_spring_framework.game.MarioGame@4e3958e7
Jump
Go into a hole
Go back
Accelerate

Process finished with exit code 0

Spring Bean - Game Example (iteration 4)

// Iteration 4. Loose Coupling - Spring Level 2.
//     Spring Annotations
//     Spring framework will create, manage & auto-wiring objects

Spring Container(Context) or IOC(Inversion of Control) Container

Spring Bean과 Spring Bean의 수명주기 관리

Java Class, Configuration(e.g., HelloWorldConfiguration.java)을 인풋으로 전달. Spring Container는 Runtime System을 리턴.

종류

  1. Bean Factory: 기본 Spring Container. MEM 제약 있는 IoT 디바이스인 경우 특별하게 사용.
  2. Application Context: Enterprise 전용 기능이 있는 advanced Spring container. 가장 일반적으로 사용하는 container.

Java Bean, POJO, Spring Bean

Java Bean: EJB(Enterprise Java Bean, 구식)에서 도입한 방법.

제한 사항

  1. public no-arg constructor 선언
  2. getter / setter 선언
  3. implements Serializable 선언

POJO: 모든 Java 객체 통칭.

Spring Bean: Spring에서 관리하는 모든 객체는 Spring Bean!

  • Dependency Injection
  • Application Context
  • Auto Wiring
  • Component Scan
profile
push hard 🥕

0개의 댓글