이번 주 목표 및 회고

📢 이번주 목표는 백준 실버1까지 승급하는 것이다. 현재 골드5이다.
     이번주 목표를 초과 달성했다. 더욱 더 열심히 해서 알고리즘 주차가 끝날 때에는 골드1까지 가보자
     갈수록 알고리즘도 더 어려워진다. 데일리 과제 매일 풀 수 있도록 부단히 노력을 해야겠다.

오늘 스케줄

09:00~09:20 팀 모닝 스크럼
09:20~12:00 데일리 과제 문제 풀이
12:00~13:00 점심 식사
13:00~17:00 데일리 과제 문제 풀이
17:00~17:40 팀 코드 선정 회의
18:00~18:20 기술 매니저님과 팀 코드 리뷰
18:20~19:20 저녁 식사
19:20~20:00 산책
20:00~00:00 백준 6문제 문제 풀이

오늘 목표

👉 데일리 과제 모두 풀기

👉 TIL 빠지지 않고 작성하기

👉 백준 6문제 문제 풀이

👉 북 스터디 참여

오늘 한 것

  • 데일리 과제 모두 풀기
  • TIL 빠지지 않고 작성하기
  • 백준 6문제 문제 풀이

오늘 못한 것

  • 북스터디 참여

알게된 것

⭐ DFS(Depth-first Search)

  • 깊이 우선 탐색
  • 연결된 노드를 따라서 계속 방문을 한 후에 연결된 노드들이 없을 때 그 전 노드로 되돌아가고 다시 연결된 노드를 따라서 탐색

특징

  • 자기 자 신을 호출하는 순환 알고리즘의 형태를 가지고 있다.
  • 전위 순회(Pre-Order Traversals)를 포함한 다른 형태의 트리 순회는 모두 DFS의 한 종류이다.
  • 이 알고리즘을 구현할 떄 가장 큰 차이점은, 그래프 탐색의 경우 어떤 노드를 방문했었는지 여부를 반드시 검사 해야 한다는 것이다. -> 이를 검사하지 않을 경우 무한루프에 빠질 위험이 있다.

자바에서의 DFS 구현 코드

방향 그래프 일 때의 DFS 구현 코드

import java.util.*;

class Graph {
    private int V; // 정점의 수
    private LinkedList<Integer> adj[]; // 인접 리스트

    // 생성자
    Graph(int v) {
        V = v;
        adj = new LinkedList[v];
        for (int i = 0; i < v; ++i)
            adj[i] = new LinkedList();
    }

    // 그래프에 간선 추가
    void addEdge(int v, int w) {
        adj[v].add(w);
    }

    // 주어진 시작 정점에서부터 도달 가능한 정점들을 DFS로 탐색
    void DFS(int s) {
        // 모든 정점을 방문하지 않은 상태로 초기화
        boolean visited[] = new boolean[V];

        // DFS를 위한 스택 생성
        Stack<Integer> stack = new Stack<>();

        // 현재 노드를 방문한 것으로 표시하고 스택에 넣음
        visited[s] = true;
        stack.push(s);

        while (!stack.isEmpty()) {
            // 스택에서 정점을 꺼내고 출력
            s = stack.pop();
            System.out.print(s + " ");

            // 꺼낸 정점의 모든 인접 정점을 확인
            // 방문하지 않은 인접 정점이 있다면 방문 표시하고 스택에 넣음
            for (int v : adj[s]) {
                if (!visited[v]) {
                    visited[v] = true;
                    stack.push(v);
                }
            }
        }
    }

    public static void main(String args[]) {
        Graph g = new Graph(4);

        g.addEdge(0, 1);
        g.addEdge(0, 2);
        g.addEdge(1, 2);
        g.addEdge(2, 0);
        g.addEdge(2, 3);
        g.addEdge(3, 3);

        System.out.println("정점 2에서 시작하는 그래프의 DFS 탐색:");
        g.DFS(2);
    }
}

무방향 그래프 일 때의  DFS 구현 코드

import java.util.*;

class Graph {
    private int V; // 정점의 수
    private LinkedList<Integer> adj[]; // 인접 리스트

    // 생성자
    Graph(int v) {
        V = v;
        adj = new LinkedList[v];
        for (int i = 0; i < v; ++i)
            adj[i] = new LinkedList();
    }

    // 그래프에 간선 추가
    void addEdge(int v, int w) {
        adj[v].add(w);
        adj[w].add(v); // 무방향 그래프에서는 w에서 v로의 간선도 추가
    }

