Spring Boot 입문 — 자동 설정과 DI 한 번에

2026-05-02AWS SAA-C03 스터디

Spring Boot 3 핵심 정리 시리즈 첫 글. Spring Boot가 왜 만들어졌는지부터 회사 자동 세팅 도구 비유로 풀어가며 — @SpringBootApplication 자동 설정의 정체, IoC 컨테이너와 DI 3가지 방식(생성자/세터/필드), Bean 스코프 5종, @Service·@Repository·@Controller 스테레오타입, Project Lombok으로 보일러플레이트 줄이기까지 처음 보는 사람도 따라올 수 있게 친절하게 풀어쓴 1편.

📚 Spring Boot 3 핵심 정리 · 1편 / 14편 — 자동 설정과 DI 한 번에

이 글은 Spring Boot 3 핵심 정리 시리즈의 첫 번째 편입니다. 자바 백엔드를 본격적으로 배우려는 분이라면 Spring Boot는 거의 피할 수 없는 관문이에요. 다만 처음 펼치면 @SpringBootApplication·@Component·@Autowired·@Configuration 같은 어노테이션이 줄지어 나오고, "IoC·DI"라는 추상적인 단어가 함께 따라옵니다. "이게 다 뭐야?" 싶은 마음이 드는 게 자연스러워요.

이 시리즈는 17편을 통해 Spring Boot 3 + Spring Framework 6의 큰 그림과 디테일을 차근차근 쌓아 갑니다. 한 번에 다 외우려 하지 마시고, 이번 1편에서는 "Spring Boot가 도대체 뭘 자동으로 해 주는 도구인가, IoC·DI는 왜 만들어졌는가, Bean·Lombok은 어디 자리 잡는가" — 이 세 가지 질문의 답만 머리에 들어와도 충분합니다.

본문 흐름은 회사 비유를 따라 풀어 가요. Spring Boot는 "복잡한 회사 시스템을 자동으로 세팅해 주는 도구" 이고, IoC 컨테이너는 "회사 직원 관리 부서" 같은 거예요. 이 두 비유만 잡고 가면 어노테이션 50개가 한 번에 정리됩니다.

📚 학습 노트

이 시리즈는 Spring 공식 문서, Spring Boot 레퍼런스 가이드, Java 백엔드 학습 자료 등 여러 공개 자료를 참고해 한국어 학습 노트로 풀어쓴 자료입니다.

읽으면서 IDE를 열고 직접 한두 개라도 따라 만들어 보면 어노테이션이 머리에 훨씬 잘 박혀요. start.spring.io에서 빈 프로젝트를 받아 같이 따라가시는 걸 권장합니다.

왜 Spring Boot가 처음엔 어렵게 느껴질까요

이유는 네 가지예요.

첫째, 어노테이션이 너무 많아요. @SpringBootApplication·@Component·@Service·@Repository·@Controller·@RestController·@Autowired·@Configuration·@Bean·@Value·@Profile·@Conditional… 한 페이지에 이 단어들이 다 나오면 머리가 어지러워집니다.

둘째, 자동 설정이 마법처럼 동작해요. "그냥 됐는데 왜 됐는지 모르겠다"가 자주 일어납니다. 무엇이 어디서 일어나는지 안 보이니 디버깅도 어려워요.

셋째, IoC·DI 같은 추상 개념이 처음엔 와닿지 않아요. "제어의 역전"이라는 단어부터가 이미 어렵죠. 왜 이런 패턴이 만들어졌는지 동기부터 알아야 와닿습니다.

넷째, Java EE / Jakarta EE / Spring Framework / Spring Boot 관계가 헷갈려요. 비슷한 이름이 줄지어 있어서 어느 게 어느 위에 얹혀 있는지 한 번에 안 잡힙니다.

