알고스팟 문제 링크: https://algospot.com/judge/problem/read/GALLERY

 

algospot.com :: GALLERY

감시 카메라 설치 문제 정보 문제 전세계의 유명한 인물화들을 모아 두는 미술관에 괴도 콩의 도전장이 날아들었습니다. 2022년 2월 2일을 기념하여, 미술관에 전시된 인물화 중 하나의 얼굴을 모

algospot.com

그래프에서 인접한 노드중에 적어도 하나의 노드에 감시카메라가 설치되어있지 않으면 해당 노드에 감시카메라를 설치 해야한다.

이때 필요한 최소한의 감시카메라의 개수를 구하는 문제이다.

 

문제 조건에서 "미술관은 한 번 관람한 갤러리를 다시 가기 위해서는 이전에 지나왔던 복도를 반드시 한 번 지나야 하는 구조로 설계되어 있으며"가 있으므로 해당 그래프는 트리형태임을 알 수 있다.

따라서 자손 노드들의 상태를 보고 하나라도 UNWATCHED상태라면 해당 노드에 감시카메라를 설치하는 알고리즘을 사용할 수 있다.

또한 "모든 갤러리가 서로 연결되어 있지 않을 수도 있습니다."라는 조건이 있으므로 모든 노드가 방문되었는지 확인하는 절차도 필요하다.

 

코드 원본: https://github.com/sbl133/JongmanBook/blob/main/28.%20DFS/GALLERY.cpp

 

GitHub - sbl133/JongmanBook

Contribute to sbl133/JongmanBook development by creating an account on GitHub.

github.com

댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다!

reference: 프로그래밍 대회에서 배우는 알고리즘 문제해결전략2

알고스팟 문제 링크: https://algospot.com/judge/problem/read/NH

 

algospot.com :: NH

보안종결자 문제 정보 문제 음지에서 살아가는 보안 초전문가 슈퍼코더 K 는 이번에 새로운 은행의 전산망을 해킹하려고 합니다. 그에게는 원래 은행의 전산망을 해킹하는 것은 손바닥 뒤집는

algospot.com

입력으로 주어지는 특정한 문자열 패턴들이 나타나지 않게 알파벳 소문자로만 이뤄진 n자리 암호를 만들 수 있는 모든 경우의 수를 10007로 나눈 나머지를 구하는 문제이다.

bruteforce로 풀게되면 26^n의 시간복잡도를 가지게 되므로 제한시간 안에 문제를 푸는것이 불가하다.

동적계획법을 이용하면 문제를 풀 수 있는데 그 방법이 매우 어렵고 까다롭다. (종만북의 연습문제중 가장 어려웠던것 같다.) 

먼저 입력으로 주어진 문자열 패턴들을 트라이로 만들고 각 노드마다 상태를 부여한다.

앞으로 만들어야하는 글자의 갯수 length, 현재 트라이의 상태를 state라 했을때 cache[length][state]형태의 캐시를 이용한다.

하지만 이렇게 트라이를 이용한 동적계획법을 구현하기 위해서는

먼저 트라이의 각 노드들이 실패연결과 출력문자열을 가지고 있어야하고, 각 노드마다 상태 번호가 부여되야 한다.

 

코드 원본: https://github.com/sbl133/JongmanBook/blob/main/26.%20Trie/nh.cpp

 

GitHub - sbl133/JongmanBook

Contribute to sbl133/JongmanBook development by creating an account on GitHub.

github.com

댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다!

reference: 프로그래밍 대회에서 배우는 알고리즘 문제해결전략2

BOJ 문제 링크 : https://www.acmicpc.net/problem/1759

 

1759번: 암호 만들기

첫째 줄에 두 정수 L, C가 주어진다. (3 ≤ L ≤ C ≤ 15) 다음 줄에는 C개의 문자들이 공백으로 구분되어 주어진다. 주어지는 문자들은 알파벳 소문자이며, 중복되는 것은 없다.

www.acmicpc.net

L개의 알파벳이 주어졌을때 L개중 C개를 다음 조건에 맞게 골라 나열하여 출력하는 문제이다.

조건은 C개의 알파벳을 나열한 문자열은 알파벳순으로 정렬되 있어야 하고 모음이 한 개 이상 자음이 두 개 이상이여야 한다.

 

L개의 문자들을 알파벳 순으로 정렬한후 순서대로 조합하는 방식으로 문제를 해결 할 수 있다.

이때 완성된 암호의 모음의 갯수와 자음의 갯수를 확인하는 유효성 검사가 출력전에 선행되야 한다.

 

코드 원본 : https://github.com/sbl133/BOJ/blob/main/%231759.cpp

 

