[Spring] 멀티 모듈 다른 모듈 ComponentScan

Woo Yong·2024년 3월 16일
2

Spring

목록 보기
15/15
post-thumbnail

멀티 모듈 프로젝트에 공부를 하면서 에러를 발견해서 해당 글을 작성하게 되었습니다.
multi-module 프로젝트 하위에 domain, database, service라는 3개의 모듈을 생성하였습니다. 그리고 domain모듈에는 Entity, database모듈에는 Repository, service 모듈에는 Service 클래스를 생성하여 각 모듈에서 의존 객체를 주입받아서 단위 테스트를 진행 하던 중 Bean으로 등록되지 않아 문제를 해결하는 내용을 작성해보려고합니다.

Book.java

@Entity
@Builder
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
public class Book {
    // 현재는 Import 가 정상적으로 수행되지 않는 것을 확인 할 수 있다.
    // 왜냐하면 현재 module에 대한 의존성을 관리하는 build.gradle파일에는 lombok, jpa와 관련된 의존성이 없기 때문이다.
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private String content;
    private Integer stock;
}

BookeService

@Service
@RequiredArgsConstructor
@Slf4j
public class BookService {
    private final BookRepository bookRepository;

    public Book purchase(String title){
        Book book = bookRepository.findByTitle(title).get();
        if ( book == null){
            log.warn("Purcharse Book Title : {} does not exist" , title);
            throw new EntityNotFoundException("해당 도서는 존재하지 않습니다.");
        }
        return book;
    }
}

BookRepository

public interface BookRepository extends JpaRepository<Book, Long> {
    Optional<Book> findByTitle(String title);
}

위 코드를 기반으로 service 모듈에서는 purchase()메서드를 테스트하고, database모듈에서는 findByTitle() 메서드가 정상적으로 작동하는지 테스트 하였다.

service 모듈 테스트 코드

@SpringBootTest
@Slf4j
public class BookServiceTest {
    @Autowired
    BookService service;
    @Autowired
    BookRepository repository;
    @Test
    public void purchaseTest(){
        Book book = Book.builder().title("title 이랍니다.").build();
        repository.save(book);

        Book result = service.purchase(book.getTitle());
        Assertions.assertThat(book.getId()).isEqualTo(result.getId());

    }
}

database 모듈 테스트 코드

@DataJpaTest
@Import(DatabaseConfig.class)
@Slf4j
public class BookRepositoryTest {
    @Autowired
    BookRepository repository;

    @Test
    public void findByTitle_Test(){
        Book origin = Book.builder().title("Hello").build();
        repository.save(origin);
        Book result = repository.findByTitle(origin.getTitle()).get();
        Assertions.assertThat(origin).isEqualTo(result);
    }
}

하지만 두 개의 테스트가 모두 실패했다.... service 모듈 테스트 코드부터 살펴보자.

service 모듈 테스트 오류 원인

purchase 테스트 코드를 실행하면 BookRepository의 Bean을 주입받지 못한다는 에러 메시지를 확인할 수 있다.

그리고 코드를 실행하기 전에 컴파일 에러가 발생하는 것도 확인할 수 있다.

그리고 gradle 설정할 때 service의 dependencies에 implementatinon project(":database")를 추가해주어서 당연히 applicatin context에 당연히 Bean으로 등록될 줄 알았다.

에러를 해결하기 위해 각 모듈 별 ComponentScan 영역을 확인해봤다.
확인 결과로, 각 모듈은 main메서드를 가진 클래스에 SpringBootApplication 어노테이션은 해당 패키지 및 하위 패키지에 대해서만 자동 구성을 활성화하고 구성 요소를 스캔한다는 것을 확인했다.

따라서, service 모듈에서 테스트를 진행하면 service 패키지 하위의 Component만 Scan을 하기 때문에 import는 이루어지지만 Bean으로 주입이 안되는 것이었다.

service 모듈 해결방법

에러가 발생 원인은 sevice 모듈은 하위 패키지의 Componenet만 Scan하여 database에 존재하는 BookRepository를 Bean으로 주입하지 못하는 것이었다.
이를 해결하기 위해 service 모듈에 Component Scan범위를 database 패키지까지 포함하도록 수정하였다.

package com.example.service;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.example")
public class ServiceConfig {
}

위 코드와 같이 service 모듈에 Component Scan 영역을 기본값인 com.example.service가 아니라 com.example로 선언한 설정 파일을 Bean으로 등록해주었다.
그리고 테스트 코드를 다시 돌려보면.....

정상적으로 테스트가 통과되는 것을 확인할 수 있다.

그리고 database 모듈에도 동일하게 Component Scan 범위를 변경하여 설정파일을 Bean으로 등록하였다.

@Configuration
@ComponentScan("com.example")
public class DatabaseConfig {
}

하지만 에러가 발생했다...

database 모듈 테스트 오류 원인

사진과 같이 BeanCreationException예외가 발생하였다. 정상적으로 설정 파일을 Bean으로 등록했는데 에러가 발생하는 이유를 찾는데 해맸다..

에러의 원인으로는 @DataJpaTest 어노테이션은 일반 @Component@ConfigurationProperties 빈은 스캔되지 않는다는 것을 알게되었다.

즉, @DataJpaTest를 사용하면 @SpringBootTest과 달리 Bean을 전부 등록하지 않고 JPA 환경에 필요한 것들만 가지고 테스트를 한다는 것이다.

따라서 @DataJpaTest를 사용했기 때문에 우리가 만든 @Configuration 설정 파일도 Bean으로 등록하지 않아서 에러가 발생한 것이었다.

하지만 BookRepository는 database 모듈 하위에 존재하기 때문에 정상적으로 Bean으로 등록되어야한다.
에러 로그를 다시 확인해봤다.

에러 로그를 확인해보면
BookRepository를 생성할 때 제네릭 값으로 명시한 Entity 객체가 내부에서 Entity 객체로 인식되지 않는다는 문제이다.

즉, BookRepository의 Entity값으로 설정한 Book이 인식되지 않는 문제였다. 결국에는 Book은 domain 모듈에 존재하기 때문에 Bean으로 등록되지 않는 문제인 것이다.

database 모듈 해결 방법

이러한 문제를 해결하기 위해서는 @ContextConfiguration, @Import 어노테이션을 사용해서 스프링 컨텍스트에 대한 구성을 지정하여야한다.

@DataJpaTest
@ContextConfiguration(classes = DatabaseConfig.class)
@Slf4j
public class BookRepositoryTest {
    @Autowired
    BookRepository repository;

    @Test
    public void findByTitle_Test(){
        Book origin = Book.builder().title("Hello").build();
        repository.save(origin);
        Book result = repository.findByTitle(origin.getTitle()).get();
        Assertions.assertThat(origin).isEqualTo(result);
    }
}

정상적으로 테스트가 통과하는 것을 확인할 수 있었다.

What I Learned

  • 각 모듈 별로 Bean 등록은 동일한 경로에 패키지로 제한된다.
  • @DataJpaTest 은 @SpringBootTest와 달리 모든 Component를 스캔하지 않고 JPA와 관련된 내용만 Bean으로 등록한다.
profile
Back-End Developer

0개의 댓글