    // 주어진 시작 정점에서부터 도달 가능한 정점들을 DFS로 탐색
    void DFS(int s) {
        // 모든 정점을 방문하지 않은 상태로 초기화
        boolean visited[] = new boolean[V];

        // DFS를 위한 스택 생성
        Stack<Integer> stack = new Stack<>();

        // 현재 노드를 방문한 것으로 표시하고 스택에 넣음
        visited[s] = true;
        stack.push(s);

        while (!stack.isEmpty()) {
            // 스택에서 정점을 꺼내고 출력
            s = stack.pop();
            System.out.print(s + " ");

            // 꺼낸 정점의 모든 인접 정점을 확인
            // 방문하지 않은 인접 정점이 있다면 방문 표시하고 스택에 넣음
            for (int v : adj[s]) {
                if (!visited[v]) {
                    visited[v] = true;
                    stack.push(v);
                }
            }
        }
    }

    public static void main(String args[]) {
        Graph g = new Graph(5);

        g.addEdge(0, 1);
        g.addEdge(0, 2);
        g.addEdge(1, 2);
        g.addEdge(1, 3);
        g.addEdge(2, 4);

        System.out.println("정점 0에서 시작하는 그래프의 DFS 탐색:");
        g.DFS(0);
    }
}

 

🤔오늘의 반성

- 바쁘다는 핑계로 책을 안 읽고 있다.
- TIL을 늦게 작성했다.

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. TIL을 빠지지 않고 작성했다.
4. 오늘의 데일리 과제 모두 풀었다.
5. 이번주 목표인 백준 골드5을 달성했다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

이번 주 목표

📢 이번주 목표는 백준 실버1까지 승급하는 것이다. 현재 실버1이다.

오늘 스케줄

09:00~09:20 팀 모닝 스크럼
09:20~12:00 데일리 과제 문제 풀이
12:00~13:00 점심 식사
13:00~17:00 데일리 과제 문제 풀이
17:00~17:20 팀 코드 선정 회의
17:20~18:20 인프런 DB 1편 강의수강
18:20~18:40 기술 매니저님과 팀 코드 리뷰
18:40~19:40 저녁 식사
20:00~휴식

오늘 목표

👉 데일리 과제 모두 풀기

👉 TIL 빠지지 않고 작성하기

👉 북스터디 참여

👉 마이크로서비스 강의 수강

👉 인프런 DB 1편 강의 듣기

오늘 한 것

  • 데일리 과제 모두 풀기
  • 인프런 DB 1편 강의 듣기
  • TIL 빠지지 않고 작성하기

오늘 못한 것

  • 북스터디 참여
  • 마이크로서비스 강의 수강

알게된 것

이분 탐색 / 이진 탐색 (Binanry Search)

이진 탐색의 이해를 돕기 위한 예시
  • 이진 탐색(이분 탐색) 알고리즘은 정렬되어 있는 리스트에서 탐색 범위를 절반씩 좁혀가며 데이터를 탐색하는 방법이다.
  • 이진 탐색은 배열 내부의 데이터가 정렬되어 있어야만 사용할 수 있는 알고리즘이다.
  • 변수 3개(start, end, mid)를 사용하여 탐색한다. 찾으려는 데이터와 중간점 위치에 있는 데이터를 반복적으로 비교해서 원하는 데이터를 찾는 것 이진 탐색의 과정이다

출처: https://velog.io/@kimdukbae/%EC%9D%B4%EB%B6%84-%ED%83%90%EC%83%89-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-Binary-Search

예시 코드

직접 구현한 Binary Search

public class BinarySearch {
    int binarySearch(int arr[], int x) {
        int l = 0, r = arr.length - 1;
        while (l <= r) {
            int m = l + (r - l) / 2;

            // x가 중간에 있는지 확인
            if (arr[m] == x)
                return m;

            // x가 더 크면 왼쪽 절반 무시
            if (arr[m] < x)
                l = m + 1;

            // x가 더 작으면 오른쪽 절반 무시
            else
                r = m - 1;
        }

        // 여기에 도달하면, 원소가 존재하지 않음
        return -1;
    }
}

 자바에서 제공하는 Binary Search

import java.util.Arrays;

public class BinarySearch {
    public static void main(String[] args) {
        int arr[] = {10, 20, 30, 40, 50};
        int key = 30;
        int result = Arrays.binarySearch(arr, key);
        if (result < 0)
            System.out.println("Element is not found!");
        else
            System.out.println("Element found at index: " + result);
    }
}

🤔오늘의 반성

- 마이크로서비스 강의를 못 들었다 ㅠㅠ
- 내일은 꼭 책 읽자.. 좀 제발..
- 손님이 오셔서 저녁 시간 이후로 공부를 못함
- TIL을 늦게 작성했다.

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. TIL을 빠지지 않고 작성했다.
4. 오늘의 데일리 과제 모두 풀었다.
5. 이번주 목표인 백준 실버1을 달성했다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

이번 주 목표

📢 이번주 목표는 백준 실버1까지 승급하는 것이다. 현재 실버2이다.

오늘 스케줄

