이 포스트는 인프런의 스프링부트 시큐리티 강의를 참고하여 작성되었습니다. 

 

 

프로젝트 구조

구글 로그인은 소셜로그인의 일종이기 때문에 스프링 시큐리티를 사용할 때 OAuth와 함께 사용해야 한다. 홈페이지에서는 일반 로그인과 구글로그인을 같이 구현하는 것이 목표이므로, 일반 로그인의 코드를 따로 건드리지 않고 구글 로그인 코드를 작성해 주어야 하겠다. 

 

일반 로그인 관련 코드는 auth라는 패키지에, 소셜 로그인 관련 코드는 oauth 패키지에 따로 묶어두자. 

 

스프링 시큐리티를 사용할 때 가장 기본이 되는 클래스는 SecurityConfig 클래스다. 이 클래스 내부에는 SecurityFilterChain 인스턴스를 리턴하는 filterChain() 메소드가 재정의된다. filterChain() 메소드는 스프링 시큐리티가 개입할 때 가장 먼저 호출되는 메소드이다.

 

이 클래스에 @EnableWebSecurity 어노테이션을 붙여서 내부에 정의된 filterChain() 메소드를 스프링 필터체인(filterChain)에 등록한다. 

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

    @Autowired
    public PrincipalOauth2UserService principalOauth2UserService;

    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.csrf(csrf -> csrf.disable());
        http
                .cors(cors -> cors.disable())
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/login", "/loginForm", "/loginProc", "/join", "/joinProc", "/user").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin((formLogin) ->
                        formLogin
                                .loginPage("/loginForm")
                                .loginProcessingUrl("/loginProc")
                                .defaultSuccessUrl("/"))
                .oauth2Login((oauth2Login) ->
                        oauth2Login
                                .loginPage("/loginForm")
                                .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(principalOauth2UserService))
                )
                .httpBasic(withDefaults());
        return http.build();
    }

}

 

HttpSecurity.cors(), HttpSecurity.csrf()의 내부에서는 CORS, CSRF 처리를 어떻게 할지를 정의할 수 있다. 

 

HttpSecurity.authorizeHttpRequests() 내부에서는 프로젝트의 각 엔드포인트에 대해 접근권한을 허용할지 말지를 결정할 수 있다. requestMatchers() 안에 url 패턴을 선언해서 해당 패턴들을 가진 엔드포인트들을 한 번에 처리할 수도 있다. 

 

뒤에 authenticated()를 붙이면 해당 엔드포인트들에 대해서는 인증을 실행하고, permitAll()을 붙이면 해당 엔드포인트에 대한 요청들은 별도의 인증을 안 하고 통과시킨다. 

 

HttpSecurity.formLogin() 내부에서는 시큐리티 기본 로그인을 어떻게 할지를 설정한다. 기본 로그인이란 아이디, 비밀번호를 입력해서 사용자를 인증하는 로그인이다. 메소드 내부에서는 .loginPage()로 어떤 엔드포인트를 기본 로그인 페이지로 설정할지를 정할 수 있다. 이 경우 인증되지 않은 사용자가 인증이 필요한 페이지를 요청할 경우 기본 로그인 페이지로 리다이렉트 된다. 

 

.loginProcessingUrl()은 로그인 과정을 처리하는 엔드포인트다. 보통 로그인 폼에서 '로그인'을 눌렀을 때 로그인 과정을 처리하는 엔드포인트로 이동하고, 해당 엔드포인트에서 로그인 인증이 진행된다. 

 

.defaultSuccessUrl()은 로그인이 성공했을 때 리다이렉트 될 엔드포인트이다. 

 

HttpSecurity.oauth2Login() 내부에서는 OAuth 로그인을 어떻게 설정할지를 작성한다. .loginPage()에서 설정한 엔드포인트로부터 들어오는 요청들에 대해서 oauth provider(구글, 네이버 등등)가 인증을 진행한다. 로그인 페이지 말고도 여러 엔드포인트를 설정할 수 있다. 

 

그 전에, OAuth에서는 3가지의 주체가 있다: Client, Resource Owner, Resource Server

client는 소셜로그인을 이용하려는 웹사이트의 서버, resource owner는 사용자, resource server는 구글 등 소셜로그인 서비스를 제공하는 서버이다. 

 

 

스프링 시큐리티 OAuth 2.0에서 지정한 엔드포인트(protocol endpoint)

1. Authorization Endpoint

client가 resource owner로부터 권한(authorization)을 획득하는 부분이다. 

즉 사용자(resource owner)의 로그인이 진행되는 부분이다. 사용자가 로그인을 진행하면 그 과정에서 앱 서비스(client)는 권한 코드(authorization code)를 얻을 수 있다. 사용자의 로그인이 성공적이었다면 1번에서 권한 코드를 얻을 수 있다. 

 

2. Token Endpoint

1번에서 얻은 권한 코드를 액세스 토큰으로 교환하는 지점이다. 

 

3. Redirection Endpoint

인증 서버(resource server)가 앱 서비스(클라이언트)에게 권한 정보(authorization)를 리턴하는 지점이다. 

 

4. UserInfo Endpoint

클라이언트가 인증된 사용자의 정보(claims)를 받기 위해서는 이 엔드포인트로 요청하면 된다. 

 

코드에서는 .userInfoEndpoint()로 사용자의 인증 정보를 받아서 어떻게 처리할지를 설정했다. 

.userInfoEndpoint().userService() 에서는 사용자의 인증 정보를 엔드포인트로 받아서 어떤 UserDetailsService 클래스에 넘겨줄지를 설정한다. 여기서는 앞에서 미리 작성한 principalOauth2UserService 클래스에게 유저 정보를 넘겨주기로 설정했다. 

 

 

OAuth 서비스 클래스 작성 - 기존 로그인 로직과 연동

principalOauth2UserService 클래스는 DefaultOAuth2UserService 클래스를 상속하고, DefaultOAuth2UserService 클래스는 OAuth2Service 인터페이스를 구현한다. 

OAuth2Service 인터페이스는 UserInfo Endpoint에서 유저 정보를 얻은 뒤 그에 맞는 OAuth2User 인터페이스 타입의 인스턴스를 리턴한다. 즉 우리 클래스는 이 인터페이스를 구현받은 클래스를 상속받고 있으므로 OAuth2Service 인터페이스의 역할 + a를 한다고 볼 수 있다. 

 

