728x90

저번 주에는 서버와 클라이언트들의 소켓을 열고 통신하는 실습을 해보았다.

 

이번 주에는 그 때 사용했던 socket 함수에 대하여 더 자세히 알아보도록 하자.

 

socket

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

- domain

소켓이 사용할 프로토콜 체계의 정보

- type

소켓 데이터 전송방식에 대한 정보 전달(TCP, UDP)

- protocol

두 컴퓨터간 통신에 사용되는 프로토콜 정보

return: file descriptor(success), -1(error)

 

여기에 들어가는 인자들을 하나씩 알아보자.

 

프로토콜 체계

프로토콜 체계는 Protocol Family로 줄여서 PF라고도 부른다.

프로토콜도 종류에 따라서 부류가 나뉘는데, 그 부류들을 가리켜 프로토콜 체계라고 한다.

많이 사용하는 프로토콜 쳬계들이다.

이름 프로토콜 체계(Protocol Family)
PF_INET IPv4 인터넷 프로토콜 체계
PF_INET6 IPv6 인터넷 프로토콜 체계
PF_LOCAL 로컬 통신을 위한 UNIX 프로토콜 체계
PF_PACKET Low Level 소켓을 위한 프로토콜 체계
PF_IPX IPX 노벨 프로토콜 체계

우리는 이 중에서 IPv4에 해당하는 PF_INET을 사용해 학습할 것이다.

 

소켓 타입

소켓의 타입은 데이터 전송 방식을 의미한다.

socket 함수로 소켓을 생성할 때 소켓의 타입도 같이 결정이 되어야 한다.

우리가 사용하는 PF_INET의 대표적인 소켓 타입은 TCP와 UDP에 해당하는 연결 지향형 소켓 타입과 비 연결 지향형 소켓 타입이 있다.

연결지향형(SOCK_STREAM) - TCP 비 연결지향형(SOCK_DGRAM) - UDP
중간에 데이터가 소멸되지 않는다. 순서 상관없이 빠르 속도로 전송한다.
전송 순서대로 데이터가 수신된다.(순서가 유지된다.) 데이터 손실 및 파손의 우려가 있다.
데이터의 경계가 존재하지 않는다. 데이터의 경계가 존재한다.
소켓: 소켓의 연결은 1:1의 구조로 이루어진다. 한번에 전송할 수 있는 데이터의 크기가 제한된다.

 

연결지향형 소켓을 사용시

int SOCKET = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
int SOCKET = socket(PF_INET, SOCK_STREAM, 0);

 

비 연결지향형 소켓을 사용시

int SOCKET = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
int SOCKET = socket(PF_INET, SOCK_DGRAM, 0);

첫 번째, 두 번째 인자로 사실상 소켓이 결정되기 때문에 세 번째 인자는 0으로 전달 해도 된다.

 

2주차에는 1주차에서 실습했던 내용에 기능을 추가하려 한다.

1. Server에서 메시지를 전송하는 것이 아닌, Client에서 메시지를 전송
2. Server는 수신한 Message를 출력

3. 수신한 문자열의 길이를 strlen() 함수를 이용하여 출력
4. Server는 수신한 Message에 ("-> from Server")라는 문자열을 추가하여 다시 클라이언트로 전송
5. Client는 수신한 Message를 출력

 

그렇게 작성한 Server 프로그램의 코드이다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size;

	char message[BUFSIZ];
	
	if(argc!=2){
		printf("Usage : %s port\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);

	if(serv_sock == -1)
		error_handling("socket() error");
	
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); //IP 주소 할당(자기 자신의 IP 주소로)
	serv_addr.sin_port=htons(atoi(argv[1])); //PORT 번호 할당
	
	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1) //bind 함수로 IP, PORT 할당
		error_handling("bind() error"); 
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
	if(clnt_sock==-1)
		error_handling("accept() error");  
	
    //client로 부터 데이터를 읽어옴
	read(clnt_sock, message, BUFSIZ);
	printf("Received message: %s\n", message); 
    //BUFSIZ의 버퍼 중에서 문자열의 길이를 구함
	printf("Length of message : %lu\n", strlen(message));
	strcat(message, "-> from Server");
    //write 함수를 이용해 descriptor에 문자열을 작성
	write(clnt_sock, message, strlen(message) + 1);

	close(clnt_sock);
	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

