Programming/Spring framework

@Value 애너테이션 static 필드에 적용하기.

최동훈1 2024. 1. 25. 03:26

application.yml에 적어놓았던, 클라이언트 키를 이용해서 접근할려고 하니, 계속 유효하지 않다는 애러가 반복되었다.

 

spring:
  datasource:
    url: ${DB_URL}
    driver-class-name: ${DB_DRIVER}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        show_sql: true
        format_sql: true
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
    defer-datasource-initialization: true
  cache:
    jcache:
      config: classpath:ehcache.xml
  sql:
    init:
      mode: always
  # error 404
  mvc:
    throw-exception-if-no-handler-found: true
    dispatch-options-request: false
logging:
  level:
    org.hibernate.SQL: debug

management:
  endpoints:
    web:
      exposure:
        include: '*'


spotify:
  registration:
    client-id: 9aa067b28e444056ab123c76058ca7ab
    client-secret: 28da9a874ab248ecae461398a4a20d53
    redirect-uri: ${SPOTIFY_REDIRECT_URI}
    scope: user-read-private,user-read-email
  provider:
    authorization-uri: https://accounts.spotify.com/authorize
    token-uri: https://accounts.spotify.com/api/token
    user-info-uri: https://api.spotify.com/v1/me

custom:
  s3url : ${s3url}

 

package com.hositamtam.plypockets.config;

import java.io.IOException;
import java.time.Instant;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials;
import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest;

@Slf4j
@Component
public class SpotifyConfig {

    @Value("${spotify.registration.client-id}")
    private static String CLIENT_ID ;
    
    @Value("${spotify.registration.client-secret}")
    private static String CLIENT_SECRET ;
    
    private static final SpotifyApi spotifyApi = new SpotifyApi.Builder().setClientId(CLIENT_ID).setClientSecret(CLIENT_SECRET).build();

    private static String accessToken;
    private static Instant accessTokenExpiration;

    public static String getAccessToken(){
        if (accessToken == null || Instant.now().isAfter(accessTokenExpiration)) {
            refreshAccessToken();
        }
        return accessToken;
    }

    private static void refreshAccessToken() {
        ClientCredentialsRequest clientCredentialsRequest = spotifyApi.clientCredentials().build();
        log.info("새로운 리프레쉬 토큰 발급을 위한 과정 시작");

        try {

            final ClientCredentials clientCredentials = clientCredentialsRequest.execute();

            log.info("0");
            accessToken = clientCredentials.getAccessToken();
            log.info("현재 발급된 토큰 : "+accessToken);
            // 토큰의 유효 시간을 계산하여 저장합니다.
            accessTokenExpiration = Instant.now().plusSeconds(clientCredentials.getExpiresIn());
            log.info("2");

            spotifyApi.setAccessToken(accessToken);
            log.info("3");

        } catch (IOException | SpotifyWebApiException | org.apache.hc.core5.http.ParseException e) {
            System.out.println("Error: " + e.getMessage());
            accessToken = "error";
        }
    }
}

원본 코드.

나는 이해가 잘 되지 않았다. 분명 application.yml 도 클라이언트 키가 정상적으로 적혀있는데 왜 null pointer 익셉션이 나는지 이해할 수 없었다. 왜냐하면 static은 클래스 변수이기 때문에 클래스가 메모리에 올라가자마자 함게 정의되어 버린다. 그렇기에 null이 들어가 있던 사실이였다.

즉, 왜 이런지 원리를 이해하기 위해서는 @Value 애너테이션의 동작방식에 주목 할 필요가 있다.

@Value 애너테이션은 기본적으로 ${} 문법을 사용할 경우 PropertyPlaceHolderConfigurer 을 이용한 메타데이터 정보를 불라온다. 

 

 PropertyPlaceHolderConfigurer작동 과정

