스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택 가능하게 기능을 제공.

 

스트림 map

List<Game> games = Arrays.asList(
	new Game("CALL 1", 20000)
	, new Game("DIA 1", 30000)
	, new Game("DIA 2", 40000)
	, new Game("DIA 3", 50000)
);

객체 데이터를 생성 후에 간단하게 리스트 요소들의 게임명을 가져오는 map에 예제.

List<String> gameNameList = games.stream()
				.map(Game::getName)
				.collect(toList());
		
gameNameList.stream().forEach(name -> System.out.println(name));

만약에 각 요소들의 게임명의 길이가 필요하다면 아래와 같이 체이닝 처리할 수 있다.

List<Integer> gameNameLengthList = games.stream()
				.map(Game::getName)
				.map(String::length)
				.collect(toList());
		
gameNameLengthList.stream().forEach(itemLength -> System.out.println(itemLength));

 

결과값

6
5
5
5

 

스트림 flatMap

메서드 map을 이용해 리스트의 중복을 제거한 각 단어의 길이를 반환한다고 하자.

아래와 같이 작성한다면 리턴 값은 Stream<String[]>이 된다.

각각의 스트림으로 반환이 되는 것이다.

List <String> words = Arrays.asList("Goodbye", "World");
		
List<String[]> uniqueCharList = words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());

하나의 스트림으로 반환하고 싶다면 어떻게 해야 할까?

아래와 같이 작성해도 결국 Stream 리스트가 반환된다.

 

List<Stream<String>> uniqueCharList = words.stream()
		.map(word -> word.split(""))
		.map(Arrays::stream)
		.distinct()
		.collect(toList());​

flatMap을 사용하면 각 배열을 스트림이 아니라 스트림의 콘텐츠로 맵핑한다.

앞의 예제와는 다르게 하나의 스트림을 반환한다.

List<String> uniqueCharList = words.stream()
		.map(word -> word.split(""))
		.flatMap(stream -> Arrays.stream(stream))
		.distinct()
		.collect(toList());
		
uniqueCharList.stream().forEach(System.out::println);

 

결과값

GodbyeWrl

 

'Java > Stream' 카테고리의 다른 글

기본형 특화 스트림  (0) 2022.10.27
리듀싱  (0) 2022.10.26
검색과 매칭  (0) 2022.10.26
필터링 및 슬라이싱  (0) 2022.10.10
정의 및 특징  (0) 2022.10.03

스트림 필터링

스트림에서 지원하는 filter 메서드 사용.

프리디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환.

List<Integer> numArr = Arrays.asList(1,2,1,3,3,3,2,4,5);
numArr.stream()
	.filter(i -> i == 3)
	.forEach(System.out::println);

 

요소 중에 3인 요소들이 있으면 출력.

여기서 중복을 제거하는 고유 요소 필터링을 하고 싶다면 스트림에서 지원하는 distinct 메서드를 사용한다.

고유 여부는 스트림에서 만든 객체의 hashcode, equals로 결정.

List<Integer> numArr = Arrays.asList(1,2,1,3,3,3,2,4,5);
numArr.stream()
	.filter(i -> i == 3)
	.distinct()
	.forEach(System.out::println);

 

스트림 슬라이싱

프리디케이트를 이용한 슬라이싱, 스트림 요소 무시, 축소 등 다양한 방법으로 요소를 선택하거나 스킵할 수 있다.

아래 예제는 단순히 게임 목록에 대한 가격 필터링을 한 예제이다.

List<Game> games = Arrays.asList(
	new Game("CALL 1", 20000)
	, new Game("DIA 1", 30000)
	, new Game("DIA 2", 40000)
	, new Game("DIA 3", 50000)
);
List<Game> filteredGame = games.stream()
			.filter(game -> game.getPrice() < 40000)
			.collect(toList());

만약 filter 메서드의 요소를 만족했을 때 작업을 중단하고 싶다면 takeWhile, dropWhile을 사용하면 된다.

사용하지 않는 경우 모든 요소만큼 작업을 수행하므로 요소 목록 크기가 크다면 그만큼 낭비가 된다.

 

takewhile 메서드

프레디케이트가 처음으로 참이 되는 지점까지 발견된 요소를 반환.

List<Game> takeWhileGame = games.stream()
		.takeWhile(game -> game.getPrice() < 40000)
		.collect(toList());

 

dropwhile 메서드

프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버림.

List<Game> dropWhileGame = games.stream()
		.dropWhile(game -> game.getPrice() < 40000)
		.collect(toList());

 

limit 메서드

