💋 문제 상황
코드를 작성하던 도중 예상치 못한 에외를 만났다.
에러가 발생한 코드는 아래와 같다.
@Repository
public final class DbGameDao implements GameDao {
public DbGameDao(DataSource dataSource) {
...
}
public Number insertGame(TryCount tryCount) {
...
}
public List<Integer> selectGameIds() {
...
}
}
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webController’
예외 메세지가 상당히 길고 복잡한데,
중요한 말들을 뽑아서 보아하니
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webController’
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'gameService’
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dbGameDao’
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class racingcar.dao.DbGameDao
Caused by: java.lang.IllegalArgumentException: Cannot subclass final class racingcar.dao.DbGameDao
해석해 보자면...
webController 이름의 빈을 못 만들겠는데, 그건 gameService 이름의 빈을 못 만들기 때문인데, 그건 또 dbGameDao 이름의 빈을 못 만들기 때문이라는 것 같다.
dbGameDao를 이름의 빈을 못 만드는 이유는 DbGameDao에 대해서 CGLIB subclass를 생성할 수 없어서인데, 이건 DbGameDao가 final class이기 때문이라고 한다.
뭥미???
💋 문제 해결
final을 떼고 다시 실행해 보았다.
@Repository
public class DbGameDao implements GameDao {
public DbGameDao(DataSource dataSource) {
...
}
public Number insertGame(TryCount tryCount) {
...
}
public List<Integer> selectGameIds() {
...
}
}
이렇게 변경하니 에러가 싹 사라졌다. 어떤게 문제였을까…?
스프링 프레임워크에서 @Repository 객체는 @Component 어노테이션을 상속받아서 기능을 확장한다.
예외 처리에 대한 기능 등을 추가로 가지고 있다.
따라서 스프링 IoC 컨테이너는 부가 기능을 위해 예외 처리에 대한 기능을 추가해 확장한 객체를 생성하고, 이 객체에서 예외 처리 등의 기능을 수행한다. 이렇게 확장한 객체를 프록시 객체라고 한다. 따라서 @Repository를 붙인 클래스를 빈으로 관리하려면, DBGameDao 객체를 확장한 프록시 객체가 필요하게 된다.
기능 확장에는 상속, 인터페이스, 조합 등등 다양한 방법이 사용될 수 있다.
이중 Spring에서 사용하는 것은 CGLIB라는 라이브러리로, 상속의 방법을 사용하는 라이브러리이다.
따라서 상속이 불가능한 final class는, 프록시 객체를 생성할 수 없기 때문에 빈을 생성할 수 없다.
참고: 부가기능을 위해 프록시 객체를 사용하지 않고, 어노테이션을 작성한 객체를 그대로 사용하는 경우라면, final class로 만들어도 괜찮다.
✔ 프록시 객체란?
프록시 객체는 실제 객체 대신 클라이언트의 요청을 처리하는 객체다.
클라이언트로부터 들어온 요청은 곧바로 실제 객체로 가지 않고, 프록시 객체에게로 간다. 그러면 프록시 객체 내부에서 실제 객체를 호출한다. 따라서 프록시 객체는 실제 객체의 대리자 역할을 하는 객체라고 볼 수도 있다. 실제 객체에 곧장 접근하는 것을 막기 때문에 객체의 접근 제어를 위해 사용될 수도 있다.
참고로 프록시 객체는 객체 생성 비용이 높은 경우, 객체의 생성 시점을 늦춰서 필요할 때 생성하는 지연 로딩을 구현할 때 사용되기도 한다.