[Generic (제네릭)]

클래스 내부에서 사용할 데이터 타입을 인스턴스 생성시 확정하는 것을 제네릭이라 한다.

제네릭은 다양한 타입의 객체를 다루는 메소드 및 컬렉션 클래스를 컴파일 시, 타입 체크를 해주는 기능을 한다.

객체 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄인다.

 

※ ArrayList 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있지만, 보통 아래와 같이 한 종류의 객체를 담는 경우가 더 많다. 또한 꺼낼 때 마다 타입체크를 하고 형변환 하는 것은 아무래도 불편할 수 밖에 없는데, 제네릭이 이와 같은 불편함 들을 해소해준다.

List<Person> list = new ArrayList<>();
list.add(new Person("영심이"));
list.add(new Person("홍길동"));
list.get(0);

 

1. 제네릭 타입

타입을 파라미터(<T>)로 가지는 클래스 혹은 인터페이스

타입 파라미터(<T>) 를 클래스 혹은 인터페이스 명 뒤에 두어 선언

public class Sample<T>{}
public interface Sample<T>{}

[Print.class (제네릭 타입)]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package Java.Generic;
 
public class Print<T> {
    
    private T text;
    
    public void setText(T text) {
        this.text = text;
    }
    
    public void printText() {
        System.out.println(text);
    }
}
 
cs

 

[Main.class]

p.text 는 String,

b.text 는 Integer(int) 데이터 타입을 갖게된다.

: 제네릭을 사용하여 클래스를 정의할 때 데이터 타입을 정하지 않고, 인스턴스를 생성할 때 타입을 저장

 

※ jdk 1.7 버전 이상은 실제 타입 생략 가능

Print<Integer> p = new Print<>();  // Print<Integer> p = new Print<Integer>(); 와 같음

※ Raw Type : 타입 매개변수가 없는 제네릭 클래스

generic은 jdk 1.5 버전에 지원됐으며, 1.5 버전 이전의 소스와 문제가 생기는걸 방지하기 위해 Raw Type 을 허용

아래와 같이 데이터타입을 주지 않고 선언 할 경우 데이터 타입(타입 매개변수)은 Object로 간주한다.

List list = new ArrayList<>();   //List<Object> list = new ArrayList<>(); 와 같음

[제네릭의 장점]

1. 타입안정성 제공 :

명시한 타입의 데이터가 아닌 다른 형태의 데이터 타입으로의 형변환을 방지 (위 Main.class 의 8번, 14번 참고)

2. 타입체크와 형변환 생략 가능 :

다룰 객체의 데이터 타입을 미리 명시

 

[다중 타입매개변수]

타입매개변수가 N개인 경우, 아래와 같이 사용 가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package Java.Generic;
 
public class Pair<K, V> {
 
    private K key;
    private V value;
    
    public Pair(K key, V value){
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }    
}
 
cs

Client(호출부)

Pair<String, Integer> pair1 = new Pair<>("key1", 1);
System.out.println(pair1.getKey());    //key1
System.out.println(pair1.getValue());  //1

Pair<Integer, Integer> pair2 = new Pair<>(1, 2);
System.out.println(pair2.getKey());     //1
System.out.println(pair2.getValue());   //2

[제네릭과 다형성]

public class Print{}
public class ChilePrint extends Print{}

위처럼 Print 클래스, 이를 상속하는 ChildPrint 클래스가 있을 때

아래와 같이 사용 가능.

1
2
3
4
5
6
7
8
9
10
public class Client {
    public static void main(String[] args) {
        List<Print> list = new ArrayList<>();
        list.add(new Print());
        list.add(new ChildPrint());
        
        Print p = list.get(0);
        ChildPrint cp = (ChildPrint)list.get(0);    
    }
}
cs

Print 를 상속하는 ChildPrint 는 Print 타입파라미터를 갖는 list 인스턴스에 add 할 수 있다.

 

 

2. 제네릭 메소드

리턴 타입 및 매개변수 타입을 타입 매개변수로 갖는 메소드

일반 클래스의 메소드에서도 타입 매개변수를 사용하여 제네릭 메소드 정의 가능

리턴타입 앞에 타입 매개변수(<>)를 추가한 후, 리턴타입과 매개타입을 타입 매개변수로 사용가능

 

