데이터베이스 예약어 예외

후추·2023년 4월 30일
0

예외

자바, 스프링 환경에서 H2 데이터베이스를 사용하던 중 다음과 같은 상황을 마주했다.

예외 메시지를 읽어보니 여러 예외가 중첩되어 있었다. 하나씩 살펴보았다.

Controller 예외

org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'productApiController' defined in file [/.../ProductApiController.class]:
Unsatisfied dependency expressed through constructor parameter 0;

첫 번째 예외는 UnsatisfiedDependencyException 이었다.

ProductApiController 클래스의 bean을 생성할 때 오류가 발생했고, 생성자의 0번째 파라미터의 의존성이 충족되지 않은 것으로 나타났다.

ProductApiController 의 생성자는 아래와 같다. 0번째 파라미터인 ProductService 에서 문제가 생긴 것으로 파악할 수 있었다.

@RestController
@RequestMapping("/admin")
public class ProductApiController {

    private final ProductService productService;
    
    public ProductApiController(final ProductService productService) {
        this.productService = productService;
    }

	//...
}

Service 예외

nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'productService' defined in file [/.../ProductService.class]:
Unsatisfied dependency expressed through constructor parameter 0;

Service 에서도 첫 번째와 동일한 UnsatisfiedDependencyException 예외가 발생했다.

ProductService 의 생성자에서 의존성이 충족되지 못했다. ProductService의 코드는 다음과 같다.

ProductService 의 생성자는 Dao를 주입받고 있었다.

@Service
public class ProductService {

    private final ProductDao productDao;

    public ProductService(final ProductDao productDao) {
        this.productDao = productDao;
    }
    
	// ...
}

코드를 그림으로 간략히 표현하면 다음과 같았다.

Controller 에서 Service를 주입받고, Service에서 Dao를 주입받았다.

앞서 Controller 에서 Service의 의존성에서 문제가 생긴 것이 당연했다. Service에서 Dao에 대한 의존성이 충족되지 못해 예외가 발생했기 때문이다. Service 에서 발생한 예외가 Controller 까지 올라간 것이었다.

그렇다면, Dao 에서는 무슨 일이 일어났을까?

Dao 예외

nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'productDao' defined in file [/.../ProductDao.class]:
Unsatisfied dependency expressed through constructor parameter 0;

ProductDao 클래스에서도 위와 동일하게 UnsatisfiedDependencyException 이 발생했다.

ProductDao 역시 생성자의 0번째 파라미터의 의존성이 충족되지 못했다.

@Repository
public class ProductDao {

    private final JdbcTemplate jdbcTemplate;

    public ProductDao(final JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
	// ...
}

하지만 ProductDao 생성자의 0번째 파라미터는 JdbcTemplate 이었다.

JdbcTemplate 객체의 의존성이 어째서 충족되지 못했는지 의아했다.

SQL 예외

nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [/.../DataSourceInitializationConfiguration.class]:
Invocation of init method failed;

nested exception is org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #2 of URL [/.../data.sql]:
CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, email VARCHAR(320) NOT NULL, password VARCHAR(20) NOT NULL, PRIMARY KEY (id) );

JdbcTemplate에서 문제가 발생했던 이유는 다른 예외 메시지를 통해 확인할 수 있었다.

상위 예외는 BeanCreationException로 DataSourceInitializationConfiguration 클래스의 bean을 생성하지 못해서 일어난 예외였다.

하위 예외는 ScriptStatementFailedException 이었는데, SQL script 를 실행하는데 실패했다는 내용이었다.

정리

예외 상황을 정리하면 다음과 같았다.

  • SQL script 를 실행하는데 실패했다.
  • 따라서 DataSourceInitializationConfiguration 의 bean 을 생성할 수 없었다.
  • DataSource 관련 문제가 생기니 JdbcTemplate 역시 문제가 생겼다.
  • 그로 인해 ProductDao에서 JdbcTemplat 에 대한 의존성이 충족되지 못했고, 결국 Controller, Service 모두 연쇄적으로 의존성이 충족되지 못했다.

