Spring Boot version 2.1.x에서 2.2.x (spring frame 5.2 이상)으로 버전업 진행 시 Spring Cloud AWS SnsAutoConfiguration 에서 TypeNotPresentExceptionProxy 가 발생하며 실행안되는 문제

web/Spring|2020. 8. 21. 15:10

문제 발생


평소 문제가 많았던 webflux 부분 수정을 위해 spring boot 2.1.3에서 2.2.7버전으로 업그레이드를 진행하기 위해 gradle에서 spring boot version을 2.2.7로 변경하고 애플리케이션을 실행 시켰다. 

 

그런데 평소에는 자주본적이 없던 TypeNotPresentExceptionProxy 에러가 발생했다. 

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.baemin.bmart.search.BmartSearchAdminApplication]; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
	at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:609)
	at org.springframework.context.annotation.ConfigurationClassParser.access$800(ConfigurationClassParser.java:110)
	at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.lambda$processGroupImports$1(ConfigurationClassParser.java:811)
	at java.util.ArrayList.forEach(ArrayList.java:1257)

에러가 발생한 호출 스택 정보를 보니 ConfigurationClassParser에 processImports에서 발생한걸로 보아 @Configuration 어노테이션이 적용된 클래스에서 빈정보를 import하다가 에러가 발생한 것 같았다.

 

그래서 어떤 에러로 인해 발생한 것인지 확인해보기 위해서 TypeNotPresentExceptionProxy 클래스에 디버깅을 찍고 문제가 발생한 로그를 확인해봤다.

 

java.lang.NoClassDefFoundError org/springframework/web/servlet/config/annotation/WebMvcConfigurer 가 발행했다.

 

WebMvcConfigurer NoClassDefFoundError가 발생할 이유가 없는데 발생하여 난감해하고 있었는데 확인해보니 내부적으로 사용중이던 compile 'org.springframework.cloud:spring-cloud-aws-messaging' 라이브러리에서 SnsWebConfiguration이 있는데 이게 2.1.0버전에는 아래와 같이 구현되어 있다.

@Configuration
@ConditionalOnClass("org.springframework.web.servlet.config.annotation.WebMvcConfigurer")
public class SnsWebConfiguration implements WebMvcConfigurer {

    @Autowired
    private AmazonSNS amazonSns;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(getNotificationHandlerMethodArgumentResolver(this.amazonSns));
    }
}

여기서 Class가 class path에 잘 있는지 확인하는 @ConditionalOnClass 어노테이션의 value에 WebMvcConfigurer 클래스의 존재를 체크하도록 설정되어 있다.

 

@ConditionalOnClass의 경우에는 해당 클래스가 잘 있으면 Bean으로 등록하라는 하나의 조건인데 만약 WebMvcConfigurer이 없으면 해당 Configuraion을 안쓰면 되는데 왜 죽은거서 인가 하고 의아했다.

 

여기서 같은 실에 개발자인 용근님이 올리신 issue를 보고 알게 되었다. 

https://github.com/spring-cloud/spring-cloud-aws/issues/549

https://github.com/spring-cloud/spring-cloud-aws/issues/503

 

ArrayStoreException in SnsWebConfiguration. · Issue #549 · spring-cloud/spring-cloud-aws

Hello, Because implement and ConditionalOnClass are specified in SnsWebConfiguration at the same time, an exception occurs during the class load. When WebMvcConfigurer does not exist, Caused by: ja...

github.com

SnsWebConfiguration에서 WebMvcConfigurer를 implements하고 있는데 해당 classpath를 찾을 수 없어 발생한 오류입니다.

 

그런데 왜 단순히 spring boot version만 올렸고 spring cloud aws 버전은 2.1.0버전 그대로 사용하고 있는데, 이게 2.1.3버전에서는 발생하지 않고 2.2.7버전에서만 발생한건지 궁금해서 원인을 찾아보았다.

 

 

 

원인분석


우선 두 개의 버전에서 어떤 차이가 있는지 정확하게 알지 못하기에 spring boot가 뜨기 위해서 호출하는 ConfigurationConfigurationClassParser 부분을 집중적으로 확인해봤다.

 

두 개의 버전을 사용하는 모듈을 동시에 실행시켜 놓고 ConfigurationClassParser클래스에 processImports부분을 살펴봤다. 디버깅 코드를 찍고 오류가 시작되는 부분을 찾고 호출스택을 하나씩 역추적해봤다. 

 

