정상혁정상혁

코드 검사도구인 PMD 를 Eclipse plugin을 설정하고, Maven을 통해서도 같은 규칙으로 코드를 검사한 보고서를 생성하고, Hudson을 통해서 확인하는 과정을 정리해 봤습니다.

Eclipse에서는 update site를 http://pmd.sf.net/eclipse 로 지정해서 플러인을 설치합니다.

Eclipse 메뉴의 Window- Preferences를 가면 Rule설정 파일을 export, import할 수 있는 기능이 있습니다.

eclipse-pmd-rule.jpg

Project의 Properties에도 PMD관련 설정이 있습니다. 외부에서 만든 Rule파일을 바로 참조해도 되고, 여기서 설정된 것을파일로 생성할 수도 있습니다. 이미 있는 프로젝트에서 Rule를 설정할 때는 Project의 Properties에서 Rule들을고른 후에 생기는 warning이나 error를 보고 warning이 안 뜨게 소스를 고거나 Rule을 제외한 후, 최종결정Rule들로 Ruleset 정의 파일을 생성하는 것이 편리할 것입니다. 저는 처음에 모든 Rule을 다 선택한 다음에warning들을 없애가면서 Rule들을 하나하나 검토해 나갔었습니다.

eclipse-pmd-project.jpg

위의 화면에서 Enable PMD를 선택하면 .project파일에 아래와 같은 부분이 추가될 것입니다.

<natures>
      ....
   <nature>net.sourceforge.pmd.eclipse.plugin.pmdNature</nature>
</natures>

이제 해당 프로젝트는 Eclipse의 Problems View에서 PMD에서 보고하는 error, warning까지 모두 다 뜨게됩니다. 개발을 하면서 Rule 준수에 대한 빠른 피드백을 얻기 위해 되도록 이 기능을 사용하는 것이 좋습니다. 대신 이미Project에 warning이 많으면 새로운 warning들이 잘 눈에 띄지 않게 되므로, Project를 warning없이깨끗히 정리한 다음에 사용할 것을 권장해 드립니다.

모든 Rule을 다 적용하면 아마 굉장히 많은 warning과 때로는 에러까지도 나올 것입니다. 그런 것들을 다 검토해서 포함시킬지를 결정해야 합니다. http://pmd.sourceforge.net/rule-guidelines.html를 참조하셔서, 프로젝트 상황에 맞게 적용해야겠죠. 그중 몇가지 Rule에 대해서만 언급을 하고 넘어가겠습니다.

  • Basic Rules-EmptryInitializer: PMD 5.0에서 추가된 룰로 Maven의 PMD plugin버전 2.4에서는 PMD 버전 4.2.2를 참조하기 때문에 이Rule은 지원되지 않습니다. 따라서 PMD의 Eclipse plugin에서 이를 지원한다고 할지라도 Maven plugin과같이 쓰기 위해서는 이 Rule을 반드시 제외해야 합니다.

  • Optimization Rules-LocalVariableCouldBeFinal과 Controversial Rules-AvoidFinalLocalVariable : 서로 상반되는 Rule로 한쪽 Rule을피하면 다른 쪽에 걸려드는 Rule입니다. 그래서 warning을 안보려면 둘 중에 하나는 꼭 제외해야 합니다. 그런데,final을 Local variable에 일일히 선언하는 것도 번거로운 일이고, 메소드 내의 inner class에서 참조해야되어서 꼭 final이 되어야하는 local variable도 있으므로, 둘 다 제외하는 것도 좋습니다

  • Controversial Rules-OnlyOneReturnRule : 메서드에서 return문이 여러 개일 경우 경고를 주는데, 메서드 중간의 return문은 복잡한 조건문의 구조를 단순하게 하는데 도움이 경우가 많고, 켄트벡의 구현패턴 7장 중 '보호절’을 보면 이를 권장하고 있습니다.

  • Design Rules-UnnecessaryLocalBeforeReturn: return 전에 따로 local 변수로 반환할 값을 선언할 때 주는 경고인데, 기능적으로는 별 의미 없는 코드이나,return 문장에는 @SupressWarning 의 Annotation을 추가할 수 없기 때문에, Annotation 적용범위를 최소화하기 위해 그런 선언이 필요한 때도 있습니다. (Java Language Spec 9.7, EffectiveJava 2nd Edition Item 24 참조)