진짜 파일 입출력 하던 것처럼 read, write를 사용했다.

 

이번엔 client 코드이다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
	char sendMSG[BUFSIZ], recvMSG[BUFSIZ];
	
	if(argc!=3){
		printf("Usage : %s IP port\n", argv[0]);
		exit(1);
	}

	printf("Please send message: ");
	scanf("%s", sendMSG);
	
	sock= socket(PF_INET, SOCK_STREAM, 0);
	if(sock == -1)
		error_handling("socket() error");
	
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]); //IP 주소 할당
	serv_addr.sin_port=htons(atoi(argv[2])); // PORT 번호 할당
		
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 
		error_handling("connect() error!");

	//write 함수를 이용하여 해당 descriptor에 문자열을 작성
	write(sock, sendMSG, strlen(sendMSG));
    //read 함수를 이용하여 해당 descriptor에서 버퍼로 문자열을 읽어옴
	read(sock, recvMSG, BUFSIZ);

	printf("Receive message: %s", recvMSG);

	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

이렇게 잘 작동하는 것을 볼 수 있다.

여기서 server 코드를 작성하면서 socket과 accept의 리턴값이 같은지 다른지 궁금해졌다.

 

그래서 어차피 int 값의 descriptor들이기 때문에 %d로 출력해보았다.

출력 값들을 보면 다른 것을 볼 수 있다.

서버에서 socket 함수의 리턴 값은 서버 자체의 소켓이고

그 소켓을 이용해서 client들과 통신할 수 있는 개별 소켓들을 열어주어야 한다.

728x90
  • Layered Architecture

우선 기본적인 Layered Architecture에 대해 살펴보자.

사실 저번 HTTP 시간에 공부했지만, 다시 한 번 살펴보는 느낌으로...

 

데이터 통신은 5가지 요소로 이루어져있다.

 

- Message

통신하려고 하는 데이터 자체를 말한다.

 

- Sender

당연히 보내는 사람을 말한다.

 

- Receiver

당연히 받는 사람을 말한다.

 

- Transmission Medium

Sender에서 Receiver로 갈 때 운반되는 물리적인 경로를 말한다.

 

- Protocol

데이터 통신을 위해 고려해야 하는 규칙들을 말한다.

 

이 중에서 Protocol을 살펴보자.

위에서 말한 것처럼 Protocol은 데이터 통신에서 Sender와 Receiver, 그리고 모든 중간 단계에서 통신을 직접적으로 하기 위해 정의한 규칙들을 말한다.

 

현재 인터넷에서 가장 많이 사용하는 TCP/IP Protocol Suite에 대해 알아보자.

TCP/IP Protocol Suite는 5계층으로 이루어져있다.

Applcation -> Layer 5
Transport -> Layer 4
Network -> Layer 3
Data link -> Layer 2
Physical -> Layer 1

 

Application 계층에서 데이터를 전송하려고 하면, Transport 계층에 해당하는 TCP or UDP로 메시지나 데이터 스트림을 전송한다.

Transport 계층은 대상 데이터를 작은 조각으로 나누고 대상 주소를 포함시켜 패킷으로 만든 후에 다음 계층으로 넘긴다.

Network 계층은 패킷을 IP 데이터그램에 포함한 후 데이터그램 헤더 및 트레일러에 넣고 데이터그램 전송 위치(대상에 직접 또는 게이트웨이에)를 결정한 후 Data link 단계로 넘긴다.

Data link 단계는 충돌과 보낼 시기를 관리하고 Physical 단계로 넘긴다.

Physical 단계는 물리적인 하드웨어를 이용하여 데이터를 전송한다.

Application Layer
      DATA
Transport Layer
    TCP 헤더 DATA
Network Layer
  IP 헤더 TCP 헤더 DATA
Data link Layer
이더넷 헤더 IP 헤더 TCP 헤더 DATA
Physical Layer

이렇게 단계를 하나씩 거칠 때마다, 헤더를 하나씩 추가한다.

