보통 MariaDB 포트는 3306을 사용하지만, 보안 정책에 따라 포트를 변경하여 사용하는 경우도 있다.

 

이때, 해당 포트를 입력하여 접속하는 방법은 아래와 같다.

[root@whylee mariadb] # mysql -u[계정이름] -p -h[ip주소or도메인] -P[포트번호]

 

예시) mysql -u whylee -p -h127.0.0.1 -P3309

 

해당 방법을 몰랐을 때에는

[root@whylee mariadb] # mysql -u whylee -p

Enter password:  xxxxxx

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)

↑ 

요 에러만 주구장창 났었다 ㅠㅠㅠ

해당 경로에 mysql.sock 이 있었는데에도 계속해서 해당 에러를 뱉으니까 답답했다..

 

이때 중요한 건, 포트 입력할 때 -P (대문자 P) 를 입력해야한다는 것이었다!

상황

300명 이상 수신자에게 예약/대량 메일 발송 시 두 번 이상 발송되는 문제

 

문제

첫 번째 요청이 끝나기도 전에 동일한 요청이 발생됨.

그 시간이 딱 1분.

WEB Server 로그 분석 (이중화 환경)

클라이언트 요청이 중복으로 들어온 것을 확인.

그 시간 간격이 딱 1분이었다.

어떤 상황에서는 1번기에 동일하게 중복 요청이 들어왔고 어떤 상황에서는 1번기, 2번기 각각 중복 요청이 들어왔다.

해결 과정

  • 원타임토큰 문제?

처음엔 메일 중복 발송을 막기 위한 OneTimeToken 의 문제로 파악했다.

사용자가 [보내기] 버튼 혹은 네트워크 장애로 인해 두 번 이상 호출되어 중복 메일 발송되는 문제를 예방하는 역할을 한다. 그러나 로그 심어 확인해 보니, 원타임토큰은 동일했다.

  • ajax 호출 응답 시간?

메일 발송은 ajax 이용하여 비동기 처리로 응답시간 최대 timeout 1분 설정을 해 놓은 상태였다.

대량 수신자 메일 발송 처리에 대한 시간이 지연되는 것으로 생각하여 이를 최대 3분으로 늘렸다.

결과는 동일했다.

  • 예약메일 로직 쓰레드 방식으로 개선

대량 수신자의 예약메일의 경우 예약메일을 관리하는 테이블에 데이터를 넣는데 그 시간이 오래 걸려 발생되는 문제로 파악했다. 이를 개선하고자 사용자한테는 예약메일 발송 시, 바로 응답을 해주고 백단에서 멀티스레드 방식으로 큐테이블에 데이터 삽입하도록 개선하였다.

그럼에도 동일한 문제가 발생….

  • L4 로드밸런서 문제를 의심

이중화 된 웹서버 로그가 수상했다.

위에서도 말했듯이 어떤 상황에서는 1번기에 동일하게 중복 요청이 들어왔고 어떤 상황에서는 1번기, 2번기 각각 중복 요청이 들어왔다.

 

중간에 거치는 게 뭐가 있을까?

L4 의 어떤 설정 때문에 그런가?

 

원인 : 로드 밸런서 유휴 시간 초과

클라우드 인프라 L4 설정 중 Connection Idle Timeout 설정의 문제였다.

디폴트 1분(60초)으로 설정되어 있었다. ( 그래서 로그에서도 딱 1분이 걸렸었구나...)

로드밸런서가 target 에 대해 요청하는 동안 target 이 TCP 연결을 닫았다.

개발기에서 3초로 설정하고 테스트 해보았더니 이슈와 같이 동일한 요청이 들어왔다.

찾았따!!!!!

**1. Connection Idle Timeout 이 뭔가?**

Http 통신할 때, client 에서 아무런 데이터 보내지 않을 경우 idle timeout 이 지난 후 
connection 을 close 한다.

즉, http 통신 연결이 자동으로 닫힐 때까지의 시간을 말한다.
이때 502/504 에러 응답코드가 나올 수 있다.

연결 유휴 시간 제한(기본값은 60초)은 "표준" 시간 제한으로 작동한다.
따라서 Idle Timeout 경과할 때까지 데이터가 전송되거나 수신되지 않은 경우 로드 밸런서가 연결을 닫는다.
애플리케이션의 Idle Timeout 을 로드밸런서 Idle Timeout 보다 크게 설정하는 것을 권장한다.

