no image
[Network] 프로토콜 표준과는 다르게 실제로는 소켓(Socket)이 어떻게 식별되는가?
네트워크 프로그래밍에서 Socket의 의미와 TCP/IP Stack에 대하여 TCP/IP 스택은 프로토콜 스택으로, 컴퓨터 시스템의 관점에서 어플리케이션이 사용할 수 있는 네트워크 기능을 지원하는 역할을 한다. TCP/IP 스택은 전체를 4개의 레이어로 이루어진 시스템(Transport + Internet + Network Interface)에 제공되는 네트워크 기능의 구현과 Application 레벨에서 구현된 네트워크 기능으로 구분된다. 네트워크 통신을 위해서는 Application 레이어에서 통신이 가능하도록 System 레벨에서 지원되어야 한다는 것이 중요하다. Socket Programming 어플리케이션이 운영체제의 기능, 즉 커널 코드나 시스템 코드에 바로 접근하여 사용하는 것이 불가능하다..
2024.02.20
no image
[Network] 프로토콜 표준 스펙에서 정의한 Socket(소켓), Port(포트), TCP connection(연결) 개념
OSI 7 Layer, TCP/IP Stack네트워크 모델은 OSI 7 레이어와 TCP/IP 스택 두 가지로 구성되는데, TCP/IP 스택은 인터넷에서 실제 사용되는 프로토콜로 OSI 7 레이어는 네트워크 시스템의 개념적 모델이다.TCP/IP 스택은 IETF에서 관리하며, TCP, UDP, IP 프로토콜의 표준 스펙은 RFC문서에 정의되어 있다.TCP, UDP는 Transport 레이어에, IP 프로토콜은 Network 레이어에 속하는 프로토콜로 호환이 가능하다.Socket, Port, TCP 커넥션은 TCP/IP 스택이 발전하면서 나온 개념으로, 시스템 레벨에서 관리되는 네트워크 기능을 지원한다.TCP/IP 프로토콜 스택에서 Application layer와 Transport layer..
2024.02.20
[Database] SQL로 DB에 데이터를 추가(insert)하고 수정(update)하고 삭제(delete)하는 방법
SQL을 사용해서 데이터 추가하기 데이터를 추가할 때는 INSERT INTO 키워드를 사용하고, 그 뒤에 테이블 이름과 VALUES 값을 적어준다. INSERT INTO employees (name, department, salary) VALUES ('김철수', '영업', 3000000); 값들을 써줄 때 Attribute 순서에 맞게 값을 채워 넣어야 하는데, 이때 NULL값을 넣어야 하는 Attribute가 있다면 일단 NULL값을 넣어 넘겨야 한다. Query가 성공적으로 동작하면 해당 테이블에 데이터가 추가된다. ️️Primary키가 중복되는 경우 Primary key는 유일한 값이며 중복되면 안 된다. 이미 존재하는 Primary Key에 중복해서 데이터를 Insert하려 할 때 발생하는 에러..
2024.02.20
no image
[Spring] 빌드 관리 도구,Gradle과 Maven
빌드 관리 도구 빌드 관리 도구는 소스 코드에서 실행 가능한 애플리케이션을 자동으로 생성하는 데 도움을 주는 프로그램입니다. 이름에서 알 수 있듯이, 이는 다양한 작업을 개발하거나 스크립팅하는 데 중요합니다. 빌드 관리 도구는 다음과 같은 프로세스에 필요합니다. 빌드 툴은 임의의 명령을 실행합니다: 각 배포 시나리오는 독특하며, 각자가 파일을 다른 폴더로 복사하거나, 다른 형식으로 압축하거나, 다른 방식으로 정리해야 할 필요가 있습니다. 한 명령의 결과를 다른 명령에 적용합니다: 빌드는 거의 항상 여러 단계의 과정입니다. 소스 코드에서 문서를 생성합니다 . 소스 코드를 컴파일합니다 수집된 코드를 JAR 파일로 패키징합니다 패키지된 코드를 로컬/중앙 리포지토리 또는 서버에 설치합니다. 빌드 관리 도구에는 ..
2024.02.19
no image
[Spring] Spring Data JPA에서 getReferenceById vs findById (지연로딩과 즉시로딩)
Spring Data JPA로 데이터의 조회를 구현할 때, 사용할 수 있는 메서드중, getReferenceById와 findById 두 메서드가 있습니다. 이 글에서는 이 둘의 차이점을 비교해보려 합니다. getRefereceById (구: getOne(ID), findOne(ID), getById(ID)) 내부적으로 EntityManager의 getReference() 메서드를 호출합니다. getReference() 메서드를 호출하면 Proxy 객체를 리턴합니다. 실제 쿼리는 Proxy 객체를 통해 최초로 데이터에 접근하는 시점에 실행됩니다. (지연 로딩(Lazy Loading)) 이때 데이터가 존재하지 않는 경우에는 EntityNotFoundException이 발생합니다. 아래는 실제 구현체 코드입..
2024.02.18
[Database] SQL의 개념과 SQL로 데이터베이스를 정의하는 법(table 생성하기, 각종 데이터 타입, constraints, 키(key)들을 활용)
SQL의 기본 개념과 구조, RDBMS의 SQL 스펙 SQL은 Relational Database Management System에서 사용되는 표준 언어로, 데이터베이스를 정리하고 데이터를 조작하고 조회하는 등 종합적인 데이터베이스 작업이 가능하다. SQL에서 Table은 데이터 모델에서 Relation을 나타내며, attribute는 column, tuple은 row, domain은 domain으로 표현한다. SQL에서 Relation은 multiset of tuples로, 중복된 tuple을 허용한다. MySQL은 InnoDB를 기준으로 가장 많이 사용되는 RDBMS이다. 데이터베이스를 생성하고 사용할 때는 'create database' 명령어를 사용하여 새로운 데이터베이스를 만들고,..
2024.02.18
no image
[Spring] DAO (Data Access Object), DTO (Data Transfer Object)
DAO(Data Access Object) 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행함 단, Spring Data JPA에서 DAO의 개념은 Repository(레포지토리)가 대체 규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비스 레이어에서 데이터베이스에 접근해서 구현하기도 함 하지만 DAO를 서비스 레이어와 리포지토리 중간 계층을 구성하여 사용하면, 비즈니스 로직을 개발할 때 유지보수 측면에서 용이한 경우가 많음 서비스와 비즈니스 레이어 객체지향적인 설계에서는 서비스와 비즈니스 레이어를 분리해서 서비스 레이어에서는 서비스 로직을, 비즈니스 레이어에서는 비즈니스 로직을 수행해야 한다는 의견이 많다. ..
2024.02.18
no image
[Spring] Spring Data JPA, Repository
Spring Data JPA Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공 Springboot로 JpaRepository를 상속하는 인터페이스를 생성하면, 기존의 다양한 메소드를 손쉽게 활용할 수 있음 Repository Spring Data JPA가 제공하는 인터페이스 엔티티가 데이터베이스의 테이블과 구조를 생성하는 데 사용했다면, 리포지토리(Repository)는 엔티티가 생성한 데이터베이스에 접근하는 용도로 사용 리포지토리를 생성하려면 테이블과 엔티티에 대한 인터페이스를 생성하고, 아래 코드와 같이 JpaRepository를 상속받으면 됨 package org.spring.study.data.repository; impor..
2024.02.17

네트워크 프로그래밍에서 Socket의 의미와 TCP/IP Stack에 대하여

이미지 출처: https://www.guru99.com/images/1/093019_0615_TCPIPModelW1.png

  • TCP/IP 스택은 프로토콜 스택으로, 컴퓨터 시스템의 관점에서 어플리케이션이 사용할 수 있는 네트워크 기능을 지원하는 역할을 한다.
  • TCP/IP 스택은 전체를 4개의 레이어로 이루어진 시스템(Transport + Internet + Network Interface)에 제공되는 네트워크 기능의 구현과 Application 레벨에서 구현된 네트워크 기능으로 구분된다.
  • 네트워크 통신을 위해서는 Application 레이어에서 통신이 가능하도록 System 레벨에서 지원되어야 한다는 것이 중요하다.

Socket Programming

  • 어플리케이션이 운영체제의 기능, 즉 커널 코드나 시스템 코드에 바로 접근하여 사용하는 것이 불가능하다.
  • 시스템은 어플리케이션이 사용할 수 있는 인터페이스를 제공해야 하고, 네트워크 기능을 사용하고 싶다면 Socket을 통해서만 사용이 가능하다.
  • Socket 프로그래밍을 통해, 어플리케이션이 네트워크를 통해 다른 프로세스와 데이터를 주고 받을 수 있다.
  • Socket프로그래밍은 어플리케이션 개발자들이 Socket 인터페이스를 호출할 수 있도록 하는 프로그래밍 방식이다.
  • 따라서, 어플리케이션을 구현하는 개발자들은 Socket 프로그래밍을 사용하여 네트워크에서 데이터 주고받기를 구현할 수 있다.