검토해보니 가장 부담없이 적용가능한 RuleSet이 괄호에 대한 규칙을 정의하는 Braces Rules이고, Controversial Rules가 이름 그래도 가장 제외할 것이 많은 Rule Set입니다.

이런 과정을 거쳐서 선별된 Rule 정의 파일이 만들어지면 그것을 Maven의 PMD plugin에서도 참조할 수 있게 설정합니다. 저는 Rule설정 파일이름을 .ruleset으로 하고 pom.xml에 추가했습니다.

  <reporting>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-pmd-plugin</artifactId>
        <version>2.4</version>
        <configuration>
          <rulesets>
            <ruleset>$\{basedir}/.ruleset</ruleset>
          </rulesets>
          <sourceEncoding>utf-8</sourceEncoding>
          <targetJdk>1.6</targetJdk>
          <minimumTokens>10</minimumTokens>
        </configuration>
      </plugin>
  </reporting>

그리고는 mvn site 혹은 mvn pmd:pmd pmd:cpd 처럼 PMD plugin의 goal을 포함시킨 빌드를 한번 실행시켜 봅니다.

문제가 없이 돌아갔으면 hudson에도 PMD플러그인을 설정합니다.. PMD 플러그인의 goal이 포함된 빌드를 돌리고 나면PMD warning라는 링크가 해당 프로젝트에 생기고, 거기서 아래와 같은 보고서를 확인할 수 있습니다.

hudson-pmd-report.jpg

정상혁정상혁
수정이력
  • 2019.04.13

    • 유효하지 않은 JavaDoc 문서 링크 최신화

    • 예제에서 Diamond 문법 활용

    • JUnit4에 대한 일반적인 설명 링크 제거

    • `ServletRequest.getParameterMap()`에 대한 최신 JavaDoc 내용 추가

    • `List<ScheduledFuture<?>>`가 쓰인 예시를 Spring Integration의 소스에서 LogBack의 것으로 변경

Generics가 들어간 테스트 코드를 통과시켜 봅시다.

아래에 있는 테스트 1~5까지의 테스트 코드들을 모두 한번에 통과시키는 ListUtils.max메서드는 어떻게 선언하고 구현해야 할까요?

Generics를 써보신 분이라면 ListUtilsTest.java를 다운 받으셔서 한번 풀어 보시기 바랍니다. Collections.max()를 아시는 분도 그 메소드를 참고하시지 마시고 직접 메서드를 만들어 보시면 재미있으실 겁니다.

제약조건은 다음과 같습니다.

  • @SuppressWarnings("unchecked") 를 쓰지 않고도 Generics에 대한 warning이 없고, Casting도 한번도 하지 않아야 하고

  • 컴파일 시점에서 ListUtils.max 메소드에 Comparable 인터페이스를 구현한 객체들을 쌓은 List가 넘어온다는 것을 검증할 수 있어야 한다.

테스트 1 : 빈 리스트가 넘어오면 null값 반환
    @Test
    public void getNullIfEmptyList(){
        List<Integer> numbers = new ArrayList<>();
        Integer max = ListUtils.max(numbers);
        assertThat(max,is(nullValue()));
    }
테스트 2 : Integer객체의 최대값 구하기
    @Test
    public void getMaxInteger(){
        List<Integer> numbers = new ArrayList<>();
        numbers.add(Integer.valueOf(1))
        numbers.add(Integer.valueOf(2));
        Integer max = ListUtils.max(numbers);
        assertThat(max,is(Integer.valueOf(2)));
    }
테스트 3 : BigInteger객체의 최대값 구하기
    @Test
    public void getMaxBigInteger(){
        List<BigInteger> numbers = new ArrayList<>();
        numbers.add(BigInteger.ZERO);
        numbers.add(BigInteger.ONE);
	BigInteger max = ListUtils.max(numbers);
        assertThat(max,is(BigInteger.ONE));
    }
테스트 4: java.sql.Date 객체의 최대값 구하기
    @Test
    public void getMaxDate(){
        java.sql.Date now = new java.sql.Date(new Date().getTime());
        java.sql.Date afterAWhile = new java.sql.Date(new Date().getTime()+6000);
        List<java.sql.Date> dates = new ArrayList<java.sql.Date>();
        dates.add(now);
        dates.add(afterAWhile);
        java.sql.Date max = ListUtils.max(dates);
        assertThat(max,is(afterAWhile));
    }
