반응형

groupingBy

groupingBy 는 Java Stream collect 메서드에서 사용하는 Collector 객체이자 특정 속성(property) 값에 의해서 그룹핑을 짓는 메서드이다. 결과값으로 항상 Map<K, V> 형태를 리턴하며 아래와 같이 최대 3가지 파라미터를 받을 수 있다. 두 가지 인수만 사용된다면 classifier 와 downstream 만 사용한다.

  1. classifier (Function<? super T, ? extends K>): 분류 기준을 정의한 함수
  2. mapFactory (Supplier<M>): 결과 값 Map<K,V> 를 다른 Object 로 맵핑하는 함수
  3. downStream (Collector<? super T,A,D>): 집계 방식을 변경하는 또 다른 Collector 객체로 결과 값 Map<K,V> 에서 V 의 타입을 변경

자세한 사항은 아래 공식 DOCS 문서를 참고한다.

Collectors (Java Platform SE 8 ) (oracle.com)

 

Collectors (Java Platform SE 8 )

Returns a Collector implementing a "group by" operation on input elements of type T, grouping elements according to a classification function, and returning the results in a Map. The classification function maps elements to some key type K. The collector p

docs.oracle.com

 

groupingBy 예제

아래와 같이 Person 타입이 있다고 가정할 때 다양한 groupingBy 예제를 살펴보고 정리한다.

@AllArgsConstructor
@Setter
public static class Person {
        private String name;
        private String city;
        private Integer age;
        private PersonJob personJob;
}

public static class PersonTuple {
        private String name;
        private String city;

        public PersonTuple(String name, String city) {
            this.name = name;
            this.city = city;
        }
}

 

private static List<Person> getPersons() {
        return List.of(
            new Person("안유진", "서울", 20, PersonJob.ARMY),
            new Person("리즈", "서울", 22, PersonJob.STUDENT),
            new Person("레이", "도쿄", 19, PersonJob.STUDENT),
            new Person("가을", "수원", 18, PersonJob.EMPLOYEE),
            new Person("장원영", "수원", 20, PersonJob.POLICE),
            new Person("이서", "청주", 18, PersonJob.POLICE)
        );
}

 

단일 키로 그룹핑하기

var result = getPersons().stream().collect(groupingBy(Person::getCity));

Map<String, List<Person>> 객체로 리턴되고, 수원, 서울, 청주, 도쿄 키로 Person 객체로 그룹핑된다.

 

복합 키로 그룹핑하기

public static class PersonTuple {
        private String name;
        private String city;

        public PersonTuple(String name, String city) {
            this.name = name;
            this.city = city;
        }
}
var result2 = getPersons().stream().collect(groupingBy(person -> new PersonTuple(person.getName(), person.getCity())));

Map<PersonTuple, List<Person>> 객체로 리턴되고, 위에 정의된 Tuple 로 Person 객체가 그룹핑된다.

 

집계 변경해서 그룹핑하기 (toSet)

var result3 = getPersons().stream().collect(groupingBy(Person::getPersonJob, toSet()));

groupingBy 를 적용했을 때 기본 Value 타입은 리스트다. 위 예제처럼 toSet 로 집계함수를 변경하면,

Map<Person, Set<Person>> 객체로 리턴된다.

 

집계 변경해서 통계 그룹핑하기 (sum)

단순히 타입을 변경하는 것 이외에 reduce 같은 함수로 통계 데이터를 얻어올 수 있다.

var result4 = getPersons().stream().collect(groupingBy(Person::getPersonJob, summingInt(Person::getAge)));

downStream 방식을 summingInt 메서드로 합계를 낸 예제이다. 직업을 기준으로 Person 객체를 그룹핑했을 때 그 객체 나이들을 전부 더해서 산출한다. 리턴 타입은 Map<Person, Integer>> 이다.

 

Map 의 Value 값을 다른 타입으로 리턴하기

var result5 = getPersons().stream().collect(groupingBy(Person::getPersonJob, mapping(Person::getName, joining(",", "[", "]"))));