Socket

  • 소켓은 컴퓨터 시스템이 제공하는 인터페이스로, 어플리케이션 개발자가 네트워크 상에서 데이터를 주고받을 수 있도록 도와준다.
  • 일반적으로 개발자가 소켓을 직접 조작하여 통신 기능을 구현하는 것은 적으며, 대부분은 라이브러리나 모듈을 통해 구현된다.
  • 라이브러리나 모듈을 열어보면 소켓을 활용해서 프로토콜을 구현했음을 알 수 있다.
  • 네트워크 통신 기능은 소켓을 활용하는데, 프로토콜 표준에 따라 소켓의 동작이 정의되며, 운영체제마다 소켓 형태로 네트워크 기능을 제공한다.

Socket 생성, 프로토콜 설정, 주소 할당 과정

  • 소켓 프로그래밍에서 프로토콜과 IP 주소, 포트 넘버를 할당하여 소켓을 생성한다. <protocol, IP address, port number>
  • 포트 넘버는 서버 측에서 반드시 명시를 해주어야 클라이언트 측에서도 해당 포트 넘버로 데이터를 보낼 수 있다.
  • 클라이언트 측에서는 주소를 바인딩할 필요가 없어 OS가 자동으로 할당한다.
import socket

# 서버 소켓 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 서버 주소 및 포트 설정
server_address = ('localhost', 8080)

# 서버 소켓 바인딩
server_socket.bind(server_address)

# 서버 소켓 리슨 시작
server_socket.listen()

# 클라이언트 연결 수락
client_socket, client_address = server_socket.accept()

# 클라이언트로부터 데이터 수신
data = client_socket.recv(1024)

위 코드에서 SOCK_STREAM은 프로토콜이며, server_socket.bind(server_address) 를 통해 IP와 Port number를 정의한다.

실제 구현에서 프로토콜과 IP Address, Port number를 통해 소켓을 유니크하게 식별할 수 있을까?

  • 실제 구현에서 UDP의 경우 IP Address, Port로 유니크하게 식별 가능하지만, TCP의 경우 IP와 포트 넘버로는 유니크하게 식별할 수 없다.
  • TCP 소켓은 Connection을 맺기 위해 클라이언트가 서버로 요청을 보내야하며, 서버는 항상 요청을 기다리는 소켓이 필요하다.

️TCP Connection에서 소켓 식별 과정

  • TCP Connection은 3-way hand shake를 통해 성립된다.
  • 클라이언트와 서버는 각각 소켓을 만들어 커넥션을 맺는다.
  • Connection이 맺어지면, 이후 데이터를 주고받기 위해 새로운 소켓을 만들어 사용한다.
  • 이 때 서버쪽에서 모든 소켓은 같은 IP 주소와 포트를 가지고 있음.
  • 이 경우, 어떤 소켓으로 데이터를 보내야 하는지 알 수 없으므로 Connection을 맺기 전에는 Listening 소켓으로 보내며, 맺은 후에는 src IP, src port 정보를 확인하여 적절한 소켓으로 전달함.

TCP 스펙에 따라 IP 주소와 Port number가 중복될 때 문제점

  • 클라이언트 소켓이 주소를 찾아주는데, 그 주소 할당 방식은 OS가 이전에 사용되지 않은 포트 넘버를 할당한다.
  • 그러나 이미 모든 포트 번호가 사용중인 경우, 동일한 IP 주소와 포트 번호를 가진 서로 다른 소켓이 생성될 수 있으나, TCP 스펙상 이는 허용되지 않는다.
  • 한 IP 주소와 포트 넘버를 가진 커넥션이 이미 존재하는 경우, TCP 스펙상 유니크한 값이 아니므로 해당 소켓이 다시 연결을 맺을 수 없다.
  • 따라서, 모든 커넥션은 포트와 IP 주소 값이 다르게 유니크해야 한다는 조건 때문에 같은 IP 주소와 포트 넘버를 가지고 있는 소켓이 추가 연결을 맺는 것이 불가능하다.

UDP Socket의 동작

  • TCP는 IP 프로토콜 위에서 동작하고 안정적으로 데이터를 전송하기 위해 Connection 기반으로 동작한다.
  • UDP는 IP 주소와 Port number로 소켓을 식별하며, Connection 개념이 없다. 
  • UDP는 데이터 손실이나 데이터 순서 변경 등의 불안정한 프로토콜로 동작한다.
  • UDP 소켓에서 데이터를 보낼 때마다 해당 UDP 소켓 주소를 지정하여 데이터를 전송하고, 받는 쪽에서도 해당 소켓 주소를 확인하여 데이터를 읽는다.

Port number

  • 포트 번호(Port number)는 16비트로 이루어지며, 0에서부터 65,535까지 사용 가능하다.
  • 포트 번호는 크게 3가지로 나뉘는데, 0 ~ 1023번은 Well known, System 포트 e.g.) HTTP(80), HTTPS(443), DNS(53), 1024 ~ 49151은 registered 포트(IANA에 등록된 포트) e.g.) MySQL DB(3306), Apache Tomcat(8080), 그리고 나머지는 Dynamic 포트이다.
  • 등록 포트는 IANA(인터넷 할당 번호 관리 기관)에서 번호가 등록되어 있는 포트를 의미하는데, MySQL을 위한 3306번, 아파치 웹 서버의 기본 포트 80 등등이 이에 속한다.

참고 자료

https://youtu.be/WwseO8l8rZc?si=tR33YDUZTjKBYXl2

 

OSI 7 Layer, TCP/IP Stack

  • 네트워크 모델은 OSI 7 레이어와 TCP/IP 스택 두 가지로 구성되는데, TCP/IP 스택은 인터넷에서 실제 사용되는 프로토콜로 OSI 7 레이어는 네트워크 시스템의 개념적 모델이다.
  • TCP/IP 스택은 IETF에서 관리하며, TCP, UDP, IP 프로토콜의 표준 스펙은 RFC문서에 정의되어 있다.
  • TCP, UDP는 Transport 레이어에, IP 프로토콜은 Network 레이어에 속하는 프로토콜로 호환이 가능하다.
  • Socket, Port, TCP 커넥션은 TCP/IP 스택이 발전하면서 나온 개념으로, 시스템 레벨에서 관리되는 네트워크 기능을 지원한다.

TCP/IP 프로토콜 스택에서 Application layer와 Transport layer

  • Transport, internet, link 레이어는 하드웨어/펌웨어 OS 레벨에서 구현/관리하며 네트워크 기능을 지원하는데 목적이다.
  • Application layer는 지원받은 네트워크 기능을 사용하는데 목적이 있다.
  • Application 레이어와 Transport 레이어 사이에 있는 소켓과 포트에 대한 개념과 역할을 이해해야 한다.

Socket과 Port

  • Socket과 Port는 어플리케이션에서 시스템(Transport layer)으로 데이터 전송을 위한 연결 통로를 뜻한다.
  • TCP/IP 스택 기술이 개발되던 1970~1980년대 초반부터 사용되었다.
  • 소켓이 연결된 프로세스는 네트워크 상의 다른 프로세스에게 데이터를 송수신할 수 있다.
  • 데이터 전송을 위해 연결된 통로가 없으면, 네트워크 기능을 사용할 수 없다.

Port: 프로세스와 연결된 데이터 패스 혹은 데이터 채널

  • 포트는 프로세스와 연결된 데이터 패스 혹은 데이터 채널 역할을 하는 예를 말한다.
  • 한 시스템 내에 여러 개의 포트가 있을 수 있기 때문에, 각 포트를 유니크하게 식별하기 위해, Port name을 통해 구분한다.
  • 프로세스 하나에 두 개 이상의 포트가 연결될 수 있다.

️1970~1980년대, 신뢰할 수 없는 인터넷 프로토콜 위에 안정적인 TCP 프로토콜 개발

  • 1970~1980년대에는 TCP/IP 스택의 프로토콜이 개발되고 정립되는 시기였다.
  • 프로세스끼리 통신하기 위해 포트가 필요하며, 이를 위해 인터넷 프로토콜이 데이터를 안정적으로 주고받게 하는 프로세스 간 통신 프로토콜인 TCP가 개발되었다.
  • Internet 프로토콜은 구조가 단순하여 속도가 빠르다는 점이 있지만, 데이터를 무결성과 순서를 보장하지 않는다는 특징이 있어서 Network 레이어 위에 Transport 레이어에서 동작하는 TCP 프로토콜을 통해 안정적인 데이터 전송을 실현하였다.

️ TCP(Transmission Control Protocol) 

  • TCP에서 말하는 Connection 은 프로세스 간 안정적이고 논리적인 통신 통로로, Connection을 열고(3-way connection) 데이터를 주고 받은 후 Connection을 닫는다. (4-way connection)
  • 이는 물리적인 통신 통로의 반대 개념이며, 논리적인 통신 통로는 Socket(소켓), Port(포트), TCP connection(연결) 개념을 포함한다.
  • 전송 과정에서 순서가 있고, 에러 체크를 포함한다.

TCP 3-way connection (TCP 3-Way Handshake)

이미지 출처:&nbsp;https://sjlim5092.tistory.com/35

1. SYN (Synchronization):

  • 클라이언트는 SYN 패킷을 서버에 보낸다. 이 때 ISN_client와 같은 sequence number를 포함시킨다.
  • 클라이언트는 SYN_SENT state에 진입한다. 