GitHub - sbl133/BOJ

Contribute to sbl133/BOJ development by creating an account on GitHub.

github.com

댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다!

BOJ 문제 링크 : https://www.acmicpc.net/problem/5430

 

5430번: AC

각 테스트 케이스에 대해서, 입력으로 주어진 정수 배열에 함수를 수행한 결과를 출력한다. 만약, 에러가 발생한 경우에는 error를 출력한다.

www.acmicpc.net

 

뒤집기 함수인 R과 배열의 첫번째 요소를 버리는 함수 D를 조합해서 문자열의 형태로 입력에 주어지고 배열이 주어졌을때 함수의 조합들을 실행한후의 배열을 형태를 출력하는 문제이다.

 

문제에 주어진 조건에 맞게 입력과 출력을 해야하기 때문에 입출력 처리과정이 별도로 필요했다.

또한 R을 수행할때 배열을 직접 뒤질을 필요없이 deque를 이용하여 R이 짝수번 출현한 후의 D들은 deque.pop_front를,

R이 홀수번 출현한 후의 D들은 deque.pop_back으로 D를 수행하면 됐다.

출력시에 deque.size함수를 이용하였는데 deque의 사이즈가 0일시에는 deque.size()-1이 정상적으로 수행되지 않아

별도의 예외처리가 필요했다.(사실 이부분에서 몇시간을 해맸다....)

 

 

코드 원본 : https://github.com/sbl133/BOJ/blob/main/%235430.cpp

 

GitHub - sbl133/BOJ

Contribute to sbl133/BOJ development by creating an account on GitHub.

github.com

댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다!

최근 진행하는 프로젝트가 어느정도 개발이 완료되서 AWS의 EC2를 이용해 배포를 진행하였다.

하지만 서버를 배포한 후에 프론트와 통신을 시작한 후에는 develop 브랜치에서 수행한 커밋들을 거의 바로바로 release브랜치가 pull 해야하는 상황이 되었고

release 브랜치가 바뀔 때마다 서버를 수동으로 재배포 해야했다.

EC2에 접속해서 작동중인 서버를 멈추고 다시 재가동 시키는 일련의 반복작업들이 귀찮아 지기 시작하였고 Github Action를 이용해서 이러한 반복작업들을

자동으로 수행하게끔 진행하였다.

물론 젠킨스라는 툴도 있지만 프로젝트 규모에 비해 오버스펙인 감이 있었고, release브랜치에 일어나는 event중심으로한 작업이 필요했으며,

설정또한 비교적 쉬운  Github Actions이 적당해 보였다.

 

진행하고자하는 프로젝트의 최종 배포 구조는 다음과 같다.

그림을 보면서 대략적인 배포흐름을 얘기하자면 먼저 Github의 release브랜치에서 push 또는 merge 이벤트가 발생하면,

github Actions가 작동하면서 Github에 있는 프로젝트를 build하여 jar파일을 생성한다.

이때 Github Actions에서 생성한 jar파일을 EC2로 직접 보내는 방법이 없기 때문에 AWS 저장소인 S3를 이용한다.

S3에 저장한 jar파일을 EC2로 가져오기 위해서는 CodeDeploy와 CodeDeployAgent가 필요하다.

Github Actions에서 AWS의 CodeDeploy에 요청을 보내면 EC2에 설치한 CodeDeployAgent를 통해 S3에 있는 jar파일을 EC2로 가져온다.

 

먼저 Github Actions 설정방법이다.

Github Actions를 수행할 브랜치를 default 브랜치로 설정한 후 action 탭에서 workflow를 생성하면 자동으로 .github/workflows 경로에 main.yml파일이 생성된다.

해당 main.yml파일의 초기 설정은 다음과 같이 하였다.

name: CI

on:
  push:
    branches: [ release ]
  pull_request:
    branches: [ release ]
  workflow_dispatch:
  
env:
  S3_BUCKET_NAME: fis-police-back-githubaction
  PROJECT_NAME: github-action

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Build with Gradle
        run: ./gradlew bootJar
        working-directory: ./fis_police_server
        shell: bash
  • name : workflow의 이름을 나타낸다.
  • on : 어떤 이벤트가 일어났을때 Github Actions가 작동할지 정하는 부분이다. 해당 프로젝트에서는 release 브랜치에 push 또는 pull_request가 발생하거나, 수동으로 작동시킬때(workflow_dispatch) 실행시키라고 설정하였다.
  • env: 현재는 쓰이지 않지만 나중에 S3의 버킷 이름과 프로젝트 이름을 사용할때 환경변수를 정의하여 사용하기 위해 설정하였다.
  • jobs, steps : 작업단위로 jobs안에 여러개의 steps를 둘수 있다.
  • runs-on : 해당 workflow를 어떤 환경에서 실행할 것인지 지정한다.
  • checkout : Github의 workflow를 실행할때 준비 동작으로 Github workspace에서 내 저장소로 이동한다고 생각하면 된다.