**2. Idle Timeout 과 request timeout 의 차이가 뭔가?**

Connection Idle Timeout 은 tcp 연결이 유휴한 상태로 있을 수 있는 최대 시간이다.
요청 시간 제한과 동일하게 동작한다.

Connection Timeout 은 tcp 연결을 지속하는 최대 시간을 말한다.

여기서 Idle Timeout 과 request timeout 중 Idle Timeout 이 우선순위로 지정된다.
그래서 Idle Timeou 이 request timeout 보다 낮아도 우선순위로 적용된다.

출처 : <https://repost.aws/questions/QUqP4BHC9iQ0uIB69M7QrjSg/502-errors-with-application-load-balancer-idle-timeout-apache2-keep-alive-timeout>

해결

60 초로 설정되어 있는 idle timeout 시간을 600초(10분) 으로 늘려주었다.


그 외 원타임토큰에 대한 문제

첫 번째 요청이 들어오면 해당 토큰은 세션에서 remove 된다.

그런데 두 번째 요청이 들어왔을 때 세션에 해당 토큰이 살아있었다.

테스트 해보니, 하나의 요청이 완전히 끝나기 전까지는 세션 정보가 변경되지 않았다.

그래서 두 번째 요청이 들어왔을 때 변경된 세션 정보가 아닌 기존 정보를 가지고 있었던 것이다.

원타임토큰을 세션에서 관리하는 것이 아닌 DB 에서 관리는 하는 것으로 변경하는 것이 좋을 것 같다.

개요

1. 사설 IP 사용하는 WAS  A서버에서 ssh 로 서버 B서버(WEB/WAS단일) 접근 후

2. B서버에서 다시 WAS A서버로 L4 통해 API 통신 시도

문제

사설 대역에서 접속한 B서버는 https://cmail.cloud.kr/api/xxx/xxxx/xxxx 이라는 API URL  호출한다.

이 과정이 3번 과정이다. 3번을 보면 URL 호출 후 외부로 나갔다가 다시 L4 통해 WEB1, WAS A서버로 들어온다.

 

문제는 해당 API URL 호출 시 WAS A서버까지 들어오지 않았다.

 

분석

1. B서버에서 80/443 통신이 안되나?

서버 통신할 때 확인하는 방법으로 아래와 같이 확인해봤다.

echo > /dev/tcp/cmail.cloud.kr/80

echo > /dev/tcp/cmail.cloud.kr/443

정상 통신 되는 걸 확인하였다.

-> 나가는 포트는 모두 열려있으므로 L4까지는 도달한다는 의미이다.

 

2. 그래서 그 다음으로 L4 통해 들어오는 WEB 서버 apache access 로그를 봤다.

client IP 가 찍혀 있어야 하는데 웬 사설 IP 가 찍혀있었다.

 

동일한 WEB서버를 바라보는 다른 L4 로 등록된 mail.sensmailcloud.kr URL 로 API 호출해봤떠니 얘는 정상적으로 WAS 까지 들어오더라.

apache access 로그 봤더니 얘는 사설 IP가 아닌 clientIP(=공인IP) 가 찍혀있었다. (정상)

 

핵심

모든 곳에서 https://cmail.cloud.kr 에 접속하여 아파치 access 로그를 확인한 결과  모두 동일한 사설IP 가 찍히고 있었다.

사무실 와이파이로 접속해도, LTE로 접속해도, 다른 서버에서 접속해도 모두 같은 IP 로 찍혔다.

정상적인 Client IP 설정이 되어 있다면, 사무실 와이파이로 접속하면 사무실 IP 가 찍혀야하고 다른 서버에서 접속했다면 접속한 서버 IP 가 찍혀야한다. (Client IP=요청한 곳의 IP)

 

해결

이 부분은 클라우드 콘솔에서 LB 설정 proxy protocol사용을 해야한다.

프록시 프로토콜을 사용(체크)하지 않으면 실제 Client Ip 가 아닌 L4 사설 Ip 가 찍히게 된다.

출처 : 네이버클라우드플랫폼 로드밸런서 생성화면

생각넓히기

Proxy Protocol 설정을 왜 하는가?