groupingBy 의 downStream 을 통해 Map 의 value 값을 다른 타입으로 리턴할 수 있다. 위 처럼 기본 컬렉션 타입은 toSet, toList, toMap, toConcurrentMap, toCollection 으로 static 메서드를 제공하지만 그 이외의 타입은 mapping 메서드를 통해 다른 타입을 변경할 수 있다. mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream) 원형을 살펴보면 groupingBy 의 2개 인자를 받는 함수 원형과 같다. 똑같이 Key 를 분류하는 함수와 value 를 집계하기 위한 함수를 제공하면 된다.

 

Map<PersonJob, String> 객체로 리턴된다. Value 가 집계될 때 "[" "]" 로 감싸서 값들을 , 콤마로 묶는다.

 

Map 을 다른 타입으로 리턴하기

var result6 = getPersons().stream().collect(groupingBy(Person::getPersonJob, () -> new EnumMap<>(PersonJob.class), toList()));

EnumMap<PersonJob, List<Person>> 객체로 리턴된다. 세 가지 인수를 받을 때는 두 번째 인자가 MapFactory 이고 세 번째 인자가 downstream 이다. mapFactory 가 Supplier 이므로 인자가 없는 함수형 인터페이스를 제공해야 하므로 () -> new 형식으로 새로운 Map 타입으로 리턴하면 된다.

반응형

'JAVA' 카테고리의 다른 글

[JAVA] 타입 토큰과 슈퍼 타입 토큰이란?  (0) 2022.10.10
반응형

1. 자바 제네릭의 한계?

자바 제네릭은 클래스에서 사용할 타입을 외부(사용부)에서 사용하게 해주는 일반적인 기법을 의미한다.

이러한 제네릭을 사용하면, 타입만 다르고 공통된 기능을 가지고 있는 클래스나 메서드들에 파라미터로 타입을 외부에서 전달받아 조금 더 범용적인 코드를 만들 수 있다. 이러한 제네릭이 없었다면, 자바 객체의 최고 조상인 Object 로 전달받아 객체 타입에 따라 캐스팅해 로직을 별도로 만들어야 되는데 이러면 오히려 런타임 시 예기치 못한 오류가 더 많이 발생하고 별도의 타입으로 만들어둔 클래스 및 함수들과 다를 바 없다.

 

public class Generic {
    public List<String> list = new ArrayList<>();

    public void addString(String str) {
        list.add(str);
    }

    public String getString(int index) {
        return list.get(index);
    }
}

// 제네릭이 아니라면, 이렇게 Object 로 전달
public class NonGeneric {
    public List list = new ArrayList();

    public void addString(Object obj) {
        list.add(obj);
    }

	// 원하는 타입으로 캐스팅 해 반환
    public String getString(int index) {
        return (String) list.get(index);
    }
}

 

그런데 자바 제네릭은 다른 언어의 제네릭과 달리 다른 점이 존재한다.

제네릭 도입 이후, 컴파일 타임 때까지는 타입 안전성을 얻을 수 있었지만, Type Erasure 기능으로 인해 컴파일 이후에는 타입이 소거가 되기 때문이다. 반쪽 제네릭이라고 불리우는 이유이다.

 

Class Test<T> {
	private T value;
    
    public Test(T t) {
    	this.value = t;
    }
    
    public T get() {
    	return value;
    }
}

// -> 컴파일 이후
Class Test {
	private Object value;
    
    public Test(Object t) {
    	this.value = t;
    }
    
    public Object get() {
    	return value;
    }
}

 

컴파일 타임 이후에 Object 로 변환이 된 것을 볼 수 있다. 컴파일 때까지만 타입 안전성을 제공하고, 그 이후에는 Object 로 변환이 되기 때문에 런타임 시에는 오류가 발생할 수도 있다.

 

자바 진영에서는 동세대 언어인 C# 과 다르게 보급률이 좋아서 현업 프로젝트들이 월등히 많았고 이러한 프로젝트들을 지키고자 하위 호완성을 위해 Type Erasure 를 도입했다고 한다. 컴파일 타임 때 Type Erasure 를 수행하면서 제네릭이 없던 하위 버전들과 동일한 형태로 class 파일을 생성할 수 있었다.

 