테스트5 : ScheduledFuture를 구현한 객체의 최대값 구하기
    @Test
    public void getMaxScheduledFuture() throws InterruptedException{
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        ScheduledFuture<?> after1Second = executor.schedule(getTask("first"),1,TimeUnit.SECONDS);
        ScheduledFuture<?> after2Seconds = executor.schedule(getTask("second"),2,TimeUnit.SECONDS);
        List<ScheduledFuture<?>> futures = new ArrayList<ScheduledFuture<?>>();
        futures.add(after1Second);
        futures.add(after2Seconds);
        ScheduledFuture<?> max = ListUtils.max(futures);
        long maxDelay = max.getDelay(TimeUnit.SECONDS);
        assertThat(maxDelay,is(after2Seconds.getDelay(TimeUnit.SECONDS)));
        Thread.sleep(3000);
        assertTrue(max.isDone());
    }
    private Runnable getTask(final String message) {
        Runnable task = new Runnable(){
            public void run() {
                System.out.println(message);
            }
        };
        return task;
    }

풀이와 설명

테스트 1,2,3번 까지의 코드만이라면 아래와 같이 선언하셔도 됩니다.

리스트 1: 간단한 max 메서드 선언
public static <T extends Comparable<T>> T max(List<T> list)

이렇게 <T extends Comparable<T>> 처럼 Type parameter가 그 자신이 포함된 표현으로 그 범위가 선언되는 것을 recursive type bound라고 합니다.

Integer와 BigDecimal의 클래스 선언을 보면 아래와 같습니다.

리스트 2: Integer 와 BigInteger 클래스 선언부
public final class Integer extends Number implements Comparable<Integer>

public class BigInteger extends Number implements Comparable<BigInteger>

두 클래스 모두 자신의 타입이 Parameterized type으로 들어간 Comparable 인터페이스를 구현하고 있기 때문에 리스트3의 메소드 선언으로도 Integer나 BigInteger가 담긴 리스트를 받을 수 있습니다.

그러나 리스트 1의 선언으로는 테스트4,5에 있는 메서드에서 컴파일 에러가 날 것입니다. 그 이유는 다음과 같습니다.

테스트4의 java.sql.Datejava.util.Date를 상속한 클래스입니다. 그런데 java.sql.Date 는 따로 comparesTo 메서드를 재정의하고 있지 않고, 상위클래스인 java.util.Date 에 있는 메서드를 그대로 쓰고 있습니다. java.sql.DateComparable<java.sql.Date> 한 것이 아닌 Comparable<java.util.Date> 를 구현한 것이라고 볼 수 있습니다. (두 클래스의 이름이 같아서 혼동이 되실 수도 있습니다. Java Puzzler에서는 이 두 클래스의 예를 들면서 자바 플랫폼 설계자가 이름을 지으면서 깜빡 존 듯하다고 언급하고 있습니다.)

그리고 테스트5의 java.util.concurrent.ScheduledFuture 인터페이스는 Comparable<ScheduledFuture> 를 구현한 것이 아닌, Delayed라는 인터페이스를 상속한 것이고, 이 Delayed는 Comparable<Delayed> 를 상속한 인터페이스입니다. 리스트 3의 인터페이스 선언을 보시면 쉽게 이해가 되실 것입니다.

리스트 3: Delayed 인터페이스 선언부
public interface ScheduledFuture<V> extends Delayed, Future<V>

public interface Delayed extends Comparable<Delayed>

이런 경우도 모두 통과할 수 있게 ListUtils.max() 메서드를 선언하고 구현하면 아래와 같습니다.

리스트4: ListUtils 구현
public class ListUtils {
    public static <T extends Comparable<? super T>> T max(List<T> list){
        T result = null;
        for(T each : list) {
            if (result==null) result = each;
            if(each.compareTo(result)>0) result = each;
        }
        return result;
    }
}

public static <T extends Comparable<? super T>> T max(List<T> list) 라는 긴 메서드 선언입니다. 이 선언 안에는 recursive type bound, wild card, upper bound, lower bound가 다 들어가 있습니다. 이 정도 메서드를 설계할 수 있어야지, Java generics를 제대로 아는 것이라고 할 수 있겠습니다.