다음 작업들은 JDK를 setup하고 gradlew의 실행권한을 준 뒤 bootjar하는 과정이다.

이때 bootjar대신 build를 사용할 경우 test과정을 거치기 때문에 test가 실패할 경우 build가 실패할 수 있다.

working-directory는 작업을 실행할 경로를 지정해 주는 것인데,

깃허브에 저장된 프로젝트 구조상 gradlew이 있는 directory를 따로 지정해 주어야 했기 때문에 설정을 해주었다.

일단 main.yml파일을 이정도로 작성하고 github에 push 하면 github Actions가 작동하는것을 actions 탭에서 확인할 수 있다.

 

다음으로 S3에 jar파일을 업로드 하는 과정이다.

먼저 AWS console에서 S3 버킷을 생성한다. 이때 IAM를 사용하여 AWS의 권한을 따로 설정할거기 때문에 '모든 퍼블릭 액세스 차단'를 체크한다.

Github Actions가 현재 생성한 S3에 접근하기 위해 IAM사용자를 추가한다.

이때 중요한것은 Github Actions가 AWS에 접근하여 S3 버킷에 파일을 업로드 할때 액세스 키 ID와 비밀 액세스 키를 통해

접근권한을 받아올 것이기 때문에 프로그래밍 방식 액세스를 체크해야 한다.

부여할 권한은 AmazonS3FullAccess 와 AWSCodeDeployFullAccess이다.

생성을 완료한 후에 보여지는 액세스 키 ID와 비밀 액세스 키는 잘 보관하였다가 나중에 리포지토리 setting에서 secret를 통해 등록해 놓을 것이다.

S3버킷 생성과 IAM 사용자 추가 설정을 마쳤으면 GithubActions를 제어하기 위한 workflows의 main.yml파일을 다음과 같이 수정한다.

name: CI

on:
  push:
    branches: [ release ]
  pull_request:
    branches: [ release ]
  workflow_dispatch:
  
env:
  S3_BUCKET_NAME: fis-police-back-githubaction
  PROJECT_NAME: github-action

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Build with Gradle
        run: ./gradlew bootJar
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Make zip file
        run: zip -r ./$GITHUB_SHA.zip .
        shell: bash
        
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 
          aws-region: ${{ secrets.AWS_REGION }}
          
      - name: Upload to s3
        run: aws s3 cp --region us-east-1 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

Make zip file에서는 빌드한 파일을 압축하는 작업을 실행한다. S3 버킷에 파일을 올리기 위해서는 해당 작업이 선행되어야 한다.

이때 $GITHUB_SHA는 커밋의 해쉬값으로 해당 작업에서 workflow가 진행하는 커밋의 해쉬값을 파일명으로 하는 압축파일을 만든다는것을 의미한다.

다음으로 Configure AWS credentials는 IAM에서 추가한 사용자의 액세스 키 정보를 통해 S3에 접근권한을 받아오기 위한 작업이다.

이때 ${{SECRETS.~}}는 github의 해당 리포지토리의 setting에서 Secrets설정한 값을 읽어 온다.

따라서 해당 작업을 위해서는 github에 IAM 사용자 추가에서 나온 액세스 키 정보들을 등록해야한다.

다음 설정들을 모두 끝낸후 Github Actions를 돌리면 S3 버킷에 zip파일이 담긴것을 AWS console를 통해 확인할 수 있다.

 

다음으로 CodeDeploy 설정을 해야한다.

CodeDeploy를 작동시키기 위해서는 먼저 Github Actions이 참조하는 workflow의 main.yml파일에 CodeDeploy를 통해 배포하기 위한 명령어를 추가하고,

프로젝트 최상단에 AppSpec.yml파일을 추가한다.

AppSpec.yml은 각 배포단계에서 어떤 스크립트를 실행시킬지 적어놓은 명세서라고 할 수 있다.

GithubActions는 main.yml에 추가한 명령어를 통해 AWS의 CodeDeploy에 배포 요청을 보내고

CodeDeploy는 EC2에 설치되어있는 CodeDeployAgent에게 배포 명령을 내린다. 

 배포명령을 받은 Agent들은 깃허브에 저장되있는 프로젝트 전체를 서버에 내려받고 appspec.yml파일을 읽어 알맞은 스크립트를 실행시켜 배포를 진행한다.