2. 타입 토큰의 등장

자바언어 개발자였던 Neal Gafter는 JAVA JDK5에 generics를 추가할 때 java.lang.Class 가 generic type이 되도록 변경했다고 한다. 예를 들어, String.class 의 Type 이 Class<String> 되도록 만들어 주었다. 그리고 이를 타입 토큰이라고 불렀다.

 

클래스 리터럴과 타입 토큰의 차이

클래스 리터럴은 어떤 클래스인지 파라미터로 전달되는 정보로 타입 토큰의 클래스 정보이다. 타입 토큰과 동일한 개념으로 타입 토큰은 클래스의 타입을 명시하고, 클래스 리터럴은 그 타입 토큰과 매칭되는 매개변수로 보면 된다.

 

// 선언부 (타입 토큰)
void myMethod(Class<?> clazz) {
	...
}
  
// 사용부 (클래스 리터럴)
myMethod(String.class);

 

이 타입 토큰은 컴파일 타임 이후에도 타입에 대한 안전성을 확보하고자 사용된다.

제네릭만 사용했을 때, Operation 이나 타입들의 메서드들이 동일해야 사용할 수 있다는 한계가 있긴 하지만 그래도 동일한 기능을 한 코드로 만들 수 있어 좋다. 그런데 원하는 타입으로 반환은 안 된다. 아까 위에서 언급한 것처럼 타입이 모두 Object 로 변환되기 때문에 타입 캐스팅이 되지 않는다. 이 때 타입 토큰이 유용하게 사용된다.

 

// 선언부
public <T> T readValue(String content, Class<T> valueType) {
	...
}

// 사용부
ProductDto productDto = objectMapper.readValue(jsonString, ProductDto.class);

 

위 예제처럼, valueType 으로 클래스 타입을 매개변수로 전달해주면 json 을 읽어 그 타입으로 반환해줄 수 있다.

조금 더 나아가 THC (Typesafe Heterogenous Container) Pattern 으로 사용될 수 있다.

 

// 선언부
public class SimpleTypeSafeMap {
	private Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> k, T v) {
    	map.put(k, v);
    }
    public <T> T get(Class<T> clazz) {
    	return clazz.cast(map.get(clazz));
    }
}

// 사용부
simpleTypeSafeMap.put(String.class, "테스트");
simpleTypeSafeMap.put(Integer.class, 1);

String string1 = simpleTypeSafeMap.get(String.class);
Integer integer1 = simpleTypeSafeMap.get(Integer.class);

 

타입 토큰을 키로 사용해 타입끼리 저장하고 다시 그 타입 토큰으로 캐스팅해서 가져올 수 있다. 타입 안전 이종 컨테이너라고 불리우며 영어로는 THC (Typesafe Heterogenous Container) 라고 한다. 컨테이너가 타입을 체크해서 컴파일 여부를 결정하는 것이 아니라 타입 토큰을 가지고 프로그래머가 직접 핸들링하는 개념이다.

 

3. 슈퍼 타입 토큰의 등장

타입 토큰의 한계

이 타입 토큰에도 한계는 있다. 제네릭 타입의 클래스 리터럴과 타입 토큰은 존재하지 않는다. Type Erasure 때문에 <> 내부에 있는 타입들이 제거되기 때문이다. 조금 더 쉽게 생각해보면 이중 삼중... 다중 타입에 대한 정보들을 가지고 있지 않아서 그렇다. 이러한 타입 토큰의 한계를 극복한 개념이 슈퍼 타입 토큰이다.

 

슈퍼 타입 토큰

다중 타입에 대한 정보들을 리터럴로 표현할 수 있으면 타입 토큰처럼 안전성을 확보할 수 있을 것이다.

Class.getGenericSuperclass() 와 ParameterizedType.getActualTypeArguments() 를 사용해서 전체 타입에 대한 정보와 실제 타입을 가져올  수 있다.

 

class Super<T> {}
class MyClass extends Super<List<String>> {}