주어진 값 이하의 크기를 갖는 새로운 스트림을 반환.

List<Game> dropWhileGame = games.stream()
		.dropWhile(game -> game.getPrice() < 40000)
       		.limit(1)
		.collect(toList());

 

skip 메서드

처음 N개 요소를 제외한 스트림을 반환.

List<Game> dropWhileGame = games.stream()
		.dropWhile(game -> game.getPrice() < 40000)
       		.skip(2)
		.collect(toList());


위에서 작성한 예제들의 요소 값들을 출력해보면 어떻게 동작하는지 짐작할 수 있습니다.

filteredGame.stream().forEach(game -> System.out.println(game.getName()));
Sytem.out.println();
takeWhileGame.stream().forEach(game -> System.out.println(game.getName()));
Sytem.out.println();
dropWhileGame.stream().forEach(game -> System.out.println(game.getName()));

 

결과 값

CALL 1
DIA 1

CALL 1

DIA 1

 

'Java > Stream' 카테고리의 다른 글

기본형 특화 스트림  (0) 2022.10.27
리듀싱  (0) 2022.10.26
검색과 매칭  (0) 2022.10.26
map과 flatMap  (0) 2022.10.17
정의 및 특징  (0) 2022.10.03

정의

스트림이란?

데이터 처리 연산을 지원하는 추출된 연속된 요소.

자바 8 컬렉션에 stream 메서드 및 java.util.steam.Stream 에 인터페이스 정의가 추가되었다.

 

특징

1. 주제

컬렉션과 마찬가지로 스트림은 특정 요소 형식의 연속된 값 집합 인터페이스 제공.

컬렉션은 주제가 데이터이면 스트림은 데이터의 계산입니다.

 

2. 소스

스트림은 컬렉션, 배열, I/O 자원 등 데이터 제공 소스로부터 데이터를 소비.

정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지.

 

3. 연산

일반적인 함수형 프로그래밍 언어 및 DB 비슷한 연산 기능 지원.

filter, map, reduce, find, match, sort 등 데이터를 조작. 

연산을 순차적, 병렬적으로 실행 가능.

 

4. 파이프라이닝

스트림 연산은 연산 끼리 연결하여 파이프 라이닝 구성 가능.

 

5. 내부 반복

외부에서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복 지원.

 

스트림과 컬렉션

1. 데이터 계산

컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 구조.

반면 스트림은 이론적으로는 요청할 때만 요소를 계산하는 구조.

 

2. 탐색

반복자와 마찬가지로 스트림도 소비되고 나면 반복 사용할 수가 없습니다.

 

3. 데이터 반복 처리

컬렉션은 요소를 사용자가 for 문 등을 사용하여 직접 요소를 꺼내어 연산하고 다시 요소를 넣는 외부 반복을 이 필요.

반면 스트림은 반복은 내부에서 알아서 처리하고 어떤 연산을 할지만 지정하면 처리가 가능.

 

스트림 연산

1. 중간 연산

filter나 sorted 같은 중간 연산은 다른 스트림을 반환.

중간 연산을 연결하여 파이프 라이닝 가능.

 

2. 최종 연산

스트림 파이프라인에서 List, Integer 등 스트림 이외의 결과를 반환.

 

비교 예제

기존 외부 반복을 통한 연산과 스트림을 통한 연산을 비교해보기 위한 간략한 예제.

1. Game 이라는 게임명, 가격을 가진 객체를 생성.

2. 30,000원 초과하는 가격의 게임의 게임명 리스트를 반환.

public class Game {
	private String name;
	private int price;
	
	public Game(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;
	}
}

 

 

package demo;

import java.util.ArrayList;
import java.util.List;
import static java.util.stream.Collectors.toList;

public class Main {
	public static void main(String[] args) {
		
        // 1. 데이터 초기화
		List<Game> gameList = new ArrayList<>();
		gameList.add(new Game("Hell Gate", 39900));
		gameList.add(new Game("Heaven Gate", 49900));
		gameList.add(new Game("DIA 1", 10000));
		gameList.add(new Game("DIA 2", 20000));
		gameList.add(new Game("DIA 3", 30000));
		
        // 2. 외부반복을 통한 처리
		List<String> highPriceGameList = new ArrayList<>(gameList.size()); 
		
		for(Game game : gameList) {
			if (game.getPrice() > 30000) {
				highPriceGameList.add(game.getName());
			}
		}
		System.out.println(highPriceGameList);
	}
}

 

package demo;

import java.util.ArrayList;
import java.util.List;
import static java.util.stream.Collectors.toList;

