🚨 들어가며
"왜 안 되지?"
Spring Boot 개발을 하다 보면 수많은 오류를 만나게 됩니다. 빨간색 에러 로그를 보면 막막하지만, 사실 각 오류는 우리에게 문제를 해결할 힌트를 제공합니다.
이 글에서는 Spring Boot 개발 중 자주 만나는 오류들을 체계적으로 정리하고, 각 오류의 원인과 해결 방법을 실제 사례와 함께 설명합니다. 이 가이드를 통해 더 이상 에러 메시지가 두렵지 않게 될 것입니다!
📑 목차
시작 단계 오류
1. ApplicationContext 로드 실패
오류 메시지
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
원인
- 데이터베이스 설정이 없는데 JPA 의존성이 있음
- application.properties에 DB 설정 누락
해결 방법
방법 1: 데이터베이스 설정 추가
# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
방법 2: 자동 설정 제외
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. 포트 충돌
오류 메시지
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
원인
- 이미 8080 포트를 다른 프로세스가 사용 중
해결 방법
방법 1: 포트 변경
# application.properties
server.port=8081
방법 2: 사용 중인 프로세스 종료
# Windows
netstat -ano | findstr :8080
taskkill /PID <PID> /F
# Mac/Linux
lsof -i :8080
kill -9 <PID>
방법 3: 랜덤 포트 사용
server.port=0
3. 메인 클래스를 찾을 수 없음
오류 메시지
Error: Could not find or load main class com.example.Application
Caused by: java.lang.ClassNotFoundException: com.example.Application
원인
- 잘못된 패키지 구조
- 빌드 문제
- IDE 설정 오류
해결 방법
<!-- pom.xml에 메인 클래스 명시 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.Application</mainClass>
</configuration>
</plugin>
</plugins>
</build>
의존성 관련 오류
1. Bean 생성 실패
오류 메시지
***************************
APPLICATION FAILED TO START
***************************
Description:
Field userService in com.example.controller.UserController required a bean of type 'com.example.service.UserService' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'com.example.service.UserService' in your configuration.
원인
- @Service, @Component 어노테이션 누락
- 컴포넌트 스캔 범위 밖에 클래스 위치
- 인터페이스 구현체 없음
해결 방법
방법 1: 어노테이션 추가
@Service // 이 어노테이션 추가!
public class UserService {
// ...
}
방법 2: 컴포넌트 스캔 설정
@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "com.other.package"})
public class Application {
// ...
}
방법 3: @Bean 직접 정의
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService();
}
}
2. 순환 의존성
오류 메시지
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| userController defined in file [UserController.class]
↑ ↓
| userService defined in file [UserService.class]
↑ ↓
| orderService defined in file [OrderService.class]
└─────┘
원인
- A → B → C → A 형태의 순환 참조
- 생성자 주입 시 서로 의존
해결 방법
방법 1: @Lazy 사용
@Service
public class UserService {
private final OrderService orderService;
public UserService(@Lazy OrderService orderService) {
this.orderService = orderService;
}
}
방법 2: Setter 주입 사용
@Service
public class UserService {
private OrderService orderService;
@Autowired
public void setOrderService(OrderService orderService) {
this.orderService = orderService;
}
}
방법 3: 설계 개선 (권장)
// 중간 서비스나 이벤트 기반으로 변경
@Service
public class UserOrderMediator {
private final UserService userService;
private final OrderService orderService;
// 순환 의존성 해결
}
3. 중복 Bean 정의
오류 메시지
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.service.NotificationService required a single bean, but 2 were found:
- emailSender: defined in file [EmailSender.class]
- smsSender: defined in file [SmsSender.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
원인
- 같은 타입의 Bean이 여러 개 존재
- 어떤 Bean을 주입해야 할지 모호함
해결 방법
방법 1: @Primary 사용
@Component
@Primary // 기본으로 사용될 Bean 지정
public class EmailSender implements MessageSender {
// ...
}
방법 2: @Qualifier 사용
@Service
public class NotificationService {
public NotificationService(@Qualifier("emailSender") MessageSender sender) {
this.sender = sender;
}
}
방법 3: 모든 구현체 주입
@Service
public class NotificationService {
private final List<MessageSender> senders;
public NotificationService(List<MessageSender> senders) {
this.senders = senders;
}
}
데이터베이스 연결 오류
1. 데이터베이스 연결 실패
오류 메시지
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
원인
- 데이터베이스 서버가 실행되지 않음
- 잘못된 호스트/포트
- 방화벽 차단
해결 방법
방법 1: 연결 정보 확인
# MySQL
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
# 연결 테스트
spring.datasource.hikari.connection-test-query=SELECT 1
방법 2: 타임아웃 설정
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.maximum-pool-size=10
2. 인증 실패
오류 메시지
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
원인
- 잘못된 사용자명/비밀번호
- 권한 부족
해결 방법
-- MySQL에서 권한 부여
CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'mypass';
GRANT ALL PRIVILEGES ON mydb.* TO 'myuser'@'localhost';
FLUSH PRIVILEGES;
3. 드라이버 클래스 없음
오류 메시지
Cannot load driver class: com.mysql.cj.jdbc.Driver
원인
- MySQL 드라이버 의존성 누락
해결 방법
<!-- pom.xml -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
JPA/Hibernate 오류
1. LazyInitializationException
오류 메시지
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.entity.User#1] - no Session
원인
- 트랜잭션 밖에서 Lazy 로딩 시도
- 세션이 이미 종료된 상태
해결 방법
방법 1: Eager 로딩 (비추천)
@Entity
public class Order {
@ManyToOne(fetch = FetchType.EAGER) // LAZY → EAGER
private User user;
}
방법 2: @Transactional 확대
@Service
@Transactional // 클래스 레벨에 추가
public class OrderService {
public OrderDto getOrder(Long id) {
Order order = orderRepository.findById(id).orElseThrow();
order.getUser().getName(); // Lazy 로딩 발생
return new OrderDto(order);
}
}
방법 3: Fetch Join 사용 (권장)
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.id = :id")
Optional<Order> findByIdWithUser(@Param("id") Long id);
}
방법 4: DTO Projection
@Query("SELECT new com.example.dto.OrderDto(o.id, o.amount, u.name) " +
"FROM Order o JOIN o.user u WHERE o.id = :id")
OrderDto findOrderDto(@Param("id") Long id);
2. TransientPropertyValueException
오류 메시지
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing
원인
- 저장되지 않은 엔티티를 참조
- Cascade 설정 누락
해결 방법
방법 1: Cascade 설정
@Entity
public class Post {
@OneToMany(cascade = CascadeType.ALL) // CASCADE 추가
private List<Comment> comments;
}
방법 2: 명시적 저장
@Transactional
public void createPost(Post post) {
// 연관된 엔티티 먼저 저장
for (Comment comment : post.getComments()) {
commentRepository.save(comment);
}
postRepository.save(post);
}
3. N+1 문제
증상
-- 1번의 쿼리
SELECT * FROM orders;
-- N번의 추가 쿼리
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
...
해결 방법
방법 1: Fetch Join
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();
방법 2: @EntityGraph
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findAll();
방법 3: Batch Size
spring.jpa.properties.hibernate.default_batch_fetch_size=100
4. 유일성 제약 조건 위반
오류 메시지
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [UK_email]; nested exception is org.hibernate.exception.ConstraintViolationException
원인
- 중복된 값으로 저장 시도
해결 방법
@Service
public class UserService {
public User createUser(UserDto dto) {
// 중복 검사
if (userRepository.existsByEmail(dto.getEmail())) {
throw new DuplicateEmailException("이미 존재하는 이메일입니다.");
}
return userRepository.save(new User(dto));
}
}
REST API 오류
1. 415 Unsupported Media Type
오류 메시지
{
"timestamp": "2024-01-20T10:15:30.123+00:00",
"status": 415,
"error": "Unsupported Media Type",
"path": "/api/users"
}
원인
- Content-Type 헤더 누락
- 잘못된 Content-Type
해결 방법
클라이언트 측
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 필수!
},
body: JSON.stringify(data)
});
서버 측
@PostMapping(value = "/users",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public User createUser(@RequestBody User user) {
return userService.save(user);
}
2. 400 Bad Request - JSON 파싱 오류
오류 메시지
JSON parse error: Unrecognized field "unknownField" (class com.example.dto.UserDto), not marked as ignorable
원인
- DTO에 없는 필드가 JSON에 포함
- JSON 형식 오류
해결 방법
방법 1: 알 수 없는 필드 무시
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserDto {
// ...
}
방법 2: 전역 설정
spring.jackson.deserialization.fail-on-unknown-properties=false
3. 날짜 형식 오류
오류 메시지
JSON parse error: Cannot deserialize value of type `java.time.LocalDateTime` from String "2024-01-20"
해결 방법
public class EventDto {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime eventDate;
}
// 또는 전역 설정
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=Asia/Seoul
4. CORS 오류
브라우저 콘솔 오류
Access to XMLHttpRequest at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy
해결 방법
방법 1: @CrossOrigin
@RestController
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
// ...
}
방법 2: 전역 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}
보안 관련 오류
1. 401 Unauthorized
오류 메시지
{
"timestamp": "2024-01-20T10:15:30.123+00:00",
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/api/admin/users"
}
원인
- 인증 토큰 없음
- 만료된 토큰
해결 방법
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new CustomAuthEntryPoint())
);
return http.build();
}
}
2. 403 Forbidden
오류 메시지
{
"timestamp": "2024-01-20T10:15:30.123+00:00",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/admin/users"
}
원인
- 권한 부족
- CSRF 토큰 누락
해결 방법
CSRF 비활성화 (REST API)
http.csrf(csrf -> csrf.disable());
권한 확인
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
3. JWT 관련 오류
오류 메시지
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2024-01-20T10:00:00Z. Current time: 2024-01-20T11:00:00Z
해결 방법
@Component
public class JwtTokenProvider {
public String validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return "valid";
} catch (ExpiredJwtException e) {
// 토큰 갱신 로직
return "expired";
} catch (JwtException e) {
return "invalid";
}
}
}
설정 관련 오류
1. 프로퍼티 바인딩 오류
오류 메시지
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target [Bindable@1234 type = com.example.config.AppProperties, value = 'provided', annotations = array<Annotation>[@org.springframework.boot.context.properties.ConfigurationProperties(prefix=app)]] failed:
Property: app.upload.maxFileSize
Value: 10MB
Origin: class path resource [application.yml]:10:20
Reason: failed to convert java.lang.String to org.springframework.util.unit.DataSize
원인
- 잘못된 프로퍼티 형식
- 타입 변환 실패
해결 방법
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotNull
private Upload upload = new Upload();
public static class Upload {
private DataSize maxFileSize = DataSize.ofMegabytes(10);
// getter/setter
}
}
# application.yml
app:
upload:
max-file-size: 10MB # 또는 10485760
2. 프로파일 활성화 오류
증상
No active profile set, falling back to default profiles: default
해결 방법
방법 1: 실행 시 지정
java -jar app.jar --spring.profiles.active=prod
방법 2: 환경 변수
export SPRING_PROFILES_ACTIVE=prod
방법 3: application.properties
spring.profiles.active=dev
3. 로깅 설정 오류
오류 메시지
ERROR in ch.qos.logback.core.joran.spi.Interpreter@10:26 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]]
해결 방법
<!-- logback-spring.xml (logback.xml이 아님!) -->
<configuration>
<springProfile name="dev">
<logger name="com.example" level="DEBUG"/>
</springProfile>
<springProfile name="prod">
<logger name="com.example" level="INFO"/>
</springProfile>
</configuration>
런타임 오류
1. NullPointerException
오류 메시지
java.lang.NullPointerException: Cannot invoke "com.example.service.UserService.findById(Long)" because "this.userService" is null
원인
- Bean 주입 실패
- static 메소드에서 @Autowired 필드 접근
해결 방법
@RestController
public class UserController {
// ❌ 잘못된 방법
@Autowired
private static UserService userService;
// ✅ 올바른 방법
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
2. StackOverflowError
오류 메시지
java.lang.StackOverflowError
at com.example.entity.User.toString(User.java:50)
at com.example.entity.Order.toString(Order.java:45)
at com.example.entity.User.toString(User.java:50)
...
원인
- 양방향 연관관계에서 toString() 순환 참조
- 재귀 호출
해결 방법
@Entity
public class User {
@OneToMany(mappedBy = "user")
@ToString.Exclude // Lombok 사용 시
private List<Order> orders;
// 또는 수동으로 toString() 구현
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
// orders는 제외
}
}
3. OutOfMemoryError
오류 메시지
java.lang.OutOfMemoryError: Java heap space
원인
- 메모리 누수
- 대용량 데이터 한 번에 로딩
해결 방법
JVM 옵션 조정
java -Xms512m -Xmx2048m -jar app.jar
페이징 처리
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u")
Stream<User> findAllAsStream(); // 대용량 처리
}
@Transactional(readOnly = true)
public void processAllUsers() {
try (Stream<User> users = userRepository.findAllAsStream()) {
users.forEach(this::processUser);
}
}
4. 트랜잭션 관련 오류
오류 메시지
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
원인
- 중첩된 트랜잭션에서 롤백 마크
- 예외 발생 후 계속 진행
해결 방법
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processOrder(Order order) {
try {
// 주문 처리
paymentService.processPayment(order);
} catch (PaymentException e) {
// 별도 트랜잭션으로 처리
compensationService.handleFailure(order);
throw e;
}
}
}
디버깅 팁
1. 상세 로그 활성화
# application.properties
logging.level.org.springframework=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.com.example=TRACE
# 예외 스택 트레이스 전체 출력
server.error.include-stacktrace=always
server.error.include-message=always
2. Actuator 활용
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoints.web.exposure.include=health,info,beans,env,metrics
management.endpoint.health.show-details=always
3. 조건부 브레이크포인트
// IntelliJ에서 브레이크포인트 우클릭 → Condition
user.getId() == 123L && user.getStatus() == Status.ERROR
4. 예외 발생 위치 추적
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Unhandled exception occurred", e);
// 스택 트레이스 분석
StackTraceElement[] stackTrace = e.getStackTrace();
String location = stackTrace.length > 0 ?
stackTrace[0].toString() : "Unknown";
return ResponseEntity.status(500)
.body(new ErrorResponse("Internal Error", location));
}
}
마무리
Spring Boot 개발 중 만나는 오류들은 처음에는 당황스럽지만, 각각의 오류는 명확한 원인과 해결책을 가지고 있습니다. 이 가이드를 참고하여 오류를 빠르게 해결하고, 더 나아가 오류가 발생하지 않는 견고한 코드를 작성하시기 바랍니다.
핵심 팁
- 🔍 에러 메시지를 꼼꼼히 읽기 - 대부분의 힌트가 들어있음
- 📝 로그 레벨 조정 - 개발 중에는 DEBUG 레벨 활용
- 🧪 단위 테스트 작성 - 오류를 사전에 방지
- 📚 공식 문서 참고 - Spring Boot Reference Documentation
- 💬 커뮤니티 활용 - Stack Overflow, GitHub Issues
체크리스트
- ✅ 의존성 버전 호환성 확인
- ✅ 설정 파일 문법 검증
- ✅ 데이터베이스 연결 정보 확인
- ✅ 적절한 예외 처리
- ✅ 트랜잭션 경계 설정
- ✅ 보안 설정 검토
태그: #SpringBoot #Troubleshooting #ErrorHandling #Debugging #Java
'유용한 컴공 테크닉' 카테고리의 다른 글
Claude Code 완벽 가이드: AI와 함께하는 차세대 코딩 경험 (5) | 2025.07.23 |
---|---|
Thymeleaf 오류 완벽 해결 가이드: 빨간 화면과 작별하기 (4) | 2025.07.21 |
Docker로 Spring Boot 애플리케이션 배포하기: 개발부터 운영까지 완벽 가이드 (3) | 2025.07.21 |
Spring Boot와 MariaDB/MySQL 연결하기: H2에서 실전 DB로 레벨업 (0) | 2025.07.21 |
Spring Boot + Thymeleaf로 웹 페이지 만들기: Bootstrap을 곁들인 CRUD 완성하기 (1) | 2025.07.20 |