MyClass myClass = new MyClass();

Type typeOfGenericSuperclass = myClass.getClass().getGenericSuperclass();

// ~~~$1Super<java.util.List<java.lang.String>> 출력됨
System.out.println(typeOfGenericSuperclass);

 

Super 껍데기 클래스의 파라미터로 해당 타입을 전달하면 getGenericSuperclass 메서드로 전체 타입에 대한 정보를 가져올 수 있다. Class (Java Platform SE 8 ) (oracle.com)

 

Class (Java Platform SE 8 )

Determines if the specified Class object represents a primitive type. There are nine predefined Class objects to represent the eight primitive types and void. These are created by the Java Virtual Machine, and have the same names as the primitive types tha

docs.oracle.com

 

getGenericSuperclass 메서드에 대한 내용을 번역해보면 다음과 같다.

해당 객체의 상위 클래스 타입을 반환하는 메서드. 만약 상위 클래스가 매개변수화된 타입이라면 소스코드에서 사용된 실제 타입들을 반영해 반환해야 한다. 매개변수화된 타입에 대한 내용은 별도의 문서를 확인하라고 적혀 있다.(ParameterizedType은 제네릭 타입을 가지고 있는 타입을 말한다.)

ParameterizedType (Java Platform SE 8 ) (oracle.com)

 

ParameterizedType (Java Platform SE 8 )

ParameterizedType represents a parameterized type such as Collection<String>. A parameterized type is created the first time it is needed by a reflective method, as specified in this package. When a parameterized type p is created, the generic type declara

docs.oracle.com

 

제네릭 타입이라면, getActualTypeArguments 메서드를 통해 이 제네릭의 실제 타입을 가져올 수 있다.

 

class Super<T> {}
class MyClass extends Super<List<String>> {}

MyClass myClass = new MyClass();

Type typeOfGenericSuperclass = myClass.getClass().getGenericSuperclass();

// ~~~$1Super<java.util.List<java.lang.String>> 출력됨
System.out.println(typeOfGenericSuperclass);

// 수퍼 클래스가 ParameterizedType 이므로 ParameterizedType으로 캐스팅 가능
// ParameterizedType의 getActualTypeArguments()으로 실제 타입 파라미터의 정보를 구함
Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];

// java.util.List<java.lang.String>가 출력됨
System.out.println(actualType);

 

슈퍼 타입 토큰 예제

// 선언부
public abstract class TypeReference<T> {
    private final Type type;

    protected TypeReference() {
    	Type superClassType = getClass().getGenericSuperclass();
        if (superClassType instanceof ParameterizedType) {
        	this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
        } else {
            throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보가 있어야 합니다.");
        }
    }

    public Type getType() {
        return type;
    }
}

// 사용부
public class TypeSafeMap {
    private final Map<Type, Object> map = new HashMap<>();

    public <T> void put(TypeReference<T> k, T v) {
        map.put(k.getType(), v);
    }

    public <T> T get(TypeReference<T> k) {
        final Type type = k.getType();
        final Class<T> clazz;
        if (type instanceof ParameterizedType) {
            clazz = (Class<T>) ((ParameterizedType) type).getRawType();
        } else {
            clazz = (Class<T>) type;
        }
        return clazz.cast(map.get(type));
    }
}

 

Class.getGenericSuperclass() 와 ParameterizedType.getActualTypeArguments() 를 활용해서 실제 RawType 를 가져온다.

 

4. 프레임워크의 슈퍼 타입 토큰

이렇게 매번 직접 만들기에는 수고스럽다. 프레임워크에서도 이런 슈퍼 타입 토큰을 지원한다.

 

Spring 의  ParameterizedTypeReference

Spring 프레임워크에서도 동일하게 런타임 시 발생하는 타입 안정성 문제를 해결하기 위해 ParameterizedTypeReference라는 클래스를 만들었다. 매개변수화된 타입에 대한 정보를 가져올 때 사용하면 된다. 클래스 리터럴을 전달하는 대신 아래와 같이 사용하면 된다. new ParameterizedTypeReference<List<User>>()

 