09:00~09:20 팀 모닝 스크럼
09:20~12:00 데일리 과제 문제 풀이
12:00~13:00 점심 식사
13:00~17:00 데일리 과제 문제 풀이
17:00~17:20 팀 코드 선정 회의
17:20~18:00 휴식
18:00~18:20 기술 매니저님과 팀 코드 리뷰
18:20~19:20 저녁 식사
19:30~20:00 산책
20:00~21:00 장보기
21:30~00:30 프로그래머스 문제 풀이(5문제)
01:00~01:10 TIL 작성

오늘 목표

👉 데일리 과제 모두 풀기

👉 TIL 빠지지 않고 작성하기

👉 북스터디 참여

👉 마이크로서비스 강의 수강

👉 프로그래머스 5문제 풀기

 

오늘 한 것

  • 데일리 과제 모두 풀기
  • TIL 빠지지 않고 작성하기
  • 프로그래머스 5문제 풀기

오늘 못한 것

  • 북스터디 참여
  • 마이크로서비스 강의 수강

알게된 것

⭐우선순위 큐(Priority Queue)

우선순위 큐의 이해를 돕기 위한 max, min heap 자료구조

✅설명

PriorityQueue 우선순위 큐로써 일반적인 큐의 구조 FIFO(First In First Out)를 가지면서, 데이터가 들어온 순서대로 데이터가 나가는 것이 아닌 우선순위를 먼저 결정하고 그 우선순위가 높은 데이터가 먼저 나가는 자료구조이다.

PriorityQueue를 사용하기 위해선 우선순위 큐에 저장할 객체는 필수적으로 Comparable Interface를 구현해야한다.

Comparable Interface를 구현하면 compareTo method를 override 하게 되고 해당 객체에서 처리할 우선순위 조건을 리턴해주면 PriorityQueue 가 알아서 우선순위가 높은 객체를 추출 해준다.

PriorityQueue는 Heap을 이용하여 구현하는 것이 일반적이다.

데이터를 삽입할 때 우선순위를 기준으로 최대 힙 혹은 최소 힙을 구성하고 데이터를 꺼낼 때 루트 노드를 얻어낸 뒤 루트 노드를 삭제할 때는 빈 루트 노드 위치에 맨 마지막 노드를 삽입한 후 아래로 내려가면서 적절한 자리를 찾아 옮기는 방식으로 진행된다.
최대 값이 우선순위인 큐 = 최대 힙, 최소 값이 우선순위인 큐 = 최소 힙

출처: https://velog.io/@gillog/Java-Priority-Queue%EC%9A%B0%EC%84%A0-%EC%88%9C%EC%9C%84-%ED%81%90

 

특징

  1. 높은 우선순위의 요소를 먼저 꺼내서 처리하는 구조이다.
    우선순위 큐에 들어가는 원소는 비교가 가능한 기준이 있어야한다.
  2. 내부 요소는 힙으로 구성되어 이진트리 구조로 이루어져 있다.
  3. 내부구조가 힙으로 구성되어 있기에 시간 복잡도는 O(NLogN)이다.
  4. 우선순위를 중요시해야 하는 상황에서 주로 쓰인다.

 

코드 예시

import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) {
        // 우선순위 큐 선언
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        // 내림차순 우선순위 큐 선언
        PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());

        // 삽입 (offer)
        pq.offer(10);
        pq.offer(30);
        pq.offer(20);

        // 조회 (peek)
        System.out.println("큐의 맨 앞 요소: " + pq.peek());

        // 삭제 (poll)
        while (!pq.isEmpty()) {
            System.out.println("큐에서 삭제: " + pq.poll());
        }
    }
}

 

시간 복잡도

최고(최소) 우선순위 확인 O(1)
요소 검색 O(n)
크기(요소 수) O(1)
배열/목록으로 변환 O(n)

 

🤔오늘의 반성

- 마이크로서비스 강의를 못 들었다 ㅠㅠ
- 내일은 꼭 책 읽자..

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. TIL을 빠지지 않고 작성했다.
4. 오늘의 데일리 과제 모두 풀었다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

이번 주 목표

📢 이번주 목표는 백준 실버1까지 승급하는 것이다. 현재 실버2이다.

오늘 스케줄

09:00~09:30 알고리즘 2주차 발제 세션
09:30~12:00 데일리 과제 문제 풀이
12:00~13:00 점심 식사
13:00~14:00 데일리 과제 문제 풀이
14:00~16:00 병원 진료
16:00~17:00 데일리 과제 문제 풀이
17:00~17:40 팀 코드 선정 회의
17:40~18:20 휴식
18:20~18:40 기술 매니저님과 팀 코드 리뷰
18:40~20:30 저녁 식사 및 휴식
20:30~21:00 데일리 과제 문제 풀이 8번(뱀)
21:00~22:30 MVC2편 강의 수강(파일 업로드)
22:30~00:00 마이크로서비스 강의 수강(섹션 1 - Service Discovery)
00:00~00:20 TIL 작성

오늘 목표

👉 데일리 과제 모두 풀기

👉 TIL 빠지지 않고 작성하기

👉 북스터디 참여

