먼저 제네릭 타입에 대해 알고 넘어가자

제네릭 타입(generic type)이란?

제네릭 클래스와 제네릭 인터페이스를 통틀어 일컫는 말
클래스에서 사용할 타입을 클래스 외부에서 설정하는 것
-> class 클래스명 <타입 매개변수> { ... }

이때 타입 매개변수에는 제네릭 타입을 사용시 받아올 객체에 대한 파리미터를 대표

클래스와 인터페이스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스 또는 제네릭 인터페이스라 한다.

ex) List <String>의 경우
      List <String>은 원소 타입이 String인 리스트를 뜻하는 매개변수화 타입인데,
      String이 정규(formal) 타입 매개변수 E에 해당하는 실제(actual) 타입 매개변수이다. 



또한, 제네릭 타입을 하나 정의하면 그에 딸린 로 타입(raw type)도 함께 정의된다.

 

로 타입(raw type)이란?


로 타입: 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 의미

ex) List <E>의 로 타입은 List이고, List <String>의 로 타입 또한 List가 된다.

 

로 타입을 사용하는 이유)

로 타입은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭이 등장하기 전 코드와 호환되도록 하기 위한 궁여지책 

 

 

제네릭 도입 이전

제네릭을 도입하기 이전에는 컬렉션을 다음과 같이 선언 (자바 9에서도 여전히 동작하지만 좋은 예는 아님)

//Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;

//실수로 동전을 넣는다. 
stamps.add(new Coin(...)); // "unchecked call" 경고를 내뱉는다.

위의 코드를 사용시 실수로 도장(Stamp)대신 동전(Coin)을 넣어도 아무 오류 없이 컴파일되고 실행된다. 

 

로 타입을 사용했을 때의 문제

컴파일 오류를 발생시키지 않으므로 실행중에 오류(Runtime Exception)가 발생할 수 있다.

 

아래 코드처럼 add한 Coin 객체를 꺼내서 Stamp 변수에 할당하는 순간 ClassCastException이 발생한다.

즉, 컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못한다.

for(Iterator i = stamps.iterator(); i.hasNext();) {
	Stamp stamp = (Stamp) i.next(); // ClassCastException을 던진다. 
    stamp.cancel(); 
}

 

위의 코드들의 문제점)

  • ClassCastException이 발생하면 stamps에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있다.
  • 주석( //Stamp  인스턴스만 취급한다.)  은 컴파일러가 이해하지 못하니 별 도움이 되지 못한다.
오류는 가능한 한 발생한 즉시, 이상적으로는 컴파일 할때 발견하는 것이 좋다. 

 

 

제네릭 도입 이후

 

아래 코드와 같이 제네릭을 도입하면, Stamps 인스턴스만 취급한다는 정보가 주석이 아닌 타입 선언 자체에 녹아든다.

private final Collection<Stamp> stamps = ...;

이렇게 선언하면

  • 컴파일러는 stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지한다.
  • stamps에 엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생하며 무엇이 잘못됐는지를 정확히 알려준다.

 

Test.java:9: error: imcompatible types: Coin cannot be converted to Stamp 
stamps.add(new Coin()); 
				^

컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하여 절대 실패하지 않음을 보장한다.

 

로 타입을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 된다.

-> 왜냐하면, 로 타입을 쓰면 제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.

 

그럼에도 로 타입을 만들어 놓은 이유는 무엇일까?

 

 

로 타입을 쓰는 이유


  • 호환성 문제
자바가 제네릭을 받아들이기까지 거의 10년간 제네릭 없이 짠 코드가 이미 세상을 뒤덮어 버림
                                                                          ↓
기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야 했음
                                                                          ↓
마이그레이션 호환성을 위해서 로 타입을 지원하고 제네릭 구현하는 소거 방식을 사용하기로 함

 

 

 

List 같은 로 타입은 사용해서는 안되나 List<Object> 처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다. 

  • List는 제네릭 타입에서 완전히 발을 뺀 것이고, List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에 전달한 것

List를 받는 메서드에 List<String>은 넘길 수 있지만, List<Object>를 받는 메서드에는 넘길 수 없다.

  • 제네릭의 하위 타입 규칙 때문으로 List<String>은 List의 하위 타입이지만, List<Object>의 하위 타입은 아님
  • List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 타입 안전성을 잃게 됨

 

 

public static void main(String[] args) {
	List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42)); 
    String s = strings.get(0); // 컴파일러가 자동으로 형변환 코드를 넣어준다. 
} 

private static void unsafeAdd(List list, OBject o) { 
	list.add(o); 
}

 

위의 코드는 컴파일은 되지만 로 타입인 List를 사용하여 아래와 같은 경고가 발생

Test.java:10: warning: [unchecked] unchecked call to add(E) as a 
member of the raw type List 
	list.add(o); 
    		^

 

이 프로그램을 이대로 실행하면 strings.get(0)의 결과를 형변환하려 할 때 ClassCastException을 던진다.

(왜냐하면 Integer를 String으로 변환하려 시도했기 때문)

 

이 경우, 컴파일러의 경고를 무시하여 그 대가를 치른 것이다.

 

 

 

이제 로 타입인 List를 매개변수화 타입인 List<Object>로 바꾼다음, 다시 컴파일 해보면 오류 메시지가 발생하면서 컴파일 조차 되지 않는다.

Test.java:5: error: incompatible types: List<String> cannot be 
converted to List<Object> 
	unsafeAdd(strings, Integer.valueOf(42)); 
    	^

 

 

한편, 원소의 타입을 몰라도 되는 로 타입을 쓰고 싶어질 수 있다.