CodeDeployAgent는 배포 성공 여부를 CodeDeploy에 알려주기 때문에 AWS console의 CodeDeploy에서 배포 성공여부를 확인할 수 있다.

 

이제 본격적으로 CodeDeploy설정을 해보겠다.

먼저 EC2에 CodeDeployAgent를 설치하여한다. EC2에 자바를 설치되어있다고 가정하고 다음 명령어들을 치면 된다.

# 패키지 매니저 업데이트, ruby설치
sudo apt-get update
sudo apt-get install ruby
sudo apt-get install wget

# 만약 서울리전에 있는 CodeDeploy 리소스 키트 파일을 다운받고 싶다면
# wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
cd /home/ec2-user
wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install

# 해당 파일에 실행 권한 부여
chmod +x ./install

# 설치 진행 및 Agnet 상태확인
sudo ./install auto
sudo service codedeploy-agent status

공식문서에서는 CodeDeployAgent를 설치하는 과정에서 apt-get대신 yum를 사용하는데 이것은 Linux가 어떤 계열인지에 따라 달라진다.

현재 사용중인 Linux가 레드헷 계열이라면 yum을 사용하고 우분투, 데비안계열이라면 apt-get를 사용해야한다.

나는 우분투 계열의 Linux를 사용중이기 때문에 yum대신 apt-get를 사용하였다.

 

이제는 EC2가 S3와 CodeDeploy에 대한 접근권한을 얻기 위해 IAM역할을 부여해야 한다.

위에서 GithubActions가 AWS의 S3에 접근하기위해 IAM사용자를 추가했었다. 

IAM에대해 부연설명 하자면, GithubActions같이 외부에서 AWS의 접근 권한을 얻기 위해서는 IAM사용자를 추가해야하고,

EC2처럼 AWS에서 제공하는 서비스가 AWS내부 서비스에 접근하기 위해서는 IAM역할을 추가해줘야 한다.

역할 만들기에 들어가 사용 사례에서 EC2를 선택하고, AmazonS3FullAccess와 AWSCodeDeployFullAccess 정책을 추가한다.

역할을 성공적으로 생성했다면 EC2대시보드에 들어가 해당 EC2에 생성한 IAM역할을 부여한다.

 

다음으로 CodeDeploy에도 IAM역할을 생성해서 부여해야 한다.

IAM 역할만들기에서 CodeDeploy를 선택하고 사용사례에서 CodeDeploy를 선택하여 넘어간다.

 

이제 CodeDeploy 애플리케이션을 생성해야한다. 

AWS console에서 CodeDeploy-애플리케이션-애플리케이션생성 에서 EC2를 선택하고 진행한다.

배포그룹 생성에서는 서비스역할에는 방금전 생성한 CodeDeploy용 IAM역할을 입력하고 배포유형은 현재 위치를 선택한다.

환경구성에서는 Amazon EC2 인스턴스를 선택하고 EC2 인스턴스의 태그를 알맞게 입력한다.

배포설정에서는 EC2가 한대이기 때문에 CodeDeployDefault.AllAtOnce를 선택한다.

로드밸런서는 없기 때문에 로드 밸런싱 활성화 체크는 해체하고 배포그룹을 생성한다.

프로젝트 최상단에 appspec.yml를 놓아야하는데 형식은 아래 코드와 같다.

# appspec.yml

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/github-action/
    overwrite: yes
    
permission:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  ApplicationStart:
    - location: fis_police_server/scripts/deploy.sh
      timeout: 180
      runs: ec2-user

files.destination은 S3에 있는 zip파일을 EC2의 어느경로로 가져올지를 나타낸다.

permission은 인스턴스에 복사된 파일에 특수권한(있는 경우)이 어떻게 적용되어야 하는지를 지정하는 것이다.

ApplicationStart는 ApplicationStart단계에서 해당 쉘 스크립트를 실행하겠다는 내용이다.

이 경우 deploy.sh의 내용은 다음과 같다.