2. SYN-ACK (Synchronization-Acknowledgment

  • 서버는 클라이언트의 SYN 패킷 요청을 받고, SYN과 ACK를 함께 포함한 패킷을 클라이언트에게 전송한다.
  • 서버는 자체적으로 랜덤한 sequence number를 생성한다.
  • 서버는 SYN-RECEIVED state에 진입한다. 

3. ACK (Acknowledgment):

  • 클라이언트는 SYN-ACK 패킷을 받고 ACK을 서버에 보낸다.
  • 클라이언트는 ESTABLISHED state에 진입한다.
  • 서버는 클라이언트가 보낸 ACK 패킷을 받고 ESTABLISHED state에 진입한다.
  • 양쪽의 연결이 설정되어 데이터 전송이 가능해진다.

3-Way connection(HandShake)의 목적

  • sequence number는 양쪽에서 동기화하여 데이터의 정확한 전송과 순서를 유지한다.
  • SYN, SYN-ACK 패킷을 통해 양쪽이 통신이 가능한지의 상태를 확인하여 안정적인 연결을 확보한다. 

TCP 4-Way Handshake (Connection Termination)

이미지 출처:&nbsp;https://sjlim5092.tistory.com/37

1. FIN (Finish):

  • 클라이언트가 연결을 종료하고자 FIN 패킷을 보낸다.
  • 클라이언트는 FIN-WAIT-1 state에 진입한다.

2. ACK (Acknowledgment):

  • 서버는 클라이언트로부터 FIN 패킷을 받고 ACK 패킷을 보낸다.
  • 서버는 CLOSE-WAIT state에 진입한다.

3. FIN (Finish):

  • 서버가 연결을  닫을 준비가 되면, FIN 패킷을 보낸다.
  • 서버는 LAST-ACK state에 진입한다.

4. ACK (Acknowledgment):

  • 클라이언트는 서버로부터 FIN 패킷을 받고 마지막으로 ACK 패킷을 보낸다.
  • 클라이언트는 TIME-WAIT state 에 진입한다.
  • 이제 양쪽 모두 연결이 완전히 종료된다.

 

4-Way Connection(Handshake)의 목적

  • 양쪽이 통신의 종료를 확인한 후 종료하므로 데이터 손실이 없도록 한다.

Port

  • 인터넷 상에서 프로세스 간 통신을 하기 위해서는 상대방 프로세스의 포트를 식별할 수 있어야 함.
  • 이를 위해 TCP와 UDP에서는 포트 넘버를 16비트 숫자로 정의함.
  • 포트 넘버는 0에서부터 65,535 사이의 값을 가지며, 이것을 부르는 용어는 'Port' 또는 'Port number'가 있음.
  • 포트가 가지는 의미는 프로세스와 연결된 데이터 패스 혹은 데이터 채널을 의미할 수도 있고, 포트 넘버를 식별하기 위한 값일 수도 있음.
  • 이러한 이유로, 포트라는 용어는 의미에 따라 다르게 사용됨.

인터넷 상에서 어떻게 Port를 유니크하게 식별할까?

  • 포트 넘버는 인터넷상에서 포트를 유일하게 식별할 수 없으므로, 이에 추가적인 원소가 필요하다고 생각됨.
  • 그 때문에 호스트의 Internet Address(=IP)가 유일하게 식별할 수 있는 대상이므로, Host와 Port Number의 조합으로 Socket이 등장하는데, Socket은 인터넷상에서 유일해야 한다.
  • TCP/IP 스택에서 Socket은 Port를 유일하게 식별할 수 있는 '주소'로 볼 수 있으며, 또한 각각의 Socket은 인터넷상에서 유일해야 하는 특징이 있다.

Connection과 Socket

  • TCP에서 Connection은 유니크하게 식별해야 한다.
  • 한 커넥션에 대한 처리를 판단하기 위해, 커넥션은 하나의 소켓 쌍으로 식별 가능해야한다.
  • 이 소켓은 컴퓨터의 IP 주소와 포트 넘버를 가지고 있으며, 소켓 쌍이 유일하므로 한 커넥션도 유일하게 식별 가능하다.
  • 따라서 커넥션 연결에 요청하는 쪽과 받는 쪽 각각의 source와 destination 소켓이 있으며, source는 요청하는 쪽, destination은 받는 쪽을 의미한다.  
  • 각 소켓은 여러 커넥션에서 동시에 사용될 수 있으므로, 하나의 소켓은 여러 커넥션의 사용이 가능하다.
  • 한 프로세스에서 이용되는 서비스를 여러 프로세스가 공유할 수 있기 때문에, 해당 서비스를 제공하는 소켓은 다른 컴퓨터에 위치한 다른 프로세스와 커넥션하기 위해 사용될 수 있다.

UDP (User Datagram Protocol)

  • TCP와 더불어 같은 레이어의 UDP 프로토콜은 connectionless 프로토콜로, 연결을 맺고 있지 않고 데이터를 직접 주고 받는다.
  • UDP는 인터넷 프로토콜 위에서 동작하며, 소켓 개념 또한 IP 주소와 포트 넘버를 합친 형태이다.
  • 표준 문서에는 소켓이라는 단어가 빠져 있어서 UDP 표준 문서에서 'Socket'이라는 단어를 찾을 수 없지만, 실제로 UDP에서도 Socket 개념을 사용한다.
  • UDP는 간단한 에러체크 정도만 하고, 세밀한 체크는 하지 않는다.
  • UDP는 port number란 개념에서 TCP와 크게 차이가 없기 때문에 이후 자연스럽게 소켓 개념을 사용하기 시작한다.

Socket 개념을 이용한 TCP/IP 통신

  • TCP/IP 스택에서 Socket은 포트를 식별하기 위한 주소로 사용된다.
  • 소켓은 TCP와 UDP 프로토콜에 의해 사용되며, TCP 안에서만 사용되던 소켓 개념이 UDP까지 확장되면서 <protocol, ip address, port number)으로 유니크하게 식별된다.
  • 같은 IP 주소와 포트를 가진 프로세스 들끼리는 프로토콜이 달라야 소켓을 지정할 수 있다.
  • IP 주소가 다르면서 프로토콜과 포트 넘버가 같은 경우, 하나의 컴퓨터도 여러 개의 IP 주소로 소켓을 열 수 있다.
  • 인터넷상의 다른 프로세스들이 TCP통신을 위한 IP와 포트 번호를 할당하여 Connection을 맺고, 이런 Connection은 유일해야 한다.
  • 프로세스 각각은 IP 주소와 포트 번호를 가지고 있다.
  • 각 컴퓨터의 IP 주소와 포트 번호를 통해 이루어진 TCP 커넥션들은 소켓의 쌍으로 표현될 수 있어 유니크한 커넥션이어야 한다.
  • 소켓은 <src IP, src Port, dest IP, dest Port>의 구조로 이루어져있다.

Unique한 Connection을 식별하기

  • IP 주소와 port number를 비교하여 유니크한 커넥션을 식별한다.
  • IP 주소가 같으나 port가 다르면 유니크한 커넥션으로 본다.

참고 자료

https://youtu.be/X73Jl2nsqiE?si=8bfwucXJ8WWufjbo

SQL을 사용해서 데이터 추가하기

  • 데이터를 추가할 때는 INSERT INTO 키워드를 사용하고, 그 뒤에 테이블 이름과 VALUES 값을 적어준다.
INSERT INTO employees (name, department, salary)
VALUES ('김철수', '영업', 3000000);
  • 값들을 써줄 때 Attribute 순서에 맞게 값을 채워 넣어야 하는데, 이때 NULL값을 넣어야 하는 Attribute가 있다면 일단 NULL값을 넣어 넘겨야 한다.
  • Query가 성공적으로 동작하면 해당 테이블에 데이터가 추가된다.

️️Primary키가 중복되는 경우

  • Primary key는 유일한 값이며 중복되면 안 된다.
  • 이미 존재하는 Primary Key에 중복해서 데이터를 Insert하려 할 때 발생하는 에러는 각 DBMS마다 다르다.
  • MySQL: Duplicate entry '값' for key 'PRIMARY'
  • PostgreSQL: ERROR: duplicate key value violates unique constraint "constraint_name"
  • Oracle: ORA-00001: unique constraint (schema_name.table_name_pk) violated

Constraint를 위배하는 데이터를 넣으려는 경우

  • Show Create Table 키워드를 활용하면 Constraint의 구체적인 의미를 확인할 수 있다.
  • CHECK 제약 위배 에러 메시지: 
    - MySQL: CHECK constraint 'employees_salary_check' failed
    - PostgreSQL: ERROR: check constraint "employees_salary_check" violated
     -Oracle: ORA-00001: check constraint (employees_salary_chk) violated
  • NOT NULL 제약 위배 에러 메시지:
    - MySQL: Column 'name' cannot be null
    - PostgreSQL: ERROR: null value in column "name" violates not-null constraint "employees_name_not_null"
    - Oracle: ORA-00001: not null constraint (employees_name_nn) violated
  • Foreign Key 제약 위배 에러 메시지:
    - MySQL: Cannot add or update a child row: a foreign key constraint fails (orders_ibfk_1, CONSTRAINTorders_ibfk_1FOREIGN KEY (product_id) REFERENCESproducts(id))
    - PostgreSQL: ERROR: insert or update on table "orders" violates foreign key constraint "orders_ibfk_1" on table "products"
    - Oracle: ORA-00001: foreign key constraint (orders_fk) violated