의미있는 부분부터 정리하면 processImports 메소드가 호출되고 내부 코드에서 아래 processConfigurationClass를 호출하고 그 내부에서 doProcessConfigurationClass메소드를 다시 호출한다.

 

이 과정을 통해 configuration class들에 대한 메타데이터 정보를 수집하게 되는데 이때 메타데이터를 수집하기 위해서 사용되는 클래스가 SimpleMetadataReader 이다.

 

아래 두 개의 코드를 보면 차례로 2.1.3 버전의 코드와 2.2.7버전의 SimpleMetadataReader를 확인할 수 있다.

SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
   InputStream is = new BufferedInputStream(resource.getInputStream());
   ClassReader classReader;
   try {
      classReader = new ClassReader(is);
   }
   catch (IllegalArgumentException ex) {
      throw new NestedIOException("ASM ClassReader failed to parse class file - " +
            "probably due to a new Java class file version that isn't supported yet: " + resource, ex);
   }
   finally {
      is.close();
   }

   AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
   classReader.accept(visitor, ClassReader.SKIP_DEBUG);

   this.annotationMetadata = visitor;
   // (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
   this.classMetadata = visitor;
   this.resource = resource;
}
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
   SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
   getClassReader(resource).accept(visitor, PARSING_OPTIONS);
   this.resource = resource;
   this.annotationMetadata = visitor.getMetadata();
}

private static ClassReader getClassReader(Resource resource) throws IOException {
	try (InputStream is = resource.getInputStream()) {
		try {
			return new ClassReader(is);
		}
		catch (IllegalArgumentException ex) {
			throw new NestedIOException("ASM ClassReader failed to parse class file - " +
				"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
		}
	}
}

두 개 생성자를 보면 ClassReader를 가지고 오는 부분이 메서드로 2.2.7버전에서 빠진것을 제외해보면 다른 부분이 MetadataReadingVisitor 클래스가 다르다는 것이다.

 

2.1.3버전에서는 AnnotationMetadataReadingVisitor를 사용하고 2.2.7버전에서는 SimpleAnnotationMetadataReadingVisitor를 사용하는데 AnnotationMetadataReadingVisitor은 spring 5.2버전 부터 deprecated되었다.

 * @deprecated As of Spring Framework 5.2, this class has been replaced by
 * {@link SimpleAnnotationMetadataReadingVisitor} for internal use within the
 * framework, but there is no public replacement for
 * {@code AnnotationMetadataReadingVisitor}.
 */

 

그럼 이 MetadataReadingVisitor클래스가 무엇인지 확인해보자. 이 클래스들에 대한 설명은 아래와 같은데 정리해보면 AnnotaionMetadata 인터페이스를 통해 공개되어 있는 클래스 이름과 구현된 클래스에 정의되어 있는 어노테이션 뿐만 아니라 구현된 타입을 찾기 위한 ASM Class visitor 라고 설명이 되어있다.

* ASM class visitor which looks for the class name and implemented types as
* well as for the annotations defined on the class, exposing them through
* the {@link org.springframework.core.type.AnnotationMetadata} interface.

ASM은 자바 바이트코드를 조작하고 분석하는 프레임워크인데 위에 MetadataReadingVisitor들은 결국 Configuration class import parse를 하는 작업에서 메타 데이터를 가지고 오는 SimpleMetadataReader에서 class들에 대한 정보를 가지고 오기 위한 역할을 하는 클래스들이다.

 

상위 추상클래스 ClassVisitor의 메소드를 살펴보면 visit, visitSource, visitModule, visitOuterClass, visitAnnotation, visitTypeAnnotation, visitAttribute, visitInnerClass, visitEnd등등 클래스에 대한 정보를 가져올 때 사용되는 것이라는걸 확인할 수 있다.

 

이 때 에러가 발생되면서 애플리케이션이 죽냐 안죽냐가 결정이 되는걸로 봐서 이쪽에 처리가 서로 다르게 되어있을 거라고 짐작하고 더 확인해봤다.

 

우선 2.1.3 버전에 경우에는 visitEnd() 메소드를 호출할 때 에러가 발생하게 되는데 AnnotationMetadataReadingVisitor의 visitEnd 메소드는 우선 아래와 같이 되어있다.

@Override
public void visitEnd() {
   super.visitEnd();

   Class<? extends Annotation> annotationClass = this.attributes.annotationType();
   if (annotationClass != null) {
      List<AnnotationAttributes> attributeList = this.attributesMap.get(this.annotationType);
      if (attributeList == null) {
         this.attributesMap.add(this.annotationType, this.attributes);
      }
      else {
         attributeList.add(0, this.attributes);
      }
      if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationClass.getName())) {
         try {
            Annotation[] metaAnnotations = annotationClass.getAnnotations();
            if (!ObjectUtils.isEmpty(metaAnnotations)) {
               Set<Annotation> visited = new LinkedHashSet<>();
               for (Annotation metaAnnotation : metaAnnotations) {
                  recursivelyCollectMetaAnnotations(visited, metaAnnotation);
               }
               if (!visited.isEmpty()) {
                  Set<String> metaAnnotationTypeNames = new LinkedHashSet<>(visited.size());
                  for (Annotation ann : visited) {
                     metaAnnotationTypeNames.add(ann.annotationType().getName());
                  }
                  this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);
               }
            }
         }
         catch (Throwable ex) {
            if (logger.isDebugEnabled()) {
               logger.debug("Failed to introspect meta-annotations on " + annotationClass + ": " + ex);
            }
         }
      }
   }
}

 