👉 MVC2편_파일업로드 강의 수강

👉 마이크로서비스 강의 수강

 

오늘 한 것

  • 데일리 과제 모두 풀기
  • TIL 빠지지 않고 작성하기
  • MVC2편_파일업로드 강의 수강
  • 마이크로서비스 강의 수강

오늘 못한 것

  • 북스터디 참여

알게된 것

스택(Stack)

스택 자료구조 이미지
  • 블록을 아래에서 위로 쌓아 올리는 구조를 가지고 있다.
  • 가장 마지막에 삽입한 데이터를 가장 먼저 사용 LIFO(Last-in, First-out)
  • 삽입(Push) : Push를 하게 되면 가장 위쪽에 데이터가 저장된다.
  • 삭제(Pop): Push와 반대로 데이터를 삭제하는 것을 Pop, 가장 위쪽의 데이터가 삭제된다.
  • 읽기(Peek): 마지막 위치(top)에 해당하는 데이터를 읽는다. 이 때, 삭제를 진행하지 않음

스택 구현

import java.util.Stack;

public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();

        // 삽입 (push)
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println("스택: " + stack)

        // 조회 (peek)
        int top = stack.peek();
        System.out.println("스택의 맨 위 요소: " + top);

        // 삭제 (pop)
        int removed = stack.pop();
        System.out.println("삭제된 요소: " + removed);
        System.out.println("스택: " + stack);
    }
}

시간 복잡도

자바의 Stack 클래스는 내부적으로 Vector를 사용하여 데이터를 저장합니다. 따라서 스택의 삽입(push), 삭제(pop), 조회(peek) 연산의 시간 복잡도는 모두 O(1)입니다.
이는 스택이 LIFO(Last In First Out) 원칙을 따르는 자료구조이기 때문에, 삽입과 삭제가 항상 스택의 맨 위에서 이루어지므로 상수 시간이 소요됩니다. 그리고 peek 연산은 단순히 스택의 맨 위 요소를 조회하는 것이므로 역시 상수 시간이 소요됩니다.
하지만 이러한 시간 복잡도는 스택의 크기에 따라 메모리를 동적으로 할당하거나 해제하는 데 필요한 시간은 고려하지 않습니다. 이러한 경우에는 시간 복잡도가 O(n)이 될 수 있습니다. 여기서 n은 스택의 크기입니다.

 

큐(Queue)

큐 자료구조 이미지
  • 큐(Queue) 데이터를 한쪽 끝에서 삽입하고 반대쪽 끝에서 삭제하는 구조를 가지고 있다
  • 가장 먼저 삽입한 데이터를 가장 먼저 사용 FIFO(First-in, First-out)
  • 삽입(Enqueue) : Enqueue를 하게 되면 큐의 뒤쪽에 데이터가 저장된다.
  • 삭제(Dequeue): Enqueue와 반대로 데이터를 삭제하는 것을 Dequeue, 가장 앞쪽의 데이터가 삭제된다.
  • 읽기(Peek): 큐의 맨 앞에 위치한 데이터를 읽는다. 이 때, 삭제를 진행하지 않음

큐 구현

import java.util.Queue;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();

        // 삽입 (enqueue)
        queue.add(1);
        queue.add(2);
        queue.add(3);
        System.out.println("큐: " + queue);

        // 조회 (peek)
        int front = queue.peek();
        System.out.println("큐의 맨 앞 요소: " + front);

        // 삭제 (dequeue)
        int removed = queue.poll();
        System.out.println("삭제된 요소: " + removed);
        System.out.println("큐: " + queue);
    }
}

시간 복잡도

자바의 Queue 인터페이스를 구현하는 LinkedList 클래스는 내부적으로 연결 리스트를 사용하여 데이터를 저장합니다. 
따라서 큐의 삽입(enqueue), 삭제(dequeue), 조회(peek) 연산의 시간 복잡도는 다음과 같습니다:

삽입(enqueue): O(1)
삭제(dequeue): O(1)
조회(peek): O(1), 중간의 요소를 탐색할 때(O(n))
이는 큐가 FIFO(First In First Out) 원칙을 따르는 자료구조이기 때문에, 삽입은 항상 리스트의 끝에서, 삭제와 조회는 항상 리스트의 시작에서 이루어지므로 상수 시간이 소요됩니다.

 

🤔오늘의 반성

- 팀 코드 선정 전까지 데일리 과제 모두 못 풀었다.
- 사람인/잡코리아 이력서 업데이트 못한 점
- 북 스터디 참여를 못함 책도 열심히 읽자...

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. TIL을 빠지지 않고 작성했다.
4. 백준 실버2로 승급했다 v

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

 

이번 주 목표

📢 이번주 목표는 백준 실버1까지 승급하는 것이다. 현재 실버3이다.

오늘 스케줄