로드밸런서에서 TCP/SSL 프로토콜 사용 시, Proxy Protocol 을 사용하여 Client IP를 추출하고 특정 IP만 접근 허용할 수 있도록 한다.

 

그렇다.  Client IP 를 추출하기 위해 해당 설정을 해주는 것이다.

IP 기반으로 처리하기 위해서는 Client Ip가 필요하다.

Ip 추척 기능이라든가 API 기능이라든가 IP 제한 이라든가 하는 다양한 IP 기반의 기능을 처리해야한다면 Client IP가 필요하기 때문이다.

 

★ 주의할 점은 Proxy Protocol 을 사용하기 위해서는 반드시 TCP나 SSL 프로토콜선택해 주어야 한다.

Proxy Protocol  https://whyjlee.tistory.com/31

 

참고

 

L4 환경에서의 ACL 설정 — TCP 프로토콜 사용 시

로드밸런서에서 TCP/SSL 프로토콜 사용 시, Proxy Protocol을 사용하여 Client IP를 추출하고 특정 IP만 접근을 허용하는 법에 대해서 소개해드리고자 합니다.

medium.com

 

 

Proxy Protocol을 이용해 Client IP 확인하기 CentOS

Ncloud Network Proxy Load Balancer의 Proxy Protocol을 이용해 CentOS 서버에서 클라이언트 IP 주소를 확인하는 방법입니다.

docs.3rdeyesys.com

 

  • 자신의 OS 정보 확인
[root@localhost ~]$ hostnamectl
   Static hostname: localhost
	...
  Operating System: CentOS Linux 7 (Core)
       CPE OS Name: cpe:/o:centos:centos:7
            Kernel: Linux 3.10.0-1160.el7.x86_64
      Architecture: x86-64

 

  • 하단 링크 연결하여 오라클 클라이언트 버전에 맞춰 Basic 과 Sqlplus 의 RPM 파일을 다운로드 한다.

https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html

 

★ 여기서 주의 ★

내가 연결하려는 원격 Oracle 버전을 확인하자!

해당 버전에 맞는 클라이언트 버전을 설치해줘야 한다.!!

 

  • root 계정으로 rpm 파일을 설치한다. (basic 먼저 설치한 후, sqlplus 설치한다.)
$ yum install -y oracle-instantclient-basic-10.2.0.5-1.x86_64.rpm
$ yum install -y oracle-instantclient-sqlplus-10.2.0.5-1.x86_64.rpm

 

ERROR: Failed dependences: libaio is needed by oracle_instantclient10.2.0.5.-1.x86_64

에러 경우 의존성 라이브러리 파일이 없어서 발생하는 것이므로 아래 라이브러리 설치하면 된다.

$ yum install -y libaio

 

  • ORACLE_HOME, TNS_ADMIN 환경변수를 위한 설정파일을 추가한다.
$ vi /etc/profile.d/oracle.sh

export ORACLE_HOME=/usr/lib/oracle/10.2.0.5/client64
export TNS_ADMIN=/usr/lib/oracle/10.2.0.5/client64/bin

  • .bash_profile 하단에 ORACLE_HOME, TNS_ADMIN, PATH 설정 추가 후 .bash_profile 을 적용한다.
vi ~/.bash_profile

export ORACLE_HOME=/usr/lib/oracle/10.2.0.5/client64
export TNS_ADMIN=/usr/lib/oracle/10.2.0.5/client64/bin
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:$LD_LIBRARY_PATH
export PATH=$ORACLE_HOME/bin:$PATH
export NLS_LANG=KOREAN_KOREA.AL32UTF8

설정 후, 바로 적용을 위해 source 명령어를 사용한다.

$ source ~/.bash_profile

정상적으로 적용된 것을 확인하려면 env 명령어를 사용해보자.

$ env
  • oracle instant client 에서 oracle server 로 접속하기 위해서 tnsnames.ora 를 설정한다. 
    TNS(Transparent Network Substrate) 는 오라클에서 사용하는 네트워크 기술이다.
    Client/Server 또는 Server/Server 간에도 Data 전송을 가능하게 해주는 기술이다.

    ★ tnsnames.ora 파일은 오라클 클라이언트 측에서 오라클 서버로 접속할 때 필요한 프로토콜 및 포트번호, 서버주소, 인스턴스 등을 설정해주는 파일이다.
    ★tnsnames.ora 파일은 우리가 TNS_ADMIN 경로로 잡아둔 곳 아래에 위치해 있어야 한다.
    나는 TNS_ADMIN  경로를 /usr/lib/oracle/10.2.0.5/client64/bin 으로 잡았기에, 해당 폴더 아래에 tnsnames.ora 파일을 두었다.
