반응형
public final class Optional<T> {

    private final T value;
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    private Optional(T value) {
        this.value = value;
    }
    public static <T> Optional<T> of(T value) {
        return new Optional<>(Objects.requireNonNull(value));
    }
    @SuppressWarnings("unchecked")
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? (Optional<T>) EMPTY
                             : new Optional<>(value);
    }
    ......

Optional Class는 모든 개발자의 친구이며 원수인 NULL을 상대하기 위해 자바8 이후에 개발자들에게 주어진 무기쯤으로 생각하면 편하다.

 

기본적으로 Optional은 Wrapper Class로써, 모든 형태의 변수를 감싸고, 주어진 매소드들을 통해 Null 처리를 할 수 있도록 도와준다. 주어진 기능은 Stream과 어느정도 유사하여, 리스트가 1개인 Stream정도로 생각해도 좋다.

 

뭔지 모르겠을땐 일단 사용해보면서 알아보자.

 

예제 클래스

public class Department { //부서 클래스
    private String departmentName; // 부서명
    private int departmentNumber; // 부서번호
    private String responsibilities; // 담당업무
}

public class Employee { // 사원 클래스
    private Department department; // 부서
    private String employeeName; // 사원명
    private int employeeNumber; // 사원번호
}

public class Stock { // 물품 클래스
    private Employee usingEmployee; // 사용사원
    private int stockNumber; // 물품번호
    private String stockName; // 물품명
}

 

생성 (1) <T> Optional<T> of

public static <T> Optional<T> of(T value) {
        return new Optional<>(Objects.requireNonNull(value));
}

가장 기본적인 Optional 객체 생성 방법인 of이다.

사용시 주의해야 할 점은 바로 of는 NULL을 인자로 허용하지 않는 다는 점이다. 만약 of를 통해 optional 객체 생성시 null 일경우, NullPointException이 발생한다.

Department itDepartment = new Department("IT Department", "System maintenance", 101);

Optional<String> dptName = Optional.of(itDepartment.getDepartmentName());

System.out.println(dptName.get()); // RESULT String "IT Department"

/****************************************************************/

Department itDepartment = new Department(null, "System maintenance", 101);

Optional<String> dptName = Optional.of(itDepartment.getDepartmentName());

System.out.println(dptName.get()); // NullPointException

of를 통해 객체 생성을 할 경우는 해당 값이 무조건 Null이 아닐 경우에만 사용해야 한다. 만약 해당 값이 Null 가능성이 있다면 아래의 ofNullable을 사용하여 Optional객체를 생성해야 한다. 

 

생성 (2) <T> Optional<T> ofNullable

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? (Optional<T>) EMPTY
                         : new Optional<>(value);
}

of 와 달리 만약 참조할 객체 또는 값이 null 가능성이 있을 경우, ofNullable을 사용하여 Optional 객체를 생성한다.

Department itDepartment = new Department("IT Department", "System maintenance", 101);
Optional<String> dptName = Optional.ofNullable(itDepartment.getDepartmentName());
System.out.println(dptName); //RESULT : Optional[IT Department]
System.out.println(dptName.get()); // RESULT : "IT Department"

뭐... 별차이 없어 보인다. 하지만 Intelij를 사용하면....

of를 사용할 때와는 다르게 경고줄이 표시되며 "Optional.get() without "isPresent" check" 라는 경고문이 나온다.

isPresent는 Optional에서 제공해 주는 메소드로 값이 존재할 경우 True null일경우 False를 반환해 주는 메소드이다.

if(dptName != null) { // 이 코드와 동일하다고 보면 된다.
    return true;
} else {
    return false;
}

 

즉 Null 가능성이 있으니 isPresent 등을 사용하여 Null Check를 하라는 말이다.(컴파일 에러는 아니다.)

 

반응형

이거 그래서 왜씀? 뭐임?

답은 하나다. 앞에 힘들게 정리해 왔던 funtion / consumer / stream ...등등... 처럼 lamda 식으로 간결하게 처리 하기 위함이다.

 

자 그럼 Optional에서 제공하는 여러가지 메소드 몇몇을 사용해서 기존 Null처리 방식과 어떤 차이가 있는지 예제를 통해 확인해보자

 

1. ifPresent(Consumer<? super T> action)

public void ifPresent(Consumer<? super T> action) {
    if (value != null) {
        action.accept(value);
    }
}

ifPresent는 Optional에서 제공하는 메소드 중 하나로, 만약 참조하는 인자 값이 null이 아닐 경우 주어진 Consumer Action을 실행하는 메소드 이다.

단순하게 부서 클래스로 부터 부서명을 받아 출력하는 코드를 보면

// 전통적인 방식
String dptName = itDepartment.getDepartmentName();
if(dptName != null)
    System.out.println(dptName);

이걸 Optional의 ifPresent를 사용하여 코드를 작성한다면...

Optional<String> dptName = Optional.ofNullable(itDepartment.getDepartmentName());
dptName.ifPresent(s -> System.out.println("test1 data exist"));

이렇게 lamda식으로 간결하게 표현이 가능하다.(그냥 이게 좋은거라고 가스라이팅 합시다.)

 

2. Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)

or 메소드는 of나 ofNullable을 통해 객체를 생성하여 사용시, null 일경우 Default 객체 또는 Default 값을 넣어주기 위해 사용하는 메소드이다.

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
    Objects.requireNonNull(supplier);
    if (isPresent()) {
        return this;
    } else {
        @SuppressWarnings("unchecked")
        Optional<T> r = (Optional<T>) supplier.get();
        return Objects.requireNonNull(r);
    }
}

구현된 내용을 보면 현제 Optional 객체의 값이 isPresent() 즉 존재하면 자기 자신을 return하고, 만약 값이 존재하지 않으면(Null일 경우) 인자로 받은 Supplier 인터페이스에 존재하는 Optional 객체를 반환한다.

 

3. T orElse(T other)

orElse또한 or과 동일하게 of나 ofNullable을 통해 객체를 생성하여 사용시, null 일경우 Default 객체 또는 Default 값을 넣어주기 위해 사용하는 메소드이다.

큰  차이점은 orElse는 Optional 객체를 반환하는게 아닌 값 그 자체를 반환한다는 것이다.

public T orElse(T other) {
    return value != null ? value : other;
}

or 메소드와 구현된 내용이 많이 다른걸 볼 수 있다. 단순하게 Optional의 Value값이 존재하면 해당 값을 return하고, null일경우 인자로 받은 값을 return 한다.

 

Optional<String> dptName = Optional.ofNullable(itDepartment.getDepartmentName());

Optional<String> hrDepartment = dptName.or(() -> Optional.of("HR Department"));
String hrDepartment = dptName.orElse("HR Department");

해당 코드를 통해 or과 orElse 반환값 차이를 확인 할 수 있다.

 

그 밖에도

2024.01.30 - [개발/Java] - Java 기초부터 다시 - interface Stream 에서 설명한 Stream의 Filter , Map 등의 기능도 사용 가능하다.

 

