minwoorich / 2024-spring-jpa-study

4 stars 5 forks source link

스프링부트와 자동 설정 #55

Open minwoorich opened 3 months ago

minwoorich commented 3 months ago

1. AutoConfiguration 이란?

스프링 부트의 핵심이자 꽃이라 볼 수 있는 기능이 바로 이 AutoConfiguration 기능이다.

Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added.

스프링 부트의 auto-configuration 은 jar 기반의 스프링 어플리케이션을 자동으로 설정해준다.

스프링 부트 공식 가이드에 나온 설명인데 좀 빈약한 것 같다.

자동으로 설정? 뭘 자동 설정 해준다는거지? 컴포넌트 스캔을 말하는건가?

처음부터 스프링 부트를 사용하신 분들이라면 조금 와닿지 않을 수 있기 때문에 예시 코드와 함께 설명을 하도록 하겠다.

우선 모델과 레포지토리를 먼저 간단하게 정의해주자.

Member 및 MemberRepository

@Data
public class Member {

    private String memberId;
    private String name;

    public Member() {
    }

    public Member(String memberId, String name) {
        this.memberId = memberId;
        this.name = name;
    }
}

@Repository
public class MemberRepository {

    // JdbcTemplate 사용
    public final JdbcTemplate template;

    public MemberRepository(JdbcTemplate template) {
        this.template = template;
    }

    public void initTable() {
        template.execute("create table member(member_id varchar primary key, name varchar)");
    }

    public void save(Member member) {
        template.update("insert into member(member_id, name) values(?,?)",
                member.getMemberId(),
                member.getName());
    }

    public Member find(String memberId) {
        return template.queryForObject("select member_id, name from member where member_id=?",
                BeanPropertyRowMapper.newInstance(Member.class),
                memberId);
    }

    public List<Member> findAll() {
        return template.query("select member_id, name from member",
                BeanPropertyRowMapper.newInstance(Member.class));
    }
}

DB 연동하기 (스프링 부트 사용 X)

스프링과 DB를 연동하기 위해서는 가장 먼저 해줘야 할것이 있는데 바로 DataSource 객체 그리고 TranscationManager를 빈으로 등록해줘야한다. 참고로 어차피 AutoConfiguration 기능을 설명하는 시간이기 때문에 굳~이 DB를 연동한다고 MySQL 이니 오라클이니 설치 할 필요 없이 스프링에 기본적으로 H2 DB 가 내장되어있으니 그걸 사용하면된다.

DbConfig

@Slf4j
@Configuration 
public class DbConfig {

    @Bean
    public DataSource dataSource() {
        log.info("dataSource 빈 등록"); // 빈이 잘등록 되었는지 로그 찍기
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setJdbcUrl("jdbc:h2:mem:test");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    @Bean
    public TransactionManager transactionManager() {
        log.info("transactionManager 빈 등록"); // 빈이 잘등록 되었는지 로그 찍기
        return new JdbcTransactionManager(dataSource());
    }

    //  DB 액세스 기술 : 편의를 위해 등록함
    @Bean
    public JdbcTemplate jdbcTemplate() {
        log.info("jdbcTemplate 빈 등록"); // 빈이 잘등록 되었는지 로그 찍기
        return new JdbcTemplate(dataSource());
    }
}

이제 설정을 완료 했으니 DB에 데이터를 저장 및 조회 해볼까?

테스트 코드

간단하게

1) 테이블 생성 2) 멤버 저장 3) 멤버 조회

를 테스트 코드로 작성하고 돌려보자.

결과는 당연히 성공! 빈 등록할 때 찍어둔 로그도 잘 출력되는것을 확인 할 수 있다.

스프링 부트가 아닌 순수 스프링만 사용하는 경우 이와 같이 우리가 직접 빈을 @Configuration 클래스를정의해서 등록해줘야한다. 물론 컴포넌트 스캔을 이용하면 조금 더 편하게 빈을 등록 할 순 있지만, 이미 스프링 혹은 라이브러리로 딸려오는 객체들을 사용하기 위해서는 우리가 직접 빈을 등록해줘야하는것이다.

하지만 스프링 부트를 사용한다면???

DB 연동하기 (스프링 부트 사용 O)

@Slf4j
// @Configuration -> 주석 처리!!!
public class DbConfig {