더보기

원하는 TNS 이름(별명) = 

   (DESCRIPTION = 

      (ADDRESS_LIST = 

          (ADDRESS = (PROTOCOL = TCP)(HOST = 아이피주소)(PORT = 1521)

 

예제)

ORA_DB(별명)=
  (DESCRIPTION =
    (ADDRESS = 
      (PROTOCOL = TCP)
      (HOST = 아이피주소)
      (PORT = 1521)
     )

    (CONNECT_DATA =
      (SID = 접속할 SID)
    )
  )

  • oracle client 에서 oracle server 로 sqlplus 명령어로 접속해보자!! (root 계정으로 접속해야 한다!!)
$ sqlplus id/passwd@별명

 

★ 여기서 주의 ★

아래와 같은 에러를 마주쳤다면, 내가 연결하려는 원격 Oracle 버전을 확인하자!

해당 버전에 맞는 클라이언트 버전을 설치해줘야 한다.!!

 

: 내가 연결하려는 오라클 DB 버전이 10 버전대였는데, 서버에 붙으려서 클라이언트 버전은 그보다 높은 12버전이라 발생한 문제였다.

 

 

출처 

https://gomu92.tistory.com/72

https://bangu4.tistory.com/13

 

'리눅스&네트워크' 카테고리의 다른 글

[운영체제] 프로세스와 스레드  (0) 2022.12.11
마운트  (0) 2022.07.20

1. 프로세스

[ 공부하기 전 나 ]

프로세스? 실행중인거!

1) 프로그램과 프로세스의 차이

생명이 있냐, 없냐

프로그램은 보조기억장치(SSD,하드디스크)에 존재하는, 실행되기를 기다리는 코드와 정적인 데이터의 묶음이다.

요것이 메모리에 적재되면 생명이 있는 '프로세스'가 된다.

즉, 실행파일이 메모리에 적재될 때 프로그램은 프로세스가 된다라고 할 수 있다.

 

프로세스란 쉽게 말해 프로그램 실행 그 자체를 의미한다.

 

2) 동시 실행의 착각

컴퓨터에서 프로세스가 동시에 실행된다는 건 당연하지 않은 일.

하나의 CPU(프로세서) 는 한 순간에 하나의 프로세스만 실행할 수 있기 때문이다.

 

그러면 프로세스가 동시에 실행된다라고 말할 수 없는거 아닌가?

맞다.

물 속에서 헤엄치는 오리를 생각해보자.

 © yxelle, 출처 Unsplash

둥둥 떠있는 오리는 사실 물 속에서 엄청나게 발을 휘저으며 헤엄친다.

프로세스가 동시에 실행될 수 있는 건 운영체제가 엄청난 속도로 CPU가 실행할 프로세스를 교체하고 있기 때문이다.

눈 깜박할 사이에 이 교체가 수십번~수천번 일어나기 때문에 사람은 동시에 여러개의 프로세스가 실행되고 있다고 느끼는 것이다.

 

그럼 도대체 운영체제는 어떻게 이렇게 빨리 휘리릭 프로세스 교체를 할 수 있는 것일까?

프로세스의 구성 관리에 대해 알아보면 해답이 나올지도 모르겠다.

 

3) 프로세스 구성

프로세스에 대한 정보는 프로세스 제어블록(PCB,Process Control Block) 이라고 부르는 자료구조에 저장이 된다.

이 PCB 에는 다음과 같은 정보가 담겨있다. ( 운영체제가 프로세스를 관리하기 위해 필요한 정보를 저장함 )