Optional 실 사용

앞서 예제로 설명한 부서(Department) / 사원(Employee)  / 물품(Stock) 클래스에서 물품을 입력하면 해당 물품을 사용하는 사원의 부서를 반환하는 함수를 만들어보자.

전통적인 방식대로 만든다면....

public String getDepartmentOfEmployeeFromStock(Stock stock) {
    if (stock != null) {
        Employee emp = stock.getUsingEmployee();
        if (emp != null) {
            Department department = emp.getDepartment();
            if (department != null) {
                String departmentName = department.getDepartmentName();
                if (departmentName != null) {
                    return departmentName;
                }
            }
        }
    }
    return "not exist department";
}

이렇게 하나하나 Null검사를 하면 Null에 안전하게 개발을 진행할것이다. PTSD 오지쥬?

Optional을 사용해서 조금 간편하게 구현을 다시하면...

public String getDepartmentOfEmployeeFromStock(Stock stock) {
    return Optional.ofNullable(stock)
            .map(s -> s.getUsingEmployee())
            .map(e -> e.getDepartment())
            .map(d -> d.getDepartmentName())
            .orElse("not exist department");
}

이렇게 표현이 가능하다. != null 로 부터 자유를 찾았다...

반응형
반응형

이전에 정리한 모든 자바 기본 함수형 인터페이스들(Function<T,R> , Consumer<T> ... 등등)은 사실 해당 Stream, Optional  인터페이스를 사용하기 위함이다.(개인적인 생각...)

 

그럼 먼저 자바8버전 이후의 핵심 Stream에 대해서 먼저 정리해 보자.

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    Stream<T> filter(Predicate<? super T> predicate);

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    
    ....

<stream에서 가장 많이 쓰이는 filter, map 추상 메소드>

 

Stream은 자바8 이전 for, foreach, sort 등을 사용하여 리스트 관리를 해온것을 좀더 간결하고 직관적(쉽게 말해 람다로 표현 하도록) 도와주는 인터페이스 이다.

 

예제

예제로 알아 보기전 예제로 사용할 기본적인 클래스를 먼저 정의 해보겠다.

public class Vehicle {
    public enum Type {
        SUV,
        SEDAN,
        TRUCK
    }

    private Type type;
    private String vendor;
    private String modelName;
    private String color;
    private int price;
}

Vehicle vehicle1 = new Vehicle(Vehicle.Type.SUV, "BMW", "L3", "white", 250000);
Vehicle vehicle2 = new Vehicle(Vehicle.Type.TRUCK, "HYUNDAI", "Q3", "black", 150000);
Vehicle vehicle3 = new Vehicle(Vehicle.Type.SEDAN, "HONDA", "W3", "white", 550000);
Vehicle vehicle4 = new Vehicle(Vehicle.Type.SUV, "BMW", "R3", "black", 23000);
Vehicle vehicle5 = new Vehicle(Vehicle.Type.SUV, "HYUNDAI", "N3", "blue", 125000);
Vehicle vehicle6 = new Vehicle(Vehicle.Type.TRUCK, "HYUNDAI", "V3", "blue", 951000);
Vehicle vehicle7 = new Vehicle(Vehicle.Type.SEDAN, "KIA", "Z3", "red", 123000);

vehicleList = Arrays.asList(vehicle1,vehicle2,vehicle3,vehicle4,vehicle5,vehicle6,vehicle7);

Collection Type(Collection, List , Set 등)을 Stream으로 사용하기 위해서는 자바8 이후 추가된 Default Method인 stream()을 사용하면 된다.

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

 

 

 

1. <R> Stream<R> map(Function<? super T, ? extends R> mapper)

[Vehicle 리스트의 stream에서 map 매소드 호출 시 매개변수 및 반환값]

매개변수 : Function<Vehicle, R>

반환값 : Stream<R>

말로 풀어 보면 Vehicle객체받아 R을 반환하는 Function 인터페이스를 사용하여 반환된 R타입의 Stream을 반환 한다.

즉 Stream의 MAP은 기존의 Stream을 변경 및 새롭게 생성을 하는 역할을 한다.

List<String> vehicleColors_V1 = vehicleList.stream().map(new Function<Vehicle, String>() {
            @Override
            public String apply(Vehicle vehicle) {
                return vehicle.getColor();
            }
}).toList();
// Vehicle들의 Color 리스트

[vehicle stream을 각각의 vehicle들의 색상 stream(String type)으로 변경]

 

 

해당 표현방식은 Lamda로 표현이 가능하다.

List<String> vehicleColors = vehicleList.stream().map(vehicle1 -> vehicle1.getColor()).toList();
//또는
List<String> vehicleColors = vehicleList.stream().map(Vehicle::getColor).toList();

 

 

2.  Stream<T> filter(Predicate<? super T> predicate)

[Vehicle 리스트의 stream에서 filter매소드 호출 시 매개변수 및 반환값]

매개변수 : Predicate<Vehicle>

반환값 : Stream<Vehicle>

말로 풀어 보면 Vehicle객체를 받는 Predicate 인터페스를 통해 True/False를 판단하고, True인 Vehicle들의 Stream을 반환한다.

즉 Stream의 Filter는 기존의 Stream에서 Predicate를 사용하여 알맞는 객체들만 필터링 후, 자신과 동일한 형태의 Stream을 반환하는 역할을 한다.

List<Vehicle> blackVehicles = vehicleList.stream().filter(new Predicate<Vehicle>() {
    @Override
    public boolean test(Vehicle vehicle) {
        if (vehicle.getColor().equals("black")) return true;
        else return false;
    }
}).toList();

Map과 동일하게 해당 Lamda로 표현이 가능하다.

List<Vehicle> blackVehicles = vehicleList.stream().filter(v -> v.getColor().equals("black")).toList();

 

 

3. 마무리

기본적으로 Stream은 Map과 Filter만으로도 리스트를 다양하게 원하는 형태로 바꾸는 것이 가능하다.

//EXAMPLE
List<String> stringList2 = vehicleList2.stream().filter(v -> v.getColor().equals("blue")).map(v -> v.getVendor()).toList();

 

이 외에도 Stream은 flatMap, foreach, sorted등 다양한 기능을 제공한다. 필요에 따라서 구글링을 통해 자기가 필요한 기능을 찾아가면서 사용한다면 확실히 기존보다 편하게 리스트를 관리할 수 있다.

반응형
반응형

1. Consumer<T>

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    .....

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Consumer<T>은 T 타입의 인자를 받아 이름 그대로 특정 작업(Accept)을 수행 하는 함수형 인터페이스 이다.

 

예제

//Integer 값을 받아, 출력한는 인터페이스
Consumer<Integer> printT = new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        System.out.println("Consumer 는 리턴값이 없어요.>>>" + integer);
    }
};

//람다 표현
Consumer<Integer> printT = integer -> 
	System.out.println("Consumer 는 리턴값이 없어요.>>>" + integer);

 