PropertyPlaceHolderConfigurer은 빈 팩토리 후처리기로써 매번 빈 오브젝트가 만들어진 직후에 오브젝트의 내용이나 오브젝트 자체를 변경하는 빈 후처리기와 달리 빈 설정 메타정보가 모두 준비됐을 때 빈 메타정보 자체를 조작하기 위해 사용된다.

예를 들어 database.username과 같이 properties에 작성된 키 값을 ${}안에 넣어주면 Spring이 PropertyPlaceHolderConfigurer를 통해 초기화 작업 중에 해당 값을 실제 값으로 치환한다.

예를 들어 다음과 같이 사용할 수 있는 것이다.

database.username = MangKyu
database.className=org.mariadb.jdbc.Driver

@Value("${database.username}")
private String userName;

@Value("${database.className}")
private String className;

 

하지만 이러한 방법은 대체할 위치를 치환자로 지정해두고, 별도의 후처리기가 값을 변경해주기를 기대하기 때문에 수동적이다. 그래서 초기 빈 메타정보에는 ${database.username}와 같은 문자열이 등록되어 있다. 그러다가 스프링 컨테이너가 설정 파일에서 대체할 적절한 키 값을 찾지 못하면 해당 문자열이 그대로 남아있게 된다. 그래서 치환자의 값이 변경되지 않더라도 예외가 발생하지 않으므로 SpEL을 통한 능동 변환을 권장한다.

초기에는 PropertyPlaceHolderConfigurer를 사용하는 방식을 사용했어야 했지만 Spring3부터 SpEL을 지원하면서 이를 이용하는 방식을 권장하고 있다.

 

 

그래서 static 필드는 클래스가 메모리에 올라가는 동시에 붙어서 올라가기 때문에 Null값이 저장된 것이였다. 이런 점을 해결하기 위해서 Setter 방식으로 생성자 주입을 이용해서 해당 클래스를 @Component로 스프링 빈으로 등록 한 다음에, 스프링 컨테이너에 등록될때 자동으로 의존 주입이 되도록 수정하였다.

 

package com.hositamtam.plypockets.config;

import java.io.IOException;
import java.time.Instant;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.apache.bcel.generic.RET;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials;
import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest;

@Slf4j
@Component
public class SpotifyConfig {

    private static String CLIENT_ID;

    private static String CLIENT_SECRET;


    @Value("${spotify.registration.client-id}")
    public void setClientId(String clientId) {
        CLIENT_ID=clientId;
    }

    @Value("${spotify.registration.client-secret}")
    public void setClientSecret(String clientSecret) {
        CLIENT_SECRET=clientSecret;
    }
    private SpotifyApi spotifyApi = new SpotifyApi.Builder().setClientId(CLIENT_ID).setClientSecret(CLIENT_SECRET).build();

    private  String accessToken;
    private  Instant accessTokenExpiration;

    public String getAccessToken(){
        if (accessToken == null || Instant.now().isAfter(accessTokenExpiration)) {
            refreshAccessToken();
        }
        return accessToken;
    }

    private  void refreshAccessToken() {
        ClientCredentialsRequest clientCredentialsRequest = spotifyApi.clientCredentials().build();
        log.info("새로운 리프레쉬 토큰 발급을 위한 과정 시작");
        log.info("현재 정의된 Client-id : "+CLIENT_ID);
        log.info("현재 정의된 Client-secret : "+CLIENT_SECRET);

        try {

            final ClientCredentials clientCredentials = clientCredentialsRequest.execute();

            log.info("0");
            accessToken = clientCredentials.getAccessToken();
            log.info("현재 발급된 토큰 : "+accessToken);
            // 토큰의 유효 시간을 계산하여 저장합니다.
            accessTokenExpiration = Instant.now().plusSeconds(clientCredentials.getExpiresIn());
            log.info("2");

            spotifyApi.setAccessToken(accessToken);
            log.info("3");

        } catch (IOException | SpotifyWebApiException | org.apache.hc.core5.http.ParseException e) {
            System.out.println("Error: " + e.getMessage());
            accessToken = "error";
        }
    }
}