Boot 실전 가이드
Boot의 핵심 개념과 실무 활용
학습 항목
이미지 로딩 중...
Spring Boot 3.0 시작하기 완벽 가이드
Spring Boot 3.0의 핵심 개념부터 실전 프로젝트 구조까지, 초보 개발자를 위한 친절한 안내서입니다. 최신 Java 17 기반의 Spring Boot 3.0으로 현대적인 웹 애플리케이션을 만드는 방법을 단계별로 배워보세요.
목차
- Spring Boot란 무엇인가 - 스프링 부트의 탄생 배경과 철학
- 의존성 주입(Dependency Injection) - 스프링의 핵심 원리
- 자동 설정(Auto Configuration) - Spring Boot의 마법
- 스타터 의존성(Starter Dependencies) - 라이브러리 관리의 혁신
- REST API 개발 - 실무 중심의 RESTful 엔드포인트 만들기
- application.properties 설정 - 환경별 설정 관리
- 예외 처리(Exception Handling) - 우아한 에러 응답 만들기
- 테스트 작성(Testing) - 안정적인 코드를 위한 필수 과정
- Actuator - 프로덕션 모니터링과 관리
- 프로파일과 환경 분리 - 개발/스테이징/프로덕션 관리
1. Spring Boot란 무엇인가 - 스프링 부트의 탄생 배경과 철학
시작하며
여러분이 처음 Java로 웹 애플리케이션을 만들려고 할 때, XML 설정 파일 수십 개를 만들고 라이브러리 버전 충돌을 해결하느라 며칠을 보낸 경험이 있나요? 아니면 "Hello World" 하나 출력하는데 수백 줄의 설정 코드를 작성해야 했던 적은요?
이런 복잡성은 전통적인 Spring Framework의 가장 큰 진입 장벽이었습니다. 강력한 기능을 제공하지만, 초기 설정과 구성에 너무 많은 시간과 노력이 필요했죠.
특히 초보 개발자들에게는 실제 비즈니스 로직을 작성하기도 전에 지쳐버리는 경우가 많았습니다. 바로 이런 문제를 해결하기 위해 탄생한 것이 Spring Boot입니다.
"설정보다 관례(Convention over Configuration)"라는 철학으로, 복잡한 설정 없이도 바로 실행 가능한 애플리케이션을 만들 수 있게 해줍니다.
개요
간단히 말해서, Spring Boot는 Spring Framework를 더 쉽고 빠르게 사용할 수 있도록 만든 프레임워크입니다. 전통적인 Spring으로 웹 애플리케이션을 만들려면 톰캣 설정, 스프링 MVC 설정, 데이터베이스 연결 설정 등을 일일이 해야 했습니다.
하지만 Spring Boot를 사용하면 이 모든 것이 자동으로 설정됩니다. 예를 들어, JPA 라이브러리만 추가하면 데이터베이스 연결부터 엔티티 매니저 설정까지 자동으로 처리되죠.
기존에는 WAR 파일을 만들어 별도의 톰캣 서버에 배포했다면, 이제는 JAR 파일 하나로 내장 서버까지 포함된 독립 실행형 애플리케이션을 만들 수 있습니다. Spring Boot의 핵심 특징은 세 가지입니다.
첫째, 자동 설정(Auto Configuration)으로 개발자가 최소한의 설정만 하면 됩니다. 둘째, 스타터 의존성(Starter Dependencies)으로 관련 라이브러리들을 한 번에 가져올 수 있습니다.
셋째, 내장 서버(Embedded Server)로 별도의 서버 설치 없이 바로 실행할 수 있습니다. 이러한 특징들이 개발 생산성을 극적으로 향상시켜줍니다.
코드 예제
// Spring Boot 3.0 기본 애플리케이션 - 단 몇 줄로 웹 서버 실행!
@SpringBootApplication
public class MyFirstApplication {
// main 메서드만으로 전체 애플리케이션 실행
public static void main(String[] args) {
SpringApplication.run(MyFirstApplication.class, args);
}
// 간단한 REST API 엔드포인트 생성
@RestController
class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, Spring Boot 3.0!";
}
}
}
설명
이것이 하는 일: 위 코드는 단 15줄로 완전히 작동하는 웹 서버를 만들고, REST API 엔드포인트까지 제공합니다. 첫 번째로, @SpringBootApplication 어노테이션이 핵심입니다.
이 하나의 어노테이션은 실제로 세 가지 역할을 동시에 수행합니다. @Configuration으로 이 클래스를 설정 클래스로 지정하고, @EnableAutoConfiguration으로 스프링 부트의 자동 설정 기능을 활성화하며, @ComponentScan으로 현재 패키지 이하의 모든 컴포넌트를 스캔합니다.
전통적인 Spring이었다면 이 세 가지를 각각 설정했어야 하지만, Spring Boot는 하나로 통합했습니다. 그 다음으로, SpringApplication.run() 메서드가 실행되면서 마법이 일어납니다.
클래스패스를 스캔해서 필요한 라이브러리를 찾고, 톰캣 서버를 내장 모드로 시작하며, 스프링 컨테이너를 초기화하고, 모든 빈을 등록합니다. 이 모든 과정이 자동으로 진행되며, 기본 포트 8080으로 서버가 구동됩니다.
마지막으로, @RestController와 @GetMapping 어노테이션으로 간단한 API를 만들었습니다. /hello 경로로 GET 요청이 오면 "Hello, Spring Boot 3.0!" 문자열을 JSON 형태로 반환합니다.
별도의 뷰 리졸버나 메시지 컨버터 설정 없이도 자동으로 HTTP 응답이 처리됩니다. 여러분이 이 코드를 실행하면 몇 초 만에 완전한 웹 서버가 구동되고, 브라우저에서 http://localhost:8080/hello로 접속하면 즉시 결과를 확인할 수 있습니다.
전통적인 방식이었다면 수백 줄의 XML 설정과 여러 설정 파일이 필요했겠지만, Spring Boot는 이 모든 것을 자동화했습니다. 이것이 바로 Spring Boot가 "opinionated framework(주관이 있는 프레임워크)"라고 불리는 이유입니다.
최선의 실무 관행을 기본값으로 제공하여 개발자가 비즈니스 로직에만 집중할 수 있게 해줍니다.
실전 팁
💡 Spring Boot 3.0은 Java 17 이상을 필수로 요구합니다. 프로젝트 시작 전에 JDK 버전을 반드시 확인하세요. Java 11로는 실행되지 않습니다!
💡 application.properties 또는 application.yml 파일에서 포트 번호, 데이터베이스 설정 등을 쉽게 변경할 수 있습니다. 예: server.port=9090으로 포트 변경
💡 개발 중에는 spring-boot-devtools 의존성을 추가하면 코드 변경 시 자동으로 재시작되어 생산성이 크게 향상됩니다
💡 @SpringBootApplication 어노테이션은 반드시 루트 패키지에 위치해야 합니다. 하위 패키지의 모든 컴포넌트를 스캔하기 때문입니다
💡 Spring Initializer(https://start.spring.io)를 사용하면 필요한 의존성이 포함된 프로젝트를 30초 안에 생성할 수 있습니다
2. 의존성 주입(Dependency Injection) - 스프링의 핵심 원리
시작하며
여러분이 대형 쇼핑몰 애플리케이션을 개발한다고 상상해보세요. 주문 처리 클래스가 결제 서비스, 재고 서비스, 알림 서비스 등 수십 개의 다른 클래스들을 사용해야 합니다.
전통적인 방식이라면 이 모든 객체를 직접 new 키워드로 생성하고 관리해야 했을 것입니다. 이런 방식의 문제점은 명확합니다.
코드 간 결합도가 높아져서 테스트가 어렵고, 한 곳을 수정하면 연쇄적으로 여러 곳을 수정해야 하며, 객체의 생명주기를 일일이 관리해야 합니다. 특히 결제 서비스를 테스트용으로 바꾸고 싶을 때, 모든 코드를 수정해야 하는 악몽 같은 상황이 벌어지죠.
바로 이럴 때 필요한 것이 의존성 주입(Dependency Injection)입니다. 객체의 생성과 관리를 Spring 컨테이너에 맡기고, 필요한 곳에 자동으로 주입받는 방식으로 이 모든 문제를 해결합니다.
개요
간단히 말해서, 의존성 주입은 객체가 필요로 하는 다른 객체를 직접 만들지 않고 외부(Spring 컨테이너)로부터 받아오는 설계 패턴입니다. 전통적인 개발에서는 PaymentService paymentService = new PaymentService()처럼 직접 객체를 생성했습니다.
하지만 Spring Boot에서는 @Autowired나 생성자 주입을 통해 Spring이 자동으로 생성하고 관리하는 객체를 받아옵니다. 예를 들어, 테스트 환경에서는 실제 결제 대신 가짜 결제 서비스를 주입받아 안전하게 테스트할 수 있습니다.
기존에는 객체 간 의존관계가 코드에 하드코딩되어 있었다면, 이제는 설정이나 어노테이션으로 유연하게 관리할 수 있습니다. 의존성 주입의 핵심 이점은 세 가지입니다.
첫째, 느슨한 결합(Loose Coupling)으로 코드 변경이 쉬워집니다. 둘째, 테스트가 용이해져서 단위 테스트 작성이 간편합니다.
셋째, 코드의 재사용성과 유지보수성이 향상됩니다. 이러한 특징들이 대규모 엔터프라이즈 애플리케이션 개발에 필수적입니다.
코드 예제
// 주문 서비스 - 생성자 주입 방식 (Spring Boot 권장)
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
// 생성자가 하나면 @Autowired 생략 가능 (Spring Boot 3.0)
public OrderService(PaymentService paymentService,
InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public void processOrder(Order order) {
// 재고 확인 - inventoryService를 직접 생성하지 않고 주입받음
inventoryService.checkStock(order.getProductId());
// 결제 처리 - paymentService도 자동 주입됨
paymentService.processPayment(order.getAmount());
}
}
설명
이것이 하는 일: 이 코드는 주문 처리에 필요한 다른 서비스들을 Spring이 자동으로 찾아서 주입해주는 구조를 보여줍니다. 첫 번째로, @Service 어노테이션이 이 클래스를 Spring의 관리 대상(Bean)으로 등록합니다.
Spring Boot가 시작될 때 클래스패스를 스캔하면서 이 어노테이션을 발견하고, OrderService 객체를 하나 생성해서 컨테이너에 보관합니다. 이 객체는 싱글톤 패턴으로 관리되어 애플리케이션 전체에서 하나의 인스턴스만 존재합니다.
그 다음으로, 생성자에서 두 개의 서비스를 파라미터로 받습니다. Spring Boot 3.0부터는 생성자가 하나뿐이라면 @Autowired 어노테이션을 생략할 수 있습니다.
Spring 컨테이너가 OrderService를 생성할 때, 필요한 PaymentService와 InventoryService를 자동으로 찾아서 생성자에 전달합니다. 만약 이 서비스들이 아직 생성되지 않았다면 먼저 생성하고, 순환 참조가 있다면 에러를 발생시켜 문제를 조기에 발견할 수 있습니다.
마지막으로, processOrder 메서드에서는 주입받은 서비스들을 그냥 사용하기만 하면 됩니다. 객체의 생성, 생명주기 관리, 스레드 안전성 등은 모두 Spring이 알아서 처리합니다.
테스트할 때는 실제 서비스 대신 Mock 객체를 생성자에 전달하면 되므로, 단위 테스트 작성이 매우 간단해집니다. 여러분이 이 패턴을 사용하면 코드 간 결합도가 낮아져서 한 서비스를 수정해도 다른 곳에 영향을 주지 않습니다.
또한 final 키워드로 불변성을 보장하여 런타임에 의존성이 바뀌는 것을 방지할 수 있습니다. Spring 공식 문서에서도 생성자 주입을 가장 권장하는 이유가 바로 이러한 안정성과 테스트 용이성 때문입니다.
필드 주입(@Autowired private PaymentService...)보다 훨씬 안전하고 명확한 코드를 작성할 수 있습니다.
실전 팁
💡 항상 생성자 주입을 사용하세요. 필드 주입(@Autowired private)은 테스트가 어렵고 불변성을 보장할 수 없습니다
💡 주입받는 필드는 final로 선언하여 실수로 null이 되는 것을 방지하세요. 컴파일 타임에 에러를 잡을 수 있습니다
💡 순환 참조(A가 B를 주입받고, B가 A를 주입받는 경우)는 설계 문제입니다. 발생하면 설계를 다시 검토하세요
💡 @Autowired(required = false)로 선택적 의존성을 표현할 수 있지만, 가능하면 Optional<T>를 사용하는 것이 더 명확합니다
💡 같은 타입의 빈이 여러 개일 때는 @Qualifier("beanName")로 구체적으로 지정하거나, @Primary로 기본 빈을 설정하세요
3. 자동 설정(Auto Configuration) - Spring Boot의 마법
시작하며
여러분이 데이터베이스를 연결하려고 할 때를 생각해보세요. 전통적인 Spring에서는 DataSource 빈 설정, TransactionManager 설정, EntityManagerFactory 설정 등 수십 개의 빈을 XML이나 Java Config로 정의해야 했습니다.
설정 파일만 수백 줄이 넘어가는 것은 기본이었죠. 이런 반복적인 설정 작업은 프로젝트마다 거의 동일합니다.
MySQL을 사용하든 PostgreSQL을 사용하든, 기본적인 설정 패턴은 비슷합니다. 그런데도 매번 똑같은 설정을 복사-붙여넣기 하거나 처음부터 작성해야 했습니다.
실수라도 하면 애플리케이션이 시작조차 되지 않았죠. 바로 이럴 때 빛을 발하는 것이 Spring Boot의 자동 설정(Auto Configuration)입니다.
클래스패스에 있는 라이브러리를 보고 "아, MySQL을 쓰는구나" 하고 알아서 필요한 모든 설정을 자동으로 해줍니다.
개요
간단히 말해서, 자동 설정은 클래스패스와 정의된 빈을 분석해서 필요한 설정을 자동으로 적용하는 Spring Boot의 핵심 기능입니다. Spring Boot는 수백 개의 자동 설정 클래스를 제공합니다.
예를 들어, spring-boot-starter-data-jpa 의존성을 추가하면 JPA 관련 설정이 자동으로 활성화되고, H2 데이터베이스가 클래스패스에 있으면 자동으로 인메모리 데이터베이스가 구성됩니다. 개발자는 application.properties에서 데이터베이스 URL만 지정하면 됩니다.
기존에는 @Configuration 클래스를 여러 개 만들어서 일일이 빈을 등록했다면, 이제는 Spring Boot가 조건부로 자동 설정을 적용해줍니다. 자동 설정의 핵심 원리는 세 가지입니다.
첫째, @Conditional 어노테이션으로 특정 조건이 만족될 때만 설정이 활성화됩니다. 둘째, spring-boot-autoconfigure 라이브러리에 미리 정의된 설정들이 활용됩니다.
셋째, 개발자가 직접 정의한 빈이 있으면 자동 설정보다 우선순위가 높아서 커스터마이징이 가능합니다. 이러한 원리가 "설정보다 관례"를 구현하면서도 유연성을 잃지 않게 해줍니다.
코드 예제
// application.properties - 단 세 줄로 데이터베이스 연결 완료!
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
// 엔티티 클래스만 정의하면 됨
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// JPA가 자동으로 테이블 생성, CRUD 메서드 제공
}
// Repository 인터페이스만 선언 - 구현체는 Spring Data JPA가 자동 생성
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 메서드 이름만으로 쿼리 자동 생성!
List<User> findByName(String name);
}
설명
이것이 하는 일: 위 코드는 JPA와 데이터베이스 연결에 필요한 수십 개의 빈 설정을 자동으로 처리합니다. 첫 번째로, application.properties에 데이터베이스 연결 정보만 입력하면 Spring Boot가 자동으로 동작합니다.
spring-boot-starter-data-jpa 의존성이 있으면 DataSourceAutoConfiguration이 활성화되어 DataSource 빈을 자동 생성하고, HibernateJpaAutoConfiguration이 EntityManagerFactory와 TransactionManager를 설정합니다. 이 모든 과정이 조건부로 실행되는데, 예를 들어 DataSource 빈이 이미 있으면 자동 설정은 건너뛰어집니다.
그 다음으로, @Entity 어노테이션만으로 User 테이블이 자동 생성됩니다. spring.jpa.hibernate.ddl-auto 속성이 기본값인 create-drop(개발 모드) 또는 update로 설정되어 있으면, 애플리케이션 시작 시 엔티티 클래스를 분석해서 DDL을 자동 실행합니다.
@GeneratedValue도 데이터베이스 방언에 맞게 자동으로 처리되어, MySQL에서는 AUTO_INCREMENT를, PostgreSQL에서는 SEQUENCE를 사용합니다. 마지막으로, JpaRepository를 상속받기만 하면 CRUD 메서드가 자동으로 생김니다.
findByName 같은 메서드 이름은 Spring Data JPA가 분석해서 SELECT * FROM user WHERE name = ? 쿼리로 자동 변환합니다. 구현 클래스를 작성할 필요가 전혀 없습니다.
Spring Boot가 프록시 객체를 런타임에 생성해서 주입해주기 때문이죠. 여러분이 이 방식을 사용하면 코드량이 90% 이상 줄어듭니다.
전통적인 방식이었다면 DAO 클래스, JDBC Template 설정, 트랜잭션 관리 코드 등을 모두 직접 작성해야 했을 것입니다. 하지만 Spring Boot는 "합리적인 기본값"을 제공하여 대부분의 경우 추가 설정 없이 바로 사용할 수 있습니다.
물론 프로덕션 환경에서는 spring.jpa.hibernate.ddl-auto=validate로 변경하고, 커넥션 풀 설정을 튜닝하는 등의 커스터마이징이 필요하지만, 기본 설정만으로도 충분히 동작합니다. 이것이 바로 Spring Boot가 스타트업부터 대기업까지 사랑받는 이유입니다.
실전 팁
💡 --debug 옵션으로 애플리케이션을 실행하면 어떤 자동 설정이 적용되었는지 상세 로그를 볼 수 있습니다
💡 특정 자동 설정을 비활성화하려면 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 사용하세요
💡 spring-boot-configuration-processor 의존성을 추가하면 IDE에서 application.properties 자동완성을 지원합니다
💡 프로덕션에서는 반드시 spring.jpa.hibernate.ddl-auto=validate로 설정하세요. create나 update는 데이터 손실 위험이 있습니다
💡 자동 설정의 조건을 확인하려면 @ConditionalOnClass, @ConditionalOnMissingBean 등의 어노테이션을 공부하세요
4. 스타터 의존성(Starter Dependencies) - 라이브러리 관리의 혁신
시작하며
여러분이 웹 애플리케이션을 만들기 위해 Maven이나 Gradle에 의존성을 추가한다고 생각해보세요. Spring MVC, Jackson(JSON 처리), Hibernate Validator, Tomcat, Logging 라이브러리 등 수십 개를 일일이 찾아서 추가해야 합니다.
더 큰 문제는 버전 호환성입니다. Spring 5.3과 Hibernate 5.6은 호환되지만 5.4는 문제가 생긴다는 식의 정보를 일일이 확인해야 했죠.
이런 의존성 지옥(Dependency Hell)은 모든 Java 개발자의 악몽이었습니다. 라이브러리 하나를 추가했더니 다른 라이브러리와 충돌해서 애플리케이션이 시작되지 않거나, 런타임에 ClassNotFoundException이 발생하는 경우가 부지기수였습니다.
특히 초보 개발자들은 어떤 라이브러리가 필요한지조차 모르는 경우가 많았습니다. 바로 이럴 때 구세주가 되는 것이 Spring Boot의 스타터 의존성입니다.
spring-boot-starter-web 하나만 추가하면 웹 개발에 필요한 모든 라이브러리가 호환되는 버전으로 자동 추가됩니다.
개요
간단히 말해서, 스타터 의존성은 특정 기능에 필요한 모든 라이브러리를 하나로 묶어서 제공하는 편리한 의존성 묶음입니다. Spring Boot는 40개 이상의 공식 스타터를 제공합니다.
spring-boot-starter-web은 Spring MVC, Tomcat, Jackson 등 웹 개발에 필요한 15개 이상의 라이브러리를 포함합니다. spring-boot-starter-data-jpa는 Hibernate, Spring Data JPA, JDBC 등 데이터베이스 작업에 필요한 라이브러리들을 묶어줍니다.
예를 들어, REST API 서버를 만든다면 starter-web 하나면 충분하고, 보안이 필요하면 starter-security를 추가하기만 하면 됩니다. 기존에는 pom.xml이나 build.gradle에 수십 줄의 의존성을 작성하고 버전을 일일이 맞춰야 했다면, 이제는 필요한 스타터 몇 개만 선언하면 됩니다.
스타터 의존성의 핵심 이점은 세 가지입니다. 첫째, 버전 호환성이 보장됩니다.
Spring Boot 팀이 테스트한 조합만 제공하기 때문이죠. 둘째, 필요한 라이브러리를 찾는 시간이 절약됩니다.
웹 개발에 뭐가 필요한지 고민할 필요 없이 starter-web만 추가하면 됩니다. 셋째, 의존성 충돌이 극적으로 줄어듭니다.
스타터들끼리는 서로 호환되도록 설계되어 있습니다. 이러한 이점들이 초보자의 진입 장벽을 낮추고 전문가의 생산성을 높여줍니다.
코드 예제
// build.gradle - Gradle 설정 예시
dependencies {
// 웹 애플리케이션 개발 - Spring MVC, Tomcat, JSON 처리 등 모두 포함
implementation 'org.springframework.boot:spring-boot-starter-web'
// 데이터베이스 연동 - JPA, Hibernate, JDBC 등 포함
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 보안 기능 - Spring Security, 인증/인가 등 포함
implementation 'org.springframework.boot:spring-boot-starter-security'
// 테스트 도구 - JUnit, Mockito, AssertJ 등 포함
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// MySQL 드라이버만 추가로 필요
runtimeOnly 'com.mysql:mysql-connector-j'
}
// 버전은 명시하지 않아도 됨 - Spring Boot BOM에서 자동 관리
설명
이것이 하는 일: 위 코드는 단 5줄의 의존성 선언으로 수십 개의 라이브러리를 프로젝트에 추가합니다. 첫 번째로, spring-boot-starter-web을 추가하면 전이 의존성(Transitive Dependencies)으로 많은 라이브러리가 딸려옵니다.
spring-web, spring-webmvc(핵심 MVC 기능), tomcat-embed-core(내장 톰캣), jackson-databind(JSON 직렬화), spring-boot-starter-json, spring-boot-starter-logging(로깅) 등이 자동으로 추가됩니다. 이 모든 라이브러리의 버전은 Spring Boot의 BOM(Bill of Materials)에서 관리되므로, 개발자가 버전을 명시할 필요가 없습니다.
Spring Boot 3.0.0을 사용하면 모든 스타터가 3.0.0 버전에 맞게 조정됩니다. 그 다음으로, starter-data-jpa는 데이터베이스 작업에 필요한 모든 것을 제공합니다.
spring-data-jpa(Repository 추상화), hibernate-core(JPA 구현체), spring-jdbc(저수준 JDBC), jakarta.persistence-api(JPA 스펙) 등이 포함됩니다. 주목할 점은 실제 데이터베이스 드라이버(MySQL, PostgreSQL 등)는 포함되지 않는다는 것입니다.
이는 개발자가 선택할 수 있도록 의도적으로 빠뜨린 것이죠. 마지막으로, starter-test는 테스트 작성에 필요한 도구들을 한꺼번에 가져옵니다.
JUnit 5(테스트 프레임워크), Mockito(Mock 객체), AssertJ(유창한 단언문), Hamcrest(매처), Spring Test(통합 테스트) 등이 모두 testImplementation 스코프로 추가됩니다. 덕분에 별도의 테스트 라이브러리를 찾아 헤맬 필요가 없습니다.
여러분이 스타터를 사용하면 pom.xml이나 build.gradle 파일이 극도로 간결해집니다. 전통적인 프로젝트가 50-100줄의 의존성을 가진 반면, Spring Boot 프로젝트는 5-10줄이면 충분합니다.
또한 Spring Boot 버전만 업그레이드하면 모든 라이브러리가 함께 업그레이드되므로 유지보수가 간편합니다. 물론 특정 라이브러리의 버전을 개별적으로 오버라이드할 수도 있지만, 대부분의 경우 기본값을 사용하는 것이 안전합니다.
공식 스타터 외에도 서드파티 라이브러리들이 xxx-spring-boot-starter 형식으로 자체 스타터를 제공하는 추세라, 생태계 전체가 이 패턴을 따르고 있습니다.
실전 팁
💡 ./gradlew dependencies 또는 mvn dependency:tree 명령으로 실제로 어떤 라이브러리가 추가되었는지 확인할 수 있습니다
💡 특정 라이브러리를 제외하고 싶다면 exclude 사용: implementation('starter-web') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' }
💡 내장 Tomcat 대신 Jetty나 Undertow를 쓰려면 starter-tomcat을 제외하고 starter-jetty를 추가하세요
💡 spring-boot-dependencies BOM을 직접 import하면 스타터가 아닌 개별 라이브러리도 버전 관리를 받을 수 있습니다
💡 커스텀 스타터를 만들 수도 있습니다. 사내 공통 설정을 스타터로 묶으면 모든 팀이 일관된 설정을 사용할 수 있습니다
5. REST API 개발 - 실무 중심의 RESTful 엔드포인트 만들기
시작하며
여러분이 모바일 앱이나 프론트엔드 개발자에게 데이터를 제공하는 API 서버를 만든다고 상상해보세요. 사용자 목록 조회, 신규 사용자 등록, 사용자 정보 수정, 삭제 등의 기능이 필요합니다.
전통적인 Servlet 방식이라면 각 요청마다 클래스를 만들고, web.xml에 매핑을 등록하고, JSON 변환 로직을 직접 작성해야 했을 것입니다. 이런 방식의 문제는 코드 중복이 심하고, HTTP 메서드(GET, POST, PUT, DELETE)와 URL 구조를 일관성 있게 관리하기 어렵다는 것입니다.
특히 에러 처리, 유효성 검증, 상태 코드 관리 등을 매번 수동으로 처리해야 해서 실수하기 쉬웠죠. 바로 이럴 때 Spring Boot의 REST API 기능이 빛을 발합니다.
@RestController와 @RequestMapping 어노테이션만으로 깔끔하고 표준적인 RESTful API를 몇 분 만에 만들 수 있습니다.
개요
간단히 말해서, REST API는 HTTP 프로토콜을 활용해서 자원(Resource)을 CRUD하는 웹 서비스 아키텍처이며, Spring Boot는 이를 매우 쉽게 구현할 수 있게 해줍니다. REST의 핵심 원칙은 자원을 URL로 표현하고, HTTP 메서드로 행위를 표현하는 것입니다.
예를 들어, GET /users/123은 123번 사용자 조회, POST /users는 사용자 생성, PUT /users/123은 수정, DELETE /users/123은 삭제를 의미합니다. Spring Boot에서는 @GetMapping, @PostMapping 등으로 이를 직관적으로 표현합니다.
기존에는 Jackson 라이브러리를 수동으로 설정하고, ObjectMapper로 객체를 JSON으로 변환했다면, 이제는 메서드에서 객체만 반환하면 자동으로 JSON으로 변환됩니다. Spring Boot REST API의 핵심 이점은 세 가지입니다.
첫째, 어노테이션 기반으로 코드가 간결하고 읽기 쉽습니다. 둘째, 자동 JSON 직렬화/역직렬화로 변환 로직을 작성할 필요가 없습니다.
셋째, @Valid와 Bean Validation으로 입력 검증이 선언적으로 처리됩니다. 이러한 특징들이 API 개발 속도를 크게 향상시켜줍니다.
코드 예제
// REST API 컨트롤러 - CRUD 엔드포인트 구현
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// 전체 사용자 조회 - GET /api/users
@GetMapping
public List<User> getAllUsers() {
return userService.findAll(); // 자동으로 JSON 배열로 변환
}
// 특정 사용자 조회 - GET /api/users/123
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404 Not Found
}
// 신규 사용자 생성 - POST /api/users
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 201 Created
public User createUser(@Valid @RequestBody User user) {
return userService.save(user); // @Valid로 자동 검증
}
// 사용자 수정 - PUT /api/users/123
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}
// 사용자 삭제 - DELETE /api/users/123
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204 No Content
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
설명
이것이 하는 일: 이 컨트롤러는 사용자 자원에 대한 모든 CRUD 작업을 RESTful하게 처리하는 완전한 API를 제공합니다. 첫 번째로, @RestController는 @Controller와 @ResponseBody를 합친 것으로, 모든 메서드의 반환값이 HTTP 응답 본문에 직접 작성됩니다.
@RequestMapping("/api/users")는 클래스 레벨에 선언되어 모든 메서드의 기본 경로가 됩니다. 따라서 @GetMapping은 실제로 GET /api/users에 매핑되고, @GetMapping("/{id}")는 GET /api/users/{id}에 매핑됩니다.
이런 구조로 URL이 일관성 있게 관리됩니다. 그 다음으로, 각 메서드는 적절한 HTTP 메서드와 매핑됩니다.
getUserById 메서드는 @PathVariable로 URL의 {id} 부분을 파라미터로 받아옵니다. ResponseEntity를 사용하면 상태 코드를 세밀하게 제어할 수 있어서, 사용자가 존재하면 200 OK, 없으면 404 Not Found를 반환합니다.
createUser 메서드는 @RequestBody로 HTTP 요청 본문의 JSON을 User 객체로 자동 변환받고, @Valid로 Bean Validation을 수행합니다. 만약 이메일 형식이 잘못되었다면 400 Bad Request가 자동으로 반환됩니다.
마지막으로, 각 메서드는 서비스 레이어에 위임하여 실제 비즈니스 로직을 처리합니다. 컨트롤러는 HTTP 요청/응답 처리만 담당하고, 실제 데이터 처리는 서비스와 리포지토리에 맡기는 것이 레이어드 아키텍처의 핵심입니다.
deleteUser는 @ResponseStatus(HttpStatus.NO_CONTENT)로 204 상태 코드를 반환하는데, 이는 "요청은 성공했지만 응답 본문은 없다"는 REST 규약을 따른 것입니다. 여러분이 이 패턴을 사용하면 API가 표준화되어 프론트엔드 개발자가 쉽게 이해하고 사용할 수 있습니다.
URL 구조만 봐도 어떤 동작을 하는지 명확하고(/api/users/123 = 123번 사용자), HTTP 메서드로 의도가 분명하게 드러납니다(GET = 조회, POST = 생성). 또한 Spring Boot가 제공하는 에러 처리 메커니즘 덕분에, 예외가 발생하면 적절한 JSON 에러 응답이 자동으로 생성됩니다.
Postman이나 Swagger 같은 도구로 쉽게 테스트할 수 있고, 모바일 앱이나 SPA(Single Page Application)에서 바로 호출할 수 있는 실전 API입니다. 실무에서는 여기에 인증/인가, 페이징, 정렬, 필터링 등을 추가하지만, 기본 구조는 동일합니다.
실전 팁
💡 @RestControllerAdvice로 전역 예외 처리를 구현하면 모든 API에서 일관된 에러 응답을 제공할 수 있습니다
💡 페이징은 Pageable 파라미터를 사용하세요: @GetMapping public Page<User> getUsers(Pageable pageable)
💡 CORS 설정은 @CrossOrigin 또는 WebMvcConfigurer로 처리하세요. 프론트엔드와 다른 도메인에서 호출할 때 필수입니다
💡 버전 관리는 URL에 포함하세요: /api/v1/users, /api/v2/users 형식으로 호환성을 유지하세요
💡 민감한 데이터는 DTO(Data Transfer Object)로 변환해서 반환하세요. 엔티티를 직접 반환하면 비밀번호 같은 정보가 노출될 수 있습니다
6. application.properties 설정 - 환경별 설정 관리
시작하며
여러분이 로컬 개발 환경에서는 H2 인메모리 데이터베이스를 쓰고, 테스트 서버에서는 개발용 MySQL을, 프로덕션에서는 AWS RDS를 사용한다고 상상해보세요. 각 환경마다 데이터베이스 URL, 포트 번호, 로그 레벨 등이 다릅니다.
전통적인 방식이라면 환경마다 다른 설정 파일을 만들고 배포 시 교체해야 했을 것입니다. 이런 방식의 문제점은 실수로 프로덕션 설정으로 로컬에서 실행하거나, 반대로 개발 설정으로 프로덕션을 배포하는 사고가 발생할 수 있다는 것입니다.
또한 설정이 코드에 하드코딩되면 보안 문제도 발생합니다. 데이터베이스 비밀번호가 Git에 커밋되는 것처럼요.
바로 이럴 때 Spring Boot의 application.properties와 프로파일 기능이 구세주가 됩니다. 환경별로 다른 설정 파일을 만들고, 실행 시 프로파일만 지정하면 자동으로 적절한 설정이 적용됩니다.
개요
간단히 말해서, application.properties는 Spring Boot 애플리케이션의 모든 설정을 관리하는 중앙 집중식 설정 파일이며, 프로파일 기능으로 환경별 설정을 분리할 수 있습니다. Spring Boot는 src/main/resources 디렉토리의 application.properties 또는 application.yml을 자동으로 로드합니다.
여기에 서버 포트, 데이터베이스 연결 정보, 로깅 설정 등 모든 것을 키-값 형태로 작성합니다. 예를 들어, server.port=9090만 추가하면 기본 8080 포트 대신 9090 포트를 사용합니다.
기존에는 XML이나 Java Config 클래스로 설정을 관리했다면, 이제는 간단한 텍스트 파일로 모든 것을 제어할 수 있습니다. application.properties의 핵심 기능은 세 가지입니다.
첫째, 프로파일별 설정 파일(application-dev.properties, application-prod.properties)로 환경을 분리합니다. 둘째, 시스템 환경변수나 커맨드라인 인자로 설정을 오버라이드할 수 있어 배포 시 유연성이 높습니다.
셋째, @ConfigurationProperties로 타입 안전하게 설정값을 객체로 바인딩할 수 있습니다. 이러한 기능들이 12-Factor App의 "환경에서 설정 분리" 원칙을 자연스럽게 구현하게 해줍니다.
코드 예제
# application.properties - 공통 설정
# 서버 설정
server.port=8080
server.servlet.context-path=/api
# 데이터베이스 공통 설정
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# 로깅 설정
logging.level.root=INFO
logging.level.com.myapp=DEBUG
---
# application-dev.properties - 개발 환경
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
---
# application-prod.properties - 프로덕션 환경
spring.datasource.url=jdbc:mysql://prod-db.example.com:3306/mydb
spring.datasource.username=${DB_USERNAME} # 환경변수에서 주입
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validate
logging.level.root=WARN
# 실행 시 프로파일 지정: java -jar app.jar --spring.profiles.active=prod
설명
이것이 하는 일: 이 설정 파일들은 환경에 따라 다른 데이터베이스와 로그 레벨을 자동으로 적용합니다. 첫 번째로, application.properties는 모든 환경에서 공통으로 사용되는 기본 설정입니다.
server.port=8080은 기본 포트를 지정하고, server.servlet.context-path=/api는 모든 URL 앞에 /api 접두사를 붙입니다. spring.jpa.show-sql=true는 Hibernate가 실행하는 SQL을 콘솔에 출력해서 디버깅을 돕습니다.
이러한 설정들은 Spring Boot의 자동 설정에 의해 자동으로 적용되며, 개발자는 필요한 부분만 오버라이드하면 됩니다. 그 다음으로, application-dev.properties는 개발 환경 전용 설정입니다.
H2 인메모리 데이터베이스를 사용하므로 별도의 데이터베이스 설치 없이 바로 실행할 수 있습니다. spring.jpa.hibernate.ddl-auto=create-drop은 애플리케이션 시작 시 테이블을 생성하고, 종료 시 삭제합니다.
이는 개발 중에는 유용하지만, 프로덕션에서는 절대 사용해서는 안 됩니다. --spring.profiles.active=dev 옵션으로 실행하면 이 설정이 활성화되고, 공통 설정과 병합됩니다(dev가 우선).
마지막으로, application-prod.properties는 프로덕션 환경 설정입니다. 실제 MySQL 데이터베이스 URL을 사용하고, ${DB_USERNAME} 문법으로 환경변수를 주입받습니다.
이렇게 하면 민감한 정보가 코드에 포함되지 않아 보안이 강화됩니다. spring.jpa.hibernate.ddl-auto=validate는 엔티티와 테이블 스키마가 일치하는지만 검증하고, 절대 테이블을 수정하지 않습니다.
로그 레벨도 WARN으로 올려서 불필요한 로그가 쌓이지 않게 합니다. 여러분이 이 방식을 사용하면 같은 JAR 파일을 모든 환경에 배포하고, 실행 시 프로파일만 바꾸면 됩니다.
Docker 컨테이너라면 SPRING_PROFILES_ACTIVE=prod 환경변수를 설정하고, Kubernetes라면 ConfigMap으로 설정을 주입할 수 있습니다. 또한 @Value("${server.port}") 어노테이션으로 코드에서 설정값을 읽어올 수 있고, @ConfigurationProperties로 복잡한 설정을 객체로 바인딩할 수도 있습니다.
Spring Cloud Config를 사용하면 설정을 외부 Git 저장소에서 관리하여 재배포 없이 설정을 변경할 수도 있습니다. 이처럼 Spring Boot의 설정 관리는 단순하면서도 강력하여, 마이크로서비스 아키텍처에서도 널리 사용됩니다.
실전 팁
💡 민감한 정보(비밀번호, API 키)는 절대 Git에 커밋하지 마세요. 환경변수나 AWS Secrets Manager 같은 도구를 사용하세요
💡 YAML 형식(application.yml)이 계층 구조 표현이 더 직관적입니다. 취향에 따라 선택하세요
💡 @ConfigurationPropertiesScan과 @ConfigurationProperties로 타입 안전한 설정 객체를 만들면 IDE 자동완성과 검증이 가능합니다
💡 spring.config.import=optional:file:.env로 .env 파일을 로드할 수 있습니다(Spring Boot 2.4+)
💡 설정 우선순위를 이해하세요: 커맨드라인 > 환경변수 > application-{profile}.properties > application.properties 순으로 오버라이드됩니다
7. 예외 처리(Exception Handling) - 우아한 에러 응답 만들기
시작하며
여러분이 만든 API에서 사용자가 존재하지 않는 ID로 조회하거나, 잘못된 형식의 데이터를 보냈을 때를 생각해보세요. 아무런 처리 없이 그냥 두면 500 Internal Server Error와 함께 스택 트레이스가 고스란히 클라이언트에 노출됩니다.
이는 보안상 위험할 뿐만 아니라, 사용자 경험도 최악입니다. 전통적인 방식이라면 모든 컨트롤러 메서드에 try-catch 블록을 넣고, 일일이 에러 응답을 만들어야 했습니다.
코드 중복이 심하고, 일관성 없는 에러 메시지가 난무하게 되죠. "User not found"라고 할 때도 있고 "사용자를 찾을 수 없습니다"라고 할 때도 있는 식입니다.
바로 이럴 때 Spring Boot의 전역 예외 처리 기능이 빛을 발합니다. @RestControllerAdvice와 @ExceptionHandler로 애플리케이션 전체의 예외를 한 곳에서 일관되게 처리할 수 있습니다.
개요
간단히 말해서, 예외 처리는 애플리케이션에서 발생하는 오류를 적절한 HTTP 상태 코드와 메시지로 변환하여 클라이언트에 반환하는 메커니즘입니다. Spring Boot는 기본적으로 예외를 JSON 형태의 에러 응답으로 변환하지만, 커스터마이징이 필요합니다.
예를 들어, UserNotFoundException이 발생하면 404 Not Found를, ValidationException이 발생하면 400 Bad Request를 반환하도록 설정할 수 있습니다. 각 에러마다 적절한 메시지와 에러 코드를 포함시켜 프론트엔드가 상황을 정확히 파악할 수 있게 해줍니다.
기존에는 각 메서드에서 try-catch로 처리하고 ResponseEntity를 직접 생성했다면, 이제는 예외만 던지면 전역 핸들러가 자동으로 처리해줍니다. Spring Boot 예외 처리의 핵심 이점은 세 가지입니다.
첫째, @RestControllerAdvice로 모든 컨트롤러의 예외를 중앙에서 처리하여 코드 중복이 없습니다. 둘째, 커스텀 예외 클래스로 비즈니스 로직의 예외 상황을 명확히 표현할 수 있습니다.
셋째, 일관된 에러 응답 형식으로 API 문서화와 클라이언트 구현이 쉬워집니다. 이러한 특징들이 프로페셔널한 API를 만드는 데 필수적입니다.
코드 예제
// 커스텀 예외 클래스
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found with id: " + id);
}
}
// 에러 응답 DTO
@Getter
@AllArgsConstructor
public class ErrorResponse {
private String message;
private int status;
private String timestamp;
}
// 전역 예외 핸들러
@RestControllerAdvice
public class GlobalExceptionHandler {
// 사용자 미발견 예외 처리 - 404 반환
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
return new ErrorResponse(
ex.getMessage(),
404,
LocalDateTime.now().toString()
);
}
// Validation 실패 - 400 반환
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationError(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return new ErrorResponse(errorMessage, 400, LocalDateTime.now().toString());
}
// 그 외 모든 예외 - 500 반환
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericError(Exception ex) {
// 프로덕션에서는 상세 메시지를 로깅만 하고, 일반적인 메시지 반환
return new ErrorResponse("Internal server error", 500, LocalDateTime.now().toString());
}
}
설명
이것이 하는 일: 이 코드는 애플리케이션 전체에서 발생하는 예외를 적절한 HTTP 응답으로 자동 변환합니다. 첫 번째로, UserNotFoundException은 도메인 특화된 커스텀 예외입니다.
RuntimeException을 상속하므로 체크 예외가 아니어서, 메서드 시그니처에 throws 선언 없이도 던질 수 있습니다. 생성자에서 ID를 받아 명확한 에러 메시지를 만듭니다.
서비스 레이어에서 userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id))처럼 사용하면, 컨트롤러까지 예외가 전파되어 전역 핸들러가 처리합니다. 그 다음으로, @RestControllerAdvice는 모든 @RestController에 적용되는 횡단 관심사(Cross-Cutting Concern)를 처리합니다.
@ExceptionHandler 메서드는 특정 예외 타입을 잡아서 처리하는데, 스프링이 예외를 발견하면 타입이 일치하는 핸들러를 자동으로 호출합니다. handleUserNotFound는 404 상태 코드와 함께 JSON 형태의 ErrorResponse를 반환합니다.
@ResponseStatus로 상태 코드를 지정하거나, ResponseEntity를 반환해서 더 세밀하게 제어할 수도 있습니다. 마지막으로, handleValidationError는 @Valid 검증 실패 시 발생하는 예외를 처리합니다.
BindingResult에서 모든 필드 에러를 추출하여 "email: must not be blank, age: must be greater than 0" 같은 구체적인 메시지를 만듭니다. handleGenericError는 catch-all 핸들러로, 예상하지 못한 모든 예외를 잡아서 500 에러로 반환합니다.
프로덕션 환경에서는 상세한 스택 트레이스를 클라이언트에 노출하지 않고, 로그에만 기록하는 것이 보안상 중요합니다. 여러분이 이 패턴을 사용하면 컨트롤러 코드가 극도로 간결해집니다.
try-catch 없이 그냥 비즈니스 로직만 작성하고, 예외는 자유롭게 던지면 됩니다. 예외 처리 로직이 한 곳에 집중되어 유지보수가 쉽고, 모든 API가 일관된 에러 형식을 반환하여 클라이언트 개발자가 예측 가능한 코드를 작성할 수 있습니다.
Swagger나 API 문서에도 "404: User not found", "400: Validation failed" 같은 명확한 설명을 제공할 수 있습니다. 실무에서는 에러 코드 체계를 정의해서(예: ERR_USER_001) 프론트엔드가 다국어 메시지를 표시하거나 특별한 처리를 할 수 있게 하기도 합니다.
실전 팁
💡 프로덕션에서는 상세한 예외 메시지를 클라이언트에 노출하지 마세요. 일반적인 메시지만 반환하고 상세 정보는 로그에만 기록하세요
💡 @ControllerAdvice(basePackages = "com.myapp.api")로 특정 패키지에만 적용할 수 있습니다
💡 비즈니스 예외는 커스텀 예외 클래스를 만들고, 기술적 예외(IOException 등)는 일반 핸들러로 처리하세요
💡 ProblemDetail(Spring 6.0+)을 사용하면 RFC 7807 표준 에러 응답을 쉽게 만들 수 있습니다
💡 테스트할 때는 @WebMvcTest와 MockMvc로 예외 시나리오를 검증하세요: mockMvc.perform(...).andExpect(status().isNotFound())
8. 테스트 작성(Testing) - 안정적인 코드를 위한 필수 과정
시작하며
여러분이 새로운 기능을 추가했는데, 기존 기능이 망가졌는지 확인하려면 어떻게 하나요? 애플리케이션을 실행하고, 브라우저나 Postman으로 모든 API를 일일이 테스트하나요?
이런 수동 테스트는 시간이 오래 걸리고, 실수하기 쉬우며, 매번 반복해야 한다는 문제가 있습니다. 더 큰 문제는 코드 변경 시 영향 범위를 파악하기 어렵다는 것입니다.
결제 로직을 수정했는데 주문 생성이 깨질 수도 있고, 사용자 서비스를 변경했는데 인증이 작동하지 않을 수도 있습니다. 자동화된 테스트가 없으면 배포가 두려워지고, 리팩토링을 꺼리게 되어 코드 품질이 점점 나빠집니다.
바로 이럴 때 Spring Boot의 강력한 테스트 지원이 필수입니다. @SpringBootTest, @WebMvcTest, @DataJpaTest 등으로 유닛 테스트부터 통합 테스트까지 쉽게 작성할 수 있습니다.
개요
간단히 말해서, 테스트는 코드가 의도대로 작동하는지 자동으로 검증하는 코드이며, Spring Boot는 다양한 레벨의 테스트를 간편하게 작성할 수 있는 도구를 제공합니다. Spring Boot의 spring-boot-starter-test는 JUnit 5, Mockito, AssertJ, Spring Test 등을 모두 포함합니다.
단위 테스트는 개별 메서드나 클래스를 격리해서 테스트하고, 통합 테스트는 여러 컴포넌트를 함께 테스트하며, E2E 테스트는 실제 HTTP 요청으로 전체 플로우를 검증합니다. 예를 들어, @WebMvcTest는 컨트롤러만 로드해서 빠르게 API를 테스트하고, @DataJpaTest는 JPA 리포지토리만 테스트합니다.
기존에는 테스트를 위해 복잡한 설정과 Mock 객체 생성이 필요했다면, 이제는 어노테이션 하나로 필요한 환경이 자동 구성됩니다. Spring Boot 테스트의 핵심 이점은 세 가지입니다.
첫째, 슬라이스 테스트(@WebMvcTest, @DataJpaTest)로 필요한 부분만 로드하여 테스트가 빠릅니다. 둘째, @MockBean으로 의존성을 쉽게 모킹하여 격리된 테스트가 가능합니다.
셋째, TestContainers와 통합하여 실제 데이터베이스로 테스트할 수 있습니다. 이러한 특징들이 TDD(Test-Driven Development)와 CI/CD를 가능하게 합니다.
코드 예제
// 컨트롤러 단위 테스트
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // HTTP 요청을 시뮬레이션
@MockBean
private UserService userService; // 실제 서비스 대신 Mock 사용
@Test
@DisplayName("사용자 조회 API는 존재하는 ID로 200 OK를 반환한다")
void getUserById_Success() throws Exception {
// Given - 테스트 데이터 준비
User user = new User(1L, "John", "john@example.com");
when(userService.findById(1L)).thenReturn(Optional.of(user));
// When & Then - API 호출 및 검증
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk()) // 200 상태 코드
.andExpect(jsonPath("$.name").value("John")) // JSON 필드 검증
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
@DisplayName("존재하지 않는 사용자 조회 시 404를 반환한다")
void getUserById_NotFound() throws Exception {
// Given
when(userService.findById(999L)).thenReturn(Optional.empty());
// When & Then
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
// 리포지토리 테스트
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("이름으로 사용자를 찾을 수 있다")
void findByName() {
// Given
User user = new User(null, "Alice", "alice@example.com");
userRepository.save(user);
// When
List<User> result = userRepository.findByName("Alice");
// Then
assertThat(result).hasSize(1);
assertThat(result.get(0).getEmail()).isEqualTo("alice@example.com");
}
}
설명
이것이 하는 일: 이 테스트 코드들은 컨트롤러와 리포지토리가 올바르게 작동하는지 자동으로 검증합니다. 첫 번째로, @WebMvcTest(UserController.class)는 전체 애플리케이션 컨텍스트가 아닌 웹 레이어만 로드합니다.
UserController와 관련된 빈들만 생성되므로, @SpringBootTest보다 훨씬 빠르게 실행됩니다(수십 배 차이). MockMvc는 실제 HTTP 서버를 띄우지 않고도 HTTP 요청을 시뮬레이션하여, 빠르고 안정적인 테스트가 가능합니다.
@MockBean으로 UserService를 가짜 객체로 대체하여, 실제 데이터베이스 연결 없이도 테스트할 수 있습니다. 그 다음으로, 각 테스트는 Given-When-Then 패턴을 따릅니다.
getUserById_Success에서는 먼저 Mock 객체의 동작을 정의(when(userService.findById(1L)).thenReturn(...))하고, API를 호출하며(mockMvc.perform(get(...))), 결과를 검증합니다(andExpect(...)). jsonPath로 JSON 응답의 특정 필드를 검증할 수 있어, REST API 테스트에 매우 유용합니다.
@DisplayName으로 테스트 의도를 명확히 표현하면, 테스트 실패 시 어떤 기능이 깨졌는지 즉시 파악할 수 있습니다. 마지막으로, @DataJpaTest는 JPA 관련 컴포넌트만 로드하고, 내장 H2 데이터베이스를 자동으로 구성합니다.
각 테스트는 트랜잭션 안에서 실행되고 롤백되므로, 테스트 간 데이터 간섭이 없습니다. userRepository.save(user) 후 findByName으로 조회했을 때 올바르게 찾아지는지 검증합니다.
AssertJ의 assertThat은 유창한 API로 가독성이 높아서, assertEquals보다 선호됩니다. 여러분이 테스트를 작성하면 리팩토링이 자신감 있게 가능해집니다.
코드를 변경한 후 테스트를 실행하면 몇 초 만에 모든 기능이 정상인지 확인할 수 있습니다. CI/CD 파이프라인에서 테스트가 실패하면 배포가 차단되어, 버그가 프로덕션에 배포되는 것을 방지합니다.
또한 테스트 코드는 살아있는 문서 역할을 해서, 새로운 팀원이 코드를 이해하는 데 큰 도움이 됩니다. @DisplayName에 "사용자 조회 API는..."이라고 적혀 있으면, 코드를 읽지 않아도 기능을 알 수 있죠.
커버리지 도구(JaCoCo)로 테스트 커버리지를 측정하여, 테스트되지 않은 코드를 찾아낼 수도 있습니다. 실무에서는 최소 70-80% 커버리지를 목표로 하며, 핵심 비즈니스 로직은 100% 테스트하는 것이 권장됩니다.
실전 팁
💡 테스트는 빠르고, 독립적이며, 반복 가능해야 합니다. 외부 API나 파일 시스템에 의존하면 불안정해집니다
💡 @SpringBootTest는 전체 컨텍스트를 로드하므로 느립니다. 가능하면 슬라이스 테스트(@WebMvcTest, @DataJpaTest)를 사용하세요
💡 BDD 스타일을 선호한다면 @Nested와 @DisplayName으로 테스트를 그룹화하고, AssertJ 대신 Kotest를 고려하세요
💡 통합 테스트에서 실제 데이터베이스가 필요하다면 TestContainers를 사용하세요. Docker로 PostgreSQL을 띄워서 테스트할 수 있습니다
💡 테스트 픽스처(공통 테스트 데이터)는 @BeforeEach로 준비하되, 테스트 간 결합을 피하기 위해 메서드로 분리하는 것이 좋습니다
9. Actuator - 프로덕션 모니터링과 관리
시작하며
여러분의 애플리케이션이 프로덕션 환경에서 실행 중일 때, "지금 서버가 정상인가?", "메모리 사용량은 얼마나 되나?", "어떤 빈들이 등록되어 있나?" 같은 질문에 답할 수 있나요? 전통적인 방식이라면 로그를 뒤지거나, JMX로 연결하거나, 커스텀 모니터링 엔드포인트를 직접 만들어야 했을 것입니다.
이런 운영 정보를 얻기 위해 코드를 추가하고, 별도의 모니터링 시스템을 구축하는 것은 시간과 비용이 많이 듭니다. 특히 클라우드 환경에서는 헬스 체크 엔드포인트가 필수인데, 이것조차 직접 구현해야 했죠.
데이터베이스 연결 상태, 디스크 용량, 외부 API 연결 여부 등을 체크하는 로직을 일일이 작성하는 것은 번거롭습니다. 바로 이럴 때 Spring Boot Actuator가 구세주가 됩니다.
단 한 줄의 의존성 추가만으로 수십 개의 모니터링 엔드포인트가 자동으로 생성됩니다.
개요
간단히 말해서, Actuator는 Spring Boot 애플리케이션의 운영 정보를 HTTP 엔드포인트나 JMX를 통해 노출하는 프로덕션급 기능 모음입니다. Actuator는 /actuator/health(헬스 체크), /actuator/metrics(메트릭), /actuator/env(환경 설정), /actuator/beans(등록된 빈 목록) 등 40개 이상의 엔드포인트를 제공합니다.
예를 들어, Kubernetes의 liveness/readiness probe는 /actuator/health를 호출해서 컨테이너가 정상인지 확인합니다. Prometheus는 /actuator/prometheus에서 메트릭을 수집해서 그래프로 시각화합니다.
기존에는 이런 기능들을 직접 구현하거나 서드파티 라이브러리를 복잡하게 통합해야 했다면, 이제는 자동 설정으로 즉시 사용할 수 있습니다. Actuator의 핵심 이점은 세 가지입니다.
첫째, 헬스 체크로 데이터베이스, 디스크, 외부 API 등의 상태를 자동 확인합니다. 둘째, 메트릭으로 JVM 메모리, HTTP 요청 수, 응답 시간 등을 실시간 수집합니다.
셋째, 엔드포인트 커스터마이징으로 비즈니스 특화 정보를 추가할 수 있습니다. 이러한 기능들이 DevOps와 SRE(Site Reliability Engineering)의 필수 도구가 되었습니다.
코드 예제
// build.gradle - Actuator 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
// application.properties - Actuator 설정
# 모든 엔드포인트 활성화 (기본적으로 일부만 노출됨)
management.endpoints.web.exposure.include=health,info,metrics,env
# 헬스 체크 상세 정보 표시
management.endpoint.health.show-details=always
# 커스텀 헬스 인디케이터
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// 외부 API 연결 체크
boolean externalApiUp = checkExternalApi();
if (externalApiUp) {
return Health.up()
.withDetail("externalApi", "Available")
.build();
} else {
return Health.down()
.withDetail("externalApi", "Unavailable")
.build();
}
}
private boolean checkExternalApi() {
// 실제 API 호출 로직
return true;
}
}
// 커스텀 메트릭 수집
@Service
public class OrderService {
private final MeterRegistry meterRegistry;
public OrderService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void createOrder(Order order) {
// 주문 생성 횟수를 메트릭으로 기록
meterRegistry.counter("orders.created", "type", order.getType()).increment();
// 실제 주문 처리...
}
}
설명
이것이 하는 일: 이 코드는 애플리케이션의 상태를 모니터링하고, 커스텀 비즈니스 메트릭을 수집합니다. 첫 번째로, spring-boot-starter-actuator 의존성만 추가하면 기본 엔드포인트가 자동 활성화됩니다.
/actuator/health에 접속하면 {"status":"UP"}처럼 간단한 응답이 나오고, show-details=always로 설정하면 데이터베이스 연결 상태, 디스크 용량, 각 컴포넌트의 상태가 자세히 표시됩니다. 기본적으로 보안상 이유로 일부 엔드포인트만 노출되므로, exposure.include로 필요한 것을 명시해야 합니다.
프로덕션에서는 민감한 정보(/env, /beans)는 제한하고, health와 metrics만 공개하는 것이 안전합니다. 그 다음으로, HealthIndicator 인터페이스를 구현하면 커스텀 헬스 체크를 추가할 수 있습니다.
health() 메서드는 Health 객체를 반환하는데, up()은 정상, down()은 장애를 의미합니다. 여러 개의 HealthIndicator가 있으면 모두 체크해서, 하나라도 down이면 전체 헬스가 DOWN이 됩니다.
Kubernetes는 이를 감지해서 문제 있는 Pod를 재시작하거나 트래픽을 차단합니다. withDetail로 추가 정보를 제공하면, 운영 팀이 문제를 빠르게 파악할 수 있습니다.
마지막으로, MeterRegistry로 비즈니스 메트릭을 수집합니다. meterRegistry.counter(...).increment()는 주문 생성 횟수를 카운터로 기록하는데, "type" 태그로 주문 유형별로 분리할 수 있습니다.
/actuator/metrics/orders.created로 조회하거나, Prometheus가 자동 수집해서 Grafana 대시보드에 표시할 수 있습니다. Timer, Gauge, Distribution Summary 등 다양한 메트릭 타입을 지원하여, 응답 시간 분포나 동시 접속자 수 같은 정보도 수집 가능합니다.
여러분이 Actuator를 사용하면 애플리케이션의 내부 상태를 실시간으로 파악할 수 있습니다. "/actuator/metrics/jvm.memory.used"로 메모리 누수를 조기에 발견하고, "/actuator/metrics/http.server.requests"로 느린 API를 식별할 수 있습니다.
클라우드 환경에서는 필수 기능으로, AWS ECS나 Kubernetes의 Auto Scaling 결정에도 활용됩니다. 또한 /actuator/loggers로 실행 중에 로그 레벨을 동적으로 변경할 수 있어, 재배포 없이 디버깅 로그를 활성화할 수도 있습니다.
Spring Boot Admin 같은 도구와 연동하면 여러 애플리케이션을 하나의 대시보드에서 모니터링할 수 있어, 마이크로서비스 환경에서 특히 유용합니다. 보안을 위해 Spring Security와 통합하여 인증된 사용자만 민감한 엔드포인트에 접근하도록 설정하는 것이 권장됩니다.
실전 팁
💡 프로덕션에서는 /actuator 경로를 변경하세요: management.endpoints.web.base-path=/internal. 보안상 예측 가능한 경로는 위험합니다
💡 /actuator/metrics는 사용 가능한 모든 메트릭 목록을 보여주고, /actuator/metrics/{name}으로 구체적인 값을 조회합니다
💡 Spring Security로 Actuator 엔드포인트를 보호하세요: .requestMatchers("/actuator/**").hasRole("ADMIN")
💡 Micrometer는 Prometheus, Datadog, New Relic 등 다양한 모니터링 시스템과 통합됩니다. micrometer-registry-prometheus 의존성 추가만으로 가능합니다
💡 /actuator/shutdown으로 애플리케이션을 원격 종료할 수 있지만, 기본적으로 비활성화되어 있습니다. 활성화하려면 management.endpoint.shutdown.enabled=true
10. 프로파일과 환경 분리 - 개발/스테이징/프로덕션 관리
시작하며
여러분이 로컬에서 개발할 때는 H2 데이터베이스와 DEBUG 로그를 사용하고, 스테이징 서버에서는 테스트용 MySQL과 INFO 로그를, 프로덕션에서는 RDS와 WARN 로그를 사용한다고 상상해보세요. 각 환경마다 설정이 다른데, 어떻게 관리하시겠습니까?
코드에 if문으로 환경을 체크하고 분기 처리하시겠습니까? 이런 방식은 코드가 지저분해지고, 실수로 프로덕션에서 개발 설정이 활성화될 위험이 있습니다.
또한 새로운 환경(예: QA 환경)이 추가되면 코드를 수정해야 하는 문제도 있습니다. 설정이 코드와 섞이면 12-Factor App의 원칙을 위반하게 되어, 컨테이너 기반 배포에도 적합하지 않습니다.
바로 이럴 때 Spring Boot의 프로파일 기능이 완벽한 해결책을 제공합니다. 환경별로 설정 파일을 분리하고, 실행 시 프로파일만 지정하면 자동으로 적절한 설정이 로드됩니다.
개요
간단히 말해서, 프로파일은 환경별로 다른 설정과 빈을 활성화하는 메커니즘이며, 같은 코드를 다양한 환경에서 다르게 동작시킬 수 있게 해줍니다. Spring Boot는 application-{profile}.properties 파일을 자동으로 인식합니다.
spring.profiles.active=prod로 실행하면 application-prod.properties가 로드되고, application.properties와 병합됩니다(프로파일 설정이 우선). 예를 들어, 로컬에서는 dev 프로파일로 실행하고, CI/CD 파이프라인에서는 prod 프로파일로 배포하면, 같은 JAR 파일이지만 다르게 동작합니다.
기존에는 Maven 프로파일이나 별도의 빌드를 만들어야 했다면, 이제는 런타임에 환경을 선택할 수 있어 훨씬 유연합니다. 프로파일의 핵심 이점은 세 가지입니다.
첫째, 같은 아티팩트(JAR)를 모든 환경에 배포하여 "빌드는 한 번, 배포는 여러 번" 원칙을 지킵니다. 둘째, @Profile 어노테이션으로 특정 환경에서만 빈을 활성화할 수 있습니다(예: 개발용 데이터 초기화).
셋째, 프로파일 그룹으로 여러 프로파일을 조합할 수 있습니다(예: "cloud" 프로파일은 "aws"와 "monitoring"을 포함). 이러한 기능들이 클라우드 네이티브 애플리케이션의 필수 요소입니다.
코드 예제
// application.properties - 공통 기본 설정
spring.application.name=my-app
server.port=8080
// application-dev.properties - 개발 환경
spring.datasource.url=jdbc:h2:mem:devdb
spring.jpa.show-sql=true
logging.level.root=DEBUG
# 개발용 더미 데이터 자동 생성
spring.sql.init.mode=always
// application-prod.properties - 프로덕션 환경
spring.datasource.url=${DATABASE_URL} # 환경변수에서 주입
spring.jpa.show-sql=false
logging.level.root=WARN
# 실제 DB 스키마 검증만
spring.jpa.hibernate.ddl-auto=validate
// 프로파일별 빈 등록
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public CommandLineRunner initData(UserRepository userRepository) {
// 개발 환경에서만 테스트 데이터 생성
return args -> {
userRepository.save(new User(null, "Test User", "test@example.com"));
System.out.println("개발용 테스트 데이터 생성 완료");
};
}
}
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public Filter securityHeaderFilter() {
// 프로덕션에서만 보안 헤더 추가
return (request, response, chain) -> {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("X-Frame-Options", "DENY");
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
chain.doFilter(request, response);
};
}
}
// 실행 방법
// 개발: java -jar app.jar --spring.profiles.active=dev
// 프로덕션: java -jar app.jar --spring.profiles.active=prod
설명
이것이 하는 일: 이 코드는 동일한 애플리케이션이 개발과 프로덕션 환경에서 완전히 다르게 동작하도록 만듭니다. 첫 번째로, application.properties는 모든 환경에서 공통으로 사용되는 기본 설정입니다.
애플리케이션 이름이나 기본 포트처럼 환경과 무관한 설정을 여기에 둡니다. 그리고 application-dev.properties와 application-prod.properties는 환경 특화 설정으로, 활성화된 프로파일에 따라 하나가 선택됩니다.
만약 같은 키가 여러 곳에 있다면, 프로파일 설정 > 기본 설정 순으로 우선순위가 적용됩니다. 그 다음으로, @Profile("dev") 어노테이션으로 특정 환경에서만 빈이 등록됩니다.
DevConfig의 initData 빈은 개발 환경에서만 생성되어, 애플리케이션 시작 시 테스트 데이터를 자동으로 넣어줍니다. 프로덕션에서 실행하면 이 빈은 아예 생성되지 않으므로, 실제 고객 데이터가 오염될 위험이 없습니다.
반대로 ProdConfig의 securityHeaderFilter는 프로덕션에서만 활성화되어, XSS 등의 보안 헤더를 추가합니다. 마지막으로, 실행 시 --spring.profiles.active=dev 옵션으로 프로파일을 지정합니다.
환경변수로도 가능합니다: export SPRING_PROFILES_ACTIVE=prod. Docker 컨테이너라면 ENV SPRING_PROFILES_ACTIVE=prod로 Dockerfile에 명시하거나, Kubernetes ConfigMap으로 주입할 수 있습니다.
여러 프로파일을 동시에 활성화할 수도 있는데(--spring.profiles.active=prod,monitoring), 이 경우 두 프로파일의 설정이 모두 적용됩니다. 여러분이 프로파일을 사용하면 환경 간 차이를 명확하게 관리할 수 있습니다.
개발자는 로컬에서 dev 프로파일로 편하게 개발하고, CI 서버에서는 test 프로파일로 통합 테스트를 실행하며, 프로덕션에는 prod 프로파일로 배포합니다. 같은 JAR 파일을 사용하므로 "개발에서는 되는데 프로덕션에서는 안 돼요" 같은 문제가 줄어듭니다.
또한 @Profile("!prod")처럼 부정 표현식도 지원하여, "프로덕션이 아닌 모든 환경"에서 빈을 활성화할 수도 있습니다. Spring Cloud Config와 결합하면 설정을 외부 Git 저장소에서 관리하여, 애플리케이션 재배포 없이 설정을 변경할 수도 있습니다.
프로파일 그룹 기능(spring.profiles.group.cloud=aws,logging,monitoring)으로 관련 프로파일을 묶어서 관리하면, 복잡한 마이크로서비스 환경도 체계적으로 운영할 수 있습니다.
실전 팁
💡 기본 프로파일을 설정하세요: spring.profiles.default=dev. 프로파일을 지정하지 않으면 개발 환경으로 실행됩니다
💡 민감한 정보는 프로파일 파일에도 직접 쓰지 마세요. 환경변수나 AWS Secrets Manager를 사용하세요
💡 @ActiveProfiles("test")로 테스트에서 사용할 프로파일을 지정할 수 있습니다
💡 프로파일은 소문자와 하이픈을 권장합니다: application-dev.properties, application-prod.properties (카멜케이스 피하기)
💡 YAML 파일은 한 파일 안에서 ---로 프로파일을 구분할 수 있어 관리가 편합니다: spring.config.activate.on-profile: prod