해결법은 한 가지예요. Spring Boot를 "회사 시스템 자동 세팅 도구" 로 잡고, IoC 컨테이너를 "회사 직원 관리 부서" 로 풀면 갑자기 명확해집니다. 어노테이션은 직원에게 붙는 명찰이고, DI는 외부에서 직원을 받아오는 방식이에요. 이 글은 그 비유를 따라 처음부터 풀어 갑니다.

Spring Boot가 도대체 뭘 해 주는 도구인가요

Spring Boot 는 Spring Framework 위에 올라가는 "빠르게 프로덕션급 자바 애플리케이션을 띄우게 해주는 프레임워크" 입니다. 핵심 철학을 한 줄로 풀면 — Convention over Configuration(관습이 설정보다 우선) 이에요.

회사 비유로 풀어 봅시다. 새 회사를 차린다고 칠 때, 모든 시스템(전화·이메일·인사·회계·출입 보안·인터넷)을 직접 세팅하면 한 달이 걸려요. 그런데 누군가가 "보통 회사들이 다 이렇게 쓰니까 이 패키지대로 자동 세팅해 줄게요"라고 해 주면 한 시간이면 끝나죠. Spring Boot가 자바 백엔드 분야에서 그 역할을 합니다.

핵심 동작 한 줄 — 클래스패스(라이브러리 묶음)에 어떤 라이브러리가 있는지 보고 그에 맞는 빈을 자동으로 만들어 줍니다. 예를 들어 spring-boot-starter-web이 있으면 내장 Tomcat 서버와 Spring MVC를 자동으로 세팅해요. spring-boot-starter-data-jpa가 있으면 Hibernate·JPA 빈이 자동으로 만들어집니다.

Spring Boot 3는 한 가지 큰 변화가 있어요 — Java 17 이상 + Jakarta EE 10 기반으로 바뀌었습니다. 기존 javax. 패키지가 모두 jakarta.로 마이그레이션됐어요. Spring Boot 2에서 3으로 올라갈 때 가장 흔한 빌드 에러가 이 import 변경입니다. 더 자세한 마이그레이션 가이드는 Spring Boot 공식 문서에서 확인할 수 있어요.

@SpringBootApplication — 모든 시작점

Spring Boot 프로젝트의 메인 클래스에 단 하나만 박는 어노테이션이 @SpringBootApplication 입니다. 사실 이건 세 가지 어노테이션의 조합이에요.

구성 어노테이션역할
@SpringBootConfiguration@Configuration을 포함, 설정 클래스로 등록
@EnableAutoConfiguration클래스패스를 스캔해 사용 가능한 라이브러리를 찾고 자동 빈 생성
@ComponentScan@Component·@Service·@Repository·@Controller가 붙은 클래스 자동 빈 등록
// Spring Boot 메인 클래스 — 이 한 줄이 다 합니다
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

여기서 시험 함정이 하나 있어요. @SpringBootApplication이 자동 설정을 어떻게 하는지 디버깅하려면 — application.propertiesdebug=true를 추가하면 됩니다. 어떤 자동 설정이 적용됐고 어떤 게 제외됐는지 상세 리포트가 콘솔에 찍혀요. 이걸 모르면 마법 같은 동작이 영원히 안 보입니다.

자동 설정의 조건부 동작은 @Conditional 계열로 제어돼요. @ConditionalOnClass(DataSource.class)는 클래스패스에 DataSource 클래스가 있을 때만 빈을 만든다는 뜻이고, Spring Boot의 모든 자동 설정 클래스가 이런 조건부 어노테이션으로 깐깐하게 제어돼 있습니다.

> 한 줄 정리 — @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan. 자동 설정 마법은 debug=true로 들여다볼 수 있다.

IoC 컨테이너 — 회사 직원 관리 부서

여기서 Spring의 가장 핵심 개념이 등장해요. IoC(Inversion of Control, 제어의 역전) 입니다.

이름이 어렵지만 비유로 풀면 단순해요. 옛날 자바 프로그래밍에선 우리가 직접 new 키워드로 객체를 만들고 의존성을 연결했어요. 회사로 치면 "내가 일할 사람을 직접 채용해서 직접 데려와 같이 일한다" 는 식이죠.

