JSR-303 Annotation Customizing

데이터를 저장하기 전에는 데이터에 대한 검증이 요구된다.
서비스에 정의한 값들이 매우 반복적이여서 검증하는 코드를 숨기고 명시적으로 보여줄 수 있는 것 같아서 JSR-303 어노테이션을 커스텀하는 방식을 사용해봤다.

우선 필드에서 검증할 어노테이션을 생성해준다. 이 때, message, groups, payload 메소드는 반드시 존재해야 한다.
그리고 values 라는 속성을 추가하여 주로 사용되는 값을 디폴트로 설정했다. 다른 값을 검증하려면 필드에 설정한 어노테이션에 values 속성을 설정해주면 된다.
@Constraint 어노테이션에는 직접 생성할 ConstraintValidator를 구현하는 Validator를 지정해줘야 한다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AllowedValueValidator.class)
public @interface AllowedValue {
  String[] values() default { “S”, “M”, “L" };
  String message() default “{com.validator.constraints.AllowedValue.message}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

Validator를 생성하여 ConstraintValidator 인터페이스를 상속 받고, 인터페이스에 선언된 메소드를 구현해줘야 한다.
initialize 메소드에서는 초기화할 데이터가 필요한 경우 설정해주면 된다. 앞서 만든 어노테이션을 인자로 하여 디폴트로 설정한 값으로 초기화했다.
isValid 메소드에서는 실제 검증할 내용을 작성해주면 된다. 검증할 필드에 요청된 값이 values에 정의된 값 중에 존재하는지 확인했다.

public class AllowedValueValidator implements ConstraintValidator<AllowedValue, String> {
  private String[] values;
  @Override
  public void initialize(AllowedValue constraintAnnotation) {
    this.values = constraintAnnotation.values();
  }
  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    return ArrayUtils.contains(values, value);
  }
}

검증할 필드에 커스텀한 어노테이션을 표기한다.
특정 필드에서는 기본 검증 값이 아닌 “red”, “blue” 값을 검증하도록 표기했다.

public class Clothing {

  private String id;

  private String name;

  @AllowedValue
  private String topSize;

  @AllowedValue
  private String bottomSize;

  @AllowedValue(values = { "red", "blue" })
  private String color;
}

컨트롤러에서는 기존 처럼 모델에 @Valid와 BindingResult만 표기해준다.
해당 요청이 들어오면 모델 필드에 표기한 커스텀 어노테이션의 Validator에 작성한 isValid 메소드로 검증하게 된다.
조건에 맞지 않는 경우에는 bindingResult의 해당 필드에 대한 에러 내용이 담긴다.

@PostMapping("/clothing/update")
public void update(@Valid Clothing clothing, BindingResult bindingResult) {
  if (bindingResult.hasErrors()) {
    FieldError fieldError = bindingResult.getFieldError();
    throw new RuntimeException(fieldError.getDefaultMessage());
  }
  ...
  clothingManager.update(clothing);
}

매우 반복적인 값들에 대한 검증으로 괜찮다고 생각한다.
하지만 모델 필드에 values 에 표기할 값들이 많아지는 경우는 오히려 코드가 복잡해 보일 수도 있다.
검증하는 방법은 다양하니 상황에 적절하게 잘 사용하면 좋을 것 같다.

DatabaseConfiguration 적용


PropertiesConfiguration을 사용하여 서비스에 사용되는 설정 값들을 실시간으로 변경할 수 있다.
서버가 적을 경우에는 각 서버마다 설정을 변경하는 것이 크게 번거롭지 않지만
서버가 많을 경우에는 모든 서버의 설정을 변경하는 것은 곤혹스럽다.
물론 서버마다 설정을 다르게 지정해야 하는 경우라면 어쩔 수 없는 것 같다.
모든 서버의 설정값을 동일하게 가져간다면 DB편히 관리할 수 있다.

그 뿐만이 아니다. AWS Auto Scaling 같은 것을 사용하고 있다면 서버 사용량에 따라 미리 만들어 놓은 서버 이미지로 서버가 늘고 줄게된다.
해당 서버 이미지의 설정값은 변경되지 않은 기존 이미지의 설정값을 따르기 때문에 스케일링 서버들은 다르게 동작하게 된다.
그렇기 때문에 설정값을 각각 수정하더라도 이미지를 다시 만들어야하기에 번거롭다.
그러므로 DatabaseConfiguration을 적용하면 스케일링에 상관없이 DML 수행만으로 서버에 구애 받지 않고 실시간으로 설정값을 변경할 수 있다. 

#DatabaseConfiguration 빈 등록
@Configuration
public class DatabaseConfig {