️️️테이블에 데이터를 추가하는 쿼리 방법 3가지

테이블의 모든 애트리뷰트에 대응하는 값을 넣어주는 쿼리
INSERT INTO employees (name, department, salary)
VALUES ('김철수', '영업', 3000000);
일부 애트리뷰트에 대해서만 원하는 순서로 값을 넣는 쿼리
INSERT INTO employees (department, name, salary)
VALUES ('영업', '김철수', 3000000);​
한 번에 여러 개의 데이터를 한 테이블에 넣는 쿼리
INSERT INTO employees (name, department, salary)
VALUES ('김철수', '영업', 3000000), ('박지영', '마케팅', 3500000);

SQL 쿼리를 이용해 데이터 수정 하기

UPDATE 테이블명 SET 열1 = 값1, 열2 = 값2, ... WHERE 조건;
UPDATE employees SET salary = 4000000 WHERE id = 1;

여러 개의 행을 수정 하는 쿼리

UPDATE employees SET salary = CASE WHEN department = '영업' THEN salary * 1.1 WHEN department = '마케팅' THEN salary * 1.2 END;

위 쿼리는 employees 테이블에 salary 컬럼이 영업이면 salary 값을 * 1.1로 업데이트, 마케팅이면 *1.2로 업데이트한다.

테이블 연결 후 업데이트 쿼리 작성 방법

  • 두 개의 테이블을 연결하려면, 두 테이블 사이의 연결고리 역할(즉, 공통으로 가지는)을 하는 애트리뷰트를 찾아봐야 한다.
  • 애트리뷰트 이름을 좀 더 직관적으로 적기 위해, Table_name.Attribute로 표현해주면 좋다.
  • 만약 모든 데이터의 값을 수정하고 싶다면, WHERE 절 없이 SET 키워드 뒤에 새로운 값을 적어주면 된다.

데이터 삭제 하기

DELETE FROM 테이블명 WHERE 조건;
DELETE FROM employees WHERE department = '마케팅';
  • 데이터를 삭제하기 위해서는 DELETE 키워드로 시작하여 테이블 이름을 작성하고, WHERE 절을 사용하여 삭제할 튜플의 조건을 정의한다.
  • WHERE 절을 빼먹고 삭제하게되면 모든 데이터를 삭제하게 됨

외래키 설정에 따른 삭제

CASCADE
  • 외래키를 참조하는 튜플도 함께 삭제
DELETE FROM departments WHERE id = 1;
  • 위 쿼리는 departments 테이블에서 id가 1인 부서를 삭제
  • departments 테이블에 외래키를 참조하는 employees 테이블의 튜플도 함께 삭제
  • 만약 WHERE id <> 1; (또는 !=)의 경우 1을 제외한 모든 데이터 삭제
RESTRICT
  • 외래키를 참조하는 튜플이 존재하면 삭제를 거부
DELETE FROM departments WHERE id = 1;
  • 위 쿼리는 departments 테이블에서 id가 1인 부서를 삭제를 시도하지만, employees 테이블에 departments 테이블의 id를 참조하는 튜플이 존재하는 경우 삭제가 거부

참고 자료

https://youtu.be/mgnd5JWeCK4?si=_Ii-pRA3AKiq5N4A

이미지 출처: https://stackify.com/gradle-vs-maven/

빌드 관리 도구

빌드 관리 도구는 소스 코드에서 실행 가능한 애플리케이션을 자동으로 생성하는 데 도움을 주는 프로그램입니다.

이름에서 알 수 있듯이, 이는 다양한 작업을 개발하거나 스크립팅하는 데 중요합니다.

빌드 관리 도구는 다음과 같은 프로세스에 필요합니다.

 

  • 빌드 툴은 임의의 명령을 실행합니다: 각 배포 시나리오는 독특하며, 각자가 파일을 다른 폴더로 복사하거나, 다른 형식으로 압축하거나, 다른 방식으로 정리해야 할 필요가 있습니다.
  • 한 명령의 결과를 다른 명령에 적용합니다: 빌드는 거의 항상 여러 단계의 과정입니다.
  • 소스 코드에서 문서를 생성합니다 .
  • 소스 코드를 컴파일합니다
  • 수집된 코드를 JAR 파일로 패키징합니다
  • 패키지된 코드를 로컬/중앙 리포지토리 또는 서버에 설치합니다.

빌드 관리 도구에는 Maven과 Gradle이 있습니다.

Maven

Apache Maven은 앱 개발의 전체 과정을 자동화하고 Java 개발자들의 일을 쉽게 만들어줍니다. Maven은 Apache Group이 설계한 유명한 오픈 소스 빌드 도구로, 여러 프로젝트를 한 번에 개발하고, 배포하고, 출시하는 데 사용됩니다. 이 빌드 도구는 Project Object Model(POM)에 기반을 두고 있으며, 빌드 과정을 더욱 간소화하고 표준화하는 데 중점을 둡니다. 이 Maven은 또한 코드를 설계하고 의존성을 다운로드하는 데 도움이 됩니다. Maven을 사용하기 시작하면, 의존성을 다운로드할 필요가 더 이상 없습니다.

 

Maven은 Apache Ant에 다음과 같은 특성을 포함합니다.

 

  • 저장소 관리: 이는 빌드에 필요한 jar를 저장하는 위치입니다. 세 가지 유형의 저장소가 있습니다: 중앙, 로컬, 원격. 첫 번째는 빌드가 실행되는 기계에 위치하고 있고, 다른 두 가지는 HTTP를 통해 원격으로 얻어집니다. Maven은 먼저 로컬 저장소에서 jar를 검색하는 데 집중합니다. 찾을 수 없다면 원격으로 찾아서 로컬로 다운로드하여 미래의 빌드를 가속화합니다.
  • 의존성 관리: 이는 프로젝트가 구축을 위해 필요로 하는 jar의 선언입니다.
  • Maven을 사용하기로 결정한 후에는 다음 세 가지를 고려해야 합니다:
  • Java에서 Maven을 설정하려면, pom.xml 파일에 위치한 Project Object Model(POM)을 사용합니다.
  • 모든 Maven 관련 설정은 POM에 위치해 있습니다. pom.xml 파일의 태그에서 플러그인을 설정하고 편집할 수 있습니다.
  • Maven은 설정에 대한 기본 설정을 제공하는데, 이는 모든 설정을 pom.xml 파일에 포함할 필요가 없음을 의미합니다.

Maven의 주요 기능은 다음과 같습니다:

  • 모델 기반 빌드 - Maven은 다양한 프로젝트를 war, 메타데이터, jar와 같은 미리 정의된 출력 유형으로 개발할 수 있습니다.
  • 프로젝트 정보의 명확한 사이트 - 빌드 과정에서 사용하는 동일한 메타데이터를 사용하여, Maven은 전체 문서를 포함하는 웹사이트와 PDF를 생성할 수 있습니다.
  • 릴리즈 관리 및 배포 출판 - 추가 설정 없이, Maven은 CVS(Concurrent Versions System)와 같은 소스 제어 시스템과 결합하고 프로젝트의 릴리즈를 관리합니다.
  • 후진 호환성 - 프로젝트의 다양한 모듈을 쉽게 이전 버전에서 최신 버전의 Maven으로 포팅할 수 있습니다. 또한 이전 버전을 지원합니다.
  • 자동 부모 버전 관리 - 하위 모듈에서 부모를 지정할 필요가 없습니다.
  • 병렬 빌드 - 프로젝트 의존성 그래프를 분석하고 모듈을 병렬로 설계할 수 있게 해줍니다. 이를 통해 성능 향상을 20-50% 얻을 수 있습니다.
  • 개선된 오류 및 무결성 보고 - Maven은 오류 보고를 개선하고, 링크를 제공하여 Maven 위키 페이지로 이동할 수 있게 해줍니다. 여기에서 오류에 대한 자세한 설명을 볼 수 있습니다.
  • 강화된 오류 및 무결성 보고: 상세 설명 링크와 함께 개선된 오류 보고 제공

Gradle

Gradle은 안드로이드, C/C++, Java, Scala, Groovy 등의 언어에서 빌드 자동화를 수행할 수 있는 능력으로 인해 많은 사람들이 찾습니다. 이 도구는 XML 대신에 Groovy 기반 도메인 특정 언어를 지원합니다. Gradle은 다양한 플랫폼에서 소프트웨어를 개발, 테스트, 배포할 수 있게 해줍니다.

 

다음은 Gradle을 사용하는 주요 이유입니다:

  • Gradle은 ANT와 Maven과 같은 다른 빌드 도구에서 발생하는 모든 문제를 해결합니다.
  • 이 도구는 성능, 사용성, 유지보수성, 유연성, 소프트웨어의 확장성에 중점을 둡니다.
  • Gradle은 다양한 기술을 다루는 여러 프로젝트에 대해 사용자 정의 가능하기 때문에 인기가 있습니다. 우리는 안드로이드 프로젝트, Groovy 프로젝트, Java 프로젝트 등에서 Gradle을 여러 가지 방식으로 사용할 수 있습니다.
  • Gradle은 고속성을 제공하는 것으로 잘 알려져 있으며, 대략 Maven의 두 배 정도의 속도를 낼 수 있습니다.