IoC는 그 반대예요. "회사 직원 관리 부서(IoC 컨테이너)가 알아서 채용·관리·배정" 하고, 우리는 그저 "이런 사람이 필요해요"라고 신청만 하면 부서가 알맞은 사람을 보내줍니다. 객체 생성·생명주기 관리·의존성 연결 — 이 모든 게 컨테이너의 책임이에요.

Spring IoC 컨테이너는 두 가지 핵심 인터페이스로 동작합니다.

인터페이스특징
BeanFactory기본 IoC 컨테이너, 빈 지연 로딩(Lazy Loading)
ApplicationContextBeanFactory 확장 + 이벤트·국제화·AOP 통합

Spring Boot는 항상 ApplicationContext를 씁니다. 직접 빈을 꺼내야 한다면(일반적으로 권장 X) 이런 식이에요.

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(MyApplication.class, args);
        OrderService orderService = ctx.getBean(OrderService.class);
        orderService.doSomething();
    }
}

IoC 컨테이너는 빈의 생명주기까지 관리해요. 빈이 생성될 때 @PostConstruct가 붙은 메서드를 실행하고, 컨테이너가 종료될 때 @PreDestroy 메서드를 실행합니다. 회사로 치면 입사할 때 오리엔테이션, 퇴사할 때 인수인계 같은 거예요.

DI — 외부에서 직원 받아오기

DI(Dependency Injection, 의존성 주입) 는 IoC를 구현하는 주요 방법이에요. 객체가 필요로 하는 다른 객체(의존성)를 스스로 만들지 않고 외부에서 받아오는 패턴입니다.

회사 비유로 — 부서장이 "내가 일하려면 회계 담당, 디자이너, 개발자가 필요해요"라고 신청서를 쓰면 인사부가 알맞은 사람을 보내주는 식이에요. 부서장은 회계 담당이 누구인지 직접 채용하지 않습니다.

Spring DI 방법은 세 가지예요. 각각의 자리가 다릅니다.

생성자 주입 — 가장 권장되는 방식

입사 첫날 명찰·책상·도구를 한꺼번에 다 받고 시작하는 식이에요. 한 번 받으면 그 후로 못 바꿉니다(final).

@Service
public class OrderService {
    // final로 불변성 보장
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    // @Autowired는 생성자 하나일 때 생략 가능 (Spring 4.3+)
    public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }

    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }
}

장점이 셋 — final 불변성 보장, 컴파일 타임 의존성 검증, Spring 없이도 new로 단위 테스트 가능.

세터 주입 — 선택적 의존성

@Service
public class NotificationService {
    private EmailService emailService;

    @Autowired(required = false)  // 선택적 의존성
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

이메일 서비스가 있을 수도, 없을 수도 있는 선택적 의존성에 어울려요. 하지만 일반 개발에서는 거의 안 씁니다.

필드 주입 — 간단하지만 함정 많음

@Service
public class UserService {
    @Autowired  // 프로덕션 코드에서는 사용 자제
    private UserRepository userRepository;
}

코드가 짧긴 한데 — 단위 테스트에 리플렉션이 필요해 어렵고, 순환 의존성이 시작 시점에 안 잡혀 런타임에 터지고, final 불가. 이래서 권장되지 않습니다.

여기서 시험 함정이 하나 있어요. Spring 공식 문서·여러 책·실무 모두 생성자 주입을 강력 권장합니다. "필드 주입이 더 짧으니 좋지 않냐"는 의견이 있을 수 있는데, 짧은 만큼 위에 적은 함정 셋이 따라옵니다. 결정적으로 — 생성자 주입은 Spring 컨테이너 없이도 객체 생성·테스트가 가능해요.

> 한 줄 정리 — DI 3가지 중 생성자 주입이 정답. final + 컴파일 타임 검증 + 테스트 용이성 셋 다 잡힙니다.

Bean 스코프 — 직원 고용 형태 5가지

빈은 고용 형태가 다섯 가지 있어요. 각자 인스턴스 생성·소멸 시점이 달라요.

스코프비유인스턴스 수사용처
singleton (기본)정규직컨테이너당 1개대부분의 Service·Repository
prototype필요할 때마다 신규 채용요청마다 새로상태 가지는 임시 객체
request한 번 손님 응대하고 떠나는 임시 직원요청당 1개 (웹)요청별 상태
session한 세션 담당 직원세션당 1개 (웹)사용자 세션 상태
application회사 전체 안내 데스크ServletContext 1개앱 전역 상태
// Singleton (기본값)
@Component
// @Scope("singleton") 생략 가능
public class SingletonService {
    // 모든 요청에서 같은 인스턴스 공유 — 스레드 안전 필수!
}

// Prototype
@Component
@Scope("prototype")
public class PrototypeService {
    private int requestCount = 0;
}

// Request 스코프 (웹 환경 전용)
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String requestId = UUID.randomUUID().toString();
}