여기서 annotaion 정보를 가지고 올 때 TypeNotPresentExceptionProxy에러가 동일하게 발생한다.

 

하지만 여기서 2.2.7버전과의 차이점은 이 AnnotationMetadataReadingVisitor 경우에는 에러가 발생했을 때 내부적으로 로깅만 하고 상위로 에러를 전파하지 않는다는 점이다. 그래서 결국 에러가 발생하지 않고 로깅만 되고 애플리케이션이 실행되는데 까지는 문제가 없었고 내부적으로 Sns 기능을 쓰지 않기에 문제가 없었던 것이었다.

 

 

 

그럼 이제 2.2.7버전에서는 어떤지 확인해보자. 

2.2.7에서 사용하는 SimpleAnnotationMetadataReadingVisitor의 경우에는 클래스에 정보를 얻기위해 작업하는 visit와 같은 동작들에 대해서 별도의 try catch 작업이 되어 있지 않다.

 

실제로 에러가 발생되는 visitAnnotation메소드를 보면 이 곳에서는 visitAnnotaion을 통해 정보를 가져올 때 내부적으로 MergedAnnotaionReadingVisitor visitor를 사용하지만 별도에 에러에 대한 처리가 되어 있지 않는걸 볼 수 있다. 

@Override
@Nullable
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
	return MergedAnnotationReadingVisitor.get(this.classLoader, this::getSource,
			descriptor, visible, this.annotations::add);
}

 

그로 인해 visitor를 통해 configuration class에 내부정보를 가져오다가 에러가 발생하게 되면 processImport에 에러가 전파되어 BeanDefinitionStoreExeption이 발생하면서 결국 application이 실행이 되지 않는 것이다.

ConfigurationClassParser클래스 내부에 processImports 코드

 

 

 

해결방법


문제가 어찌되었든 우리는 spring boot의 버전을 올려야한다. 그래서 정식으로 수정이 완료된 2.2.3버전을 사용을 하던지 또는 임시로 webmvc 라이브러리를 추가해주는 것이다.

 

 

해결방법 1.

https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-messaging

// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-messaging
compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-messaging', version: '2.2.3.RELEASE'

 

해결방법 2.

compile('org.springframework:spring-webmvc')

 

 

댓글()

inteliij 사용 시 related gradle configuration 설정을 찾지 못할 경우 해결방법

web/Spring|2018. 12. 19. 23:42

Inteliij로 프로젝트를 정상적으로 작업하고 있었는데 어느날 application.yml 설정을 읽지 못해서 오류가 막 발생했다.


그래서 인텔리제이 오류 내용에서 찾아보던 중 다음과 같은 오류 내용을 볼 수 있었다.

Unable to make the module: related gradle configuration was not found. Please, re-import the Gradle project and try again


그래서 무엇인고 하니 gradle 설정을 찾지 못한다는 이슈인 것 같아서 gradle 설정을 새로고침 해줬다.

방법은 Gradle Task 관련 view에서 새로고침 버튼만 눌러주면 된다.


## 만약 view가 보이지 않는다면 여기 메뉴에서 찾아서 설정할 수 있다.





댓글()
  1. 나그네 2019.09.03 08:58 댓글주소  수정/삭제  댓글쓰기

    위와 같은 오류에서 제시한 방법은 유효하지 않습니다. spring boot 2.1.7 / jdk11

Spring Validation을 이용해서 요청 검증처리

web/Spring|2018. 8. 9. 23:30