spring-framework/ParameterizedTypeReference.java at main · spring-projects/spring-framework (github.com)

 

GitHub - spring-projects/spring-framework: Spring Framework

Spring Framework. Contribute to spring-projects/spring-framework development by creating an account on GitHub.

github.com

 

앞에서 살펴본 구현방식과 유사하다. 보통 다른 서버에서 자원을 가져올 때 사용한다고 한다.

ObjectMapper 에서는 ParameterizedTypeReference 를 사용하지 않고 직접 위에서 소개한 것처럼 구성하여 만들고, Spring Cloud feign 에서는 자체적으로 타입을 넘겨주지 않더라도 Reflection 기능을 통해 확인한다고 한다.

반응형

'JAVA' 카테고리의 다른 글

[JAVA] Stream Collector groupingBy  (0) 2023.02.26
반응형

Spring 에서 Bean 으로 등록할 수 있는 @Bean, @Component, @Controller, @Service, @Repository 등 어노테이션들은 그 구현 클래스 내부에 변수를 생성해서는 안 된다. Bean 객체들은 기본 정책 상 싱글톤 객체이기 때문에 그렇다.

 

서로 다른 요청에 따라 쓰레드 별로 스택 메모리 영역을 차지한다고 하더라도, 이미 힙 영역에 Bean 객체가 로딩되어 그 로딩된 객체를 공유하기 때문에 상태 변수가 내부에 있다면 그 상태 변수가 의도하지 않은 상태로 실행이 될 수 있다.

 

따라서, Bean 객체는 메소드만 공유해야 하며, Bean 객체의 상태값을 유지하게 만드는 필드 값이 없는 무상태(Stateless)로 설계해야한다.

 

그런데 직접 설계해보니 결국 스프링은 대량의 요청을 받는 프레임워크이기 때문에 공유하는 오브젝트가 있을 수도 있다. 예를 들어 해당 요청의 서비스를 해야할지 말아야 할지 결정하는 전역변수 같은 경우이다. 되도록이면 사용하지 말아야 겠지만...

 

그 대신 레이스 컨디션이 되지 않도록 multi-thread 환경에 안전한 자료구조와 알고리즘을 사용하자.

 

참고

[스프링] 싱글톤 방식의 주의점 - 스프링 빈은 항상 무상태(stateless)로 설계하자 (tistory.com)

반응형

'JAVA > Spring' 카테고리의 다른 글

[Spring Boot] Auto-Configuration  (0) 2022.09.23
반응형

Spring Boot 에서는 Spring 모듈들의 설정들을 자동으로 셋팅해주어 일일히 구현하지 않더라도 빠르게 사용할 수 있다.

이를 Auto-Configuration 이라고 한다. 예를 들어, 서버의 IP 나 포트 설정 정보 등을 받기 위해 클래스를 구현하고 외부 파일에서 값을 받아와 셋팅을 해야 하는데 이러한 절차를 각자 코딩하여 셋팅하면 설정 파일들도 중구난방이 되고 유지보수 하기가 불편해진다. 다행히도 Spring Boot 에서는 외부에서 값을 설정할 수 있는 방법을 통일했다.

 

Spring Boot 가 애플리케이션을 구동할 때 자동으로 로딩하여 참조하는 파일이 applicatioin.properties 이다.

이 application.properties 에 정의된 형식에 맞게 key 와 value 를 셋팅하면, @Value("${변수 이름}") 어노테이션으로 가져와서 사용할 수 있다. 그런데 내가 작성한 프로퍼티가 아니라 써드 파티 모듈의 프로퍼티라면 이야기가 달라진다.

 

모듈이기 때문에 현재 프로젝트의 application.properties 파일에 정의되어 있지도 않고, 공식 문서도 옛날 것이라면 어디서 이러한 정보를 찾아야 될까? 정답은 아래 spring-projects/spring-boot 프로젝트 github 소스코드를 직접 살펴보아야 한다.

 

써드 파티 모듈에서 application.properties 설정 값 찾는 방법

 

spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure at main · spring-projects/spring-boot (github.com)

 

GitHub - spring-projects/spring-boot: Spring Boot

Spring Boot. Contribute to spring-projects/spring-boot development by creating an account on GitHub.

github.com

 

예를 들어, rsocket 관련 spring boot 설정 값들을 보고 싶다면, 위 github 에서 rsocket 관련 소스코드를 찾아 @ConfigurationProperties 어노테이션이 선언된 클래스를 확인하면 된다.

 

 

@ConfigurationProperties(" 속성 이름 ") 에서 첫 번째 속성을 찾고, 클래스 내부에 변수들을 콤마 뒤에 붙여 설정 값을 기입하면 된다. 이 예제에서는 @NestedConfigurationProperty 어노테이션이 추가로 붙어 있으므로 두 번째 속성을 기입해야 Server 클래스 내부의 변수들을 설정 값으로 사용할 수 있다. 사용 방법은 아래와 같다.

 

spring.rsocket.server.port = Integer 형식

spring.rsocket.server.address = InetAddress 형식

spring.rsocket.server.transport = RSocketServer.Transport 형식

spring.rsocket.server.mappingPath = String 형식

반응형

'JAVA > Spring' 카테고리의 다른 글

[Spring] Bean 내부에는 변수를 생성해서는 안 된다.  (0) 2022.10.04
반응형

Reactor Reactive Streams 구현체이다.

프레셔가 가능한 PUB/SUB 모델로 싱글 스레드에서 이벤트 처리를 하는 이벤트 처리 프로세스이다.

( 여담이지만, NodeJS 따라하는 것처럼 보이지만 사실 Spring 진영에서 먼저 나왔다고 한다. 관심이 없었을 )

 

Reactor 이해하려면 Reactive Streams PUB/SUB 모델의 디자인 패턴을 살펴보아야 한다.

 

 

 

Application 에서 Publisher 객체와 Subscriber 객체를 생성하고 Publisher.subscribe 메서드를 호출한다.

Publisher 에서는 생성된 데이터를 포장하여 Subscription 객체를 생성하고, subscribe 메서드에서 전달받은 Subscriber onSubscribe 메서드를 호출하여 생성된 Subscription 객체를 인자로 넘긴다. Publisher 역할은 여기서 끝이다.

마디로 Publisher 정보를 생성하여 포장하고 그것을 구독자와 연결시켜주는 중매자이다.

 

onSubScribe 메서드에서는 인자로 넘어온 Subscription request 호출하여 데이터들을 요청된 크기만큼 가져온다.

( 프레셔 기능이다.) 요청된 크기만큼 데이터를 넘겨줄 때까지 데이터들을 하나씩 SubScriber onNext 메서드를 출하고 넘겨주었다면 onComplete 메서드를 호출한다. 자세한 내용은 아래 동영상을 참고하자.

 

반응형
반응형

Supplier 인터페이스

 

도입 시기 JAVA 8
분류 함수형 인터페이스
함수 원형 @FunctionalInterface
public interface Supplier<T>
필드 X
메서드 T get()
특징 T 타입을 반환하는 함수를 정의하고 get 메서드를 통해 결과를 리턴하는 메서드 하나만 가지고 있다.

 

Intellij 에서 정의부분 캡처

 

사용 예제

public class Test {
    private int id;
    private String value;
    public Test(int id, String value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public String getValue() {
        return value;
    }
}

public class Example {