특정 값을 인자로 받아 자유롭게 처리가 가능 하다.

 

사용 예제 (기타 Default 메소드)

public class SomethingConsumer implements Consumer<Integer> {
    @Override
    public void accept(Integer integer) {
        System.out.println("SomeThingConSumer Plus 10 >>>>" + integer + 10);
    }
    @Override
    public Consumer<Integer> andThen(Consumer<? super Integer> after) {
        return Consumer.super.andThen(after);
    }
}

andThen: 본체의 Apply를 적용 후, 인자로 받은 Funtion의 Apply 적용.

 

Function의 compose와 addThen처럼 실제 사용을 해보자.

SomethingConsumer plus10 = new SomethingConsumer();
Consumer<Integer> minus10 = (i) -> System.out.println("SomethingConsumer minus10 >>>> " + (i - 10));
Consumer<Integer> multiply2 = (i) -> System.out.println("SomethingConsumer Multiply *2  >>> " + (i * 2));
        
plus10.andThen(minus10.andThen(multiply2)).accept(30);

/**
RESULT
SomeThingConSumer Plus 10 >>>>40
SomethingConsumer minus10 >>>> 20
SomethingConsumer Multiply *2  >>> 60
**/

순서 대로 plus10의 accept > minus10의 accept > multiply2 의 accept가 실행되는 것을 확인할 수 있다.

 

 

2. Supplier<T>

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Suppplier<T>은 매개변수를 받지 않고 정의된 T 타입의 변수를 받환하는 추상 메소드 get()을 제공한다.

 

예제

Supplier<Integer> get10 = new Supplier<Integer>() {
            @Override
            public Integer get() {
                System.out.println("Supplier는 파라미터 없이 바로 넘겨요>>>>" + 10);
                return 10;
            }
};

Supplier<String> getString = () -> "Hello World";

System.out.println(get10.get()); // RESULT Integer 10
System.out.println(getString.get()); // RESULT "Hello World"

 

 

2. Predicate<T>

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    ....

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Predicate<T>은 매개변수 T를 받은 후, 전달된 인자를 사용하여 boolean(true, false)를 반환 하는 인터페이스를 제공 한다.

 

예제

Predicate<Integer> isEven = i -> i%2 == 0;
System.out.println(isEven.test(10)); // RESULT true
System.out.println(isEven.test(7)); // RESULT false
System.out.println(isEven.test(1)); // RESULT false
반응형
반응형
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    ......

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Funtion<T, R>은 T 타입의 인자를 받아 R타입의 인자를 반환하는 apply라는 추상 메소드를 제공한다.

 

사용 예제 (기본)

// Integer 타입의 수를 인자로 받아 +10 후, Integer 타입 반환
Function<Integer, Integer> plus10_v1 = new Function<Integer, Integer>() {
    @Override
    public Integer apply(Integer integer) {
        return integer + 10;
    }
};
//람다 표현1
Function<Integer, Integer> plus10_v2 = (i) -> i + 10;
//람다 표현2
Function<Integer, Integer> plus10_v3 = (i) -> {
	i = i + 10;
    return i;
};
plus10_v1.apply(10); // result Integer 20
plus10_v2.apply(10); // result Integer 20
plus10_v3.apply(10); // result Integer 20

이렇게 IN, OUT 인자 타입만 잘 맞춰 주면 자유롭게 사용이 가능하다.

반응형

사용 예제 (기타 Default 메소드)

Default Method란 자바8 이후 추가된 기능으로 기본 인터페이스에 추상 메소드가 추가 될 경우, 이를 상속받는 모든 Class에 해당 메소드를 구현해야 하는 이슈가 있었지만, Default 메소드는 메소드의 구현체를 제공 함으로써 기존의 추상메소드가 구현된 Class가 깨지지 않고 사용 가능하게 해주는 기능이다.

 

Default 메소드각 몇개가 있던 추상 메소드가 1개일 경우, FuntionalInterface로 간주 된다.

// 위에 Funtion 기능을 상속받아 구현한 클래스
public class Plus10 implements Function<Integer, Integer> {
    @Override
    public Integer apply(Integer integer) {
        return integer + 10;
    }

    @Override
    public <V> Function<V, Integer> compose(Function<? super V, ? extends Integer> before) {
        return Function.super.compose(before);
    }

    @Override
    public <V> Function<Integer, V> andThen(Function<? super Integer, ? extends V> after) {
        return Function.super.andThen(after);
    }
}

Funtion<T, R>에서 제공되는 Default 메소드는 compose , andThen 2가지의 Default 메소드를 제공한다.

 

compose : 인자로 받은 Funtion 의 Apply를 먼저 적용 후, 본체의 Apply 적용.

andThen: 본체의 Apply를 적용 후, 인자로 받은 Funtion의 Apply 적용.

 

이렇게만 보면... 뭔말인지 모르겠으니 예제 코드로 확인해 보자.

Plus10 plus10 = new Plus10();
// 받은 인자의 x2를 하는 Funtion
Function<Integer, Integer> multiply = integer -> integer * 2;

System.out.println(plus10.apply(10)); // result : 20
System.out.println(plus10.compose(multiply).apply(20)); // result (20 * 2) + 10 = 50
System.out.println(plus10.andThen(multiply).apply(2)); // result (2 + 10) * 2 = 24

 

첫번째 출력 값 : 인자로 받은 10에 +10을 한 값을 반환.

두번째 출력 값 : 인자로 받은 20을 Compose의 인자인 multiply의 apply(x2)를 먼저 적용 후, 본체의 apply(+10)을 적용.

세번째 출력 값 : 인자로 받은 2를 본체의 apply(+10) 을 먼저 적용 후, addThen의 인자인 multiply의 apply(x2)를 적용.

반응형
반응형

FunctionalInterface란?

함수형인터페이스(Functional Interface)는 단 하나의 추상 메소드만 가지는 인터페이스를 말한다. 다른 말로 SAM 인터페이스(Single Abstract Method Interface)라고도 한다.

 

예시

@FunctionalInterface
public interface RunSomething {
    void doIt();
    //이렇게 추상 메소드가 2개 있으면 안됨. 오류 발생
    //void doItAgain();
}

 

 

 

그래서... 이게 다인가? 왜쓰는거임?

여러 가지 이유가 있겠지만... 이유는 단하나 Lamda(함수형 프로그래밍)을 위해서 라고 생각한다.

예제를 보며 한번 확인해보자.

 

        //기존에 쓰던 방식
RunSomething runSomething = new RunSomething() {
    @Override
    public void doIt() {
        System.out.println("ki hy");
    }
};
// 람다 방식1 한줄일 경우 이렇게 쓸수 있음.
RunSomething runSomething2 = () -> System.out.println("ki hy");