[Cart.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Java.Generic;
 
import java.util.ArrayList;
import java.util.List;
 
public class Cart<T> {
    
    private List<T> items;
    
    public Cart() {
        if(items == null)items = new ArrayList<>();
    }
    
    public void setItem(T item) {
        this.items.add(item);
    }
    
    public List<T> getItems() {
        return items;
    }
    
}
 
cs

[Calculator.class (제네릭 메소드)]

1
2
3
4
5
6
7
8
9
10
11
12
package Java.Generic;
 
public class Calculator {
    public static <T> int add(Cart<T> cart1, Cart<T> cart2) {
        int result = 0;
        
        result = cart1.getItems().size() + cart2.getItems().size();
        
        return result;
    }
}
 
cs

[Client.class (호출부)]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package Java.Generic;
 
public class Client {
    public static void main(String[] args) {
        
        Cart<String> cart1 = new Cart<>();
        cart1.setItem("초콜렛");
        cart1.setItem("슬리퍼");
        
        Cart<String> cart2 = new Cart<>();
        cart2.setItem("생수");
        
        int totalSize = Calculator.add(cart1, cart2);
        System.out.println(totalSize);  // 3
        
    }
}
cs

 

 

3. 타입 파라미터의 제한

상속 및 구현 관계를 이용하여 타입 제한

※ 상위 타입이 interface인 경우에도 extends 키워드 사용.

public <T extends 상위타입> 리턴타입 메소드(매개변수, ...) {...}

[Product.class]

최상위 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package Java.Generic;
 
public class Product {
    
    private String product;
    
    public void setProduct(String product) {
        this.product = product;
    }
    
    public String getProduct() {
        return this.product;
    }
}
 
cs

[Food.class]

Product를 상속받는 하위 클래스

1
2
3
4
package Java.Generic;
 
public class Food extends Product{}
 
cs

[Fruit.class]

Food 를 상속받는 하위 클래스

1
2
3
4
package Java.Generic;
 
public class Fruit extends Food{}
 
cs

[Cart.class]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Java.Generic;
 
import java.util.ArrayList;
import java.util.List;
 
public class Cart<T> {
    
    private List<T> items;
    
    public Cart() {
        if(items == null)items = new ArrayList<>();
    }
    
    public void setItem(T item) {
        this.items.add(item);
    }
    
    public List<T> getItems() {
        return items;
    }
    
}
 
cs

[Calculator.class (제네릭 메소드)]

타입 파라미터를 Food 로 제한한다. (Food 혹은 Foor 하위 클래스만 타입 파라미터로 받을 수 있다)

1
2
3
4
5
6
7
8
9
10
11
12
package Java.Generic;
 
public class Calculator {
    public static <extends Food> int add(Cart<T> cart1, Cart<T> cart2) {
        int result = 0;
        
        result = cart1.getItems().size() + cart2.getItems().size();
        
        return result;
    }
}
 
cs

[Client.class (호출부)]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package Java.Generic;
 
public class Client {
    public static void main(String[] args) {
        
        Cart<Food> cart1 = new Cart<>();
        Product p = new Food();
        p.setProduct("초콜릿");
        cart1.setItem((Food)p);
        
        Cart<Food> cart2 = new Cart<>();
        Product p2 = new Food();
        p2.setProduct("과자");
       cart2.setItem((Food)p2);
        
        int totalSize = Calculator.add(cart1, cart2);
        System.out.println(totalSize);
    }
}
cs

※ 해당 메소드 호출 시, 타입 파라미터를 섞어서 사용 할 수 없다.

: 16번 라인의 .add(...) 호출 시, 넘겨주는 매개변수 cart1 의 타입 파라미터(6번 라인의 Cart<Food>)에 의해 .add(...) 메소드의 타입파라미터(Calculator.class의 4번 라인의 <T>)가 정해진다.

 

예를 들어, 아래와 같은 경우

14번 라인에서 빨간줄이 그어진다. 

cart3 의 Fruit 으로 .add(...) 메소드의 타입파라미터가 정해지지만, Food 를 담고있는 cart4 를 매개변수로 넣으려고 하고 있으므로 not applicable for the arguments 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Java.Generic;
 
public class Client {
    public static void main(String[] args) {
        
        Cart<Food> cart1 = new Cart<>();
        Calculator.add(cart1, cart1);
        
        Cart<Fruit> cart2 = new Cart<>();
        Calculator.add(cart2, cart2);
                
        Cart<Fruit> cart3 = new Cart<>();
        Cart<Food> cart4 = new Cart<>();
        Calculator.add(cart3, cart4);    //compile error
    }
}
cs

 

 

4. 와일드카드

1. 제네릭 타입<?>: Unbounded Wildcards(제한 없음)

  타입 파라미터로 모든 클래스나 인터페이스 타입이 올 수 있다.

2. 제네릭 타입<? extends 상위 타입>: Upper Bounded Wildcards(상위 클래스 제한)

  타입 파라미터로 상위 타입의 하위 타입만 올 수 있다.

3. 제네릭 타입<? super 하위 타입>: Lower Bounded Wildcards(하위 클래스 제한)

  타입 파라미터로 하위 타입의 상위타입만 올 수 있다.

 

[example]

public class Product {}
public class Food extends Product {}
public class Fruit extends Food {}

[Calculator.class : 타입 파라미터 제한과 와일드카드 사용의 차이]

add1(...) : 타입 파라미터를 제한

add3(...) : 와일드카드를 사용하여 매개변수의 타입을 제한

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
package Java.Generic;
 
public class Calculator {
    public static <extends Food> int add1(Cart<T> cart1, Cart<T> cart2, Cart<T> cart3) {
        //제네릭 메소드에서의 타입 파라미터의 제한
        //타입파라미터를 Food 로 제한
        //매개변수는 타입파라미터인 T를 갖는 Cart이며, T는 Food 및 Food 의 subclass 로 제한
        int result = 0;
        result = cart1.getItems().size() + cart2.getItems().size() + cart3.getItems().size();
        return result;
    }
    
//    public static <T super Food> int add2(Cart<T> cart1, Cart<T> cart2, Cart<T> cart3) {    
//        //제네릭 메소드에서 타입 파라미터를 제한할 때 위와 같은 super키워드는 불가 
//        //compile error
//        int result = 0;
//        result = cart1.getItems().size() + cart2.getItems().size() + cart3.getItems().size();
//        return result;
//    }
    
    public static int add3(Cart<super Food> cart1, Cart<?> cart2, Cart<extends Food> cart3) {
        //매개변수의 타입을 와일드카드 (?)를 사용하여 제한
        //cart1 맥대변수(Cart)의 타입파라미터 : Food 및 Food 를 subclass 
        //cart2 매개변수(Cart)의 타입파라미터 : 아무 타입이나 담고있는 Cart
        //cart3 매개변수(Cart)의 타입파라미터 : Food 및 Food 의 subclass
        int result = 0;
        result = cart1.getItems().size() + cart2.getItems().size() + cart3.getItems().size();
        return result;
    }
}
 
cs

[Client.class]

1. add1사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package Java.Generic;
 
public class Client {
    public static void main(String[] args) {
        
        Cart<Food> cart1 = new Cart<>();
        Calculator.add1(cart1, cart1, cart1);    //타입파라미터를 Food 로 지정
        
        Cart<Fruit> cart2 = new Cart<>();
        Calculator.add1(cart2, cart2, cart2);    //타입파라미터를 Fruit 으로 지정
        
        Cart<Fruit> cart3 = new Cart<>();
        Cart<Food> cart4 = new Cart<>();
        Calculator.add1(cart3, cart3, cart4);    //car3 매개변수에 의해 Fruit 으로 타입파라미터가 정해지지만, cart4 Food를 넣으려하기 때문에 Compile Error
        
    }
}
 
cs

2. add3 사용

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
package Java.Generic;
 
public class Client {
    public static void main(String[] args) {
        
        Cart<Product> cart1 = new Cart<>();
        Product p = new Product();
        p.setProduct("바가지");
        cart1.setItem(p);
        
        Cart<String> cart2 = new Cart<>();
        cart2.setItem("연필");
        
        Cart<Fruit> cart3 = new Cart<>();
        Product p2 = new Fruit();
        p2.setProduct("사과");
        Product p3 = new Fruit();
        p3.setProduct("수박");
        cart3.setItem((Fruit)p2);
        cart3.setItem((Fruit)p3);
        
        int totalSize = Calculator.add3(cart1, cart2, cart3);
        System.out.println(totalSize);
    }
}
cs

 

※ 

E - Element (used extensively by the Java Collections Framework)

K - Key

N - Number

T - Type

V - Value

 

 

참고:

https://devbox.tistory.com/entry/Java-%EC%A0%9C%EB%84%A4%EB%A6%AD

https://movefast.tistory.com/74

https://ict-nroo.tistory.com/42

 

 

 

 

반응형

'back > java' 카테고리의 다른 글

[Java] lambda 람다 2 (메소드참조)  (0) 2020.01.17
[Java] lambda 람다 1 (기본문법)  (0) 2020.01.15
[Java] Compile  (0) 2019.12.22
[Java] Collection Framework  (0) 2019.12.21
Comparator, Comparable + Arrays.sort() 그리고 인터페이스..  (0) 2019.09.02

일반 프로그램은 하드웨어 위에서 하드웨어를 제어하기 위한 OS 동작,

OS 위에서 프로그램이 실행.

그래서 프로그램을 OS 마다 다르게 제작.

윈도우에서 동작하는 프로그램, 리눅스에서 동작하는 프로그램.

반면 자바는 OS 위에 JVM(java virtual machine) 존재하여 jvm 위에서 프로그램이 실행되기 떄문에 운영체제에 의존적이지 않음.

운영체제 상관없이 자바 프로그램 실행이 가능

 

일반 프로그램 hardware > OS(operating system) > program

자바 프로그램 hardware > OS(operating system) > jvm > java program

 

컴파일 과정 :

개발자가 작성한 자바 코드를 컴파일러가 기계어인 자바 바이트 코드(.class)로 변환,

변환된 코드를 인터프리터가 한줄 씩 실행시키며 애플리케이션을 실행.

 

 

자바 코드 --컴파일러--> 자바 바이트 코드(class) --인터프리터--> 애플리케이션 실행

https://moomini.tistory.com/13

 

반응형

이미지 출처: https://stackoverflow.com/questions/11541338/should-i-use-arraylist-or-list/11541387#11541387

Set, List Interface 는 Collection 을 extends 하고 있다 (구조 동일)

Map Interface 는 별도로 정의돼있다.

 

[List]

순서가 있는 데이터의 집합, 데이터 중복을 허용

ArrayList, Vector, LinkedList 차이에 대한 설명

 

[Set]

순서가 없는 데이터의 집합, 데이터 중복을 허용하지 않음

HashSet, LinkedHashSet, TreeSet 차이에 대한 설명

 

[Map]

키와 값, 한 쌍으로 이루어지는 데이터의 집합으로 순서가 없음

값은 중복을 허용하나 키는 중복될 수 없음(키는 중볼될 경우 덮어 씀)

HashTable, HashMap, LinkedHashMap, TreeMap 차이에 대한 설명

 

[Queue]

First In First Out 구조를 따름

LinkedList, PriorityQueue 차이에 대한 설명

 

 

반응형

'back > java' 카테고리의 다른 글

[Java] Generic 제네릭  (0) 2019.12.28
[Java] Compile  (0) 2019.12.22
Comparator, Comparable + Arrays.sort() 그리고 인터페이스..  (0) 2019.09.02
깊은복사(Deep Copy)와 얕은복사(Shallow Copy)  (3) 2019.08.31
LocalHost IP 가져오기  (0) 2019.05.28

배열안에 담겨있는 값(VO)들을 특정 기준으로 비교하여 순서를 정렬하기 위한 방법을 알아보자.

 

1. Car 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package comparator;
 
public class Car {
    
    private String name;
    private int price;
    
    public Car (String name, int price) {
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
}
cs

 

2. Car 객체 정렬 및 실행을 위한 Main 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package comparator;
 
import java.util.Arrays;
 
public class Main {
 
    public static void main(String[] args) {
        
        Car c1 = new Car("K5"2000);
        Car c2 = new Car("A6"8000);
        Car c3 = new Car("BMW3"4000);
        
        Car[] cars = {c1, c2, c3};
        
        Arrays.sort(cars);
 
        for(Car tmp : cars) {
            System.out.println(tmp.getName()+" ");
        }        
    }
}
 
cs

'정렬기준이 없는데?' 라고 생각 들 수 있겠지만 일단 실행시켜보자.

 

역시나 에러가 발생한다.

comparator.Car 가 java.lang.Comparable 로 캐스팅 될 수 없다는 캐스팅 에러.

Arrays.sort 내에서 comparator, comparable 을 사용하다 에러가 발생했다.

Exception in thread "main" java.lang.ClassCastException: comparator.Car cannot be cast to java.lang.Comparable
at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:290)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:157)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:146)
at java.util.Arrays.sort(Arrays.java:472)
at comparator.Main.main(Main.java:19)

 