09:00~12:00 자료구조/알고리즘 1주차 시험(4문제)응시
12:00~13:00 점심 식사
13:00~17:00 못 푼 자료구조/알고리즘 1주차 문제 풀이
17:00~17:30 프로젝트 주차 설명 듣기
17:30~18:00 팀 코드 선정 회의
18:00~18:40 휴식
18:40~19:00 기술매니저님과 팀 코드 리뷰
19:00~20:00 저녁 식사
20:00~21:30 산책
21:30~23:30 ORM 섹션11 객체지향 쿼리 언어2 부분 수강
23:30:24:00 MVC2편-파일업로드 수강
24:00~00:20 TIL 작성

오늘 목표

👉 자료구조/알고리즘 1주차 시험 응시

👉 TIL 빠지지 않고 작성하기

👉 북스터디 참여

👉 ORM_객체지향 쿼리 언어2 부분 수강

👉 MVC2편_파일업로드 강의 수강

👉 마이크로서비스 강의 수강

 

오늘 한 것

  • 자료구조/알고리즘 1주차 시험 응시
  • TIL 빠지지 않고 작성하기
  • ORM_객체지향 쿼리언어 기본 문법 강의 수강
  • MVC2편_파일업로드 강의 수강

오늘 못한 것

  • 북스터디 참여
  • 마이크로서비스 강의 수강

알게된 것

⭐ Fetch Join

  • JPQL에서 성능 최적화를 위해 제공하는 조인의 종류

JPQL(Java Persistence Query Language)

  • SQL이 DB에 있는 테이블을 조회하는 쿼리라고 한다면, JPQL은 엔티티 객체를 조회하는 객체지향 쿼리를 의미.
  • 문법은 SQL과 비슷하고 SQL이 제공하는 기능을 유사하게 지원
public class Main {