여기서 정말 중요한 시험 함정 — Singleton 빈이 Prototype 빈을 의존할 때입니다. Singleton은 한 번만 만들어지니까 Prototype도 최초 한 번만 주입돼서 사실상 Singleton처럼 동작해요. 이걸 풀려면 ApplicationContext.getBean()을 매번 호출하거나 @Lookup 어노테이션을 써야 합니다.

> 한 줄 정리 — 스코프 5종 중 90%는 singleton. 다른 스코프 쓰는 건 명확한 이유가 있을 때만.

스테레오타입 어노테이션 — 부서 명패

@Component 계열 어노테이션은 모두 "이 클래스를 빈으로 등록해 주세요"라는 뜻이에요. 다만 역할별로 명패를 따로 둡니다.

// 프레젠테이션 레이어 — HTTP 요청 처리
@Controller
public class HomeController {
    @GetMapping("/")
    public String home() { return "index"; }
}

// REST API — @Controller + @ResponseBody
@RestController
@RequestMapping("/api")
public class ApiController {
    @GetMapping("/hello")
    public String hello() { return "Hello, World!"; }
}

// 비즈니스 로직 레이어
@Service
public class UserService {
    // 트랜잭션·비즈니스 규칙
}

// 데이터 접근 레이어 — SQL 예외를 DataAccessException으로 변환
@Repository
public class UserRepository {
    // 데이터베이스 CRUD
}

// 일반 Spring 관리 컴포넌트
@Component
public class UtilityHelper {
    // 범용 유틸리티
}

여기서 시험 함정이 하나 있어요. @Repository만 특별한 추가 기능이 있어요. SQL/JPA 예외를 Spring의 DataAccessException 계층으로 자동 변환합니다. 다른 @Service·@Controller는 현재로선 @Component와 기능적으로 같지만, 역할 명시의 의미가 커서 코드 가독성에 큰 도움을 줘요.

비교표:

어노테이션레이어추가 기능
@Component범용없음
@Controller프레젠테이션ViewResolver 처리
@RestControllerREST API@ResponseBody 포함
@Service비즈니스의미적 구분만
@Repository데이터 접근예외 변환

Project Lombok — 자동 보일러플레이트 작성기

자바 클래스를 만들면 getter·setter·생성자·toString·equals·hashCode를 매번 손으로 적어야 해요. 한 클래스에 200줄 중 180줄이 이런 반복 코드일 때가 흔합니다. Lombok은 이걸 어노테이션 한두 개로 자동 생성해 줘요.

회사 비유로 — 자동 명함·자동 인사 카드·자동 출입증 발급기예요. 신입 직원 한 명마다 다 손으로 적던 걸 자동화한 거죠.

// Lombok 없이 일반 자바 클래스 — 너무 길어요
public class ProductWithoutLombok {
    private Long id;
    private String productName;
    private String category;
    private BigDecimal price;

