[Java 8] 날짜 API - 2 Java

- 출처: https://www.oracle.com/technical-resources/articles/java/jf14-date-time.html
- 출처: 자바 8 인 액션

- Truncation
신규 API 는 날짜, 시간, 날짜 및 시간을 표현하는 타입을 제공하여 정밀한 시간을 지원하지만, 이것보다 더 세밀하게 정의된 정밀도의 개념을 지원한다.
truncatedTo 메소드는 이런 경우에 사용하는데, DB의 truncate 와 비슷하게 해당 필드에 대한 값을 비운다.
1
2
3
4
5
	LocalDateTime timePoint = LocalDateTime.now();
LocalDateTime truncatedTime = timePoint.truncatedTo(ChronoUnit.SECONDS);

System.out.println("timePoint: " + timePoint);
System.out.println("truncatedTime: " + truncatedTime);
- 실행결과
1
2
timePoint: 2020-09-25T23:50:22.651
truncatedTime: 2020-09-25T23:50:22
실행결과에서 truncatedTime 변수 값이 timePoint 에서 초 이하 단위가 절삭(truncate) 된것을 볼 수 있다.

- Instant
사람은 주, 날짜, 시간, 분으로 날짜와 시간을 계산하지만 기계에서는 특정지점을 숫자로 표현하는것이 자연스러운 방법이다. Instant는 1970년 1월 1일 0시 0분 0초 UTC 로 부터 특정 지점까지 흐른 시간을 초로 표현한다. 
1
2
	System.out.println("Instant : " + Instant.now());
System.out.println("Day of month : " + Instant.now().get(ChronoField.DAY_OF_MONTH));
1
2
3
4
Instant : 2020-09-29T05:32:13.502Z
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
at java.time.Instant.get(Instant.java:566)
at java8.JavaTest.main(JavaTest.java:16)
Instant 도 현재를 표현하는 now 메소드를 제공하지만, 사람이 읽을 수 있는 DayOfMonth 와 같은 필드는 지원하지 않는다.

- Time Zones
우리가 앞서 살펴본 local 클래스들은 time zone 과 같은 복잡한 정보는 없었다. time zone 은 표준 시간이 같은 지역들에 대응하는 규칙들의 집합이라고 할 수 있다. 대략 40개 정도가 있으며, Coordinated Universal Time(협정 세계 표준시 - UTC) 을 기준으로 하여 차이값(offset)에 따라 정의되어 있다. 특별한 경우를 제외하고는 거의 맞물려 움직인다.
Time zone 들은 2개의 식별자에 의해 참조된다. 예를 들면 함축형(ex - "PLT")과 더 긴 (ex - "Asia/Karachi") 와 같은 식이다. 어플리케이션을 설계할 때 타임존을 사용하기에 적절한 시나리오와 오프셋을 사용하기 적절한 경우를 고려해야 한다.
ZoneId 는 지역에 대한 식별자이다. 각 ZoneId 는 위치를 위한 타임존을 정의하는 몇몇 규칙에 부합한다. 소프트웨어를 설계할 때, 만약 "PLT" 나 "Asia/Karachi" 와 같은 문자열을 반환하려고 한다면 이렇게 처리하는 대신 도메인 클래스를 사용해야 한다. 
아래는 특정 타임존을 변수에 저장하는 코드이다.
1
2
3
4
5
6
7
	LocalDateTime timePoint = LocalDateTime.now();

ZoneId id = ZoneId.of("Europe/Paris");
ZonedDateTime zoned = ZonedDateTime.of(timePoint, id);

System.out.println("timePoint: " + timePoint);
System.out.println("zoned: " + zoned);
- 실행결과 
1
2
timePoint: 2020-09-26T00:20:15.462
zoned: 2020-09-26T00:20:15.462+02:00[Europe/Paris]
timePoint 의 값을 타임존 정보와 함께 저장하였다. 아래는 LocalDate, LocalTime, LocalDateTime, ZonedDateTime, ZoneId 와의 관계를 나타낸것이다.
  • LocalDate + LocalTime = LocalDateTime
  • LocalDateTime + ZoneId = ZonedDateTime
또 ZoneOffset 이라는 표현도 있는데, 이는 해당 타임존과 UTC 간의 시간차이를 표현한것이다. 
1
ZoneOffset offset = ZoneOffset.of("+02:00");
타임존을 사용할 때 헷갈리는 개념이 있는데, 바로 현재 시간을 다른 TimeZone 에서 표현하고자 할 때 LocalDateTime.now()로 현재 시간을 얻고, atZone 메소드로 해당 타임존의 시각을 얻으면 해당 지역에서의 현재 시간을 알 수 있지 않을까라고 생각하는 경우가 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	ZoneId parisZone = ZoneId.of("Europe/Paris");
ZoneId losAngelesZone = ZoneId.of("America/Los_Angeles");

LocalDateTime timePoint = LocalDateTime.now();
System.out.println("timePoint: " + timePoint);