해당 클래스는 앞서 구현 및 상속받은 인터페이스, 클래스가 있기 때문에 loadUser() 라는 메소드를 재정의해야 한다. loadUser() 메소드는 UserInfo Endpoint에서 유저 정보를 얻은 뒤 그에 맞는 OAuth2User 인터페이스 타입의 인스턴스를 리턴한다. 인터페이스 타입을 리턴할 수는 없으므로, OAuth2User 인터페이스를 구현한 클래스를 하나 만든 뒤 그 클래스 타입의 인스턴스를 리턴시켜야 하겠다. 

 

(예시 코드)

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2User oauth2User = super.loadUser(userRequest);
        // code
        return new PrincipalDetails(userEntity, oauth2User.getAttributes());
    }
}

 

여기서 PrincipalDetails라는 클래스는 강의에서 정의한 클래스로, 기존 로그인에서 필요했던 UserDetails 인터페이스와, loadUser()의 리턴 타입인 OAuth2User 인터페이스를 구현한 클래스이다. 

 

앞서 기본 로그인을 진행하기 위해서는 PrincipalOauth2UserService와 같은 역할을 하는 서비스 클래스로 PrincipalDetailsService 클래스를 정의했었고, 해당 클래스는 UserDetailsService 라는 인터페이스를 구현하고 있었다. UserDetailsService 인터페이스에서는 loadUserByUsername() 메소드를 오버라이딩 해야 했는데, 이 메소드의 리턴 타입이 UserDetails 이었다.

 

즉 PrincipalDetailsService와 PrincipalOauth2UserService의 관계는 매우 비슷하다. 각각 UserDetailsService와 OAuth2Service 인터페이스를 구현하고, 그 인터페이스에 선언된 메소드에서는 각각 UserDetails와 OAuth2User 인터페이스 타입을 리턴한다. 

 

그러므로 PrincipalDetails 클래스에서 두 인터페이스(UserDetails, OAuth2User)를 모두 구현해버리면 하나의 클래스가 일반 로그인 서비스인 PrincipalDetailsService와 소셜로그인 서비스인 PrincipalOAuth2UserService 모두에서 쓰일 수 있게 된다. 

 

대신 일반 로그인과 소셜로그인의 처리 절차가 다를 수 있으므로, UserDetailsService의 loadUserByUsername() 메소드와 OAuth2UserService의 loadUser() 메소드를 각각 다르게 오버라이딩 해 주면 같은 엔드포인트와 같은 클래스 타입을 사용해서 두 가지의 로그인을 한번에 진행할 수 있다. 

 

구조 설명

 

해당 그림은 스프링 시큐리티 구조를 간단히 나타낸 것이다. 

 

앞서 언급한 PrincipalOauth2UserService와 PrincipalDetailsService는 그림에서 'UserDetailsService' 부분에 해당한다. 이 클래스들은 AuthenticationProvider에서 authentication 객체를 건네받은 뒤, 해당 객체를 통해 UserDetails 인터페이스에 접근해서 유저 정보를 가져온 뒤 다시 AuthenticationProvider에게 그 정보를 리턴하는 역할을 한다. 

 

또한 앞서 나왔던 PrincipalDetails는 UserDetails 부분에 해당한다. 보통 모델에서 사용하는 유저 클래스가 이 인터페이스를 구현한다. 이 인터페이스에는 스프링 시큐리티에서 해당 유저 모델을 사용하기 위해서 필요한 여러 필드와 메소드(개별 유저의 권한 리턴하기, 유저가 유효한지 확인하는 메소드 등)가 선언되어 있다. 

 

(이 포스트는 우테코 테코톡 영상 - Spring Security 편의 내용을 정리하기 위해서 작성했습니다.)

 

Spring Security는 내부적으로 여러 클래스와 인터페이스들을 거침으로써 웹에서 인증(Authentication)과 인가(Authorization) 작업을 쉽게 구현할 수 있도록 한다. 

 

Spring Security 구조

내부 구조는 크게 보면 다음과 같다. 

1. AuthenticationFilter

2. AuthenticationManager

3. AuthenticationProvider

4. UserDetailsService

5. UserDetails

6. SecurityContextHolder

 

(1) AuthenticationFilter

Spring Security의 구조를 크게 살펴보면, 들어온 요청은 제일 먼저 AuthenticationFilter를 거친다. AuthenticationFilter에서는 앞으로의 인증 과정에서 사용하기 위해 UsernamePasswordAuthenticationToken(일종의 토큰으로 보면 된다)을 만들고, 이를 다음 단계인 AuthenticationManager 인터페이스에 전달한다. 

나중에는 AuthenticationManager로부터 인증이 완료된 User 객체를 받아서 SecurityContextHolder에 전달하는 역할까지 맡는다. 

(SecurityContextHolder는 스프링 시큐리티의 In-Memory DB라고 보면 된다.)

 

(2) AuthenticationManager

AuthenticationManager는 사실 인터페이스이고, 이를 구현한 클래스로는 ProviderManager가 있다. 이 인터페이스/구현 클래스에서는 AuthenticationFilter에서 넘어온 토큰이 유효한지 확인한 뒤(validate token credentials) 다음 단계인 AuthenticationProvider로 넘겨준다. 

 

(3) AuthenticationProvider

인증 작업이 이뤄지는 단계다. 토큰의 정보가 인증 서버에 있는 credentials 정보와 일치하는지를 검사한다. 일치하지 않으면 AuthenticationException을 발생시키고, 일치한다면 Authentication 토큰 객체에 추가 정보를 담는다. 

 

(4) UserDetailsService

이름처럼 UserDetails의 서비스 클래스라고 이해했다. 인증이 완료된 토큰을 UserDetails 인터페이스에게 넘겨준다. 

 

(5) UserDetails

인터페이스로, User 클래스가 이를 구현한다. UserDetailsService에서 받은 토큰의 정보를 사용해서 인증 서버나 일반 DB에서 유저 정보를 조회한다. 그렇게 조회된 User 객체를 다시 UserDetailsService에 전달한다.