수신 할 때에도 단계를 하나씩 올리면서 헤더를 하나씩 확인하며 떼어 내면서 수신하게 된다.

 

이정도만 소개하고 만약 더 필요한 내용이 있다면, HTTP 공부한 내용을 참고하기 바란다.

 

  • 네트워크 프로그래밍

네트워크 프로그래밍은 소켓을 기반으로 프로그래밍을 한다. 그렇기 때문에 소켓 프로그래밍이라고도 한다.

네트워크로 연결된 둘 이상의 컴퓨터 사이에서의 데이터 송수신 프로그램 작성을 의미한다.

 

소켓은 네트워크 연결의 도구로, 운영체제에 의해 제공이 되는 소프트웨어 장치이다.

프로그래머는 이 소켓만 이용하고, 소켓 덕분에 더 아래 계층에 대한 사용과 이해를 피할 수 있게 한다.

아래 부분은 아직 1주차에 배울 내용은 아니지만, 한 번 실습을 해보는 부분이다.

이해가 안되어도 그냥 넘어가면 된다.

 

Server 함수

소켓은 전화기라고 생각하면 된다.

하지만, 서버와 클라이언트에 따라 전화를 받는 용도의 소켓이 있으며 전화를 거는 소켓이 있다.

이 소켓은 다른 방법으로 생성을 해야한다.

 

socket

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

- domain

생성할 소켓이 통신을 하기 위해 사용할 프로토콜

- type

소켓이 데이터를 전송하는 데, 사용하는 전송 타입

- protocol

소켓을 사용하기 위한 프로토콜을 지정

return: File descriptor, -1(error)

 

시스템 프로그래밍에서 사용했던 File descriptor처럼 생각을 하면 된다.

이 곳에다가 쓰면 데이터가 송신이 되고, 이곳을 읽으면 데이터를 수신하는 것이다.

 

소켓은 전화기로 생각을 하면 된다고 했었다.

하지만 전화기를 샀다고 바로 전화가 되지는 않을 것이다.

해당 전화기에 번호를 부여해 주어야 한다.

마찬 가지로 소켓에도 주소 정보를 넣어주어야 한다.

주소 정보는 당연히 IP와 PORT 번호로 구성이 된다.

 

bind

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

- sockfd

socket 함수로 생성한 file descriptor

- myaddr

IP 주소와 PORT 번호를 지정한 sockaddr 구조체

- addrlen

주소 정보를 담은 변수의 길이

return: 0(success), -1(error)

 

이렇게 번호까지 지정을 해주었다.

 

그러면 이제 이 전화기를 VOIP처럼 연결 가능상태로 만들어주어야 한다.

자리 비움이 아닌 수신 가능 상태로 설정한다고 생각하면 될 것이다.

 

특이하게 전화를 받을 수 있는 횟수가 지정이 되는데

그 값을 backlog 인수로 넣어준다.

 

listen

#include <sys/socket.h>

int listen(int sockfd, int backlog);

- sockfd

socket 함수로 생성한 descriptor

- backlog

수신할 큐의 개수 설정

return: 0(success), -1(error)

 

이제 VOIP에서 전화를 받아야 한다.

스피커 폰이 아닌 이상, 수화기를 들어야 내용을 수신할 수 있다.

 

그렇기 때문에 listen으로 연결 가능으로 설정을 해주어도, accept로 수락을 해주어야 내용을 수신할 수 있다.

수락 이후에 데이터의 송수신은 양방향으로 가능하다.

 

accept

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

- sockfd

socket 함수로 생성한 descriptor

- addr

sockaddr 구조체 포인터로, 연결이 성공하면 이 포인터에 클라이언트에 대한 정보를 채워 돌려주게 된다.

- addrlen

sockaddr 크기

return: file descriptor(success), -1(error)

 

이렇게 4가지를 알아보았고, 단계별로 이해하는 게 제일 쉬울 것이다.

STEP 1 소켓을 생성 socket 함수
STEP 2 IP와 PORT 번호를 할당 bind 함수
STEP 3 연결 가능한 상태로 변경 listen 함수
STEP 4 연결 요청에 대한 수락 accept 함수

 

Client 함수

클라이언트는 소켓을 생성하고 연결을 하면 된다.

 