    public ProductWithoutLombok(Long id, String productName, String category, BigDecimal price) {
        this.id = id;
        this.productName = productName;
        this.category = category;
        this.price = price;
    }

    public Long getId() { return id; }
    public String getProductName() { return productName; }
    public String getCategory() { return category; }
    public BigDecimal getPrice() { return price; }

    public void setId(Long id) { this.id = id; }
    public void setProductName(String productName) { this.productName = productName; }
    public void setCategory(String category) { this.category = category; }
    public void setPrice(BigDecimal price) { this.price = price; }

    @Override public String toString() { /* ... */ }
    @Override public boolean equals(Object o) { /* ... */ }
    @Override public int hashCode() { /* ... */ }
}
// Lombok 적용 — 훨씬 간결!
@Data           // Getter + Setter + ToString + EqualsAndHashCode + RequiredArgsConstructor
@Builder        // 빌더 패턴
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    private Long id;
    private String productName;
    private String category;
    private BigDecimal price;
}

// Builder 패턴 사용
Product product = Product.builder()
        .productName("Sample Product")
        .category("Books")
        .price(new BigDecimal("12.99"))
        .build();

자주 쓰는 어노테이션 모음:

어노테이션생성되는 코드
@Getter / @Setter모든 필드의 getter/setter
@ToStringtoString()
@EqualsAndHashCodeequals()·hashCode()
@NoArgsConstructor기본 생성자 (JPA 엔티티 필수)
@AllArgsConstructor모든 필드 생성자
@RequiredArgsConstructorfinal·@NonNull 필드만 생성자 (생성자 주입 필수템)
@Builder빌더 패턴
@Data@Getter+@Setter+@ToString+@EqualsAndHashCode+@RequiredArgsConstructor
@Slf4jlog 필드 자동 (Logger log = ... 생략)

생성자 주입과 @RequiredArgsConstructor의 조합이 시험·실무에 가장 자주 등장합니다.

@Slf4j
@Service
@RequiredArgsConstructor  // final 필드의 생성자 자동 생성 → 생성자 주입 자동화
public class ProductService {
    private final ProductRepository productRepository;  // 생성자 주입

    public Product createProduct(ProductRequest request) {
        log.info("Creating product: {}", request.getProductName());

        Product product = Product.builder()
                .productName(request.getProductName())
                .category(request.getCategory())
                .price(request.getPrice())
                .build();

        return productRepository.save(product);
    }
}

여기서 시험 함정이 하나 있어요. JPA 엔티티에 @Data를 무분별하게 쓰면 위험합니다. @EqualsAndHashCode가 양방향 관계의 다른 쪽을 참조하면 무한 루프(StackOverflowError)가 납니다. JPA 엔티티에서는 @Getter·@Setter·@NoArgsConstructor만 쓰고, @ToString.Exclude·@EqualsAndHashCode.Exclude로 관계 필드를 빼는 게 정석이에요.

@Getter
@Setter
@NoArgsConstructor  // JPA 스펙 요구사항
@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    @ToString.Exclude        // 무한 루프 방지
    @EqualsAndHashCode.Exclude
    private List<OrderItem> items = new ArrayList<>();
}

DI 3방식 비교 — 시험 단골

구분생성자 주입세터 주입필드 주입
권장도★★★★★★★★
불변성 (final)가능불가불가
필수 의존성컴파일 타임런타임런타임
단위 테스트new로 생성세터로 주입리플렉션 필요
순환 의존성 감지시작 시 즉시런타임런타임
Spring 없이 사용가능가능불가

자주 만나는 함정 5가지

1. @SpringBootApplication 패키지 위치

@SpringBootApplication이 붙은 메인 클래스는 모든 컴포넌트 패키지의 루트 또는 상위에 있어야 컴포넌트 스캔이 됩니다.

잘못된 구조:
com.example.controller.MyController  ← 스캔 안 됨!
com.example.myapp.MyApplication