그럼 이제 Arrays.sort 를 탐구해보자.

 

※ Arrays.sort 

아래는 java.util.Arrays 의 정적메소드 sort.

정확한 파악은 힘들지만 대충 살펴보았을 때 2번째 인자인 Comparator c 값의 유무에 따라 사용되는 메소드가 분기처리 되는 듯 하다.

1) 은 인자가 한개인경우 호출되는 메소드,

2) 는 인자가 두개인 경우 호출되는 메소드

 

1) Arrays.sort(배열) 일 경우 사용되는 method

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
private static void mergeSort(Object[] src,
                                  Object[] dest,
                                  int low,
                                  int high,
                                  int off) {
        int length = high - low;
 
        // Insertion sort on smallest arrays
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
                for (int j=i; j>low &&
                         ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }
 
        // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >>> 1;
        mergeSort(dest, src, low, mid, -off);
        mergeSort(dest, src, mid, high, -off);
 
        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
            System.arraycopy(src, low, dest, destLow, length);
            return;
        }
 
        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }
cs

내부적으로 객체를 Comparable 로 형변환을 한 후 compareTo 메소드를 호출하여 값을 비교(정렬) 하고 있다.

첫번째 인자로 넘겨준 Car 객체는 Comparable(인터페이스) 를 구현하고 있지 않으므로 Comparable 객체로 형변환시 cast exception이 발생한 것이다.

 