connect

#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);

- sockfd

socket 함수로 생성한 descriptor

- serv_addr

연결할 서버의 IP 주소와 PORT 번호들을 담아올 sockaddr 구조체

- addrlen

sockaddr 구조체의 크기

return: 0(success), -1(error)

 

 

다시 한 번 말하지만, 1주차에 한 번 해보는 내용으로 당연히 이해가 안 될수도 있다.

client와 server의 코드를 한 번 작성해보도록 하겠다.

 

실제 학교에서 실습했던 코드를 살짝만 수정해서 보도록 하겠다.

함수들에 들어가는 인자들은 나중에 배우도록 하겠다.

 

Server 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size;

	char message[]="KNU, Han Seungkyu";
	
	if(argc!=2){
		printf("Usage : %s port\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);

	if(serv_sock == -1)
		error_handling("socket() error");
	
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); //IP 주소 할당(자기 자신의 IP 주소로)
	serv_addr.sin_port=htons(atoi(argv[1])); //PORT 번호 할당
	
	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1) //bind 함수로 IP, PORT 할당
		error_handling("bind() error"); 
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
	if(clnt_sock==-1)
		error_handling("accept() error");  
	
	write(clnt_sock, message, sizeof(message));
	close(clnt_sock);
	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

Client 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
	char message[30];
	int str_len=0;
	int idx=0, read_len=0;
	
	if(argc!=3){
		printf("Usage : %s IP port\n", argv[0]);
		exit(1);
	}
	
	sock= socket(PF_INET, SOCK_STREAM, 0);
	if(sock == -1)
		error_handling("socket() error");
	
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]); //IP 주소 할당
	serv_addr.sin_port=htons(atoi(argv[2])); // PORT 번호 할당
		
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 
		error_handling("connect() error!");

	while(read_len=read(sock, &message[idx++], 1))
	{
		if(read_len==-1)
			error_handling("read() error!");
		
		str_len+=read_len;
	}

	printf("Message from server: %s \n", message);
	printf("Function read call count: %d \n", str_len);
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

실행을 해보면 비록 스스로와의 통신이긴 하지만 잘 작동하는 것을 볼 수 있다.

컴파일 하는 방법과 실행 방법은 시스템 프로그래밍에서 배웠을 것이다.

728x90

이번엔 List이다.

문제에 관련된 DB가 있다고 하자.

테이블은

question
id varchar(10)
text varchar(20)
question_choice
question_id varchar(10)
idx integer(10)
text varchar(20)

그러면 question안에 객관식 보기인 question_choice들이 순서대로 들어가 있어야 할 것이다.

이런 경우에는 순서가 존재하기 때문에 저번에 사용한 Set을 사용할 수 없고, List를 사용해야 한다.

import jakarta.persistence.*;

import java.util.List;

@Entity
@Table(name = "question")
public class question {
    @Id
    private String id;
    private String text;

    @ElementCollection
    @CollectionTable(
            name = "question_choice",
            joinColumns = @JoinColumn(name = "question_id")
    )
    @OrderColumn(name = "idx")
    @Column(name = "text")
    private List<String> choices;

    protected question () {}

    public question(String id, String text, List<String> choices) {
        this.id = id;
        this.text = text;
        this.choices = choices;
    }
    //getter... settter
}
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

import java.util.List;

public class questionMain {

    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            question question1 = new question("1", "문제1", List.of("1번", "2번", "3번", "4번", "5번"));
            entityManager.persist(question1);
            entityTransaction.commit();
        }catch (Exception ignored){

        }

        try{
            question question2 = entityManager.find(question.class,"1");
            for(String choice : question2.getChoices()){
                System.out.println(choice);
            }
        }finally {
            entityManager.close();
        }

        entityManagerFactory.close();
    }
}

이렇게 Set과 똑같이 작성하지만, @OrderColumn을 추가해준다.

당연히 @Embedded도 사용이 가능하다.

import jakarta.persistence.Embeddable;

@Embeddable
public class choice {
    private String text;
    private boolean input;

    protected choice() {}

    public choice(String text, boolean input) {
        this.text = text;
        this.input = input;
    }

	//getter... setter...
}
import jakarta.persistence.*;