public class Main {
	public static void main(String[] args) {
		
        // 1. 데이터 초기화
		List<Game> gameList = new ArrayList<>();
		gameList.add(new Game("Hell Gate", 39900));
		gameList.add(new Game("Heaven Gate", 49900));
		gameList.add(new Game("DIA 1", 10000));
		gameList.add(new Game("DIA 2", 20000));
		gameList.add(new Game("DIA 3", 30000));
		
        // 스트림 연산을 통한 처리
		List<String> highPriceGameList = gameList.stream()
							.filter(game -> game.getPrice() > 30000)
							.map(Game::getName)
							.limit(gameList.size())
							.collect(toList());
		System.out.println(highPriceGameList);
	}
}

 

마치며

비교적 간단한 예제라서 코드 길이는 큰 차이가 없어 보이지만 파이프 라이닝과 외부 반복 연산 없이

내부 연산을 통해 가독성이 좋게 처리가 가능하다.

 

'Java > Stream' 카테고리의 다른 글

기본형 특화 스트림  (0) 2022.10.27
리듀싱  (0) 2022.10.26
검색과 매칭  (0) 2022.10.26
map과 flatMap  (0) 2022.10.17
필터링 및 슬라이싱  (0) 2022.10.10

람다 표현식?

람다 표현식은 간략하게 표현하면 익명 함수의 단순화입니다.

 

특징

1. 익명(이름 없음)으므로 작성할 코드량이 줄어듭니다.

2. 특정 클래스에 종속되지 않으므로 메서드 대신 함수라고 부르지만

    메서드 처럼 파라미터 리스트, 바디, 반환 형식, 예외 리스트를 포함합니다.

3. 람다 표현식은 메서드 인수로 전달하거나 변수에 저장 가능합니다.

 

기존 익명함수 사용한 코드

new Thread(new Runnable() {

    @Override
    public void run() {
        System.out.println("Thread Start");		
    }
}).start();
Comparator<Integer> num = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
};

람다 표현식을 사용한 코드

new Thread(() -> System.out.println("Thread Start")).start();
Comparator<Integer> ramDaExpNum1 = (o1, o2) -> o1.compareTo(o2);
Comparator<Integer> ramDaExpNum2 = (Integer o1, Integer o2) -> o1.compareTo(o2);

람다 파라미터는 위와 같이 생략이 가능합니다.

 

함수형 인터페이스

함수형 인터페이스는 오직 하나의 추상 메서드만 가지는 인터페이스입니다.

public class Main {
	public static void main(String[] args) {
    		Concat c = (a, b) -> System.out.println(a+b);	
		c.concat("홍", "길동");
		c.concat("11월", "11일");
    	}
}

@FunctionalInterface 어노테이션

interface에 추가해주면 컴파일 시점에 해당 인터페이스가 규칙을 지키는지 검증합니다.

@FunctionalInterface
public interface Concat {
	void concat(String a, String b);
}

 

기본 제공 함수형 인터페이스 

Interface Descripter Abstract Method
Predicate T -> boolean boolean test(T t)
Consumer T -> void void accept(T t)
Supplier () -> T T get()
Function<T, R> T -> R R apply(T t)
Comparator (T, T) -> int int compare(T o1, T o2)
Runnable () -> void void run()
Callable () -> T V call()

 

함수형 인터페이스 사용

함수형 인터페이스의 추상 메서드의 시그니처는 람다 표현식의 시그너처를 나타냅니다.

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터(descriptor)라고 하고

추상 메서드의 시그니처와 함수 디스크립터가 같다면 함수형 인터페이스를 활용 가능합니다.

 

public class Main {
	public static void main(String[] args) {
		Runnable r1 = () -> System.out.println("Runnable" + 1);
		Runnable r2 = () -> System.out.println("Runnable" + 2);
		print(r1);
		print(r2);
	}
	
	public static void print(Runnable r) {
		 r.run();
	}
}

위와 같이 자주 변경되는 요구사항이 있다면 매번 메서드를 생성할 것이 아니라 변경하는 부분만 람다 함수로 만들어

활용이 가능합니다.

 

메서드 참조

메서드 참조를 사용하여 가독성을 높일 수 있습니다.

 

메서드 참조를 사용하지 않은 코드

public class Main {
	public static void main(String[] args) {
		List<String> list = Arrays.asList("a","b","A","B");
		list.sort((a, b) -> a.compareToIgnoreCase(b));
		for(String str : list) {
    		System.out.print(str);
		}
  	}
}

 

메서드 참조를 사용 코드

