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 등등이 이에 속한다.
빌드 관리 도구는 소스 코드에서 실행 가능한 애플리케이션을 자동으로 생성하는 데 도움을 주는 프로그램입니다.
이름에서 알 수 있듯이, 이는 다양한 작업을 개발하거나 스크립팅하는 데 중요합니다.
빌드 관리 도구는 다음과 같은 프로세스에 필요합니다.
빌드 툴은 임의의 명령을 실행합니다: 각 배포 시나리오는 독특하며, 각자가 파일을 다른 폴더로 복사하거나, 다른 형식으로 압축하거나, 다른 방식으로 정리해야 할 필요가 있습니다.
한 명령의 결과를 다른 명령에 적용합니다: 빌드는 거의 항상 여러 단계의 과정입니다.
소스 코드에서 문서를 생성합니다 .
소스 코드를 컴파일합니다
수집된 코드를 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 자체를 사용하여 해당 의존성을 구현할 수 없습니다
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을 발생시킵니다.
그러나 프록시 객체를 호출하거나, 내부 정보를 필요로 하는 방식으로 이 프록시 객체와 상호작용한다면, 실제 데이터베이스 요청이 이루어집니다.
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 쿼리가 필요없이 바로 조회가 되지만, 그 외 속성에 대해 접근하려 한다면 쿼리가 발생합니다.
이제 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 메서드에서는 어떻게 동작하는지 확인해보겠습니다.
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은 이 경우 외부 트랜잭션과 내부 트랜잭션은 서로 다른 지속성 컨텍스트를 사용하므로 데이터를 공유할 수 없습니다.
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 문을 통해 테이블 스키마를 변경할 수 있으며, 이를 이용하여 속성 추가, 이름 변경, 타입 변경, 제약조건 추가, 제약조건 삭제 등의 작업이 가능하다.
데이터베이스 스키마 변경은 이미 서비스 중인 테이블의 스키마를 변경하는 것으로, 서비스에 악영향을 줄 수 있는 위험 부담이 있다.
@Repository 어노테이션을 적용하면, 그 클래스는 Bean 객체화 된다. 따라서 Bean의 생명주기를 따르게 되고, @Autowired와 같은 어노테이션으로 의존성을 주입할 수 있다. 또한 @Component 어노테이션을 상속하고 있으며, Spring이 자동으로 해당 클래스를 발견하고 Bean에 등록한다. 결과적으로 @Repository가 달려있는 클래스는 Spring이 관리하는 Bean이 되며, 데이터에 접근하는 계층으로 인식된다.
JpaRepository의 상속 구조
이러한 리포지토리 추상화는 아키텍처와 기능적 요구에 따라 기본 리포지토리를 선택할 수 있게 함.
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의 목적과, 주의해야할 점
기본적으로 리포지토리를 선택하는 것은 두 가지 주요 목적이 있음
Spring Data repository 인프라가 사용자의 인터페이스를 찾아 프록시 생성을 트리거하게 하고, 클라이언트로 인터페이스 인스턴스를 주입할 수 있게 함
필요한 기능을 가능한 한 많이 인터페이스에 포함시키기 위해 추가 메소드 선언 없이 사용
기본 인터페이스에 의존하는 것의 단점은
Spring Data repository 인터페이스에 의존하면, 리포지토리 인터페이스가 라이브러리에 종속됨
예를 들어 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에서 정상적으로 인식하고 쿼리를 자동으로 만들어줌.
다음은 몇 가지 주요 키워드와 사용 예이다:
findBy - 특정 필드를 기준으로 검색한다. 예: `findByName(String name)`은 `name` 필드가 주어진 값과 일치하는 엔티티를 반환한다.
And - 여러 필드를 기준으로 검색한다. 예: `findByNameAndAge(String name, Integer age)`는 `name`과 `age` 필드가 각각 주어진 값과 일치하는 엔티티를 반환한다.
Or - 한 개 이상의 필드를 기준으로 검색한다. 예: `findByNameOrAge(String name, Integer age)`는 `name` 또는 `age` 필드가 주어진 값과 일치하는 엔티티를 반환한다.
Is, Equals - 특정 필드가 주어진 값과 일치하는지 검사한다. 예: `findByNameIs(String name)` 혹은 `findByNameEquals(String name)`은 `name` 필드가 주어진 값과 일치하는 엔티티를 반환한다.
Between - 특정 필드의 값이 두 값 사이에 있는 엔티티를 검색한다. 예: `findByAgeBetween(int start, int end)`는 `age` 값이 `start`와 `end` 사이에 있는 엔티티를 반환한다.
LessThan, GreaterThan, LessThanEqual, GreaterThanEqual - 특정 필드의 값이 주어진 값보다 작거나 큰 엔티티를 검색한다. 예: `findByAgeLessThan(int age)`는 `age` 필드의 값이 주어진 값보다 작은 엔티티를 반환한다.
IsNull, IsNotNull - 특정 필드의 값이 null인지 아닌지를 기준으로 검색한다. 예: `findByNameIsNull()`은 `name` 필드의 값이 null인 엔티티를 반환한다.
Like, NotLike - 특정 필드의 값이 주어진 패턴과 일치하는지를 기준으로 검색한다. 예: `findByNameLike(String pattern)`은 `name` 필드의 값이 주어진 패턴과 일치하는 엔티티를 반환한다.
In, NotIn - 특정 필드의 값이 주어진 컬렉션에 포함되어 있는지를 기준으로 검색한다. 예: `findByNameIn(Collection<String> names)`은 `name` 필드의 값이 주어진 컬렉션에 포함된 엔티티를 반환한다.
이 외에도 `OrderBy`, `Top`, `First` 등 다양한 키워드를 사용할 수 있다. 이 방식은 복잡한 쿼리를 작성하지 않고도 간단한 검색을 수행할 수 있어 편리하다.
참고 자료
스프링 부트 핵심 가이드 "스프링 부트를 활용한 애플리케이션 개발 실무" , 장정우, 2022