// 람다 방식2 한줄 넘을 경우 저렇게 중괄호로 묶어 줌.
RunSomething runSomething3 = () -> {
     System.out.println("ki hy");
     System.out.println("ki hy2");
};
runSomething.doIt();
runSomething2.doIt();
runSomething3.doIt();

위 코드 처럼 정의된 함수형 인터페이스는 이렇게 람다식으로 표현이 가능 하다.

 

만약 인자(Arguments)가 있는 경우라면?

@FunctionalInterface
public interface RunSomeThingArguments<T,R> {
    R doSomething(T a, T b);
}


RunSomeThingArguments<Integer,String> runSomeThingArguments = new RunSomeThingArguments<Integer, String>() {
    @Override
    public String doSomething(Integer a, Integer b) {
        return Integer.toString(a+b);
    }
} ;
        
RunSomeThingArguments<Integer,String> runSomeThingArguments2 = (a, b) -> Integer.toString(a+b);
RunSomeThingArguments<Integer,String> runSomeThingArguments3 = (a, b) -> {
    //각 인자에 2배를 곱해서 더하는 경우.
    a = a * 2;
    b = b * 2;
    return Integer.toString(a + b);
};
runSomeThingArguments.doSomething(1,3); // result : String type "4"
runSomeThingArguments2.doSomething(1,3); // result : String type "4"
runSomeThingArguments3.doSomething(1,3); // result : String type "12"

 

이런식으로 사용할 수 있겠다.

 

사실 개념 자체는 간단하지만... 글쓴이도 처음 배웠을때 왜 쓰는지 언제 써야할지에 대해서 감이 안잡혔다.

결국 이러한 개념은 Java8 이후 도입된 기본 함수형 인터페이스를 사용하기 위함이라는 것을 깨달았다.

 

기본 함수형 인터페이스

public interface Function<T, R> 

public interface Consumer<T> 

public interface Supplier<T>

public interface Predicate<T>

해당 기본 함수형 인터페이스에 대해서 하나씩 알아보자.

반응형
반응형

< TrueBear >('https://sap2.tistory.com/16'이하 'TureBear')() 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.

 

1(개인정보의 처리목적)

< TrueBear >(
)가 개인정보 보호법 제32조에 따라 등록
공개하는 개인정보파일의 처리목적은 다음과 같습니다.

1. 회원 가입 및 로그인, 관리 회원 가입의사 확인과 회원제 서비스 제공에 따른 회원자격 유지 및 관리, 14세 미만 아동의 개인정보 처리 시 법정대리인의 동의여부 확인, 서비스 부정이용 방지, 각종 고지와 통지를 목적으로 개인정보를 처리합니다.

 2. 재화 또는 서비스 제공 본인 인증, 결제, 서비스 및 콘텐츠, 광고를 포함한 맞춤형 서비스 제공을 목적으로 개인정보를 처리합니다.

3. 고충처리 이용자의 신원 확인, 문의사항 확인, 사실조사를 위한 연락, 통지, 처리결과 통보의 목적으로 개인정보를 처리합니다.

4. 제휴 및 제안 제휴 및 제안 시 이용자 또는 사업자가 작성한 내용을 검토하고, 필요 시 연락을 위한 목적으로 개인정보를 처리합니다.

  • 수집방법 : 간편가입(Kakao, Google, Apple)
  • 보유근거 : 회원가입
  • 보유기간 : 서비스 종료 시

관련법령 :
[전자상거래 등에서의 소비자 보호에 관한 법률]소비자의 불만 또는 분쟁처리에 관한 기록 : 3년

대금결제 및 재화 등의 공급에 관한 기록 : 5년

계약 또는 청약철회 등에 관한 기록 : 5

 

2(정보주체와 법정대리인의 권리·의무 및 그 행사방법)

① 정보주체는 TrueBear에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.

② 제1항에 따른 권리 행사는TrueBear에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 TrueBear() 이에 대해 지체 없이 조치하겠습니다.

③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다. 이 경우 “개인정보 처리 방법에 관한 고시(2020-7)” 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.

④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4, 37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.

⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.

TrueBear() 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.




3(처리하는 개인정보의 항목 작성)

 < TrueBear >() 다음의 개인정보 항목을 처리하고 있습니다.

처리 항목 처리 목적
[필수]
SNS Profile
회원가입, 중복확인, 서비스 제공 및 상담, 본인확인, 만 14세 이상 확인, 민족도 및 설문 조사, 서비스 품질 확인 및 개선, 부정이용방지, 사고조사, 보안정책 수립
[선택]
SNS ID,
성별, 연령대, 생일
중복확인, 서비스 제공 및 상담, 본인확인, 부정이용방지, 만족도 및 설문 조사



4(개인정보의 파기)


< TrueBear > () 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.

② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.

③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.
1.
파기절차
< TrueBear >
() 파기 사유가 발생한 개인정보를 선정하고, < TrueBear > 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.

2. 파기방법

전자적 파일 형태의 정보는 기록을 재생할 수 없는 기술적 방법을 사용합니다.

종이에 출력된 개인정보는 분쇄기로 분쇄하거나 소각을 통하여 파기합니다

 

5(개인정보의 안전성 확보 조치)

< TrueBear >
() 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.

1. 개인정보 취급 직원의 최소화 및 교육
개인정보를 취급하는 직원을 지정하고 담당자에 한정시켜 최소화 하여 개인정보를 관리하는 대책을 시행하고 있습니다.

2.
내부관리계획의 수립 및 시행
개인정보의 안전한 처리를 위하여 내부관리계획을 수립하고 시행하고 있습니다.

3.
접속기록의 보관 및 위변조 방지
개인정보처리시스템에 접속한 기록을 최소 1년 이상 보관, 관리하고 있으며,다만, 5만명 이상의 정보주체에 관하여 개인정보를 추가하거나, 고유식별정보 또는 민감정보를 처리하는 경우에는 2년이상 보관, 관리하고 있습니다.
또한, 접속기록이 위변조 및 도난, 분실되지 않도록 보안기능을 사용하고 있습니다.

4.
개인정보에 대한 접근 제한
개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.

5.
비인가자에 대한 출입 통제
개인정보를 보관하고 있는 물리적 보관 장소를 별도로 두고 이에 대해 출입통제 절차를 수립, 운영하고 있습니다.

 

6(개인정보 자동 수집 장치의 설치•운영 및 거부에 관한 사항)



TrueBear
() 정보주체의 이용정보를 저장하고 수시로 불러오는 ‘쿠키(cookie)’를 사용하지 않습니다.

7 (개인정보 보호책임자)

 TrueBear () 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.

  • ▶ 개인정보 보호책임자
  • 성명 :김영호
  • 직책 :선임
  • 직급 :선임
  • 연락처 :0221518708, cto.truebear@gmail.com,

※ 개인정보 보호 담당부서로 연결됩니다.

  • ▶ 개인정보 보호 담당부서
  • 부서명 :TrueBear
  • 담당자 :김영호
  • 연락처 :0221518708, cto.truebear@gmail.com,