ZonedDateTime zdt2 = timePoint.atZone(losAngelesZone);
ZonedDateTime ztd2WithParis = zdt2.withZoneSameInstant(parisZone);
System.out.println("zdt2: " + zdt2);
System.out.println("ztd2WithParis: " + ztd2WithParis);

Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(losAngelesZone);
ZonedDateTime ztd3WithParis = zdt3.withZoneSameInstant(parisZone);
System.out.println("zdt3 : " + zdt3);
System.out.println("ztd3WithParis: " + ztd3WithParis);
1
2
3
4
5
timePoint: 2020-09-29T20:03:41.394
zdt2: 2020-09-29T20:03:41.394-07:00[America/Los_Angeles]
ztd2WithParis: 2020-09-30T05:03:41.394+02:00[Europe/Paris]
zdt3 : 2020-09-29T04:03:41.414-07:00[America/Los_Angeles]
ztd3WithParis: 2020-09-29T13:03:41.414+02:00[Europe/Paris]
위에서 출력결과 2번행인 zdt2: 2020-09-29T20:03:41.394-07:00[America/Los_Angeles] 가 실행결과로 나올 때, 현재시간인 timePoint 에서 atZone 메소드를 통해 미국의 Zone 정보를 인자로 주었으니, 로스앤젤레스 시간이 나와야하는게 아닌가라고 생각할 수 있다.
LocalDateTime 은 마치 길을가다가 옆의 친구에게 지금 몇시냐 라고 물어보면 옆의 친구는 자신의 시계를 보고 답을 해주는 것과 같다. 대답을 해주는 사람은 현재 "몇시 몇분 UTC+9"라고 대답하지 않는다.
LocalDateTime 은 단지 날짜와 시간의 정보만 가지고있을 뿐이고, atZone 메소드로 해당 날짜와 시간에 단순하게 지역정보를 추가하여 새로운 ZonedDateTime 을 만들 뿐이다. 해당 지역으로 시간을 변환하지 않는다. 만약 로스앤잴레스 시간으로 변환하고자 한다면, 변환하기전의 타임존 정보가 어디여야 하는지를 알아야 하는데, LocalDateTIme은 Zone에 대한 정보가 없기 때문이다.
그래서 zdt2는 timePoint 날짜와 시간에 단순하게 로스앤젤레스 Zone 정보만 추가된것이다.
withZoneSameInstant 메소드로 Zone 정보를 넘겨서 생성한 zdt2WithParis는 사정이 달라진다. zdt2가 UTC-7 이었는데 UTC+2인 Paris 로 변환하라고 하였으니 상대적으로 +9시간을 더한 30일 오전 05시가 출력된것이다.
만약 설명이 너무 복잡해서 모르겠다면 LocalDateTime 에는 타임존의 정보가 없으며, 어떤 시간으로 부터 특정 타임존의 시간정보를 알아야 한다면, 변환하려는 시간은 어떤 타임존이었는가를 알아야 변환할 수 있다는 개념을 생각하면서 천천히 생각해보면 될 것이다.

- Time Zone 클래스
ZonedDateTime 은 타임존 정보와 함께 날짜와 시간을 표현한것이다. ZonedDateTime 은 시간의 어느 시점에서 offset을 표현할 수 있다. 일반적으로 만약 날짜와 시간을 특정 서버의 종속성없이 표현하길 원한다면 아래처럼 ZonedDateTime을 사용해야 한다.
1
2
3
ZonedDateTime timePoint = ZonedDateTime.parse("2020-09-28T10:15:30+01:00[Europe/Paris]");

System.out.println(timePoint);
- 실행결과 
1
2020-09-28T10:15:30+02:00[Europe/Paris]
OffsetDateTime 은 offset과 함께 날짜와 시간을 표현한 클래스이다. 이 클래스는 데이터를 DB로 직렬화 하거나 만약 타임존이 다른 서버들을 가지고있다면 로깅을 위한 타임스탬프 포맷 직렬화하는데 유용하게 사용될 수 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	ZoneOffset offset = ZoneOffset.of("+02:00");

OffsetTime time = OffsetTime.now();

OffsetTime sameTimeDifferentOffset = time.withOffsetSameInstant(offset);

OffsetTime changeTimeWithNewOffset = time.withOffsetSameLocal(offset);

OffsetTime changeTimeWithNewOffset2 = changeTimeWithNewOffset
.withHour(0).plusSeconds(0);

OffsetTime changeTimeWithNewOffset3 = changeTimeWithNewOffset
.withHour(3).plusSeconds(2);

System.out.println("time : " + time);
System.out.println("sameTimeDifferentOffset : " + sameTimeDifferentOffset);
System.out.println("changeTimeWithNewOffset : " + changeTimeWithNewOffset);
System.out.println("changeTimeWithNewOffset2 : " + changeTimeWithNewOffset2);
System.out.println("changeTimeWithNewOffset3 : " + changeTimeWithNewOffset3);
- 실행결과 
1
2
3
4
5
time : 11:56:58.564+09:00
sameTimeDifferentOffset : 04:56:58.564+02:00
changeTimeWithNewOffset : 11:56:58.564+02:00
changeTimeWithNewOffset2 : 00:56:58.564+02:00
changeTimeWithNewOffset3 : 03:57:00.564+02:00