//잘못된 예 - 모르는 타입의 원소도 받는 로 타입을 사용
static int numElementsInCommon(Set s1, Set s2) { 
	int result = 0; 
    for (Object o1 : s1) 
    	if (s2.contains(o1))
        	result++; 
    return result; 
}

위의 메서드의 문제점)

 

메서드는 동작하지만 로 타입을 사용해 안전하지 않다.

 

-> 비한정적 와일드카드 타입 (unbounded wildcard type)을 대신 사용하는 것이 좋음 

 

  • 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용할 것
  • 제네릭 타입인 Set<E>의 비한정적 와일드카드 타입은 Set<?> (어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입)



비한정적 와일드카드 타입을 사용해 numElementsInCommon을 다시 선언

static numElementsInCommon(Set<?> s1, Set<?> s2) {...}

비한정적 와일드카드 타입을 사용했을 때 장점)

  • 비한정적 와일드카드 타입은 안전하지만, 로 타입은 안전하지 않음
  • 로 타입 컬렉션은 아무 원소나 넣을 수 있어 타입 불변식을 훼손하기 쉽지만, Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없다. 만약 다른 원소를 넣으려 하면 컴파일할 때 오류를 발생시킨다.(타입 불변식을 훼손하지 못하게 막는다)
//만약 다른 원소를 넣는 경우, 컴파일시 다음과 같은 오류 메시지 발생 
WildCard.java.13: error: incompatible types: String cannot be 
converted to CAP#1 
	c.add("verboten");
    	^
 where CAP#1 is a fresh type-variable:
 	CAP#1 extends Object from capture of ?

 

 

이러한 제약을 받아들일 수 없다면 제네릭 메서드나 한정적 와일드카드 타입을 사용하면 된다.

 

 

 

와일드 카드란 무엇인가?


와일드카드: 제네릭코드에서 물음표(?)로 표기되어 있는 모든 것을 말하며, 아직 알려지지 않은 타입을 나타냄


1) 한정적 와일드카드(bounded wildcards)
●  Upper Bounded wildcards (extends를 사용한 한정적 와일드카드) :
      타입의 제한을 풀어줄 때 사용하며 제네릭 타입들을 상위의 제네릭 타입으로 묶어주는 것 (상위타입 이하로만 올 수 있음)
      <? extends 상위타입> 

ex)  <? extends D> => D, E 가능

● Lower Bounded wildcards (super를 사용한 한정적 와일드카드) :
     타입을 제한할 때 사용하며 유연성을 극대화하기 위해 지정된 타입의 상위 타입만 허용하는 것(하위타입 이상으로만 올 수 있음)
     <? super 하위타입> 

ex) <? supper D> => D, A 가능

2) 비한정적 와일드카드(unbounded wildcards)
    와일드카드 문자인 ?만 사용할 때 비한정적 와일드카드라고 하며, 알려지지 않은 타입의 리스트라고 불리며 다음과 같은 상황일      때 비한정적 와일드카드를 쓴다.
   
   1. Object 클래스에서 제공하는 메서드일 때
   2. 매개변수 타입에 의존하지 않는 제네릭 클래스의 메서드를 사용할 때

ex) <?> => 모든 클래스나 인터페이스가 올 수 있다. 즉 제한없음. A ~ E 모두 올 수 있다.
     
      List<Object>의 경우 어떤 타입도 상관없이 사용하는 것이 아니라, List의 원소로 Object타입만 받는다.
      단, Object위치에는 Object의 하위타입을 넣을 수 있지만, List<?>에는 null만 넣을 수 있다.
      왜냐하면 List<?>에 어떤 타입의 List가 올지 모르기 때문에 타입이 존재하는 값을 넣을 수 없기 때문이다.

 

 

로 타입을 사용하는 예외 케이스


  • class 리터럴에 매개변수화 타입을 사용하지 못하게 했다. (배열과 기본 타입은 허용)

    ex) List.class, String[].class, int.class는 허용하지만 List<String>.class와 List<?>.class는 허용X

 

  • instanceof 연산자를 사용하는 경우
 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.

 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.

 비한정적 와일드카드 타입의 꺽쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로 로 타입을 쓰는 편이 깔끔하다.

제네릭 타입에 instanceof를 사용하는 올바른 예
if (o instanceof Set) {          // 로 타입
	Set<?> s = (Set<?>) o;       // 와일드카드 타입
	...
}​


위의 코드에서, o의 타입이 Set임을 확인한 다음 와일드카드 타입인 Set<?>로 형변환해야 한다.
이는 검사 형변환(checked cast)이므로 컴파일러 경고가 뜨지X

 

소스 파일 하나에 톱레벨 클래스를 여러개 선언한다면?

자바 컴파일러는 아무런 문제가 없다.

"but" 아무런 득도 없고, 심각한 위험을 감수해야 한다.

 

문제


※ 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느  것을 사용하지는 어느 소스 파일을 먼저 컴파일하냐에 따라 달라지기 때문

 

 

예시)

Main 클래스 하나를 담고 있고, Main 클래스는 다른 톱레벨 클래스 2개(Utensil과 Dessert)를 참조할 때