② 정보주체께서는 TrueBear 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. TrueBear () 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.

8(가명정보의 처리)

< TrueBear >
() 다음과 같은 목적으로 가명정보를 처리하고 있습니다.

▶ 가명정보의 처리 목적

- 통계작성, 추천, 과학적 연구, 공익적 기록보존에 활용

▶ 가명정보의 처리 및 보유기간

- 가명처리 계획 수립 시 정한 목적을 달성하는 기간(시점)까지만 보유 이용 됩니다.

▶ 가명처리하는 개인정보의 항목

- 필수, 선택적 개인정보 중에서 이용 목적에 필요한 최소한의 항목을 가명처리하고 있습니다.

▶ 법 제28조의4(가명정보에 대한 안전조치 의무 등)에 따른 가명정보의 안전성 확보조치에 관한 사항 준수

 

9(개인정보 열람청구)
정보주체는
개인정보 보호법35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
< TrueBear >
() 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.

 개인정보 열람청구 접수·처리 부서

  • 부서명 : TrueBear
  • 담당자 : 김영호
  • 연락처 : 0221518708, cto.truebear@gmail.com,

 

10(권익침해 구제방법)



정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.

1.
개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr)
2.
개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)
3.
대검찰청 : (국번없이) 1301 (www.spo.go.kr)
4.
경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)

