ZoneId

시간대.  java.time.ZoneId 클래스에 대해 알아보겠습니다.

지역 ID는 {지역}/{도시}로 구성됩니다. 

다음 주소에서 각 정보들을 확인 가능합니다. (https://www.iana.or/time-zones)

 

ZoneDateTime

지정한 시간대에 상대적인 시점으로 LocalDateTime과 ZoneId가 합쳐진 개념이라고 보면 됩니다.

 

ZoneOffset

런던 그리니치 0도 자오선과 시간 값의 차이를 표현할 수 있는 기능들을 제공하는 클래스입니다.

 

예제를 통해서 사용 예시를 확인해보겠습니다.

import java.time.*;
import java.util.TimeZone;

public class Main {
	public static void main(String[] args) {
		ZoneId romeZone = ZoneId.of("Europe/Rome");
		ZoneId seoulZone = ZoneId.of("Asia/Seoul");
		
		ZoneId zoneId = TimeZone.getDefault().toZoneId();
		
		ZonedDateTime zdt1 = date.atStartOfDay(seoulZone);
		LocalDateTime dateTime = LocalDateTime.of(2022, Month.DECEMBER, 22, 23, 45);
		ZonedDateTime zdt2 = dateTime.atZone(seoulZone);
		Instant instant = Instant.now();
		ZonedDateTime zdt3 = instant.atZone(seoulZone);
		
		System.out.println(zdt1);
		System.out.println(zdt2);
		System.out.println(zdt3);

		ZoneOffset newYork = ZoneOffset.of("-05:00");
		OffsetDateTime daOffsetDateTime = OffsetDateTime.of(dateTime, newYork);
		System.out.println(daOffsetDateTime);
    }
}

출력 값

2022-10-22T00:00+09:00[Asia/Seoul]
2022-12-22T23:45+09:00[Asia/Seoul]
2022-11-28T20:54:25.808109400+09:00[Asia/Seoul]
2022-12-22T23:45-05:00

※ UTC = 협정 세계시, GMT(그리니치 표준시)

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

TemporalAdjusters와 DateTimeFormatter  (0) 2022.11.28
Instant, Duration, Period  (0) 2022.11.28
LocalDate, LocalTime, LocalDateTime  (0) 2022.11.28

TemporalAdjusters는 복잡한 날짜 조정 기능에 사용하는 기능을 제공하는 클래스입니다.

다음 예제를 통해 사용 예시를 알아보겠습니다.

 

import java.time.*;
import java.time.temporal.ChronoUnit;
import static java.time.temporal.TemporalAdjusters.*;

public class Main {
	public static void main(String[] args) {
    	LocalDate date1 = LocalDate.of(2022, 11, 21);
		LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
		LocalDate date3 = date1.with(lastDayOfMonth());
		LocalDate date4 = date1.with(firstDayOfNextMonth());
		
		System.out.println(date1);
		System.out.println(date2);
		System.out.println(date3);
		System.out.println(date4);
    }
}

출력 값

2022-11-21
2022-11-27
2022-11-30
2022-12-01

 

TemporalAdjusters 클래스의 팩토리 메서드

메서드 설명
dayOfWeekInMonth 서수 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환.
firstDayOfMonth 현재 달의 첫번째 날짜를 반환하는 TemporalAdjuster를 반환.
firstDayOfNextMonth 다음 달의 첫번째 날짜를 반환하는 TemporalAdjuster를 반환.
firstDayOfNextYear 내년의 첫번째 날짜를 반환하는 TemporalAdjuster를 반환.
firstDayOfYear 올해의 첫번째 날짜를 반환하는 TemporalAdjuster를 반환.
firstInMonth 현재 달의 첫번째 요일에 해당하는 날짜를 반환하는
TemporalAdjuster를 반환.
lastDayOfMonth 현재 달의 마지막 날짜를 반환하는 TemporalAdjuster를 반환.
lastDayOfNextMonth 다음 달의 마지막 날짜를 반환하는 TemporalAdjuster를 반환.
lastDayOfNextYear 내년 마지막 날짜를 반환하는 TemporalAdjuster를 반환.
lastDayOfYear 올해 마지막 날짜를 반환하는 TemporalAdjuster를 반환.
lastInMonth 현재 달의 마지막 요일에 해당하는 날짜를 반환하는 
TemporalAdjuster를 반환.
nextOrSame 현재 날짜 이후로 지정한 요일이 처음으로 나타나는
날짜는 반환하는 TemporalAdjuster를 반환.
previousOrSame 현재 날짜 이후로 지정한 요일이 이전으로 나타나는
날짜는 반환하는 TemporalAdjuster를 반환.

 

DateTimeFormatter

포매팅과 파싱 전용 java.time.format 패키지 중 DateTimeFormatter 클래스입니다.

정적 팩토리 메서드와 상수를 이용해 쉽게 포매터를 만들 수 있습니다.

예제를 통해 사용 예시를 확인해보겠습니다.

 

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import static java.time.temporal.TemporalAdjusters.*;

public class Main {
	public static void main(String[] args) {
		String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
		String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
		LocalDate ld1 = LocalDate.parse("20221121", DateTimeFormatter.BASIC_ISO_DATE);
		LocalDate ld2 = LocalDate.parse("2022-11-21", DateTimeFormatter.ISO_LOCAL_DATE);
				
		System.out.println(s1);
		System.out.println(s2);
		
		System.out.println(ld1);
		System.out.println(ld2);
		
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
		LocalTime colonTime = LocalTime.of(17, 59, 59);
		
		System.out.println(formatter.format(colonTime));
    }
}

출력 값

20221022
2022-10-22
2022-11-21
2022-11-21
17:59:59

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

ZoneId, ZoneDateTime, ZoneOffset  (0) 2022.11.28
Instant, Duration, Period  (0) 2022.11.28
LocalDate, LocalTime, LocalDateTime  (0) 2022.11.28

Instant 

Instant 클래스는 앞에서 봤던 LocalDate, LocalTime, LocalDateTime 과 달리

기계 관점에서 날짜와 시간을 표현하기 위해 사용합니다.

기계 관점에서는 사람과 달리 날짜와 시간을 구분하기 보다는 하나의 수를 사용하는 것이

계산에 용이하기 때문에 유닉스 에포크 시간(unix epoch time) 1970년 1월 1일 0시 0분 0초 UTC를 기준으로

특정 지점까지 시간을 초로 표현합니다.

팩토리 메서드 ofEpochSecond에 초를 넘겨서 클래스 인스턴스를 생성할수 있으며, 나노초의 정밀도를 제공합니다.

다음 예제를 통해 사용 예시를 확인해보겠습니다.

 

import java.time.*;

public class Main {
	public static void main(String[] args) {
 		Instant i3 = Instant.ofEpochSecond(3);
		Instant i4 = Instant.ofEpochSecond(3, 1_000_000_000);
		Instant i2 = Instant.ofEpochSecond(3, -1_000_000_000);
		
		System.out.println(i3);
		System.out.println(i4);
		System.out.println(i2);   
    }
}

출력 값

1970-01-01T00:00:03Z
1970-01-01T00:00:04Z
1970-01-01T00:00:02Z

 

Duration

두 시간의 객체 사이의 지속 시간을 만들고 표현할수 있습니다.

나노초, 초 단위로 출력이 가능하게 메서드를 제공합니다.

두 개의 LocalTime, LocalDateTime, Instant을 Duration으로 만들 수있습니다.

다음 예제를 통해서 확인해보겠습니다.

 

import java.time.*;
import java.time.temporal.ChronoUnit;

public class Main {
	public static void main(String[] args) {
		LocalTime t1 = LocalTime.of(15, 05, 10);
		LocalTime t2 = LocalTime.of(16, 05, 10);
		
		LocalDateTime dateTime1 = LocalDateTime.of(2022, 12, 1, 12, 00, 00);
		LocalDateTime dateTime2 = LocalDateTime.of(2022, 12, 2, 13, 00, 00);
		
		Duration duration1 = Duration.between(t1, t2);
		Duration duration2 = Duration.between(dateTime1, dateTime2);
		
		Duration fiveMin = Duration.ofMinutes(5);
		Duration sixMin = Duration.of(6, ChronoUnit.MINUTES);
		
		System.out.println(duration1.getSeconds());
		System.out.println(duration2.getSeconds());
		System.out.println(fiveMin);
		System.out.println(sixMin);
    }
}

출력 값

3600
90000
PT5M
PT6M

 

Period

두 시간의 객체 사이의 지속 시간을 만들고 표현할수 있습니다.

년/월/일 단위로 나타냅니다.

Duration의 경우 최대 제어 가능한 단위는 "일" 입니다.

다음 예제를 통해 사용 예시를 확인해보겠습니다.

 

import java.time.*;
import java.time.temporal.ChronoUnit;

public class Main {
	public static void main(String[] args) {
		Period fourDays = Period.ofDays(4);
		Period TwoWeeks = Period.ofWeeks(2);
		Period oneYearTwoMonthsThreeDays = Period.of(1, 2, 3);
		
		System.out.println(fourDays.get(ChronoUnit.DAYS));
		System.out.println(TwoWeeks.get(ChronoUnit.DAYS));
		System.out.println(oneYearTwoMonthsThreeDays.get(ChronoUnit.YEARS));
		System.out.println(oneYearTwoMonthsThreeDays.get(ChronoUnit.MONTHS));
		System.out.println(oneYearTwoMonthsThreeDays.get(ChronoUnit.DAYS));
		
		Period fiveDays = fourDays.plus(Period.ofDays(1));
		System.out.println(fiveDays.get(ChronoUnit.DAYS));
		
		Period threeDays = fourDays.minus(Period.ofDays(1));
		System.out.println(threeDays.get(ChronoUnit.DAYS));
    }
}

출력 값

 

4
14
1
2
3
5
3

 

Duration과 Period 공통 제공 메서드

메서드 정적 메서드 여부 설명
from Y 주어진 Temporal 객체를 이용하여 클래스 인스텉스 생성.
of Y 주어진 구성 요소로 Temporal 객체의
인스턴스 생성.
now Y
시스템 시계로 Temporal 객체 생성.
parse Y 문자열 파싱하여 Temporal 객체 생성.
atOffset N 시간대 오프셋과 Temporal 객체 합침.
atZone N 시간대 오프셋과 Temporal 객체를 합침
format N 지정된 포매터를 이용하여 Temporal
객체를 문자열로 변환.
get N Temporal 객체 상태를 읽어서 반환.
minus N 특정 시간을 뺀 Temporal 객체의 
복사본을 생성.
plus N 특정 시간을 더한 Temporal 객체의 
복사본을 생성.
with N 일부 상태를 바꾼 Temporal 객체의 
복사본을 생성.

 

ChronoUnit

표준 날짜 기간 단위 집합.

이 단위 세트는 날짜, 시간 또는 날짜-시간을 조작하기위한 단위 기반 액세스를 제공합니다. 

 

 

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

ZoneId, ZoneDateTime, ZoneOffset  (0) 2022.11.28
TemporalAdjusters와 DateTimeFormatter  (0) 2022.11.28
LocalDate, LocalTime, LocalDateTime  (0) 2022.11.28

자바 1.0에서 java.util.Date 클래스로 날짜와 시간 관련 기능을 제공하였으나

1900년 기준으로 하는 오프셋, 0으로 시작하는 달 인덱스 등 애매한 설계로

유용성이 떨어진다는 이야기가 많았고, 이를 보완할 수 있게 자바 1.1에서 Calandar 클래스가

추가되었으나 Daeformat 같은 일부 기능 들은 Date 클래스에서만 동작한다던 지

DateFormat이 스레드에 안전하지 않다 던지 여러 가지 문제점이 존재하였습니다.

자바 9에서 이를 보완 가능한 java.util.time 패키지가 추가되었고 이에 대해 알아보겠습니다.

 

LocalDate

시간을 제외한 날짜를 표현하는 불변 객체입니다.

어떤 시간대 정보도 포함하지 않습니다.

정적 팩토리 메서드 of로 LocalDate 인스턴스 생성 가능하고

연도, 달, 요일 등을 반환하는 메서드 제공합니다.

예제로 사용 예시를 확인해보겠습니다.

메서드 이름도 굉장히 직관적이고 이전에 비해 사용이 매우 쉬운 편입니다.

import java.time.*;

public class Main {
	public static void main(String[] args) {
		LocalDate date = LocalDate.of(2022, 10, 22);
		LocalDate parseDate = LocalDate.parse("2022-10-22");
		LocalDate today = LocalDate.now();
		int year = date.getYear();
		Month month = date.getMonth();
		DayOfWeek dow = date.getDayOfWeek();
		int len = date.lengthOfMonth();
		boolean leap = date.isLeapYear();
        
        System.out.println(date);
		System.out.println(parseDate);
		System.out.println(today);
		System.out.println(year);
		System.out.println(month);
		System.out.println(dow);
		System.out.println(len);
		System.out.println(leap);
    }
}

출력 값

2022-10-22
2022-10-22
2022-11-28
2022
OCTOBER
SATURDAY
31
false

 

LocalTime

시간 표현이 가능한 객체입니다.

LocalDate 객체와 사용 방법이 유사합니다.

다음 예제를 통해 사용 예시를 확인해보겠습니다.

import java.time.*;

public class Main {
	public static void main(String[] args) {
		LocalTime time = LocalTime.of(15, 56, 59);
        // 포맷이 틀릴 경우 DateTimeParseException 발생.
		LocalTime parseTime = LocalTime.parse("13:56:59"); 
        
		
		int hour = time.getHour();
		int minute = time.getMinute();
		int sec = time.getSecond();
		
		System.out.println(time);
		System.out.println(parseTime);
		
		System.out.println(hour);
		System.out.println(minute);
		System.out.println(sec);	
    }
}

출력 값

15:56:59
13:56:59
15
56
59

 

LocalDateTime

LocalDate와 LocalTime을 갖는 복합 클래스입니다.

날짜와 시간을 모두 표현 가능합니다.

다음 예제를 통해 사용 예시를 확인해보겠습니다.

 

import java.time.*;

public class Main {
	public static void main(String[] args) {
		LocalDateTime dt1 = LocalDateTime.of(2022, Month.DECEMBER, 22, 13, 56, 59);
		LocalDateTime dt2 = LocalDateTime.of(date, time);
		LocalDateTime dt3 = date.atTime(13, 45, 20);
		LocalDateTime dt4 = date.atTime(time);
		LocalDateTime dt5 = time.atDate(date);
		
		LocalDate ldate1 = dt1.toLocalDate();
		LocalTime time1 = dt1.toLocalTime();
		
		System.out.println(dt1);
		System.out.println(dt2);
		System.out.println(dt3);
		System.out.println(dt4);
		System.out.println(dt5);
		System.out.println(ldate1);
		System.out.println(time1);
    }
}

출력 값

2022-12-22T13:56:59
2022-10-22T15:56:59
2022-10-22T13:45:20
2022-10-22T15:56:59
2022-10-22T15:56:59
2022-12-22
13:56:59

 

 

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

ZoneId, ZoneDateTime, ZoneOffset  (0) 2022.11.28
TemporalAdjusters와 DateTimeFormatter  (0) 2022.11.28
Instant, Duration, Period  (0) 2022.11.28

자바의 모든 객체는 null 일 수 있고 null을 참조할 경우 NullPointerException을 만나게 됩니다.

NullPointerException가 무엇인지 자바 기초 문법을 익힌 사람이라면 대부분 아는 내용이므로

이 글에서 자세하게 언급하지는 않겠습니다.

현업에서 종종 레거시 프로젝트에서 한 번쯤은 확인 가능한 코드가 있습니다.

어떤 조건들에 대해 무한히 체크하는 패턴을 볼 수 있습니다.

특히 비즈니스 로직이 복잡한 곳이 그런데요.

아래와 같이 a, b, c 객체의 null 체크 조건식과 그에 따른 비즈니스 로직이 추가되는 sudo 코드입니다.

체크해야 할 조건이 늘어날수록 코드는 복잡해지고 가독성도 떨어지게 됩니다.

이러한 반복되는 패턴 코드를 깊은 의심(deep doubt) 이라고도 합니다.

if (a != null) { // 객체 a의 null 체크 
	B b = a.getXxx();
    ...
    if ( b != null) { // 객체 b의 null 체크 
    	C c = b.getXxx();
        ...
        if ( c != null) { // 객체 c의 null 체크 
        	D d = c.getXxx();
            ...
        }
    }
}

 

자바 8 이후로 추가된 Optional 클래스를 통해 NullPointerException 등 null 참조로 인해 발생하는 문제들을

처리하는 코드를 가독성 있게 작성할 수 있습니다.

java.util.Optional<T> 클래스는 선택형 값을 캡슐화하는 클래스입니다.

예제를 통해서 기능들을 확인해보겠습니다.

 

Phone, Person, Insurance, Main 클래스를 각각 생성하겠습니다.

처음 예제와 같이 null 참조에 대해 조건문으로 처리한 예제이고

Insurance → Phone Person  중첩되어 있는 구조입니다.

public class Insurance {
	private String name;
	public String getName() { return name; }
}
public class Phone {
	private Insurance insurance;
	public Insurance getInsurance() { 
		return insurance;
	};
}
public class Person {
	private Phone phone;
	
	public Phone getPhone() {
		return phone;
	}
}
public class Main {
	public static void main(String[] args) {
		Person p = new Person();
		System.out.println(getPhoneInsuranceName(p));
	}
	
	public static String getPhoneInsuranceName(Person person) {
		if (person != null) {
			Phone phone = person.getPhone();
			if(phone != null) {
				Insurance insurance = phone.getInsurance();
				if(insurance != null) {
					return insurance.getName();
				}
			}
		}
		return "None";
	}
}

이전 문법으로 이렇게 처리는 가능합니다

여기서 조금 더 복잡해져서 if 문을 누락한다면? 런타임에서 null 참조가 있는 속성에 접근 전까지는 알 수가 없습니다.

Optional 클래스를 이용해 개선해보겠습니다.

import java.util.Optional;

public class Person {
	private Optional<Phone> phone;
	
	public Optional<Phone> getPhone() {
		return phone;
	}
}
import java.util.Optional;

public class Phone {

	private Optional<Insurance> insurance;

	public Optional<Insurance>  getInsurance() { 
		return insurance;
	}
}
public class Insurance {
	private String name;
	public String getName() { return name; }
}
import java.util.Optional;

public class Main {
	public static void main(String[] args) {
		Person person = new Person();
		Optional<Person> optPerson = Optional.of(person);
		System.out.println(getPhoneInsuranceName(optPerson));
	}
	
	public static String getPhoneInsuranceName(Optional<Person> person) {
		String name = person.flatMap(Person::getPhone)
						.flatMap(Phone::getInsurance)
						.map(Insurance::getName)
						.orElse("None");
		return name;
	}
}

Main의 테스트 코드를 실행하면 NullPointerException이 발생합니다.

기존 처리방식의 코드와 차이점은 Person 속성에 접근하여 null 참조하기 전에 NullPointerException이 발생하게 됩니다.

도메인에서 사용 주의점은 직렬화에서 문제가 발생하는데 이것은 자바 설계 상 Optional의 용도가

선택형 반환 값을 지원하지 않게 설계됐기 때문입니다. 필드 형식으로 사용하지 않는 것을 가정하므로 Serializable 인터페이스를 구현하지 않기 때문입니다.

 

Optional은 예제에서 사용한 of 메서드 포함해서 다음과 같은 메서드를 제공합니다.

 

empty 메서드

빈 Optional 객체를 얻을 수 있습니다.

Optional<Person> optPerson = Optional.empty();

 

of 메서드

null 아닌 값을 포함하는 Optional을 만들 수 있습니다.

person이 null 이면 즉시 NullPointerException 발생.

Person person = new Person();
Optional<Person> optPerson = Optional.of(person);

 

ofNullable 메서드

null 값을 저장할 수 있는 Optional을 만들 수 있습니다.

person이 null이면 빈 Optional 객체를 반환합니다.

Person person = new Person();
Optional<Person> optPerson = Optional.ofNullable(person);

 

get 메서드

Optional 랩핑 된 값이 있으면 반환하고 없으면 NoSuchElementException을 발생시킵니다.

따라서 반드시 Optional에 값이 있다고 가정할 수 있는 상황에서만 사용하는 것이 바람직합니다.

Person person = new Person();
Person tmp = Optional.get(person);

 

orElse 메서드

Optional 랩핑된 값이 있는 경우 해당 값을 반환하고 랩핑된 값이 없으면 인수(기본값)를 제공.

첫 번째, 두 번째 줄 주석을 각각 해제/주석 처리하면서 테스트해보면 어느 때나 randomName 메서드를 호출하는 것을

알 수 있습니다.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		//Optional<String> optStr = Optional.empty();
		Optional<String> optStr = Optional.of("ABC");
		System.out.println(optStr.orElse(randomName()));
	}
	
	public static String randomName() {
		System.out.println("randomName");
		return "T" + new Random().nextInt(); 
	}
 }

 