올바른 구조:
com.example.myapp.MyApplication
com.example.myapp.controller.MyController  ← 스캔됨
com.example.myapp.service.MyService        ← 스캔됨

2. 순환 의존성 (Circular Dependency)

A가 B를 의존하고 B가 A를 의존하는 구조. 생성자 주입을 쓰면 시작 즉시 감지돼서 막아 주지만, 필드 주입은 런타임까지 미뤄집니다. 해결법은 설계를 단방향으로 다시 짜거나 공통 의존성을 별도 클래스로 추출하는 것.

3. Lombok @Data와 JPA 엔티티

위에 언급한 무한 루프 위험. 엔티티에는 @Getter+@Setter+@NoArgsConstructor만.

4. Singleton 빈에 인스턴스 변수 저장

// 잘못된 예 — 상태를 가진 Singleton
@Service
public class BadService {
    private String currentUser;  // 위험! 모든 스레드 공유
}

Singleton은 컨테이너에 한 번만 만들어져서 모든 요청이 공유해요. 인스턴스 변수에 요청별 상태를 저장하면 스레드 간에 데이터가 섞입니다. 상태는 지역 변수로, 영구 데이터는 Repository로.

5. 같은 타입 빈이 여러 개일 때

@Service("sqlUserService")
public class SqlUserServiceImpl implements UserService { }

@Service("ldapUserService")
public class LdapUserServiceImpl implements UserService { }

@Controller
public class UserController {
    @Autowired
    @Qualifier("sqlUserService")  // 명시적 지정
    private UserService userService;
}

@Qualifier로 빈 이름을 지정하거나, 한 빈에 @Primary를 박아 기본 빈으로 정합니다.

application.properties / application.yml — 외부 설정

Spring Boot는 application.properties 또는 application.yml로 다양한 설정을 외부화해요. yml이 계층 표현이 더 명확합니다.

# application.yml
spring:
  application:
    name: product-service
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: ''
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop  # 개발 시에만!
    show-sql: true
  h2:
    console:
      enabled: true

server:
  port: 8080

logging:
  level:
    com.example: DEBUG
    org.springframework.web: DEBUG

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 Spring Boot 1편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Spring Boot = Spring Framework + 자동 설정 + 내장 서버
  • @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
  • 자동 설정 디버깅 = application.propertiesdebug=true
  • Spring Boot 3 = Java 17+ + Jakarta EE 10, javax.jakarta. 마이그레이션
  • IoC = "직원 관리 부서가 알아서" / DI = "외부에서 직원 받아오기"
  • DI 3방식 — 생성자 주입(★★★★★) > 세터 주입(★★★) > 필드 주입(★)
  • 생성자 주입 = final 불변 + 컴파일 타임 검증 + Spring 없이 테스트 가능
  • 빈 스코프 5종 — singleton(기본·정규직) / prototype / request / session / application
  • Singleton에 Prototype 의존 시 함정 — 한 번만 주입돼 사실상 Singleton 동작
  • Singleton 빈에 인스턴스 변수로 상태 저장 X — 스레드 안전 깨짐
  • 스테레오타입 — @Component < @Controller·@Service·@Repository (역할 명시)
  • @Repository만 예외 변환 기능 — SQL 예외를 DataAccessException으로
  • @RestController = @Controller + @ResponseBody
  • @SpringBootApplication컴포넌트 패키지 루트 또는 상위에 위치
  • 순환 의존성 = 생성자 주입 쓰면 시작 즉시 감지, 필드 주입은 런타임에 터짐
  • Lombok 자주 — @Data(범용 DTO) / @Builder / @RequiredArgsConstructor(생성자 주입 자동) / @Slf4j(log 필드)
  • JPA 엔티티에 @Data 금지 — 무한 루프 위험. @Getter+@Setter+@NoArgsConstructor만, 관계 필드는 @ToString.Exclude
  • 같은 타입 빈 여러 개 — @Qualifier로 빈 이름 지정 또는 @Primary로 기본 지정

시리즈 다른 편

같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!