정수란

수학에서 정수는 양의 정수, 음의 정수 및 0으로 이루어진 수의 체계이다. 또는 자연수, 자연수의 음수 및 영을 통칭하는 말이다.

n이 0 또는 자연수일 때, n + x = 0 만족하는 모든 x, 모든 n을 통틀어 '정수'라고 한다.

 

컴퓨터에서 정수를 표현하는 방법

컴퓨터는 정수를 2진법으로 표현한다. n 개의 비트(bit)로 2^n 개의 정수를 표현할 수 있다.

컴퓨터에서 정수를 표현하는 방법은 부호없는 정수(Unsigned Integer), 부호있는 정수(Signed Integer) 2가지로 나뉜다.

 

부호없는 정수는 0 또는 양의 정수를 표현한다.

부호없는 정수는 0, 양의정수 또는 음의정수를 표현한다.

그런데 비트에는 +, - 와 같은 기호가 없고 0, 1만 있을 뿐이다. 비트로 어떻게 음수를 나타낼 수 있을까?

 

부호있는 정수

부호있는 정수가 음수를 표현하는 방법은 3가지로 부호 절대값, 1의 보수, 2의 보수가 있다.

 

부호 절대값

최상위 비트인 MSB (Most Significant Bit) 를 기준으로 양수와 음수를 구분한다. 최상위 비트는 비트에서 가장 왼쪽에 있는 숫자다. 예를 들어 8비트 2진수 10000000이 있다면 1이 최상위 비트다.

 

최상위 비트가 0이면 0 또는 양의 정수, 최상위 비트가 1이면 음의 정수를 나타낸다. 최상위 비트만 보고 양수인지 음수인지 바로 알 수 있다.

 

최상위 비트는 부호로 사용하고, 나머지 비트로 숫자를 나타낸다.

 

00000011(2) -> +3
10000011(2) -> -3

 

0이 2개

00000000(2) -> +0
10000000(2) -> -0

부호 절대값 방식은 0이 양수, 음수 2개가 존재해서 비트로 표현할 수 있는 숫자가 하나 줄어든다.

 

 

연산이 복잡하다

부호 비트와 나머지 비트를 나눠서 계산

부호 비트의 계산 방법은 절대값의 계산 방법과 다를 수 있다. 두 수가 모두 음수인 경우 덧셈을 해도 음수이므로 덧셈한 결과의 부호 비트는 1이어야 한다. 예를 들어 -3인 1011(2) 와 -2 인 1010(2) 덧셈은 부호 비트가 0이 아니라 1이 되어야 한다.

 

1011(2) -> -3
1010(2) -> -2

  1011
+ 1010
------
  1101

 

뺄셈 + 두 피연산자간의 절대값 크기를 고려해서 계산

이진수의 뺄셈은 주로 덧셈을 활용하는데 부호 절대값에서는 이게 적용되지 않을 수 있다. 적용되지 않으면 가산기 외에 뺄셈기가 필요하다.

 

부호 절대값의 연산

1) 두 피연산자가 모두 양수

 

  • 앞의 절대값 > 뒤의 절대값

 

(+3) - (+2) 를 (+3) + (-2) 로 계산하면

 

0011(2) -> 3
0010(2) -> 2

 

0011(2) -> 3
1010(2) -> -2

 

1이 나와야 하는데 -5가 됐다.

 

  0011
+ 1010
------
  1101

 

위처럼 부호가 다르면 부호비트는 덧셈으로 구할 수 없다. 뺄셈으로 계산하면 원하는 값을 얻을 수 있다. 단, 부호비트는 별도로 처리해야 한다. 부호비트는 1이 아닌 0으로 처리해야 한다.

 

부호비트 연산을 이해 하는데 이 글이 많은 도움이 됐다. 이 경우 부호비트는 -1이 된다. 1은 음수고 음수에 음수를 나타내는 -를 붙이면 양수가 되므로 양수를 나타내는 0을 대입하면 된다.

 

  0011
- 1010
------
  0001

 

  • 앞의 절대값 < 뒤의 절대값
0010(2) -> 2
0011(2) -> 3

 

그대로 계산하면 안되고 두 숫자의 순서를 바꿔서 계산해야 한다.

그런데 (+2) - (+3) 을 순서를 바꿔서 (-3) + (+2) 계산해도 원하는 값이 나오지 않는다.

 

  1011
+ 0010
------
  1101

 

'앞의 절대값이 뒤의 절대값 보다 작은 경우'도 '앞의 절대값이 뒤의 절대값 보다 큰 경우'처럼 덧셈이 아닌 뺄셈으로 구해야 한다.

부호비트는 1 - 0 에서 -0이 된다. -0은 양수에 음수 기호를 붙인 것이라 음수가 되므로 1로 나타낼 수 있다.

 

  1011
- 0010
------
  1001

 

2) 앞의 피연산자가 음수면서 뒤의 피연산자가 양수

 

-3 과 2를 뺄셈할 경우 (-3) - (+2) 를 (-3) + (-2) 로 계산하고 부호 비트만 따로 연산하면 덧셈으로 구할 수 있다.

 

1011(2) -> -3
1010(2) -> -2

 

  1011
+ 1010
------
  1101

 

부호 비트를 따로 연산해야 해서 회로 구성이 복잡해진다.

 

1의 보수

1의 보수란 어떤 수를 커다란 2의 거듭제곱수-1에서 빼서 얻은 이진수이다.

 

커다란 2의 제곱수는 어떤 수 보다 1자리수가 많으면서 1로 시작하고 나머지가 0인 수다. 이 커다란 2의 제곱수에서 1을 빼면 어떤 수와 자리수가 같으면서 자리수의 모든 값이 1이다.

 

101의 커다란 2의 거듭제곱수 - 1은 111이다. 101의 1의 보수는 111에서 101을 빼서 구하는 숫자다. 111에서 101을 빼면 010이 되고 010은 101의 자리수를 모두 반전한 것과 같다.

 

0이 2개

2진수 10진수
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1111 -0
1110 -1
1101 -2
1100 -3
1011 -4
1010 -5
1001 -6
1000 -7

1의 보수 표현으로 나타낼 수 있는 4비트 2진수는 위와 같다. 1의 보수도 부호 절대값과 마찬가지로 0이 2개다.

 

 

연산시 캐리 처리 필요

캐리는 최상위 비트(MSB) 에서 그 위의 비트로 자리올림이 발생하는 것을 의미한다

 

1의 보수로 표현한 이진수 연산시 캐리가 발생 여부에 따라 처리 방법이 달라진다. 캐리가 발생한 경우 발생한 캐리 1을 최하위 비트에서 더한다.

 

2와 -1을 더할 경우 최상위 비트에서 자리올림이 발생한다. 자리올림된 1을 자리올림을 제외한 값과 더하면 (2) + (-1)의 몫인 1을 구할 수 있다.

 

0010(2) -> 2
1110(2) -> -1

 

  0010
+ 1110
------
1 0000
(최상위 비트에서 1 자리올림 발생)

 

  0000
+ 0001
------
  0001
(자리올림된 1을 0000과 더한다)

 

2의 보수

2의 보수란 어떤 수를 커다란 2의 제곱수에서 빼서 얻은 이진수이다.

 

커다란 2의 제곱수는 어떤 수 보다 1자리수가 많으면서 1로 시작하고 나머지가 0인 수다. 예를 들어 011의 커다란 2의 제곱수는 1000이다.

 

2의 보수에서 2는 2^n 의 2다. 2의 보수는 2^n 을 만드는 수다.

 

예를 들어 2진수 011이 있다고 하면 011의 2의 보수는 101 이다. 2진수 011은 길이가 3이다. 길이를 2^n 의 n 에 대입하면 2^3 이고 이는 10진수로 표현하면 8이고 2진수로 표현하면 1000이다. 즉 011의 2의 보수는 1000을 만드는 수다. 011에서 101을 더하면 1000이 된다.

 

2진수 10진수
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1111 -1
1110 -2
1101 -3
1100 -4
1011 -5
1010 -6
1001 -7
1000 -8

 

2의 보수는 1의 보수에서 1을 더한다. 자리수를 모두 반전하고 1을 더하면 2의 보수를 구할 수 있다. 5의 2의 보수는 아래와 같이 구할 수 있다.

 

0101(2) -> 5
1010(2) -> 비트를 모두 반전
1011(2) -> 1을 더한다 (2의 보수)

 

0이 1개

부호 절대값, 1의 보수와 달리 2의 보수는 0이 1개다.

 

캐리는 무시

2의 보수에서 캐리는 무시하면 되는데 오버플로우는 막을 수 없다.

오버플로우는 비트로 표현할 수 있는 수의 범위를 벗어난 경우다. 캐리는 오류가 아니지만 오버플로우는 오류다.

 

 

  • 캐리 O, 오버플로우 X
  0001
+ 1111
------
1 0000
(캐리 발생)

1 + (-1)은 0이다.

이진수 연산을 하면 캐리가 발생하는데 캐리를 제외하고 나머지 비트를 보면 0000으로 0이 나왔다. 캐리는 무시한다.

 

 

  • 캐리 X, 오버플로우 O
  0001
+ 0111
------
  1000

1 + 7은 8이다.

4비트에서 2의 보수로 표현할 수 있는 수의 범위는 -8 ~ 7이다. 8은 4비트에서 표현할 수 있는 수의 범위를 벗어났다.

