스프링을 사용하다 보면 @Value를 통해. yml 또는. properties에 정의된 속성을 가져오곤 합니다.
현재 진행중인 프로젝트에서도 @Value를 통해 jwt 설정을 가져와 사용 중입니다.
public class JwtService {
private static final String USER_UNIQUE_CLAIM = "user_unique_id";
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String TOKEN_TYPE = "Bearer ";
@Value("${jwt.secretKey}")
private String jwtSecret;
@Value("${jwt.access.expiration}")
private Long accessTokenExpirationPeriod;
@Value("${jwt.access.header}")
private String accessTokenHeader;
@Value("${jwt.refresh.expiration}")
private Long refreshTokenExpirationPeriod;
@Value("${jwt.refresh.header}")
private String refreshTokenHeader;
// 생략
}
위 코드가 가지는 문제점은 속성이 많아지면 많아질수록 필드는 그만큼 늘어나게 됩니다.
필드가 늘어날 수록 한 곳에서 관리하고 싶어지기 마련입니다.
그보다 더 큰 문제는 단위 테스트하기가 매우 어려워진다는 것입니다.
스프링은 @ConfigurationProperties 를 통해 스프링 빈으로 객체를 등록하고 해당 객체에 설정 변수를 한 곳에서 관리할 수 있게 해줍니다.
Setter 바인딩
@ConfigurationProperties은 값을 바인딩하기 위해서는 setter를 열어두어야 합니다.
@ConfigurationProperties("jwt")
@Getter
@Setter
public class JwtConfigProperties {
private String jwtSecret;
private Long accessTokenExpirationPeriod;
private String accessTokenHeader;
private Long refreshTokenExpirationPeriod;
private String refreshTokenHeader;
}
또한 컨테이너가 시작될 때 위 클래스에 적절히 바인딩 되도록 아래와 같이 설정이 필요합니다.
@SpringBootApplication
@EnableConfigurationProperties({JwtConfigProperties.class})
public class ProjectlinkApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectlinkApplication.class, args);
}
}
하지만 setter를 열어두는 것은 변경가능성을 의미하기 때문에 되도록 열지 않는 것이 좋습니다.
또한 관리해야할 설정클래스가 많아지면 @EnableConfigurationProperties가 그만큼 무거워지고 관리가 어려워집니다.
Constructor 바인딩
setter 대신 생성자를 통해서 값을 주입받을 수 있습니다.
@ConfigurationProperties(prefix = "jwt")
@RequiredArgsConstructor
@Getter
public class JwtConfigProperties {
private final String secretKey;
private final Long accessExpiration;
private final String accessHeader;
private final Long refreshExpiration;
private final String refreshHeader;
}
기존에는 @ConstructorBinding 을 통해 생성자 주입임을 명시해야 했지만, 스프링 3.x 이후 단일 생성자라면 생략할 수 있게 변경됐습니다.
또한 @ConfigurationPropertiesScan을 통해 @ConfigurationProperties 이 붙은 어노테이션을 찾아 빈으로 등록할 수 있습니다.
@SpringBootApplication
@ConfigurationPropertiesScan
public class ProjectlinkApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectlinkApplication.class, args);
}
}
테스트를 더 쉽게
아래는 jwt토큰의 유효성을 검사하는 간단한 단위 테스트 코드입니다.
@Test
@DisplayName("생성한 액세스 토큰은 유효한 토큰이여야한다.")
void createAccessToken() {
// given
String userId = "1";
String accessToken = jwtService.createAccessToken(userId);
// when
boolean isValidToken = jwtService.isValidToken(accessToken);
// then
Assertions.assertThat(isValidToken).isTrue();
}
위 테스트는 단위 테스트이기 때문에 스프링 컨테이너를 이용하지 않습니다.
또한 @Value는 스프링이 의존관계 주입 시점에 필드에 값을 주입하기 때문에 NPE가 터지게 됩니다.
테스트가 아주 보기 좋게 실패합니다. (당연한 결과입니다.)
기존에는 이 문제를 아래와 같이 리플렉션을 이용해 값을 강제로 주입해 주었습니다.
@BeforeEach
void setUp() throws IllegalAccessException {
ReflectionTestUtils.setField(jwtService, "jwtSecret", "44WB44WI44S344S566i47J6z65+s66iA44WQ44WI44S365+s44WQ44WR44WB44WIO+OFk+OEtzvrnpjjhZHjhZPjhYHsnqzjhZHrn6zrjJzjhZDjhYHsoIDrnrTjhZDjhYjrqLjrjIDjhZHroILrp6TjhZHjhZPrnpjjhZHjhLfjhYjrqLjjhZDjhZHrn7zjhYjrjIg=");
ReflectionTestUtils.setField(jwtService, "accessTokenExpirationPeriod", 10000L);
ReflectionTestUtils.setField(jwtService, "refreshTokenExpirationPeriod", 10000L);
ReflectionTestUtils.setField(jwtService, "accessTokenHeader", "Authorization");
ReflectionTestUtils.setField(jwtService, "refreshTokenHeader", "Authorization-refresh");
}
테스트에 리플렉션을 사용한다해서 큰 문제는 없다고 생각했지만, 보다 객체지향적인 방법을 찾고 싶었습니다.
위에서 .yml파일을 자바 객체로 바인딩해 사용할 수 있었기 때문에 이제 테스트는 쉽게 작성할 수 있습니다.
JwtConfigProperties를 상속받는 Test용 객체를 하나 생성하고 내부에 필드를 설정합니다.
public class JwtConfigPropertiesTest extends JwtConfigProperties {
public JwtConfigPropertiesTest() {
super(
"YXNlZmphb2lzZWo7Zm9haXdqZjI5b29pYXc7amVpZjtqYXdvZTtmamF3ZWZhd2VmamF3b2llO2ZqYW93ZWZqYWt3ajtlb2ZqYXdvaWVm",
10000L,
"Authorization",
10000L,
"Authorization-refresh"
);
}
}
테스트 코드를 조금 수정해줍니다.
class JwtServiceTest {
private final JwtConfigPropertiesTest jwtConfigPropertiesTest = new JwtConfigPropertiesTest();
private final JwtService jwtService = new JwtService(jwtConfigPropertiesTest);
@Test
@DisplayName("생성한 액세스 토큰은 유효한 토큰이여야한다.")
void createAccessToken() {
// given
String userId = "1";
String accessToken = jwtService.createAccessToken(userId);
// when
boolean isValidToken = jwtService.isValidToken(accessToken);
// then
Assertions.assertThat(isValidToken).isTrue();
}
}
'Spring' 카테고리의 다른 글
게시글과 이미지 등록 API 분리로 API Latency 개선기 (1) (0) | 2024.06.19 |
---|---|
회원 도메인 객체 코드 개선하기 (0) | 2024.04.12 |
[Spring Security JWT 로그인 (3)] DelegatingFilterProxy Deep Dive (0) | 2024.03.15 |
[Spring Security JWT 로그인 (1)] 어떤 로그인 방식이 좋을까 (1) (0) | 2024.03.11 |
[Spring Security JWT 로그인 (2)] JWT가 탈취된다면 어떻게 대응해야 할까 (0) | 2024.03.10 |