- Periods
Period 는 3달 그리고 하루와 같은 시간대의 거리 값을 표현한다. 이같은 표현법은 시간대의 한 지점을 표현하는 다른 클래스들과는 대조적이다. 아래 에서 살펴보겠지만 Period는 Duration 과는 달리 년, 월, 일에 대한 표현을 한다.
 1
2
3
4
5
6
7
8
9
10
	Period period = Period.of(3, 2, 1);

LocalDate oldDate = LocalDate.now();
LocalDate newDate = oldDate.plus(period);

ZonedDateTime oldDateTime = ZonedDateTime.now();
ZonedDateTime newDateTime = oldDateTime.minus(period);

System.out.println("newDate: " + newDate);
System.out.println("newDateTime: " + newDateTime);
- 실행결과
1
2
newDate: 2023-11-30
newDateTime: 2017-07-28T13:52:54.953+09:00[Asia/Seoul]

- Durations
Duration 은 시간(시,분,초)의 관점에서 측정된 시간대의 거리인데, Period와 비슷한 목적이 있지만 정밀성에서 차이점이 있다.
 1
2
3
4
5
6
7
8
9
10
11
	Duration duration = Duration.ofSeconds(3, 5);

LocalDateTime today = LocalDateTime.now();
LocalDateTime yesterday = today.minus(1, ChronoUnit.DAYS);

Duration oneDay = Duration.between(today, yesterday);

System.out.println("duraiton: " + duration);
System.out.println("today: " + today);
System.out.println("yesterday: " + yesterday);
System.out.println("oneDay: " + oneDay);
1
2
3
4
duraiton: PT3.000000005S
today: 2020-09-29T14:11:42.381
yesterday: 2020-09-28T14:11:42.381
oneDay: PT-24H
처음 duration은 3초 5나노초 라는 기간을 표현하고 있다. 그리고 각각 오늘과 어제의 LocalDateTime 간의 Between을 보면 -24H 로 두 시점의 기간을 계산한다.
만약 LocalDateTime을 LocalDate 로 형을 바꾸면 지원하지 않는 Type이라고 Exception을 던지니 주의하도록 하자.




[Java 8] 날짜 API - 1 Java

- 출처: https://www.oracle.com/java/technologies/jf14-date-time.html

- 왜 새로운 날짜 API가 필요한가?
자바8 이전에는 날짜와 시간에 대한 기능 지원이 부족했다. 예를 들어 java.util.Date 와 SimplteDateFormatter 와 같은 클래스들은 스레드-세이프 하지 않아서 사용자에게 잠재적인 병렬성 이슈를 불러일으킬 가능성이 있었다.
또한 날짜와 시간 관련 클래스중 일부는 API 디자인 측면에서 부족한 부분이 있었다. 예를 들면 java.util.Date는 1900년 부터 시작하고, 달의 인덱스는 0부터 시작하며, 일의 인덱스는 1부터 시작하여 직관적이지 않은 부분이 있다.
이런 이슈들과 다른 문제점들은 Joda-Time 같은 제 3의 라이브러리를 사용하게 만들었다. 자바8에서는 이런 문제를 해결하고 더 나은 지원을 위해 새로운 날짜와 시간 API를 소개한다.

- 핵심 사상
- 불변 객체 클래스
기존에 있던 자바 formatter 의 문제점 중 하나는 스레드-세이프 하지 않다는것이었다. 이런 문제점은 개발자들에게 해당 API를 사용하기 위해서 스레드-세이프한 방법으로 사용하게 강제하는 짐을 주었다. 새로운 API는 코어 클래스가 불변이며, 잘 정의된 값을 표현할 수 있음을 보장하여 이런 문제를 회피하였다.
- DDD(도메인 드리븐 디자인)
새로운 API는 각 Date와 Time에 사용에 대해 엄격하게 나누어 표현하는 클래스로 정확하게 도메인을 모델링하였다. 새로운 API는 앞서 세심함이 부족한 자바 라이브러리들과는 다르다. 예를 들어 java.util.Date는 UNIX 시대에 밀리 세컨드를 감싼 타임라인의 순간을 표현한다. 하지만 toString() 을 했을 때, 그 결과는 타임존을 가지고 있기 때문에 혼란스러움이 존재한다.
이런점은 도메인 기반 디자인이 명료성과 이해력 관점에서 장기적인 이익을 제공하지만, 이전 API를 자바 8로 포팅할 때, 날짜의 어플리케이션 도메인 모델을 생각할 필요가 있다.
- 연대의 분리
새로운 API는 전세계의 특정 지역, 예를 들어 일본이나 태국같이 ISO-8601을 따를 필요가 없는 유저들이 필요한 지원을 받을 수 있도록 다른 캘린더 시스템을 제공한다. 

- LocalDate와 LocalTime
날짜 API를 처음사용할 때 LocalDate와 LocalTime을 처음으로 접하게 될 것이다. 이 클래스들은 책상위의 달력이나 벽의 시계같이 관찰자의 관점에서 날짜와 시간을 표현한다는 점에서 지역적인 특성을 가지고있다. 또한 LocalDate와 LocalTime이 결합된 LocalDateTime 이라 불리는 복합 클래스가 있다.
새로운 API에서 핵심 클래스들은 팩토리 메소드로 구성되어있다. 구성하는 필드에 의해 값을 생성할 때 팩토리 메소드 of 를 호출하고, 다른 타입에서 변환할 때는 from 을 호출한다. 또 문자열을 인자로 받아 파싱할때는 parse 메소드를 이용한다.
 1