이 User 객체는 (4), (3), (2), (1)을 거쳐서 (6)의 SecurityContextHolder로 전달된다. 

 

(6) SecurityContextHolder

스프링 시큐리티에서 사용하는 인메모리 저장소로 볼 수 있는데, SecurityContextHolder 안 SecurityContext 안 Authentication 부분에 인증 관련 정보들이 저장된다. 

그리고 유저 상태를 authenticated로 바꾼 뒤 요청을 DispatcherServlet에게 넘긴다. 그러면 DispatcherServlet에서 유저가 원래 요청했던 URL에 해당하는 컨트롤러의 메소드를 호출하여 응답을 리턴하게 된다. 

 

참고한 컨텐츠

https://www.youtube.com/watch?v=aEk-7RjBKwQ 

 

AWS RDS란?

Relational Database Service으로, 데이터베이스를 AWS 인스턴스로 올려서 원격으로 접속 가능하게 하는 서비스이다. 

보통은 AWS EC2나 S3을 사용해서 서버 인스턴스를 배포하거나 파일을 저장하는 용도로 사용하지만, RDS를 사용하면 DB를 원격으로 접근 가능하게 할 수 있다. 

 

이번에 RDS를 쓴 이유도 같은 DB 스키마를 가지고 협업할 때 공통으로 접근할 수 있는 DB가 있으면 좋을 것 같아서였다. 

각자 로컬에 DB를 만들고 마이그레이션해도 되지만, 그러다가 DB가 꼬일 수도 있기 때문이다. 

 

AWS RDS와 스프링 서버를 연결하는 방법은 총 두 단계이다. 

1. AWS RDS 인스턴스 시작

2. 스프링 서버 설정 변경

 

이중 1번 과정에서는 한 유튜브 영상을 참고해서 진행하였다. 

 

AWS RDS 인스턴스 시작

우선 AWS RDS 페이지에 접속하거나 aws rds를 검색한다. 

데이터베이스 생성 버튼을 누른다. 

 

데이터베이스 생성 방법에 정답이나 매뉴얼은 없지만, 내가 한 방식을 그대로 올려보겠다. 

(모든 항목 내용은 없고, 변경한 부분 위주로 올려보겠다.)

 

 

엔진 옵션-엔진 유형: MySQL.

어떤 데이터베이스를 사용할지 정한다. 

 

엔진 옵션-엔진 버전: MySQL 8.0.32

해당 버전이 DB 버전이 된다.

 

템플릿: 프리 티어

프리 티어를 제외한 프로덕션과 개발/테스트는 모두 사용 시 요금이 부과된다. 

 

설정-DB 인스턴스 식별자

DB 이름을 입력한다. 각 DB 인스턴스에 고유한 이름을 붙여준다고 생각하면 된다. 

 

설정-마스터 사용자 이름

DB에 접근하기 위해서 아이디(유저 이름)와 비밀번호를 요구하는데, 이중 아이디 영역이다. 이후 MySQL Workbench와 스프링 설정 변경에서 사용된다. 

 

설정-마스터 암호

DB에 접근하기 위해서 필요한 비밀번호이다. 이후 MySQL Workbench 설정과 스프링 설정 변경 시 사용된다. 

 

인스턴스 구성-DB 인스턴스 클래스

DB 인스턴스를 어떤 타입으로 할지를 정의하며, 나는 기본값인 db.t3.micro를 선택했다. 

 

앞서 요금이 나오지 않는 프리 티어를 사용했는데, 프리티어도 사용 제한이 있다. 

총 750시간 이상 사용하거나, 사용한 지 12개월이 지난 경우부터는 과금이 되므로 주의하자!

 

어쨌든 이 과정을 완료하면 RDS DB 인스턴스가 생성된다. 생성 과정은 몇 분이 걸린다. 

이 과정을 완료하고 DB가 실행되면 다음과 같은 화면이 나온다. 

 

 

DB 식별자를 눌러서 상세보기를 하면, DB 인스턴스에 접근할 수 있는 URL이 나온다. 이 URL과 아까의 유저 이름, 비밀번호를 통해서 DB에 접근할 수 있다. 

 

✅스프링 서버 설정 변경

앞서 설정한 엔드포인트, 포트 번호, 유저 이름, 비밀번호를 사용해서 설정을 변경하면 된다. 

application.properties 파일에서 다음 내용만 바꾸자. 

spring.datasource.url=jdbc:mysql://{$endpoint}:{$port}/{$schema_name}
spring.datasource.username={$username}
spring.datasource.password={$password}

 

$schema_name에는 해당 전체 DB의 database들 중 사용하고 싶은 database의 이름을 적어주면 된다. 

 

 

참고한 포스트

https://chung-develop.tistory.com/18

https://stackoverflow.com/questions/50837960/spring-boot-connection-to-mysql-remote-database

https://deeplify.dev/database/troubleshoot/unknown-initial-character-set-index

https://stackoverflow.com/questions/50855622/unknown-initial-character-set-index-255-received-from-server

https://zel0rd.tistory.com/141

https://velog.io/@dongzooo/My-sql-workbench-%EC%97%90%EB%9F%AC%ED%95%B4%EA%B2%B0-could-not-acquire-management-access-for-administration

(인프런 Spring Boot JWT Tutorial 강의Amigoscode 유튜브 강의를 참고하여 진행했습니다.)

 

인프런 강의 2강에서 SecurityConfig 클래스를 등록할 때 WebSecurityConfigurerAdapter 클래스를 상속받아 진행하는 부분이 있었는데, 현재는 WebSecurityConfigurerAdapter 클래스는 deprecated 된 상태라 사용할 수 없었다. 따라서 다른 유튜브 강의를 참고해서 진행하였다. 

 

Basic Authentication

프로젝트를 시작할 때 spring security를 dependency로 더하지 않았었다. MavenRepository에서 코드를 찾아 build.gradle 파일의 dependencies 변수에 추가해 준다. 

implementation("org.springframework.boot:spring-boot-starter-security")

 

그리고 기존에 작성한 /ok API를 실행하면 바뀌는 점이 있는데, 기존에는 바로 200 OK와 함께 문자열을 리턴하였던 API가 로그인 폼을 리턴한다. 

 