bounded wild card를 적용하는 기준은 Effective Java 2nd Edition에 나와 있는 PECS(Producer-extends, Consumer-super)원칙을 기억하시면 도움이 됩니다. 원래 PECS의 뜻은 가슴 근육이라는군요.

<T extends Comparable…​.

Comparable인터페이스를 구현한 클래스가 그 대상이어야 max내부에서 Comparable.compareTo를 이용해서 최대값을 구할 수 있습니다. 그래서 타입 T는 T extends Comparable이 되어야 합니다. PECS원칙으로도 리턴값으로 생산되는 (Producer) 타입이 T이므로 extends를 쉽게 연상하실 수 있습니다.

Comparable<? super T> 부분

max 메서드 내부에서 타입 T는 Comparable.compareTo(T o)메서드 뒤에 파라미터로 넘어가는, 소비되는(Consumer) 대상으로 쓰이기에 PECS원칙으로 super로 연결시킬 수 있습니다. 테스트5의 코드를 예로 보면, ScheduledFuture는 ScheduledFuture의 상위 인터페이스인 Delayed가 Comparable의 Parameterized type으로 넘어가는 Comparable<Delayed>형태의 Comparable인터페이스를 상속하고 있습니다. T를 ScheduledFuture로 봤을 때 Comparable<? super T>는 Comparable<Delayed>와 잘 맞아떨어집니다.

이 리스트4의 ListUtils.max 메서드는 Effective Java 2nd Edition의 Item28에 나오는 코드를 보고서 약간 변경을 해 본 것입니다. 원래 책에 나오는 코드는 아래와 같습니다.

리스트5: Effective Java 2nd Edition에 있는 max메서드
public static <T extends Comparable<? super T>> T max(List<? extends T> list){
  Iterator<? extends T> i = list.iterator();
  T result = i.next();
  while(i.hasNext()){
            T t = i.next();
            if (t.compareTo(result)>0) result = t;
   }
   return result;
}

메서드 선언이 public static <T extends Comparable<? super T>> T max(List<? extends T> list) 로 예제보다 더 늘어난 부분이 있습니다. 끝에 있는 List<? extends T> 가 추가된 것입니다. 이 부분은 PECS원칙에 따르면 List객체로부터 T를 생산(Producer)해 오기 때문에 ? extends T 로 하는 것이 적절해 보이는 합니다. 그러나 테스트1~5의 코드에서는 List<T> 만으로도 컴파일러가 수행하는 형추론(type inference)에 문제가 없었기에 제가 만든 코드인 리스트4에는 추가하지는 않았습니다. 컴파일러가 수행하는 Type inference는 굉장히 복잡하고, Java Language Spec에서 16페이지나 차지한다고 합니다.

그리고 리스트5에서는 길이가 0인 List가 넘어간 값일 때는 첫번째 i.next();에서 NoSuchElementException을 내게 되어 있습니다.

java.util.Collections.max 메서드에서도 같은 결과가 나오는 것으로 보아서, 유사한 구현방식이 쓰인 것으로 추측됩니다. 제가 만든 문제에서는 Collection.max와 약간 다른 부분을 만들어 보고 싶어서 길이가 0일 list가 올 때는 null을 반환하는 방식으로 바꾸어 보았습니다.

그렇다면 `java.util.Collections.max`의 메서드 시그니처는 어떻게 되어 있을까요?

리스트6: java.util.Collections.max 메서드
static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

일단 대상이 List보다 확장된 Collection이니 Parameter가 Collection인 것이 눈에 들어 옵니다. 그런데 T의 제약조건이 <T extends Object & Comparable<? super T>>로 선언되어 있는 것이 리스트5의 코드보다 'Object &' 부분이 더 들어가 있습니다.

이것은 java1.4와의 하위 호환성을 위한 것입니다. Java에서는 하위호환성 지원 때문에 컴파일 시에 Generics관련 정보를 모두 검사한 후에는 실제로는 Generics 정보가 전혀 없는 바이트코드를 생성하게 되어 있습니다. 그래서 리스트5처럼 메서드를 선언했을 때에는 런타임시에는 리스트 7과 같은 코드와 같은 바이트코드가 생성됩니다.