public class Main {
	public static void main(String[] args) {
		List<String> list = Arrays.asList("a","b","A","B");
		list.sort(String::compareTo);
		list.stream().forEach(System.out::print); // 스트림 사용
  	}
}

 

지역변수의 사용

외부에 정의된 변수를 사용할 수 있습니다. 이를 람다 캡쳐링이라고 합니다.

public class Main {
	public static void main(String[] args) {
		int num = 12345;
		Runnable r = () -> System.out.println(num);
        // 아래 주석 해제 시 에러 발생.
        // num = 123;
  	}
}

여기서 지역 변수는 final로 선언하거나 final 변수처럼 사용해야 합니다.

람다가 실행되는 스레드에서 변수를 할당한 스레드가 해제되었는데도 접근하려 할 수 있기 때문에

이 같은 제약이 있다고 합니다.

 

 

영속성 컨텍스트

번역하면 엔티티를 영구 저장하는 환경으로 논리적인 개념입니다.

엔티티 매니저에 의해 관리되며, DB와 애플리케이션 사이에서 엔티티의 일종의 저장소 같은 역할.

 

엔티티의 생명주기

  • 비영속(New/Transient)
    영속성 컨텍스트와 관계가 없는 상태.
  • 영속(Managed)
    영속성 컨텍스트에 저장된 상태
  • 준영속(Detached)
    영속성 컨텍스트에 저장되었다가 분리된 상태

  • 삭제(Removed)
    삭제된 상태

 

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();

Member member = new Member();
member.setId(11L);
member.setName("Jack");
/* 여기까지는 비영속 */
em.persist(member); // 영속성 컨텍스트 관리 시작
em.clear(); // 준영속 상태로 영속성 컨텍스트 삭제
em.remove(member); // 비영속 상태로 영속성 컨텍스트와 DB 삭제

 

영속성 컨텍스트 관리의 장점

  • 1차 캐시
    영속성 컨텍스트에 저장해놓았다가 트랜잭션이 커밋되기 전까지 DB를 조회하지 않고 캐시에 있는 값을 조회합니다.
    비즈니스 로직이 매우 복잡할 경우 약간의 이득이 있을 수 있지만 트랜잭션 내에서 일어나므로 효과는 크지 않을 수 있습니다.

  • 동일성 보장
    같은 트랜잭션 안에서 엔티티의 비교의 == 연산자를 통해 비교할 수 있습니다.

  • 트랜잭션을 지원하는 쓰기 지연
    트랜잭션 커밋 전까지 INSERT SQL을 쓰기 SQL 저장소에 모았다가 전송할 수 있습니다.

  • 변경 감지
    엔티티를 수정하면 스냅샷과 비교하여 변경된 부분이 있는 경우
    업데이트 SQL을 쓰기 SQL 저장소에 저장했다가 트랜잭션 커밋 시 DB에 전송합니다.

플러시

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영.

영속성 컨텍스트의 내용들을 DB와 동기화한다고 이해하면 됩니다.

이름 때문에 오해할 수 있지만 삭제하거나 비우는 작업이 아닙니다.

 

스냅샷과 비교해서 수정된 엔티티를 찾고, 수정된 엔티티에 대해 쿼리를 생성하여 쓰기 지연 SQL 저장소에 등록합니다.
쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송합니다.

 

플러시 하는 경우

  • flush() 메소드 명시적 호출.
  • 트랙잭션 커밋할 경우
  • JPQL 쿼리 실행할 경우

참고

자바 ORM 표준 JPA 프로그래밍 / 김영한 저자

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

JPA (Java Persisitence API) - JPA 소개 (1)  (0) 2022.03.21

1. 소개

JAVA 진영의 ORM 표준.

스프링 기반의 실무에서는 Spring Data JPA를 주로 사용하여 개발하며 Hibernate 구현체를 대부분 많이 사용합니다.

JPA, Spring Data JPA, Hibernate 관계에 대해 조금 쉽게 설명하면 Hibernate는 JPA 구현체이고

Spring Data JPA는 Spring 프레임워크에서 JPA를 다루기 쉽게 구현된 것이라 생각하면 됩니다.

Spring Data JPA는 Hibernate만을 구현체로 사용하지 않고 여러 다른 구현체를 선택할 수 있습니다.

하지만 대부분 Hibernate를 구현체로 사용하고 있기때문에

이런 관계에 대해서 많이 혼동이 있는 분들이 많은 것 같습니다.

JPA, Spring Data JPA, Hibernate 가 혼동된다면 아래 이미지를 보며 어느 정도 관계에 대해서 이해가 쉬울 것 같습니다.

 

 