로그인 폼의 Username은 기본 상태에서는 "user"이고, 비밀번호는 서버를 실행시킬 때 일시적인 비밀번호가 부여된다. 

로그인한 이후에는 기존 /ok API와 같은 Response를 반환한다. 

똑같은 요청을 postman으로 보내보았다. 결과는 401 Unauthorized였다. 즉 /ok API로 보내진 요청이 unauthorized response를 받고 로그인 폼으로 리다이렉트(redirect) 되었던 것이다. 

 

왜 그랬는지는 Spring Security의 구조를 보면 알 수 있다.

Spring Security는 앱이 처음 시작되면 SecurityFilterChain 타입의 Bean을 찾는다고 한다. 이때 SecurityFilterChain은 인터페이스 타입이다. 

@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin(withDefaults());
    http.httpBasic(withDefaults());
    return http.build();
}

SecurityFilterChain은 다음과 같은 구조를 가진다. 

어떤 http 요청이 들어오건 모든 요청에 대해서는 해당 요청이 인가되었는지를 확인한다. 

만약 그렇지 않다면 로그인 폼을 렌더링하고, 여기서도 실패할 경우는 basic 폼을 렌더링한다. 

 

이제 이 코드를 그대로 가져와서 SecurityConfig 클래스를 정의해 보자. 

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.formLogin(withDefaults());
        http.httpBasic(withDefaults());
        return http.build();
    }

}

 

@EnableWebSecurity 어노테이션은 보안 관련 클래스와 Bean들을 포함하는 클래스에 명시한다. 그러면 스프링 컨테이너에서는 해당 클래스를 자동으로 Security Configuration 클래스로 인식한다. 

 

JWT Authentication

방금 전까지는 가장 기본적으로 Spring Security를 사용하는 경우를 살펴보았는데, 이번에 구현할 JWT 인증은 조금 더 복잡하다. 

우선 @Controller 클래스가 호출되기 전까지 요청에 관여하는 클래스들이 많다. 

 

Client -> JWTAuthFilter -> UserDetailsService -> (InMemory)DB -> SecurityContextHolderr -> Controller

 

해당 순서를 거쳐서 controller 클래스가 호출되고, 중간에 인증이 실패하면 아까처럼 401 Unauthorized 응답을 리턴하게 된다. 

 

(1) JWTAuthFilter

가장 먼저 요청을 받는 클래스로, 요청의 헤더에 'JWT'가 포함되어있는지를 확인한다. 만약 JWT가 없다면 JWT 토큰 관련 정보가 없는 것이므로 401 응답을 리턴한다. 

만약 JWT 헤더가 있다면 그 다음 단계인 UserDetailsService에다가 JWT 헤더의 토큰에서 추출한 유저 관련 정보를 가지고 해당 정보를 요청한다. (ex. id=1인 유저의 정보 요청)

UserDetailsService로부터 정보를 받으면 받은 유저 정보와 JWT 토큰을 디코딩해서 얻은 정보를 비교한다. 둘이 다르면 401 에러를 리턴하고, 같으면 SecurityContextHolder를 호출한다. 

 

(2) UserDetailsService

JWTAuthFilter의 요청을 받아서 그 다음 클래스인 DB(InMemoryDB일 수도, 유저 정보가 저장된 일반 DB일 수도 있고 구현은 다양하다)에다가 유저 정보를 요청하고, DB가 유저 정보를 리턴하면 그걸 JWTAuthFilter에게 전달한다. 

 

(3) DB

UserDetailsService의 요청을 받아서 DB에서 유저 정보를 조회한 뒤 UserDetailsService에게 리턴한다. 

 

(4) SecurityContextHolder

이 안에 SecurityFilterChain 인터페이스도 포함되어 있다. JWTAuthFilter에 의해 호출되면 이 클래스에서는 유저의 상태를 authenticated로 바꾸고 요청을 DispatcherServlet에게 넘긴다.

그러면 DispatcherServlet에서 유저가 원래 요청했던 URL에 해당하는 컨트롤러의 메소드를 호출하고 해당 컨트롤러에서 응답을 리턴하게 된다. 

 

JWT 인증 구현 - JwtAuthFilter

본격적인 JWT 인증을 구현하기 전, 하나의 dependency가 더 필요하다.

implementation("io.jsonwebtoken:jjwt-api:0.11.5")

 

이후 config 디렉토리에 JwtAuthFilter 클래스를 추가하고, 해당 클래스에 @Component 어노테이션을 붙인다. 

또한 해당 클래스가 OncePerRequestFilter 클래스를 상속하도록 한다. 

 

OncePerRequestFilter는 Filter의 일종으로, 한 번의 요청당 한 번만 호출되는 filter 클래스라고 보면 된다. 그리고 해당 클래스를 상속하려면 반드시 doFilterInternal() 메소드를 구현해야 한다. 한 번의 요청이 호출될 때 해당 filter 클래스가 어떤 작업을 할지를 이 메소드에서 정의한다고 보면 된다. 

 

구현하면 이런 모양이 되겠다. (메소드의 구현은 유튜브 영상 45:34에 나와 있다.)

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // implementation
    }
}

 

JwtAuthFilter 메소드를 보면 Username을 얻는 부분과 토큰 유효성 검증 부분은 구현되어 있지 않다. 

이 부분은 jwtutils 파일을 검색하면 나오는 여러 깃헙 리포에서 가져올 수 있는데, 이들 중 하나에서 코드를 가져와서 사용했다. 

 

JwtAuthFilter.java에서 아직 구현되지 않은 부분을 메소드를 호출하여 완성하자. 

@Component
@RequiredAllArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // code
        userEmail = jwtUtils.extractUsername(jwtToken);
        final boolean isTokenValid = jwtUtils.validateToken(jwtToken, userDetails);
        // code
    }
}

 

이제 스프링 컨테이너에게 이 필터를 사용하겠다고 선언해야 한다. 

앞서 맨 먼저 만들었던 SpringConfig.java에는 @EnableWebSecurity 어노테이션이 붙어 있어서 스프링 컨테이너는 이 클래스를 security configuration 클래스로 인식한다. 따라서 이 클래스에 선언하면 된다. 

(코드의 구현은 유튜브 영상의 53:42에서 볼 수 있다.)

 