그럼 Car 객체가 Comparable 인터페이스를 구현하도록 Car Class 를 아래와 같이 수정해보자.

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 comparator;
 
public class Car implements Comparable<Car> {
    
    private String name;
    private int price;
    
    public Car (String name, int price) {
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    
    @Override
    public int compareTo(Car c) {
        
        int comPrice = c.getPrice();
        
        return this.price-comPrice;
    }
}
cs

 

다시 한 번 2번의 Main 클래스를 실행해보자

실행 결과는 아래와 같이 성공이다.

※ Car 객체의 compareTo 메소드의 return 부분(30 line : this.price-comPrice)

을 comPrice-this.price 와 같이 반대로 바꾸면 정렬 기준이 바뀐다(오름차순/내림차순).

 

위와 같이 VO 클래스를 Comparable interface 를 구현하도록 수정 후 compareTo 를 overriding 하면 원하는 방식으로 정렬을 시킬 수 있다.

VO 클래스를 이와 같이 매번 구현하여 사용하긴 번거로운데, 다른 방법은 없을까?

 

Arrays.sort 를 다시 한 번 들여다 보자.

2) Arrays.sort(배열, Comparator ?) 일 경우 사용되는 method가 아래와 같이 구현되어 있다.

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
private static void mergeSort(Object[] src,
                                  Object[] dest,
                                  int low, int high, int off,
                                  Comparator c) {
        int length = high - low;
 
        // Insertion sort on smallest arrays
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
                for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }
 
        // Recursively sort halves of dest into src
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >>> 1;
        mergeSort(dest, src, low, mid, -off, c);
        mergeSort(dest, src, mid, high, -off, c);
 
        // If list is already sorted, just copy from src to dest.  This is an
        // optimization that results in faster sorts for nearly ordered lists.
        if (c.compare(src[mid-1], src[mid]) <= 0) {
           System.arraycopy(src, low, dest, destLow, length);
           return;
        }
 
        // Merge sorted halves (now in src) into dest
        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }
cs

내부적으로 인자값으로 넘어온 Comparator의 compare 메소드를 호출하여 값을 비교(정렬) 하고 있다.

 

Comparable 을 구현한 Car 객체는 원복 시킨 후, Main 클래스를 아래와 같이 수정해보자.

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
package comparator;
 
import java.util.Arrays;
import java.util.Comparator;
 
public class Main {
 
    public static void main(String[] args) {
        
        Car c1 = new Car("K5"2000);
        Car c2 = new Car("A6"8000);
        Car c3 = new Car("BMW3"4000);
        
        Car[] cars = {c1, c2, c3};
        
        System.out.println("sort start!");
        
        Arrays.sort(cars, new Comparator<Car>(){
            @Override
            public int compare(Car c1, Car c2) {
                //return c1.getPrice() - c2.getPrice();
                return c2.getPrice() - c1.getPrice();
            }
        });
        
        for(Car tmp : cars) {
            System.out.println(tmp.getName()+" ");
        }
        
    }
 
}
 
cs

Arrays.sort 의 두번째 인자값으로 Comparator 를 넘겨주었다. (익명클래스로 구현)

 

실행시 결과는 아래와 같다.

 

사족 : Comparable, Comparator를 통해 바라본 인터페이스에 대해..

Arrays.sort 메소드는 내부적으로 파라미터 값으로 받은 객체의 compareTo 메소드 및 Comparable interface 의 compare 메소드를 사용하고 있다.