「개인정보보호법」제35(개인정보의 열람), 36(개인정보의 정정·삭제), 37(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.

※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(www.simpan.go.kr) 홈페이지를 참고하시기 바랍니다.

11(개인정보 처리방침 변경)

 

① 이 개인정보처리방침은 2021 12 30부터 적용됩니다.

② 본 개인정보처리방침의 내용 추가, 삭제 및 수정이 있을 경우 개정 최소 7일 전에 ‘공지사항’을 통해 사전 공지를 하겠습니다. 다만, 수집하는 개인정보의 항목, 이용목적의 변경 등과 같이 이용자 권리의 중대한 변경이 발생할 경우 이용자 동의를 다시 받을 수도 있습니다.

 

반응형

'개발 > Unity' 카테고리의 다른 글

Unity Study (1) 용어 및 Unity 기본 구성 정리  (0) 2021.06.01
반응형

"레트로의 Unity 게임 프로그래밍 에센스" 책을 보고 학습한 내용 정리

Unity 기본 정리

1. 기본 화면 구성

Unity실행 후, "2 by 3"로 화면을 설정하면 위와 같은 화면이 구성된다.

Scene :  월드인 씬을 시각적으로 편집하는 창

Hierarchy : Scene에 존재하는 오프젝트들을 나열하여 보여주는 창

Inspector : 선택된 오브젝트의 정보 표시 및 수정, 추가하는 창

Game : 실행 시, 게임을 시작및 조작할 수 있는 창

Project :  해당 프로젝트에 사용할 에셋들을 표시하는 창

Console : 개발로그 및 오류로그등을 확인 할 수 있는 창

 

3.  기본 조작

기본 프로젝트에 왼쪽상단에 해당 툴바가 존재한다.(손바닥 모양, 4방향 화살표 등..)

순서대로

 * Hand Tool - 씬 카메라를 이동

 * Translate Tool - 오브젝트를 이동(오브젝트의 화살 표가 표시되는데.. 이걸로 이동하면 깔끔하게 이동 가능)

 * Roate Tool - 오브젝트를 회전

 * Scale Tool - 오브젝트 크기 조절

 * Rect Tool - UI와 2D 오브젝트 크기 조절

 * Transform Tool - Translate, Rotate, Scale 합친거 한번에

 

오브젝트 선택후 해당 기능이 활성화 된것에 따라 위치, 회전, 크기 등을 조절 할 수 있다.

단축키는 순서대로 Q W E R T Y 이다.

 

그외에 화면 이동 및 조작 기능

 - 오브젝트 선택 후, f : 해당 오브젝트 focus

 - 마우스 휠 버튼 :  화면 좌우 상하 이동

 - Alt + 마우스 왼쪽 : 화면 회전

 - 마우스 오른쪽 + (WASD 키) : 게임하듯이 화면 이동 

상황에 따라 편한걸로 사용하면 될듯

 

3. Gizmo(기즈모)

씬창에서 오브젝트 위에 표시되는 아이콘, 편집 도구, 화살표, 외곽선 등을 기즈모라고 부른다. 개발자의 편집화면에서만 표시가 되고 실제 실행화면에서는 나타나지 않는다.

4. Rigidbody Component

Rigidbody의 사전적 의미는 "강체"라는 뜻으로, 오브젝트에 해당 컴포넌트를 추가할 경우 Unity에서 제공하는 물리엔진의 기능을 사용할 수 있게 해준다.

중력, 충돌, 충격 등의 물리적 기능이 기본적으로 적용이 되므로 공중에 떠있는 오브젝트에 해당 컴포너트를 추가하기만 해도 실행 시 자동적으로 중력에 의해 바닥으로 떨어진다.

오브젝트 선택 후, inspector창에서 Rigidbody component추가
Rigidbody component

5. MonoBehaviour Class

Unity에서 기본 스크립트 생성 시, 모든 클래스들은 해당 클래스를 상속받는 것을 default로 생성된다. 자세히 공부는 하지 않았으나 Android의 Object class , View Class처럼 최상위 클래스라고 이해했다.

그리고 특이한 점을 한가지 발견했는데... 해당 클래스를 상속받은 클래스를 생성할때는 new 키워드를 사용하지 않는다 라는 것이다.

// GmaeManager, PlayerController는 MonoBehavior 클래스를 상속 받고, 필자가 생성한 클래스

GameManager gameManager = FindObjectOfType<GameManager>();

PlayerController playerController = other.GetComponent<PlayerController>();

해당 코드 처럼 new 키워드 없이 객체를 생성했다.

 

6. Start() , Update() 메소드

스크립트 생성시 기본적으로 생성되는 함수이다.

1. Start() 

객체 생성시 실행되야 하는 코드 작성 (생성자라 생각중임)

 

2. Update() 

초당 프레임 만큼 호출되는 함수. 컴퓨터는 초당 약 60회의 프레임, 영화는 초당 약 24회의 프레임을 갖는다. Update() 함수는 그러한 프레임 수만큼 호출되며 오브젝트의 움직임, 사용자의 입력을 실행하는 것. 하지만 프레임의 수는 컴퓨터의 성능 마다 다를 수 있기에 모든 상황에 따라 같은 객체라도 Update()호출 횟수가 다를 수 있다.(FPS 게임하면서 핑 조절되는게...이거 때문인듯? 아닌가?)

 

 

7.  Prefab(프리팹)

언제든지 재사용할 수 있는 게임 오브젝트를 의미한다. 예를 들어 전쟁게임을 만들며 유니티에서 K2총을 구현하였다. 이 총은 한번만 사용하는 하는 것이 아닌 전쟁게임에 등장하는 많은 군인들이 사용되어야 한다. 그렇다면 해당 총을 구현 후, 프리팹화 하여 Project에 넣고 원할때마다 Hierarchy에 가져다가 사용하면 된다.

보통 Hierarchy에서 구현 하고, project 탭으로 옮겨 프리팹화 하는듯? 프리팹화 된 오브젝트는 Hierarchy 창에 파란색으로 표시됨

 

 

8. 기타 공부내용

* Hierarchy 창 > 오브젝트 선택 > Ctrl+D : 오브젝트 복제

GameObject 는 컴포넌트 입장에서 자신이 추가된 게임 오브젝트를 가리키는 변수

GetComponent() 메소드 : 원하는 타입의 컴포넌트를 자신의 게임 오브젝트에서 찾아오는 메소드.제네릭을 통해 다양한 객체를 가저올 수 있음

 

 

추후 더 학습이 필요한 부분

Rigidbody Constraints
콜라이더

OnCollision

OnTrigger

반응형

'개발 > Unity' 카테고리의 다른 글

개인정보처리방침  (0) 2021.12.16
반응형

시작하기

첫시도

Taling(https://taling.me)을 접속하면 놀랍게도 등록된 클래스들을 전체적으로 조회 할 수 있는 메뉴가 존재 하지 않았다.

그래도 희망을 가지고 해당 사이트를 분석을 해보았다.

taling 메인 페이지

먼저 메인 페이지에 접속해서 해당 네모칸에 검색어를 넣으면 검색된 강의들이 나오는데...

[https://taling.me/Home/Search/?query=노래] >> 해당 Url로 페이지가 조회된다. 이때, [query=] 해당 쿼리뒤에 아무 문자도 적지 않으면 전체 강의가 조회된다는 것을 발견했다!!!! 역시 인생은 잔머리!

더 분석을 해본 결과 Class101때와는 다르게 하단으로 스크롤링 할때마다 강좌가 더 조회되는 것이 아니라, 하단의 페이지 넘버가 존재한다. 한 페이지에 15개의 강좌가 조회되고 다음 번호를 누를때 마다 15개씩 반복적으로 조회된다.

그 후, 이제 조회할 데이터를 분석하였다.

1. 할인률

2. 강좌명

3. 원가, 할인 가격, 강의 방식(시간당)

4. 강의 위치

5. 별점

6. 강사 정보

이러한 데이터가 조회가능하다! 자 이제 크롤링 시자아아악!!!!!!

.....

1번부터 6번까지 데이터에 강좌별 카테고리 데이터가 없다! 쓸모 없는 데이터가 되버려따.. 어떻하지....

 

두번째 시도

마음을 가다듬고 다시한번 페이지를 분석해 보았다.

상단에 커다란 대메뉴(뷰티/헬스)가 있고 그 아래 소메뉴(메이크업/퍼스트컬러/패션/셀프케어...등)이 있고 해당 소메뉴를 클릭하면 그 메뉴에 맞는 데이터가 조회된다. 즉 카테고리 별로 강좌의 조회할려면 해당 방법으로 메뉴들을 클릭하면서 조회한 후, 저장을 해야한다는 것이다.

 

자, 이제 다시 개발할 내용을 정리해 보았다.

 

1. 한페이지당 15개씩 조회되는 강좌를, 마지막 페이지 까지 자동으로 넘어가면서 데이터를 수집하는  코드

2. 상단의 존재하는 대메뉴 > 소메뉴를 자동으로 클릭 및 이동하는 소스 코드

 

1번의 경우 첫시도 시 개발을 다 했음으로... 2번만 개발 하면된다!

 

개발환경

  • MAC OS Big Sur 11.2.3
  • Python3
  • Chrome 버전 90.0.4430.93(공식 빌드) (x86_64)
  • Chrome Driver 90.0.44

개발준비

사용한 패키지 및 도구

  • selenium, BeautifulSoup, pandas
pip3 install selenium pip3 install BeautifulSoup pip3 install pandas

 

  • ChromeDriver 설치 (자신이 로컬에 설치된 Chrome버전과 동일한 버전으로...)
    ChromeDriver - WebDriver for Chrome - Downloads
    Current Releases If you are using Chrome version 91, please download ChromeDriver 91.0.4472.19 If you are using Chrome version 90, please download ChromeDriver 90.0.4430.24 If you are using Chrome version 89, please download ChromeDriver 89.0.4389.23 If you are using Chrome version 88, please
    chromedriver.chromium.org

개발내용

다시한번 알려드리지만 필자는 파이썬 개발자가 아님. 그리고... 하다가..너무 귀찮아서 코드질이 많이 많이 떨어진다... 눈갱 주의

전체 소스 코드(펼치기 시 스크롤 압박 주의 ;;;;)

더보기
# 목표 - 탈잉에 등록된 모든 강의들의 정보(클래스 명, 카테고리, like, 가격, 정보 등)
# https://taling.me/Home/Search/?query= 접속 시, 전체 강의목록을 조회할 수 있다.
# 한 화면에 15개의 강의가 나오게 되고, 하단 다음 페이지 번호를 눌러서 15개씩 조회가 가능하다.
# 따라서 현재 화면의 15강의씩 가장 마지막 페이지 번호까지 넘어가면서 저장을 해야함.
# 단순 화면의 강의 데이터로는 카테고리 분류가 불가능한 상황임 > 상단 메뉴를 옮겨 가면서 카테고리 데이터를 가져와야함.
from selenium import webdriver
import time
import re
from bs4 import BeautifulSoup
import pandas as pd
from selenium.webdriver.common.action_chains import ActionChains


#####상수#####
#뷰티/헬스	 	- sub1 , #cate2
#액티비티		- sub2 , #cate3
#라이프		    - sub3 , #cate4
#취미/공예	 	- sub4 , #cate5
#머니 			- sub5 , #cate6ㅣ,ㅣㅏ,,ㅏㅣㅡㅓㅏ mkkkkkkk
#커리어	 		- sub6 , #cate7
#디자인/영상	 - sub7 , #cate8
#외국어 		- sub8 , #cate9
# 하위 메뉴 선택시 sub로 시작하는 숫자 시작과 #cate 숫자 시작값이 달라서 이를 보정하기 위한 상수
MATCH_FOR_CATE_AND_SUB_NUMBER = 1
TEMP_SAVE_POINT = 100


# 전체 수강 강좌 조회 사이트 열기.
driver = webdriver.Chrome("./chromedriver")
driver.get('https://taling.me/Home/Search/?query=')
time.sleep(3)

pageNumber = [0,1,2,3,4,5,6,7,8,9,10,11,12]
goNextCss = '#container > div.main3_cont > div.page > a:nth-child'
moveCategoriQuery = '//*[@id="sub@subNumber"]/div/li[@childNumber]/a'
#'#sub@subNumber > div > li:nth-child(@childNumber) > a'
# str.replace('@subNumber', '8')
# str.replace('@childNumber', '1')

result = [ ]
subList = []
cateList = []

currentSubName = ''
currentSubCate = ''


######################## 전체 메뉴 및 메뉴 이동 관련 로직 ###########################

#메뉴 클릭 로직 정리
# action = ActionChains(driver)
# driver.find_element_by_css_selector('#cate2').click() # 큰 메뉴 클릭 한 후
# action.move_to_element(driver.find_element_by_css_selector('#cate2')).perform() # 마우스 올리고 
# driver.find_element_by_css_selector('#sub1 > div > li:nth-child(4) > a').click() # 소메뉴 클릭

#전체 메뉴 리스트 (큰 subList) 가져오기
def getSubMenuList():
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	_subList = soup.select('li.cate > a > div')
	numOfSub = len(_subList)
	for i in range(numOfSub):
		subList.append(_subList[i].text.strip())
	print(subList)

def setcurrentSubName(idx):
	currentSubCate = subList[idx]


# 전체 소메뉴? 카테고리 조회
def getCategori():
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	sublen = len(subList)
	for i in range(sublen) :
		cate = soup.select('#sub' + str(i) + ' > div > li')
		catelen = len(cate)
		for j in range(catelen):
			cateList.append(cate[j].text)
	print(cateList)

#소메뉴 갯수 가져오기
def getLenCategoriInSub(subIdx):
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	cate = soup.select('#sub' + str(subIdx) + ' > div > li')
	return len(cate)


#대메뉴 이동
def clickSubMenu(idx):
	global currentSubName
	try:
		driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).click() # 큰 메뉴 클릭 한 후
	except:
		saveFile()
	time.sleep(0.5)
	####메뉴위로 마우스 이동 하기
	action = ActionChains(driver)
	action.move_to_element(driver.find_element_by_css_selector('#cate' + str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER))).perform() # 마우스 올리기
	currentSubName = driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).text
	print('**************************************  currentSubName>>>>>>>  ' + currentSubName)
	time.sleep(0.5)