SQL 예약어 문제

nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "CREATE TABLE [*]user ( id INT NOT NULL AUTO_INCREMENT, email VARCHAR(320) NOT NULL, password VARCHAR(20) NOT NULL, PRIMARY KEY (id) )"; expected "identifier";

마지막 예외는 JdbcSQLSyntaxErrorException 였다.

SQL 문법 문제로 인해 발생한 예외였다.

예외 메시지는 SQL 문에서 어떤 부분이 문제였는지 표시하고 있었고, 해당 부분에 식별자(identifier)가 있어야 한다고 전해주었다.

([*] 는 SQL 문에서 문제가 되는 부분을 표시하는 기호이다.)

작성해둔 SQL 문을 다시 살펴보았다.

CREATE TABLE user
(
    id       INT          NOT NULL AUTO_INCREMENT,
    email    VARCHAR(320) NOT NULL,
    password VARCHAR(20)  NOT NULL,
    PRIMARY KEY (id)
);

SQL 자체에는 문법적으로 잘못된 부분이 없어 보였다.

그래서 식별자(identifier)가 예상되었다는 말에서 힌트를 얻어 구글링하고, H2 공식문서를 뒤졌다.

결국 user가 H2 데이터베이스의 예약어임을 알게 되었다.

공식문서를 통해 identifiers 라는 말도 이해가 됐다.

위와 같은 맥락에서 identifiers 는 table 이름, column 이름 등을 가리키는 말이었다.

해결

user가 예약어이기 때문에 생긴 문제임을 알았으니 이를 해결해보자.

세 가지 방식을 택할 수 있다.

1. "user" 큰 따옴표로 감싼다.

CREATE TABLE "user"
(
    id       INT          NOT NULL AUTO_INCREMENT,
    email    VARCHAR(320) NOT NULL,
    password VARCHAR(20)  NOT NULL,
    PRIMARY KEY (id)
);

위와 같이 Table의 이름 user를 큰 따옴표로 감싸주면, "user"를 예약어로 인식하지 않는다.

하지만 이 방법은 다른 SQL 문을 작성할 때에도 항상 "user"로 작성해야 하기 때문에 번거롭다.

만일 따옴표를 작성하는 것을 잊는다면 바로 예외를 발생시키게 된다.

2. NON_KEYWORDS=USER 설정한다.

NON_KEYWORDS 설정은 데이터베이스에서 사용하는 키워드(예약어)를 일반 식별자로 사용할 수 있도록 하는 기능이다.

데이터베이스 URL에 NON_KEYWORDS=USER 로 설정하면 user를 이름으로 사용할 수 있다.

다만 공식문서는 이 설정에 대해 부정적으로 언급한다.

This setting may break some commands and should be used with caution and only when necessary. Use quoted identifiers instead of this setting if possible. - h2 database

이 설정으로 인해 일부 명령이 손상될 수 있으므로, 필요한 경우에만 주의해서 사용해야 한다고 적혀있다.

또한 가능하다면 따옴표로 묶은 식별자를 사용하라고 전한다.

3. user 를 포기한다.

user 라는 이름은 서비스 사용자의 데이터를 가리키기 위해 지은 이름이다.

사용자를 꼭 user로 지칭할 필요는 없다.

user를 대신해서 customer, consumer, member 등을 사용할 수 있다.

만약 도메인 코드(가령 자바 클래스 등)와 이름의 통일성을 맞추기 위함이라면, users로 교묘하게 예약어를 피해갈 수도 있다.

이러한 방법은 따옴표를 사용하는 것만큼 번거롭거나, 데이터베이스의 설정을 바꿀만큼 위험하지도 않다.

따라서 user 를 포기하고 다른 이름을 사용하는 것이 합리적인 선택이다.

결론

user는 H2 데이터베이스의 예약어이다.

만약 SQL 문에 user를 Table, Column 등의 이름으로 사용한다면, SQL 문법 문제로 인한 예외를 마주하게 된다.

user 를 다른 이름으로 바꾸어 사용하자.

0개의 댓글

관련 채용 정보