import java.util.List;

@Entity
@Table(name = "question")
public class question2 {
    @Id
    private String id;
    private String text;

    @ElementCollection
    @CollectionTable(
            name = "question_choice",
            joinColumns = @JoinColumn(name = "question_id")
    )
    @OrderColumn(name = "idx")
    private List<choice> choiceList;

    protected question2 (){}

    public question2(String id, String text, List<choice> choiceList) {
        this.id = id;
        this.text = text;
        this.choiceList = choiceList;
    }
	//getter... setter...
}
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

import java.util.List;

public class questionMain2 {

    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            question2 question1 = new question2("1", "문제1", List.of(new choice("1", false),
                    new choice("2", true)));
            entityManager.persist(question1);
            entityTransaction.commit();
        }catch (Exception ignored){

        }

        try{
            question2 question2 = entityManager.find(question2.class,"1");
            for(choice choice : question2.getChoiceList()){
                System.out.println(choice.getInput() + ", " + choice.getText());
            }
        }finally {
            entityManager.close();
        }

        entityManagerFactory.close();
    }
}

'백엔드 > JPA' 카테고리의 다른 글

JPA 10장 (영속 컨텍스트와 LifeCycle)  (0) 2023.03.22
JPA 9장 (Map collection mapping)  (0) 2023.03.21
JPA 7장 (Set collection mapping)  (0) 2023.03.18
JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
728x90

 

이런 테이블들이 있다고 해보자.

job
id VARCHAR(10)
name VARCHAR(100)

 

job_perm
job_id VARCHAR(10)
permission VARCHAR(20)

 

이렇게 job 안에 permission에 해당 하는 값들을 보관하고 있어야 한다.

import jakarta.persistence.*;

import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "job")
public class job {
    @Id
    private String id;
    private String name;

    @ElementCollection
    @CollectionTable(
            name="job_perm", //job_perm 테이블 명
            joinColumns = @JoinColumn(name = "job_id") //job_perm 테이블의 job_id column
    )
    @Column(name = "perm") //job_perm 테이블의 permission column
    private Set<String> permissions = new HashSet<>();

    protected job() {}

    public job(String id, String name, Set<String> permissions) {
        this.id = id;
        this.name = name;
        this.permissions = permissions;
    }
	//getter... setter...
    public Set<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<String> permissions) {
        this.permissions = permissions;
    }
}

이렇게 내부에 Set을 만들고 @ElementCollection을 작성해준다.

 

한 번에 이해하기 힘들테니 바로 실행을 해보고 결과를 확인해보자.

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

import java.util.Set;

public class roleMain{
    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            job job1 = new job("1", "안녕", Set.of("11", "22"));
            entityManager.persist(job1);
            entityTransaction.commit();
        }catch(Exception ignored){

        }

        try{
            job job2 = entityManager.find(job.class, "1");
            for(String perm : job2.getPermissions()){
                System.out.println(perm);
            }
        }finally {
            entityManager.close();
        }

        entityManagerFactory.close();
    }
}

job을 persist 할 때 Set에 11, 22에 해당하는 값을 입력해주면

이렇게 job 테이블에 데이터 하나가 추가가 되고

job_perm 테이블에는 집합으로 넣었던 데이터 2개가 추가가 된다.

find로 조회할 때는

class에 작성했던 getPermission을 통해 for-each문으로 가져오면 된다.

 

당연히 Embeddable 타입도 가능하다.

이런 관계에서도 위와 비슷하게 @Embeddable 클래스만 만들어주고 동일 작업을 수행하면 된다.

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;

@Embeddable
public class information {
    @Column(name = "perm")
    private String permission;
    private String info;

    protected information () {}

    public information(String permission, String info) {
        this.permission = permission;
        this.info = info;
    }
    
    //getter... setter
}
import jakarta.persistence.*;

import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name="role")
public class job2 {
    @Id
    private String id;
    private String name;

    @ElementCollection
    @CollectionTable(
            name="role_perm",
            joinColumns = @JoinColumn(name = "role_id")
    )
    private Set<information> informationSet = new HashSet<>();

    protected job2(){}