#소메뉴 이동
def clickCate(subNum, cateNum):
	global currentSubCate
	base = moveCategoriQuery
	base = base.replace('@subNumber', str(subNum))
	base = base.replace('@childNumber', str(cateNum))
	print('Move Cate Query>>>>>>>>>>>>>   ' + base)
	try:
		currentSubCate = driver.find_element_by_xpath(base).text
		print('currentSubCate >>>>>>  '+ currentSubCate)
	except:
		saveFile()
	
	try:
		driver.find_element_by_xpath(base).click()
	except:
		saveFile()

	time.sleep(0.5)

def saveFile():
	global result
	_result = pd.DataFrame(result)
	_result.to_csv('/Users/kimyoungho/python/taling.csv', index=False, encoding="utf-8-sig")



################################################데이터 수집 소스 코드 #############################################

#아래 3개는 존재할 수도 없을 수도 있으니 예외처리 필수임.
# soup.select('div.cont2 > div')[0].select('div.day')[0].text  '1DAY수업'
# soup.select('div.cont2 > div')[0].select('div.d_day')[0].text.strip()    'D-50'
# soup.select('div.cont2 > div')[0].select('div.soldoutbox')[0].text.strip() 'SOLDOUT'
# soup.select('div.cont2 > div')[0].select('div.sale')[0].text	'50%할인'
# soup.select('div.cont2 > div')[0].select('span.reward_badge')[0].text   '30일 무료코칭'  , 리뷰 100건
# soup.select('div.cont2 > div')[0].select('div.title')[0].text.strip()   '[재무설계/저축/재테크/투자/주식] 미래설계를 위해 "돈"을 알려드립니다'
# soup.select('div.cont2 > div')[0].select('div.name')[0].text  '나종길'   이름
# soup.select('div.cont2 > div')[0].select('div.nick')[0].text    '미래설계사'  닉네임


####가격 데이터
#  soup.select('div.cont2 > div')[0].select('div.price1')[0].text.strip()   '\n₩11,000\n'
#  soup.select('div.cont2 > div')[0].select('div.price2')[0].text.strip()   '\n₩5,500\n'
#  soup.select('div.cont2 > div')[0].select('div.price > div.sale')[0].text.strip()   '50%'
#   soup.select('div.cont2 > div')[0].select('span.hour_unit')[0].text  '/시간'

#########평점 평가수 지역
#  soup.select('div.cont2 > div')[0].select('div.info2 > div.star')[0].text.strip()  '★★★★★'
#  soup.select('div.cont2 > div')[0].select('div.info2 > div.review')[0].text.strip()   '(26)'
# soup.select('div.cont2 > div')[0].select('div.info2 > div.location')[0].text.strip()   '온라인 Live'



# 현제 화면에 15개의 강의데이터 뽑는 쿼리.
#soup.select('div.cont2 > div')
def setClassList(driver, result) :
	global currentSubCate
	global currentSubName
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	classList = soup.select('div.cont2 > div')
	numOfClass = len(classList)

	classPeriod = ''
	classDday = ''
	classSale = ''
	classReward = ''
	classSoldout = ''
	classTitle = ''
	lectureName = ''
	lectureNick = ''

	originPrice =''
	salePrice =''
	saleRate = ''
	classunit = ''

	classStar = ''
	classReview = ''
	classLocation = ''

	for i in  range(numOfClass) :
		try:
			classPeriod = classList[i].select('div.day')[0].text
		except:
			classPeriod = ''
		
		try:
			classDday = classList[i].select('div.d_day')[0].text.strip()
		except:
			classDday = ''
		
		try:
			classSoldout = classList[i].select('div.soldoutbox')[0].text.strip()
		except:
			classSoldout = ''
		
		try:
			classSale = classList[i].select('div.sale')[0].text
		except:
			classSale = ''

		try:
			classReward = classList[i].select('span.reward_badge')[0].text
		except:
			classReward = ''

		try:
			classTitle = classList[i].select('div.title')[0].text.strip() 
		except:
			classTitle = ''
		
		try:
			lectureName = classList[i].select('div.name')[0].text
		except:
			lectureName = ''

		try:
			lectureNick = classList[i].select('div.nick')[0].text
		except:
			lectureNick = ''

		try:
			originPrice = classList[i].select('div.price1')[0].text.strip()
		except:
			originPrice = ''

		try:
			salePrice = classList[i].select('div.price2')[0].text.strip()
		except:
			salePrice = ''

		try:
			saleRate = classList[i].select('div.price > div.sale')[0].text.strip()
		except:
			saleRate = ''

		try:
			classunit = classList[i].select('span.hour_unit')[0].text
		except:
			classunit = ''

		try:
			classStar = classList[i].select('div.info2 > div.star')[0].text.strip()
		except:
			classStar = ''

		try:
			classReview = classList[i].select('div.info2 > div.review')[0].text.strip()
		except:
			classReview = ''

		try:
			classLocation = classList[i].select('div.info2 > div.location')[0].text.strip()
		except:
			classLocation = ''
		result.append([currentSubCate, currentSubName, classTitle, lectureName, lectureNick, classPeriod, classDday, classSale, classReward, originPrice, salePrice, saleRate, classunit, classStar,classReview, classLocation, classSoldout])
		
		# result.append(classList[i])
	print(len(result))
	global TEMP_SAVE_POINT
	if len(result) > TEMP_SAVE_POINT:
		print('temp save >>>>>>>>>>>>')
		saveFile()
		TEMP_SAVE_POINT = TEMP_SAVE_POINT + 1000