1000은 2의 보수에서 -8이다. 오버플로우가 발생해서 올바른 연산 결과가 나오지 않았다.

 

 

  • 캐리 X, 오버플로우 X
  0001
+ 0011
------
  0100

1 + 3은 4다.

이 경우 캐리와 오버플로우 모두 발생하지 않았다. 정상적인 연산 결과가 나왔다.

 

 

  • 캐리 O, 오버플로우 O
  1110
+ 1001
------
1 0111
(캐리 발생)

-2 + (-7) 은 -9다.

이진수 연산시 7이 나온다. 오버플로우가 발생해서 올바른 연산 결과가 나오지 않았다. 캐리도 발생했으나 캐리는 무시한다.

 

 

-0b1010

접두어 0b

print(0b1001)
# 9
print(~0b1001)
# -10 
# (~ 과 - 의 구분에 주의)

파이썬에서는 이진수임을 나타내기 위해 이진수 앞에 0b 접두어를 붙인다.

-0b1010은 접두어 0b 앞에 -가 붙었는데 이는 어떤 의미일까?

 

1010이 8비트 단위의 이진수였다면 1010은 00001010과 같다.

파이썬에서 정수 크기는 8비트 보다 훨씬 크기(파이썬의 정수 크기는 기본적으로 28바이트고 이보다 더 큰 수도 표현이 가능해서 사실상 무한대라고 볼 수 있다고 한다) 때문에 0b1010는 0b와 1010사이에 아주 많은 0이 생략되어 있다고 할 수 있다.

 

~(tilde)

파이썬에서 ~(tilde) 는 비트를 반전한다.

10진수 9는 2진수로 0b1001이고 0b와 1001사이에는 아주 많은 0이 생략됐다.

0b1001을 비트 반전하면 0b와 1001사이에 있던 아주 많은 0이 모두 1이 된다. 그대로 나타내려면 화면에 아주 많은 공간이 필요하다.

 

비트 반전은 2의 보수에서 1을 뺀 값과 같다.

비트 반전은 1의 보수를 구하는 것과 같고, 1의 보수는 2의 보수에서 1을 뺀 값이다. 반대로 2의 보수는 1의 보수에서 1을 더한 값이다. 파이썬은 음수를 나타내기 위해 2의 보수법을 사용한다.

 

2의 보수법에서 비트 반전
0010(2) -> 2
1101(2) -> -3

 

따라서 9를 비트 반전하면 -10이 된다.

-10을 2의 보수법으로 나타내려면 1을 아주 많이 표시해야 해서 화면에 많은 공간이 필요하다.

이를 간략하게 나타내기 위해 -를 사용할 수 있다.

 

-(minus)

이진수 0b1001(0b와 10001사이에 아주 많은 0이 생략된 상태)의 비트 반전은 0b111....0110 과 같다.

비트 반전한 수는 2의 보수법에서 -10이다. -10을 이진수로 간략하게 나타내기 위해 먼저 -와 10을 나눠서 다룬다.

10을 이진수로 표현하면 0b1010(0b 와 1010 사이에 아주 많은 0이 생략된 상태)고 이진수 앞에 -를 붙여서 표현하면 -0b1010이다.

 

bin(~0b1001)
# -0b1010
# 1001에서 앞에 -기호가 붙고 1001이 1010으로 바뀌었다

참고로 파이썬에서 9를 비트 반전한 이진수를 출력하려고 하면 자체적으로 -를 붙인다.

 

-0b1010을 10진수로 표현하면 -10이다.

0b뒤의 1은 부호비트가 아니다. 2진수 1010을 10진수로 변환하면 10이고 음수기호 -를 붙여서 -10이 된다.

즉, -는 2진수가 음수임을 (간략하게) 나타내기 위해 사용한다.

 

비트 반전

import sys


sys.getsizeof(0b1001)
# 28
sys.getsizeof(~0b1001 + 1)
# 28

10진수 9와 같은 2진수 0b1001의 크기는 28바이트다. 0b1001의 2의 보수는 ~0b1001 + 1 과 같다. 2의 보수 크기도 28바이트다. 28바이트를 비트로 표현하면 아주 많은 자리수가 필요하다.

파이썬에서 ~(tilde) 연산자는 비트를 반전한다. 비트 반전은 2의 보수에서 1을 뺀 값과 같다.

 

2진수    0010
1의 보수 1101
2의 보수 1110

4비트 이진수 2인 0010 의 1의 보수는 1101 이고 2의 보수는 1110 이다. 1의 보수에서 1을 더하면 된다.

여기서 2의 보수 기준으로 1101 은 -3이다. 즉, 2의 보수에서 1을 뺀 값과 같다.

2의 보수법을 사용하는 파이썬에서 비트 반전은 2의 보수에서 1을 뺀 값이다. 그래서 9의 비트 반전은 -10이 된다.

 

2의 보수와 비트 반전

  • 10진수에서 2의 보수는 -만 붙여주면 된다
5 -> -5

 

  • 10진수에서 비트 반전은 -붙이고 1을 뺀다
5 -> -6

 

  • 2진수에서 2의 보수는 비트 반전하고 1을 더한다
0001(2) -> 1001(2)

 

  • 2진수에서 비트 반전은 비트만 바꾼다
0001(2) -> 1000(2)

 

 

<참고>

파이썬 알고리즘 인터뷰

https://ko.wikipedia.org/wiki/%EC%A0%95%EC%88%98

https://ko.wikipedia.org/wiki/%EC%B5%9C%EC%83%81%EC%9C%84_%EB%B9%84%ED%8A%B8

https://ko.wikipedia.org/wiki/%EC%B5%9C%ED%95%98%EC%9C%84_%EB%B9%84%ED%8A%B8\

https://ko.wikipedia.org/wiki/1%EC%9D%98_%EB%B3%B4%EC%88%98

https://namu.wiki/w/%EC%A0%95%EC%88%98

https://ndb796.tistory.com/4

https://kangdy25.tistory.com/50

https://hs-archive.tistory.com/26

https://cosmosproject.tistory.com/569

https://hemahero.tistory.com/29

https://hemahero.tistory.com/31

https://hemahero.tistory.com/32

https://blog.naver.com/piyoro/221774762703

https://en.wikipedia.org/wiki/Two%27s_complement

https://ko.wikipedia.org/wiki/2%EC%9D%98_%EB%B3%B4%EC%88%98

https://seollal.tistory.com/700

https://blog.naver.com/hjyang0/183698525

https://tyoon9781.tistory.com/entry/python-int-size-28bytes

https://velog.io/@toezilla/1D1Q-001.-Python%EC%9D%98-int-%EC%9E%90%EB%A3%8C%ED%98%95%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B2%94%EC%9C%84%EA%B0%80-%EB%AC%B4%EC%A0%9C%ED%95%9C%EC%9D%BC%EA%B9%8C

https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex

https://man-25-1.tistory.com/60

name vs referencedColumnName

엔티티 필드에 연관관계를 설정할 때 @JoinColumn 어노테이션을 사용할 수 있다.

@JoinColumn 의 속성 중 name 과 referencedColumnName 가 혼동돼서 정리해두려고 한다.

두 속성은 외래키와 연관이 있다. 외래키는 한 테이블에서 다른 테이블의 행을 식별하기 위해 사용한다.


name

The name of the foreign key column.


자신의 테이블에 선언한 외래키 컬럼명이다.

다른 테이블의 행을 식별하기 위해 해당 테이블에 선언한 외래키의 컬럼명을 뜻한다.


referencedColumnName

The name of the column referenced by this foreign key column.


참조하는 테이블에 선언된 컬럼명이다.

자신의 테이블에 선언한 외래키로 조회하는 참조 테이블의 컬럼명을 나타낸다.

별도로 선언하지 않으면 디폴트로 참조 테이블의 PK(Primary Key) 컬럼명이 설정된다.


예시

create table post 
(
    id int primary key auto_increment,
    title varchar(50) not null,
    content varchar(200) not null
);
create table comment 
(
    id int primary key auto_increment,
    post_id int not null,
    content varchar(200) not null,
    foreign key (post_id) references post(id)
);

post, comment 테이블의 구조는 위와 같다.



@Entity
@Table(name = "post")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private String title;

    @Column(length = 200, nullable = false)
    private String content;

    @OneToMany(mappedBy = "post")
    private List<Comment> comments;
}
@Entity
@Table(name = "comment")
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    @Column(length = 200, nullable = false)
    private String content;
}

post, comment 테이블을 엔티티로 각각 Post, Comment 로 표현했다.

두 엔티티는 1:N 양방향 연관관계를 맺는다.


Comment 엔티티의 @JoinColumn 어노테이션의 name 속성 값으로 post_id 를 설정했다. comment 테이블에서 post 테이블의 id 컬럼을 조회할 외래키를 post_id 로 설정했다. name 속성 값은 이 post_id 를 가리킨다.


위 예시에서 Comment 엔티티의 @JoinColumn 어노테이션에 referencedColumnName 을 작성하지 않았지만 만약에 작성한다면 id 로 작성하면 된다. post 테이블의 pk 컬럼명이 id 다.



<참고>

https://ko.wikipedia.org/wiki/%EC%99%B8%EB%9E%98_%ED%82%A4

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/joincolumn