  @Bean
  public DataSource dataSource() {
    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost/test");
    dataSource.setUsername("root");
    dataSource.setPassword("root");
    return dataSource;
  }

  @Bean
  public DatabaseConfiguration databaseConfig() {
    return new DatabaseConfiguration(dataSource(), "config", "key", "value"); // table명, key/value명
  }

}

#config 테이블 정의 (JPA)
@Entity
@Table(name = "config")
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "key" })
@NoArgsConstructor
public class Config {

  @Id
  @Column
  private String key;

  @Column
  private String value;

}

#프로퍼티 값 설정
@Component
public class ConfigUtil {

  @Autowired
  private DatabaseConfiguration dbConfig;

  // key, default value

  public int getRetryCount() {
    return dbConfig.getInteger("retry.count", 3);
  }

  public boolean isSendAsync() {
    return dbConfig.getBoolean("send.async", false);
  }

  public String getPaymentServerAddress() {
    return dbConfig.getString("payment.server.address", "www.pay.com");
  }

}

동작 방식 예시
1. config 테이블에서 retry.count 라는 key로 DB를 읽는다.
2. DB에서 해당 key의 value 값을 반환한다.
3. DB에 해당 key가 존재하지 않으면 default 값으로 지정된 3을 반환한다.

추가적으로 해당 설정값들의 변경될 가능성을 감안하여 캐시를 적용하면 DB를 읽는 수고를 덜 수 있다.


Configuration Condition

조건에 따라 환경을 설정할 수 있다.
RabbitMQ를 사용하는 서버와 사용하지 않는 서버의 환경을 달리 설정하는데 적용했다.

- Example
amqp.enabled 프로퍼티로 amqp 사용여부를 판단한다.
amqp를 사용하는 경우, 해당 클래스에 정의한 빈들을 등록한다.
amqp를 사용하지 않는 경우, 빈을 등록하지 않는다.

@Configuration
@Conditional(AmqpConfig.Condition.class)
public class AmqpConfig {

  @Autowired
  private Environment env;

  public static class Condition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
      return context.getEnvironment().getProperty("amqp.enabled", Boolean.class);
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
      return ConfigurationPhase.PARSE_CONFIGURATION;
    }
  }

  // @Bean
}

컨텍스트에 특정 빈이 등록되어 있는가? 조건 등으로도 사용할 수 있다.

multiple 'X-Frame-Options' headers with conflicting 이슈 해결

동일 도메인에서 <iframe>을 통해 접근하는 경우 X-Frame-Options을 DENY로 설정하면 최신(?) 브라우저에서는 접근할 수 없는 문제가 발생할 수 있다. 이 경우 동일 도메인에서는 <iframe> 접근이 가능하도록 X-Frame-Options를 SAMEORIGIN으로 설정해줘야 한다.

<iframe>을 사용하는 부분이 있어 기존에 아래와 같이 SAMEORIGIN으로 설정해서 잘 사용하고 있었다.

http.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN));

그런데 스프링 시큐리티 버전(3.2.3 -> 4.0.4)을 올리면서 동작에 문제가 생겼다. 브라우저에서 아래와 같은 에러가 발생했다.

이슈

[Chrome]
in a frame because it set multiple 'X-Frame-Options' headers with conflicting values ('DENY, SAMEORIGIN'). Falling back to 'deny'


http 응답 헤더를 보니 DENY와 SAMEORIGIN 설정이 중첩되는 문제가 발생했다.
스프링 시큐리티 버전이 올라가면서 default 값인 DENY에 SAMEORIGIN이 추가되어 충돌난 듯하다.

해결방안

내용을 찾아보니 스프링 시큐리티 4.x 부터 설정하는 방식이 변경되었다.

http.headers().frameOptions().sameOrigin();

위와 같이 설정하니 문제없이 잘 동작하는 것을 확인했다.

'Development > Spring' 카테고리의 다른 글

DatabaseConfiguration 적용  (0) 2018.08.17
Configuration Condition  (0) 2018.07.16
비동기 메소드 HttpServletRequest 에러  (0) 2017.12.03
비동기 어노테이션  (0) 2017.11.25
캐시 어노테이션  (0) 2017.10.06