# 하단에 화면 번호를 클릭하는 쿼리
# 첫화면은  a:nth-child(2) 의 번호가 2 > 3 > 4 .... > 10 > 11(11은 다음으로)
# 그 다음부터는 "이전 버튼이 활성화 되어서" 3 > 4 > 5 > 6 ... > 11 > 12 (12는 다음으로)
# 위와 같은 형식으로 조회가 돠어야 함.
# 다음 조회목록으로 이동하기 위해 클릭하는 로직.
# driver.find_element_by_css_selector('#container > div.main3_cont > div.page > a:nth-child(2)').click()
def nextList(idx,driver):
	try:
		nextButton = driver.find_element_by_css_selector(goNextCss + '(' + str(pageNumber[idx]) + ')')
	except:
		return True
	nextButton.click()
	time.sleep(2)
	return False

def getBottomPageNum() :
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	return len(soup.select('div.page > a'))

def startCrawling():
	pageNum = getBottomPageNum()
	if pageNum < 11 :
		for i in range(2, 2 + pageNum): 
			setClassList(driver, result)
			nextList(i,driver)
	else :
		#첫페이지만 따로 10번 돌린다... 인덱스가 달라서
		#TODO 처음 페이지 수가 
		for i in range(2, 12): 
			setClassList(driver, result)
			nextList(i,driver)
		goNextCssNumber = 3
		while(1):
			setClassList(driver, result)
			if(nextList(goNextCssNumber,driver)) :
				break
			else :
				goNextCssNumber = goNextCssNumber + 1
				if(goNextCssNumber > 12) :
					goNextCssNumber = 3	

	print("Crawling Complete....")
	# time.sleep(2)



##################################### Main Code ##################################################


getSubMenuList()
getCategori()
numOfSumList = len(subList)
for subMenuNum in range(1, numOfSumList):
	print('NumOf MENU>>>>>>>>>>' + str(subMenuNum))
	cateLen = getLenCategoriInSub(subMenuNum)
	clickSubMenu(subMenuNum)
	for cate in range(1, 1 + cateLen):
		clickCate(subMenuNum, cate)
		startCrawling()
saveFile()





 

간략하게 로직을 ... 아니 절대 간략하지 않다!

1. 탈잉에 접속한다.

2. 대 메뉴 클릭 > 대 메뉴 위에 마우스 오버

3. 소 메뉴 클릭

4. 15개의 강좌 크롤링 > 다음 페이지 넘기기 반복

5. 다음 소 메뉴 or 대 메뉴 클릭

 

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 이것만 보면 아주 단순해 보인다...하지만 ㅠㅠㅠㅠㅠㅠ 쉽지 않았다. 아니 솔직히 어렵지는 않았는데.. 노가다가 엄청 났다.

[2. 대 메뉴 클릭 > 대 메뉴 위에 마우스 오버] 해당 로직의 경우 좀 설명이 필요한게... 

두 이미지를 봐보자. 차이는 첫 번째 화면은 필터 기능을 제공한다는 것이고 두번째 이미지는 소 메뉴가 보인다는 것이다.

마우스가  대메뉴 위에 있을때 Drop Down으로 소메뉴가 화면에 보이게 되고 다른 곳을 클릭하면 필터링 기능을 제공한다.

이때 문제가 발생하는것이.. 우리는 소메뉴를 클릭을 해야한다. 하지만 소메뉴가 화면에 나타나지 않을 경우 

"selenium.common.exceptions.ElementNotInteractableException: Message: element not interactable"

이러한 오류에 마딱트리고 만다. 즉 화면에 보이는 Element만 클릭이 가능하다는 것이다. 그러기 위해서는 클릭 전에 대메뉴에 마우스를 올려서 소메뉴가 보이도록 해야한다는 말이다..그러한 로직을 구현한게 아래 부분이다.

#대메뉴 이동
def clickSubMenu(idx):
	global currentSubName
	try:
		driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).click() # 큰 메뉴 클릭 한 후
	except:
		saveFile()
	time.sleep(0.5)
	####메뉴위로 마우스 이동 하기
	action = ActionChains(driver)
	action.move_to_element(driver.find_element_by_css_selector('#cate' + str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER))).perform() # 마우스 올리기
	currentSubName = driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).text
	print('**************************************  currentSubName>>>>>>>  ' + currentSubName)
	time.sleep(0.5)

그리고 메뉴 이동의 경우 버튼들의 Xpath를 분석해 보니 아래 규칙으로 움직이는 것을 파악했다.

moveCategoriQuery = '//*[@id="sub@subNumber"]/div/li[@childNumber]/a'
# str.replace('@subNumber', '8') 해당 데이터를 replace해가면서 이동
# str.replace('@childNumber', '1') 해당 데이터를 replace해가면서 이동

 

실행결과

탈잉 크롤링

크롤링 하는 로직을 제외하고 화면 이동 하는 것만 동영상으로 찍어 봤다. 해당 방식으로 메뉴를 이동하면서 데이터를 가져오는 것이다.

나도 처음 해보는거라서...진짜 신기하긴 했다. 이렇게 크롤링을 하니 수집한 데이터를 대메뉴 > 소메뉴 별로 나눌 수 있었다.

 

마무리

점점...크롤링 실력이 늘어가는 것은 착각인가...?

이번 탈잉 사이트를 크롤링하면서 다른 사이트보다 훨씬 강좌수가 많이 있다는것에 놀랐다. 다른 사이트보다 강좌들이 온라인 보다 오프라인 위주라는 것, 원데이 클래스가 많이 있다는 것도 알게 되었다. 그리고...이제 또 그 많은 데이터들로 어떠한 유의미한 정보를 파악할 수 있을지도 스터디 중(사실 이것부터 시작이다) 

사이트를 보니 약 6500개의 강좌가 있는거 같은데... 내가 크롤링한 데이터는 6000개 정도 이다..(500개 어디 갔지???)

이번에 작성한 프로그램 정확도가 100프로도 아니고, 코드 질도 많이 떨어 지지만... 그래도 결과 자체는.. 나름 너무 뿌듯 >_< 헤헤

시간 날때 천천히 코드 질도 올리고 사라진 500개의 데이터도 찾아 봐야겠다... 찡긋 >_o

반응형

'개발 > Python' 카테고리의 다른 글

[Python] Class101 Crawling  (0) 2021.05.18
[Python] Instagram Crawling  (0) 2021.05.13

+ Recent posts