Gradle의 주요 기능은 다음과 같습니다:

  • Gradle은 입력이나 출력이 변경된 작업만 실행하여 불필요한 작업을 피합니다. 이전 실행 또는 별도의 기계(공유 빌드 캐시가 있는 경우)에서 작업 출력을 재사용하도록 개발된 캐시도 사용할 수 있습니다.
  • Gradle은 JVM에서 작동하며, 이를 사용하려면 Java Development Kit(JDK)가 설치되어 있어야 합니다. 이는 Java 플랫폼에 익숙한 사용자에게는 편할 수 있는데, 사용자 정의 작업 유형과 플러그인 등의 빌드 로직에서 일반적인 Java API를 사용할 수 있기 때문입니다.
  • 또한 Gradle을 여러 플랫폼에서 쉽게 실행할 수 있도록 합니다. Gradle은 JVM 프로젝트만 빌드하는 데 국한되지 않으며, 네이티브 프로젝트 개발 지원이 포함되어 있습니다.
  • 빌드 스캔은 빌드 실행에 대한 포괄적인 정보를 제공하여 빌드 문제를 식별하는 데 사용할 수 있습니다. 이는 특히 빌드 성능 문제를 인식하는 데 도움이 됩니다. 또한 빌드 스캔을 다른 사람과 공유할 수도 있습니다. 이는 빌드 문제를 해결하는 데 도움이 필요한 경우 유용합니다.

Gradle과 Maven의 장단점

Gradle의 장점

  • 범용 빌드 도구: Gradle은 어떤 종류든 애플리케이션을 빌드할 수 있도록 설계되었습니다. 
  • 고도의 커스터마이징 가능: Gradle은 다양한 기술에 맞게 맞춤화될 수 있습니다.
  • 성능: Gradle은 성능 면에서 매우 빠르고 효율적으로 작동합니다. 모든 경우에 Gradle은 Maven의 두 배 정도의 속도를 내며, 빌드 캐시의 수백 배의 속도를 냅니다.
  • 유연성: Gradle은 유연한 도구입니다. Kotlin, Scala, Java, Groovy 등의 프로그래밍 언어에서 플러그인을 개발할 수 있는 도구로 사용됩니다.
  • 사용자 경험: 다양한 IDE를 제공하여 사용자 경험을 향상시킵니다.

Gradle의 단점

  • 기술적 전문성: Gradle로 작업을 빌드하려면 기술적 기술이 필요합니다.
  • Gradle은 통합된 ant 프로젝트 구조를 가지고 있지 않습니다. 우리가 어떤 빌드 구조든 우리의 프로젝트에 사용할 수 있기 때문에, 새로운 프로그래머들은 프로젝트 구조와 빌드 스크립트를 이해하는 데 어려움을 겪습니다.
  • XML을 사용하여 스크립트를 초안하고 빌드해야 합니다. 복잡한 프로젝트를 자동화하려는 경우, XML 파일에 많은 로직을 작성해야 합니다.
  • 이해도: Gradle 문서는 꽤 광범위합니다. 이전에 용어 지식이 필요합니다.

Maven의 장점

  • 프로젝트 관리에서 빌드, 문서화, 출판, 배송 등 모든 프로세스를 관리하는 데 도움을 줍니다
  • 프로젝트 빌드 과정을 간소화합니다
  • 프로젝트의 성능과 빌드 과정을 향상시킵니다
  • Maven은 Jar 파일과 기타 의존성을 자동으로 다운로드하는 작업을 수행합니다.
  • 모든 필수 정보에 쉽게 접근할 수 있습니다
  • 의존성, 프로세스 등에 대해 걱정하지 않고 다양한 환경에서 프로젝트를 개발하는 것을 프로그래머에게 간단하게 만들어줍니다.
  • Maven에서는 POM 파일에 의존성 코드를 작성함으로써 쉽게 새로운 의존성을 포함시킬 수 있습니다.

Maven의 단점

  • Maven은 작업 시스템에 설치되어야 하며, IDE에는 Maven 플러그인이 필요합니다
  • 기존 의존성에 대한 Maven 코드를 찾을 수 없는 경우, Maven 자체를 사용하여 해당 의존성을 구현할 수 없습니다
  • Maven은 프로젝트를 실행하는 데 있어서도 상당히 느립니다.

참고 자료

https://stackify.com/gradle-vs-maven/

Spring Data JPA로 데이터의 조회를 구현할 때, 사용할 수 있는 메서드중, getReferenceById와 findById 두 메서드가 있습니다.

이 글에서는 이 둘의 차이점을 비교해보려 합니다.

getRefereceById (구: getOne(ID), findOne(ID), getById(ID))

내부적으로 EntityManager의 getReference() 메서드를 호출합니다.

getReference() 메서드를 호출하면 Proxy 객체를 리턴합니다. 실제 쿼리는 Proxy 객체를 통해 최초로 데이터에 접근하는 시점에 실행됩니다. (지연 로딩(Lazy Loading))

이때 데이터가 존재하지 않는 경우에는 EntityNotFoundException이 발생합니다. 

아래는 실제 구현체 코드입니다.

 

SimpleJpaRepository.class의 일부

public T getReferenceById(ID id) {
        Assert.notNull(id, "The given id must not be null");
        return this.entityManager.getReference(this.getDomainClass(), id);
    }

findById

내부적으로 EntityManager의 find() 메서드를 호출합니다.

이 메서드는 영속성 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않는다면 실제 데이터베이스에서 데이터를 조회합니다. (즉시 로딩(eager loading))

이 메서드는 특정 ID를 가진 엔티티를 리포지토리에서 찾습니다

리턴 값으로 Optional 객체를 전달합니다. 즉 잘못된 Id를 넘겨주더라도 예외가 발생하지 않을 수 있습니다.

아래는 실제 구현 코드입니다.

    public Optional<T> findById(ID id) {
        Assert.notNull(id, "The given id must not be null");
        Class<T> domainType = this.getDomainClass();
        if (this.metadata == null) {
            return Optional.ofNullable(this.entityManager.find(domainType, id));
        } else {
            LockModeType type = this.metadata.getLockModeType();
            Map<String, Object> hints = this.getHints();
            return Optional.ofNullable(type == null ? this.entityManager.find(domainType, id, hints) : this.entityManager.find(domainType, id, type, hints));
        }
    }

두 메서드의 주요 차이

getReferenceById()는 엔티티를 찾지 못하면 예외를 발생 시키고, findById()는 예외를 안 일으킨다는 차이도 있을 수 있지만, 이 둘의 주요 차이는 로딩 방식입니다.

getReferenceById()가 지연 로딩(lazy), findById는 즉시 로딩(eager) 입니다.

 

Spring은 우리가 Transaction 내에서 명시적으로 엔티티를 사용하려고 시도할 때까지 데이터베이스 요청을 보내지 않습니다.

각 Transaction 은 작업을 수행하는 전용 영속성 컨텍스트를 가지고 있습니다.

때때로, 영속성 컨텍스트를 트랜잭션 범위 밖으로 확장할 수 있지만, 이는 일반적이지 않고 특정 시나리오에만 유용합니다. 영속성 컨텍스트가 트랜잭션에 대해 어떻게 작동하는지 확인해봅시다.

즉시 로딩

findById를 사용하면 해당 메서드가 호출될 때 즉시 데이터베이스에서 엔티티를 조회하고, 결과를 반환합니다. 즉, 메서드 호출 시점에서 데이터베이스와의 통신이 발생하며, 해당 트랜잭션 범위 내에서 엔티티가 영속성 컨텍스트에 로드됩니다.

이것은 Managed 상태입니다. 따라서 엔티티에 대한 모든 변경사항은 데이터베이스에 반영됩니다.

트랜잭션 외부에서는 엔티티가 detached 상태로 이동하고, 엔티티가 다시 managed 상태로 이동하기 전까지는 변경사항이 반영되지 않습니다.

지연 로딩에 관하여

지연 로딩된 엔티티는 약간 다르게 동작합니다.

Spring은 영속성 컨텍스트 내에서 엔티티들을 명시적으로 사용할 때까지 로드하지 않습니다.

Spring은 데이터베이스에서 엔티티를 지연해서 가져오기 위해 빈 프록시 placeholder를 할당합니다.

이 프록시 객체와 어떠한 상호작용이 없다면, 트랜잭션 외부에서 빈 프록시로 남아있고, 그것에 대한 어떤 호출이든 LazyInitializationException을 발생시킵니다.

그러나 프록시 객체를 호출하거나, 내부 정보를 필요로 하는 방식으로 이 프록시 객체와 상호작용한다면, 실제 데이터베이스 요청이 이루어집니다.

 

예를 보겠습니다. (지연 로딩에 이해를 위해 OSIV 설정을 false로 합니다.)

@Override
@Transactional
    public ProductResponseDto getProduct(Long number) {
        Product product = productDAO.selectProduct(number);

        ProductResponseDto productResponseDto = ProductResponseDto.builder()
                .number(product.getNumber())
                .name(product.getName())
                .price(product.getPrice())
                .stock(product.getStock())
                .build();
                
        return productResponseDto;
    }