PID
운영체제가 각 프로세스를 식별하기 위해 부여된 식별번호
프로세스 상태
진행중인 상태인지, 대기중인 상태인지, 종료된 상태인지
프로그램 카운터
CPU가 다음 실행할 명령어의 주소를 담고 있는 레지스터이다.
프로그램의 순차적 흐름을 위해 필요하다고 한다...
스케줄링 우선순위
운영체제가 CPU 에서 실행될 여러 개의 프로세스 순서를 정해주는 것을 스케줄링 이라고 한다.
스케줄링에서 우선순위가 높으면 먼저 실행될 수 있는데 이를 스케줄링 우선순위라고 한다.
권한
프로세스가 접근할 수 있는 자원을 결정하는 정보이다.
프로세스마다 어디까지 접근할 수 있는지에 대한 권한이다.
프로세스의 부모와 자식 프로세스
최초로 생성되는 init 프로세스를 제외하고 모든 프로세스는 부모 프로세스를 복제하여 생성되고 이 계층관계는 트리를 형성한다.
그래서 각 프로세스 자식프로세스와 부모프로세스에 대한 정보를 가지고 있다.
프로세스의 데이터와 명령어가 있는 메모리 위치를 가리키는 포인터
프로그램 대한 정보는 프로세스가 메모리에 가지는 자신만의 주소 공간에 저장된다.
프로세스는 프로그램의 실행이기 때문에 실행하기 위해서는 프로그램 주소에 대한 정보가 필요한 것이다.
실행문맥
마지막에 실행된 프로세스의 레지스터 내용을 담고 있다.
운영체제에 의해 교체되었다가 다시 자신의 차례가 되어 실행될 때 중단된 적 없고 마치 연속적으로 실행된 것처럼 보이기 위해 해당 레지스터 정보를 가지고 있다.

 

4) 프로세스 관리

- 프로세스 상태

  • New: 프로그램이 메인 메모리에 할당된다.
  • Ready: 할당된 프로그램이 초기화와 같은 작업을 통해 실행되기 위한 모든 준비를 마친다.
  • Running: CPU가 해당 프로세스를 실행한다.
  • Waiting: 프로세스가 끝나지 않은 시점에서 I/O로 인해 CPU를 사용하지 않고 다른 작업을 한다. (해당 작업이 끝나면 다시 CPU에 의해 실행되기 위해 ready 상태로 돌아가야 한다.)
  • Terminated: 프로세스가 완전히 종료된다.
위 그림은 프로세스 상태 전이도의 모습이다. new에서부터 프로세스가 어떤 작업에 의해 상태가 변하는지 나타낸다.

running에서 ready로 변할 때는 time sharing system(시분할,각 사용자들에게 컴퓨터 자원을 시간적으로 분할)에서 해당 프로세스가 CPU시간을 모두 소진하였을 때 인터럽트에 의해 강제로 ready상태로 변하고, CPU는 다른 프로세스를 실행시킨다.

5) 프로세스의 메모리 영역

  • Code 영역

실행할 프로그램의 코드나 명령어들이 기계어 형태로 저장된 영역이다.

CPU는 코드영역에 저장된 명령어들을 하나씩 처리한다.

 

  • Data 영역

코드에서 선언한 전역 변수와 정적 변수가 저장되는 영역이다.

프로그램이 실행되면서 할당되고 종료되면서 소멸한다.

 

  • Stack 영역

함수 안에서 선언된 지역변수, 매개변수, 리터값등이 저장된다.

함수 호출시 기록되고 종료되면 제거된다.

 

  • Heap 영역

관리가 가능한 데이터 이외의 다른 형태의 데이터를 관리하기 위한 자유공간이다.

 

2. 스레드

스레드는 프로세스 내의 Data, Code, Heap 영역을 공유한다.

운영체제는 프로세스마다 독립된 메모리 영역을 Code/Data/Stack/Heap의 형식으로 할당한다.

각각 독립된 메모리 영역을 할당해주기 때문에 프로세스는 다른 프로세스의 변수나 자료에 접근할 수 없다.

(이미지 출처 : https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html)

 

반면에 스레드는 메모리를 서로 공유할 수 있다.

프로세스가 할당받은 메모리 영역 내에서 Stack 형식으로 할당된 메모리 영역은 별도로 할당받고,

나머지 Code/Data/Heap 형식으로 할당된 메모리 영역 공유한다.

따라서, 각각의 스레드는 별도의 스택을 가지고 있지만 힙 메모리는 서로 읽고 쓸 수 있게 된다.

 

* 단일 스레드와 다중 스레드의 차이

예) 웹서버가 여러 개의 클라이언트 요청을 받는 경우

  • 단일스레드 : 한 번에 하나의 클라이언트만 서비스 할 수 있어 시간이 오래 걸린다.