orElseGet 메서드

Optional 랩핑 된 값이 있는 경우 해당 값을 반환하고 랩핑된 값이 없으면 Supplier를 실행하고 호출 결과를 반환.

첫 번째, 두 번째 줄 주석을 각각 해제/주석 처리하면서 테스트해보면 null 일 경우에만 randomName() 메서드가 호출되는 것을 확인 가능합니다.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		Optional<String> optStr = Optional.empty();
		//Optional<String> optStr = Optional.of("ABC");
		System.out.println(optStr.orElseGet(() -> randomName()));
		//System.out.println(getPhoneInsuranceName(optPerson));
	}
	
	public static String randomName() {
		System.out.println("randomName");
		return "T" + new Random().nextInt(); 
	}
}

 

orElseThrow

Optional 랩핑 값이 존재하면 값을 반환하고, 값이 없으면 Supllier에서 생성한 예외 발생.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		Optional<String> optStr = Optional.empty();
		//Optional<String> optStr = Optional.of("ABC");
		try {
			System.out.println(optStr.orElseThrow(() -> new Exception("orElseThrow")));
		} catch(Exception e) {
			e.printStackTrace();
		}
		//System.out.println(getPhoneInsuranceName(optPerson));
	}
}

첫 번째, 두 번째 줄 주석을 각각 해제/주석 처리하면서 테스트해보면 null 일 경우에만 예외 발생.