참고로 해당 영상에서는 Lombok를 dependency에 추가해서 모든 생성자 관련 코드를 @RequiredArgsConstructor으로 대체하는데, Lombok을 추가하지 않았다면 다음 작업으로 대체하면 된다. 

 

(예시) Lombok을 dependencies에 추가했을 경우

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

}

 

(예시) Lombok을 dependencies에 추가하지 않았을 경우

@Configuration
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Autowired
    public SecurityConfig(JwtAuthFilter jwtAuthFilter){
        this.jwtAuthFilter = jwtAuthFilter;
    }
}

 

JWT 인증 구현 - UserDetailsService

UserDetailsService를 구현하는 방법은 두 가지가 있다. 

UserDetailsService 인터페이스를 구현한 개별 클래스를 만들 수도 있고, SecurityConfig 클래스에 @Bean을 붙여 메소드로 구현할 수도 있다. 

강의에서는 비교적 간단한 메소드 구현 방식을 사용하였으나, 여기서는 circular import 에러 문제로 UserDetailsService 인터페이스를 클래스로 구현하여 사용하였다.

 

UserDetailsServiceImpl.java

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Autowired
    public UserDetailsServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberRepository
                .findByName(username)
                .stream()
                .findAny()
                .orElseThrow(() -> new UsernameNotFoundException("해당 유저가 없습니다."));
    }

}

 

이후의 과정에 대해서는 다음 포스팅에서 이어가 보겠다. 

 

 

참고한 포스트

https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html

https://www.youtube.com/watch?v=6n4k8Van_HI&list=PL93mKxaRDidERCyMaobSLkvSPzYtIk0Ah&index=3 

https://fusiondeveloper.tistory.com/69

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt#curriculum

https://velog.io/@suhongkim98/Spring-Security-JWT%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EC%9D%B8%EA%B0%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

https://velog.io/@pjh612/Deprecated%EB%90%9C-WebSecurityConfigurerAdapter-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8C%80%EC%B2%98%ED%95%98%EC%A7%80

https://www.youtube.com/watch?v=b9O9NI-RJ3o 

https://github.com/MajidLamghari/Spring-boot-security-jwt/blob/master/src/main/java/io/javabrains/springsecurityjwt/util/jwtUtil.java 

 

controller-service-repository 디자인 패턴

디자인 패턴 중 controller-service-repository 패턴을 사용해서 클래스를 만들고, 저번에 연결했던 mysql에 API를 사용해서 데이터가 저장되도록 해 보았다. 

 

컨트롤러의 경우, @RestController 어노테이션(Annotation)을 사용해서 해당 클래스를 REST 컨트롤러로 등록해 준다. 

@RestController는 REST API 기능을 수행할 때 붙이고, @Controller는 REST API가 아닌, 일반 뷰를 렌더링하는 용도의 클래스에 등록한다. 

 

또한 디자인패턴 클래스들 중 컨트롤러가 가장 먼저 요청(request)을 받게 되는데, 해당 요청을 다음 클래스인 서비스(service)로 넘겨주기 위해서, private final 타입으로 service 멤버 변수를 정의한다. 

 

private final로 선언하는 이유는 한번 서비스 클래스와 컨트롤러 클래스가 맵핑된 뒤 그 맵핑이 바뀌지 않게 하기 위해서이다.

 

또한 @Autowired를 생성자에 붙여서 컨트롤러가 생성될 때 매개변수로 서비스를 받도록 한다. 서비스와 컨트롤러가 같이 생성되도록 하기 위해서이다. 

 

그런데 스프링에서는 객체의 생성을 스프링 컨테이너에서 관리하므로, 서비스와 컨트롤러를 할당할 때 new 생성자를 쓰면 안 된다. 그냥 매개변수로 받은 서비스를 멤버 변수에 연결하면 된다. 

 

컨트롤러 클래스 내부에는 각 메소드를 정의할 수 있는데, 메소드 하나당 하나의 API가 된다. 

각 메소드에는 @GetMapping, @PostMapping 등의 어노테이션을 붙여서 해당 API가 어떤 HTTP 방식으로 작동할지를 명시한다. 또한 @GetMapping의 경우 @RequestParam 어노테이션을 붙여서 쿼리 변수를 받을 수도 있다. 

 

MemberController.java

@RestController
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService){
        this.memberService = memberService;
    }

    @GetMapping("/member")
    public Member member(@RequestParam(value = "name", defaultValue = "user") String name){
		// API logic
    }
}

 

서비스 클래스도 마찬가지이다. 컨트롤러 클래스가 로직 가장 위단에서 요청을 처리하는 반면(통신 관련 로직 등), 서비스 클래스에는 소위 말하는 비즈니스 로직이 포함된다. 

 

@Service 어노테이션으로 해당 클래스를 서비스 클래스로 등록한다. 마찬가지로 private final 멤버 변수로 리포지토리(repository)를 등록한 뒤, 생성자에 @Autowired를 붙여서 서비스 클래스가 생성될 때 리포지토리 클래스가 같이 생성 및 매칭되도록 한다. 

 

MemberService.java

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    public Member join(Member member){
        // business logic
    }
}

 

리포지토리 클래스의 경우 로직이 상대적으로 간단하다. 정확하게는 리포지토리 클래스가 아니라 리포지토리 인터페이스가 된다. 

 

리포지토리 인터페이스를 만들고, 해당 인터페이스가 <도메인 클래스 타입, 도메인 클래스 id 타입>JpaRepository를 상속받도록 한다. 

 

해당 JpaRepository 안에는 DB에 접근할 때 사람들이 많이 사용하는 메소드들(findById 등)이 들어 있기 때문에, 이 인터페이스를 상속하면 해당 메소드를 별도로 정의하지 않고 사용할 수 있다. 

 

다만 JpaRepository에서는 ID같은 PK 필드에 대해서만 findById 메소드를 제공하므로, 다른 컬럼을 기준으로 찾고 싶다면 해당 인터페이스에 추가로 메소드를 정의해주면 된다. 

 

MemberRepository.java

@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {
    Optional<Member> findByName(String name);

}

 

마지막으로 데이터베이스에 저장할 객체가 될 도메인(domain) 클래스를 정의한다. 

 