대게 개발을 진행할 때 front에서 validate를 체크하고 민감한 정보에 대해서는 한번더 체크를 진행하고 작업을 했었다. 

하지만 클라이언트에서만 validation을 체크하게 되는 경우 브라우저에서 악의적인 행동에 대해서 대응하기 어려워질수 있기 때문에 백엔드에서도 Validation을 처리해야한다. 

여기서 사용되는 @valid 어노테이션들을 알아보자.

1. DTO validation 선언
우선적으로 DTO에 각 속성에 필요한 @valid 옵션들을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.wedul.springboottest.rest.dto;
 
import lombok.*;
 
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
 
/**
 * 사용자 클래스.
 * User: wedul
 * Date: 2018-08-05
 * Time: 오후 9:47
 */
@Data
@AllArgsConstructor
@EqualsAndHashCode
@NoArgsConstructor
public class UserDto {
    @NotBlank(message = "이름을 입력해주세요.")
    private String name;
 
    private int age;
 
    @NotBlank(message = "이메일을 입력해주세요.")
    @Email(message = "이메일을 양식을 지켜주세요.")
    private String email;
 
    public void updateInfo(UserDto user) {
        this.age = user.getAge();
        this.email = user.getEmail();
    }
}
cs


@Valid 데이터 종류

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@AssertFalse : false 값만 통과 가능
@AssertTrue : true 값만 통과 가능
@DecimalMax(value=) : 지정된 값 이하의 실수만 통과 가능
@DecimalMin(value=) : 지정된 값 이상의 실수만 통과 가능
@Digits(integer=,fraction=) : 대상 수가 지정된 정수와 소수 자리수보다 적을 경우 통과 가능
@Future : 대상 날짜가 현재보다 미래일 경우만 통과 가능
@Past : 대상 날짜가 현재보다 과거일 경우만 통과 가능
@Max(value) : 지정된 값보다 아래일 경우만 통과 가능
@Min(value) : 지정된 값보다 이상일 경우만 통과 가능
@NotNull : null 값이 아닐 경우만 통과 가능
@Null : null일 겨우만 통과 가능
@Pattern(regex=, flag=) : 해당 정규식을 만족할 경우만 통과 가능
@Size(min=, max=) : 문자열 또는 배열이 지정된 값 사이일 경우 통과 가능
@Valid : 대상 객체의 확인 조건을 만족할 경우 통과 가능
 
출처: http://goldenraccoon.tistory.com/entry/Valid-annotation-종류 [황금너구리 블로그]
cs


2. @Request에서 전달받는 파라미터 설정
간단하게 @Valid 설정을 통해 전달받을 객체에 대한 유효성 체크를 설정할 수 있다.

1
2
3
4
5
6
7
8
9
10
 /**
   * Edit user info response entity.
   *
   * @param user the user
   * @return the response entity
 */
 @PutMapping(value = "/user/info")
 public ResponseEntity<?> editUserInfo(@RequestBody @Valid UserDto user) {
     return ResponseEntity.ok(userService.updateUser(user));
 }
cs

3. 테스트
위의 설정한 validation 설정을 위반하여 request를 보내보자

요청

응답

validation 위반 시 400에러와 함께 오류 field와 기존에 설정한 defaultMessage를 확인할 수 있다.


4. 추가적인 action에 대한 validation 설정
단순하게 regex와 null, email 등등에 대한 validation 체크 이외에 중복 체크등을 진행하기 위해서는 별도의 작업이 필요하다.

4-1)  기존의 Validation과 동일한 방식으로 표현하기 위해서 별도의 RuntimeException 클래스를 만든다.

이 예외 클래스는 Response Stauts는 400으로 전달될 수 있게 @ResponseStatus(HttpStatus.BAD_REQUEST)로 설정하고 예외가 발생한 field와 defaultMessage를 출력해주기 위해 별도의 속성을 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.wedul.springboottest.rest.exception;
 
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
 
import java.util.List;
 
/**
 * Created by Leo.
 * User: wedul
 * Date: 2018-08-09
 * Time: 오후 9:06
 */
// 기존에 validation의 에러와 동일하게 400 에러가 발생하도록 추가
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class ValidationException extends RuntimeException {
 
    private Error[] errors;
 
    public ValidationException(String defaultMessage, String field){
        this.errors = new Error[]{new Error(defaultMessage, field)};
    }
 
    public ValidationException(Error[] errors) {
        this.errors = errors;
    }
 
    public Error[] getErrors() {
        return errors;
    }
 
    public static class Error {
 
        private String defaultMessage;
        private String field;
 
        public Error(String defaultMessage, String field) {
            this.defaultMessage = defaultMessage;
            this.field = field;
        }
 
        public String getDefaultMessage() {
            return defaultMessage;
        }
 
        public String getField() {
            return field;
        }
    }
 
 
}
cs