2
3
4
5
6
7
8
9
10
11
	LocalDateTime timePoint = LocalDateTime.now();

LocalDate localDate1 = LocalDate.of(2012, Month.DECEMBER, 12);
LocalDate localDate2 = LocalDate.ofEpochDay(150);
LocalTime localTime1 = LocalTime.of(17, 18);
LocalTime localTime2 = LocalTime.parse("22:40:30");

System.out.println("localDate1 : " + localDate1);
System.out.println("localDate2 : " + localDate2);
System.out.println("localTime1 : " + localTime1);
System.out.println("localTime2 : " + localTime2
실행결과는 아래와 같다.
1
2
3
4
localDate1 : 2012-12-12
localDate2 : 1970-05-31
localTime1 : 17:18
localTime2 : 22:40:30
위와 같이 시간을 정의한 후, 표준적인 getter 메소드를 이용하여 값을 얻을 수도 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
	LocalDateTime timePoint = LocalDateTime.now();

LocalDate theDate = timePoint.toLocalDate();
Month month = timePoint.getMonth();
int day = timePoint.getDayOfMonth();
int second = timePoint.getSecond();

System.out.println("timePoint: " + timePoint);
System.out.println("theDate: " + theDate);
System.out.println("month: " + month);
System.out.println("day: " + day);
System.out.println("second: " + second);
1
2
3
4
5
timePoint: 2020-09-23T22:46:16.238
theDate: 2020-09-23
month: SEPTEMBER
day: 23
second: 16
또 계산을 수행하기 위해 객체값을 변경할 수 있다. 새로운 API에서 모든 코어 클래스는 변경 불가능(immutable)이기 때문에, setter 를 사용하기 보다는 with 메소드를 호출하여 새로운 객체를 반환한다.
1
2
3
4
5
6
	LocalDateTime timePoint = LocalDateTime.now();

LocalDateTime thePast = timePoint.withDayOfMonth(1)
.withMonth(Month.AUGUST.getValue()).withYear(2019);

System.out.println("thePast: " + thePast);
현재 시간에서 2019년, 8월, 1일로 시간을 돌렸다. 출력하면 아래와 같이 나타난다.
1
thePast: 2019-08-01T22:54:59.334
위의 with 메소드들을 계속 사용하였는데, 이렇게 사용할 수 있는 이유는 with 메소드에서 빌더 패턴처럼 자기자신을 반환하기 때문이다. 어떻게 정의되어있는지 내부 메소드를 살펴보자.
 1
2
3
4
5
6
7
8
9
10
    public LocalDateTime withDayOfMonth(int dayOfMonth) {
return with(date.withDayOfMonth(dayOfMonth), time);
}

private LocalDateTime with(LocalDate newDate, LocalTime newTime) {
if (date == newDate && time == newTime) {
return this;
}
return new LocalDateTime(newDate, newTime);
}
withDayOfMonth 에서 with 메소드를 호출하고, with 메소드는 LocalDate 와 LocalTime을 인자로 받아 새로운 LocalDateTime 객체를 반환한다.

- TemporalAdjusters
새로운 API는 adjuster(조절자, 조절장치) 개념을 가지고 있다. WithAdjuster는 1개 이상의 필드들을 설정하는데 사용되며, PlusAdjuster 는 몇개의 필드에 더하고 차감하는 연산에 사용된다. Value 클래스들은 adjuster 처럼 행동하는데, 이런 경우에 그들은 표현하는 필드의 값을 갱신한다. 내장된 adjuster들은 새로운 API에 의해 정의되어있지만, 만약 재사용해야하는 특별한 비즈니스 로직이 있다면 자신만의 커스텀 adjuster를 만들수도 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
	LocalDateTime timePoint = LocalDateTime.now();

LocalDateTime firstDayOfMonth = timePoint.with(TemporalAdjusters.firstDayOfMonth());
LocalDateTime nextMonth = timePoint.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDateTime prevFriday = timePoint.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
LocalDateTime nextFriday = timePoint.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));

System.out.println("timePoint: " + timePoint);
System.out.println("firstDayOfMonth: " + firstDayOfMonth);
System.out.println("nextMonth: " + nextMonth);
System.out.println("prevFriday: " + prevFriday);
System.out.println("nextFriday: " + nextFriday);
1
2
3
4
5
timePoint: 2020-09-23T23:09:33.512
firstDayOfMonth: 2020-09-01T23:09:33.512
nextMonth: 2020-10-01T23:09:33.512
prevFriday: 2020-09-18T23:09:33.512
nextFriday: 2020-09-25T23:09:33.512
메소드 이름과 실행결과를 보면 어떤 기능이나 동작을 하는지 추측하는것은 어렵지 않다는것을 알 수 있다. TemporalAdjusters 클래스에는 이런 편의기능을 제공하는 메소드들이 어느정도 구현되어있어서 복잡한 날짜 조정기능을 직관적으로 사용할 수 있다.