public class Main {
	public static void main(String[] args) {
    	System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

집기(Utensil)와 디저트(Dessert) 클래스가 Utensil.java 한 파일에 정의되어 있다면?

class Utensil {
	static final String NAME = "pan";
}

class Dessert {
	static final String NAME = "cake";
}

Main을 실행할 때 pancake를 출력한다.

이 때, 우연히 똑같은 두 클래스를 담은 Dessert.java라는 파일을 만들었다면?

class Utensil {
	static final NAME = "pot";
}

class Dessert {
	static final NAME = "pie";
}
  • 운 좋게 javac Main.java Dessert.java 명령으로 컴파일한다면 컴파일 오류가 나고 Utensil과 Dessert 클래스를 중복 정의했다고 알려준다.
 1.  컴파일러는 먼저 Main.java를 컴파일하고,  Main.java파일 안에서 가장 먼저 나오는 Utensil 참조를 만나면 Utensil.java 파        일을 찾아  그안에 있는 Utensil과 Dessert를 모두 찾는다.
2. 그 후 컴파일러가 두번째 명령줄 인수인 Dessert.java 처리하려고 할 때, 같은 클래스가 있음을 알아 낸다.
3. 컴파일 오류를 발생 시킴( 클래스를 중복 정의)
  • 컴파일러에 어떤 소스를 먼저 제공하느냐에 따라 동작이 달라지는 문제가 발생한다.
javac Main.java 나 javac Main.java Utensil.java 명령으로 컴파일하여 실행하는 경우 의도한 pancake 출력

"but"  javac Dessert.java Main.java 명령으로 컴파일하면 potpie를 출력 

 

해결


1. 톱레벨 클래스를 서로 다른 소스 파일로 분리시킨다.

2. 만약 여러 톱레벨 클래스를 한 파일에 담고 싶다면, 정적 멤버 클래스로 만들어라

정적 멤버 클래스로 만들었을 때 장점)

1. 다른 클래스에 딸린 부차적인 클래스라면 정적 멤버 클래스로 만드는 쪽이 일반적으로 낫다.
2. 읽기 좋고, private으로 선언시 접근 범위도 최소로 관리가 가능하다.


위의 예시를 정적 멤버 클래스로 바꾼 경우
public class Test {
	public static void main(String[] args) {
    	System.out.println(Utensil.NAME + Dessert.NAME);
    }
    
	private static class Utensil {
    	static final String NAME = "pan";
    }
    
    private static class Dessert {
    	static final String Name = "cake";
    }
}​

■ 중첩클래스 : 다른 클래스 안에 정의된 클래스이며, 중첩 클래스의 종류는 4가지이다.

 

  • 정적 멤버 클래스
  • (비정적) 멤버 클래스
  • 익명 클래스
  • 지역 클래스

 이중, 정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)에 해당한다. (그 외의 쓰임새는 톱레벨 클래스로 만듦)

 

 

그러면 중첩 클래스는 왜 사용하는가?
  • 내부 클래스에서 외부 클래스에 손쉽게 접근 가능
  • 서로 관련있는 클래스들을 논리적으로 묶어, 코드의 캡슐화↑
  • 외부 클래스에서 내부 클래스에 접근할 수 없으므로 코드의 복잡성↓
  • 외부 클래스의 복잡한 코드를 내부 클래스로 옮겨 코드의 복잡성↓ 

 

▶ 정적 멤버 클래스

해당 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 동일

(다른 정적 멤버와 똑같은 접근 규칙을 적용)

 

흔히, 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰임 (대표적인 예로 Builder 패턴이 존재)

 

Builder 패턴의 예)

public class NutritionFacts {
  private final int servingSize;
  private final int servings;
  private final int calories;
  private final int fat;

  public static class Builder {
    // 필수 매개변수 - 외부 클래스의 pirivate 변수에 접근 가능
    private final int servingSize;
    private final int servings;

    // 선택 매개변수 - 기본값으로 초기화 & 외부 클래스의 private 변수에 접근 가능
    private int calories      = 0;
    private int fat           = 0;

    public Builder(int servingSize, int servings) {
      this.servingSize = servingSize;
      this.servings    = servings;
    }

    public Builder calories(int val) {
      calories = val;      
      return this; 
    }
    public Builder fat(int val) {
      fat = val;           
      return this; 
    }

    public NutritionFacts build() {
      return new NutritionFacts(this);
    }
  }

  public NutritionFacts(Builder builder) {
    servingSize  = builder.servingSize;
    servings     = builder.servings;
    calories     = builder.calories;
    fat          = builder.fat;
  }

  public static void main(String[] args) {
    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
      .calories(100).build(); 
      //calories(100)을 하지 않아도 생성 가능
  }
}

 

 

▶ (비정적) 멤버 클래스

구문상 차이는 단지 static의 유무 이지만, 의미상 차이는 매우 큼

멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경할 수 X

즉, 바깥 클래스의 인스턴스와 멤버 클래스의 인스턴스는 서로 암묵적으로 연결되어 있다.

 

->  멤버 클래스의 인스턴스 메소드에서 정규화된 this를 사용해 바깥 인스턴스의 메소드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

(드물게 바깥 클래스의 인스턴스.new 비정적클래스를 통해 생성하기도 하나, 이는 비용과 생성시간 측면에서 좋지 못하다.)

 

public class ClassA {
  public int a = 0;

  public void print() {
    System.out.println("pring ClassA");
    ClassB b = new ClassB();
    b.print();
  }

  public class ClassB {
    public void print() {
      //ClassA.this를 통해 ClassA에 접근이 가능.
      //외부 인스턴스의 클래스.this 형태
      System.out.println("print ClassB : " + ClassA.this.a);
    }
  }
}


public class ClassAMain {
    public static void main(String[] args) {
        ClassA a = new ClassA();
        a.print();
        System.out.println("================");
        
        ClassA.ClassB b = a.new ClassB();
        b.print();
    }
}
//결과 
//pring ClassA
//print ClassB : 0
//================
//print ClassB : 0
만약, 개념상 중첩 클래스(nested class)의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 없다면 정적 멤버 클래스로 만들어야 한다.

 

왜냐하면,
◎ 바깥 인스턴스 없이는 멤버 클래스 생성 불가
◎ 바깥 인스턴스로의 숨은 외부참조를 저장하려면 시간과 공간이 소비