    public static void main(String[] args) {

        Supplier<Test> testSupplier = ()-> new Test(0, "Hello");
        Test result = testSupplier.get();
        System.out.println(result.getId() + ", " + result.getValue());
    }
}

 

Why? 어디에 쓰이는가?

함수형 인터페이스를 왜 사용하는 것인가에 대한 질문과 동일하다.

함수형 인터페이스는 1개의 추상 메서드를 가지고 있는 인터페이스를 의미한다. 자바의 람다 표현식이 이 함수 인터페이스로만 사용 가능하다. (람다 함수는 익명 함수로 함수를 간단하게 만드는 표현식이다.)

 

함수를 변수화할 수 있다는 의미이다. 변수로 다룰 경우 직접 계산하지 않아도 된다는 장점이 있다. (Lazy Evaluation)

 

다음과 같이 여러 변수를 체크하는 함수가 있는데 "func(T a, T b, T c)", c 라는 변수가 고비용이 발생하는 함수를 거쳐 반환이 된다고 가정하자. func 함수에서는 a 와 b 를 체크해서 c 라는 값을 계산할지 결정한다고 하면 c 라는 변수를 실제로 사용하지 않는 경우의 수도 있을 것이다. 보통 순차적으로 실행되기 때문에 c 라는 고비용 함수를 매번 거쳐야 되서 성능 이슈에 문제가 있을 수도 있다. 아래 예제를 살펴보자.

 

public class NumberWorld {
    private static void negativeTest(int number, String expression) {
        if (number < 0) {
            System.out.println("This Value is Negative Written by" + expression);
        } else {
            System.out.println("This Value is Positive");
        }

    }
    private static String expressionProcessing() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "gold-egg";
    }
    public static void main(String[] args) {
        negativeTest(-1, expressionProcessing());
        negativeTest(1, expressionProcessing());
        negativeTest(2, expressionProcessing());
    }
}

 

위 예제 negativeTest 함수는 number 가 양수일 때 고비용 함수 expressionProcessing 의 결과 값인 expression 을 사용하지 않아도 된다. 그럼에도 불구하고 매번 expressionProcessing 함수가 호출되는 것을 볼 수 있다. 실제 필요한 구간에만 실행하게 해준다면 더 빠른 속도로 처리할 수 있을 것이다. 이런 개념을 Lazy Evaluation 라고 한다.

 

Supplier 를 활용하여 Lazy Evaluation 을 적용하면 다음과 같이 작성할 수 있다.

 

public class NumberWorld {
    private static void negativeTest(int number, Supplier<String> expressionSupplier) {
        if (number < 0) {
            System.out.println("This Value is Negative Written by" + expressionSupplier.get());
        } else {
            System.out.println("This Value is Positive");
        }

    }
    private static String expressionProcessing() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "gold-egg";
    }
    public static void main(String[] args) {
        negativeTest(-1, () -> expressionProcessing());
        negativeTest(1, () -> expressionProcessing());
        negativeTest(2, () -> expressionProcessing());
    }
}

 

Supplier와 Lazy Evaluation 에 대해 더 자세히 알고 싶으면 아래 동영상 링크를 확인하자.

반응형
반응형

오늘의 공부는 백기선님의 @OneToMany 양방향 관계 쿼리 문제입니다.

원본 영상은 아래 링크를 참조해주세요~

 

 

@Entity
@Getter @Setter
public class Book {
	@Id @GeneratedValue
    private Integer id;
    
    private String isbn;
    
    private String Title;
    
    @ManyToOne
    private BookStore bookstore;
}

@Entity
@Getter @Setter
public class BookStore {
	@Id @GeneratedValue
    private Integer id;
    
    private String name;
    
    @OneToMany(mappedBy="bookStore")
    private Set<Book> books = new HashSet<>();
    
    void add(Book book) {
    	this.books.add(book);
    }
}

@Test
public void contextLoads() {
	BookStore bookStore = new BookStore();
    bookstore.setName("시애틀 책방");
    bookStoreRepository.save(bookStore);
    
    Book book = new Book();
    book.setTitle("JAP 공부 좀 하면서 쓰세요");
    
    bookStore.add(book);
    bookRepository.save(book);
}

책 테이블에 연관관계가 설정되지 않은 모습

Q . 왜 책 테이블에 연관관계가 설정되지 않았는가?

양방향 관계의 기본 문제이다. 테이블과 객체는 본질적으로 다르다.

테이블은 foreign key 가 어디 있던지 각 테이블에서 join 에서 조회할 수 있지만, 객체는 알 수 없다.

그래서 양방향 관계 설정 시 연관관계의 주인이 누구인지(FK 키를 누가 가지고 있는지) 알려주어야 한다.

 