https://velog.io/@beomdrive/JPA-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-JoinColumn

'Dev > Database' 카테고리의 다른 글

트랜잭션 격리 수준  (0) 2023.08.25
Clustered Index & Non Clustered Index  (0) 2023.05.10
TypeORM - getMany vs getRawMany  (0) 2023.02.21

IP 주소

IP 주소에서 IP 는 Internet Protocol 을 나타낸다. Internet Protocol 은 네트워크간에 이동하는 패킷 단위로 나눈 네트워크 데이터를 정확한 목적지로 보내기 위한 규약이다.


IP 주소는 Internet 에 연결된 모든 기기가 갖는 고유한 식별자다. IP 주소는 네트워크 ID (Network ID) 와 호스트 ID (Host ID) 로 구성된다. 네트워크 ID 가 네트워크 대역이라면 호스트 ID 는 같은 네트워크 대역을 공유하는 기기 하나라고 볼 수 있다.


IP 클래스와 CIDR

IP 주소는 IPv4, IPv6 2가지 종류가 있다. IP 클래스는 IPv4 주소에서 존재하는 방식이고 1993년부터는 CIDR 로 대체됐다.

IPv4 주소는 32비트의 이진수로 구성되어 있고 32비트를 8비트씩 4개로 나눠서 나타낸다. 각 8비트를 옥탯(Octet) 이라고 부른다.


IP 클래스는 5개 종류가 있고 클래스마다 네트워크 ID 와 호스트 ID 의 크기가 다르다. 각 IP 클래스는 고정된 네트워크, 호스트 크기를 갖는다.


네트워크 호스트 첫번째 옥텟의 고정 비트 IP 범위
클래스 A 8비트 24비트 0 0.0.0.0 ~ 127.255.255.255
클래스 B 16비트 16비트 10 128.0.0.0 ~ 191.255.255.255
클래스 C 24비트 8비트 110 192.0.0.0 ~ 223.255.255.255
클래스 D 1110 224.0.0.0 ~ 239.255.255.255
클래스 E 1111 240.0.0.0 ~ 255.255.255.255

클래스 D 멀티캐스팅용, 클래스 E는 예약주소라 일반적으로 사용되지 않는다고 한다. AWS, Cloudflare 문서에서는 클래스 D, E 를 제외하고 클래스 A, B, C 만 다룬다.


클래스 A 에서는 하나의 네트워크가 2 ^ 24개의 호스트, 클래스 B는 하나의 네트워크가 2 ^ 16 개의 호스트, 클래스 C 는 하나의 네트워크가 2 ^ 8개의 호스트를 갖는다.


CIDR

하나의 IP 를 할당받은 사용자는 호스트가 100가 필요한데 할당받은 IP 는 호스트 200개까지 가질 수 있다고 하면 100개가 낭비된다. IP 클래스는 고정된 네트워크, 호스트 크기를 갖기 때문에 호스트 크기를 유연하게 조절하기 어렵다. 이를 개선하기 위해 CIDR 이 나오게 된다. CIDR 은 1993년부터 IP 클래스를 대체해서 사용되고 있다.


CIDR 은 IP 주소에 접두어 길이를 함께 표기한다. 접두어 길이는 네트워크 ID 의 비트 수를 의미한다.


// IP 주소
255.255.255.128 /25

위와 같은 IP 주소는 네트워크 ID 가 25비트고 호스트 ID 가 7비트로 이루어진다. IP 클래스와 달리 네트워크 ID 를 8, 16, 24 비트 외에 세분화해서 나눌 수 있다.

접두어의 길이별로 할당할 수 있는 IP 주소 갯수는 여기 서 확인할 수 있다.


서브넷 마스크

네트워크를 보다 효율적으로 관리하기 위해 네트워크를 보다 작은 단위의 네트워크로 나눈다. 이렇게 나눠진 네트워크를 서브넷이라고 하고 서브넷으로 나누는 행위를 서브네팅이라고 한다. 서브네팅을 하려면 서브넷 마스크가 필요하다.


IP 주소를 통해 한 기기에서 다른 기기로 데이터를 전송하려면 데이터를 수신할 기기가 어떤 네트워크 대역에 있는지 알아야 한다. 서브넷 마스크를 통해 수신할 기기가 어떤 네트워크 대역에 있는지 알 수 있다.

서브넷 마스크의 네트워크 ID 는 1로 이루어져 있고, 호스트 ID 는 0으로 이루어져 있다.


네트워크 ID (네트워크 접두사, 서브넷 네트워크, 네트워크 주소)

IP 주소와 서브넷 마스크를 AND 연산하여 IP 주소의 네트워크 ID 를 구할 수 있다. 이 네트워크 ID 를 위키피디아 에서는 네트워크 접두사 로 표현하고, 이분 은 서브넷 네트워크로 표현하고, 이분 은 네트워크 주소로 표현하고 있다.


// IP 주소
192.168.5.96 /24
// 2진수로 표현하면
11000000.10101000.0000101.01100000
// 접두어 24는 (서브넷 마스크)
11111111.11111111.11111111.00000000
// AND 연산하면
11000000.10101000.0000101.00000000
// 10진수로 바꾸면
192.168.5.0

IP 주소 192.168.5.96 /24가 있다고 가정한다.


이 주소의 네트워크 ID 는 32비트 중 앞의 24비트를 차지하므로 192.168.5.0이 되고, 호스트 ID 32비트 중 네트워크 ID 를 제외한 8비트를 차지하므로 0.0.0.96이 된다.


IP 주소 192.168.5.96 /24를 서브넷팅해서 각 서브넷에 128개의 호스트를 할당할 수 있도록 하려면 서브넷 마스크의 비트수를 1 늘리면 된다.


/24는 호스트 길이가 8비트라 2 ^ 8인 256개의 호스트를 하나의 네트워크 ID 가 가질 수 있다.

/25로 서브넷 마스크를 설정하면 호스트 길이가 7비트라 2 ^ 7인 128개의 호스트를 하나의 네트워크 ID 가 가질 수 있다.


// 서브넷 마스크
11111111.11111111.11111111.10000000
// 10진수로 바꾸면
255.255.255.128

/25로 서브넷 마스크를 설정하면 서브넷 갯수는 2개가 되고 각 서브넷 마다 호스트 128개를 갖게 된다.


// 각 서브넷의 Network ID
11000000.10101000.0000101.0
11000000.10101000.0000101.1
// 11000000.10101000.0000101.0 의 IP 주소 범위
11000000.10101000.0000101.00000000 ~ 11000000.10101000.0000101.01111111
// 11000000.10101000.0000101.1 의 IP 주소 범위
11000000.10101000.0000101.10000000 ~ 11000000.10101000.0000101.11111111

<참고>

https://www.cloudflare.com/ko-kr/learning/network-layer/internet-protocol/

https://aws.amazon.com/ko/what-is/cidr/

https://nordvpn.com/ko/blog/what-is-subnet-mask/

https://ko.wikipedia.org/wiki/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC_%ED%81%B4%EB%9E%98%EC%8A%A4

https://ko.wikipedia.org/wiki/CIDR

https://ko.wikipedia.org/wiki/IPv4

https://ko.wikipedia.org/wiki/%EB%B6%80%EB%B6%84%EB%A7%9D

https://limkydev.tistory.com/166

https://code-lab1.tistory.com/34

https://hwannny.tistory.com/86

https://velog.io/@hidaehyunlee/%EB%84%B7%EB%A7%88%EC%8A%A4%ED%81%ACNetmask%EC%99%80-%EC%84%9C%EB%B8%8C%EB%84%B7%EB%A7%88%EC%8A%A4%ED%81%ACSubnetmask

https://kldp.org/node/127933

https://m.blog.naver.com/wb__/221097841823

트랜잭션 격리 수준

트랜잭션의 격리 수준(isolation level) 이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.


트랜잭션 격리 수준은 서로 다른 트랜잭션이 같은 데이터에 접근할때 데이터를 처리하는 방법과 관련이 있다.


격리 수준은 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 4가지로 나눌 수 있다.

격리 수준에 따라 발생할 수 있는 문제가 달라지는데, 이 문제는 Dirty Read, Non Repetable Read, Phantom Read 3가지가 있다.


Dirty Read Non Repetable Read Phantom Read
READ UNCOMMITTED O O O
READ COMMITTED X O O
REPETABLE READ X X O (MySQL InnoDB, PostgreSQL 는 X)
SERIALIZABLE X X X

격리 수준

글에서 언급할 트랜잭션 A, 트랜잭션 B 는 트랜잭션을 구분하기 위해 임의로 정한 트랜잭션 이름이다.


READ UNCOMMITTED

트랜잭션 A 에서 변경한 내용을 commit 여부와 관계없이 트랜잭션 B 에서도 볼 수 있는 격리 수준이다. 이 현상을 Dirty Read 라고 한다.


Dirty Read 뿐만 아니라 Non Repeatable Read 와 Phantom Read 도 발생한다.



READ COMMITTED

트랜잭션 A 에서 commit 한 데이터만 트랜잭션 B 에서 볼 수 있으나 트랜잭션 B 에서 동일한 데이터를 조회할 때 결과가 서로 다른 Non Repeatable Read 문제가 발생할 수 있다. Non Repeatable Read 는 Update 쿼리와 연관해서 생각해볼 수 있다.