리스트 7: 리스트5의 메서드 선언이 자료형 지우기가 수행된 뒤의 모습
static Comparable max(Collection c)

그러나 이전 버전에서의 max메서드의 모습은 다음과 같았습니다.

리스트8: Java5 이전의 Collections.max 메서드
public static Object max(Collection c)

따라서 리스트7처럼 Comparable을 반환하게 된다면 이것은 이전버전의 메서드 Signature를 바꿔버린 것이 되므로 하위버전에서 컴파일된 코드에서 Collections.max를 호출할 때 에러를 발생시키게 됩니다. 그래서 Object &이 더 추가된 것이죠. (Agile Java의 Lesson 12 중 Additional Bounds에서 참조)

여기까지 이해하셨으면, 실무에서 어떤 Generics 관련 코드를 봐도 이제 쉬워보이실 겁니다.

Generics의 표현력

하나의 예제로 Generics의 많은 부분을 설명하기 위해서 다소 복잡한 코드를 보여드렸습니다. 혹시나 Generics를 이제 막 적용하시고 싶으신 분들의 마음을 어둡게 한 것이 아닌가 걱정이 되기도합니다. 그러나 대부분의 Generics적용 사례는 훨씬 간단하고, 특히 Collection 선언에 genercics를 활용하는 정도는 어렵지 않습니다.

Generics는 컴파일시점에서의 에러 검출 영역을 넓혀줘서 보다 이른 시점에 버그를 잡을 수 있게 해주고, 코드I의 설명력을 높여줘서 API사용자들이 보다 쉽게 API를 쓸 수 있게 해줍니다. 컴파일타입의 에러체크 능력은 위의 예제를 통해서 설명했으니, 표현력에 대해서도 제가 겪은 사례를 이야기해 볼까 합니다.

오래 전에 저는 Java 인터페이스를 엑셀파일로 만드는 산출물 작업을 했었는데, 리턴타입이 List인 메서드들은 그 안에 어떤 객체들이 들어있는지 메서드 시그니처만으로는 표현할 수 없어서 답답했던 적이 있었습니다. 그래서 아예 List대신 배열을 쓸까도 고민하다가 List가 가진 편의성들을 버릴 수가 없어서 List를 쓰고 따로 문서에 그 안에 어떤 객체가 들어가 있는지를 적을 수 밖에 없었습니다.

javax.servlet.ServletRequest.getParameterMap()를 사용할 때는 API사용자로서 아쉬움을 느꼈었습니다. API문서를 보면 이 메서드가 반환하는 Map에는 key로 String이, value로 String배열이 들어가 있는 것으로 설명되어 있습니다.

request getParametersMap

저는 처음에 이 문서를 안 보고 key가 String, 값이 하나일 때는 그냥 String, 값이 2개 이상이면 String배열이 들어가 있지 않을까하는 추측을 바탕으로 한 코드를 짜서 몇번 에러를 냈었었습니다. 결국 API문서를 보고서 어떤 형식으로 자료가 들어가 있는지 알게 되어있습니다. 이 메소드의 리턴타입이 Map<String,String[]> 과 같이 선언되어 있었다면, 문서를 안 보고도, Runtime 에러를 안 겪고도 바로 올바른 자료형으로 사용이 가능했을 것입니다. 이렇듯 Generics를 잘 활용할 수 없는 API를 쓸 때에는 소비자 입장에서 불편합니다.

나중에 저는 이 메서드를 호출하는 부분을 아래와 같이 감싸는 부분을 넣었습니다.

리스트9: javax.servlet.ServletRequest.getParameterMap() 메서드를 Generics를 이용한 코드로 감싸기
@SuppressWarnings("unchecked")
Map<String,String[]> requestMap = request.getParameterMap();

@SuppressWarnings("unchecked") 은 어쩔 수 없는 경우에만 써야 하고, 쓸 때도 클래스 단위, 메서드 단위가 아닌 이런 최소 라인 단위로 써야 합니다. Effective Java 2nd Edition Item 24참조) 이 경우는 Generics지원하지 않는 외부 인터페이스를 호출하는 것이라서 불가피한 경우입니다. 형에 대해서는 API 문서에 명시된 내용라서 이렇게 @SuppressWarnings을 선언해도 문제가 없습니다. 필요에 따라서 이 requestMap을 리턴해 준다면 그것을 쓰는 코드에서는 더 이상 이 안에 무엇이 들어있는지 문서를 찾아보지 않아도 됩니다.