비동기 메소드 HttpServletRequest 이슈 해결

@Async로 선언된 비동기 메소드를 호출했다. 메소드는 해당 클래스에 @Autowired된 HttpServletRequest로 부터 정보를 가져오다가 SimpleAsyncUncaughtExceptionHandler.handleUncaughtException() 예외가 발생했다.

No thread-bound request found: Are you referring to request attributesoutside of an actual web request, or processing a request outside of theoriginally receiving thread.

원인은 비동기 처리시 다른 스레드에서 동작하기 때문이다.

해결방안

  • request로 부터 필요한 정보를 메소드의 파라미터로 전달한다.
  • 다음과 같이 RequestContextHolder에서 현재 HttpServletRequest를 가져온다.
// WebApplicationInitializer 설정에 리스너 추가
servletContext.addListener(new RequestContextListener());

ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();


'Development > Spring' 카테고리의 다른 글

DatabaseConfiguration 적용  (0) 2018.08.17
Configuration Condition  (0) 2018.07.16
multiple 'X-Frame-Options' headers with conflicting 에러  (0) 2017.12.15
비동기 어노테이션  (0) 2017.11.25
캐시 어노테이션  (0) 2017.10.06

Spring @Async

  • 메소드를 비동기 처리하면 호출자는 호출한 메소드가 완료될 때 까지 기다릴 필요가 없다.
  • @Async를 사용하여 별도의 쓰레드에서 실행시킬 것이다.

Async Configuration

// Java Config
@Configuration
@EnableAsync
public class SpringAsyncConfig {
  ...
}

// XML Config
<task:executor id="myexecutor" pool-size="5"/>
<task:annotation-driven executor="myexecutor"/>

@Async

제약사항

  • @Async로 명시된 메소드는 반드시 public으로 선언되어야 한다. 메소드가 public 이어야 프록시가 될 수 있기 때문이다.
  • 같은 클래스 내에서 해당 메소드를 호출할 경우 비동기로 작동하지 않는다. 셀프호출은 프록시를 우회하고 해당 메소드를 직접 호출하기 때문이다.

void 반환형

@Async
public void asyncMethodWithVoidReturnType() {
    System.out.println("Execute method asynchronously. "
      + Thread.currentThread().getName());
}

Future 반환형

@Async
public Future<String> asyncMethodWithReturnType() {
    System.out.println("Execute method asynchronously - "
      + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return new AsyncResult<String>("hello world !!!!");
    } catch (InterruptedException e) {
        //
    }

    return null;
}

Executor

  • 스프링은 기본값으로 SimpleAsyncTaskExecutor를 사용하여 실제 메소드들을 비동기 실행한다. 기본 설정은 메소드/애플리케이션 레벨로 재정의하여 사용할 수 있다.
  • 다음과 같이 설정하면 @Async로 지정된 메소드를 실행하는 기본 실행자가 변경된다. 실행자의 상세 옵션들도 변경할 수 있다.

Method Level Override

  • 실행자 빈을 설정하고 @Async 속성에 실행자명을 명시해야 한다.
@Configuration
@EnableAsync
public class SpringAsyncConfig {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
    System.out.println("Execute method with configured executor - "  + Thread.currentThread().getName());
}

Application Level Override

  • AsyncConfigurer 인터페이스를 구현하여 getAsyncExecutor() 메소드를 오버라이드해야 한다.
@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(100);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("TEST_");
    executor.initialize();
    return executor;
  }

}

Exception Handling

  • 반환형이 void일 때, 예외는 호출 스레드에 전달되지 않는다. 예외 처리를 위한 설정이 추가적으로 필요하다.
@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(100);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("TEST_");
    executor.initialize();
    return executor;
  }

  @Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return new SimpleAsyncUncaughtExceptionHandler();
    // Custom 가능
    // return new CustomAsyncExceptionHandler();
  }

}
  • AsyncUncaughtExceptionHandler 인터페이스를 구현하여 커스텀 비동기 예외처리자를 만들 수도 있다. handleUncaughtException() 메소드는 잡히지 않은 비동기 예외가 발생할때 호출된다.
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.out.println("Exception message : " + throwable.getMessage());
        System.out.println("Method name : " + method.getName());
        for (Object param : obj) {
            System.out.println("Parameter value : " + param);
        }
    }

}