    public job2(String id, String name, Set<information> informationSet) {
        this.id = id;
        this.name = name;
        this.informationSet = informationSet;
    }
	//getter... setter...
    public Set<information> getInformationSet() {
        return informationSet;
    }

    public void setInformationSet(Set<information> informationSet) {
        this.informationSet = informationSet;
    }
}

이렇게 정상적으로 작동하게 된다.

'백엔드 > JPA' 카테고리의 다른 글

JPA 9장 (Map collection mapping)  (0) 2023.03.21
JPA 8장 (List collection mapping)  (0) 2023.03.18
JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
728x90

@Embeddable annotation에 대해 배워보자.

 

테이블에 값을 저장할 때 

title date category publisher writer_name writer_mail
... ... ... ... ... ...

writer_name, witer_mail 같이 서로 묶이면 더 보기 편할 거 같은 column들이 있다.

이럴 때 @Embeddable을 사용한다.

@Embeddable은 엔티티가 아닌 타입을 한 개 이상의 필드와 매핑할 때 사용한다.

 

package user;

import jakarta.persistence.Embeddable;

@Embeddable
public class school {

    private String elementarySchool;
    private String middleSchool;
    private String highSchool;

    public school(String elementarySchool, String middleSchool, String highSchool) {
        this.elementarySchool = elementarySchool;
        this.middleSchool = middleSchool;
        this.highSchool = highSchool;
    }

    protected school() {

    }

	//getter..., setter...
}

예시를 보자.

학교와 관련된 column들을 school이라는 클래스에 묶었다.

당연히 여기서 변수 명들은 각자의 column명과 일치하거나, 일치하지 않는다면 @Column으로 mapping을 해주어야 한다.

그리고 school이라는 클래스에 @Embeddable을 달아준다.

 

이제 테이블에 해당하는 클래스를 작성하자.

package user;

import jakarta.persistence.*;

@Entity
@Table(name = "user")
public class user {

    @Id
    private String email;
    private String name;
    @Embedded
    private school school;

    protected user() {
    }

    public user(String email, String name, school school) {
        this.email = email;
        this.name = name;
        this.school = school;
    }

	//getter... setter...
}

그러고 Id에 해당하는 멤버에 @Embedded를 달아준다.

이렇게 정상적으로 생성과 조회가 되는 것을 볼 수 있다.

 

만약 여기 school에 null을 넣어준다면, school에 들어있는 column의 값들에도 모두 null이 들어가게 된다.

 

같은 @Embeddable 타입 필드가 두 개라면 어떻게 될까?

당연히 에러가 난다.

 

그럴 때는 뒤에 오는 column은 @AttributeOverride로 설정을 재정의 해줘야 한다.

package school;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;

@Embeddable
public class info {

    @Column(name = "eleaddr")
    private String addr;
    @Column(name = "elenum")
    private String num;

    protected info(){}

    public info(String addr, String num) {
        this.addr = addr;
        this.num = num;
    }
    //getter... setter...
}
package school;

import jakarta.persistence.*;

@Entity
@Table(name = "school_table")
public class school {

    @Id
    @Embedded
    private info elementInfo;

    @AttributeOverrides({
            @AttributeOverride(name = "addr", column = @Column(name = "midaddr")),
            @AttributeOverride(name = "num", column = @Column(name = "midnum")),
    })
    @Embedded
    private info midInfo;

    protected school () {}

    public school(info elementInfo, info midInfo) {
        this.elementInfo = elementInfo;
        this.midInfo = midInfo;
    }
}

이렇게 info에 mapping 되는 변수가 2개 있으면 하나를 @AttributeOverride로 재정의 해준다.

그러면 충돌이 일어나지 않고 정상적으로 생성, 조회가 되는 것을 볼 수 있다.

'백엔드 > JPA' 카테고리의 다른 글

JPA 8장 (List collection mapping)  (0) 2023.03.18
JPA 7장 (Set collection mapping)  (0) 2023.03.18
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 3장 (간단한 CRUD 구현해보기)  (0) 2023.03.16
728x90
  • 직접 할당

@Id 설정 대상에 직접 값 설정

저장하기 전에 생성자를 할당한다, 보통 생성 시점에 전달