예외 종류는 선택 가능합니다.

 

 

ifPresent

값이 존재하면 지정된 Consumer를 실행. 

값이 없으면 아무 수행하지 않음.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		Optional<String> optStr = Optional.empty();
		//Optional<String> optStr = Optional.of("ABC");
		optStr.ifPresent(t -> randomName());
	}
	
	public static String randomName() {
		System.out.println("randomName");
		return "T" + new Random().nextInt(); 
	}
}

 

ifPresentOrElse

값이 존재하면 지정된 Consumer를 실행.

null 이 빈 값일 경우 Runnable을 지정할 수 있다.

import java.util.Optional;
import java.util.Random;

public class Main {
	public static void main(String[] args) {
		//Optional<String> optStr = Optional.empty();
		Optional<String> optStr = Optional.of("ABC");
		optStr.ifPresentOrElse(t-> System.out.println(t), () -> System.out.println("empty"));
	}
}

 

isPresent

값이 존재하면 true를 반환하고, 값이 없으면 false를 반환.

public class Main {
	public static void main(String[] args) {
		//Optional<String> optStr = Optional.empty();
		Optional<String> optStr = Optional.of("ABC");
		if(optStr.isPresent()) {
			System.out.println("exist");
		} else {
			System.out.println("empty");
		}
	}
}

 