트랜잭션 A 가 시작하고 한 데이터를 update 했으나 commit 을 아직 하지 않은 상태에서 트랜잭션 B 가 동일한 데이터를 조회하면 A 가 update 하기 전의 결과가 조회된다. 그러나 A 가 commit 을 하고나서 B 의 트랜잭션이 끝나기 전에 다시 B 가 동일한 데이터를 조회하면 A 가 update 한 결과가 조회된다.


한 트랜잭션 안에서 동일한 데이터를 조회할 때 서로 다른 결과가 나올 수 있다.


Dirty Read 는 발생하지 않으나 Non Repeatable Read 뿐만 아니라 Phantom Read 도 발생한다.



REPEATABLE READ

앞에서 언급한 Dirty Read, Non Repeatable Read 문제는 발생하지 않으나 REPEATABLE READ 격리 수준에서는 Phantom Read 문제가 발생할 수 있다.


단, MySQL 의 InnoDB 와 PostgreSQL 에서는 REPEATABLE READ 격리 수준에서 Phantom Read 문제가 발생하지 않는다. MySQL InnoDB 은 디폴트 격리 수준으로 REPETABLE READ 를 사용한다.


Phantom Read 는 이름에서 느껴지듯 다른 트랜잭션의 변경에 의해 트랜잭션 내에서 데이터가 마치 유령처럼 생겼다 없어졌다 하는 현상이 발생한다. Phantom Read 는 Insert 쿼리와 연관지어 생각해볼 수 있다.


트랜잭션 A 에서 Insert 쿼리로 테이블에 (Idx: 2, Name: Steve Jobs) row 를 추가한다고 할때 insert 쿼리가 발생하기 전에 트랜잭션 B 에서 트랜잭션을 시작하고 이 테이블의 모든 row 를 조회했을 때는 (Idx: 2, Name: Steve Jobs) 가 보이지 않는다.


그러나 A 의 insert 가 발생하고 commit 까지 끝나고 나서 B 의 트랜잭션이 끝나기 전에 다시 이 테이블의 모든 row 를 조회했을 때 이전에 없었던 (Idx: 2, Name: Steve Jobs) 가 보이게 되는 현상이 Phantom Read 다.




SERIALIZABLE

가장 엄격한 격리 수준으로 Dirty Read, Non Repeatable Read, Phantom Read 문제가 발생하지 않는다. 그만큼 동시 처리 성능은 떨어지지만 데이터 정합성이 높다.


<참고>

Real MySQL 8.0 1권

https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html

https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_consistent_read

https://www.postgresql.kr/blog/pg_phantom_read.html

https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED

https://hudi.blog/transaction-isolation-level/

https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-ver16

'Dev > Database' 카테고리의 다른 글

JPA - @JoinColumn  (0) 2024.04.11
Clustered Index & Non Clustered Index  (0) 2023.05.10
TypeORM - getMany vs getRawMany  (0) 2023.02.21

1. Dependency Injection (의존성 주입)

NestJS 에서 의존성 주입은 IoC Container 에 의해서 수행될 수 있다. 개발자는 직접 의존성을 주입하거나, 인스턴스를 생성하거나, 혹은 초기화할 필요가 없다.

NestJS 의 Provider 또한 IoC Container 의 의존성 주입으로 사용할 수 있다. 의존성 주입을 하는 Provider 의 타입을 이 Provider 가 상속 받은 추상 클래스로 선언하고 싶었다.

의존성 주입을 통해 의존할 대상을 내부에서 직접 생성하는게 아니라 외부로부터 전달 받을 수 있다. 내부에서 직접 의존할 대상을 생성하지 않게 되면서 런타임에 다양한 인스턴스를 외부에서 주입 받을 수 있다.

예를 들어 아래와 같은 추상 클래스 Car 를 상속받은 Mercedes, BMW 클래스가 있다고 가정한다.


abstract class Car {
  abstract drive(): void;
}

class Mercedes extends Car {
  @override
  drive() {
    console.log('mercedes');
  }
}

class BMW extends Car {
  @override
  drive() {
    console.log('bmw');
  }
}

Driver 클래스에서 Car 를 주입 받는다. 여기서 car 의 타입을 Mercedes 나 BMW 가 아닌 Car 로 선언했다.


class Driver {
  private car: Car;

  constructor(car: Car) {
    this.car = car;
  }

  void operate() {
    car.drive();
  }
}

Driver 의 인스턴스를 생성할때 Mercedes, BMW 중 원하는 클래스를 주입한다. 만약에 Driver 의 car 변수의 타입을 Mercedes, BMW 둘 중 하나로 선언하면 선언한 타입에 따라 Mercedes, BMW 둘 중 하나만 주입할 수 있다.


const driver1: Driver = new Driver(new Mercedes());
driver1.operate();
// 'mercedes'

const driver2: Driver = new Driver(new BMW());
driver2.operate();
// 'bmw'

2. Abstract Class Injection (추상 클래스 주입)

이를 응용해서 NestJS 에서도 추상 클래스 타입으로 의존성을 주입받고 싶었으나 원하는대로 되지 않았다.

비슷한 기능을 하는 여러 클래스들을 추상 클래스로 묶고 싶었다. 예를 들어 포인트를 적립하는 기능을 개발하는데 포인트 종류가 여러개면 포인트 종류별로 클래스를 만들 수 있다.

포인트 클래스의 기능은 공통적으로 적립 기능인데 클래스마다 구현이 달라진다. 포인트 종류가 추가될 때마다 클래스도 추가 되어야 하는데 만약 메소드명이 클래스마다 다르다면 같은 기능이더라도 각 클래스마다 호출할 때 메소드명이 무엇인지 파악해야 한다. 이러한 불편함을 해결하기 위해 상위 클래스로 추상 클래스를 사용할 수 있다.


export abstract class Point {
  abstract savePoint(): void;
}

포인트 종류가 추가되어 새 클래스를 구현할때 이제 Point 를 상속 받아서 추상 메소드인 savePoint 를 구현하도록 할 수 있고 메소드를 호출할 때도 동일한 메소드명이 호출되어서 코드를 파악하기도 편해졌다.


@Injectable
export class APointService extends Point {
  savePoint() {
    console.log('a-point');
  }
}
@Injectable
export class BPointService extends Point {
  savePoint() {
    console.log('b-point');
  }
}

PointModule 에서는 PointFactoryModule 을 import 해온다. 그리고 PointController, PointService 를 각각 controller 와 provider 로 등록한다.


@Module({
  imports: [PointFactoryModule],
  controllers: [PointController],
  providers: [PointService],
})
export class PointModule {}

PointController 에서 PointService 의 savePoint 메소드를 호출한다.


@Controller('point')
export class PointController {
  constructor(
    private readonly pointService: PointService,
  ) {}

  @Post('point')
  async savePoint() {
    return await this.pointService.savePoint();
  }
}

PointService 에서는 PointFactoryService 를 주입 받아 사용한다. savePoint 메소드에서 getPointService 메소드를 통해 APointService 혹은 BPointService 를 리턴받아 해당 클래스에 정의한 savePoint 메소드를 호출하게 된다.

편의상 여기서는 getPointService 의 인자를 직접 PointType.A 로 넣어줬다.


@Injectable()
export class PointService {
  constructor(
    private readonly pointFactoryService: PointFactoryService,
  ) {}

  async savePoint() {
    return this.pointFactoryService
      .getPointService(PointType.A)
      .savePoint();
  }
}

팩토리 패턴 을 통해 PointFactoryService 에서 Point 클래스를 상속받은 서비스를 찾아준다. APointService, BPointService 를 PointFactoryService 의 providers 프로퍼티에 등록한다. Nest IoC Container 에 의해 PointFactoryService 는 APointService, BPointService 를 주입 받아 사용할 수 있다.

APointService, BPointService 를 Custom Provider 로 등록한다. NestJS 에서 내부적으로 Custom Provider 여부를 어떻게 판단하는지는 마지막에 다룰 예정이다.


@Module({
  providers: [
    PointFactoryService,
    {
      provide: Point,
      useClass: APointService,
    },
    {
      provide: Point,
      useClass: BPointService,
    }
  ],
  exports: [PointFactoryService],
})
export class PointFactoryModule {}

PointFactoryService 에서는 주입받은 APointService, BPointService 를 pointMap 으로 선언한 Map 에 등록한다. getPointService 메소드에서 PointType 에 해당하는 key 로 pointMap 를 조회해서 value 로 등록한 서비스를 리턴한다.


@Injectable()
export class PointFactoryService {
  private pointMap = new Map<PointType, Point>();

  constructor(
    private readonly aPointService: Point,

    private readonly bPointService: Point,
  ) {
    this.pointMap.set(PointType.A, this.aPointService);
    this.pointMap.set(PointType.B, this.bPointService);
  }

  getPointService(pointType: PointType) {
    return this.pointMap.get(pointType);
  }
}
export enum PointType {
  A = 'a',
  B = 'b',
}

3. 문제 상황

위에서 언급한 PointService 코드를 다시 살펴보겠다. savePoint 메소드를 호출했을때 기대한 결과는 'a-point' 인데 실제로는 'b-point' 가 나온다.


@Injectable()
export class PointService {
  constructor(
    private readonly pointFactoryService: PointFactoryService,
  ) {}