위 코드에서 selectProduct 메서드는 아래와 같이 구현되어 있습니다.

@Override
    public Product selectProduct(Long number) {
        System.out.println("Before query");
        Product selectedProduct = productRepository.getReferenceById(number);
        System.out.println("After query");
        return selectedProduct;
    }

위 코드를 실행시키면 다음과 같이 출력됩니다.

Before query
After query

user와 상호작용하는 어떠한 코드가 없으므로, 결국 hibernate는 Product를 조회하는 쿼리를 실행하지 않습니다.

@Override
    public Product selectProduct(Long number) {
        Product selectedProduct = productRepository.getReferenceById(number);
        System.out.println("Before query");
        System.out.println(selectedProduct.getName());
        System.out.println("After query");
        return selectedProduct;
    }

다음와 같이 수정하면, 아래와 같이 출력됩니다.

Before query
Hibernate: 
    select
        p1_0.number,
        p1_0.created_at,
        p1_0.name,
        p1_0.price,
        p1_0.stock,
        p1_0.updated_at 
    from
        product p1_0 
    where
        p1_0.number=?
string
After query

 

 

여기서는 Hibernate가 쿼리를 발생시킵니다. (Product이름은 string입니다..!)

참고로 프록시 객체에서 @Id(DB에서 PK)는 Hibernate 쿼리가 필요없이 바로 조회가 되지만, 그 외 속성에 대해 접근하려 한다면 쿼리가 발생합니다.

외부 메서드에서 프록시 객체를 사용

 @Override
 // @Transactional
    public ProductResponseDto getProduct(Long number) {
        Product product = productDAO.selectProduct(number);

        ProductResponseDto productResponseDto = ProductResponseDto.builder()
                .number(product.getNumber())
                .name(product.getName())
                .price(product.getPrice())
                .stock(product.getStock())
                .build();
        System.out.println(productResponseDto.getName());
        return productResponseDto;
    }

이제 selectProduct 메서드를 호출하여 리턴받은 Product 객체를 Service 레이어에서 받아 사용할 것 입니다. 

@Transactional를 지우고 실행해보겠습니다.

위 코드를 실행하면 아래와 같은 예외가 발생합니다.