그러면 매번 요청할 때마다 동일한 요청을 수행하는 별도의 프로세스를 생성하면 되지 않나?

-> But, 프로세스 생성은 많은 시간과 자원이 필요하므로 비효율적이다.

 

새 프로세스 할 일이 기존 프로세스 할 일과 동일하다면 굳이 ?

그. 래. 서.

기존 프로세스 안에다가 여러 개의 스레드를 만드는 것이 더 효율적이다.

 

🟢 멀티스레드의 장점

  1. Context-Switching 할 때 공유하고 있는 메모리만큼 메모리 자원을 아낄 수 있다.
  2. 스레드는 프로세스 내의 Stack 영역을 제외한 모든 메모리를 공유하기 때문에 통신 부담이 적어서 응답 시간이 빠르다.

🔴 멀티스레드의 단점

  1. 스레드 하나가 프로세스 내 자원을 망쳐버린다면 모든 프로세스가 종료될 수 있다.
  2. 자원을 공유하기 때문에 필연적으로 동기화 문제가 발생할 수 밖에 없다. 교착상태가 발생하지 않도록 주의해야 한다.

 

출처

https://ko.wikipedia.org/wiki/%EC%8B%9C%EB%B6%84%ED%95%A0_%EC%8B%9C%EC%8A%A4%ED%85%9C

https://velog.io/@codemcd/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-5.-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B4%80%EB%A6%AC

https://bowbowbow.tistory.com/16

https://velog.io/@aeong98/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9COS-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%99%80-%EC%8A%A4%EB%A0%88%EB%93%9C

 

'리눅스&네트워크' 카테고리의 다른 글

Oracle sqlplus 설치하여 원격 DB 접속하기  (0) 2022.12.21
마운트  (0) 2022.07.20

원하는 결과 창

1번 전체 모달창

2번 하단 버튼 영역

3번 내가 추가하고 싶은 html

-> dialog option  항목 중 open 을 사용!  

  open:function () {
        var html2 = "<div class=\"button_info\">보안메일로 발송하겠습니까?</div>";
       $(".ui-dialog .ui-dialog-buttonpane").prepend(html2);
    }

 

 $("#alertWindow").dialog({
        resizable: false, // 사이즈 조절 가능 여부
        modal: true, // 배경색 어둡게:true, 밝게:false
        autoOpen:false, 
        minWidth:400, 
        height:"auto",
        zIndex:9000, 
        title : title, // 다이얼로그 제목
        buttons: [
            {
                text : message_common.CM0015,
                click:function(){
                    $( this ).dialog( "destroy" );
                    if( callback ){
                        // 확인 눌렀을 때 실행할 콜백 함수
                        callback();
                    }
                }
            },
            {
                text : message_common.CM0028,
                click:function(){
                    $( this ).dialog( "destroy" );
                   if( callback ){
                        // 취소 눌렀을 때 실행할 콜백 함수
                        callback();
                    }
                }
            }
        ],
      open:function () {
       	var html2 = "<div class=\"button_info\">보안메일로 발송하겠습니까?</div>";
      	$(".ui-dialog .ui-dialog-buttonpane").prepend(html2);
  	 }
 });

 

여기서 append 대신 prepend 를 사용한 이유는 확인/취소 버튼 앞에 메시지를 놓고 싶었기 때문이다.

<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
     <!-- ui-dialog-buttonpane 의 첫번째 자식 -->
     <div class="button_info">보안메일로 발송하겠습니까?</div>
     <!-- ui-dialog-buttonpane 의 두번째 자식 -->
     <div class="ui-dialog-buttonset">
		<button type="button" class="ui-button ui-corner-all ui-widget">확인</button>
		<button type="button" class="ui-button ui-corner-all ui-widget">취소</button>
	</div>
</div>

+) .append() 선택된 요소의 마지막에 새로운 요소나 콘텐츠를 추가한다.

    .prepend() 선택된 요소의 첫번째에 새로운 요소나 콘텐츠를 추가한다.

    .appendTo() 선택된 요소를 해당 요소의 마지막에 추가한다.

   . prependTo() 선택된 요소를 해당 요소의 첫번째에 추가한다.

 

ResultMap이란

myBatis에서 제공하는 자동 매핑으로 해결이 어려운 경우를 위해 구조를 설계할 수 있도록 만들어진 도구이다.

ResultMap이 필요한 경우