mappedBy 속성으로 상대편 테이블이 주인이라는 것을 알려준다.

관계의 주인인 쪽에서 관계가 설정이 되어야 한다. 그래야 데이터베이스에 제대로 반영이 된다.

 

그래서 public void add(Book book) 메서드 내에 book.setBookStore(this); 를 삽입하면 문제 해결이 된다.

 

반대 코드인 getBooks().add(book) 도 객체 지향적인 관점에서 보았을 때 당연히 해주어야 한다.

데이터베이스에 적용할 때는 아무런 작용이 되지 않지만 객체에서도 값을 적용해주어야 하기 때문에 당연히 해야 한다.

반응형

'JAVA > JPA' 카테고리의 다른 글

[JPA][백기선] @ManyToOne 단방향 관계 쿼리 문제  (0) 2022.04.09
반응형

오늘의 공부는 백기선님의 @ManyToOne 단방향 관계 쿼리 문제입니다.

원본 영상은 아래 링크를 참조해주세요~

 

 

Q . 왜 테스트 코드에서는 select 쿼리가 한 개만 발생하고 Controller 코드에서는 3개가 발생하는가?

테스트 코드의 트랜잭션의 범위는 testSelect 함수이다.

@DataJpaTest 어노테이션이 @Transactional 어노테이션을 가지고 있는데 이는 public 메서드들에 트랜잭션 단위를 부여하는 것과 동일하다. 즉 testSelect 함수 전체가 하나의 트랜잭션이다.

트랜잭션 하나에 Persistence Context 를 가지고 있으므로 save 를 할 때마다 1차 cache 에 저장된다.

그러므로 1차 cache 에서 바로 꺼낼 수 있기 때문에 다시 select 할 필요가 없다.

 

그런데 Controller 코드에서는 findAll 하나만 트랜잭션 범위이다.

기본 전략이 EAGER 이므로 Team 정보도 같이 가져올 수 밖에 없다.

Q . Member 정보만 쿼리 한 번으로 가져오려면 어떻게 해야 하는가?

LAZY 라는 Fetch 옵션을 사용하는 방법을 생각해 볼 수 있다.

Fetch 전략은 처음 Member 라는 정보를 조회해올 때 Team 을 참조하는 코드가 있지 않는 이상 Team 에 대한 데이터를 조회해오지 않는다. Team 에 대한 정보를 조회하는 순간이 오면 그 때 Team 에 대한 select 쿼리가 발생한다.

 

그러나, findAll 메서드는 객체를 JSON 형태로 변환해서 전달해 주는데 프록시인 객체를 JSON 으로 변환하려고 하니까 오류가 발생한다. LAZY 옵션을 사용하면 Team 에 대한 레퍼런스가 ByteBuddyInterceptor 라는 프록시 객체로 변경되기 때문이다. DTO 를 사용해서 modelmapper 로 다시 맵핑하면 된다.

 

댓글 중에 interface 를 사용하는 방법이 있다고 한다. projection!

Q . Member 와 Team 정보를 쿼리 한 번으로 가져오려면 어떻게 해야 하는가?

JPQL 에서 fetch join 을 사용하면 간단하게 해결이 될 것 같은데... JPQL 을 사용하지 않은 것을 전제로 하는 것 같다.

 

(공부해보니...)

fetch join 은 다음과 같은 단점도 존재한다. 같은 테이블을 조회하는 경우지만 어떤 테이블을 같이 조회하느냐에 따라 중복되서 쿼리를 작성하기 때문에 비추천한다고 한다.

 

@NamedEntityGraph 를 사용해서 Join 쿼리를 유도한다.

엔티티 그래프 기능은 엔티티 조회 시점에 연관된 엔티티들을 함께 조회하는 기능이다.

@NamedEntityGraph(name="엔티티 그래프 이름", attributeNodes={@NamedAttributeNode("함께 조회할 속성")})

반응형

'JAVA > JPA' 카테고리의 다른 글

[JPA][백기선] @OneToMany 양방향 관계 쿼리 문제  (0) 2022.04.09

+ Recent posts