하이어코딩 RSS 태그 관리 글쓰기 방명록 mahiru
2025-07-21 19:26:43
728x90
반응형

🚨 들어가며

"왜 안 되지?"

Spring Boot 개발을 하다 보면 수많은 오류를 만나게 됩니다. 빨간색 에러 로그를 보면 막막하지만, 사실 각 오류는 우리에게 문제를 해결할 힌트를 제공합니다.

이 글에서는 Spring Boot 개발 중 자주 만나는 오류들을 체계적으로 정리하고, 각 오류의 원인과 해결 방법을 실제 사례와 함께 설명합니다. 이 가이드를 통해 더 이상 에러 메시지가 두렵지 않게 될 것입니다!

📑 목차

  1. 시작 단계 오류
  2. 의존성 관련 오류
  3. 데이터베이스 연결 오류
  4. JPA/Hibernate 오류
  5. REST API 오류
  6. 보안 관련 오류
  7. 설정 관련 오류
  8. 런타임 오류

시작 단계 오류

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: 데이터베이스 설정 추가

 
properties
# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

방법 2: 자동 설정 제외

 
java
@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: 포트 변경

 
properties
# application.properties
server.port=8081

방법 2: 사용 중인 프로세스 종료

 
bash
# Windows
netstat -ano | findstr :8080
taskkill /PID <PID> /F

# Mac/Linux
lsof -i :8080
kill -9 <PID>

방법 3: 랜덤 포트 사용

 
properties
server.port=0

3. 메인 클래스를 찾을 수 없음

오류 메시지

 
Error: Could not find or load main class com.example.Application
Caused by: java.lang.ClassNotFoundException: com.example.Application

원인

  • 잘못된 패키지 구조
  • 빌드 문제
  • IDE 설정 오류

해결 방법

 
xml
<!-- 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: 어노테이션 추가

 
java
@Service  // 이 어노테이션 추가!
public class UserService {
    // ...
}

방법 2: 컴포넌트 스캔 설정

 
java
@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "com.other.package"})
public class Application {
    // ...
}

방법 3: @Bean 직접 정의

 
java
@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 사용

 
java
@Service
public class UserService {
    private final OrderService orderService;
    
    public UserService(@Lazy OrderService orderService) {
        this.orderService = orderService;
    }
}

방법 2: Setter 주입 사용

 
java
@Service
public class UserService {
    private OrderService orderService;
    
    @Autowired
    public void setOrderService(OrderService orderService) {
        this.orderService = orderService;
    }
}

방법 3: 설계 개선 (권장)

 
java
// 중간 서비스나 이벤트 기반으로 변경
@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 사용

 
java
@Component
@Primary  // 기본으로 사용될 Bean 지정
public class EmailSender implements MessageSender {
    // ...
}

방법 2: @Qualifier 사용

 
java
@Service
public class NotificationService {
    
    public NotificationService(@Qualifier("emailSender") MessageSender sender) {
        this.sender = sender;
    }
}

방법 3: 모든 구현체 주입

 
java
@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: 연결 정보 확인

 
properties
# 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: 타임아웃 설정

 
properties
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)

원인

  • 잘못된 사용자명/비밀번호
  • 권한 부족

해결 방법

 
sql
-- 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 드라이버 의존성 누락

해결 방법

 
xml
<!-- 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 로딩 (비추천)

 
java
@Entity
public class Order {
    @ManyToOne(fetch = FetchType.EAGER)  // LAZY → EAGER
    private User user;
}

방법 2: @Transactional 확대

 
java
@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 사용 (권장)

 
java
@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

 
java
@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 설정

 
java
@Entity
public class Post {
    @OneToMany(cascade = CascadeType.ALL)  // CASCADE 추가
    private List<Comment> comments;
}

방법 2: 명시적 저장

 
java
@Transactional
public void createPost(Post post) {
    // 연관된 엔티티 먼저 저장
    for (Comment comment : post.getComments()) {
        commentRepository.save(comment);
    }
    postRepository.save(post);
}

3. N+1 문제

증상

 
sql
-- 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

 
java
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();

방법 2: @EntityGraph

 
java
@EntityGraph(attributePaths = {"user", "items"})
List<Order> findAll();

방법 3: Batch Size

 
properties
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

원인

  • 중복된 값으로 저장 시도

해결 방법

 
java
@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

해결 방법

클라이언트 측

 
javascript
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'  // 필수!
    },
    body: JSON.stringify(data)
});

서버 측

 
java
@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: 알 수 없는 필드 무시

 
java
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserDto {
    // ...
}

방법 2: 전역 설정

 
properties
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"

해결 방법

 
java
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

 
java
@RestController
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
    // ...
}

방법 2: 전역 설정

 
java
@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"
}

원인

  • 인증 토큰 없음
  • 만료된 토큰

해결 방법

 
java
@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)

 
java
http.csrf(csrf -> csrf.disable());

권한 확인

 
java
@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

해결 방법

 
java
@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

원인

  • 잘못된 프로퍼티 형식
  • 타입 변환 실패

해결 방법

 
java
@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
    }
}
 
yaml
# application.yml
app:
  upload:
    max-file-size: 10MB  # 또는 10485760

2. 프로파일 활성화 오류

증상

 
No active profile set, falling back to default profiles: default

해결 방법

방법 1: 실행 시 지정

 
bash
java -jar app.jar --spring.profiles.active=prod

방법 2: 환경 변수

 
bash
export SPRING_PROFILES_ACTIVE=prod

방법 3: application.properties

 
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]]

해결 방법

 
xml
<!-- 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 필드 접근

해결 방법

 
java
@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() 순환 참조
  • 재귀 호출

해결 방법

 
java
@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 옵션 조정

 
bash
java -Xms512m -Xmx2048m -jar app.jar

페이징 처리

 
java
@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

원인

  • 중첩된 트랜잭션에서 롤백 마크
  • 예외 발생 후 계속 진행

해결 방법

 
java
@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. 상세 로그 활성화

 
properties
# 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 활용

 
xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
 
properties
management.endpoints.web.exposure.include=health,info,beans,env,metrics
management.endpoint.health.show-details=always

3. 조건부 브레이크포인트

 
java
// IntelliJ에서 브레이크포인트 우클릭 → Condition
user.getId() == 123L && user.getStatus() == Status.ERROR

4. 예외 발생 위치 추적

 
java
@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

728x90
반응형
이 페이지는 리디주식회사에서 제공한 리디바탕 글꼴이 사용되어 있습니다.