2. ORM(Object-relational mapping) 

객체와 관계형 데이터베이스를 맵핑하는 기술.

객체는 객체대로 설계하고 RDBMS는 RDBMS대로 설계하고 ORM 프레임워크가 중간에서 맵핑.

 

3. 기존 SQL 중심 개발의 문제점

public Class User {
	private String userId;
    private String userName;
    private String email;
    private UserGroup userGroup;
}
public Class UserGroup {
	private String groupId;
    private String name;
}

두 개의 참조 관계의 객체들이 있다면 조인하여 데이터를 얻을 수 있는

SQL을 조회 개발이 필요하고 SQL문의 조회 결과를 각각의 두 개의 객체에 결과를 다음과 같이 맵핑해줘야 합니다.

SELECT U.*, UG.*
  FROM USER U JOIN USER_GROUP UG ON U.groupId = UG.groupId
public User findUser(String userId) {
	// SQL 실행 결과를 아래의 객체에 세팅
    User user  = new User();
    user.setUserName(name);
    		...
    UserGroup userGroup = new UserGroup();
    user.setGroupName(name);
    		...
    // 회원과 그룹의 관계 세팅
    user.setUserGroup(userGroup);
    return user;
}

 

 

이런 맵핑 작업이 계속 필요하기 때문에 생산성이 떨어지다 보니 하나의 객체로 처리할 수 있게 DTO를 만들어서

조회 결과에 대해 setter/getter 처리하는 경우 많습니다.

그런데 이런 경우 SQL 문에 SELECT 필드에 따라서 데이터가 있을 수도 없을 수도 있는 경우가 발생하게 됩니다.

그러다 보니 일반적으로 MVC model을 사용하여 계층적으로 분리를 많이 하지만 논리적으로는 얽혀있는 상태가 되면서

SQL 문까지 전부 확인하기 전까지는 슈퍼 DTO(객체)의 데이터가 없어서 안 들어간 것인지 필요가 없어서 뺀 것인지

신뢰하기 어렵기 때문에 여기서 또 개발에 생산성과 유지보수가 점점 어려워지게 됩니다

문제는 객체지향 모델링할수록 이런 맵핑 작업이 늘어나게 되고

그러다 보니 SQL 문에 맞춰서 객체를 설계하는 경우가 많아지게 되면서 생산성은 떨어지게 되는 악순환이 반복됩니다.

또 다른 경우를 확인해보면 예를 들어 어떤 기능을 개발한다고 가정하면 관련 CRUD SQL에 생성이 필요하고

이렇게 생성한 상태에서 DB 테이블에 칼럼 하나가 추가된다고 하면 관련된 SQL 문 모두 수정 필요합니다.

번거롭고 생산성이 떨어지는데 혹시 실수로 하나라도 추가나 수정해야 할 필드를 SQL문에서

놓치게 되면 에러가 발생하게 됩니다.

 

4. JPA를 활용한 해결

이런 문제들에 대해서는 JPA는 솔루션이될 수 있습니다.

별도의 SQL 문 작성 없이 객체 관계 설정을 통해 SQL 문을 생성해주므로 단순히 JPA에 정의한 CRUD 메서드를 

사용하여 생산성을 높일 수 있습니다.

지연 로딩 기능을 활용해 조인 관계 놓인 객체 데이터라고 해도 객체가 조회되는 시점에 SQL을 생성/조회할 수 있고

만약 항시 채워진 데이터가 필요할 경우 옵션 설정을 통한 즉시 로딩 기능으로 데이터를 모두 하나의 SQL을 생성/조회할 수 있습니다.  이런 기능을 활용하여 맵핑을 위한 복잡한 추가 코딩을 줄일 수 있습니다.

모든 부분이 직접적인 SQL 작성 없이 처리할 수 없는 경우를 처리하기 위한 SQL과 유사한 JPQL를 사용하여 조회할 수 있게 만든 기능도 있습니다.

 

5. 마치며

간단하게 개념과 기존 SQL 중심 개발의 문제점과 JPA에 어떤 해결방법이 있는지 확인해보았습니다.

다음 글에서는 영속적 콘텍스트와 4번 항목에서 말한 지연 로딩 같은 솔루션에 대한 예제를 살펴보겠습니다.

 

※ 참고

https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html

https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/ 

자바 ORM 표준 JPA 프로그래밍 / 김영한 저자

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

JPA (Java Persisitence API) - JPA 영속성 관리 (2)  (0) 2022.05.17

+ Recent posts