  async savePoint() {
    return this.pointFactoryService
      .getPointService(PointType.A)
      .savePoint();
      // b-point
  }
}

4. 문제 원인

왜 이런 결과가 나왔는지 궁금해서 NestJS 의 소스코드를 보게 됐다. NestJS 의 소스코드가 굉장히 방대하기도 하고, 아직 모든 소스코드를 이해할 수 있는 역량이 되지 못해서 필요한 부분 위주로 살펴봤다.

소스코드에서 필요한 부분을 찾는 것 조차도 쉽지 않았는데 이분 의 글이 아주 많은 도움이 됐다. NestJS 의 의존성 주입은 1) 코드에 작성된 의존성들을 스캔한 후에 2) 스캔한 의존성들의 인스턴스를 생성하고 3) 이 인스턴스를 적용하는 과정을 거쳐서 이루어진다.

이번 글에서는 의존성 스캔을 중심으로 살펴볼 예정이다. 스캔한 의존성을 바탕으로 인스턴스를 생성하기 때문에 의존성 스캔을 어떻게 하느냐에 따라서 생성될 인스턴스가 결정된다.


// core/nest-factory.ts
export class NestFactoryStatic {
  private async initialize(
    module: any,
    container: NestContainer,
    graphInspector: GraphInspector,
    config = new ApplicationConfig(),
    options: NestApplicationContextOptions = {},
    httpServer: HttpServer = null,
  ) {
    // ...
    try {
      this.logger.log(MESSAGES.APPLICATION_START);

      await ExceptionsZone.asyncRun(
        async () => {
          await dependenciesScanner.scan(module); // 의존성 스캔
          await instanceLoader.createInstancesOfDependencies(); // 스캔한 의존성들의 인스턴스 생성
          dependenciesScanner.applyApplicationProviders(); // 인스턴스 적용
        },
        teardown,
        this.autoFlushLogs,
      );
    } catch (e) {
      this.handleInitializationError(e);
    }
  }
}
4-1. 의존성 스캔

의존성 스캔은 DependenciesScanner 클래스의 scan 메소드로 수행된다.

다음은 DependenciesScanner 클래스의 일부 코드다. 모듈들을 스캔한 후에 스캔한 모듈들의 의존성을 스캔한다. 여기서 모듈은 AppModule 과 AppModule 의imports 에 선언한 Module 들을 가리킨다.

모듈 스캔은 AppModule 을 중심으로 재귀적으로 AppModule 에 선언한 Module 들을 스캔한다. 그리고 이 모듈들의 의존성 스캔은 controllers, providers 등으로 선언한 의존성을 스캔한다. 이 글에서는 providers 를 중심으로 살펴볼 예정이다.


// core/scanner.ts
export class DependenciesScanner {
  public async scan(module: Type<any>) {
    await this.registerCoreModule();
    await this.scanForModules(module); // 모듈들 스캔
    await this.scanModulesForDependencies(); // (1) scanForModules 로 스캔한 모듈들의 의존성 스캔
    this.calculateModulesDistance();

    this.addScopedEnhancersMetadata();
    this.container.bindGlobalScope();
  } 

  public async scanModulesForDependencies(
    modules: Map<string, Module> = this.container.getModules(),
  ) {
    // modules -> ModulesContainer
    // ModulesContainer 는 Map<String, Module> 을 상속받은 클래스로
    // 키가 string, 값이 Module 이다
    // 
    // modules 에서 for loop 돌면서 token, { metatype } 꺼낸다
    // key 가 token 이고, value 는 Module 클래스인데 Module 의 metatype 속성을 구조 분해 할당으로 꺼낸다
    for (const [token, { metatype }] of modules) {
      await this.reflectImports(metatype, token, metatype.name);
      this.reflectProviders(metatype, token); // (2) 모듈의 provider 스캔
      this.reflectControllers(metatype, token);
      this.reflectExports(metatype, token);
    }
  } 

  public reflectProviders(module: Type<any>, token: string) {
    const providers = [
      // reflectMetadata 메소드는
      // MODULE_METADATA.PROVIDERS 를 key, module 을 target 으로
      // Reflect 모듈의 getMetadata 메소드를 호출한다
      // providers 를 키로 사전에 메타데이터 등록을 해놓은 provider 중에 
      // module 에 해당하는 객체를 찾는다
      ...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module),
      ...this.container.getDynamicMetadataByToken(
        token,
        MODULE_METADATA.PROVIDERS as 'providers',
      ),
    ];

    // 위에서 찾은 providers 를 forEach 문을 돌면서
    // insertProvider 메소드를 통해
    // NestContainer 의 변수인 modules (ModulesContainer 인스턴스) 로부터 Module 을 조회하여
    // 해당 Module 에 provider 를 등록한다
    providers.forEach(provider => {
      this.insertProvider(provider, token); // (3) 
      this.reflectDynamicMetadata(provider, token);
    });
  }

  public insertProvider(provider: Provider, token: string) {
    const isCustomProvider = this.isCustomProvider(provider);
    if (!isCustomProvider) {
      // custom provider 가 아니면 provider 를 바로 Module 에 등록한다
      return this.container.addProvider(provider as Type<any>, token);
    }

    // Global 로 선언한 Interceptor, Pipe, Guard, Filter 등을 담은 객체
    const applyProvidersMap = this.getApplyProvidersMap();
    const providersKeys = Object.keys(applyProvidersMap);
    // 해당 Provider 가 provide 키로 등록한 값을 조회하여 type 변수에 대입한다
    // ex> { provide: AService, useClass: AService }
    // 위의 경우 AService 가 type 변수에 담긴다
    const type = (
      provider as
        | ClassProvider
        | ValueProvider
        | FactoryProvider
        | ExistingProvider
    ).provide;

    // Provider 가 Global 로 선언한 Interceptor, Pipe, Guard, Filter 에 해당하지 않는다면
    // addProvider 메소드를 호출한다
    // 위에서 생성한 APointService, BPointService 등은 이 if 문을 충족해서
    // addProvider 메소드를 호출한다
    if (!providersKeys.includes(type as string)) {
      return this.container.addProvider(provider as any, token);
    }
    // UuidFactory의 mode 는 random, deterministic 두가지가 있는데
    // 이는 NestFactoryStatic 의 create 함수를 호출하는 방식에 의해 결정된다
    // NestFactoryStatic 의 create 함수는 NestJS 의 진입점인 
    // main.ts 의 bootstrap 함수에서 호출된다
    // 디폴트로는 create 에 AppModule 만 인자로 넣어서 호출하는데 
    // 선택적으로 두번째 인자로 NestApplicationOptions 를 넣어줄 수 있다
    // 이를 따로 넣지 않는한 random mode 가 된다
    // random mode 는 randomStringGenerator 함수를 통해 (uid 모듈을 사용하여)
    // 21자리 랜덤한 문자열을 생성한다
    // 이렇게 생성한 랜덤 문자열을 uuid 변수에 담는다
    // (UuidFactory.get 에서 type.toString() 인자를 넣는데 random mode 에서는 이 인자를 사용하지 않고 인자와 관계없이 랜덤 문자열을 생성한다)
    const uuid = UuidFactory.get(type.toString());
    // type 과 uuid 를 결합해 providerToken 을 생성한다
    const providerToken = `${type as string} (UUID: ${uuid})`;

    let scope = (provider as ClassProvider | FactoryProvider).scope;
    if (isNil(scope) && (provider as ClassProvider).useClass) {
      scope = getClassScope((provider as ClassProvider).useClass);
    }
    this.applicationProvidersApplyMap.push({
      type,
      moduleKey: token,
      providerKey: providerToken,
      scope,
    });

    const newProvider = {
      ...provider,
      provide: providerToken,
      scope,
    } as Provider;

    const enhancerSubtype =
      ENHANCER_TOKEN_TO_SUBTYPE_MAP[
        type as
          | typeof APP_GUARD
          | typeof APP_PIPE
          | typeof APP_FILTER
          | typeof APP_INTERCEPTOR
      ];
    const factoryOrClassProvider = newProvider as
      | FactoryProvider
      | ClassProvider;
    // Scope 의 종류로 DEFAULT, TRANSIENT, REQUEST 가 있는데
    // 디폴트는 DEFAULT 다
    // 그래서 이 함수의 조건은 충족하지 않는다
    if (this.isRequestOrTransient(factoryOrClassProvider.scope)) {
      return this.container.addInjectable(newProvider, token, enhancerSubtype);
    }
    this.container.addProvider(newProvider, token, enhancerSubtype);
  }
}
4-2. 스캔한 의존성 등록

스캔한 의존성들을 등록한다.


// core/injector/container.ts
export class NestContainer {
  private readonly modules = new ModulesContainer();

  public getModules(): ModulesContainer {
    return this.modules;
  }

  public addProvider(
    provider: Provider,
    token: string,
    enhancerSubtype?: EnhancerSubtype,
  ): string | symbol | Function {
    // ModulesContainer 클래스에서 token 으로 module 을 조회한다
    // ModulesContainer 클래스는 Map<string, Module> 을 상속 받은 클래스다
    // 키가 token, 값이 Module 이다
    // token 은 위의 DependenciesScanner 에서 살펴본 providerToken 이다
    const moduleRef = this.modules.get(token);
    if (!provider) {
      throw new CircularDependencyException(moduleRef?.metatype.name);
    }
    if (!moduleRef) {
      throw new UnknownModuleException();
    }
    return moduleRef.addProvider(provider, enhancerSubtype) as Function;
  }
}