PK인 필드에는 @Id를 붙이고, DB의 AUTO_INCREMENT 옵션을 적용하고 싶다면 @GeneratedValue(strategy=GenerationType.AUTO)를 적용한다. 

 

Member.java

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;

}

 

이후 실행하면 다음과 같이 생성된 객체에 대한 json이 잘 나오는 것을 볼 수 있다. 

 

또한 mysql에도 데이터가 잘 저장된다. 

 

참고한 포스트

https://spring.io/guides/gs/rest-service/

https://velog.io/@leesomyoung/SpringBoot-SQLException-No-database-selected

 

이전에 들었던 스프링 강의에서는 Spring Data JPA를 사용하기 위해서 H2 데이터베이스를 사용했는데, 이게 용량이 작을뿐더러 실제 생활에서 많이 사용되는 데이터베이스는 아니라고 하셨다. 

그래서 Spring Data JPA를 Mysql과 연동하여 사용해 보려고 한다. 

 

우선 스프링에서 제공하는 '스프링과 mysql 연동하기' 가이드 코드를 참고했다. 

mysql 프로그램은 이전에 xampp를 통해 apache 서버와 함께 다운받아 뒀었어서 추가로 설치할 것은 없었다. 

 

Driver 클래스와 Driver 연동 에러

그런데 application.properties 파일 코드에서 에러가 났다.

 

해당 코드에서 에러가 발생한 것이었다. (지금은 에러를 해결한 상태라 파란색이지만, 원래는 붉은 글씨로 ClassException 에러가 났었다.) 찾아보니 application.properties에 명시한 driver 클래스가 실제 driver랑 연동이 되지 않아 class를 불러올 수 없던 것이었다.

 

그런데 나는 이미 mysql과 java의 driver인 connector 파일을 maven에서 import해서 gradle build 파일에 명시해 둔 상태였는데도 에러가 났다. 

 

select version(); 이라는 SQL문으로 mysql 버전을 확인해 보니, 내가 쓰는 mysql의 버전은 10.0 이상이었는데 maven에서 import한 코드는 mysql 8.0 이상의 버전에 대해서는 적용할 수 없었다. (java:5.1.6 버전의 connector는 mysql 8.0 이상을 지원하지 않는 것으로 보이는데, mysql 버전이 10.4.28이었다.)

 

그러므로 따로 mysql 홈페이지에 가서 mysql connector 8.0을 새로 다운로드 받았다. (connector 버전은 8.0이지만 mysql의 가장 최신 버전까지 java와 연동할 수 있다.)

 

다운받을 connector를 선택할 때는 드롭다운 메뉴에서 platform independent를 선택해 주고, tar과 zip 중에서는 아무거나 선택하면 된다. 우리가 필요한 것은 해당 파일에 있는 .jar 파일이다. 다운받은 파일에서 jar 파일만 따로 뺀 뒤, jar 파일의 디렉토리를 복사해 두자. 

 

Intellij에서 jar 파일 새로 적용하기

그리고 Intellij를 열어서 File -> Project Structure로 들어간 뒤 왼쪽 탭에서 Module을 클릭하고, 가운데 탭 중에서 Dependencies를 클릭한다. 그리고 작은 + 버튼을 누르고 '1 JARS or directories'를 누른 뒤 jar 파일을 찾아서 등록하면 된다. 

 

그리고 다시 application.properties 파일을 확인하면 클래스를 찾지 못해서 붉게 나타나던 코드가 파란색으로 정상적으로 보이게 된다. 

 

Mysql Dialect 설정 에러

 

그런데 여기서 또 다른 에러가 발생했다. 

JDBC 메타데이터 없이는 Dialect을 결정할 수 없다-는 에러였고, 해결하려면 javax.persistence.jdbc.url / hibernate.connection.url / hibernate.dialect 값들 중 하나를 설정해야 하는 것으로 보였다. 

 

위 세 값 중 javax.persistence.jdbc.url과 hibernate.dialect 값을 설정해서 오류를 해결할 수 있었다. 

javax.persistence.jdbc.url 값은 jdbc:mysql://localhost:포트번호를 적어주면 된다. 

hibernate.dialect 값은 org.hibernate.dialect.MySQLDialect로 입력하면 된다. 다른 DB를 사용한다면 다른 DB 버전으로 입력해주면 되겠다. 

 

위와 같이 두 에러를 해결했더니 잘 실행되는 모습을 볼 수 있다. 

 

Dialect란?

SQL 언어를 사용하는 많은 데이터베이스가 있다. 각 DB가 언어를 사용할 때는 SQL 언어를 그대로 사용하는 게 아니라 SQL에는 없는 부분을 따로 정의하기도 하고, SQL의 특정 부분은 구현하지 않거나, 조금씩 다른 형식의 문법을 사용한다. 이처럼 각 DB마다 사용하는 문법을 SQL Dialect라고 한다. SQL을 표준어, 각 DB에서 사용하는 방식을 방언에 비유한 것 같다. 

 

 

참고한 포스트

https://www.youtube.com/watch?v=_7R46uVZTyc 

https://velog.io/@k_ms1998/JPA-MySQL-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0

https://dev-coco.tistory.com/85 

https://spring.io/guides/gs/accessing-data-mysql/#initial

https://learnsql.com/blog/what-sql-dialect-to-learn/#:~:text=SQL%20Is%20the%20Language%20for%20Talking%20to%20Databases&text=PostgreSQL%2C%20MySQL%2C%20Oracle%2C%20and,call%20these%20variants%20SQL%20dialects.

 

이 포스트는 인프런 김영한 님의 '스프링 핵심 원리 - 기본편' 강의를 들으면서 내용을 정리한 글입니다. 

 

[메인 컨텐츠]

스프링 핵심 원리 - 기본편 대시보드 - 인프런 | 강의 (inflearn.com)

 

📅 2022-05-23 

ℹ️ 목차

1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

2. 탐색 위치와 기본 스캔 대상

3. 필터

4. 중복 등록과 충돌

 


1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

컴포넌트 스캔의 필요성

지금까지는 스프링 빈을 수동으로 하나씩 등록하는 방법을 알아보았다. 
그러나 등록할 빈의 개수가 많아질수록 단순 반복 작업이 될 수 있고, 누락할 수 있다는 문제점도 생긴다. 

 