이외에 Stream에서 제공했던 것처럼 map, flatmap도 제공된다.

그리고 기본형 특화 클래스인 OptionalInt, OptionalLong, OptionalDouble 있으나 

map, flatmap, filter 등의 기능들을 지원하지 않는다고 합니다.

 

마치며

Optional이 많은 null 참조 관련 처리를 다 해주는 만능 해결사 노릇을 할 것이라 기대했지만

초기 설계부터가 만능 처리보다는 명시적으로 이것이 선택 값인지 아닌지를 나타내는 

역할에 초점을 맞춘 것으로 생각된다. (도메인 직렬화 문제, get의 경우 NoSuchException 처리 등 )

저 같은 경우 웹 Rest API 개발 시에 요청 변수나 반환 값에 현업에서 적용하여 사용해봤습니다.

적절하게 반환 값 같은 곳에 사용하면 좋은 효과를 거둘 수 있을 것 같습니다.

 

이번 글은 Collector의 분할에 대해서 정리해보겠습니다.

분할 함수는 맵의 key로 Boolean을 반환합니다.

참 아니면 거짓 값을 갖는 두 그룹으로 분류돼 맵이 결과로 반환되게 됩니다.

예제를 통해 확인해보겠습니다.

먼저 예제에 쓰일 games 객체 리스트를 생성합니다.

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

 