ResultMap은 다음과 같은 데이터 구조를 불러올 때 적합하다.

계층형 데이터 구조

객관식 시험과 관련된 정보들을 데이터베이스에 추가한다면 다음과 같은 구조를 가지게 된다.

- 1번 시험문제
    - 1번 보기
    - 2번 보기
    - 3번 보기
    - 4번 보기
- 2번 시험문제
    - 1번 보기
    - 2번 보기
    - 3번 보기
    - 4번 보기

시험이라는 상위 entity와 보기라는 하위 entity로 나누어 생각해본다면 다음과 같이 이해할 수 있다.

물리 테이블은 다음과 같이 구성될 것이다.

문제 테이블(quiz)

문제번호(no)문제내용(text)

1 1번 문제
2 2번 문제

보기 테이블(choice)

보기번호(no)보기내용(text)문제번호(quiz)

1 1번 보기 1
2 2번 보기 1
3 3번 보기 1
4 4번 보기 1
5 1번 보기 2
6 2번 보기 2
7 3번 보기 2
8 4번 보기 2

위의 테이블 구조를 만약 join으로 불러온다면 어떻게 될까?

select 
    문제번호, 문제내용, 보기번호, 보기내용 
from
    문제 inner join 보기 on 문제.문제번호 = 보기.문제번호;

다음과 같은 결과가 나올 것이다.
문제번호문제내용보기번호보기내용

1 1번 문제 1 1번 보기
1 1번 문제 2 2번 보기
1 1번 문제 3 3번 보기
1 1번 문제 4 4번 보기
2 2번 문제 5 1번 보기
2 2번 문제 6 2번 보기
2 2번 문제 7 3번 보기
2 2번 문제 8 4번 보기

위의 결과를 가지고 다음 화면을 출력할 수 있는지 따져봐야 한다.
join을 이용해서 위의 화면을 출력하는 것은 가능은 하지만 어렵다.
조건들이 많이 등장할 것이며, 코드는 지저분하게 표시된다.
다음 화면은 만들 수 있겠지만 우리가 원하는 화면은 아니다.
따라서 데이터를 화면과 같은 구조로 불러오고 싶은 경우 join 대신 ResultMap을 이용하면 좋다.

ResultMap을 위한 클래스 구성

- 1번 시험문제
    - 1번 보기
    - 2번 보기
    - 3번 보기
    - 4번 보기
- 2번 시험문제
    - 1번 보기
    - 2번 보기
    - 3번 보기
    - 4번 보기

위와 같은 구조에서 시험 문제의 입장으로 바라본다면 다음과 같이 구성되어 있다고 생각할 수 있다.

- 1번 시험문제
    - List<보기>
- 2번 시험문제
    - List<보기>

따라서 시험 문제와 보기의 관점에서 각각 저장할 클래스를 만들어 myBatis가 구조를 이해할 수 있도록 구성한다.

class Choice : 보기 클래스

public class Choice{
    private int no;
    private String text;
}

보기 클래스는 Choice라는 이름으로 작성하였으며, 보기 번호와 내용을 가질 수 있도록 구성하였다.

class Quiz : 문제 클래스

public class Quiz{
    private int no;
    private String text;
    private List<Choice> choices;
}

문제 클래스는 Quiz라는 이름으로 작성하였으며, 퀴즈 번호와 내용, 그리고 보기 목록(List<Choice>)을 가질 수 있도록 구성하였다.
이 Quiz라는 형태를 myBatis에 ResultMap으로 등록하여 구문과 같이 연결해두면 계층형 데이터를 쉽게 불러올 수 있다.

ResultMap 설정

ResultMap은 mapper에 설정한다.
quiz-mapper.xml이라는 파일을 만들고 내부에 다음과 같이 작성한다.

<!-- 기본 설정 생략 -->
<mapper namespace="quiz">

    <resultMap type="com.hakademy.vo.Quiz" id="quiz">
		<result column="no" property="no"/>
		<result column="text" property="text"/>
		<collection column="no" property="choices" javaType="java.util.List" ofType="com.hakademy.vo.Choice" select="getChoiceList">
        </collection>
	</resultMap>
	
    <select id="getQuizList" resultMap="quiz">
        select * from quiz order by no asc
    </select>
    
    <select id="getChoiceList" parameterType="int" resultType="com.hakademy.vo.Choice">
        select * from choice where quiz = #{quiz}
    </select>