Module 클래스의 addCustomClass 메소드가 추상 클래스 주입시 발생한 문제의 핵심이다. @Module 의 providers 에 등록할때 provide, useClass 를 사용했었는데 이때 provide 를 키로 사용해서 Map 에 등록하게 된다. 결국 Map 에는 키가 중복되지 않아서 동일한 키로 여러개의 값을 등록할 경우 마지막에 등록한 값으로 덮인다.


export class Module {
  // Module 들을 ModulesContainer 에서 Map 으로 관리하듯이
  // 각 Module 도 Map 으로 자신의 provider 들을 _providers 변수로 관리한다
  // ModulesContainer -> Module 들 관리
  // Module -> Provider 를 포함한 자신의 의존성 관리
  private readonly _providers = new Map<
    InstanceToken,
    InstanceWrapper<Injectable>
  >();

  get providers(): Map<InstanceToken, InstanceWrapper<Injectable>> {
    return this._providers;
  }

  public addProvider(provider: Provider, enhancerSubtype?: EnhancerSubtype) {
    // CustomProvider 일 경우
    if (this.isCustomProvider(provider)) {
      if (this.isEntryProvider(provider.provide)) {
        this._entryProviderKeys.add(provider.provide);
      }
      return this.addCustomProvider(provider, this._providers, enhancerSubtype);
    }

    this._providers.set(
      // 키
      provider,
      // 값
      new InstanceWrapper({
        token: provider,
        name: (provider as Type<Injectable>).name,
        metatype: provider as Type<Injectable>,
        instance: null,
        isResolved: false,
        scope: getClassScope(provider),
        durable: isDurable(provider),
        host: this,
      }),
    );

    if (this.isEntryProvider(provider)) {
      this._entryProviderKeys.add(provider);
    }

    return provider as Type<Injectable>;
  }

  // CustomProvider 등록
  public addCustomProvider(
    provider:
      | ClassProvider
      | FactoryProvider
      | ValueProvider
      | ExistingProvider,
    collection: Map<Function | string | symbol, any>,
    enhancerSubtype?: EnhancerSubtype,
  ) {
    // useClass 속성으로 등록했다면 CustomClass 에 해당한다
    // CustomClass 일 경우
    if (this.isCustomClass(provider)) {
      this.addCustomClass(provider, collection, enhancerSubtype);
    } else if (this.isCustomValue(provider)) {
      this.addCustomValue(provider, collection, enhancerSubtype);
    } else if (this.isCustomFactory(provider)) {
      this.addCustomFactory(provider, collection, enhancerSubtype);
    } else if (this.isCustomUseExisting(provider)) {
      this.addCustomUseExisting(provider, collection, enhancerSubtype);
    }
    return provider.provide;
  }

  // CustomClass 등록
  public addCustomClass(
    provider: ClassProvider,
    collection: Map<InstanceToken, InstanceWrapper>,
    enhancerSubtype?: EnhancerSubtype,
  ) {
    let { scope, durable } = provider;

    const { useClass } = provider;
    if (isUndefined(scope)) {
      scope = getClassScope(useClass);
    }
    if (isUndefined(durable)) {
      durable = isDurable(useClass);
    }

    // provider 의 provide 키의 값을 token 으로 선언
    const token = provider.provide;
    // token 을 collection 의 키로 설정한다
    // 즉 provide 에 입력한 값이 키가 되는 것이다
    // 이 collection 은 Module 클래스에 private 으로 선언된
    // _providers 를 가리킨다 
    collection.set(
      token,
      new InstanceWrapper({
        token,
        name: useClass?.name || useClass,
        metatype: useClass,
        instance: null,
        isResolved: false,
        scope,
        durable,
        host: this,
        subtype: enhancerSubtype,
      }),
    );
  }
}

5. 결론

NestJS 는 내부적으로 각 Module 의 providers 를 Map 형태로 관리한다.

@Module 의 providers 에 서비스들을 등록할때 provide 키를 이용해서 등록할 경우 provide 에 선언한 값을 Map 의 키로 사용한다. 그래서 provide 에 동일하게 선언한 값이 여러개일 경우 마지막에 선언한 값으로 Map 에 덮여 쓰인다.

위의 경우 APointService, BPointService 가 모두 provide 로 Point 를 사용했는데 APointService 이후에 작성한 BPointService 로 덮인다. 그래서 PointFactoryModule 의 providers 에서 Point 키로 등록된 값은 BPointService 가 된다.


@Module({
  providers: [
    PointFactoryService,
    {
      provide: Point,
      useClass: APointService,
    },
    {
      provide: Point,
      useClass: BPointService,
    }
  ],
  exports: [PointFactoryService],
})
export class PointFactoryModule {}

PointFactoryService 에서는 APointService 와 BPointService 모두 사용하고 싶어도 BPointService 만 주입받게 된다. APointService 의 메소드를 호출할 수 없고 BPointService 의 메소드가 호출된다. 위의 경우 aPointService, bPointService 변수를 선언했지만 BPointService 2개가 변수명만 다르게 주입된 상황이다.


@Injectable()
export class PointFactoryService {
  constructor(
    private readonly aPointService: Point,
    private readonly bPointService: Point,
  ) {}
}

6. 테스트

다음의 테스트는 통과하지 못한다.


describe('PointFactoryService', () => {
  let service: PointFactoryService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PointFactoryService,
        {
          provide: Point,
          useClass: APointService,
        },
        {
          provide: Point,
          useClass: BPointService,
        }
      ],
    }).compile();

    service = module.get<PointFactoryService>(PointFactoryService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getPointService', () => {
    it('find APointService', () => {
      expect(service.getPointService(PointType.A)).toBeInstanceOf(APointService);
    });
  });
});

PointFactoryService 의 getPointService 메소드를 테스트했다. PointType.A 를 인자로 넣었는데 결과는 BPointService 가 조회된다.



추가 - custom provider 를 판단하는 방법

NestJS 가 custom provider 를 판단하는 방법은 Provider 타입에 provide 프로퍼티가 있는지 여부로 판단한다.


// common/interfaces/modules/provider.interface.ts
export type Provider<T = any> =
  | Type<any>
  | ClassProvider<T>
  | ValueProvider<T>
  | FactoryProvider<T>
  | ExistingProvider<T>;

isNil 은 NestJS 에서 내부적으로 undefined 혹은 null 여부를 판단할 때 쓰는 함수다.


// core/scanner.ts
public isCustomProvider(
  provider: Provider,
): provider is
  | ClassProvider
  | ValueProvider
  | FactoryProvider
  | ExistingProvider {
  return provider && !isNil((provider as any).provide);
}
// core/injector/module.ts
public isCustomProvider(
  provider: Provider,
): provider is
  | ClassProvider
| FactoryProvider
| ValueProvider
| ExistingProvider {
  return !isNil(
    (
      provider as
      | ClassProvider
      | FactoryProvider
      | ValueProvider
      | ExistingProvider
    ).provide,
  );
}
// common/utils/shared.utils.ts
export const isUndefined = (obj: any): obj is undefined =>
  typeof obj === 'undefined';
export const isNil = (val: any): val is null | undefined =>
  isUndefined(val) || val === null;

<참고>

https://docs.nestjs.com/providers

https://docs.nestjs.com/fundamentals/custom-providers

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

https://velog.io/@taeni-develop/Spring-%ED%8C%A9%ED%86%A0%EB%A6%AC%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B4%80%EB%A6%AC-8mvd0mqy

https://velog.io/@coalery/nest-injection-how

query parameter 란

URL 의 경로 뒤에 키, 값(key, value) 의 목록이다. parameter 라고 부르기도 하며 경로 뒤에 ? 로 시작한다. 각 키와 값은 & 로 구분한다.

이 값에 배열을 담는 방법에 대해 정리하려고 한다.


문제 상황

Flutter 의 Dio 플러그인을 이용해서 query parameter 의 값에 배열을 담으려 했다. 배열의 요소가 2개 이상일 경우는 문제가 없었으나 1개일 때는 배열이 사라지고 그 안의 요소만 전송되는 현상이 발생했다.

예를 들어 localhost 의 8080 포트에 foo 경로로 { bar: [123] } 을 보내고 싶을때 Flutter 의 Dio 플러그인을 이용해서 아래와 같이 보낼 수 있다.


try {
  List<int> barIdx = [123];

  Response<dynamic> result = await dio.get(
    "http://localhost:8080/foo",
    queryParameters: {
      'bar': barIdx,
    }
  );

  return result;
} on DioError catch (err) {
  return err;
}

NestJS 서버에서 요청을 받는다.

(query parameter 는 문자열이라 이를 정수로 변환하는건 별도로 처리해야 한다. NestJS 에서는 class-validator, class-transformer 등을 이용할 수 있다.)


@Get('foo')
async getFooData(
  @Query() query: any
) {
  console.log(query);
  // { idx: '123' }
}

Flutter 에서 [123] 인 barIdx 값을 foo 키의 값으로 담아서 보내는데 실제 서버에서 받으면 배열은 사라졌다. 그런데 barIdx 가 [123] 이 아니라 [123, 124, 125] 일 경우는 정상적으로 배열이 유지됐다.