partitioningBy

games 객체 리스트에서 RPG 라는 타입으로 분할하여 분류하는 예제입니다. 

Boolean 타입의 각각 false, true의 key 값이 생성되고 partitioningBy에 인수로 넣은 프레디케이트 값에 따라 분류됩니다. 

Map<Boolean, List<Game>> partitionedGame = games.stream()
.collect(partitioningBy(game -> game.getType().equals("RPG")));

partitionedGame.forEach((k,v) -> {
	System.out.println("<"+k+">");
	v.forEach(game -> System.out.println(game.getName()));
	System.out.println();
});

결과값

RPG 타입과 Shooting 타입으로 분류됩니다.

<false>
CALL 1

<true>
DIA 3
DIA 2
DIA 1

 

다음 예제는 앞에서 정리한 groupingBy과 partitioningBy 같이 활용한 예제입니다.

Map<Boolean, Map<String, List<Game>>> rpgGameByType = games.stream()
	.collect(partitioningBy(game -> game.getType().equals("RPG")
	, groupingBy(Game::getType)));
		
rpgGameByType.forEach((k,v) -> {
	System.out.println(k + " >");
	v.forEach((key, value) -> {
		System.out.println(key + " >");
		value.forEach(game -> System.out.println(game.getName()));
	});
	System.out.println();
});