</mapper>

해당 구문을 호출할 때에는 다음과 같이 작성한다.

List<Quiz> list = sqlSession.selectList("quiz.getQuizList");

ResultMap 설정 상세 설명

메인 select 구문

<select id="getQuizList" resultMap="quiz">
    select * from quiz order by no asc
</select>

메인 select 구문이며, 실행 결과 형태는 미리 정의된 quiz라는 id를 가진 resultMap이 할당된다.
이 구문을 실행한 결과를 myBatis가 자동으로 quiz에 맞게 처리하게 된다.

ResultMap 설정

<resultMap type="com.hakademy.vo.Quiz" id="quiz">
    <result column="no" property="no"/>
    <result column="text" property="text"/>
    <collection column="no" 
                                property="choices" 
                                javaType="java.util.List" 
                                ofType="com.hakademy.vo.Choice" 
                                select="getChoiceList"></collection>
</resultMap>

ResultMap을 만들기 위해서는 다음 설정을 수행해야 한다.

  • type : 매핑될 클래스명을 작성한다.
  • id : 외부에서 지정할 이름을 작성한다.

내부에는 <result> 항목을 배치하여 데이터에 대한 실제 매핑 관계를 설정한다.
Result 설정 항목은 다음과 같다.

  • column : 불러올 데이터베이스 항목명을 작성한다.
  • property : 불러온 항목을 저장할 클래스 내의 변수명을 작성한다.

만약 데이터가 여러 개라면 컬렉션을 지정할 수 있는데, 이 때는 <collection> 항목을 배치하여 매핑 관계를 설정한다.
Collection 설정 항목은 다음과 같다.

  • column : 불러올 데이터베이스 항목명을 작성한다. 여기서는 하위 SQL을 실행하기 위한 항목을 작성한다.(일반적으로 PK로 작성)
  • property : 불러온 항목을 저장할 클래스 내의 변수명을 작성한다.
  • javaType : Collection의 형태를 작성한다. 내장된 별칭이 있으므로 java.util.List 또는 list 라고 작성해도 무방하다.
  • ofType : Collection의 내용물의 형태를 작성한다. Quiz 클래스에 List<Choice>라고 되어 있으므로 Choice의 경로를 작성한다.
  • select : 이 데이터는 quiz에 존재하지 않기 때문에 조회하는 구문이 따로 필요하므로 해당하는 조회 구문의 id를 작성한다.

서브 select 구문

서브 select 구문은 ResultMap에 의해서 자동으로 호출된다.

<resultMap type="com.hakademy.vo.Quiz" id="quiz">
    <!-- 생략 -->
    <collection column="no" property="choices" select="getChoiceList"></collection>
</resultMap>

<select id="getChoiceList" parameterType="int" resultType="com.hakademy.vo.Choice">
    select * from choice where quiz = #{quiz}
</select>

필요한 부분을 제외한 나머지는 생략한 구문이다.
ResultMap을 만들면서 <collection>이라는 구문을 만나면 myBatis에서는 다음의 작업을 수행한다.

  1. Collection의 column 에서 지정한 항목을 추출한다.
  2. Collection의 select 에서 지정한 구문을 호출하며 1번에서 추출한 값을 전달한다.
  3. 서브 select 구문이 실행되어 List<Choice> 형태의 데이터가 반환된다.
  4. Collection의 property 에 지정된 변수인 choices에 3번의 결과값을 설정한다.

결론

ResultMap을 이용하면 계층화된 데이터를 실제 구조와 동일하게 효과적으로 불러올 수 있다.
물론 for 구문을 이용하여 수동으로 수행해도 되지만 ResultMap을 이용하면 조금 더 구조적으로 접근할 수 있다.
단, select 구문이 여러 번(N+1) 실행되는 만큼 불러오는 데이터가 많은 경우는 권장하지 않는다.
참고 : http://www.sysout.co.kr/home/webbook/page/read/643;jsessionid=54B3D5320E73A41FC4088D5E3D322141

'웹개발 > Spring' 카테고리의 다른 글

[@InitBinder 어노테이션-WebDataBinder]  (0) 2022.08.05
[Mybatis] Mybatis 기술의 탄생! (feat.ORM)  (1) 2022.07.30

+ Recent posts