    public static void main(String[] args) {
        try {
      
            String query = "select b from Book b where b.name = '조선왕조실록'";

            List<Book> books = em.createQuery(query, Book.class)
                .getResultList();

            for (Book book : books) {
                System.out.println("book.id = " + book.getId());
                System.out.println("book.name = " + book.getName());
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

}
 JPQL은 SQL을 추상화하였기에 특정 데이터베이스에 의존하지 않고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않고 자연스럽게 데이터베이스를 변경할 수 있습니다.
또한 JPQL은 엔티티 직접 조회, 묵시적 조인, 다형성 지원 등의 기능을 제공하기에
SQL보다 간결하다는 장점을 가지고 있습니다.

페치 조인

이제 페치 조인에 대해 알아보도록 하겠습니다. 앞서 말씀드렸다시피 JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션을 한 번에 같이 조회할 수 있는 기능입니다. JOIN FETCH 명령어로 사용할 수 있습니다.

등장 배경

JPA는 2가지 페치 전략이 존재하는데 기본적으로 지연 로딩 전략을 사용하게 됩니다. 이에 대해 조금 더 알고 싶은 분은 아래의 글을 참고해주시기 바랍니다.

 

[JPA] 프록시와 로딩 전략

프록시 는 무엇을 의미할까요? 이 질문에 앞서 프록시가 등장하게 된 배경에 대해 먼저 알아보도록 하겠습니다. Proxy 등장 배경 객체는 객체 그래프로 연관된 객체들을 자유롭게 탐색할 수 있습

woo-chang.tistory.com

Library libraryA = new Library();
libraryA.setName("libraryA");
em.persist(libraryA);

Library libraryB = new Library();
libraryB.setName("libraryB");
em.persist(libraryB);

Library libraryC = new Library();
libraryC.setName("libraryC");
em.persist(libraryC);

Book bookA = new Book();
bookA.setName("bookA");
bookA.setLibrary(libraryA);
em.persist(bookA);

Book bookB = new Book();
bookB.setName("bookB");
bookB.setLibrary(libraryB);
em.persist(bookB);

Book bookC = new Book();
bookC.setName("bookC");
bookC.setLibrary(libraryC);
em.persist(bookC);

String query = "select b from Book b";

em.flush();
em.clear();

List<Book> books = em.createQuery(query, Book.class).getResultList();

for (Book book : books) {
    System.out.println(book.getLibrary());
}

지연 로딩 전략으로 인해 다음과 같은 코드가 작성되었을 때 조회된 Book의 개수만큼 추가로 Library를 조회하는 쿼리가 나가게 됩니다. 이러한 문제는 N+1 문제라고 합니다. 처음 조회부터 조인시켜 데이터를 가져올 수 있게 하려고 페치 조인이 등장하게 되었습니다.

 

그렇다면 일반 조인을 사용하면 되지 않을까 하는 의문이 생길 수 있습니다. 일반 조인과 차이점은 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않습니다. 조인은 하지만 데이터가 조회되지 않는다는 의미입니다. 그렇기에 위와 마찬가지로 N+1 문제가 발생합니다.

엔티티 페치 조인

제가 작성한 예제에서는 Library와 Book의 연관관계가 1:N으로 지정되어있습니다. 페치 조인을 사용해서 Book 엔티티를 조회하면서 Library 엔티티도 함께 조회하는 예제를 살펴보도록 하겠습니다.

String query = "select b from Book b join fetch b.library";

em.flush();
em.clear();

List<Book> books = em.createQuery(query, Book.class).getResultList();

for (Book book : books) {
    System.out.println(book.getLibrary());
}

JOIN FETCH 명령어를 사용하여 Book의 Library를 함께 조회합니다. 실제 쿼리문에서도 JOIN 쿼리가 나가게 되고 데이터가 함께 조회되기에 추가적인 쿼리가 나가지 않습니다. JPQL의 일반적인 조인과 다르게 b.library의 별칭이 존재하지 않는데 페치 조인은 별칭을 사용할 수 없습니다. (하이버네이트는 페치 조인에도 별칭을 허용합니다.)

 

페치 조인을 사용하였기에 Book과 Library가 객체 그래프를 유지하면서 데이터를 조회할 수 있습니다. Library는 프록시가 아닌 실제 엔티티이므로 Book 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 Library를 조회할 수 있습니다.

컬렉션 페치 조인

이제는 Library에서 Book을 페치 조인해보도록 하겠습니다.

String query = "select l from Library l join fetch l.books where l.name = 'libraryA'";

em.flush();
em.clear();

Library library = em.createQuery(query, Library.class).getSingleResult();

for (Book book : library.getBooks()) {
    System.out.println(book.getName());
}

Library를 조회하면서 페치 조인을 사용하여 연관된 Book 컬렉션도 함께 조회됩니다.

페치 조인과 DISTINCT

Library libraryA = new Library();
libraryA.setName("libraryA");
em.persist(libraryA);

Book bookA = new Book();
bookA.setName("bookA");
bookA.setLibrary(libraryA);
em.persist(bookA);

Book bookB = new Book();
bookB.setName("bookB");
bookB.setLibrary(libraryA);
em.persist(bookB);

String query = "select l from Library l join fetch l.books where l.name = 'libraryA'";

em.flush();
em.clear();

List<Library> libraries = em.createQuery(query, Library.class).getResultList();

for (Library library : libraries) {
    System.out.println("library.name = " + library.getName() + ", book count = " + library.getBooks().size());
}

페치 조인에 대해 알아보았으니 다음 코드의 결과가 예측되시나요? 출력 결과는 한 줄이고 libraryA에 대한 Book 개수는 2로 다들 예상할 것으로 생각합니다. 하지만 결과는 그렇지 않습니다. 결과는 아래와 같이 출력됩니다.

왜 이런 결과가 나오게 되었을까요? 지금부터 그림을 통해 설명해 드리도록 하겠습니다. 컬렉션 페치 조인을 시도하게 되면 다음과 같은 그림으로 조인을 시도합니다.

컬렉션 페치 조인 결과 테이블은 아래와 같습니다.

컬렉션 페치 조인 결과 객체는 다음과 같은 형태를 보이게 됩니다.

이제 예상과 다른 결과가 나오게 된 배경이 이해되시나요? 연관된 Book 데이터가 같이 조회되기 때문에 결과가 증가해서 위와 같은 결과가 나오게 되는 것입니다. 일대다 조인에서는 증가한 결과가 나올 수 있지만 일대일, 다대일 조인에서는 결과가 증가하지 않습니다. 조인했을 때 익숙한 결과를 얻기 위해서는 DISTINCT를 사용하면 됩니다.

String query = "select distinct l from Library l join fetch l.books where l.name = 'libraryA'";

DISTINCT는 SQL에서 중복을 제거하는 명령어입니다. JPQL에서의 DISTINCT 명령어는 SQL에 DISTINCT 명령어를 추가하는 것과 동시에 애플리케이션 상에서 중복을 제거하는 동작도 수행합니다. 따라서 SQL 결과에서는 위와 같은 결과 테이블을 반환하지만, 애플리케이션에서는 중복된 데이터가 걸러집니다. 즉, 0x100을 가리키는 포인터가 하나가 됨을 의미합니다.

페치 조인의 특징과 한계

특징

페치 타입 설정과 같이 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라고 부릅니다. 페치 조인은 글로벌 로딩 전략 보다 우선시됩니다. 그렇기에 페치 타입을 LAZY로 설정하더라도 페치 조인을 사용하면 데이터가 즉시 조회되는 결과를 가져오게 됩니다. 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적입니다.

페치 조인은 객체 그래프를 유지할 때 사용하면 효과적입니다. 만약 여러 테이블을 조인해서 엔티티가 가진 그대로의 모양이 아닌 다른 결과를 내야 한다면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 효과적입니다.

한계

페치 조인 대상에는 별칭을 줄 수 없기에 SELECT, WHERE, 서브 쿼리에 페치 조인 대상을 사용할 수 없습니다. 이러한 제약을 둔 이유는 잘못된 별칭 사용으로 인해 데이터 무결성이 깨질 수 있으므로 제약을 걸어두었습니다. JPA 표준에서는 지원하지 않지만 하이버네이트를 포함한 몇몇 구현체들은 별칭을 지원합니다. 하지만 위와 같은 문제로 인해 조심해서 사용할 수 있도록 해야 합니다.

앞서 보았던 데이터가 증폭되는 문제로 인하여 둘 이상의 컬렉션을 페치할 수 없습니다. 컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 가능한 구현체에서도 주의해서 사용해야 합니다.

컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없습니다. 일대일, 다대일과 같은 단일값 연관 필드는 페이징 API를 사용할 수 있습니다. 하이버네이트에서 사용하게 되면 경고 로그를 남기고 모든 데이터를 불러와 메모리에서 페이징을 진행하기 때문에 매우 위험한 작업입니다.

출처: https://woo-chang.tistory.com/38

🤔오늘의 반성

- 오늘 자료구조/알고리즘 1주차 시험 응시하면서 다 풀지 못한 점
- 사람인/잡코리아 이력서 업데이트 못한 점
- 오늘 해야할 일을 다 못한 점

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. TIL을 빠지지 않고 작성했다.
4. 목표한 공부 계획을 모두 수행했다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

이번 주 목표

📢 이번주 목표는 백준 실버1까지 승급하는 것이다. 현재 실버3이다.

오늘 스케줄

09:00~09:20 팀 모닝 스크럼
09:20~12:00 5일차 데일리 과제 문제 풀기
12:00~13:00 점심시간
13:00~17:00 5일차 데일리 과제 문제 풀기
17:00~17:30 휴식
17:30~18:00 팀 코드 선정 회의
18:00~18:20 기술매니저님과 팀 코드 리뷰
18:20~19:00 저녁시간
19:00~21:00 ORM_객체지향 쿼리언어 기본 문법 강의 수강
21:00~22:00 스프링 클라우드로 개발하는 마이크로서비스 강의 섹션1 수강
22:00~24:00 코딩테스트 합격자 되기:자바편 독서

 

오늘 목표

👉 5일차 데일리과제 제출

👉 TIL 빠지지 않고 작성하기

👉 북스터디 참여

👉 ORM_객체지향 쿼리언어 기본 문법 강의 수강

👉 마이크로서비스 강의 수강

👉 코딩테스트 합격자 되기:자바편 200p까지 읽기

 

오늘 한 것

  • 5일차 데일리과제 제출
  • TIL 빠지지 않고 작성하기
  • 북스터디 참여
  • ORM_객체지향 쿼리언어 기본 문법 강의 수강
  • 마이크로서비스 강의 수강
  • 코딩테스트 합격자 되기:자바편 200p까지 읽기

오늘 못한 것

 

알게된 것

1.  기술 매니저님이 비트마스크 라는 것에 대해 알려줘서 학습을 했다.

비트마스크는 이진수를 사용하는 컴퓨터의 연산 방식을 이용해, 정수의 이진수 표현을 자료 구조로 쓰는 기법을 말합니다.

이진수는 0 또는 1을 이용하므로 하나의 비트(bit)가 표현할 수 있는 2가지 경우가 있는데,

비트가 1이면 "켜져 있다"라고 말하며, 0이면 "꺼져 있다"라고 말합니다.

비트마스크를 사용하는 이유

🟢 빠른 수행시간

원소의 수가 많지 않을 때 사용되며, bit연산이기 때문에 시간복잡도 O(1)에 구현되는 것이 많습니다.

🟢 작은 메모리 사용량

만약 bit가 10개인 경우, 각 bit당 두 가지의 경우를 가지기 때문에 2^10가지의 경우를 10bit 이진수 하나로 표현 가능합니다.

그렇기 때문에 하나의 정수로 매우 많은 경우의 수를 표현할 수 있기 때문에 메모리 측면에서 효율적이며, 데이터를 미리 계산하여 저장해 둘 수 있으므로 캐시 효율이 좋습니다.

🟢 간결한 코드

다양한 집합 연사자들을 반복문 없이 한 줄에 쓸 수 있어 반복문, 조건문을 이용한 코드보다 훨씬 간결하게 코드를 작성할 수 있습니다.

비트 논리 연산자

연산자 논리 설 명
& AND 두 비트 모두 1이면 1
| OR 두 비트 중 1개만 1이면 1
^ XOR 두 비트가 서로 다르면 1
~ NOT 비트 반전(보수)
<< LEFT SHIFT 지정한 수만큼 비트들을 전부 왼쪽으로 이동(이동되고 남은 비트는 0으로 채움)
>> RIGHT SHIFT 지정한 수만큼 비트를 전부 오른쪽으로 이동(이동되고 남은 비트는 부호 비트로 채움)
부호를 유지

출처: https://hstory0208.tistory.com/entry/알고리즘-비트마스크BitMask란 [< Hyun / Log >:티스토리]

2. 백준 등급이 실버3으로 올랐다.

3. 마이크로서비스가 등장하게 된 배경이나 기본적인 아키텍처 구성에 대해 알게 되었다.

🤔오늘의 반성

- 오늘 데일리과제 8번을 어려워서 풀지 못했다.
- 1일 1지원을 하려고 했는데 못했다.

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. TIL을 빠지지 않고 작성했다.
4. 목표한 공부 계획을 모두 수행했다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

이번 주 목표 및 회고

📢 일단 이번주차 데일리 과제 문제들은 빠짐 없이 풀었다. 풀면서 다른 팀원들의 코드를 보고 많이 배웠고
    기술 매니저님이 설명해주시는 내용도 꿀팁들이 많아서 유익한 시간이였다. 앞으로 더 효율적으로 코드를 작성하도
    록 노력을 해야겠다. 다음주에 있을 과제들도 열심히 풀어야 겠다.

오늘 스케줄

09:00~09:20 팀 모닝 스크럼
09:20~12:00 4일차 데일리 과제 문제 풀기
12:00~13:00 점심시간
13:00~14:00 4일차 데일리 과제 문제 풀기
14:00~16:00 카페로 장소 이동해서 문제 풀기
17:30~18:00 팀 코드 선정 회의
18:40~19:00 기술매니저님과 팀 코드 리뷰
19:00~20:00 저녁시간
20:00~휴식

 

오늘 목표

👉 4일차 데일리과제 제출

👉 TIL 빠지지 않고 작성하기

👉 북스터디 참여

오늘 한 것

  • 4일차 데일리과제 제출
  • TIL 빠지지 않고 작성하기

오늘 못한 것

  • 북스터디 참여

 

알게된 것

1. 반복문 안에서 배열.length 나 리스트.size를 선언해서 쓰지 말고 밖에서 선언해두고 안해서 써야 성능이 더 좋다.

2. 문자열로 만들 수 있는 순열에 대해서 재귀를 활용하는 방법에 대해 배웠다.

3. 백준 등급이 생각보다 빠르게 오르지 않는다.

4. 정규표현식에 대해 간단히 알고 갈 수 있었다.

🤔오늘의 반성

- 저녁먹고 친척들이 놀러와서 식사하고 얘기 하느라 공부 못한 것
- TIL을 늦게 작성한 것

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
4. 금일 데일리 과제 8문제를 모두 풀었다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

이번 주 목표

📢 오늘부터 2주차 알고리즘 세션이 시작되었다. 매일매일 8문제씩 푸는데
     뒤쳐지지 않고 하루의 문제를 다 푸는 것과 백준 현재 실버5인데 실버1까지 가보자!

오늘 스케줄

09:00~09:20 팀 모닝 스크럼
09:20~12:00 3일차 데일리 과제 문제 풀기
12:00~13:00 점심시간
13:00~16:00 3일차 데일리 과제 문제 풀기
16:00~17:00 병원 다녀오기
17:30~18:00 팀 코드 선정 회의
18:00~18:20 기술매니저님과 팀 코드 리뷰
18:30~19:30 저녁시간(외식)
20:00~21:30 ORM_값 타입 강의 수강

 

오늘 목표

👉 3일차 데일리과제 제출

👉 마이크로서비스 관련 강의 수강하기

👉 TIL 빠지지 않고 작성하기

👉 인프런 ORM 강의 듣기 - 값 타입

👉 북스터디 참여

👉 이력서 제출하기

오늘 한 것

  • 3일차 데일리과제 제출
  • 이력서 제출하기
  • 인프런 ORM 강의 듣기 - 값 타입

오늘 못한 것

  • 마이크로서비스 관련 강의 수강
  • TIL 빠지지 않고 작성하기 - 늦게 작성했다.
  • 북스터디 참여 - 외식으로 불참

 

알게된 것

1. ORM에서 값 타입 컬렉션을 쓰기 보다는 1 : N으로 푸는 것을 권장한다.

2. 자바에서 쓰는 Arrays.sort()에 Tim Sort라는 알고리즘이 쓰인다는 점을 알았다.
   Java11부터는 Collection.sort() 도 리스트를 배열로 전환 후 Arrays.sort()를 호출해서 정렬을 구현

3. 오늘 백준 실버4로 승급했다.

4. Sting에서 제공하는 다양한 내장 함수들을 알 수 있었다.

🤔오늘의 반성

- 외식하고 조금 더 공부 안하고 쉰 것
- TIL을 늦게 작성한 것

 

👍 오늘의 칭찬

1. 지각하지 않았다.
2. 깃헙 1일 1커밋 달성
3. 백준 실버 4가 되었다.
4. 금일 데일리 과제 8문제를 모두 풀었다.

❗ 항해99 취업 리부트 코스를 수강하고 작성한 콘텐츠 입니다.
https://hanghae99.spartacodingclub.kr/reboot

+ Recent posts