어떤 자료형의 파라미터 값이 넘어올지 Arrays 의 sort 메소드는 알 수 없지만 파라미터로 넘겨받은 객체 내에 compareTo 가 존재하리라 가정하고 소스가 짜여져 있다.

이처럼 인터페이스는 구현을 강제하며(Car 클래스의 compareTo 메소드) 소스간 결합도(Arrays.sort 와 Car클래스)를 약하게 한다. 또한 내공이 필요 하겠으나, 개발자들은 이를 활용하여 다양한 디자인 패턴의 개발을 할 수 있다..

 

 

 

반응형

#1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<String> list1 = new ArrayList<String>();
        
list1.add("사과");
list1.add("딸기");
list1.add("키위");
list1.add("바나나");
 
List<String> list2 = list1;
System.out.println("list1 : " + list1);
System.out.println("list2 : " + list2);
 
System.out.println("list1 의 \"사과\" 제거 ");
list1.remove(0);
 
System.out.println("list1 : " + list1);
System.out.println("list2 : " + list2);
cs

1) list1에 사과, 딸기, 키위, 바나나 를 넣었다 (3~6 line)
2) list2에 list1을 넣어주었다 (8 line)
3) list1에서 사과(index : 0) 값을 제거 (13 line)
4) 사과 제거 전 list1, list2 출력 (9, 10 line)
5) list1에 담긴 사과 제거 후 list1, list2 출력 (15, 16 line)

 

위의 코드 실행시 결과는 어떻게 될까?

list1 의 사과 값만 제거 했으므로 list2 엔 사과 값이 존재하고, 출력될까?

 

결과는 다음과 같다. 

list1 에서 사과를 제거했는데 list2 의 사과도 제거되었다.

 

list2 에 list1을 대입한 게 아닌, list2가 list1 의 주소값을 참조하게 했기 때문이다.

List 는 참조형 이기 때문에 값자체가 아닌 주소값을 참조하므로 

#1과 같이 코딩시 위처럼 예상치 못한 결과가 나올 수 있다. (Map 및 Set 또한 마찬가지)

아래와 같이 addAll (List, Set), putAll (Map)을 사용하여 값 자체를 대입시켜 줄 수 있다.

 

 

#2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<String> list1 = new ArrayList<String>();
 
list1.add("사과");
list1.add("딸기");
list1.add("키위");
list1.add("바나나");
 
List<String> list2 = new ArrayList<String>();
list2.addAll(list1);
 
System.out.println("list1 : " + list1);
System.out.println("list2 : " + list2);
 
System.out.println("list1 의 \"사과\" 제거 ");
list1.remove(0);
 
System.out.println("list1 : " + list1);
System.out.println("list2 : " + list2);
cs

#1 코드에서 list2 의 선언 및 list1 의 값을 넣어주는 부분(8~9 line) 만 위와 같이 바꿔보았다.

 

결과는 다음과 같다.

#1과 달리 list2의 사과값이 제거되지 않았다.

 

#3 

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
List<Map<StringString>> list1 = new ArrayList<Map<StringString>>();
 
Map<StringString> map1 = new HashMap<StringString>();
map1.put("과일""사과");
map1.put("동물""물고기");
map1.put("지역""서울");
 
Map<StringString> map2 = new HashMap<StringString>();
map2.put("과일""바나나");
map2.put("동물""호랑이");
map2.put("지역""부산");
 
list1.add(map1);
list1.add(map2);
 
List<Map<StringString>> list2 = new ArrayList<Map<StringString>>();
list2.addAll(list1);
 
System.out.println("조작전 list1 : " + list1);
System.out.println("조작전 list2 : " + list2);
System.out.println("=================================");
 
list1.get(0).remove("과일");
 
System.out.println("조작후 list1 : " + list1);
System.out.println("조작후 list2 : " + list2);
cs

1) map1 에 과일:사과, 동물:물고기, 지역:서울 값을 넣는다.

2) map2 에 과일:바나나, 동물:호랑이, 지역:부산 값을 넣는다.

3) list1 에 map1, map2 값을 넣는다.

4) list2 에 list1을 넣는다.

5) list1에 담긴 map1(list1.get(0))의 과일 값을 제거한다

6) 조작전 list1 과 조작 후 list1, list2 를 출력한다.

 

결과는 아래와 같다.

list2에 list1 을 addAll 로 넣어주었는데? 라고 생각할 수 있지만,

list1 이 갖고 있는 map1, map2 값 자체가 아닌 map1, map2 의 주소값만 list2 에 저장되었기 때문이다.