[Spring] AOP - AOP Proxy와 @Aspect 선언 Spring

- 출처: https://docs.spring.io/spring/docs/5.2.5.RELEASE/spring-framework-reference/core.html#aop-introduction-proxies

- AOP 프록시
스프링 AOP는 기본적으로 표준 JDK 다이나믹 프록시를 사용한다. 이 방식은 인터페이스가 프록시 될 수 있도록 동작한다.
스프링 AOP는 CGLIB 프록시도 사용할 수 있다. CGLIB는 인터페이스들보다는 클래스들에 대한 프록시가 필요할 때 사용한다. 보통 CGLIB는 비즈니스 객체가 인터페이스를 구현하고 있지 않을 때 사용된다. 보통은 클래스를 곧바로 사용하는 것 보다 인터페이스를 사용하는것이 좋은 방식이므로, 비즈니스 클래스들이 일반적으로는 1개 이상의 비즈니스 인터페이스를 구현하고 있다. 만약 선언되어있지 않은 메소드에 advice를 할 필요가 있거나 콘크리트 타입으로서 메소드에 프록시된 객체를 타입 그대로 넘겨야할 경우와 같을 때 CGLIB 사용을 강제할 수 있다.
스프링 AOP가 프록시 기반이라는 사실을 잊지말자.

- @AspectJ 지원
@AspectJ 는 정규 자바 클래스에 어노테이션을 붙여서 aspect 를 선언하는 방식이다. @AspectJ는 AspectJ 5 릴리즈 때 AspectJ 프로젝트에 의해 소개되었다. 스프링은 포인트컷 파싱과 매칭을 위해 AspectJ에서 제공되는 라이브러리를 사용하여, AspectJ 5 와 같은 방식으로 어노테이션을 해석한다. AOP 실행시에는 여전히 순수한 스프링 AOP 이며 AspectJ 컴파일러나 weaver의 어떤 의존관계도 없다.
- @AspectJ 지원 활성화
스프링 설정에서 @AspectJ aspect를 사용하기 위해서는 @AspectJ aspect 기반을 둔 스프링 AOP 환경을 위한 스프링 지원과, 이런 aspect 들에 의해 advise 될지 여부를 기반에 둔 auto 프록싱 bean들을 활성화 시켜야 한다. auto 프록싱이란 만약 스프링이 어떤 bean 이 다른 1개 이상의 aspect에 의해 advise 되는것이 정해졌다면, 메소드 실행을 인터셉트 할 bean을 위한 프록시를 자동적으로 생성하여, 필요시 advice 가 수행되는것을 보장해준다.
@AspectJ 지원은 XML 방식이나 자바 방식으로 설정될 수 있다. 이때 어플리케이션(자바 1.8이상)의 클래스패스상에 AspectJ의 aspectjweaber.jar 라이브러리를 추가해야 한다. 이 라이브러리는 메이븐 중앙 저장소나 AspectJ 의 lib 디렉토리에서 다운받을 수 있다.
- 자바 방식 @AspectJ 지원 설정
자바 @Configuration 으로 @AspectJ 지원을 활성화하기 위해서, 아래처럼 @EnableAspectJAutoProxy 어노테이션을 추가해야한다.
1
2
3
4
5
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
- XML 방식 @AspectJ 지원 설정
XMl 기반으로 @AspectJ 지원을 설정하려면 아래처럼 aop:aspectj-autoproxy 태그를 사용해야 한다.
1
<aop:aspectj-autoproxy/>
단, 위의 태그는 aop 네임스페이스를 임포트한 후 설정해야 한다.
- Aspect 선언
@AspectJ 지원을 활성화하면, @AspectJ aspect 클래스(@Aspect 어노테이션을 가진 클래스)와 어플리케이션 컨텍스트에 ㅈ ㅓㅇ의된 bean이 스프링에 의해 자동으로 감지되어 스프링 AOP 설정에 사용된다. 아래 두 예제를 살펴보자.
우선 아래 XML은 @Aspect 어노테이션이 선언된 bean 클래스를 가리키는 어플리케이션 컨텍스트 bean 정의이다.
1
2
3
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
그리고 아래 Java 코드는 org.aspectj.lang.annotation.Aspect 어노테이션을 정의한 NotVeryUsefulAspect 클래스 정의이다.
1
2
3
4
5
6
7
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}
Aspect들(@Aspect 어노테이션이 정의된 클래스)은 다른 클래스들처럼 메소드와 필드를 가질 수 있다. 또 pointcut, advice, introduction 선언들도 포함할 수 있다.
- 컴포넌트 스캐닝을 통한 aspect 탐지
aspect 클래스들을 다른 스프링이 관리하는 bean들처럼 XML 설정이나, 클래스패스 스캐닝을 통하여 자동탐지 방식으로 등록 할 수 있다. 그러나 @Aspect 어노테이션은 클래스패스에서 자동탐지될 수 없다. 그래서 별도로 @Component 어노테이션을 추가해야 한다.
- 다른 aspect들과 aspect advising
스프링 AOP 에서 aspect들은 그 자체로 다른 aspect들로부터 advice의 타겟이 될 수 없다. 클래스의 @Aspect 어노테이션은 해당 클래스를 aspect 로서 취급하므로 auto 프록싱에서 제외한다.