try {
  List<int> barIdx = [123, 124, 125];

  Response<dynamic> result = await dio.get(
    "http://localhost:8080/foo",
    queryParameters: {
      'bar': barIdx,
    }
  );

  return result;
} on DioError catch (err) {
  return err;
}

@Get('foo')
async getFooData(
  @Query() query: any
) {
  console.log(query);
  // { idx: ['123', '124', '125'] }
}

url 로 직접 보낸다면 어떻게 보낼 수 있을까?

터미널에서 cURL 로 url 을 입력해서 보낸다면 barIdx 의 요소를 어떻게 작성해야 할까?

barIdx 의 요소 갯수만큼 parameter 를 반복하면 된다. barIdx 요소가 [123, 124, 125] 이렇게 총 3개라면 아래와 같이 보낼 수 있다.


curl -X GET "http://localhost:8080/foo?bar=123&bar=124&bar=125"

이 시점에 문제에 대한 힌트를 어느정도 얻을 수 있었다. 여러 개의 동일한 키를 보내면 값이 배열로 합쳐진다. 반면에 요소가 한 개일 경우는 한 개의 키를 보내서 값이 배열로 합쳐질 수 없다. 요청한 키의 값을 배열로 보내는지 여부를 알 방법이 없다.


try {
  List<int> barIdx = [123];

  Response<dynamic> result = await dio.get(
    "http://localhost:8080/foo",
    queryParameters: {
      'bar': barIdx,
    }
  );

  return result;
} on DioError catch (err) {
  return err;
}

다시 첫 부분인 '문제 상황'에서 언급한 코드로 돌아와 보면 위의 코드는 키가 bar 이고 값이 123 이라고 서버에서 판단하게 된다. 클라이언트에서 배열로 보냈는지 알 수가 없다.

아래와 같은 url 로 변경되어서 서버로 전송될 것이다. 그렇기 때문에 클라이언트가 배열을 보내도 서버에서는 배열임을 알 수 없다.


"http://localhost:8080/foo?bar=123"

해결방법

배열의 요소가 한 개인지 두 개인지와 관계없이 query parameter 의 값으로 배열을 보내고 싶다면 키에 [] 를 붙여주면 된다.


try {
  List<int> barIdx = [123];

  Response<dynamic> result = await dio.get(
    "http://localhost:8080/foo",
    queryParameters: {
      'bar[]': barIdx,
    }
  );

  return result;
} on DioError catch (err) {
  return err;
}

동일한 내용을 cURL 로는 아래와 같이 보낼 수 있다.


curl -X GET "http://localhost:8080/foo?bar[]=123"

@Get('foo')
async getFooData(
  @Query() query: any
) {
  console.log(query);
  // { idx: ['123'] }
}

위의 경우는 배열의 요소가 하나였는데 만약 { bar: [123, 124, 125] } 이와 같이 여러개 요소를 보낸다고 하더라도 이전과 동일하게 보낼 수 있다.


try {
  List<int> barIdx = [123, 124, 125];

  Response<dynamic> result = await dio.get(
    "http://localhost:8080/foo",
    queryParameters: {
      'bar[]': barIdx,
    }
  );

  return result;
} on DioError catch (err) {
  return err;
}

cURL 로 보낸다면 아래와 같이 키를 barIdx 의 요소 갯수만큼 반복해서 보낼 수 있다.


curl -X GET "http://localhost:8080/foo?bar[]=123&bar[]=124&bar[]=125"

@Get('foo')
async getFooData(
  @Query() query: any
) {
  console.log(query);
  // { idx: ['123', '124', '125'] }
}

<참고>

https://developer.mozilla.org/ko/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL

https://pub.dev/packages/dio

https://docs.nestjs.com/techniques/validation

https://stackoverflow.com/questions/6243051/how-to-pass-an-array-within-a-query-string

https://ryan-han.com/post/translated/pathvariable_queryparam/

클러스터형 인덱스 & 비클러스터형 인덱스

클러스터 인덱스와 비클러스터 인덱스에 대해서 정리를 해보려고 한다. MySQL, SQL Server 관련 자료를 중심으로 작성했다.


Page

클러스터형, 비클러스터형 인덱스는 B-tree 자료구조를 바탕으로 저장된다. B-tree 자료구조의 각 노드들은 페이지 단위로 관리된다. 페이지는 MySQL 의 경우 16KB 가 디폴트 크기다. MySQL 5.6 버전 이상부터는 페이지 크기를 innodb_page_size 변수 설정으로 변경할 수 있다.

page 는 SQL Server 에서 데이터 저장의 가장 기본 단위다. MySQL 에서는 InnoDB 엔진이 디스크와 메모리 간의 데이터를 전달할 수 있는 최소 단위다.

디스크 저장공간은 file 에 할당되는데 file 은 논리적으로 page 단위로 구분된다.


예시 테이블 정보

아래 이미지로 나타낼 데이터들의 가상 테이블 정보는 다음과 같다.

테이블명은 Company 이며 컬럼은 name 과 founder 로 구성된다. 10개의 데이터가 들어있다.


클러스터형 인덱스란?

데이터가 저장된 페이지 자체를 정렬하는 기준이 되는 인덱스다. 테이블 당 하나만 설정할 수 있다.

PRIMARY KEY 를 지정하면 MySQL (의 InnoDB 엔진)이나 SQL Server 는 자동으로 해당 키를 클러스터형 인덱스로 지정한다.

테이블에 PRIMARY KEY 가 없으면 UNIQUE 제약 조건(MySQL 은 모든 컬럼들 중 NOT NULL 로 정의된 첫번째 UNIQUE 컬럼) 이 지정된 컬럼을 클러스터형 인덱스로 설정한다.

만약에 PRIMARY KEY, UNIQUE 제약 조건 모두 없다면 MySQL 은 GEN_CLUST_INDEX 라는 row ID 값을 갖는 가상의 6 바이트 크기의 컬럼을 생성하여 클러스터형 인덱스로 설정한다. row ID 는 데이터 행이 insert 된 순서대로 증가한다.


위는 Company 테이블의 PRIMARY KEY 로 name 컬럼이 지정된 경우다.

클러스터형 인덱스는 B-tree 의 루트 페이지와 리프 페이지를 중심으로 살펴볼 수 있다. PRIMARY KEY 를 기준으로 물리적으로 데이터가 저장된 데이터 페이지 자체를 정렬한다.

루트 페이지에는 리프 페이지의 번호와 각 리프 페이지의 첫번째 데이터 정보를 갖는다. 리프 페이지는 실제로 데이터가 저장되어 있는 데이터 페이지와 같다.


비클러스터형 인덱스

위는 Company 테이블의 인덱스로 founder 가 지정된 경우다. PRIMARY KEY 는 없다.

비클러스터형 인덱스 SECONDARY INDEX(보조 인덱스) 라고도 하며 클러스터형 인덱스와 달리 물리적으로 저장된 데이터를 정렬하지 않는다.

비클러스터형 인덱스의 리프 페이지는 데이터 페이지와 다르다. 클러스터형 인덱스와 달리 데이터 페이지 자체도 정렬되어 있지 않은 힙 형태로 구성된다.

비클러스터형 인덱스는 한 테이블에 인덱스를 여러개 설정할 수 있고, 설정한 인덱스를 기준으로 정렬한 인덱스 정보를 갖는다.

루트 페이지는 리프 페이지의 페이지 번호와 각 페이지의 첫번째 데이터 정보를 갖는다. 리프 페이지는 인덱스로 설정한 키와 해당 키를 포함한 데이터가 저장된 데이터 페이지의 위치 정보를 갖는다. 이 위치 정보는 RID 라고 하며 파일(file) 번호, 데이터 페이지(page) 번호, 행(row) 번호로 구성된다.


클러스터형 인덱스 + 비클러스터형 인덱스

위는 Company 테이블의 name 컬럼이 PRIMARY KEY 로 지정 되었고, founder 컬럼이 SECONDARY INDEX 로 지정된 경우다.

클러스터형 인덱스와 비클러스터형 인덱스가 함꼐 구성된 경우는 클러스터형 인덱스로 사용되는 PRIMARY KEY 와 함께 별도로 비클러스터형 인덱스를 보조 인덱스로 설정한 경우다.

MySQL 에서 모든 비클러스터형 인덱스는 클러스터형 인덱스의 정보를 갖는다. 그래서 클러스터형 인덱스의 크기가 커지면 비클러스터형 인덱스의 크기도 커지게 된다.


비클러스터형 인덱스의 인덱스 행에서 데이터 행으로의 포인터를 행 로케이터라고 합니다. 행 로케이터의 구조는 데이터 페이지가 힙에 저장되는지 아니면 클러스터형 테이블에 저장되는지에 따라 다릅니다. 힙의 경우 행 로케이터는 행에 대한 포인터입니다. 클러스터형 테이블의 경우 행 로케이터는 클러스터형 인덱스 키입니다.


테이블에 비클러스터형 인덱스만 있을 경우에는 비클러스터형 인덱스의 리프 페이지는 RID 정보를 갖고 있고 이 RID 를 기준으로 데이터 페이지를 조회한다. 테이블에 비클러스터형 인덱스 뿐만 아니라 클러스터형 인덱스도 있을 경우에는 비클러스터형 인덱스는 클러스터형 인덱스 (컬럼) 값을 갖고 있고 클러스터형 인덱스를 기준으로 데이터 페이지에 접근한다.