(#2에서 확인한 것 처럼 #3에서 역시, list2.remove(0) 으로 list2에 담겨있는 map1의 주소값을 제거할 경우 list1엔 영향을 주지 않는다)

 

#4

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
List<Map<StringString>> list1 = new ArrayList<Map<StringString>>();
 
Map<StringString> map1 = new HashMap<StringString>();
map1.put("과일""사과");
map1.put("동물""물고기");
map1.put("지역""서울");
 
Map<StringString> map2 = new HashMap<StringString>();
map2.put("과일""바나나");
map2.put("동물""호랑이");
map2.put("지역""부산");
 
list1.add(map1);
list1.add(map2);
 
List<Map<StringString>> list2 = new ArrayList<Map<StringString>>();
        
for(Map<StringString> m : list1) {
    Map<StringString> map3 = new HashMap<StringString>();
    map3.putAll(m);
    list2.add(map3);
}
 
System.out.println("조작전 list1 : " + list1);
System.out.println("조작전 list2 : " + list2);
System.out.println("=================================");
 
map2.remove("과일");
 
System.out.println("조작후 list1 : " + list1);
System.out.println("조작후 list2 : " + list2);
cs

#3 코드에서 list2 의 선언 및 list1 의 값을 넣어주는 부분(18~21 line) 만 위와 같이 바꿔보았다.

 

결과는 다음과 같다.

 

* 2중 for문 및 iterator 를 돌려 map 내의 값을 직접 꺼내 map 에 직접 넣어준 후 list 에 add 를 해줘도 된다.

* map 이 아닌 VO의 경우 clone 을 사용하여 vo 값 자체를 복사한 후, list에 넣어 줄 수도 있다.

 

※ deep copy , shallow copy 에 대한 예제와 설명이 정리되어있는 글

https://howtodoinjava.com/java/collections/arraylist/arraylist-clone-deep-copy/

 

※ 참조형 같으면서 기본형 같은 String 자료형에 대한 설명이 잘 되어있는 좋은 글

https://brunch.co.kr/@kd4/1

반응형

[클라이언트 ip 가져오기]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static String getIp(HttpServletRequest request){
    String result = null;
    
    result = request.getHeader("X-Forwarded-For");
    
    if (result == null || result.length() == 0 || "unknown".equalsIgnoreCase(result)) {
        result = request.getHeader("Proxy-Client-IP");
    }
    if (result == null || result.length() == 0 || "unknown".equalsIgnoreCase(result)) {
        result = request.getHeader("WL-Proxy-Client-IP");
    }
    if (result == null || result.length() == 0 || "unknown".equalsIgnoreCase(result)) {
        result = request.getHeader("HTTP_CLIENT_IP");
    }
    if (result == null || result.length() == 0 || "unknown".equalsIgnoreCase(result)) {
        result = request.getHeader("HTTP_X_FORWARDED_FOR");
    }
    if (result == null || result.length() == 0 || "unknown".equalsIgnoreCase(result)) {
        result = request.getRemoteAddr();
    }
    
    return result==null?"":result; 
}
 
cs

 

[서버 ip 가져오기]

1
2
3
4
5
6
7
8
9
 public static String getIp(){
    String result = null;
    try {
        result = InetAddress.getLocalHost().getHostAddress();
    } catch (UnknownHostException e) {
        result = "";
    }
   return result; 
}
cs

 

찾다보니 아래와 같은 글도 찾게 되었다.

https://pkgonan.github.io/2018/06/InetAddress-getLocalHost

 

반응형

자바 컴파일 버전 Exception

 

컴파일된 앱(ex: lib/jar)이 실행하는 환경의 jdk 버전보다 높은 경우 아래와 같은 exception이 발생. 

Exception in thread "main" java.lang.UnsupportedClassVersionError: ~ : Unsupported major.minor version 52.0

여기서 version 52.0 은 java 8버전을 의미(1.8)

 

 

참고 :

J2SE 8   = Version 52

J2SE 7   = Version 51

J2SE 6.0 = Version 50

J2SE 5.0 = Version 49

JDK  1.4 = Version 48

JDK  1.3 = Version 47

JDK  1.2 = Version 46

JDK  1.1 = Version 45



해결 방법 :

jar/lib 을 본인(서버)의 실행환경(jdk버전)에 맞게 버전을 낮춰주거나,

본인(서버)의 실행환경을 사용할 jar/lib에 맞춰주어야 한다. 

보통 전자의 방법으로 해결..

반응형

emoji 처리

모바일 앱이 전달하는 채팅내용에 emoji 가 포함되어 API 서버에서 Mysql DB에 해당 채팅내용을 로그성으로 기록하기 위해 insert를 하다 DB에러가 났다.

Exception은 아래와 같이 Incorrect string value ~ 발생.

Warning: #1366 Incorrect string value: '\xF0\x9F\x8D\xB0' for column 'Text' at row ~

 

* emoji 는 모바일(android, ios)에서 사용되는 이모티콘 같이 생긴 문자열로

3byte 문자열인 utf8과 달리 4byte 문자열인 utf8mb4 charset 을 사용한다.

 

 

해결방법 1. 

DB 칼럼의 datatype을 utf8mb4로 수정을 하여 해결

 

해결방법 2.

2-1) emoji 를 정규식으로 거른다 :

android, ios os 버전업이 될 때마다 emoji가 추가되는 듯 하니, 오래 운영될 서비스라면 적합하지 않은 방법인 듯.

2-2) 문자를 제외한 모든 데이터를 정규식으로 거른다 :

허용해줄 모든 문자들을 신중히 남겨놓아야 하기 때문에 번거롭겠지만, 로그성 데이터라면 대충 걸러도 되니 해당 방법을 선택..

1
2
3
4
5
6
public static String replaceEmoji(String text){
 
    String newText = text.replaceAll("[^a-zA-Z0-9ㄱ-힣\\s!@#$%^&|:<>~/';\"`.\\?\\}\\{\\|\\*\\[\\]\\(\\)-_/]""▩");
        
   return newText;
}
cs

 

 

 

반응형

HttpUrlConnection을 이용한 외부서버 통신

프로젝트 내에서 외부 library를 사용하는데 제한이 조금 있어서 Spring이 지원하는 RestTemplate 같은 건 사용할 수 없었고, 대신 java.net.HttpUrlConnection 으로 외부 서버와 통신할 수 밖에 없었다..

 

makeParams(..) :

map에 담겨온 파라미터들을 get방식 뒤에 붙이는 url?key=value 형식으로 만드는 역할을 하는 메소드.

makeJsonParams(..) :

