Lambda와 관련해서는 학습을 진행하며 작성한 글이기 때문에 잘못된 내용이 있을 수도 있습니다. 틀린 내용이 있다면 채널톡 혹은 댓글을 통해 피드백해주시길 바랍니다. ㅎㅎ
Self Invocation
Self Invocation은 Dynamic Proxy 기반의 기능들을 사용할 때 사소한 실수로 인하여 자주 발생하는 문제입니다. 쉽게 설명하자면, 객체 외부에서 보내는 메시지(요청)에 대해서만 반응하도록 설계되어 있기에 내부의 요청에 대해서는 반응하지 못하기 때문입니다.
JVM 생태계에서 많은 사랑을 받는 Spring Framework는 다양한 기능들을 Dynamic Proxy 방식으로 제공하고 있습니다.
@Transcational, @Async, @Cacheable, AOP(Before, Around) 등의 Aspect 기능들이 속합니다.
AopContext의 currentProxy() 메서드를 통해 해당 객체를 감싸고 있는 Proxy 객체를 반환
((Type) AopContext.currentProxy()). someMethod();
상태 변수를 통한 자기 참조 (@Autowired나 ApplicationContext.getBean() 활용)
객체 외부에서 호출하는 메서드에 Dynamic Proxy가 반응하도록 설정하기
등이 있지만, 이것들을 적용하기에는 과한 상황이거나 Spring Container에 종속되는 좋지 않은 코드를 작성하게 될 수 있습니다.
Lambda를 이용하여 Self-Invocation 회피
Lambda로 어떻게 Self-Invocation을 회피할 수 있을까요?
결론만 이야기하자면 Lambda를 통해 실행되는 메서드를 접근하기 위해서 현재 호출 객체 외부로 메시지가 나가고, 최종적으로 호출해야 되는 메서드를 찾아 요청하였을 때, 외부에서 전달되기 때문에 감싸고 있는 Proxy가 해당 요청을 인지할 수 있기 때문입니다.
Java Lambda는 Reflection API, MethodHandle, LambdaMetaFactory 인터페이스를 이용하여 기능을 제공합니다.
Lambda Method를 호출하는 흐름
Reflection API를 통해 실행 대상이 되는 메서드 정보를 가져옵니다.
MethodHandle Lookup API에 정의된 Factory 메서드를 통해 Lookup 객체를 가져옵니다.
1번에서 가져온 정보를 Lookup.unreflect() 메서드에 전달함으로써 해당 메서드의 구현, 수행 정보를 알고 있는 MethodHandle 객체를 가져옵니다. (실제 메서드를 바라보고 있는 일종의 포인터)
LambdaMetafactory.metafactory() 메서드에 필요한 인자를 넘겨 CallSite 객체를 반환받습니다. 해당 객체는 Functional Interface를 객체로 다룰 수 있으며, 매개 변수를 설정하고 응답을 반환합니다. 인자 목록은 밑에 나열하였습니다.
접근 권한을 가지고 있는 Lookup 객체
구현할 메서드 이름(Supplier Interface를 사용했을 경우 get이라는 문자열을 넘긴다.)
메서드의 매개 변수와 응답 값의 Class 정보. methodType(Supplier.class, {Type}. class)
함수 객체(Lambda)에 의해 반환될 응답 값의 유형. methodType(Object.class)
메서드의 구현 및 수행 방식을 알고 있는 MethodHandle 객체
호출 시 동적으로 적용되어야 할 응답 값의 세부 정보. methodType({Type}. class)
callSite.getTarget()을 통해 호출할 메서드 정보를 가져오고 bindTo({Type}. class)를 통해 메서드에 넘길 인자 값을 지정한 뒤 Invoke를 통해 메서드를 호출합니다.
(사실상 그 호출하는 형태는 Dynamic Proxy와 유사한 것 같습니다)
꽤 복잡하지만, 결론적으로는 Lambda를 통해 호출되는 인터페이스를 인스턴스 화 하고, 메서드를 호출하기 때문에 객체 외부 요청으로 다시 돌아오는 것입니다.
이 흐름은 Bytecode의 invokedynamic이 호출된 경우에 수행 흐름을 나타냅니다. Lambda에서 Function Interface를 사용하지 않고 단순한 로직을 사용하는 경우 static 메서드를 생성하여 활용하기도 합니다.
invokedynamic은 Bootstrap 메서드라는 특정한 메서드를 호출하고, 이를 통해 위의 호출 프로세스를 초기화 및 실행하여 CallSite 객체를 반환받습니다. (InnerClassLambdaMetafactory를 활용하여 내부 클래스 생성 후 반환)
한번 Bootstrap 메서드가 실행된다면 이후에는 기존의 CallSite와 MethodHandle를 재사용하여 요청을 처리합니다.
구현 예시
Self-Invocation을 회피하기 위해 구현한 TransactionalHandler입니다.
@Service
public class TransactionHandler {
@Transactional(propagation = Propagation.REQUIRED)
public <T> T runInTransaction(Supplier<T> supplier) {
return supplier.get();
}
}
Sample Service.
@Service
@RequiredArgsConstructor
public class SampleService {
private final SampleRepository someRepository;
private final TransactionHandler transactionHandler;
// 특정 객체에서 호출하는 Method
public void addNewSamples(List<Sample> samples) {
return samples.forEach(sample ->
transactionHandler.runInTransaction(() -> addSample(sample.toEntity()))
)
}
// 외부에서 호출되는 Method
@Transcational
public SomeEntity addSample(SampleEntity newSample) {
return someRepository.insertSample(newSample);
}
}
단순한 예시여서 실제 효용성과 조금 동떨어진 감은 있지만, 실제 업무 중 활용할 수 있을만한 부분을 특정하실 수 있을 것이라고 생각합니다. ㅎㅎ 이 글은 여기까지입니다. 감사합니다.
// long과 int를 더하는 경우에는?
long longA = 10l;
int numC = 10;
System.out.println(longA+numC);
// 실제로는 int 가 long으로 Type Conversion이 된 것을 알 수 있다. - 2주차 내용
System.out.println(longA + (long)numC);
// long과 double를 더하는 경우에는?
long longA = 10;
double doubleA = 10.0;
System.out.println(longA+doubleA);
// 실제로는 long 이 double로 Numeric Promotion이 된 것을 알 수 있다.
System.out.println((double)longA + doubleA);
A - B : 뺄셈 연산자
int numA = 10;
int numB = 10;
System.out.println(numA - numB);
int j = numA - numB;
System.out.println(j); // 0 출력
A / B : 나눗셈 연산자
나눗셈 연산과 나머지 연산의 경우 피연산자로 0을 사용하면 Exception이 발생한다.
int numA = 10;
int numB = 10;
System.out.println(numA / numB);
int k = numA / numB;
System.out.println("k = " + k); // 1 출력
A % B : 나머지 연산자
int numA = 11;
int numB = 2;
System.out.println(numA % numB);
int l = numA % numB;
System.out.println("l = " + l); // 1 출력
A * B : 곱하기 연산자
int numA = 11;
int numB = 2;
System.out.println(numA * numB);
int h = numA % numB;
System.out.println("h = " + h); // 22 출력
++N, --N : 전위 증감 연산자
int numC = 10;
numC = --numC;
System.out.println("numC = " + numC);
int numD = 10;
numD = ++numD;
System.out.println("numD = " + numD);
// Decompile
// 전위 증감 연산자는 내부적으로 A = A +- 1; 의 형식을 취하는 것을 알 수 있다.
int numC = 10;
int numC = numC - 1;
System.out.println("numC = " + numC);
int numD = 10;
int numD = numD + 1;
System.out.println("numD = " + numD);
N++, N-- : 후위 증감 연산자
int numE = 10;
System.out.println(numE--);
System.out.println("numE = " + numE);
int numF = 10;
System.out.println(numF--);
System.out.println("numF = " + numF);
// Decompile
// 우선 임시변수를 만들어서 해당 값을 복사한뒤 전달하고 반영된 값을 그 이후에 제공한다.
int numE = 10;
byte var10001 = numE; // 임시변수에 값 복사
int numE = numE - 1; // 변수 연산
System.out.println(var10001); // 임시변수 출력
System.out.println("numE = " + numE); // 연산된 변수를 제공
int numF = 10;
var10001 = numF;
int numF = numF - 1;
System.out.println(var10001);
System.out.println("numF = " + numF);
Wrapper와 Primitive 간의 산술 연산
Wrapper Class* (Integer, Long, Double)와 Primitive Type 연산 시에는 Wrapper Class 피연산자가 Unboxing 되어 연산이 진행되고 다시 Boxing이 되는 등에 성능상 오버헤드가 발생한다.
int numB = 10;
Integer numA = numB; //autoboxing
numA = numA + numB; //unboxing 후 연산 -> 결과 반영 전 다시 autoboxing 진행
그리고 Operand Stack에서 관리되는 Primitive Type과 Heap에서 참조되는 Wrapper Type은 값에 접근하는 시간에 차이가 존재한다. (일반적으로 Wrapper의 Access 시간이 길다.)
Primitive 도 일정 값 이상의 경우, Constant Pool이라는 Stack 외부에 존재하는 공간에 저장된다.*
JVM 설정 중에 Primitive 값의 크기에 대한 설정이 있다고 한다.
즉 프로그램의 환경적인 부분에 따라서 수치나 연산 시간 등이 달라질 수 있다.
비트 연산자?
A & B : AND 연산 ( A와 B의 비트에 대해서 AND 연산을 수행한다.)
//양쪽의 비트가 1이면 1, 0이면 0, 0 과 1 인 경우에는 0을 나타낸다.
int num = 15; // 1111
int nums = 6; // 0110
// 0110 : AND 연산
int i = num & nums;
System.out.println("i = " + i); // 6
A | B : OR 연산 ( A와 B의 비트에 대해서 OR 연산을 수행한다.)
//하나의 비트가 1이면 1, 양쪽의 비트가 0이면 0이다.
int num = 15; // 1111
int nums = 6; // 0110
// 1111 : OR 연산
int i = num | nums;
System.out.println("i = " + i); // 15
A ^ B : XOR 연산 ( A와 B의 비트에 대해서 XOR 연산을 수행한다.)
//하나의 비트가 1이면 1, 양쪽의 비트가 0이면 0, 양쪽의 비트가 1이면 0이다.
int num = 15; // 1111
int nums = 6; // 0110
// 1001 : XOR 연산
int i = num ^ nums;
System.out.println("i = " + i); // 9
A << N : 시프트 연산자 ( A의 비트를 좌측으로 N만큼 이동한다.)
//비트를 좌측으로 N만큼 이동하고 우측에서 N만큼의 영역을 0으로 채우게된다.
int num = 15; // 1111
int nums = 2;
int i = num << nums; // 111100 = 좌측으로 비트를 2칸씩 이동.
System.out.println("i = " + i);
A >> N : 시프트 연산자 ( A의 비트를 우측으로 N만큼 이동한다.)
//비트를 우측으로 N만큼 이동하고 좌측에서 N만큼의 영역을 0으로 채우게된다.
int num = 15; // 1111
int nums = 2;
int i = num >> nums; // 0011 = 우측으로 비트를 2칸씩 이동.
System.out.println("i = " + i);
A >>> N : 부호가 없는 시프트 연산자 ( A의 2진수를 우측으로 비트를 N만큼 이동한다.)
int num = -15; // 11111111111111111111111111110001
int nums = 2;
// 부호를 신경쓰지 않고 모든 비트 값들을 오른쪽으로 이동시킨 후에 왼쪽의 빈 공간은 모두
// 0 으로 채운다.
int i = num >>> nums;
System.out.println("i = " + i); // 1073741820 = 00111111111111111111111111111100
int j = -15 >> nums;
System.out.println("j = " + j); // -4 : 11111111111111111111111111111100
~A : 반전 연산자 ( A 내부의 비트 값을 반전시킨다.)
int num = -15; // 11111111111111111111111111110001
System.out.println(~num); // 14 : 00000000000000000000000000001110
관계 연산자?
A == B : 동일성 비교 ( A와 B가 같다면?)
int numA = 10;
int numB = 10;
boolean isSame = numA == numB; // boolean 값을 반환.
if (isSame){
System.out.println(isSame); // true
}
혹은
if (numA == numB){
System.out.println(isSame); // true
}
A != B : 동일하지 않은 경우 ( A와 B가 같지 않다면?)
int numA = 10;
int numB = 9;
boolean isSame = numA != numB;
if (isSame){
System.out.println(isSame); //true
}
또는
if (numA != numB){
System.out.println(isSame); //true
}
또는
boolean isSame = numA == numB;
if (!isSame){
System.out.println(isSame); //false
}
A > B : 대소 관계 비교 ( A가 B보다 크다면?)
int numA = 10;
int numB = 9;
boolean isSame = numA > numB;
if (isSame){
System.out.println(isSame); //true
}
if (numA > numB){
System.out.println(isSame); //true
}
A >= B : 대소관계 비교 ( A가 B보다 크거나 같다면?)
int numA = 10;
int numB = 10;
boolean isSame = numA >= numB;
if (isSame){
System.out.println(isSame); //true
}
if (numA >= numB){
System.out.println(isSame); //true
}
A < B : 대소관계 비교 ( A가 B보다 작다면?)
int numA = 9;
int numB = 10;
boolean isSame = numA < numB;
if (isSame){
System.out.println(isSame); //true
}
if (numA < numB){
System.out.println(isSame); //true
}
A <= B : 대소관계 비교 ( A가 B보다 작거나 같다면?)
int numA = 10;
int numB = 10;
boolean isSame = numA <= numB;
if (isSame){
System.out.println(isSame); //true
}
if (numA <= numB){
System.out.println(isSame); //true
}
논리 연산자?
조건식 1 && 조건식2 : 조건식1과 조건식 2를 모두 만족한다면?
int numA = 10;
int numB = 10;
int numC = 10;
int numD = 10;
boolean bool = numA == numB && numC < numD; //false
if (bool){
System.out.println(bool);
}else {
**System.out.println(bool);
}
if (numA == numB && numC == numD){
System.out.println(numA == numB && numC == numD); //true
}
조건식1 || 조건식2 : 조건식1 혹은 조건식2를 만족한다면?
int numA = 10;
int numB = 10;
int numC = 10;
int numD = 10;
boolean bool = numA == numB || numC < numD; //true
if (bool){
System.out.println(bool);
}else {
System.out.println(bool);
}
if (numA == numB || numC == numD){
System.out.println(numA == numB && numC == numD); //true
}
! : 논리 반전 연산자 : != 같지않다면?, !< 작지않다면?
int num = 10;
String conn = null;
boolean bool = num != 9;
if (bool){
System.out.println(bool);
}else {
System.out.println(bool);
}
if (conn != null){
System.out.println("true");
}else {
System.out.println("false");
}
JDK Assertion (JDK 1.4+)
이전에 작성된 코드에서 사용된 assert라는 변수들과의 하위 호환성을 위해 기본적으로 비활성화되어 있다.
```java
assert service != null; // true -> 아무 일도 발생하지 않는다.
// false -> AssertionError (Unchecked Exception) 발생
혹은
assert service != null : "Service is Null"; // false 시 추가적으로 메세지를 제공한다.
instanceof
A instanceof B : (공변) 타입 검증 연산자, A가 B의 타입 혹은 하위 구현체인지 판단한다.
Reference Type 만 사용 가능하다.
public static void main(String[] args) {
InstanceofExample example = new InstanceofExample();
example.run();
}
public void run() {
SomeClass someClass = new SomeClass();
SomeClasz someClasz = new SomeClasz();
SomeClassChild someClassChild = new SomeClassChild();
typeCheck(someClass); // is SomeClass
typeCheck(someClasz); // is SomeClasz
typeCheck(someClassChild); // is SomeClass
}
public void typeCheck(Object obj){
if (obj instanceof SomeClass){
System.out.println("is SomeClass");
}else if (obj instanceof SomeClasz){
System.out.println("is SomeClasz");
}
}
class SomeClass {
}
class SomeClassChild extends SomeClass{
}
class SomeClasz {
}
대입 연산자? (assignment(=) operator)
Primitive 변수에 값을 할당하기 위해 사용하거나 Reference의 참조 값을 할당하기 위해 사용한다.
Arrow operator? ( (parameter list) -> lambda body )
Java 8에 추가된 lambda 식을 지원하기 위해 사용되는 연산자이다.
// 이러했던 구문을
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello!");
}
};
// 이렇게 축약할 수 있다.
// 하나의 식, 로직을 가지고 있다면 { } block 과 return도 생략 가능하다.
Runnable runnable = () -> System.out.println("Hello!");
// 그렇지 않다면 이런식으로 작성해야한다.
Runnable runnable = () -> {
System.out.println("Hello!");
helloLogic();
**};**
3항 연산자? ( Ternary Operator : 변수 = (조건)? true 인 경우 : false 인 경우)
삼항 연산자는 피연산자 3개를 사용하며, if-then-else 명령문의 축약형이라고 할 수 있다.
int num = 10;
String result;
if(num == 10){
result = "true";
System.out.println("result = " + result);
}else {
result = "false;
System.out.println("result = " + result);
}
result = (num == 10) ? "true" : "false";
System.out.println("result = " + result);
연산자 우선순위?
수식 내에 여러 연산자가 함께 등장할 때, 어느 연산자가 먼저 처리될 것인가를 결정합니다.
( ) 등으로 우선순위, 결합 방향에 따른 연산자의 처리 순서를 변경할 수도 있습니다.
연산자의 결합 규칙?
수식 내에 우선순위가 같은 연산자가 둘 이상 있을 때, 먼저 어느 연산을 수행할 것인가를 결정합니다.
int numA = 5;
int numB = 2;
int numC = 2;
// +, - 의 우선 순위가 동일하다.
// 결합 방향에 따라서 왼쪽부터 오른쪽으로 진행하게 된다.
int res = numA + numB - numC;
Java 13의 switch 연산자?
기존 switch 문법으로 만든 요일 반환식 (윤년을 앞선 로직에서 체크했다고 가정.)
public static String monthCheck(int num){
int days = 0;
switch (num) {
case 1 :
case 3 :
case 5 :
case 7 :
case 8 :
case 10 :
case 12 :
days = 31;
break;
case 4 :
case 6 :
case 9 :
case 11 :
days = 30;
break;
case 2 :
days = 28;
break;
default:
days = -1;
};
return "입력하신 달은 "+days+"일 입니다.";
}
Java 12에서 switch는
case 라벨을 쉼표로 구분하여 사용하게끔 문법을 지원하게 되었다.
break을 통해 값을 반환할 수 있게끔 되었다.
public static String monthCheck(int num){
int days = switch (num) {
case 1, 3, 5, 7, 8, 10, 12 :
break 31;
case 4, 6, 9, 11 :
break 30;
case 2 :
break 28;
default:
break -1;
};
return "입력하신 달은 "+days+"일 입니다.";
}
혹은 이와 같이 람다 식을 사용할 수 있게 되었다.
public static String monthCheck(int num){
int days = switch (num) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> 28;
default -> -1;
};
return "입력하신 달은 "+days+"일 입니다.";
}
Java 13에서 switch는
기존에 사용하던 break이라는 키워드를 yield로 대체(확장) 하게 되었다.
public static String monthCheck(int num){
int days = switch (num) {
case 1, 3, 5, 7, 8, 10, 12 :
yield 31;
case 4, 6, 9, 11 :
yield 30;
case 2 :
yield 28;
default:
yield -1;
};
return "입력하신 달은 "+days+"일 입니다.";
}
또는
public static String monthCheck(int num){
int days = switch (num) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> 28;
default -> {
System.out.println("잘못 입력했습니다.");
yield -1;
}
};
return "입력하신 달은 "+days+"일 입니다.";
}