Etc

  • Spring Boot 기반의 웹 애플리케이션은 HTTP 요청이 들어왔을 때 내장된 서블릿 컨테이너가 관리하는 독립적인 1개의 Worker 쓰레드에 의해 동기 방식으로 실행된다. 하지만 요청 처리 중 @Async가 명시된 메소드를 호출하면 앞서 등록한ThreadPoolTaskExecutor 빈에 의해 관리되는 또 다른 독립적인 Worker 쓰레드로실행된다. 별도의 쓰레드로 동작하기 때에 원래의 요청 쓰레드는 그대로 다음 문장을 실행하여 HTTP 응답을 마칠 수 있다.

  • Async 어노테이션을 사용하는 순간 서로 다른 Thread에서 동작하기 때문에 앞에서 생성한 Transaction이 연장되지 않으므로 주의해야 한다.

레퍼런스 - http://www.baeldung.com/spring-async

'Development > Spring' 카테고리의 다른 글

DatabaseConfiguration 적용  (0) 2018.08.17
Configuration Condition  (0) 2018.07.16
multiple 'X-Frame-Options' headers with conflicting 에러  (0) 2017.12.15
비동기 메소드 HttpServletRequest 에러  (0) 2017.12.03
캐시 어노테이션  (0) 2017.10.06
캐시 추상화
추상화는 Java 메서드에 캐싱을 적용함으로써 캐시에 보관된 정보로 메서드의 실행 횟수를 줄여준다.
즉, 대상 메서드가 실행될 때마다 추상화가 해당 메서드가 같은 인자로 이미 실행되었는지 확인하는 캐싱 동작을 적용한다.
해당 데이터가 존재한다면 실제 메서드를 실행하지 않고 결과를 반환하고,
존재하지 않는다면 메서드를 실행하여 그 결과를 캐싱한 뒤에 사용자에게 반환해서 다음 호출 시에 사용할 수 있도록 한다.

@Cacheable
- 캐시할 수 있는 메서드를 지정할 때 사용한다.  즉, 캐시를 통해 메서드 실행을 건너뛸 수 있다.
- 캐시는 본질적으로 key-value 저장소이므로 캐시된 메서드를 호출할 때마다 해당 키로 변환되어야 한다.
- 캐시 추상화는 다음 알고리즘에 기반을 둔 KeyGenerator를 사용한다.
1) 파라미터가 없으면 0을 반환한다.
2) 파라미터가 하나만 있으면 해당 인스턴스를 반환한다.
3) 파라미터가 둘 이상이면 모든 파라미터의 해시를 계산한 키를 반환한다.

@CachePut
- 메서드 실행에 영향을 주지 않고 캐시를 갱신할 때 사용한다. 즉, 메서드를 항상 실행하고 어노테이션 옵션에 따라 그 결과를 캐시에 보관한다.

@CacheEvict
- 캐시를 제거하는 메서드를 구분할 때 사용한다. 즉, 캐시에서 데이터를 제거하는 트리거로 동작하는 메서드다.
- 한 지역의 전체 캐시를 모두 지워야 할 때 옵션(allEntries=true)을 편리하게 사용할 수 있다.
   비효율적으로 각 엔트리를 하나씩 지우는 것이 아니라 한 번에 모든 엔트리를 제거한다.
- @CacheEvict를 void 메서드에 사용할 수 있다는 것은 중요한 부분이다. 메서드가 트리거로 동작하므로 반환값을 무시한다.

@Caching
같은 계열의 어노테이션을 여러 개 지정해야 하는 경우가 있다. 
예를 들어, 조건이나 키 표현식이 캐시에 따라 다른 경우이다. 자바는 이러한 선언을 지원하지 않지만 우회할 수 있다.
@Caching에서 중첩된 @Cacheable, @CachePut, @CacheEvict를 같은 메서드에 다수 사용할 수 있다.

캐시 어노테이션 활성화
캐시 어노테이션을 선언하는 것만으로 자동으로 동작이 실행되지 않는다는 것이 중요하다. 스프링의 다른 기능처럼 선언적으로 기능을 활성화해야 한다.
이는 캐시에 문제가 있다고 의심된다면 코드의 모든 어노테이션을 지우는 대신 활성화 설정만 지워서 캐시를 비활성화 시킬 수 있다는 의미이기도 하다. 
실제로는 이 선언으로 스프링이 캐시 어노테이션을 처리하도록 한다.
- XML기반 : <cache:annotation-driven/>
- Java Config 기반 : @EnableCaching


+ Recent posts