이 생성된 ValidationException 클래스를 ErrorAttributes 인터페이스를 재 정의하여 Validation 오류가 발생하였을 때 별도의 작업을 할 수 있도록 처리해준다. (이 Bean 객체는 Application에 정의해준다.)


※ ErrorAttributes는 기본적으로 스프링 부트에서 에러 처리하는 BasicErrorController인데 이 인터페이스는 DefaultErrorAttributes 클래스를 참조하여 커스터마이징이 가능하다.
참고
https://brunch.co.kr/@sbcoba/9


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
 
 
 
@SpringBootApplication
public class SpringboottestApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringboottestApplication.class, args);
    }
 
    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
 
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest,
                                                          boolean includeStackTrace) {
            Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
            Throwable error = getError(webRequest);
 
            // validatijon Exception에 경우 별도의 처리를 진행한 에러 데이터 추가
            if (error instanceof ValidationException) {
                errorAttributes.put("errors", ((ValidationException)error).getErrors());
            }
            return errorAttributes;
        }
    };
}
cs


그리고 서비스 코드 영역에서 중복, 금지 등등을 확인하고  ValidationException을 throw하면 front에 동일한 형태에 에러코드를 전송해 줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 서비스 코드
 
@Override
public ResultDto insertUser(UserDto user) {
    if (null == users) {
       return ResultDto.fail("UserList Empty");
    }
 
    if (null == user) {
        return ResultDto.fail("Request User Is Null");
    } else {
        if (users.contains(user)) {
            // 중복된 부분에 대해 ValidationException
            throw new ValidationException("name""이름이 중복됩니다.");
        }
        users.add(user);
        return ResultDto.success();
    }
}
cs

테스트 해보면 다음과 같이 중복코드에 대해서 체크가 정상적으로 되는 것을 알 수 있다.

물론 이렇게 RuntimeException을 만들지 않고 팩토리 객체를 만들어서 동일하게 실패 메시지를 만들어서 줘도 상관없다.

하지만 예측이 어려운 Runtime Exception을 잘 정의해놓으면 편하게 처리할 수 있다. 




댓글()

외부 properties 파일을 이용해서 스프링 빈을 생성하는 방법

web/Spring|2018. 5. 27. 11:36

1. XML에서 프로퍼티 설정


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// xml
 
<context:property-placeholder location="classpath:database.properties" />
 
<bean id="dataSource" class=".............">
  <property name="driverClass" value="${db.driver}" />
  <property name="jdbcUrl" value="${db.jdbcUrl}" />
  <property name="user" value="${db.user}" />
  <property name="password" value="${db.password}" />
</bean>
 
// properties 파일
db.driver=com.mysql.jdbc.Driver
db.jdbcUrl=jdbc:mysql://localhost/spring4fs?characterEncoding=utf8
db.user=test
db.password=test
cs



<context:property-placeholder> 태그는 location 속성으로 지정한 프로퍼티 파일로부터 정보를 읽어와 빈 설정에 입력한 플레이스 홀더의 값을 프로퍼티 파일에 존재하는 값으로 변경한다.
# place holder는 ${로 시작하고 }로 끝나는 값.

주의 할 점은
서로 다른 xml에서 서로다른 위치에 프로퍼티 파일을 사용한다고 해도 
먼저 열린 프로퍼티 값이 우선순위를 가지게 되어 두 번째 프로퍼티 파일이 열리지 않을 수 있다.

예를 들면



1
2
3
4
5
test.xml에서 
<context:property-placeholder location="classpath:test.properties" />
 
db.xml에서
<context:property-placeholder location="classpath:db.properties" />
cs



오류 메시지
Could not resolve placeholder .... in string value ...

이를 해결하기 위해서는 한 곳에서 placeholder를 사용하도록 구성하는 것이 좋다.

2. @Configuartion 애노테이션 이용 자바 설정에서의 프로퍼티 사용




@Configuration public class Config { @Value("${db.driver}") private String driver; @Bean public static PropertySourcesPlaceHolderConfigurer properties() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); configurer.setLocation(new ClassPathResources("test.properties")); return configurer; }


댓글()