#!/bin/bash
BUILD_JAR=$(ls /home/ec2-user/github-action/fis_police_server/build/libs/*.jar)
JAR_NAME=$(basename $BUILD_JAR)
echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/github-action/deploy.log

echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/github-action/deploy.log
CURRENT_PID=$(pgrep -f $JAR_NAME)

if [ -z $CURRENT_PID ]; then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/github-action/deploy.log
else
  echo "> kill -15 $CURRENT_PID" >> /home/ec2-user/github-action/deploy.log
  kill -15 $CURRENT_PID
  sleep 5
fi

echo "> build 파일 복사" >> /home/ec2-user/github-action/deploy.log
DEPLOY_PATH=/home/ec2-user/github-action/
cp $BUILD_JAR $DEPLOY_PATH

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo "> DEPLOY_JAR 배포" >> /home/ec2-user/github-action/deploy.log
nohup java -jar $DEPLOY_JAR >> /home/ec2-user/deploy.log 2>/home/ec2-user/github-action/deploy_err.log &

이때 주의할점은 현재 가동중인 애플리케이션을 kill한 후에 build한 jar파일을 $DEPLOY_PATH로 복사해야 한다는것이다.

만약 jar파일을 먼저 복사한 후 가동중인 애플리케이션을 kill하게 되면 다음과 같은 에러가 간헐적으로 터질 수 있다.

Failed to stop bean 'webServerGracefulShutdown'

해당 에러는 jar가 실행중일때 해당 jar파일을 바꾸게 될경우 jar내부 클래스 참조가 엉켜서 생기는 에러이다.

따라서 jar파일을 실행할 경로로 복사해오기 전에 꼭 현재 실행중인 jar를 먼저 kill한 후 복사해오자.

참조 : https://stackoverflow.com/questions/65090165/graceful-shutdown-fails

 

CodeDeploy가 작동하도록 workflow의 main.yml에 명령어를 추가한 후의 main.yml의 내용은 다음과 같다.

name: CI

on:
  push:
    branches: [ release ]
  pull_request:
    branches: [ release ]
  workflow_dispatch:
  
env:
  S3_BUCKET_NAME: fis-police-back-githubaction
  PROJECT_NAME: github-action

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Build with Gradle
        run: ./gradlew bootJar
        working-directory: ./fis_police_server
        shell: bash
        
      - name: Make zip file
        run: zip -r ./$GITHUB_SHA.zip .
        shell: bash
        
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 
          aws-region: ${{ secrets.AWS_REGION }}
          
      - name: Upload to s3
        run: aws s3 cp --region us-east-1 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

      - name: Code Deploy
        run: aws deploy create-deployment --application-name fis-police-back-githubaction --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name release --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$GITHUB_SHA.zip

위의 코드가 main.yml의 최종형태이다.

  • application-name은 CodeDeploy 애플리케이션의 이름을 지정한다.
  • deployment-config-name은 배포그룹 설정에서 선택했던 배포 방식을 지정한다.
  • deployment-group-name은 배포 그룹의 이름이다.
  • s3-location은 jar를 S3에서 가지고 오기 위해 차례로 bucket이름, 파일 타입, 파일 경로를 입력한다.

마지막으로 CodeDeploy Agent를 시작한 후에 IAM Role를 EC2에 부여했기 때문에 Agent가 IAM Role을 가지고 있지 않아 배포가 실패할 것이다.

따라서 다음 명령어로 CodeDeployAgent를 재가동 시켜줘야 한다.

sudo service codedeploy-agent restart

위에 내용들을 정확하게 수행하고 필요한 yml파일들을 정상적으로 작성해 release브랜치에 push했다면,

이제 release브랜치에 push또는 pull_request이벤트가 일어났을때 해당 내용으로 자동 배포가 이뤄지게 된다.

 

 

참고: https://wbluke.tistory.com/40?category=418851

JPA에서는 지연로딩 등에 쓰이는 프록시라는 개념이 있다.

프록시를 직접적으로 사용하는 방법은 em.getReference()를 이용하면된다.(EntityManager em)

em.find()의 경우 데이터베이스를 통해서 실제 엔티티 객체를 조회하지만,

em.getReference()를 사용하면 데이터가 실제로 필요할때까지 데이터베이스 조회를 미루는 프록시 객체를 가져온다.

프록시는 실제 엔티티 클래스를 상속 받아서 만들어졌고 따라서 실제 클래스와 겉 모양이 같다.

이론상 사용자는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

프록시와 초기화 과정을 그림과 같이 설명하면 다음과 같다.

프록시 객체가 처음 조회될때는 Entity target이 비어있는 상태였다가 실제로 데이터가 필요한 시점에 영속성 컨텍스트에 초기화 요청을 한다.

영속성 컨텍스트에서는 DB에 조회를 해서 데이터를 가져와 실제 엔티티를 생성한다.

최종적으로 프록시 객체는 영속성 컨텍스트를 통해 생성한 엔티티 객체를 target을 통해 참조한다.

 

프록시는 다음과 같은 특징들이 있다.

  • 프록시 객체는 처음 데이터가 실질적으로 쓰일때 처음 한번만 초기화 된다.
  • 프록시 객체의 초기화는 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 프록시 객체를 통해서 실제 엔티티에 접근 가능하게 되는것이다.
  • 프록시 객체는 원본 엔티티를 상속받는다. 따라서 프록시와 엔티티의 타입을 비교할때는 == 대신 instance of를 사용해야한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티가 반환된다. (JPA는 같은 영속성 컨텍스트 안에서 동일한 pk를 갖는 객체를 참조하는 경우 자바의 컬렉션처럼 ==이 true인것을 보장하는 기본 메커니즘을 제공한다.)
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.(LazyInitializationException 터짐)

 

 

JPA에서는 즉시로딩과 프록시를 이용한 지연로딩이 있다.

즉시 로딩으로 객체를 조회할 때는 연관관계 매핑이 되어있는 객체의 정보까지 모두 가져오기 위해 연관된 객체 테이블까지 join한 쿼리를 db에 요청한다. 

즉시로딩의 경우 연관관계를 맺은 객체를 사용하지 않을거라면 join을 한것이 성능측면에서 낭비가 될 수 있다.

따라서 어떤 객체를 조회할때 해당 객체와 연관된 또 다른 객체의 사용이 많지 않다면 지연로딩을 사용한다.

지연로딩이란 객체를 조회할때 해당 객체와 연관된 또 다른 객체의 정보를 즉시 가져오지 않고(join쿼리를 사용하지 않고) 해당 객체를 프록시로 대체한다.

즉, 조회한 객체(Member)와 연관되어 있는 객체(Member.team)의 실제 사용(Member.team.getName())이 일어날때 쿼리가 또 한번 나가는 형식으로 동작한다.

지연로딩을 적용한 코드는 다음과 같다.

@Entity
public class Member {
	@Id
	@GeneratedValue
	private Long id;
    
	@Column(name = "USERNAME")
	private String name;
    
	@ManyToOne(fetch = FetchType.LAZY) //**
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	..
}

그렇다면 연관된 객체의 사용이 자주 일어날때는 즉시로딩, 연관된 객체의 사용이 자주 일어나지 않을때는 지연로딩을 적용하면 될것 같다.

하지만 즉시로딩에는 다음과 같은 문제점들이 있어 실무에서는 거의 항상 지연로딩을 사용하는것이 바람직하다.

즉시로딩을 적용하면 자동으로 join이 일어나기 때문에 예상하지 못한 쿼리가 발생할 수 있다.

또한 JPQL로 해당 테이블에 있는 데이터만 가져올때 즉시로딩의 경우 연관된 객체들의 데이터를 가져오는 쿼리를 다시 데이터베이스에 요청한다.

즉, 즉시로딩은 JPQL에서 N+1문제를 일으킨다.

@ManyToOne, @OneToOne은 기본이 즉시로딩으로 설정 되어있기 때문에 fetch옵션값을 FetchType.Lazy로 설정해줘야 한다.

@OneToMany, @ManyToMany의 경우 기본값이 지연로딩으로 설정되어 있어 따로 설정할 필요는 없다. 

 

 

참고: 자바 ORM 표준 JPA 프로그래밍 (https://www.inflearn.com/course/ORM-JPA-Basic#)

관계형 데이터베이스는 상속관계가 따로있지는 않지만 데이터베이스의 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.

상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 서브타입관계를 매핑하는것을 말한다.

상속관계 매핑에는 3가지 방법이 존재한다.

  • 각각 테이블로 변환 -> 조인 전략
  • 통합 테이블로 변환 -> 단일 테이블 전략
  • 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략

먼저 조인 전략을 다이어그램과 코드로 표현하면 다음과 같다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn // 서브타입을 나타내는 속성값이 테이블에 추가됨
public class Item {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    private int price;
}

@Entity
public class Album extends Item {
    private String artist;
}
@Entity
public class Movie extends Item {
    private String director;
    private String actor;
}
@Entity
public class Book extends Item {
    private String author;
    private int isbn;
}

조인 전략은 테이블이 정규화 되있고, 외래키 참조 무결성 제약조건이 활용가능하며 저장공간이 효율적이다.

하지만 조회시 조인을 많이 사용하여 성능이 저하되고, 조회 쿼리가 복잡하며, 데이터 저장시 insert 쿼리가 2번 호출된다.

 

 

다음으로 단일 테이블 전략이 있다.

단일 테이블 전략은 자식 엔티티의 모든 속성들을 부모 테이블에 몰아넣는 방식으로 상속관계를 구현한다.

단일 테이블 전략을 표현하는 다이어그램과 코드는 다음과 같다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn // 없어도 DTYPE속성 생김
public class Item {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    private int price;
}

@Entity
public class Album extends Item {
    private String artist;
}
@Entity
public class Movie extends Item {
    private String director;
    private String actor;
}
@Entity
public class Book extends Item {
    private String author;
    private int isbn;
}

단일 테이블 전략은 조인이 필요 없으므로 일반적으로 조회 성능이 빠르고 조회 쿼리 또한 단순하다.

반면 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 하며, 단일 테이블이 커져 조회성능이 오히려 느려질 수 있다.

 

 

마지막으로 구현 클래스마다 테이블 전략이 있는데, 여러 자식 테이블을 함께 조회할 때 성능이 느리고 자식 테이블을 통합해서 쿼리하기 어렵다.

이러한 단점들로 인해 해당 전략은 데이터베이스 설계자와 ORM 전문가 양쪽다 사용하기 꺼려하므로 대부분의 경우 사용하지 않는것이 바람직하다.

 

 

 

다음으로 설명할것은 @MappedSuperclass이다.

@MappedSuperclass는 공통 매핑 정보가 필요할 때 사용된다.

예를들어 대부분의 엔티티에 해당 객체가 만들어진 날짜(craetedDate)와 마지막 수정 날짜(lastModifiedBy)가 공통적으로 들어간다고 해보자.

이 경우 엔티티 하나하나마다 공통 필드를 추가하는것보다 추상클래스를 만들어 상속하는 식으로 구현하는것이 훨씬 효율적이다.

여기서 추상클래스 역할을 하는것에 @MappedSuperclass를 사용하면 된다.

@MappedSuperclass를 구현하는 코드는 다음과 같다.

@MappedSuperclass
public abstract class BaseEntity {
    private LocalDateTime createdDate;
    private LocalDateTime lastModifiedDate;
    ... // get set 구현
}

@Entity
public class otherEntity extends BaseEntity {
	...
}

@MappedSuperclass는 자식 클래스에 매핑 정보만 제공하므로 조회나 검색은 불가능하다.(em.find(BaseEntity) 불가)

또한 직접 생성해서 사용할 일이 없으므로 추상 클래스로 작성하는걸 권장한다.

 

 

참고: 자바 ORM 표준 JPA 프로그래밍 (https://www.inflearn.com/course/ORM-JPA-Basic#)

객체 지향 프로그래밍에서의 객체와 관계형 데이터베이스의 테이블간의 차이를 해결하기 위한 연관관계 매핑은 JPA에서 정말 중요한 부분이다.

연관관계 매핑에 관한 용어들 먼저 정리하면 다음과 같다.

  • 방향(Direction): 단방향, 양방향
  • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:N)
  • 연관관계의 주인(Owner): 객체 양방향 연관관계에서의 관리 주인

여기서는 다중성을 중심으로 연관관계 매핑 방법을 설명하려 한다.

 

 

제일 먼저 다대일 연관관계가 있다.

다대일 연관관계는 가장많이 사용하는 연관관계이고 그래서 제일 중요한 연관관계이다.

다대일 단방향 연관관계를 다이어그램으로 표현하면 다음과 같다.

이러한 연관관계를 표현하기 위한 코드는 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    private String username;
}

@Entity
public class Team{
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
}

 

여기서 Team 객체를 통해서도 Member와의 관계를 알고 싶으면 양방향 연관관계를 걸어주면 된다.

다대일 양방향 연관관계를 나타내는 다이어그램과 코드는 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    private String username;
}

@Entity
public class Team{
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    private String name;
}

외래키가 있는 쪽이 연관관계의 주인이므로 Member가 연관관계의 주인이 된다.

코드를 보면 다대일 단방향 연관관계에서 Team에 @OneToMany로 member를 노드로 하는 List를 추가하기만 하면 되는것을 알 수 있다.

여기서 @OneToMany에 mappedBy로 연관관계의 주인인 Member의 team를 지정해 주면 된다.

 

 

다음으로 일대다 연관관계가 있다.

일대다는 일과 다 중에 일이 연관관계의 주인일때를 말한다. 하지만 테이블에서는 일대다 관계에서 항상 다 쪽에 외래키가 있다.

즉 일대다 연관관계에서는 연관관계의 주인이 반대편 테이블의 외래키를 관리하는 특이한 구조를 갖게 된다.

이때, 주의할 점은 @OneToMany를 사용할때 @JoinColumn을 꼭 사용해야 한다는 것이다. 그렇지 않으면 조인 테이블 방식이 디폴트로 적용되는데 이것은 중간에 테이블을 하나 추가하는 방식으로써 의도치 않은 테이블의 생성으로 인한 테이블 관리의 불편함이 생길 수 있다.

일대다 단방향 연관관계를 나타내는 다이어그램과 코드는 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
}

@Entity
public class Team{
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    
    private String name;
}

일대다 단방향 매핑은 엔티티가 관리하는 외래키가 다른 테이블에 있어서 jpa를 통해 테이블을 다루는거 자체가 쉽지않다.

예를들어 team.setMembers같은 메서드를 쓸때 TEAM 테이블이 아닌 MEMBER 테이블에 update쿼리가 나가는등 코드의 일부분만 봐서는 쿼리 예측이 쉽지 않게 된다.

따라서 대다수의 경우에 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는게 바람직하다.

 

일대다 양방향 매핑은 공식적으로 존재하지 않지만 @JoinColumn(insertable = false, updatable = false)를 이용하여 일대다 양방향 매핑을 구현할 수 있다.

일대다 양방향 매핑을 나타내는 다이어그램과 코드는 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
    
    private String username;
}

@Entity
public class Team{
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    
    private String name;
}

하지만 일대다 양방향 매핑 또한 대부분의 경우 다대일 양방향을 사용하여 해결할 수 있으므로 관리가 비교적 어려운 일대다 매핑보다는

구현이 용이하고 관리가 직관적인 다대일 양방향 매핑을 사용하는것이 바람직하다.

 

 

다음으로 일대일 연관관계가 있다.

일대일 연관관계에서는 주 테이블이나 대상 테이블 중에 외래키를 어디다 둘지 선택 가능하다.

예를들어 한명(MEMBER)당 최대 하나의 사물함(LOCKER)을 배정 받을 수 있다 했을때 MEMBER 테이블을 주 테이블, LOCKER테이블을 대상 테이블이라고 하자.

주 테이블에 외래키가 있는 단방향 매핑을 다이어그램과 코드로 보이면 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    
    private String username;
}

@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
}

보다시피 다대일 단방향 매핑과 매우 유사한것을 알 수 있다. 때문에 여기서 양방향 매핑을 거는것 또한 다대일 양방향 매핑과 매우 유사하다.

일대일 연관관계에서 주 테이블에 외래키가 있는경우 양방향 매핑에 대한 다이어그램과 코드는 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    
    private String username;
}

@Entity
public class Locker {
    @Id @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
    
    private String name;
}

지금까지는 일대일 연관관계중에서도 주 테이블에 외래키가 있는 경우를 살펴보았다.

다음으로 대상  테이블에 외래키가 있는경우에 대해 말하고자 한다.

먼저 대상 테이블(LOCKER)에 외래키가 있는 경우 Member(Member.locker)를 연관관계의 주인으로 일대일 단방향 매핑을 하는 방법은 없다.

대상 테이블(LOCKER)에 외래키가 있는 경우 양방향 매핑은 가능하다.

단, 연관관계의 주인 또한 LOCKER(Locker.member)로 설정 해야하고, 따라서 Member는 읽기만 가능하다.

 

일대일 관계에서 주 테이블에 외래키가 있는경우 다음과 같은 특징이 있다.

  1. 주 객체가 대상 객체의 참조를 가지는것 처럼 주 테이블에 외래키를 두고 대상 테이블을 찾는다.
  2. JPA 매핑이 편리하고 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하므로 객체지향 개발자가 선호한다.
  3. 단점으로는 값이 없으면 외래키에 null값을 허용해야 한다.

일대일 관계에서 대상  테이블에 외래키가 있는경우 다음과 같은 특징이 있다.

  1. 대상 테이블에 외래키가 존재한다.
  2. 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변형할 때 테이블 구조를 유지할수 있어 전통적인 데이터베이스 개발자가 선호한다.
  3. 단점으로는 프록시 기능의 한계로 지연로딩으로 설정해줘도 즉시로딩이 불가피하다.

 

 

마지막으로 다대다 연관관계가 있다.

JPA에서 @ManyToMany를 사용한 다대다 매핑을 지원하지만 해당 관계에관한 다른 속성값을 추가할 수 없는 문제가 있다.

따라서 다대다 연관관계는 연결 테이블용 엔티티를 추가해서 다대일 일대다 관계로 풀어서 구현하는게 일반적이다.

예를 들어 고객(Member)과 상품(Product)이 다대다 관계라고 했을때, 고객과 상품사이에 MemberProduct라는 엔티티를 추가하여 각각 일대다 다대일 매핑을한다.

다대다 관계를 일대다 다대일 관계로 풀어서 매핑한 다이어그램과 코드는 다음과 같다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProduct = new ArrayList<>();
    
    private String username;
}

@Entity
public class Product{
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProduct = new ArrayList<>();
    
    private String name;
}

@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
    
    private int orderAmount;
    private int orderDate
}

 

 

참고: 자바 ORM 표준 JPA 프로그래밍 (https://www.inflearn.com/course/ORM-JPA-Basic#)

+ Recent posts