이로 인해, GC가 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다.
(바깥 클래스는 더 이상 사용되지 않지만 내부 클래스의 참조로 인해 GC가 수거하지 못해서 바깥 클래스의 메모리 해제를 하지 못하는 경우가 발생)

반면, 정적 내부 클래스의 경우 바깥 클래스에 대한 참조 값을 가지고 있지 않기 때문에 메모리 누수가 발생하지 않는다. 

메모리 누수가 발생할 수 있는 문제점이 있기 때문에  내부 클래스가 독립적으로 사용된다면 정적 클래스로 선언하여 사용하는 것이 좋다. 

 

그럼에도 멤버 클래스를 사용했을 때 장점이 존재

-> 어댑터 패턴을 이용하여 바깥 클래스를 다른 클래스로 제공할 때

(어댑터 패턴을 이용하는 경우 비정적 내부 클래스는 내부 클래스가 바깥 클래스 밖에서 사용되지 X)

     ex) HashMap의 keySey() 메서드

 

 

▶ 익명 클래스

바깥 클래스의 멤버도 아니며 멤버와 달리 사용되는 시점에 선언과 동시에 인스턴스가 만들어지며 코드의 어디서나 만들 수 있다.

또한 익명 클래스가 상위 타입(자기 자신 혹은 부모)에 상속한 멤버 외에는 호출할 수가 없다.

오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조 가능하다. (정적 멤버는 객체생성 전에 메모리에 로드되기 때문) 

 

+ 또 다른 주 쓰임은 정적 팩터리 메서드를 구현할 때 사용

 

but 자바에 람다가 등장한 이후, 익명 클래스보다는 람다 사용

▶ 지역 클래스

지역 클래스는 지역변수를 선언할 수 있는 곳이라면 어디서든 선언할 수 있다.
그에 따라 유효 범위도 지역변수와 같다.

 

또한 다른 중첩 클래스들의 공통점을 하나씩 가지고 있다.

  • 멤버 클래스처럼 이름을 가질 수 있고 반복해서 사용할 수 있다.
  • 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다.
  • 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게(10줄이하)로 작성되어야 한다.

자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.

ex) InputStream, OutputStream, java.sql.Connection, ..

 

자원 닫기는 클라이언트가 놓치기 쉽기 때문에 예측할 수 없는 성능 문제로 이어지기도 한다.

이런 자원 중 상당수가 안전망으로 finalizer를 활용하고 있지만 믿을만 하지 못하다(아이템8 참고)

 

전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다.

 

try-finally

public static String firstLineOfFile(String path) throw IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}


하지만 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다!

static void copy(String src, String dst) throws IOException {
	InputStream in = new FileInputStream(src);
	try {
		OutputStream out = new FileOutputStream(dst);
		try {
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while ((n = in.read(buf)) >= 0)
			out.write(buf, 0, n);
		} finally {
			out.close();
		}
	} finally {
		in.close();
	}
}

위의 두 코드에서도 미묘한 결점이 있다.
바로 예외는 try블록과 finally 블록 모두에서 발생할 수 있다는 것이다.
(close 메서드를 호출할 때)

또한, try-finally를 중첩하는 경우 코드가 너무 지저분해져서 가독성이 떨어지게 된다.

 

해결책:  try-with-resources

try-with-resources 를 사용하려면 해당 자원이 AutoCloseable 를 구현해야 한다.

(cf. AutoCloseable : 단순히 void를 반환하는 close 메서드 하나만 정의한 인터페이스)

닫아야 하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable을 반드시 구현해야한다.

public static String firstLineOfFile(String path) throw IOException {
    try (BufferedReader br = new BufferedReader(
        	new FileReader(path))) {
        return br.readLine();
    }
}
static void copy(String src, String dst) throws IOException {
	try (InputStream in = new FileInputStream(src);
		OutputStream out = new FileOutputStream(dst)) {
		byte[] buf = new byte[BUFFER_SIZE];
		int n;
		while ((n = in.read(buf)) >= 0)
		out.write(buf, 0, n);
	}
}


이때, 실전에서는 프로그래머에게 보여줄 예외 하나만 보존되고 여러 개의 다른 예외가 숨겨질 수도 있다.
이렇게 숨겨진 예외들은 스택 추적 내역에 숨겨졌다(suppressed)는 꼬리표를 달고 출력된다.
또한, 자바 7에서 Throwable 에 추가된 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수 있다.

+ try-with-resources에서도 catch 절을 쓸 수 있다.

public static String firstLineOfFile(String path) throw IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (Exception e) {
        return defaultVal;
    }
}



 

결론

꼭 회수해야 하는 자원을 다룰 때 try-finally 말고, try-with-resources를 사용하자.
예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다.
또한 정확하고 쉽게 자원을 회수할 수 있다.

객체 소멸자 finalizer와 cleaner를 쓰지 말아야 하는 이유

  • finalizer - 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요 + 오동작, 낮은 성능, 이식성 문제의 원인
  • cleaner  - finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고 일반적으로 불필요

C++의 파괴자(destructor)와는 다른 개념이다.

자바에서 접근할 수 없게된 객체를 회수하는 역할(객체 소멸)은 GC(가비지 콜렉터)가 담당하고,
비메모리의 자원을 회수하는 역할은 try-with-resources와 try-finally를 사용

 

1. 즉시 수행된다는 보장이 없다.

객체에 접근 할 수 없게 된후 실행되기까지 얼마나 걸릴지 알 수 없다.

즉, 제때 실행되어야 하는 작업은 절대 할 수 없다.

 

finalizer와 cleaner를 얼마나 신속히 수행할지는 전적으로 GC알고리즘에 달려 있다.

(이는 GC구현마다 천차만별)

 

2. 자원회수가 제멋대로 지연된다.(수행시점 보장x)