결과값

false >
Shooting >
CALL 1

true >
RPG >
DIA 3
DIA 2
DIA 1

 

다음은 partitioningBy을 사용하여 소수를 분류하는 예제를 확인해보겠습니다.

먼저 소수를 판단하는 리턴하는 정적 메서드를 작성하겠습니다.

앞에서 배운 rangeClosed 메서드로 범위를 정하고 noneMatch 메서드를 사용하여 어떤 값들을 

찾을 것인지 조건을 정합니다.

public static boolean isPrime(int num) {
	int root = (int) Math.sqrt((double) num);
	return IntStream.rangeClosed(2, root).noneMatch(i -> num % i == 0);
}

앞에서 만든 isPrime 메서드를 활용하여 소수와 소수가 아닌 수로 분류하는 스트림을 작성합니다.

public static Map<Boolean, List<Integer>> partitionPrimes(int num) {
	return IntStream.rangeClosed(2, num).boxed()
				.collect(partitioningBy(x -> isPrime(x)));
}

partitionPrimes 메서드를 사용하여 분류하는 테스트를 해봅니다.

1~10까지 소수를 분류하여 출력합니다.

System.out.println(partitionPrimes(10));

결과값

{false=[4, 6, 8, 9, 10], true=[2, 3, 5, 7]}

 