map에 담겨온 파라미터들을 { "key" : "value" } 와 같이 json 포맷으로 만들어 주는 메소드

httpUrlConnection(..) :

실제 외부와 connection 을 하는 메소드.

header 정보를 담아 호출해야하는 경우, json 형식으로 파라미터를 넘겨야 하는 경우 등 상황에 따라 호출하는 데이터 형식 및 호출 방식이 달라지기 때문에 오버로딩하여 구현

 

소스는 아래와 같다.

 

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package ;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
import com.google.gson.Gson;
/** @author ljpyo */
@Component("httpUtil")
public class HttpUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
    public static final String POST = "POST";
    public static final String GET = "GET";
    public static final String DELETE = "DELETE";
    
    private String makeParams(Map<String, Object> params){
        String param = null;
        StringBuffer sb = new StringBuffer();
        
        if(params != null){
           for ( String key : params.keySet() ){
               logger.info(" key : " + key + " / value : " + params.get(key));
               sb.append(key).append("=").append((params.get(key)==null?"":params.get(key)).toString().trim()).append("&");
           }
        }
        param = sb.toString().substring(0, sb.toString().length()-1);
        return param;
    }
    
    private String makeJsonParams(Map<String, Object> params){
        String json = "";
        if(params != null){
            json = new Gson().toJson(params);
        }
        return json;
    }
    
    public String httpUrlConnection(String getpost, String targetUrl, Map<String, Object> params) throws Exception {
       String returnText = this.httpUrlConnection(getpost, targetUrl, params, nullfalse);
       return returnText;
    }
    
    public String httpUrlConnection(String getpost, String targetUrl, Map<String, Object> params, boolean isJson) throws Exception {
        String returnText = this.httpUrlConnection(getpost, targetUrl, params, null, isJson);
        return returnText;
     }
    
    public String httpUrlConnection(String getpost, String targetUrl, Map<String ,Object> params, Map<String, Object> header, boolean isJson) throws Exception {
       URL url = null;
       HttpURLConnection conn = null;
       
       String jsonData = "";
       BufferedReader br = null;
       StringBuffer sb = null;
       String returnText = "";
       JSONObject jobj = null;
       
       String postParams = "";
       
       try{
           
           if(getpost.equalsIgnoreCase(POST) || getpost.equalsIgnoreCase(DELETE)){
               url = new URL(targetUrl);
           } else if(getpost.equalsIgnoreCase(GET)){
               url = new URL(targetUrl + ((params!=null)?"?"+makeParams(params):""));
           }
           logger.info("request url : " + url);
           conn = (HttpURLConnection) url.openConnection();
//         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
//         conn.setRequestProperty("Accept", "application/json");
           
           if(header != null){
               conn.setRequestProperty(header.get("headerKey").toString(), header.get("headerValue").toString());
               logger.info("header : " + header.get("headerKey").toString() + "  /  headerValue : " +header.get("headerValue").toString());
           }
           
           if(isJson){
               conn.setRequestProperty("Content-Type""application/json");
           }
           
           conn.setRequestMethod(getpost);
           conn.setConnectTimeout(5000);
           conn.setReadTimeout(5000);
           conn.setDoOutput(true);
           
           if(getpost.equalsIgnoreCase(POST) || getpost.equalsIgnoreCase(DELETE)){
               if(params != null){
                   if(isJson){
                       postParams = makeJsonParams(params);
                   } else {
                       postParams = makeParams(params);
                   }
                   logger.info("isJson : " + isJson);
                   logger.info("postParam.toString()  : " + postParams);
                   logger.info("post param : " + postParams.getBytes("UTF-8").toString());
                   conn.getOutputStream().flush();
               }
           } 
           
           br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
           
           sb = new StringBuffer();
           
           while((jsonData = br.readLine()) != null){
               sb.append(jsonData);
           }
           
           returnText = sb.toString();
           
           try{
               jobj = new JSONObject(returnText);
               if! jobj.has("responseCode") ){
                   jobj.put("responseCode", conn.getResponseCode());
               }
           } catch (JSONException e){
               jobj = new JSONObject();
               jobj.put("responseCode", conn.getResponseCode());
           }
           
       } catch (IOException e){
           logger.debug("exception in httpurlconnection ! ", e);
           throw new APIException("exception in httpurlconnection !");
       } finally {
           try {
               if (br != null) br.close();
           } catch(Exception e){
               logger.warn("finally..br.close()", e);
           }
           br = null;
           try {
           if(conn!=null)
               conn.disconnect();
           } catch(Exception e){
               logger.warn("finally..conn.disconnect()", e);
           }
           conn = null;
       }
       return jobj != null ? jobj.toString() : null;
    }
    
}
 
cs

 

처음엔 GET POST만 짜놓으면 될 줄 알았는데 나중에 DELETE 방식 요청을 추가적으로 구현해야했다.