JavaEE6 버전의 JavaDoc을 보니 이제는 request.getParamersMap`의 반환형이 `Map<String,String[]> 으로 바뀌어 있음을 확인할 수 있습니다.

request getParametersMap ee6

관련자료 모음

Generics 관련 자료

이 포스트는 주로 아래 두 책을 보면서 얻은 정보를 통해 작성되었습니다.

Agile java처럼 테스트 코드를 먼저 보여주었고 , ListUtils.max 메서드는 Effective Java의 내용을 주로 참조해서 작성했습니다. Effective Java에서는 ScheduledFuture의 경우에 대해서 언급만 되어 있고 예제코드가 없는 것이 아쉬워서 테스트5의 코드를 작성했고, 비슷한 사례의 보다 친숙한 클래스를 찾다가 Agile java에서 java.sql.Date 클래스가 예제에 많이 쓰인 것을 보고 테스트4를 추가했습니다.

Effective Java 2nd Edition 에 포함된 내용 중 Java5 관련 내용은 Joshua Bloch이 했던 발표에 잘 요약되어 있습니다.

그외 Generics에 관한 자료들의 링크는 아래와 같습니다.

Concurrent 관련자료

Effective Java에서 언급한 ScheduledFuture를 이용한 예제코드를 만들다 보니 Concurrent관련 API들이 몇개 포함되었습니다. 그 API들에 관심이 있으신 분은 아래 자료를 참조하시면 됩니다.

List<ScheduledFuture<?>> 의 코드가 실전에서 쓰인 것이 없을까해서 찾아보니 로깅 프레임워크인 Logback의 소스 코드에서 그런 코드가 발견되었습니다.

정상혁정상혁

역시 최근 들어 스프링소스의 발걸음이 더욱 빨라졌습니다. Grails, Spring Python, tc Server, 3.0M1 발표.. 현란하게 소식이 쏟아지는군요. 그만큼 공부할 것도 더 늘겠지만, 그래도 반갑고 기대감을 가지게 하는 소식들입니다.

대규모 투자를 받고, 일을 더 벌이고 하면서 스프링소스가 돈맛에 초심을 잃을까봐 걱정을 하시는 분도 많이 계시는 듯 합니다. 그래도 성공적인 사업 구조가 정착되어서 그 수익이 개발자들에게 도움이 되는 제품들을 더 잘 개발하는데 쓰인다면야 굳이 돈을 많이 벌게 되는 것이 나쁘다고는 생각하지 않습니다. 유료로 제공되는 제품이 많아진다고 해도, 그럴만한 가치가 있는 제품이면 돈을 내고, 그것이 아니라도 프레임웍이나 서버, 개발도구의 시장은 독점 시장이 아니니 다른 선택을 할 수 있는 대안이 많을 것입니다. 물론 지금까지의 스프링의 행보를 봐서는 충분히 앞으로도 가치있는 결과물을 만들 것이라고 기대하고 있습니다.

여러 소식들 중에 솔직히 가장 재미있었던 것은, Spring one America 2008에 참석하신 분들을 통해 알게된, 로드존슨의 나이였습니다. 스프링 핵심 개발자인 유겐할러의 말에 의하면 로드존슨은 38살 정도라고 하네요.

왜 저는 로드존슨을 당연히 40대라고 생각했을까.. 하고 분석해보니, Expert one-on-one J2EE Design and Developement 책 때문에 그런 선입관을 가지게 된 것 같습니다.

이 책이 나온 것이 2003년인데, 그렇다면 이 때는 33살이였단 말이되는군요. 이 책을 쓰는데도 2년 정도가 걸렸다고 들은 것 같습니다. 그리고 책 표지에 보면 로드존슨은 음악학(Musicology)에 박사학위가 있다는 말도 있습니다. 그래서, 다른 분야도 박사까지 하고 컴퓨터 분야에서도 이정도 경지까지 가려면 적어도 이 책 출판시에 30대 중후반은 되지 않았을까.. 하는 추측을 한 것이죠.