@Entity
@Table(name = "user1")
public class User1{
    @Id
    private String name;
    private String email;
    
    protected User() {}
    
    public User (String name, String email){
    	this.name = name;
        this.email = email;
	}
}

이렇게 가장 단순한 방법으로 생성하는 방식이다.

  • 식별 칼럼 방식

칼럼 중에 auto_increment 같이 객체 생성시에 식별값을 설정하지 않는 경우가 있다.

그럴 때에는 설정을 추가하여 persist()를 실행할 때 객체에 식별자 값을 할당하게 할 수 있다.

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class User2{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    
    protected User2(){}

    public User2(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

그 외에도 몇가지 방식이 더 있지만 많이 사용하지 않기 때문에 이렇게만 알아보도록 하겠다.

'백엔드 > JPA' 카테고리의 다른 글

JPA 7장 (Set collection mapping)  (0) 2023.03.18
JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 3장 (간단한 CRUD 구현해보기)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
728x90
  • Entity 기본 annotation

@Entity: 엔티티 클래스에 설정, 필수!

@Table: mapping 테이블을 지정

@Id: 식별자 속성에 설정, 필수!

@Column: 매핑할 컬럼명 지정, 지정하지 않으면 필드명과 프로퍼티명을 사용

@Enumerated: enum 타입 매핑할 때 설정

 

  • Table annotation

annotation을 생략하면 클래스의 이름과 동일한 이름에 mapping

속성으로는

- name: 테이블 이름

- catalog: 카탈로그 이름

 

  • Enumerated

EnumType.STRING: enum 타입 값 이름을 저장

EnumType.ORDINAL: enum 타입의 값의 순서를 저장

 

 

Mapping이 된 설정의 예를 보도록 하자

//mapping을 합니다.
@Entity
//user 테이블에 mapping 합니다.
@Table(name = "user")
public class User {

	//식별자에 mapping 합니다.
    @Id
    //변수명에 mapping 합니다.
    private String email;
    private String name;
    
    //create_date에 mapping 합니다.
    @Column(name = "create_date")
    private LocalDateTime date;
	
    //열거타입 이름을 값으로 저장
    //grade에 mapping
    @Enumerated(EnumType.STRING)
    private Grade grade;

    protected User(){}
}

 

  • Entity 클래스의 제약 조건

1. 반드시 @Entity를 작성해야함

2. 반드시 @Id를 작성해야함

3. 인자 없는 기본 생성자 필요(해당 생성자는 public이나 protected로 작성이 되어야 함)

4. 최상위 클래스여야 함.

'백엔드 > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 3장 (간단한 CRUD 구현해보기)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
JPA 1장 (우선 시작해보기)  (0) 2023.03.15
728x90

저번에 배웠던 CRUD를 사용해서 간단한 프로그램을 구현해보자.

 

package UserProgram;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class EMF {

    private static EntityManagerFactory entityManagerFactory;

    public static void init(){
        entityManagerFactory = Persistence.createEntityManagerFactory("jpabegin");
    }

    public static EntityManager getEntityManager() {
        return entityManagerFactory.createEntityManager();
    }

    public static void close(){
        entityManagerFactory.close();
    }
}

어차피 EntityManagerFactory는 계속 사용을 해야하니, 초기화하고 EntityManagerFactory에서 EntityManager를 가져올 수 있는 class를 작성해준다.

 

당연히 간편하게 사용하기 위해 만드는 것으로 굳이 안 만들어도 되기는 한다.

 

그 다음에 DB에서 사용할 User 객체를 만들어준다.

저번에 사용했던 그대로 사용할 예정이다.

package UserProgram;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

@Entity
@Table(name = "user")
public class User {

    @Id
    private String email;
    private String name;
    private LocalDateTime create_date;

    protected User(){}

    public User(String email, String name, LocalDateTime create_date) {
        this.email = email;
        this.name = name;
        this.create_date = create_date;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalDateTime getCreate_data() {
        return create_date;
    }

    public void setCreate_data(LocalDateTime create_data) {
        this.create_date = create_data;
    }

    @Override
    public String toString() {
        return "User{" +
                "email='" + email + '\'' +
                ", name='" + name + '\'' +
                ", create_data=" + create_date +
                '}';
    }
}

이제 본격적으로 JPA를 사용할 차례이다.

Service 하는 메서드들을 한 곳에 모아 클래스로 작성했다.

 

package UserProgram;

import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;

public class UserService {

    public void CreateUser(User user){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            entityManager.persist(user);
            entityTransaction.commit();
        }catch(Exception exception){
            entityTransaction.rollback();
            throw exception;
        } finally {
            entityManager.close();
        }
    }

    public User ReadUser(String email){
        EntityManager entityManager = EMF.getEntityManager();
        try{
            User user = entityManager.find(User.class, email);
            if(user == null) throw new NullPointerException();
            else return user;
        }finally{
            entityManager.close();
        }
    }

    public void UpdateUser(String email, String name){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            User user = entityManager.find(User.class, email);
            if(user == null) throw new NullPointerException();
            user.setName(name);
            entityTransaction.commit();
        }catch (Exception exception){
            entityTransaction.rollback();
            throw exception;
        }finally {
            entityManager.close();
        }
    }

    public void DeleteUser(String email){
        EntityManager entityManager = EMF.getEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try{
            entityTransaction.begin();
            User user = entityManager.find(User.class, email);
            if (user == null) throw new NullPointerException();
            entityManager.remove(user);
            entityTransaction.commit();
        }catch(Exception exception){
            entityTransaction.rollback();
            throw exception;
        }finally {
            entityManager.close();
        }
    }
}

우선 저번에 배운 CRUD를 사용하였고, 다른 점이 있다면 static으로 작성된 EntityManagerFactory를 사용하였다.

우선 예외는 대충 넘기고 나중에 추가로 작성하도록 하겠다.

package UserProgram;

import java.time.LocalDateTime;
import java.util.Scanner;

public class UserProgram {

    private static UserService userService = new UserService();

    private static String email;
    private static String name;

    public static void main(String[] args) {
        EMF.init();

        Scanner scanner = new Scanner(System.in);

        boolean flag = true;

        while(flag){
            System.out.print("1. Create, 2. Read, 3. Update, 4. Delete, 5. exit : ");
            int answer = scanner.nextInt();
            switch(answer){
                case 1:
                    CreateMethod();
                    break;
                case 2:
                    ReadMethod();
                    break;
                case 3:
                    UpdateMethod();
                    break;
                case 4:
                    DeleteMethod();
                    break;
                case 5:
                    flag = false;
                    break;
            }
        }

        EMF.close();
        scanner.close();
    }

    private static void inputEmail(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("input email:");
        email = scanner.nextLine();
    }

    private static void inputName(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("input name:");
        name = scanner.nextLine();
    }

    private static void CreateMethod(){
        inputEmail();
        inputName();
        User user = new User(email, name, LocalDateTime.now());
        try{
            userService.CreateUser(user);
        }catch(Exception exception){
            System.out.println("createError");
        }
    }

    private static void ReadMethod(){
        inputEmail();
        try{
            User user = userService.ReadUser(email);
            System.out.println(user);
        }catch(Exception exception){
            System.out.println("readError");
        }
    }

    private static void UpdateMethod(){
        inputEmail();
        inputName();
        try{
            userService.UpdateUser(email, name);
            System.out.println(userService.ReadUser(email));
        }catch(Exception exception){
            System.out.println("updateError");
        }
    }

    private static void DeleteMethod(){
        inputEmail();
        try{
            userService.DeleteUser(email);
        }catch(Exception exception){
            System.out.println("deleteError");
        }
    }
}

이렇게 프로그램을 작성하였다.

그러면 이렇게 프로그램이 잘 작동한다.

 

값을 추가하고 삭제하는 메서드들도 잘 작동이 된다.

'백엔드 > JPA' 카테고리의 다른 글

JPA 6장 (@Embeddable)  (0) 2023.03.17
JPA 5장 (Entity 식별자 생성 방식)  (0) 2023.03.16
JPA 4장 (Entity에 대하여)  (0) 2023.03.16
JPA 2장 (영속 컨텍스트)  (0) 2023.03.15
JPA 1장 (우선 시작해보기)  (0) 2023.03.15

+ Recent posts