POST 처럼 날리면 될 줄 알았더니 DELETE 호출방식엔 outputstream을 사용할 수 없다는 예외가 발생하여 애 좀 먹었다.. (https://developyo.tistory.com/8 참고..) 

 

1년도 안된 신입이 짠 유틸을 꽤 큰 프로젝트에서 공통으로 사용하고 있으니 불안해 죽겠다.

아직 큰 문제 없는 걸 보니 그냥 잘 돌아가고 있는듯...

 

일단 본 프로젝트에선 connectionTimeout , readTimeout Exception이 발생했을 때 재시도(retry) 없이 Customizing한 Exception을 내뱉으며 접속을 종료 시키지만

공부할 겸 retry 기능을 넣어봐야겠다.

 

추후 retry 기능을 넣어 재포스팅하겠다.

 

 

* RETRY (재시도) 설정 추가

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
53
54
55
56
package ;
 
@Component("httpUtil")
public class HttpUtil {
    
    public String httpUrlConnection(~) throws Exception {
               
       //재시도 추가 190220
       for(int i=0; i < (retry<1?1:retry); i++){
               
           try{
               //파라미터 세팅
               //호출 
               //생략~               
 
               if(conn.getResponseCode() != HttpStatus.OK){
                    //응답코드가 실패인 경우 Exception 고의로 발생(catch 에서 continue 로 처리)
                    throw new CustomException();    //customized exception 사용
              }  
              //성공은 for문 나감
              break;
             
              //응답 값 파싱
           } catch (SocketTimeoutException ste){
               errMsg = ste.getMessage();
               logger.debug(errMsg);
           } catch (CustomExceptione ce){
               errMsg = ce.getMessage();
               logger.debug(errMsg);
           } catch (Exception e){
               
           } finally {
               //자원 해제
               try {
                   if (br != null) br.close();
               } catch(Exception e){
                   logger.warn("finally..br.close()", e);
               }
               br = null;
               try {
               if(conn!=null)
                   conn.disconnect();
               } catch(Exception e){
                   logger.warn("finally..conn.disconnect()", e);
               }
               conn = null;
           }
       }
       
       if(jobj!=null){
           return jobj.toString();
       } else {
           throw new APIException(errMsg, ConstantsAPI.APIResult.E_NETWORK.getCode());
       }
    }
}
cs

 

호출받는 쪽에 connect가 오래걸려 connectTimeOut 이 나거나, connect는 되었으나 내부 처리가 오래걸려 readTimeOut이 발생한 경우 특정 횟수(n)만큼 재시도를 하도록 소스를 조금 수정했다.

(보통 한번 안되면 몇 번을 다시 시도해도 안되는 것 같지만...)

 

retry 횟수를 파라미터로 받고,

for문을 돌린다.

timeout 예외 및 응답코드가 200이 아닌 경우(CustomException)가 발생하면( SocketTimeoutException ) catch 문에서 Error 를 먹어버리고 for문이 retry 횟수만큼 이어서 진행된다.

응답코드가 200인 경우 break; 구문을 타게 되어 for문이 종료한다.

* 예외가 발생하지 않을 경우 return 하고 해당 메소드는 끝남.

 

 

요청하고 있는 외부 API 에서 HttpStatus 를 제멋대로 (보통 200을 사용) 담아 리턴하고 있어서 소스 전체를 올리진 못했다. 

(결과코드를 일일히 매핑해야했기 때문에.. 소스가 요상함)

 

여튼 for문 써서 connection 객체로부터 getResponseCode()로 연결상태 확인 하여 처리하면 된다.

반응형

java.net.HttpUrlConnection 을 사용한 GET/POST 방식 호출 함수를 작성 후,

호출 방식(httpMethod) 만 DELETE 방식으로 바꿔서 함수를 동작시킬시

HTTP method DELETE doesn't support output ~

과 같은 exception 이 발생한다.

 

https://bugs.openjdk.java.net/browse/JDK-7157360

위의 URL에 기재된 jdk bug 리포트에 따르면..

When using HttpURLConnection, if I set the request method to "DELETE" and attempt to get to the output stream to write the entity body, I get:

Exception in thread "main" java.net.ProtocolException: HTTP method DELETE doesn't support output

at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1004)

As it turns out, sun.net.www.protocol.http.HttpURLConnection explicitly denies access to the output stream if the request

method is DELETE.

>> httpurlconnection 을 사용하여, DELETE request 메소드로 사용하고 (.setRequestMethod("DELETE") 의미) 바디를 쓰기위해 output stream 을 가

    져오면, HTTP 메소드 DELETE는 output 을 지원하지 않는다는 exception이 발생한다.

>> sun.net.www.protocol.http.HttpURLConnection 은 request 메소드가 DELETE라면 output stream 접근을 막는다.

한줄로 정리하자면 jdk 1.7에선 http DELETE 방식에 OutputStream을 지원하지 않는다(Exception이 발생한다).

 

해결책 1.

jdk 버전을 1.8로 올려주면 된다. (참고)

>> jdk1.8버전에 수정된 버그인 듯 하나, 직접 실험해보진 않았다.. (프로젝트 자체가 jdk1.7이었고 다른 방법을 강구해야 했다)

 

해결책 2.

조건문을 걸어 파라미터가 존재하지 않을 경우 OutputStream을 사용하지 않는다.

DELETE 방식의 호출인 경우 파라미터가 없어야 정상으로 알고 있다.

(restful api 에서의 delete 요청은 uri 에 파라미터(key)를 넘긴다)

 

[SAMPLE]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(getpost.equalsIgnoreCase(POST) || getpost.equalsIgnoreCase(DELETE)){
   if(params != null){
       if(isJson){
           postParams = makeJsonParams(params);
       } else {
           postParams = makeParams(params);
       }
       logger.info("isJson : " + isJson);
       logger.info("postParam.toString()  : " + postParams);
       conn.getOutputStream().write(postParams.getBytes("UTF-8"));
       conn.getOutputStream().flush();
   }
 
cs

(참고 : https://developyo.tistory.com/10?category=688588)

* 결과값은 GET/POST 와 같은 다른 HttpMethod과 상관없이 InputStream을 가져와서 읽어주면 된다.

 

 

쉽게 해결한 듯 보이나 실제론 2시간 가까이 삽질했다.. 

 

반응형

+ Recent posts