개인 공부 (23.07~

[프로젝트 문제 해결] 생성자 주입. 기본 생성자가 왜 안되지? 순환참조 발생

Song쏭 2023. 10. 24. 14:29

JwtTokenProvider 클래스와 WebSecurityConfig 클래스 두개를 가지고 생각해본다.

결론은 아래와 같은 코드로 마무리를 했고,

 

내가 궁금했던 것과 

결국 어떻게 해결이 되었는지를 작성하려한다.

package com.room.yeahnolja.security;

import com.room.yeahnolja.domain.member.service.MemberService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;

@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
    private final MemberService memberService;
    private String secretKey = "ReservationApp";
    private long tokenValidTime = 30 * 60 * 1000L;

    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public String createToken(String email, Collection<? extends GrantedAuthority> roles) {
        Claims claims = Jwts.claims().setSubject(email);
        claims.put("roles", roles);

        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenValidTime))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("Authorization");
    }

    public Authentication getAuthentication(String token) {
        UserDetails userDetails = memberService.loadUserByUsername(getEmail(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    public String getEmail(String token) {
        return Jwts.parser().setSigningKey(secretKey)
                .parseClaimsJws(token).getBody().getSubject();
    }
}
package com.room.yeahnolja.config;

import com.room.yeahnolja.security.JwtAuthenticationFilter;
import com.room.yeahnolja.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .antMatchers("/", "/join", "/login").permitAll()
                .antMatchers("/hotels/**").hasAuthority("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin().disable()
                .csrf().disable()
                .logout()
                .permitAll()
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

JwtTokenProvider 자체를 스프링 빈으로 등록하기 위해(다른 클래스에서도 jwtTokenProvider 필드로만 설정해서 자유롭게 사용할 수 있도록 하려고)
WebSecurityConfig 클래스에서 @Configuration/@Bean을 사용했다.


그런데 기본 생성자를 만들어서 쓰려니
    @Bean
    public JwtTokenProvider jwtTokenProvider() {
        return new JwtTokenProvider();
    }
기본 생성자가 없다며 메서드에 빨간줄로 컴파일 오류가 발생했다. [문제 발생1]


그래서 JwtTokenProvider클래스를 살펴보았다.

 

JwtTokenProvider 클래스에서는 
private final MemberService memberService;로 MemberService를 의존성 주입을 받고 있었고, 의존성 주입방식을 생성자 주입 방식을 쓰고 있었다. 
그래서 final로 설정한 후 final 필드에 대해서 자동으로 생성자를 생성해주는 @RequiredArgsConstructor 어노테이션을 쓴 것이다.

 

이미 기본 생성자 없이 MemberService를 매개변수로 갖는 생성자만을 만들어 놓았으니
WebSecurityConfig에서는 new JwtTokenProvider(); 이렇게 기본 생성자로는 기본 생성자 자체가 없어서 객체를 생성할 수는 없다고 알려준 것이다.

정리하자면 MemberService가 JwtTokenProvider 클래스에서 final로 되어있어서

객체 생성 시, 생성자에서 반드시 의존성 주입을 받아야하기 때문에 기본 생성자로는 안된 것이다.

 

그래서 해결방안으로는 
먼저, MemberService를 받는 생성자로 그럼 스프링 빈도 생성이되도록했다.

이렇게 받으니 실제로 실행은 되긴한다.
근데 찝찝하다.
기본 생성자는 영영 만들 수 없는 것인가?싶다.

방법이 있다.
JwtTokenProvider클래스를 빈 생성할 때, @Configuration/@Bean 방법을 사용하지 말고
해당 클래스 자체에 @Component를 사용하는 방법이다.

그럼 WebSecurityConfig에서는 @Bean으로 설정하는 JwtTokenProvider 생성자는 아예 지운다. 

근데 여기서 WebSecurityConfig에서 JwtTokenPriovider 생성자를 다른 메서드에서도 사용하고 있었다.

따라서, JwtTokenProvider를 의존성 주입을 받았다.
이게 가능한 것은 @Component로 이미 빈 등록을 했기 때문이다.

따라서, WebSecurityConfig에서 
private final JwtTokenProvider jwtTokenProvider로 
필드로 의존성 주입을 받고 
이 필드를 매개변수로하는 WebSecurityConfig클래스의 생성자가 만들어지도록 @RequiredArgsConstructor를 넣는다. [기본 생성자를 사용할 수 있도록 해결1]

 

나름 해결방안이라고 생각하고 했는데

순환참조라며 에러를 내뱉어주었다.

말로만 듣던 순환참조가 이렇게 생겼구나! [문제 발생2]

 

순환참조 왜 생긴거지? 다 잘 설정해줬는데?

WebSecurityConfig 하단에 

아래와 같은 코드가 있다. PasswordEncoder를 프로젝트가 실행될 때 빈 생성이 되도록 설정해둔 것이다. 이로 다른 클래스에서도 PasswordEncoder를 의존성 주입을 받아 사용할 수 있게 되는 것이다.

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

 

그런데 MemberSerivce클래스에서 이 PasswordEncoder 빈을 주입받아서 사용한다.

 

그럼.. 정리를 해보자면

WebSecurityConfig클래스에서는 JwtTokenProvider 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이고,

JwtTokenProvider클래스에서는 MemberService 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이고,

MemberService클래스에서는 MemberRepository와 PasswordEncoder 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이다.

 

각자 의존성 주입을 받아서 사용하고 있는데 왜 순환참조라는 것이지?

일단 MemberService가 MemberRepository를 의존성 주입받아 사용하고 있는 건 지금의 순환참조 문제하고는 무관하다.왜냐하면 MemberRepository는 해당 인터페이스를 @Repository를 사용해 스프링 빈으로 등록이 될 수 있었던 것이다.

 

그럼 PasswordEncoder는?

PasswordEncoder 인터페이스는 일단 내가 만든 클래스가 아니라 스프링프레임워크에서 제공하는 인터페이스여서

따로 빈 생성을 위한 @Component와 같은 어노테이션을 설정할 수가 없다.

따라서 WebSecurityConfig 클래스에서 PasswordEncoder를 @Configuration/@Bean방법으로 스프링 빈 생성하고 있었던 것이다.

 

다시 보자면,

WebSecurityConfig클래스에서는 JwtTokenProvider 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이고,

JwtTokenProvider클래스에서는 MemberService 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이고,

MemberService클래스에서는 MemberRepository와 PasswordEncoder 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이다.

 

MemberService클래스에서는 MemberRepository와 PasswordEncoder 스프링 빈(객체)을 의존성 주입을 받아서 사용하고 있는 상태이다.

이 부분에서 MemberService클래스는 PasswordEncoder를 의존성 주입 받아서 사용하는데 

이 PasswordEncoder는 WebSecurityConfig에서 빈 생성이 되고 있다.

 

최종적으로

WebSecurityConfig는 -> JwtTokenProvider에게 의존. JwtTokenProvider는 -> MemberService에게 의존. MemberService는 -> PasswordEncoder에게 의존.(여기서 MemberService가 의존하는 PasswordEncoder는 WebSecurityConfig에서 빈을 생성해준다!! 그래서 순환이 된다고 하는 것이다.)

 

자!

근데 단지 의존성 주입받은 것이 아닌데 순환이 된다고 보는 것인가???

그러니까 PasswordEncoder 자체 인터페이스에서

private final WebSecurityConfig webSecurityConfig; 이런 식으로 의존성을 주입받아서 쓰는 것이라면 진짜로
의존성 주입에 대해 계속 순환하는 것이라고 생각하는데

여기서는 단지 WebSecurityConfig클래스에서 PasswordEncoder를 빈으로 생성하는 메서드가 있기 때문에 순환이라고 하는 것이 
약간의 의문이 든다..

 

뭐 프레임워크에서 순환참조라고 알려주는 거니까 고맙다고 생각하고 이걸 해결하는게 맞는 거겠지만.. 의문은 곧 해결해야겠다. 

 

순환참조의 해결방안으로

PasswordEncoderConfig 클래스를 아래와 같이 만들어주었다.

[순환참조가 되지 않도록 새로운 클래스를 만들어서 PasswordEncoder 빈을 생성하는 코드를 작성했다. 해결2]

 

package com.room.yeahnolja.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

이런 문제를 해결하고 이해하는데 오전 오후를 다 썼다.

그래도 알아가서 뿌듯하다.

 

인간이라면 뇌를 사용해서 생각을 하고 정리를 하고 이해를 하자!!!!!!!!!