스프링에서는 자동으로 빈을 등록해주는 컴포넌트 스캔이라는 기능이 있다. 

 

컴포넌트 스캔(Component Scan)스프링 빈을 자동으로 끌어오는 기술.

 

코드로 보자.

AppConfig.java : 수동으로 스프링 빈을 등록한 클래스 파일

AutoAppConfig.java : 자동으로 빈을 등록할 클래스 파일

 

컴포넌트 스캔의 원리

@ComponentScan을 사용해서 컴포넌트 스캔을 할 수 있다. 

@ComponentScan은 @Component이 붙은 클래스를 찾아서 전부 스프링 빈으로 등록한다. 

@Configuration // 싱글톤 패턴으로 객체 생성
@ComponentScan	// 컴포넌트 스캔 역할
public class AutoAppConfig {
	// 별도의 코드 필요 X
}

 

@ComponentScan@Component 어노테이션만 스캔할까?

@Component 어노테이션은 다른 어노테이션 안에 붙어있기도 한다. 

ex) 

@Configuration 어노테이션을 타고 들어가면(원본 코드를 보면), @Component 어노테이션이 포함되어 있다. 이 경우 @Configuration이 붙은 클래스도 스프링 빈으로 등록된다. 
물론 @Configuration 말고도 @Component을 포함하는 어노테이션들은 많고, 스프링 컨테이너는 이 어노테이션들이 붙은 클래스들도 모두 스프링 빈으로 등록한다. 

 

@Component로 원하는 클래스를 스프링 빈으로 등록시키는 방법

등록하고 싶은 스프링 클래스 선언부 위에 @Component만 붙이면 된다. 

@Component
public class SpringServiceImpl {
	// code
}

⚠️인터페이스 말고, 구현 클래스의 선언부에다가 붙여야 한다.

 

지금까지 자동으로 스프링 빈을 등록해보았다. 

이제는 의존관계를 자동으로 주입해 보자. 

 

자동 의존관계 주입

@Autowired 라는 자동 의존관계 주입 어노테이션을 사용하면 된다. 

@Autowired를 @Component이 붙은 클래스의 생성자 위에다가 붙인다. 

(지금은 생성자 주입 방식을 사용해서 생성자 위에다가 붙일 건데, 다른 방법도 있다. )

@Component
public class SpringServiceImpl implements SpringService {
	
    private final SpringRepository springRepository;	// 해당 클래스가 의존하는 다른 클래스 객체
    
    @Autowired
    public SpringServiceImpl(SpringRepository springRepository) {
    	this.springRepository = springRepository;
    }
    
}

그러면 스프링 컨테이너에서, @Component 가 붙은 스프링 빈들 중 SpringRepository 타입의 빈을 찾는다.

('타입'으로 조회한다.) 

그리고 SpringServiceImpl이 호출될 때 그 찾은 스프링 빈을 매개변수로 넣어준다. 

 

⚠️

@Autowired를 쓸 때에는 반드시 @Autowired를 사용한 생성자를 가진 클래스(SpringServiceImpl)와 해당 생성자가 필요로 하는 매개변수 클래스(여기서는 SpringRepository의 구현 클래스인 SpringRepositoryImpl)에 모두 @Component 어노테이션이 붙어 있어야 한다. 

@Component
public class SpringRepositoryImpl implements SpringRepository {
	// code
}

 

 

이제 테스트를 만들어 보자.

: AutoAppConfigTest.java

 

1) 스프링 빈을 불러오기 위해서 AnnotationConfigApplicationContext 객체를 생성하자. 

2) 해당 객체 안에는 AppConfig 클래스 대신, 컴포넌트 스캔을 사용한 AutoAppConfig 클래스를 넣는다. 

3) AutoAppConfig 클래스로 SpringService 타입의 스프링 빈을 조회할 수 있는지 확인하자. 

public class AutoAppConfigTest {
	
    @Test
    void test() {
    	ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        SpringService springService = ac.getBean(SpringService.class);
        assertThat(springService).isInstanceOf(SpringService.class);
    }
    
}

 

일부 클래스를 제외하고 스프링 빈을 등록할 수도 있다. 

excludeFilters 옵션을 지정하면 된다. 

세부 옵션으로는 type, classes 등 여러 가지가 있다. 

@ComponentScan(
	excludeFilters = @ComponentScan.Filter( 
		type = FilterType.ANNOTATION, 
		classes = Service.class, 
	)
)
public class AutoAppConfig {
	// no code
}

: @ComponentScan의 대상 중, Configuration 어노테이션은 스프링 빈으로 포함시키지 않겠다는 의미. 

 

자동 주입 방식에서 빈 이름을 정하는 방법

빈의 기본 이름은 맨 앞 글자만 소문자로 바뀐 클래스 이름 이다. 

@Component
public class SpringRepositoryImpl implements SpringRepository {
	// code
}

: 빈 이름은 springRepositoryImpl 으로 등록된다. 

 

빈 이름을 지정할 수도 있다. 

@Component("지정하고_싶은_이름")

@Component("spring_repo")
public class SpringRepositoryImpl implements SpringRepository {
	// code
}

 

2. 탐색 위치와 기본 스캔 대상

컴포넌트 스캔을 할 때, 어느 위치부터 @Component 어노테이션을 탐색할지를 지정할 수 있다. 
basePackages 옵션으로 지정할 수 있다. 

@ComponentScan(basePackages = 'hello.core.member')
public class AutoAppConfig {
	// code
}

 

: hello.core.member 디렉토리 하위에 있는 파일들에서만 @Component를 탐색한다. 

 

기본값

아무런 값도 없다면, basePackage의 기본값은 @ComponentScan이 붙은 클래스의 바로 위 패키지가 된다. 
ex)

AutoAppConfig.java 클래스의 바로 위 폴더가 hello.core이라면,
: hello.core 아래의 파일들에 한해서 컴포넌트 스캔을 한다. 

 

AutoAppConfig.class 는 hello.core 디렉토리 바로 아래에 위치한다. 
-> hello.core 패키지(디렉토리) 아래에 있는 파일들만 탐색한다. 

@SpringBootApplication의 비밀

스프링 웹 어플리케이션을 실행할 때는 일반적으로 (패키지_이름)Application 클래스의 메인 메소드를 실행한다.