Before query
2024-02-21T17:07:23.895+09:00 ERROR 61396 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.hibernate.LazyInitializationException: could not initialize proxy [org.spring.study.data.entity.Product#1] - no Session] with root cause

org.hibernate.LazyInitializationException: could not initialize proxy [org.spring.study.data.entity.Product#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:314) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.spring.study.data.entity.Product$HibernateProxy$v0eDAZvT.getName(Unknown Source) ~[main/:na]
	at org.spring.study.data.dao.impl.ProductDAOImpl.selectProduct(ProductDAOImpl.java:34) ~[main/:na]
    ...

 

selectProduct 메서드 안에서 System.out.println(selectedProduct.getName()); 부분을 실행하려는데 LazyInitializationException이 발생했습니다.

그 이유는 @Transactional 어노테이션이 없다면,  getReferenceById 메서드가 리턴이 된 후 Transaction이 끝나고 Session이 닫힙니다.

그리고 Session 닫히고 나서 프록시 객체를 사용했기 때문에, 지연 로딩을 할 수 없어 에러가 발생합니다.

(OSIV = false를 한 이유가 이를 재현하기 위해서 인데, OSIV = true 라면 세션을 요청이 끝날 때 까지 쭉 열려있기 때문에 LazyInitializationException 재현이 불가능합니다.) 

@Transactional 서비스 내에서 getReferenceById 사용 시 동작

@Transactional 어노테이션이 적용된 Service 메서드에서는 어떻게 동작하는지 확인해보겠습니다.

@Override
    @Transactional
    public ProductResponseDto getProduct(Long number) {
        Product product = productDAO.selectProduct(number);

        ProductResponseDto productResponseDto = ProductResponseDto.builder()
                .number(product.getNumber())
                .name(product.getName())
                .price(product.getPrice())
                .stock(product.getStock())
                .build();
        System.out.println("Success " + productResponseDto.getName());
        return productResponseDto;
    }

 

이제 엔티티와 상호작용하는 코드를 추가하고 로그를 살펴보겠습니다.

Before query
1
After query
Hibernate: 
    select
        p1_0.number,
        p1_0.created_at,
        p1_0.name,
        p1_0.price,
        p1_0.stock,
        p1_0.updated_at 
    from
        product p1_0 
    where
        p1_0.number=?
Success string

@Transactional 어노테이션이 적용이 된 getProduct 메서드 내에서 엔티티를 직접 사용하면 원할히 동작합니다.

이것이 가능한 이유는 @Transactional 어노테이션이 메서드에 대한 트랜잭션 범위를 유지하고 있기 때문입니다. 이 범위 내에서는 데이터베이스 연결이 유지되므로, 필요한 시점에 지연 로딩이 가능하게 됩니다. 

따라서, @Transactional 메서드 내에서 getReferenceById 를 호출하고 그 객체에 접근하면, 그 접근 시점에 데이터베이스에서 실제 데이터를 로드하기 때문에 에러가 발생하지 않는 것입니다. 

새로운 리포지토리 트랜잭션을 가진 @Transactional 서비스

좀 더 복잡한 예시를 살펴보겠습니다. 호출될 때마다 별도의 트랜잭션을 생성하는 리포지토리 메서드가 있다고 가정합니다.

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
User getReferenceById(Long id);

Propagation.REQUIRES_NEW는 외부 트랜잭션이 전파되지 않고 리포지토리 메서드가 자체 영속성 컨텍스트를 생성한다는 의미입니다. 이 경우 트랜잭션 서비스를 사용하더라도 Spring은 이 경우 외부 트랜잭션과 내부 트랜잭션은 서로 다른 지속성 컨텍스트를 사용하므로 데이터를 공유할 수 없습니다.

@Test
void whenFindUserReferenceUsingInsideServiceThenThrowsExceptionDueToSeparateTransactions() {
    assertThatExceptionOfType(LazyInitializationException.class)
      .isThrownBy(() -> transactionalServiceWithNewTransactionRepository.findAndUseUserReference(EXISTING_ID));
}

참고 자료

SQL의 기본 개념과 구조, RDBMS의 SQL 스펙

  • SQL은 Relational Database Management System에서 사용되는 표준 언어로, 데이터베이스를 정리하고 데이터를 조작하고 조회하는 등 종합적인 데이터베이스 작업이 가능하다.
  • SQL에서 Table은 데이터 모델에서 Relation을 나타내며, attribute는 column, tuple은 row, domain은 domain으로 표현한다.
  • SQL에서 Relation은 multiset of tuples로, 중복된 tuple을 허용한다.
  • MySQL은 InnoDB를 기준으로 가장 많이 사용되는 RDBMS이다.
  • 데이터베이스를 생성하고 사용할 때는 'create database' 명령어를 사용하여 새로운 데이터베이스를 만들고, 'show databases' 명령어를 사용하여 현재 활성화된 데이터베이스를 확인할 수 있다.

MySQL에서 데이터베이스 선택용 명령어

  • MySQL의 데이터베이스 고르기용 명령어는 'SELECT DATABASE'를 사용한다.
  • 만약 아직 선택된 데이터베이스가 아닐경우, 결과값은 'NULL'이다.
  • 임의로 데이터베이스를 선택하고 싶을경우 'USE'를 사용하며, 선택한 데이터베이스의 이름을 명령어에 넣으면 된다.
  • 만약 선택된 데이터베이스를 삭제하고 싶을경우 'DROP DATABASE'를 사용하며, 삭제할 데이터베이스의 이름을 명령어에 넣으면 된다.

데이터베이스 스키마 설명과 테이블 생성 방법

  • '데이터베이스'와 '스키마'는 MySQL에서 같은 의미를 가지며, RDBMS에 따라 차이가 있다.
  • MySQL에서 'CREATE DATABASE' 또는 'CREATE SCHEMA'로 데이터베이스 생성 가능.
  • PostgreSQL에서는 스키마가 데이터베이스 안에 속하는 테이블의 네임스페이스를 의미함.
  • 스키마 안에서 각각의 테이블을 정의하며, 각 테이블은 테이블끼리의 관계를 가짐.
  • 테이블의 컬럼에는 ID, 이름, 생일, 성별과 같은 정보가 포함되며, 다른 테이블의 ID를 참조하는 관계를 가짐.

데이터 타입 중 RDMBS마다 차이가 있는 숫자 데이터 타입

  • 숫자 데이터 타입에는 정수, 부동 소수점, 고정 소수점 3가지 타입이 있고, MySQL 같은 경우에는 1바이트부터 8바이트까지의 사이즈로 저장하는 정수 타입 5가지가 있다.
  • RDBMS마다 구현하는 방식이나 디테일이 조금씩 다른데, 예시로 PostgreSQL은 smallint, int, bigint 3가지 종류만 있다.
  • 부동 소수점은 실수를 저장하며 MySQL에서는 4바이트를 float형, 8바이트를 double or double precision이라고 부르는데, 고정 소수점은 실수를 정확하게 저장할 때 사용된다.
  • 특히 고정 소수점은 실수를 정확하게 저장할 필요가 있는 경우에 쓰이며, DECIMAL or NUMERIC으로 선언한다.

문자열 타입

  • 문자열 타입은 고정 크기 문자열, 가변 크기 문자열, 사이즈가 큰 문자열이 있다.
  • 고정 크기 문자열은 최대 문자 수를 정의하며, CHAR(n) (0 <= n <= 255) 로 선언한다. 저장하는 문자열이 최대 길이보다 작으면 나머지 공간은 스페이스로 채워진다.
  • 가변 크기 문자열에서는 VARCHAR(n) (0 <= n <= 65,535)을 사용하여 최대 문자 수를 저장할 수 있다. CHAR보다 시간적인 차이가 있을 수 있다.
  • 큰 문자열의 경우 VARCHAR보다 작은 크기인 TINYTEXT, TEXT, 더 큰 타입은 MEDIUMTEXT 및 LONGTEXT를 사용하는 것이 좋다.

날짜와 시간 타입

  • 날짜는 DATE, 시간은 TIME으로 표현한다.
  • 날짜와 시간을 표시할 때는 Date, Time, DateTime, Timestamp를 사용한다. Timestamp는 Timezone에 영향을 받는다.
  • DATETIME은 '1000-01-01 00:00:00' ~ 9999-12-31 23:59:59'까지의 범위를 갖는다.
  • TIMESTAMP는 '1970-01-01 00:00:01' UTC ~ '2038-01-19 03:14:07' UTC 까지의 범위를 갖는다.

그 외 데이터 타입

  • Binary, VarBinary, Blob, Byte는 다양한 링크, 암호화 키, 프로세스 등을 저장하는 데 사용된다.
  • Boolean은 PostgreSQL에는 있지만 MySQL에는 없으며, TINYINT로 대체하여 사용한다.
  • JSON 형식의 데이터를 저장하기 위해서는 JSON 데이터 타입을 사용한다.

PRIMARY KEY, UNIQUE KEY, NOT NULL, DEFAULT, CHECK

  • PRIMARY KEY는 중복된 값이 없으며, null 값을 가질 수 없다. 여러 속성으로 구성되며, 중복된 값이면 제약 발생한다.
  • PRIMARY KEY는 애트리뷰트가 하나면 바로 선언하지만, 여러 개일 경우 프라이머리 키로 지정된 트위터 네임을 선언함.
  • UNIQUE KEY는 중복 값을 허용하지 않지만 null을 가질 수 있는 KEY로, UNIQUE KEY 선언도 프라이머리 키와 같이 애트리뷰트에 따로 선언하거나 아래에 별도로 적을 수 있음.
  • 'NOT NULL'을 선언한 애트리뷰트는 'null'을 가질 수 없음. 애트리뷰트 레벨로만 가능한 선언 방법은 애트리뷰트 이름과 타입을 정하고 'NOT NULL'을 적어줌.
  • 기본 값은 'DEFAULT'로 선언하며, 튜플을 저장할 때 명시하지 않으면 해당 애트리뷰트에 'TP' 값으로 저장됨.
  • CHECK는 애트리뷰트 값을 제한하는 용도로 사용하며, 특정 조건에 대한 제한을 설정할 수 있음.

️Foreign Key

  • 어트리뷰트가 다른 Table의 primary key나 Unique key를 참조할 때 사용한다.
  • Foreign Key는 본래 테이블의 Primary Key와 일치하는 값을 가지고 있어야 한다.
  • Foreign Key를 선언할 때는 해당 컬럼에 대해 참조할 테이블과 컬럼을 명시해야 한다.
  • 참조된 값이 삭제되거나 변경될 때 어떻게 대응할지에 대한 옵션을 설정할 수 있다.
  • 옵션에는 CASCADE: 참조값의 삭제/변경을 그대로 반영, SET NULL: 참조값의 삭제/변경을 NULL 반영, SET DEFAULT: 참조값의 삭제 시 DEFAULT 값으로 반영, RESTRICT: 참조값의 삭제/변경을 금지, NO ACTION: RESTRICT와 유사
  • MySQL은 CASCADE, SET NULL, RESTRICT만 지원

CONSTRAINT

  • Check 문 앞에 'CONSTRAINT 이름'을 명시하면 어떤 CONSTRAINT를 위반했는지 쉽게 파악할 수 있다.
  • 그 CONSTRAINT를 삭제하고 싶을 때 해당 이름으로 컨텐츠를 삭제할 수도 있음.

️테이블 스키마 변경을 위한 ALTER TABLE 문

  • ALTER TABLE 문을 통해 테이블 스키마를 변경할 수 있으며, 이를 이용하여 속성 추가, 이름 변경, 타입 변경, 제약조건 추가, 제약조건 삭제 등의 작업이 가능하다.
  • 데이터베이스 스키마 변경은 이미 서비스 중인 테이블의 스키마를 변경하는 것으로, 서비스에 악영향을 줄 수 있는 위험 부담이 있다.
  • 테이블을 삭제하려면 DROP TABLE 쿼리를 사용하면 된다.

참고 자료

https://youtu.be/c8WNbcxkRhY?si=t8D-LywajQxVaxGE

DAO(Data Access Object)

  • 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체
  • 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행함
  • 단, Spring Data JPA에서 DAO의 개념은 Repository(레포지토리)가 대체
  • 규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비스 레이어에서 데이터베이스에 접근해서 구현하기도 함
  • 하지만 DAO를 서비스 레이어와 리포지토리 중간 계층을 구성하여 사용하면, 비즈니스 로직을 개발할 때 유지보수 측면에서 용이한 경우가 많음

서비스와 비즈니스 레이어

  • 객체지향적인 설계에서는 서비스와 비즈니스 레이어를 분리해서 서비스 레이어에서는 서비스 로직을, 비즈니스 레이어에서는 비즈니스 로직을 수행해야 한다는 의견이 많다.
  • 서비스 레이어는 주로 사용자 인터페이스(UI)나 컨트롤러와 같은 프레젠테이션 계층에서 받은 요청을 처리하는 역할을 하며,
    트랜잭션 관리, 보안 및 권한 확인, 로깅, 예외 처리 등의 '서비스 로직'을 수행한다.
  • 서비스 레이어는 여러 비즈니스 로직을 조합하고 순서를 제어하여 하나의 비즈니스 프로세스를 완성하기도 한다.
  • 비즈니스 레이어는 애플리케이션의 핵심 비즈니스 로직을 담당한다.
  • 비즈니스 레이어에서는 비즈니스 규칙을 적용하고, 데이터를 처리하며, 계산을 수행한다.
  • 비즈니스 레이어는 도메인 모델을 표현하고, 데이터 액세스 레이어와 통신하여 데이터를 저장하거나 가져온다.

DAO 구현

  • DAO 클래스는 일반적으로 '인터페이스 - 구현체' 구성으로 생성함
  • DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴으로,서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성할 수 있음
  • 아래는 Product라는 엔티티의 대한 DAO의 인터페이스와 구현체 클래스 코드이다.

ProductDAO

public interface ProductDAO {

    Product insertProduct(Product product);

    Product selectProduct(Long number);

    Product updateProductName(Long number, String name) throws Exception;

    void deleteProduct(Long number) throws Exception;

}

ProductDAOImpl

@Component
public class ProductDAOImpl implements ProductDAO {

    private ProductRepository productRepository;

    @Autowired
    public ProductDAOImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public Product insertProduct(Product product) {
        Product savedProduct = productRepository.save(product);

        return savedProduct;
    }

    @Override
    public Product selectProduct(Long number) {
        Product selectedProduct = productRepository.getById(number);

        return selectedProduct;
    }

    @Override
    public Product updateProductName(Long number, String name) throws Exception {
        Optional<Product> selectedProduct = productRepository.findById(number);

        Product updatedProduct;
        if (selectedProduct.isPresent()) {
            Product product = selectedProduct.get();

            product.setName(name);
            product.setUpdatedAt(LocalDateTime.now());

            updatedProduct = productRepository.save(product);
        } else {
            throw new Exception();
        }

        return updatedProduct;
    }

    @Override
    public void deleteProduct(Long number) throws Exception {
        Optional<Product> selectedProduct = productRepository.findById(number);

        if (selectedProduct.isPresent()) {
            Product product = selectedProduct.get();

            productRepository.delete(product);
        } else {
            throw new Exception();
        }
    }
}

@Service, @Component

  • 두 어노테이션은 스프링 빈을 자동으로 등록하는 `@ComponentScan`의 대상이 되며, 이를 통해 스프링은 이들을 자동으로 빈으로 등록하고 종속성 주입을 수행한다.
  • @Component: 일반적인 컴포넌트를 등록하는 데 사용된다. 예를 들어, DAO, 서비스, 유틸리티 클래스 등을 등록할 때 사용
  • @Component로 등록된 빈은 자동으로 주입되지 않는다. 빈을 주입하려면 @Autowired 어노테이션을 함께 사용해야 한다.
  • @Service: 비즈니스 로직을 담당하는 서비스 클래스를 등록하는 데 사용됩니다. @Component보다 더 구체적인 어노테이션이다.
  • @Service로 등록된 빈은 자동으로 주입된다. @Autowired 어노테이션 없이도 다른 빈에 주입될 수 있다.
  • @Service는 @Component보다 더 명확하고, 자동 주입 기능도 제공하기 때문에 코드를 간소화할 수 있다.

DTO(Data Transfer Object)

  • 일반적으로 데이터베이스에 접근하는 계층에서만 Entity를 사용하고, 그 외 다른 계층으로 데이터를 전달할 때는 DTO 객체를 사용함
  • 아래는 Product라는 엔티티의 DTO 객체 예시 코드이다.
package com.springboot.jpa.data.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {

    private String name;

    private int price;

    private int stock;

}

스프링 부트 애플리케이션의 구조

참고 자료

  • 스프링 부트 핵심 가이드 "스프링 부트를 활용한 애플리케이션 개발 실무" , 장정우, 2022

Spring Data JPA

  • Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공
  • Springboot로 JpaRepository를 상속하는 인터페이스를 생성하면, 기존의 다양한 메소드를 손쉽게 활용할 수 있음

Repository

  • Spring Data JPA가 제공하는 인터페이스
  • 엔티티가 데이터베이스의 테이블과 구조를 생성하는 데 사용했다면, 리포지토리(Repository)는 엔티티가 생성한 데이터베이스에 접근하는 용도로 사용
  • 리포지토리를 생성하려면 테이블과 엔티티에 대한 인터페이스를 생성하고, 아래 코드와 같이 JpaRepository를 상속받으면 됨
package org.spring.study.data.repository;

import org.spring.study.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {

}
  • JpaRepository를 상속받을 때는 대상 엔티티 클래스와 그 엔티티의 Id 타입을 설정해야함.
  • JpaRepository는 아래와 같은 기본 메소드를 제공함
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    void flush();

    <S extends T> S saveAndFlush(S entity);

    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

    /** @deprecated */
    @Deprecated
    default void deleteInBatch(Iterable<T> entities) {
        this.deleteAllInBatch(entities);
    }

    void deleteAllInBatch(Iterable<T> entities);

    void deleteAllByIdInBatch(Iterable<ID> ids);

    void deleteAllInBatch();

    /** @deprecated */
    @Deprecated
    T getOne(ID id);

    /** @deprecated */
    @Deprecated
    T getById(ID id);

    T getReferenceById(ID id);

    <S extends T> List<S> findAll(Example<S> example);

    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}

@Repository 어노테이션

@Repository 어노테이션을 적용하면, 그 클래스는 Bean 객체화 된다. 따라서 Bean의 생명주기를 따르게 되고, @Autowired와 같은 어노테이션으로 의존성을 주입할 수 있다. 또한 @Component 어노테이션을 상속하고 있으며, Spring이 자동으로 해당 클래스를 발견하고 Bean에 등록한다. 결과적으로 @Repository가 달려있는 클래스는  Spring이 관리하는 Bean이 되며, 데이터에 접근하는 계층으로 인식된다.

JpaRepository의 상속 구조

이미지 출처:&nbsp;https://i.stack.imgur.com/ee7XF.jpg

이러한 리포지토리 추상화는 아키텍처와 기능적 요구에 따라 기본 리포지토리를 선택할 수 있게 함.

CrudRepository

  •  기본적인 CRUD(Create, Read, Update, Delete) 작업을 수행하는 메서드를 제공
  • 모든 JPA 엔티티에 적용 가능
  • 엔티티 생성, 조회, 수정, 삭제와 같은 간단한 데이터베이스 작업

PagingAndSortingRepository

  • CrudRepository의 기능을 포함하며, 페이징(데이터 분할) 및 정렬 기능을 추가로 제공
  • 페이지 단위로 데이터를 조회하고, 정렬 조건을 설정할 수 있음

JpaRepository

  • CrudRepository 및 PagingAndSortingRepository의 기능을 포함하며, JPA 특화 기능을 추가로 제공
  • JPA 엔터티에 대한 JPQL(Java Persistence Query Language) 쿼리를 작성할 수 있음
  • 복잡한 데이터 쿼리 가능
  • JpaRepository의 deleteInBatch(…)는 delete(…)와는 다르게 지정된 엔티티를 삭제하는 쿼리를 사용하므로 더 성능이 좋지만 JPA 정의된 cascade를 트리거하지 않는다는 부작용이 있음.

QueryByExampleExecuter

  • 예시 기반 쿼리를 사용하여 엔티티를 검색하는 기능을 제공
  • 엔티티 객체의 예시를 사용하여 동일한 속성을 가진 엔티티를 검색할 수 있음
  • 동적 쿼리
  • 엔티티 객체 기반 검색

Repository의 목적과, 주의해야할 점

기본적으로 리포지토리를 선택하는 것은 두 가지 주요 목적이 있음

  1. Spring Data repository 인프라가 사용자의 인터페이스를 찾아 프록시 생성을 트리거하게 하고, 클라이언트로 인터페이스 인스턴스를 주입할 수 있게 함
  2. 필요한 기능을 가능한 한 많이 인터페이스에 포함시키기 위해 추가 메소드 선언 없이 사용

기본 인터페이스에 의존하는 것의 단점은 

  1. Spring Data repository 인터페이스에 의존하면, 리포지토리 인터페이스가 라이브러리에 종속됨
  2. 예를 들어 CrudRepository를 확장함으로써, 한 번에 모든 지속성 메소드가 노출됨.
    이것은 더 세밀한 제어를 원하는 상황에서는 문제가 될 수 있다.

이러한 단점들을 해결하는 방법은 사용자가 자신의 기본 리포지토리 인터페이스를 만드는 것.

interface ApplicationRepository<T> extends PagingAndSortingRepository<T, Long> { }

interface ReadOnlyRepository<T> extends Repository<T, Long> {

  // Al finder methods go here
}

첫 번째 리포지토리 인터페이스는 일반적인 목적의 기본 인터페이스로, 실제로는 1번 포인트만을 고정하지만, 일관성을 위해 ID 타입을 Long으로 연결함

두 번째 인터페이스는 보통 CrudRepository와 PagingAndSortingRepository에서 복사된 모든 find…(…) 메소드를 가지고 있지만, 조작 메소드는 노출하지 않는다. 

Repository 메서드 생성 규칙

리포지토리에서 제공하는 조회 메서드는 기본값으로 단일 조회하거나 전체 엔티티 조회하는 것만 지원하고 있어, 필요에 따라 다른 조회 메서드가 필요함

메서드에 이름을 붙일 때는 첫 단어를 제외한 이우 단어들의 첫 글자를 대문자로 설정해야 JPA에서 정상적으로 인식하고 쿼리를 자동으로 만들어줌.

다음은 몇 가지 주요 키워드와 사용 예이다:

  1. findBy - 특정 필드를 기준으로 검색한다. 예: `findByName(String name)`은 `name` 필드가 주어진 값과 일치하는 엔티티를 반환한다.
  2. And - 여러 필드를 기준으로 검색한다. 예: `findByNameAndAge(String name, Integer age)`는 `name`과 `age` 필드가 각각 주어진 값과 일치하는 엔티티를 반환한다.
  3. Or - 한 개 이상의 필드를 기준으로 검색한다. 예: `findByNameOrAge(String name, Integer age)`는 `name` 또는 `age` 필드가 주어진 값과 일치하는 엔티티를 반환한다.
  4. Is, Equals - 특정 필드가 주어진 값과 일치하는지 검사한다. 예: `findByNameIs(String name)` 혹은 `findByNameEquals(String name)`은 `name` 필드가 주어진 값과 일치하는 엔티티를 반환한다.
  5. Between - 특정 필드의 값이 두 값 사이에 있는 엔티티를 검색한다. 예: `findByAgeBetween(int start, int end)`는 `age` 값이 `start`와 `end` 사이에 있는 엔티티를 반환한다.
  6. LessThan, GreaterThan, LessThanEqual, GreaterThanEqual - 특정 필드의 값이 주어진 값보다 작거나 큰 엔티티를 검색한다. 예: `findByAgeLessThan(int age)`는 `age` 필드의 값이 주어진 값보다 작은 엔티티를 반환한다.
  7. IsNull, IsNotNull - 특정 필드의 값이 null인지 아닌지를 기준으로 검색한다. 예: `findByNameIsNull()`은 `name` 필드의 값이 null인 엔티티를 반환한다.
  8. Like, NotLike - 특정 필드의 값이 주어진 패턴과 일치하는지를 기준으로 검색한다. 예: `findByNameLike(String pattern)`은 `name` 필드의 값이 주어진 패턴과 일치하는 엔티티를 반환한다.
  9. In, NotIn - 특정 필드의 값이 주어진 컬렉션에 포함되어 있는지를 기준으로 검색한다. 예: `findByNameIn(Collection<String> names)`은 `name` 필드의 값이 주어진 컬렉션에 포함된 엔티티를 반환한다.

이 외에도 `OrderBy`, `Top`, `First` 등 다양한 키워드를 사용할 수 있다. 이 방식은 복잡한 쿼리를 작성하지 않고도 간단한 검색을 수행할 수 있어 편리하다.

참고 자료

 

What is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA?

What is the difference between CrudRepository and JpaRepository interfaces in Spring Data JPA? When I see the examples on the web, I see them there used kind of interchangeably. What is the differ...

stackoverflow.com