<참고>

https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html

https://learn.microsoft.com/ko-kr/sql/relational-databases/indexes/clustered-and-nonclustered-indexes-described?view=sql-server-ver16

https://learn.microsoft.com/ko-kr/sql/relational-databases/indexes/create-clustered-indexes?source=recommendations&view=sql-server-ver16

https://hudi.blog/db-clustered-and-non-clustered-index/

https://velog.io/@gillog/SQL-Clustered-Index-Non-Clustered-Index

https://pangtrue.tistory.com/286

https://dev.mysql.com/doc/refman/8.0/en/innodb-physical-structure.html

https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_page

https://learn.microsoft.com/en-us/sql/relational-databases/pages-and-extents-architecture-guide?view=sql-server-ver16

https://ask.sqlservercentral.com/questions/39281/what-is-a-rid-lookup.html

'Dev > Database' 카테고리의 다른 글

JPA - @JoinColumn  (0) 2024.04.11
트랜잭션 격리 수준  (0) 2023.08.25
TypeORM - getMany vs getRawMany  (0) 2023.02.21

JavaScript engine 의 optimizing compiler 에 관해 정리를 해보려고 한다.

주요 내용은 구글 V8 팀 멤버의 발표를 기반으로 한다.


JavaScript engine 이란

JavaScript engine 은 대표적으로 V8, SpiderMonkey, Chakra 등이 있다.

기본적으로 JavaScript engine 은 JavaScript 코드를 machine code 로 변환하는 역할을 한다.


JIT

dynamically typed 한 언어임에도 어떻게 그렇게 빠를 수 있을까?

JIT(Just In Time) compilation 에 의해 가능하다. JIT compilation 을 통해 machine code 를 runtime 에 생성한다.

AOT(Ahead Of Time) compilation 을 수행하지 않아서 프로그램을 실행하기 전에 미리 compile 하지 않는다.

compile 과 execution 의 단계를 나누지 않고 동시에 진행한다.

C++ 는 compilation 과 execution 이 별개로 이루어진다면 JavaScript 는 compilation 과 execution 이 동시에 이루어진다.


Optimizing compiler

re-compile hot functions with type information from previous execution

de-optimize if the type has changed


JavaScript engine flow

발표 11분 20초 즈음에 나온 이미지다.


v8 engine flow

발표 12분 50초 즈음에 나온 이미지다.

baseline compiler 에 대응되는 v8 의 baseline compiler 는 Ignition 이 담당한다.

optimizing compiler 에 대응되는 v8 의 optimizing compiler 는 TurboFan 이 담당한다.


Ignition 은 baseline compiler 와 달리 machine code 가 아닌 bytecode 로 변환한다.

machine code 는 CPU 에 의해 직접 동작하는 코드라면 byte code 는 JVM 과 같은 가상 머신에서 동작하는 코드다. 가상 머신에서 동작하므로 OS 의 종류에 관계없이 동작할 수 있다.


byte code 는 machine code 와 source code 중간에 놓인 코드다. CPU 는 byte code 를 이해할 수 없어서 machine code 로 변환하는 작업이 필요하다.

machine code 와 byte code 에 대한 차이는 추가 학습이 필요하다.


re-compile & de-compile

modern JavaScript engine 은 compiler 를 대체로 2개 이상 갖는데, 그 중 하나가 Optimizing compiler 다.


Optimizing compiler 는 hot function 을 recompiling 한다. hot function 은 많이, 자주 사용되는 함수라는 의미를 갖는다.

hot function 은 많이 사용되기 때문에 최적화(optimize) 할 필요가 있다. 이때의 최적화는 machine code 에 대한 최적화를 의미한다.


Optimizing compiler 는 많이 사용되는 함수들에 대해서 정보를 수집하는데 특히 타입에 대한 정보가 중요하다. 최적화를 진행할때 이전에 사용됐던 타입과 유사한 타입을 사용할 것이라고 예상한다.

그러나 JavaScript 는 dynamically typed 하기 때문에 다른 타입을 사용할 수 있다. 그러면 Optimizing compiler 는 optimzed code 에 대한 de-optimize 를 수행하게 된다. 최적화 시켜놓은 코드를 다시 최적화하지 않은 상태로 되돌린다.


가능한 type 을 변경하지 말 것

optimizing compiler 는 이전에 사용한 type 정보를 바탕으로 최적화를 진행하므로 발표자는 이미 작성한 코드의 type 을 바꾸지 말라고 조언한다. 타입을 바꾸면 de-optimizing 을 진행하게 된다.


function load(obj) {
  return obj.x;
}

load 함수는 파라미터로 선언한 obj 의 프로퍼티 x 를 반환하는 단순한 함수다.

그렇지만 compiler 에게는 꽤 복잡한 일이다. compiler 는 obj 가 x 를 실제로 갖고 있는지, 프로토타입 체인 어딘가에 속해있는지, 메모리 어디에 x 가 저장되어 있는지 등을 파악해야 한다.


function load(obj) {
  return obj.x;
}

load({x: 4, y: 7});
load({x: 2, y: 9});
load({x: 1, y: 3});
load({x: 6, y: 1});
load({x: 3, y: 8});

load 함수를 호출하면서 객체를 인자로 넣는다.

x 와 y 의 값이 다를지라도 같은 프로퍼티(키와 값을 가리킨다) 키 x, y 가 계속 쓰이면서 프로퍼티 값의 타입이 변하지 않고 계속해서 number 로 호출된다.

compiler 는 이를 hot function 이라고 간주하고 최적화를 진행한다.


이에 대한 assembly code 코드에서는 parameter 에 대한 타입을 저장해둔다.

새롭게 함수가 호출되면 저장해둔 paramter 의 타입과 새로 들어온 parameter 의 타입이 같은지 비교한다.

assembly code 에서는 obj 객체의 프로퍼티 키에 대한 메모리 주소 정보를 갖고 있다. 프로퍼티 키에 대한 일종의 지름길을 갖고 있어서 빠르게 접근할 수 있다.


타입이 다면 프로토타입 체인을 살펴보거나 side effect 가 있는지 등을 검사하지 않아도 되고, 프로퍼티 키의 메모리 주소에 바로 접근해서 바로 값을 가져올 수 있다.

타입이 다르다면 de-optimize 를 명령하는 메모리 주소로 이동한다.


타입은 프로퍼티 값에 대한 것 뿐만 아니라 프로퍼티 키의 종류도 해당한다.


function load(obj) {
  return obj.x;
}

load({x: 4, a: 7});
load({x: 2, b: 9});
load({x: 1, c: 3});
load({x: 6, d: 1});

compiler 는 위의 코드에 대해 프로퍼티가 다르므로 같은 타입이 아니라고 판단한다.

assembly code 에는 load 함수 호출에 사용된 객체인 {x: 4, a: 7}, {x: 2, b: 9}, {x: 1, c: 3}, {x: 6, d: 1} 에 대한 각각의 메모리 주소 정보(객체 자체 뿐만 아니라 프로퍼티에 대한 정보도 포함한다)를 갖는다.


load 함수에 대한 새로운 호출이 발생하면 4개와 각각 비교하게 된다.

4개 중 하나라도 같은 타입이 있다면 메모리 정보를 바탕으로 빠르게 프로퍼티 값에 접근할 수 있다.

4개 중 같은 타입이 하나라도 없으면 de-optimize 를 진행한다.


모든 JavaScript engine 에 해당하는 내용인지는 모르겠지만 4번을 초과하는 비교를 수행하지 않는다고 한다. 특정 함수 호출에 사용된 객체의 메모리 정보를 4개 까지만 갖고 이 다음부터는 추가로 객체의 메모리 정보를 저장하지 않는다.

객체의 프로퍼티 키에 대해 개별적으로 방대한 기존 메모리 리스트에서 프로퍼티 키와 같은 정보가 있는지 조회한다(없으면 어떻게 되는지는 설명하지 않았고 expensive 한 call 이라고만 표현했다).


Write code that looks like statically typed.

성능을 위해 객체에 대한 타입을 변경하지 말라고 얘기한다.

같은 프로퍼티 키를 유지하면서 값이 없으면 프로퍼티를 빼지 않고 차라리 값을 undefined 로 설정하는 편이 낫다고 한다. compiler 는 같은 타입으로 간주할거고 그러면 메모리에 빠르게 접근할 수 있다.


function load(obj) {
  return obj.x;
}

load({x: 4, a: 7, b: undefined, c: undefined, d: undefined});
load({x: 4, a: undefined, b: 9, c: undefined, d: undefined});
load({x: 4, a: undefined, b: undefined, c: 3, d: undefined});
load({x: 4, a: undefined, b: undefined, c: undefined, d: 1});

참고

https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8_%EC%97%94%EC%A7%84

https://www.youtube.com/watch?v=p-iiEDtpy6I

https://evan-moon.github.io/2019/06/28/v8-analysis/

https://www.tutorialspoint.com/difference-between-bytecode-and-machine-code

https://byjus.com/gate/difference-between-byte-code-and-machine-code/

'Dev > JavaScript + TypeScript' 카테고리의 다른 글

var, let, const 호이스팅  (0) 2023.02.16

+ Recent posts