finalizer 스레드는 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못한다.

cleaner는 자신을 수행할 스레드를 제어할 수 있긴 하나, 여전히 백그라운드에서 수행되며 GC의 통제하에 있어서 즉각 수행된다는 보장x

 

3. 수행 여부도 보장되지 않는다.

접근할 수 없는 일부 객체에 딸린 종료 작업을 수행하지 못한 채 프로그램이 중단될 수도 있다.

따라서, 상태를 영구적으로 수정하는 작업에서는 finalizer나 cleaner에 의존해서는 안된다.

(ex. 공유 자원의 영구 lock해제를 finalizer나 cleaner에 맡겨놓으면 분산 시스템 전체가 서서히 멈추게 된다)

System.gc나 System.runFinalization 메서드는 실행될 가능성을 높여줄 수는 있으나, 보장하진 않는다.

System.runFinalozersOnExit와 Runtime.runFinalizersOnExit는 실행을 보장해 주긴 하지만, 다른 스레드가 소멸대상의 객체에 접근하고 있어도 실행해 버린다.

 

4. 동작 중 발생한 예외가 무시된다.

finalizer는 동작중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.

훼손된 객체(마무리가 덜된 객체)를 다른 스레드가 사용하려 한다면 어떻게 동작할 지 예측할 수 없다.

cleaner는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지 않는다.

 

5. 심각한 성능 문제를 동반한다.

finalizer와 cleaner는 GC의 효율을 떨어뜨리기 때문에 심각한 성능 문제를 야기한다.

 

6. 보안 문제를 일으킬 수 있다.

생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다만 객체에서 하위 클래스의 finalizer가 수행될 수 있게 한다.

이 finalizer는 정적 필드에 자신의 참조를 할당해 GC가 수집하지 못하게 만든다.

-> 해법: final 클래스를 만들어 하위 클래스를 만들 수 없도록 하거나, final이 아닌 클래스인 경우  아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언

 

finalizer나 cleaner를 대신할 해결책: AutoCloseable을 구현

파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰면 close 메서드를 호출하면 된다.

 

일반적으로 예외가 발생해도 제대로 종료가 되도록 하기 위해 try-with_resources를 사용한다.

이때 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다.

즉, close메서드에서 해당 객체가 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 그 필드를 검사해 객체가 닫힌 후에 불렀다면 IllegalStateException   을 던진다.

 

try-finally : 명시적으로 자원 반납

무조건  close 메서드가 호출되도록 하기 위해서는 try-finally block을 사용한다.

public class TryFinally implements AutoCloseable {
	@Override
	public void close() throws RuntimeException {
		System.out.println("close");
	}

	public void hello() {
		System.out.println("hello");
	}
}
public class Runner {
	public static void main(String[] args) {
		try {
			TryFinally tryFinally = new TryFinally();
			tryFinally.hello(); // 리소스 사용
		} finally {
			tryFinally.close(); // 리소스를 사용하는 쪽에서 쓴 다음 반드시 close() 호출
		}
	}
}

 

try-with-resource : 암묵적으로 자원 반납(가장 이상적인 자원 반납 방법)

AutoCloseable을 구현한다면, 명시적으로 close 메서드를 호출하지 않아도 try블록이 끝날 때 자동을 close 메서드를 호출

public class TryWithResource implements AutoCloseable {
	@Override
	public void close() throws RuntimeException {
		System.out.println("close");
	}

	public void hello() {
		System.out.println("hello");
	}
}
public class Runner {
	public static void main(String[] args) {
		try (TryWithResource resource = new TryWithResource()) {
			resource.hello(); // 리소스 사용
		}
	}
}

 

finalizer와 cleaner가 사용되는 경우

1. 해당 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할

- finalizer나 cleaner가 호출된다는 보장은 없지만, 클라이언트가 하지 않은 자원회수를 늦게라도 해줄 수 있기 때문

(ex. FileInputStream, FileOutputStream, ThreadPoolExecutor, ..)

 

2. 네이티브 피어(native peer)와 연결된 객체

(네이티브 피어: 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체)

네이티브 피어는 자바 객체가 아니기 때문에 GC의 대상이 되지 않는다. 이때 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 갖고 있지 않다면, finalizer나 cleaner를 통해 늦게라도 자원회수 가능

하지만 네이티브 피어가 사용하는 자원을 즉시 회수해야 된다면 close 메서드 사용

 

cleaner는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용해야 한다.
이때 불확실성과 성능 저하에 주의해야 한다.
 
 

자바는 GC(가비지 컬렉터)가 다 쓴 객체를 알아서 회수해가기 때문에, 메모리 관리에 더 이상 신경 쓰지 않아도 된다고  생각할 수 있지만 이것은 오해다!