마치며

스트림과 컬렉터를 쭉 살펴보면서 정리하다 보니 기존 DB 레벨에서 사용하던 SQL 처리 기능들을 

많이 Java 코드로 처리할 수 있게 많이 고민을 한 것 같습니다.

이전 글에 groupingBy, maxBy, counting는 SQL에 groupBy, max, count와 매칭이 됩니다.

현업에 있으면서 여러 회사를 거치면서 DB 프로시저로만 모든 비즈니스 로직 구현한 곳도 봤고

프로시저 없이 SQL과 Java를 적절히 섞어서 비즈니스 로직이 구현된 곳을 가장 많이 본듯합니다.

점점 JPA가 자리 잡으면서 예전처럼 Mybatis 프레임워크 위에서 SQL를 직접 구현하는 경우는 점점 줄고 있는데요.

물론 case by case로 적절히 기술을 적용하고 사용해야겠지만 이제는 DB 프로시저나 SQL을 직접 비즈니스 로직

구현하는 경우는 거의 없어지지 않을까 싶습니다.

물론 아직도 개발자가 거의 없거나 개발팀이 없는 경우 최소한에 인력으로 운영하면서 리팩터링에 대한 시간을

아까워하는 곳이 많이 있기 때문에 어느 정도 수요는 유지될 것으로 생각됩니다.

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

Collector 그룹화  (0) 2022.11.01
Collector 요약  (0) 2022.10.28
스트림 생성  (0) 2022.10.27
기본형 특화 스트림  (0) 2022.10.27
리듀싱  (0) 2022.10.26

+ Recent posts