(여기서는 CoreApplication.java)
이 클래스는 기본적으로 최상단 패키지(여기서는 hello.core)의 바로 아래에 위치하는데, 그 이유가 있다. 

@SpringBootApplication 안에는 @ComponentScan이 포함되어 있다. 
: 실제 웹앱을 만들 때는 스프링에서 @ComponentScan을 명시적으로 사용할 일이 거의 없다. 어차피 메인 메소드에 붙어있는 @SpringBootApplication 어노테이션 안에 포함되어 있기 때문이다!


그래서 웹앱을 실행하면, 프로젝트 폴더에 있는 모든 스프링 빈들을 자동으로 등록할 수 있었던 것이다. 
<-> 만약 @SpringBootApplication이 붙은 메인 클래스가 특정 하위 디렉토리에만 속해 있었다면, 모든 빈을 불러올 수 없었을 것이다. 

 

컴포넌트 스캔의 대상

컴포넌트 스캔에서는 @Component만 스캔하지 않는다. 다른 어노테이션도 내부에 @Component를 포함하고 있기 때문이다. 
: @Controller, @Service, @Repository, @Configuration, ...

 

@Component을 상속하는 다른 어노테이션들

 

⚠️ 사실 어노테이션은 자바에서 지원하는 기능이 아니다.
⚠️ 한 어노테이션이 다른 어노테이션을 포함하는 상속 관계도 마찬가지다. 자바 언어는 어노테이션의 상속 관계 인식을 지원하지 않는다. 

 

어노테이션의 상속관계

보통 한 클래스가 다른 클래스를 상속하면, 상속받은 클래스에서 지원하는 기능 + a 를 수행한다. 

어노테이션도 마찬가지이다. 

 

@Component는 @ComponentScan이 어떤 클래스를 스프링 빈으로 등록할지를 알려주는 역할을 한다. 

그러나 @Component와 다른 어노테이션까지 같이 상속한 다른 어노테이션은 그 외의 부가적인 기능도 함께 제공한다. 

 

1) @Controller

스프링 MVC 컨트롤러로 인식한다. 


2) @Repository

스프링에서 데이터로 접근하는 클래스로 인식한다. 
또한 데이터 계층에서 예외가 발생했을 때, 이를 스프링 예외로 변환시킨다. 
✖️ 스프링 예외로 변환시킨다 = 예외를 추상화시킨다

 

3) @Service

특별한 처리를 하지 않는다. 개발자들이 비즈니스 계층을 인식하는 데 도움이 된다. 

 

3. 필터

컴포넌트 스캔에서 추가할 대상과 제외할 대상을 설정할 수 있다. 
: includeFilters, excludeFilters 옵션 사용

 

추가할 어노테이션, 제외할 어노테이션을 만들고 필터가 작용하는지를 직접 확인하기 위해 어노테이션을 만들어 보자. 


어노테이션을 만드는 방법

1) 인터페이스를 선언하고, 인터페이스 키워드 앞에 @을 붙인다.

public @interface IncludeAnnotation {

}

 

2) 해당 인터페이스 위에다 추가로 어노테이션을 더 설정할 수 있다. 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IncludeAnnotation {

}

 

@Target : 해당 어노테이션이 어느 타입에 붙을지를 지정. 

ElementType.TYPE : 클래스 단위에 지정하는 어노테이션. 
TYPE이 아닌 다른 값으로 지정하면 어노테이션이 필드나 생성자에 붙게 할 수도 있다.

(ex. @Autowired)

방금은 컴포넌트 스캔에 추가할 어노테이션을 만들었다. 이제 제외할 어노테이션을 만들어 보자. 

: 같은 방법으로 ExcludeAnnotation을 만들자. 

 

테스트하기

위에서 만든 @IncludeAnnotation, @ExcludeAnnotation을 사용해서 테스트를 해 보자. 


1. BeanInclude와 BeanExclude 클래스를 만들고, 각각 @IncludeAnnotation, @ExcludeAnnotation을 붙이자. 
2. 테스트를 만들어 보자: ComponentFilterAppConfigTest.java
목적: includeFilter, excludeFilter 옵션이 잘 작동하는지 확인.

1) 테스트 클래스 안에 static class를 만들고, 해당 클래스를 @Configuration을 붙여서 스프링 빈 정보를 담는 클래스로 선언한다. 
2) 해당 클래스를 참고한 AnnotationContextApplicationConfig 객체를 만들고, 빈을 조회한다. 
3) 이때 BeanInclude(includeFilter)를 조회하면 조회가 되어야 한다. 
4) 반면 BeanExclude(excludeFilter)를 조회하면 에러(NoSuchBeanDefinitionException)가 발생해야 한다. 

 

에러는 최대한 구체적으로 명시하는 것이 좋은 테스트이다. 
<-> Exception.class -> 별로 도움 안 됨!

4. 중복 등록과 충돌

스프링 빈을 등록하는 방법으로 수동(AppConfig 클래스에 직접 등록)과 자동 등록(@ComponentScan 이용)이 있었다. 

수동 등록에서는 당연히 같은 이름의 빈이 2개 이상 있으면 충돌이 발생했다. 
그렇다면 자동 등록에서는 어떨까?


1) 자동 vs 자동

: ConflictingBeanDefinitionException이 발생한다. 


2) 자동 vs 수동

: 오류가 나지 않는다. 
수동 등록 빈이 자동 등록 빈을 오버라이딩(덮어 씌우기) 했기 때문이다. 
⚠️ 이름이 같을 경우, 자동 등록 빈보다 수동 등록 빈이 우선권을 가진다. 

그러나 이런 설정은 대부분 개발자의 의도적인 테스트에 의해 발생하기보다는, 수많은 설정이 꼬여서 발생하는 것이 대부분이다. 

따라서 스프링은 이후 자동 등록된 빈과 수동 등록한 빈의 이름이 같을 때, 어플리케이션을 종료(disable)시키는 방향으로 업데이트 되었다. 

 

disable 말고 overriding 시키는 방법

main>java>resources>application.properties

spring.main.allow-bean-definition-overriding=true

: 수동과 자동 등록된 빈 2개가 이름이 같을 때 overriding이 발생한다. 

(default : False)

 

 

+ Recent posts