    @Bean
    public DataSource dataSource() {
        log.info("dataSource 빈 등록"); // 빈이 잘등록 되었는지 로그 찍기
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setJdbcUrl("jdbc:h2:mem:test");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    @Bean
    public TransactionManager transactionManager() {
        log.info("transactionManager 빈 등록"); // 빈이 잘등록 되었는지 로그 찍기
        return new JdbcTransactionManager(dataSource());
    }

    //  DB 액세스 기술 : 편의를 위해 등록함
    @Bean
    public JdbcTemplate jdbcTemplate() {
        log.info("jdbcTemplate 빈 등록"); // 빈이 잘등록 되었는지 로그 찍기
        return new JdbcTemplate(dataSource());
    }
}

설정 클래스가 동작이 안 되도록 @Configuration 을 주석 처리 해보자!

테스트 코드

그러고 동일한 테스트 코드로 테스트를 실행해보면??

결과는 마찬가지로 성공!! 혹시나 @Configuration 어노테이션이 없어도 DBConfig 클래스를 어떻게 어떻게 읽어들인 것 아닌가 라는 의심이들어 로그창을 확인했지만 우리가 찍어둔 로그 메세지는 찾을 수 없었다.

즉, 어디선가 빈을 자동으로 등록 해주고 있는것이다!

이게 바로 스프링부트의 AutoConfiguration 기능이다. 우리가 빈으로 등록하지 않았더라도 자동으로 어디선가 빈이 등록되어 해당 기능을 사용할 수 있는것이다. 이 얼마나 편리한 기능 아닌가?

그래도 아직 뭐가 편한지 잘 모르겠다고?

그럼 라이브러리들을 한번 살펴보자.

알다시피 스프링은 이미 수 많은 객체들을 라이브러리에 한 데 묶어서 종합 선물 세트 마냥 제공 해준다. 그럼 개발자들은 웬만한 경우 굳이 따로 구현할 필요 없이 스프링에서 제공해주는 객체를 사용 만하면 된다.

하지만, 그냥 사용할 순 없고 사용하기 위해서는 결국 빈으로 등록을 해줘야 한는것인데,,,

문제는 얼마나 많은 클래스들이 라이브러리에 포함되어있는데 내가 라이브러리의 기능을 사용하기위해 어떤 걸 빈으로 등록 해야하며 빈들간의 복잡한 의존 관계들을 전부 파악하며 주입 을 한다는것은 결코 쉬운 작업이 아니다.

그래서 스프링 부트가 이 설정 마져도 자동화하여 개발자들에게 제공해준다는 것이다. 즉, 우리의 자애로운 스프링님께서 밥상을 차려준 것에 모자라 떠먹여주기까지 하는 셈이다.

2. AutoConfiguration 파헤치기

자동 설정 파일 경로

External Libraries -> org.springaframework.boot:spring-boot-autoconfigure

위 경로는 스프링 부트의 "자동 설정" 과 관련된 모든 클래스들을 담아 놓은 경로이며 그곳엔 또 수 많은 패키지들이 존재한다.

한번 jdbc 패키지에 들어가보자.

그러면 아래 사진과 같이 이미 JDBC 관련 구현체들이 준비 되어있는 것을 확인할 수 있다.

객체들의 이름을 자세히보면 죄다 XXXConfiguration 인 클래스들이 많다. 여기서 아마 눈치를 어느정도 채신분들도 있을것이다. 이미 스프링에서 설정 클래스들을 제공하고 있는셈이다. 즉, 조립할 재료조립 사용 설명서 모두 라이브러리에 포함되어 제공하고 있다. (※ 참고로 조립할 재료에 해당하는 클래스는 이 경로가 아니라 다른 라이브러리 패키지 경로에 포함되어있다.)

하지만 놀라운건 또 있다.

그럼 한번 JdbcTemplateAutoConfiguration 을 한번 들어가 확인해보자.

지금 이 클래스가 하는일이 바로 조립할 재료와 조립 사용 설명서를 가지고 조립까지 끝 마쳐서 제공 하는것이다.

코드가 어노테이션만있고 본문이 없어서 이해하기 어려우니 하나하나 살펴보도록하자.

@AutoConfiguration

@AutoConfiguration

자동 구성을 사용하기 위해서 등록해야하는 어노테이션이다. 내부적으로 @Configuration 이 등록되어있기 때문에 설정 파일로 사용 할 수 있다.

(after = DataSourceAutoConfiguration.class)

DataSource 가 빈으로 설정 된 이후에 해당 클래스(JdbcTemplateAutoConfiguration) 을 설정하라는 뜻이다. 왜냐하면 JdbcTemplate 을 사용하기 위해서는 당연히 DataSource 가 먼저 등록이 되어있어야하기 때문이다.

@Conditional

@ConditionalXXX 이름에서 알 수 있다시피 자동 설정이 조건부로 동작되게끔 할 때 사용하는 어노테이션이다.

@ConditionalOnClass 는 명시된 클래스들이 클래스패스 (build > classes) 에 존재 할 경우 자동 설정을 수행한다는 뜻이다. @ConditionalOnSingleCandidate 는 명시된 클래스가 스프링 컨테이너에 오직 하나만 등록되어있을때 자동 설정을 수행한다는 뜻이다. 하지만, 여러개 빈이 등록 되어 있더라도 빈들 중 하나가 Primary가 지정되어 있다면 그때도 자동 설정이 이뤄질 수 있다.

@ConditionallXXX 때문에 자동 설정 클래스가 아닌 우리가 작성한 DBConfig 동작했던것이다.

아마 거의 대부분의 auto-coinfiguration 클래스에는 @Conditional 이 등록되어있을 것이다. 왜냐하면 굳이 개발자가 수동으로 빈을 등록했다는것은 분명히 의도가 있다는 것이고 그런 경우엔 자동이 아닌 수동으로 설정한 값이 적용되어야하는게 상식적으로 맞는 흐름이다.

또한 언급된 @Conditional 어노테이션외에 더 많은 시리즈들이 존재하므로 더 알고 싶으신분들은 ➡️ 이곳 을 한번 참고하시라.

@EnableConfigurationProperties

@EnableConfigurationProperties(JdbcProperties.class) 명시된 클래스(JdbcProperties)를 외부 설정 값으로 사용하겠다는 어노테이션이다.

예를 들어 DB를 연동할 때 우린 DB드라이버, DB명, DB의 유저명, DB 비밀번호 등 각종 속성 값들을 지정해줘야한다.

이런건 어플리케이션 내부에서 주입되는게 아니라 개발자가 직접 외부에서 주입시켜주는 값 들이다. 그래서 외부 설정 값이라고도 불리며 스프링 부트를 이용한 분들이라면 application.properties 혹은 application.yml 이라는 파일에 해당 속성 값들을 저장해서 사용했을 것이다.

문제는 해당 파일은 글자를 틀리거나 타입이 틀려도 컴파일에러가 발생하지 않기 때문에 여러가지 에로사항들이 많다. 그래서 이런 이슈를 줄이고자 "DB드라이버, DB명, DB의 유저명, DB 비밀번호 " 만 필드로 가지고 있는 클래스를 따로 만들어서 빈으로 주입하여 해당 빈 객체로부터 값들을 주입 받을 수 있도록 하였다.

그렇게 되면 자바 코드로 관리를 하기 때문에 자바 컴파일러가 컴파일 에러를 잡아 줄 수 있는 것이다. 그리고 @Valid 어노테이션을 사용하면 값들의 유효 범위 등도 정해줄 수 있다. 예를 들어, 커넥션 풀이 제공하는 max 커넥션 수의 범위 를 임의로 개발자가 지정해놓음으로써 보다 안전한 설정이 가능하다.

@Import

@Import({ 클래스1, 클래스2, 클래스3 }) 사실 이 어노테이션은 많이 봤을건데 명시된 클래스들의 설정 정보도 같이 사용하겠다는 의미이다. 참고로 명시된 클래스들은 일반적으로 @Configuration 클래스들이다.

JdbcTemplateConfiguration

Import 된 설정 클래스들 중에 눈에 딱 들어오는 것이 있었으니 바로 JdbcTemplateConfiguration 이다.

우리가 따로 빈으로 등록하지 않더라도 스프링 부트가 기본적으로 JdbcTemplateConfiguration 을 임포트하여 JdbcTemplate 을 빈으로 등록해주고 있는것이다. 게다가 방금 전에 설명했던 외부설정값 객체인 JdbcProperties 를 주입 받아서 fetchSize , maxRows 등을 설정해주는것을 확인할 수 있다.

정리

// 자동 설정을 기능을 제공해줍니다. 다만 DataSourceAutoConfiguration 설정 이후에 동작합니다.
@AutoConfiguration(after = DataSourceAutoConfiguration.class)

// DataSource, JdbcTemplate 객체가 classpath 에 존재할 때 동작합니다.
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })

// 오직 한개의 DataSource 가 컨테이너에 빈으로 등록되어있을때만 동작합니다. (Primary면 다중 등록시에도 사용 가능) 
@ConditionalOnSingleCandidate(DataSource.class)

// JdbcProperties 에 담긴 값을 외부 설정 값으로 사용할겁니다.
@EnableConfigurationProperties(JdbcProperties.class)

// DatabaseInitializationDependencyConfigurer, JdbcTemplateConfiguration,
// NamedParameterJdbcTemplateConfiguration 들을 같이 사용할 겁니다.
@Import({ DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class,
        NamedParameterJdbcTemplateConfiguration.class })

public class JdbcTemplateAutoConfiguration {

}

즉, JdbcTemplateAutoConfigurationJdbcTemplate어떻게 자동 설정되는지 정의 되어있는 클래스인 것이다.

본 포스트는 [스프링 부트 - 핵심 원리와 활용] (https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B5%EC%8B%AC%EC%9B%90%EB%A6%AC-%ED%99%9C%EC%9A%A9/dashboard) 을 보고 정리했습니다.