[Java 8] CompletableFuture - 4 (종료조건) Java

- 출처: 자바 8 in action

- CompletableFuture 종료
앞서 작성한 코드들에서 원격 서비스를 흉내내기 위해 1초 sleep을 주었지만 사실 실제 상황에서는 네트워크 상황이나 다른 변수등 어떤일이 일어날지 알 수 없다. 그래서 원격 서비스를 사용할 때 더 실제와 같은 상황을 부여하기 위해 랜덤하게 sleep을 할당해보자.
getTicketPrice 메소드에 sleep 부분을 아래 코드로 변경하자.
1
2
3
4
5
6
7
int delay = 500 + random.nextInt(2000); 

try {
Thread.sleep(delay);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
앞선 코드는 모든 가격이 조회될때까지 기다렸지만 더 빨리 조회되는 가격정보를 먼저 보여줄 수 있다. 그러려면 CompletableFuture의 Stream을 직접 제어해야 하는데 아래와 같이 Stream을 반환하는 메소드를 추가해보자.
 1
2
3
4
5
6
7
8
9
10
11
public Stream<CompletableFuture<String>> getTicketPriceStream(List<Ticket> tickets){

return tickets.stream()
.map(ticket -> CompletableFuture.supplyAsync(() ->
getTicketPrice(ticket.getFrom(), ticket.getTo())))
.map(future -> future.thenApply(DiscountForm::new))
.map(future -> future.thenCompose((discountForm) ->
CompletableFuture.supplyAsync(() ->
DiscountService.getPriceViaDiscountForm(discountForm)
)));
}
Stream을 반환받으면 아래와 같은 형식으로 사용할 수 있다.
1
2
3
4
5
	CompletableFuture[] futures = airline.getTicketPriceStream(tickets)
.map(f -> f.thenAccept(System.out::println))
.toArray(size -> new CompletableFuture[size]);

CompletableFuture.allOf(futures).join();
thenAccept는 CompletableFuture에 등록된 동작을 소비하는 메소드이다. 그렇다고 thenAccept 메소드만 코딩 후 그대로 실행한다고 해서 결과값이 출력되는 것은 아니다. thenAccept 는 등록된 동작을 어떻게 소비할지만 정의한 상태인것이다. thenAccept는 Comsumer 함수형 인터페이스를 인자로 받으므로 CompletableFuture<Void> 형을 반환하게 된다.
소비하는 동작까지 정했으면 실제로 어떻게 CompletableFuture를 종료할것인지를 정해야 한다. 만약 모든 ticket의 가격을 구하고 싶다면 CompletableFuture.allOf 메소드로 모든 ticket의 비동기 연산이 끝날때까지 기다린다. 얼만큼의 시간이 걸렸는지 더 자세히 파악하기 위해 메소드를 수정해보자.
1
2
3
4
5
6
7
8
9
	long startTime = System.currentTimeMillis();

CompletableFuture[] futures = airline.getTicketPriceStream(tickets)
.map(f -> f.thenAccept(resultPrice -> {
long end = System.currentTimeMillis();
String result = "Date: " + ((end - startTime) / 1000.0) + ", result: " + resultPrice;
System.out.println(result);
}))
.toArray(size -> new CompletableFuture[size]);
- 실행결과
1
2
3
4
5
Date: 1.8, result: KOR,KAZ,0.6158781006883716
Date: 1.825, result: KOR,JPN,0.31299183537878195
Date: 1.951, result: KOR,CHN,0.9042009738962702
Date: 2.018, result: KOR,CHE,0.6109196728742164
Date: 2.836, result: KOR,USA,0.6322615503251896
랜덤으로 수행해서 각 Ticket 마다 가격계산을 완료한 시간이 다르다.
만약 모든 결과를 기다릴 필요 없이 1개의 결과만 받으면 되는 상황이라면 allOf 대신 anyOf를 사용한다.
1
CompletableFuture.anyOf(futures).join();
- 실행결과
1
Date: 1.586, result: KOR,KAZ,0.571177037860831



[Spring] AOP - AOP 용어와 개념 Spring

- 출처: https://docs.spring.io/spring/docs/5.2.5.RELEASE/spring-framework-reference/core.html#aop

- AOP(Aspect Oriented Programming with Spring)
관점지향 프로그래밍(AOP)는 프로그램 구조를 다른 관점으로 제공하여 객체지향프로그래밍(OOP)을 보완해준다. OOP에서 모듈의 핵심은 클래스인 반면 AOP 에서는 "aspect(관점)" 이다. Aspect는 여러 타입이나 객체들을 관통하여 관심사들의 모듈화(예를 들면 트랜잭션 관리)를 가능하게 한다.
AOP 프레임워크는 스프링의 핵심 컴포넌트중의 하나이다. Spring IoC 컨테이너는 AOP에 의존하진 않지만, AOP는 Spring IoC에 유용한 미들웨어 솔루션을 제공하여 IoC를 보완해준다.
- AspectJ 포인트컷과 스프링 AOP
스프링은 스키마 기반 접근법이나 @ApsectJ 어노테이션 스타일을 사용하여 심플하고 파워풀한 커스텀 aspect 작성을 지원한다. 이 두 스타일은 모두 완전한 형태의 advice와 AspectJ 포인트컷 언어의 사용을 제공하지만 여전히 weaving을 위해 Spring AOP를 사용한다.
AOP는 스프링 프레임워크에서 다음과 같이 사용된다.
- 선언적인 엔터프라이즈 서비스들을 제공한다. 가장 중요한 이러한 서비스에는 선언적인 트랜잭션 관리가 있다.
- 사용자들이 커스텀 aspect 들을 구현하게 해서 AOP로 그들의 OOP를 보완한다.

- AOP 개념
우선 AOP의 핵심개념과 용어를 먼저 정의해보자. 이 용어들은 스프링 전용으로 사용되는 용어가 아니다. 안타깝게도 AOP 용어는 직관적이지는 않다. 하지만 스프링 자체적으로 자신만의 용어를 정의하여 사용한다면 더 혼란스러워 질수도 있다.
  • Aspect: 여러개의 클래스들을 관통하는 관심사의 모듈화. 트랜잭션 관리는 엔터프라이즈 자바 어플리케이션에서 좋은 예제이다. 스프링 AOP 에서 관심사들은 정규 클래스들을 사용하여 구현되거나(스키마 기반의 접근법으로), @Aspect 어노테이션이 붙은 정규 클래스들(@AspectJ 방식)을 사용하여 구현된다.
  • Join point: 프로그램이 수행되는 동안의 지점, 메소드의 실행이나 익셉션 핸들링과 같은 것들이 있다. 스프링 AOP 에서 join point 는 언제나 메소드 실행을 표현한다.
  • Advice: 특정 join point 에서 관심사에 의해 일어난 행동. advice의 타입에는 "around", "before", "after" 가 있다.(advice 타입들에 대해서는 추후 기술) 스프링을 포함한 많은 AOP 프레임워크에서 인터셉터나와 join point 근처에서 인터셉터들의 체인을 유지하는 것으로서 advice를 모델링한다.
  • Pointcut: join point들과 매칭되는 술부(주어에 대해 서술하는 부분). Advice는 pointcut 표현식과 관계가 있고 포인트컷에 의해 매칭된 어떤 join point(예를 들면 특정 이름의 메소드 수행)를 실행한다. pointcut 표현식에 의해 매칭되는 join point 들의 개념은 AOP에 있어서 핵심인데, 스프링은 AspectJ pointcut 표현식언어를 기본적으로 사용한다.
  • Introduction: 타입을 대신한 추가적인 메소드들이나 필드들에 대한 선언. 스프링 AOP는 어떤 advice될 객체에게 새로운 인터페이스들을 알려준다. 예를 들어 IsModified 인터페이스를 구현하는 bean을 만들기 위한 introduction 을 사용할 수 있다.
  • Target object: 1개 이상의 aspect들에 의해 advice 되고있는 객체. 또한 "advised object" 로서 참조되고 있는 객체. Spring AOP는 런타임 프록시들을 사용하여 구현되었기 때문에, 이 객체는 언제나 프록시된 객체이다.
  • AOP proxy: aspect 규약을 구현하기 위해 AOP 프레임워크에 의해 생성된 객체. Spring 프레임워크에서는 AOP 프록시는 JDK 다이나믹 프록시 이거나 CGLIB 프록시이다.
  • Weaving: aspect들과 다른 어플리케이션 타입들의 연결이나 advised 객체를 생성하기 위한 객체들. 이 과정은 컴파일 과정(AspectJ 컴파일러), 로드 타임 또는 런타임 완료될 수 있다. 다른 순수한 자바 AOP 프레임워크들과 같이 Spring AOP는 실행시점에 weaving을 수행한다.
스프링 AOP 에는 다음과 같은 advice 타입들이 있다.
  • Before advice: join point 전에 실행하는 advice. 하지만 join point 에 선행하여 실행을 막을 권한은 없다. (exception이 발생하지 않는다는 전제하에서)
  • After returning advice: join point 일반적으로 수행 완료된 이후에 수행되는 advice(메소드가 익셉션을 발생시키지 않으면)
  • After throwing advice: 익셉션이 발생한 메소드가 존재하면 수행되는 advice
  • After (finally) advice: join point가 일반적으로 종료되든 익셉션이 발생해서 종료되든 상관없이 수행되는 advice
  • Around advice: method 수행과 같은 join point 를 감싸는 advice. advice 종류중에서 가장 파워풀하다. Around advice 는 메소드 수행 전과 후에 커스텀 동작을 수행할 수 있다. 또한 join point 를 수행하거나 익셉션이 발생하거나 자신의 값이 반환됨으로써 advised 된 메소드 수행을 막을지 결정할 수 있다.
Around advice는 가장 일반적인 형태의 advice이다. AspectJ와 같은 Spring AOP는 advice 타입들의 모든 범위를 지원하기 때문에, 가능한 최소한의 advice 타입을 사용하는걸 권장한다. 예를 들어 만약 메소드에서 반환된 값을 cache에 갱신할 때, around advice 로도 이를 구현할 수 있지만 around advice 보다 after returning advice로 구현하는것이 더 낫다는 뜻이다. 가장 최적의 advice 타입을 사용하면 잠재적인 에러를 줄이고 더 간단한 프로그래밍 모델링을 할 수 있다. 예를 들어 adround advice에서 사용되는 JoinPoint의 proceed() 메소드 수행이 필요없기 때문에 그만큼 프로그램이 실패할 확률이 줄어든다.
모든 advice 파라미터들은 Object 배열들 보다 적합한 타입의 advice 파라미터들로 동작할 수 있게 하기 위해 고정된 타입으로 되어있다.
Pointcut 들에 의해 매칭된 join point 들의 개념은 AOP에 있어서 핵심인데, 인터셉션만 제공하던 옛날 기술들보다 진일보된 기술이다. Pointcut들은 advice가 객체 지향 구조에서 독립적으로 타겟이 되게 한다. 예를 들어 여러개의 객체들을 확장한 메소드들에 선언적 트랜잭션 관리를 제공하기 위해서 around advice를 적용할 수 있다.

- 스프링 AOP의 능력과 목표
스프링 AOP는 순수한 자바로 구현되었다. AOP에는 특별한 컴파일 과정이 필요하지 않다. 스프링 AOP는 클래스 로더 구조를 제어할 필요가 없어서 서블릿 컨테이너나 어플리케이션 서버에서 사용되기 더 적합하다.
스프링 AOP는 현재 메소드 수행 join point들만 지원하고 있다. 핵심 스프링 AOP API 들에 영향없이 필드 인터셉션을 지원할 수 있지만 필드 인터셉션은 구현되어있지 않다. 만약 필드 접근이나 수정에 대한 join point들이 필요하다면 AspectJ와 같은 언어를 고려하면 된다.
AOP에 대한 스프링 AOP의 접근법은 대부분의 다른 AOP 프레임워크들과는 차이가 있다. 스프링 AOP의 목표는 완벽한 AOP 구현을 제공하는 것이 아니다. 그 보다는 엔터프라이즈 어플리케이션에서 일반적인 문제 해결 지원을 위해 스프링 IoC와 AOP 구현들과 밀접한 통합을 제공하는것이 목표이다.
그래서 스프링 프레임워크의 AOP 기능은 일반적으로 스프링 IoC 컨테이너와 함께 사용된다. Aspect 들은 일반적인 bean 정의 문법을 사용하여 설정된다. 이것은 다른 AOP 구현들과는 상당한 차이가 있다. 예를 들어 매우 잘 정제된 객체들(일반적으로 도메인 객체들)에 대한 작업을 할 때는 Spring AOP로는 효율적으로 작업을 할 순 없다. 이런 경우에는 AspectJ 가 최적이다. 그러나 스프링 AOP는 엔터프라이즈 자바 어플리케이션들에서 대부분의 문제에 대한 해결책을 제공한다.
스프링 AOP는 절대 종합적인 AOP 솔루션을 제공하기 위해 AspectJ 와 경쟁하지 않는다. 스프링 팀은 스프링 AOP와 같은 프록시 기반의 프레임워크들과 AspectJ와 같은 완전한 솔루션 둘 다 경쟁의 개념 보다는 가치가 있고 상호보완적인 개념이라고 생각하고 있다. 스프링은 스프링 기반의 어플리케이션 아키텍처에서 AOP 의 모든 사용이 가능하도록 AspectJ와 Spring AOP 그리고 IoC를 매끄럽게 통합한다. 이런 통합이 Spring AOP API나 AOP 연합체 API에 영향을 주진 않는다. 스프링 AOP는 호환가능한것이어야 한다.
스프링 프레임워크의 주된 철학중 하나는 침투성을 가지지 않는다는것이다. 이것은 비즈니스나 도메인 모델에 스프링 종속적인 클래스들이나 인터페이스들을 강제 시켜서는 안된다는 듯이다. 그러나 때로는 스프링 프레임워크가 사용자의 코드에 스프링 의존성을 추가할 옵션을 제공하는 경우가있다. 이런 이유는 그렇게 하는게 가독성이 좋거나 코드작성이 더 쉬워지기 때문이다. 그러나 스프링 프레임워크는 언제나 선택권을 제공한다.
이 챕터에서 이런 선택권과 관련된 부분은 어떤 AOP 프레임워크를 선택하느냐이다. 사용자는 AspectJ나 Spring AOP 혹은 둘다 사용할 수 있다. 또한 @AspectJ 어노테이션 스타일의 접근법이나 Spring XML 설정 스타일의 접근법중에서 선택할 수 있다. 이 챕터에서는 @Aspect 스타일의 접근법을 먼저 소개하지만, 이 이유가 스프링 팀이 XML 설정법 보다 @Aspect 어노테이션 스타일의 접근법을 더 선호해서 그런것은 아니다.


1 2 3 4 5 6 7 8 9 10 다음