public class Stack{
	private Object[	 elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack(){ 
    	elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) {
    	ensureCapacity();
        elements[++size] = e;
    }
    
    public Object pop() {
		//stack의 빈 공간에 접근했을 때, 예외처리
        if(size == 0)
        	throw new EmptyStackException();
        return elements[--size];
    }
    
    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
     */
     
     private void ensureCapacity() {
     	if(elements.length == size) 
        	elements = Arrays.copyOf(elements, 2 * size + 1)
     }
}

위의 코드는 특별한 문제 없이 별의별 테스트를 수행해도 거뜬히 통과한다. 

하지만 꼭꼭 숨어있는 문제가 존재하는데, 바로 '메모리 누수'이다.

위의 Stack을 사용하는 프로그램을 오래 실행하다 보면 GC의 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

(심한 경우, 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램을 예기치 않게 종료시킴)

 

위의 코드에서 과연 메모리 누수는 어디서 일어날까?

위의 코드에서는 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 GC가 회수하지 않는다.

왜냐하면,이 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

(cf. 다 쓴 참조(obsolete reference : 앞으로 다시 쓰지 않을 참조)

 

활성 영역 -> elemets 배열의 인덱스가 size보다 작은 원소들로 구성된 것

활성 영역 밖의 참조는 모두 다 쓴 참조 즉, 메모리 누수가 일어나는 곳이다.


가비지 컬렉션 언어(ex. 자바,..)에서는 메모리 누수를 찾기가 아주 까다롭다.

그 이유는 객체 참조 하나를 살려두면 GC는 그 객체 뿐만 아니라, 그 객체가 참조하는 모든 객체를 회수하지 못하기 때문이다.

해법은 간단하다.

 

해당 참조를 다 썼을 때 null 처리(참조 해제)를 해라

 

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;

null 처리의 이점

null 처리한 참조를 실수로 사용할 때, 프로그램은 즉시 NullPointerException을 던지며 종료 -> 프로그램 오류를 조기에 발견

 

Stack 클래스는 왜 메모리 누수에 취약할까?

스택이 자기 메모리를 직접 관리하기 때문

스택은 객체 자체가 아닌 객체 참조를 담는 elements 배열로 저장소 풀을 만들어 원소를 관리하는데, 배열에 활성 영역에 속한 원소들이 사용되고 비활성 영역은 사용되지 않는다.

 

문제: GC는 활성 영역과 비활성 영역을 알 수 없다

.( 비활성 영역이 쓸모 없다는 것은 프로그래머만 안다. GC는 영역의 구분 없이 똑같이 유효한 객체 취급)

-> 그래서 프로그래머는 비활성 영역이 되는 순간 null처리해서 해당 객체가 더 이상 안쓰이는 것을 GC에게 알려야 한다.

 

null 처리는 언제 해야 할까?

객체 참조를 null 처리하는 일은 예외적인 경우에만 해당
모든 객체를 쓰자마자 null 처리를 한다면 프로그램을 지저분하게 만든다.

 

다 쓴 참조를 해제하는 가장 좋은 방법: 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것

 

Integer pop() {

	Integer age = 27;

	...

	age = null; // 굳이 할 필요x
}

Integer age 의 scope는 pop()  안에서만 유효하다. 해당 scope를 벗어난다면 무의미한 레퍼런스 변수가 되므로 GC에 의해 수거된다.

(굳이 null 처리를 할 필요x)

 

캐시 역시 메모리 누수의 주범

객체 참조를 캐시에 넣고 난 후, 까먹고 그 객체를 놔두는 경우 메모리 누수가 일어난다.

 

해법: 1. WeekHashMap 사용(key 값을 참조하는 동안만 엔트리가 살아있도록 한다. 다 쓴 엔트리는 그 즉시 자동으로 제거)

        2. 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식 사용 (쓰지 않는 엔트리를 청소)

 ->  (ThreadPoolExecutor와 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로           LinkedHashMap의 removeEldestEntry 메서드 사용

 

 + 더 복잡한 캐시를 만들 때, java.lang.ref 패키지를 직접 활용

 

리스너 혹은 콜백 또한 메모리 누수의 주범

클라이언트가 콜백을 등록만 하고 명확히 해지를 안하고 조치를 취하지 않는 한, 콜백은 계속 쌓이게 된다.

-> 콜백을 약한 참조로 저장하면 GC가 즉시 수거해간다.

 

핵심정리

메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다.
이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야 발견되기도 한다.
그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.
 

똑같은 기능의 객체 매번 생성하기보다는 객체 하나를 재사용하는 것이 낫다.

 

String s = new String("string");

 

이 문장은 실행 될 때마다 String 인스턴스를 새로 생성 - "string" 자체가 생성자로 만들어내려는 String과 기능적으로 동일

-> 계속 호출되는 메서드 안에 존재할 경우 쓸데없는 String 인스턴스가 계속 생성될 수 있다.

 

개선된 방법

String s = "string";

이 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용

-> 가상 머신 안에서 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

 

+ 생성자 대신 정적 팩터리 메서드(아이템 1)를 사용하는 것도 불필요한 객체 생성을 피하는 하나의 방법

ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드 사용

 

 

 

만약, 생성 비용이 아주 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하는 것이 좋다.

static boolean isRomanNumeralSlow(String s) { 
	return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" 
    		+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}​

위의 코드는 String.matches 메서드를 반복하는 가장 쉬운 방법  "but" 성능이 중요한 상황에서는 적합하지 x
- 이 메서드가 내부에서 만드는 정규 표현식용 Pattern인스턴스는 한 번 쓰고 버려저서 곧바로 가비지 컬렉션 대상이 된다.
     (정규 표현식을 표현하는 Pattern 클래스는 생성비용이 높은 클래스중 하나)

해결 방법 : Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱을 한다. & 나중에 isRomanNumeralFast 메서드가 호출 될 때마다 이 인스턴스를 재사용

static final을 이용해 초기에 캐싱 
public class RomanNumerals { 
	private static final Pattern ROMAN = Pattern.compile( 
    		"^(?=.)M*(C[MD]|D?C{0,3})" 
            + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); 
    
    static boolean isRomanNumeralFast(String s) {
    return ROMAN.matcher(s).matches(); 
    } 
}

불변 객체의 경우 재사용해도 안전하다. 하지만 어댑터의 경우는 해당이 되지 않는다. 어댑터는 인터페이스를 통해 뒤에 있는 객체로 연결해주는 view라 여러 개 만들 필요x

(어댑터는 실제 작업은 뒷단 객체에 위임하고 자신은 제2의 인터페이스 역할을 해주는 객체)

 

Map<String, Object> map = new HashMap<>();
map.put("Effective", "Java");

Set<String> set1 = map.keySet();
Set<String> set2 = map.keySet();

assertThat(set1).isSameAs(set2); // TRUE

set1.remove("Effective");
System.out.println(set1.size()); // 0을 출력
System.out.println(set2.size()); // 0을 출력

Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set 인터페이스의 뷰를 반환
"but" 동일한 Map에서 호출하는 keySet 메서드는 같은 Map을 대변하기 때문에 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다.
따라서, keySet이 뷰 객체 여러 개를 만들 필요도 없고 이득도 없게 된다.

 

박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱을 하지 말아야 한다.

 

private stating long sum(){
	Long sum = 0L; // 오토박싱
    for(long i = 0; i <= Integer.MAX_VALUE; i++)
    	sum += i;
        
    return sum;
}

위 코드는 sum을 오토박싱 함으로써 불필요한 객체를 Integer.MAX_VALUE만큼 생성하게 된다. 

따라서 불필요한 Long 인스턴스를 만들지 않으려면, long타입을 선언하면 된다.

+ 아주 무거운 객체가 아닌 단순한 객체 생성을 피하고자 객체 풀을 만들지 않는 것이 좋다.

-> 왜냐하면, 요즘 JVM의 가비지 컬렉터는 상당히 최적화( 가벼운 객체용을 다룰 때 직접 만든 객체 풀보다 훨씬 빠르다.)

(아주 무거운 객체를 만드는 경우: 데이터베이스 연결의 경우)

자연어(Nautral Language)란?

인간이 일상에서 사용하는 언어를 의미

자연어 처리(Natural Laguage Processing): 인공지능의 한 분야로 머신러닝을 사용해 기계가 자연어를 이해하고 해석하여 처리할 수 있도록 하는 일

자연어 처리 vs 텍스트 분석

자연어 처리는 기계가 인간의 언어를 해석하는데 중점이 두어져 있다면, 텍스트 분석은 텍스트에서 의미 있는 정보를 추출하여 인사이트를 얻는데 더 중점을 둔다.

“but” 머신러닝이 보편화됨에 따라 자연어 처리와 텍스트 분석을 구분하는 경계가 없어짐

NLP가 활용되는 분야

  1. 텍스트 분류(Text Classification): 텍스트가 특정 분류, 카테고리에 속하는 것을 예측하는 기법을 통칭합니다. 스팸 메일 분류나 뉴스 기사의 내용을 기반으로 연애/정치/사회/문화 중 어떤 카테고리에 속하는지 자동으로 분류해주는 프로그램이 이에 속합니다. 텍스트 분류는 지도학습입니다.
  2. 감성 분석(Sentiment Analysis): 텍스트에 나타나는 감정/기분 등의 주관적 요소를 분석하는 기법을 통칭합니다. SNS의 글을 분석하여 글쓴이의 감정을 분석하는 것, 영화 및 제품의 리뷰를 분석하는 것 등이 이에 속합니다. 지도학습 뿐만 아니라 비지도학습을 이용할 수도 있습니다.
  3. 텍스트 요약(Summarization): 텍스트에서 중요한 주제를 추출하여 요약하는 기법을 의미합니다. 토픽 모델링(Topic Modeling)이 이에 속합니다.
  4. 텍스트 군집화(Clustering)와 유사도 측정: 비슷한 유형의 텍스트에 대해 군집화하는 기법을 뜻합니다.
  5. 기계 번역(Translation): 구글 번역기나 파파고와 같은 번역기에도 활용됩니다.
  6. 대화 시스템 및 자동 질의 응답 시스템: 애플의 시리나 삼성 갤럭시의 빅스비, 챗봇 등이 이에 속합니다.

NLP 처리 프로세스

1. 텍스트 전처리(Text Preprocessing): 대/소문자 변경, 특수문자 삭제, 이모티콘 삭제 등의 전처리 작업, 단어(Word) 토큰화 작업, 불용어(Stop word) 제거 작업, 어근 추출(Stemming/Lemmatization) 등의 텍스트 정규화 작업을 수행하는 것이 텍스트 전처리 단계에 속합니다.

2. 피처 벡터화 (Feature Vectorization): 전처리된 텍스트에서 피처를 추출하고 여기에 벡터 값을 할당합니다. 대표적인 피처 벡터화 기법은 BOW(Bag of words)와 Word2Vec이 있습니다.

3. 머신러닝 모델링: 피처 벡터화된 데이터에 대하여 모델을 수립하고 학습/예측을 하는 단계입니다.


Elastic Stack 8.0 릴리즈부터 Pytorch를 이용해 NLP를 처리하는 것이 가능해졌다.

BERT(구글에서 개발한 NLP 사전 훈련 기술)와 비슷한 모델을 사용하기 위해 Elasticsearch는 PyTorch 모델 지원으로 가장 일반적인 NLP 작업의 대부분을 지원한다.

NLP 작업의 예시

  • 감정 분석
  • : 긍정적인 진술과 부정적인 진술을 식별하기 위한 이진 분류
  • NER(Named Entity Recognition)
  • : 구조화되지 않은 텍스트에서 구조를 구축하고 이름, 위치 또는 조직과 같은 세부 정보를 추출하려고 시도합니다.
  • 텍스트 분류
  • : Zero-shot 분류를 사용하면 사전 교육 없이 선택한 클래스를 기반으로 텍스트를 분류할 수 있습니다.
  • 텍스트 임베딩
  • : k-nearest neighbor (kNN) search에 사용된다. (머신러닝에서 사용되는 분류(Classification) 알고리즘으로, 유사한 특성을 가진 데이터는 유사한 범주에 속하는 경향이 있다는 가정하에 사용)

Elasticsearch의 NLP

Elastic 8.0 이후, 사용자는 Elasticsearch에서 직접 PyTorch 머신 러닝 모델(BERT 등)을 사용하거나, Hugging Face와 같은 리포지토리에 있는 커뮤니티에 게시된 모델을 사용할 수 있습니다.

사용자가 Elasticsearch 내에서 직접 추론을 수행할 수 있게 됨에 따라

PyTorch 모델을 업로드하기 위한 Eland 클라이언트 와 Elasticsearch 클러스터에서 모델을 관리하기 위한 Kibana의 ML 모델 관리 사용자 인터페이스를 사용 하여 사용자는 다양한 모델을 시험해보고 데이터에서 수행하는 방식에 대해 좋은 접근을 할 수 있습니다. 또한 클러스터의 여러 사용 가능한 노드에서 확장 가능하고 우수한 추론 처리량 성능을 제공받기를 원하는데, 이 모든 것을 가능하게 하려면 추론을 수행할 기계 학습 라이브러리가 필요합니다.

Elasticsearch에서 PyTorch에 대한 지원을 추가하려면 PyTorch를 지원하는 기본 라이브러리 libtorch를 사용해야 하며, TorchScript 표현으로 내보내거나 저장된 PyTorch 모델만 지원합니다.

Elasticsearch는 다양한 NLP 작업 및 사용 사례에서 작동하는 플랫폼을 제공할 수 있습니다.

PyTorch NLP, Hugging Face Transformers 또는 Facebook의 Fairseq와 같은 라이브러리를 사용하여 모델을 Elasticsearch로 가져와 해당 모델에 대한 추론을 수행할 수 있습니다. Elasticsearch 추론은 처음에는 수집 시에만 이루어지며 향후에는 쿼리 시에도 추론을 도입할 수 있도록 확장할 수 있습니다.

Elasticsearch에서 데이터를 스트리밍하기 위한 API 호출 및 플러그인 및 기타 옵션을 통해 NLP 모델을 통합하는 방법이 8.0 이전에도 있었습니다.그러나 8.0이후 Elasticsearch 데이터 파이프라인 내에서 NLP 모델을 통합하면 다음과 같은 이점을 얻을 수 있습니다.

  • NLP 모델을 중심으로 더 나은 인프라 구축
  • NLP 모델 추론 확장
  • 데이터 보안 및 개인 정보 보호 유지

Elasticsearch가 주관한 2022 0525 NLP Webinar 내용

개요:

  1. 구글 플레이에서 앱의 리뷰(사용자ID, 별점,날짜, 리뷰내용, 좋아요등)를 크롤링한다.
  2. 구글 번역기를 통해 리뷰를 영문번역 후 각각 한글감성모델과 영어감성모델을 적용후 둘을 비교한다.

Elastic search에서 NLP를 사용하기 위한 과정

 👉
       1. Select a trained model
       2. import the trained model and vocabulary
       3. Deploy the model in elasticsearch cluster
       4. try it out

1. Select a trained model

  • HuggingFace에서 Models 선택

  • 검색창에 korean_sentiment 입력 후, 아래의 모델 선택

2. import the trained model and vocabulary

elasticsearch는 python 라이브러리로 학습된 모델만 적용이 가능 → pytorch로 학습된 모델을 사용 +

가져온 모델을 elasticsearch로 import

eland 관련 공식 document: https://www.elastic.co/guide/en/elasticsearch/client/eland/current/overview.html

eland version별 차이

8.1 - config.json이 생성됨, config.json을 통해서 수정가능

8.2 - config.json을 생성하지 않음, NlpTrainedModelConfig 객체 사용가능, NlpTrainedModelConfig의 세부내용을 customize가능

 

1. PyTorch 추가 종속성과 함께 Eland Python 클라이언트를 설치합니다 .

2. eland_import_hub_model 스크립트를 실행합니다 . 방식은 아래와 같습니다.

 

 

자세한 내용은 해당 참조: https://github.com/elastic/eland#nlp-with-pytorch

 

 

3. Deploy the model in elasticsearch cluster

kibana실행 → machine learning 누르기 → trained models 누르기→ start deployment

이때, 모델중에 용랑이 큰것들이 있으므로 인스턴스는 4GB이상으로 설정하는 것이 좋음

  • kibana접속 후, 실행하는 방법은 아래와 같다.

 

4. try it out

index 설정 → add NLP inference to ingest pipeline

 

NER(name entity recognition)- 텍스트를 분석해 사람인지 장소인지를 구분하여 추출

text classification - 긍정,부정을 구분

hugging face사이트에서 model 설정(korean-sentiment모델을 사용)

 

  • Pipeline을 만드는 방법은 2가지가 있으나 첫번째 방법이 더 쉽다.

1. Kibana → Stack Management → Ingest Pipelines → Create pipeline

 

 

2. Kibana → Dev tools → Create pipeline

 

 

분석결과

  • 각자언어에 대한 정확도는 80%이상이라고 판단
  • 이때 한글감성 모델과 영어감성 모델의 차이는 해석을 어떻게 하느냐에 따라 같은 리뷰도 긍정,부정이 다르게 나올 수 있음

 

 

위 내용은 Elasticsearch에서 주관한 Webinar를 참고하였습니다.

 

참고: https://bkshin.tistory.com/entry/NLP-1-자연어-처리Natural-Language-Processing란-무엇인가

https://www.elastic.co/blog/introduction-to-nlp-with-pytorch-models

https://www.elastic.co/kr/blog/whats-new-elastic-8-0-0

 

'ElasticSearch' 카테고리의 다른 글

조건문 사용(If Else 문)  (0) 2022.04.29
매일 ES에 있는 data를 CSV파일로 만들기  (0) 2022.04.26
ElasticStack에 대해  (0) 2022.03.15

+ Recent posts