no image
[JPA] 컬렉션 조회 최적화(OneToMany)
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.컬렉션인 일대다 관계(OneToMany) 를 조회하고, 최적화하는 방법 아래의 순서대로 최적화를 진행하는 것이 좋다.1. DTO를 반환2. 컬렉션 최적화    - 페이징 필요시 : hibernate.default_batch_fetch_size , @BatchSize 로 최적화    - 페이징 필요 없을시 : FETCH 조인으로 최적화3. DTO를 직접 조회(이 부분은 이해가 잘 되지 않아서 글에 적지 않음) DTO를 반환@RestController@RequiredArgsConstructorpublic class OrderApiController { private final OrderRepository ord..
2024.08.11
no image
[JPA] 지연 로딩과 조회 성능 최적화(ManyToOne, OneToOne)
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.API를 만들 때 엔티티 자체를 리턴하는 방식을 절대로 사용하면 안된다.기본적으로 엔티티를 DTO로 바꾸어서 리턴해야 한다. 또한 트래픽이 낮다면 굳이 성능 최적화가 필요 없겠지만, 사용자가 늘어난다면 성능 최적화를 고려해야한다. 성능 최적화 순서엔티티를 DTO로 변환하는 방법을 선택필요하면 FETCH 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결그래도 안되면 DTO로 직접 조회하는 방법을 사용최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용 엔티티를 DTO로 변환@Datastatic class SimpleOrderDto { priv..
2024.08.10
no image
[인프런 알고리즘] Chapter 5, 4번 문제(후위식 연산)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.Stack;public class sec05_04 { public static int solution(String str) { Stack stack = new Stack(); for (Character c : str.toCharArray()) { if(Character.isDigit(c)) stack.push(Character.getNumeric..
2024.08.10
no image
[인프런 알고리즘] Chapter 5, 3번 문제(크레인 인형뽑기(카카오))
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.Stack;import java.util.StringTokenizer;public class sec05_03 { public static int solution(int[][] board, int[] moves) { int count = 0; Stack stack = new Stack(); for(int i : moves) { ..
2024.08.09
no image
[인프런 알고리즘] Chapter 5, 2번 문제(괄호문자제거)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드첫 번째 코드(스택 이용)import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.Stack;public class sec05_02 { public static String solution(String str) { StringBuilder sb = new StringBuilder(); Stack stack = new Stack(); for(char c : str.toCharArray()) {..
2024.08.07
no image
[인프런 알고리즘] Chapter 5, 1번 문제(올바른 괄호)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;public class sec05_01 { public static String solution(String str) { int left = 0; for(int i = 0; i  설명주어진 문자열 str의 각 문자를 순회하면서 열린 괄호 ’(’의 개수를 카운트하고, 닫힌 괄호 ’)’를 만나면 카운트를 감소시킨다. 이때, 닫힌 괄호가 열린 괄호보다 먼저 나오는 경우(즉, left 변수 left: ..
2024.08.06
no image
[JPA] 병합(Merge)과 변경 감지(Dirty Checking)
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.JPA(Java Persistence API)에서 변경 감지(Dirty Checking)와 병합(Merge)은 엔티티 상태 관리를 위한 중요한 개념이다. 이 두 가지는 데이터베이스와 애플리케이션 간의 동기화를 효율적으로 관리하는 데 사용된다. 결론부터 말하면 두 방법중에 변경 감지를 사용하는 것이 더 좋다. 병합병합(Merge)은 JPA에서 준영속 상태의 엔티티를 영속성 컨텍스트에 포함시키고, 해당 엔티티의 상태를 데이터베이스에 반영하는 작업이다.이 과정은 데이터베이스에 이미 저장된 데이터를 수정할 때 사용된다.@PostMapping(value = "/items/{itemId}/edit")public String..
2024.08.05
no image
[인프런 알고리즘] Chapter 4, 5번 문제(K번째 큰 수)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드package inflearn_algorithm.chapter4;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.*;public class sec04_05 { public static int solution(int N, int K , Integer[] arr) { int count = 1; TreeSet reverseTreeSet = new TreeSet(Comparator.reverseOrder()); ..
2024.08.05

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


컬렉션인 일대다 관계(OneToMany) 를 조회하고, 최적화하는 방법

 

아래의 순서대로 최적화를 진행하는 것이 좋다.

1. DTO를 반환
2. 컬렉션 최적화
    - 페이징 필요시 : hibernate.default_batch_fetch_size , @BatchSize 로 최적화
    - 페이징 필요 없을시 : FETCH 조인으로 최적화
3. DTO를 직접 조회(이 부분은 이해가 잘 되지 않아서 글에 적지 않음)

 

DTO를 반환

@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;
    
//    @GetMapping("/api/v2/orders")
//    public List<OrderDto> ordersV2() {
//        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
//        List<OrderDto> result = orders.stream()
//                .map(o -> new OrderDto(o))
//                .collect(toList());
//        return result;
//    }

    @GetMapping("/api/v2/orders")
    public Result ordersV2() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<OrderDto> result = orders.stream()
                .map(o -> new OrderDto(o))
                .collect(Collectors.toList());
        return new Result(result.size(), result);
    }

    @Data
    @AllArgsConstructor
    static class Result<T> {
        //이 클래스로 한번 감싸서 리턴하면 추가적인 정보도 담을 수 있다.
        private int size; //추가 적인 정보
        private T data;
    }

    //Order 엔티티를 직접 반환하지 말고 DTO를 통해서 반환하라
    @Data
    static class OrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        private List<OrderItemDto> orderItems;

        public OrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
            orderItems = order.getOrderItems().stream()
                    .map(orderItem -> new OrderItemDto(orderItem))
                    .collect(Collectors.toList());
        }
    }

    //Order DTO안에 또 엔티티가 있다면 그 엔티티 마저 DTO로 바꿔야 한다.
    @Data
    static class OrderItemDto {
        private String itemName;//상품 명
        private int orderPrice; //주문 가격
        private int count; //주문 수량

        public OrderItemDto(OrderItem orderItem) {
            itemName = orderItem.getItem().getName();
            orderPrice = orderItem.getOrderPrice();
            count = orderItem.getCount();
        }

    }
}

주의해야할 점은 엔티티 안에 엔티티가 있다면 내부에 있는 엔티티도 DTO로 변환해야 한다.

그래야 엔티티를 외부에 노출하지 않을 수 있다.

이 방법은 N+1 문제가 발생한다.

지연 로딩으로 너무 많은 SQL 실행 수
- order 1번
- member, address -> N번(order 조회 수 만큼)
- orderItem -> N번(order 조회 수 만큼)
- item N번 -> (orderItem 조회 수 만큼)

지연 로딩은 영속성 컨텍스트에 있으면 영속성 컨텍스트에 있는 엔티티를 사용하고 없으면 SQL을 실행한다.
따라서 같은 영속성 컨텍스트에서 이미 로딩한 회원 엔티티를 추가로 조회하면 SQL을 실행하지 않는다.

 

FETCH JOIN 최적화

@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;
    
    /...
    
		@GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithItem();
        List<OrderDto> result = orders.stream()
                .map(o -> new OrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
}
@Repository
@RequiredArgsConstructor
public class OrderRepository {
    private final EntityManager em;

    //...

    public List<Order> findAllWithItem() {
        return em.createQuery(
                        "select distinct o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d" +
                                " join fetch o.orderItems oi" +
                                " join fetch oi.item i", Order.class)
                .getResultList();
    }
}

FETCH 조인으로 SQL이 1번만 실행됨

distinct 를 사용한 이유는 1대다 조인이 있으므로 데이터베이스 row가 증가한다.

그 결과 같은 order 엔티티의 조회 수도 증가하게 된다.

JPA의 distinct는 SQL에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면, 애플리케이션에서 중복을 걸러준다.

이 예에서 order가 컬렉션 페치 조인 때문에 중복 조회 되는 것을 막아준다.

하이버네이트6 부터는 DISTINCT 를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용

그러나, 모든 경우에 distinct가 필요 없다는 것은 아니다.
특히, 쿼리의 복잡성이나 특정 엔티티 그래프 구조에 따라 여전히 distinct가 필요한 경우가 있을 수 있다.
또한, JPQL에서 distinct 키워드는 SQL의 DISTINCT와는 약간 다른 의미로, JPA 레벨에서 객체 중복을 제거하기 위해 여전히 유용할 수 있다.

 

하지만 페이징이 불가능하다

가능하긴 하지만 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버리기 때문에 매우 위험하다.

 

또한 컬렉션 FETCH 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다.

→ 엔티티에 컬렉션이 여러개라면 FETCH 조인을 사용하면 안된다는 뜻

 

Batch 사이즈 사용(페이징 가능)

페이징 + 컬렉션 엔티티를 함께 조회 방법

  1. ToOne(OneToOne, ManyToOne) 관계를 모두 FETCH 조인한다.
    ToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.
  2. 컬렉션은 지연 로딩으로 조회한다.
  3. 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용한다.
        - hibernate.default_batch_fetch_size: 글로벌 설정
        - @BatchSize: 개별 최적화
        - 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.
        - 하이버네이트 6.2 부터는 where in 대신에 `array_contains 를 사용
@Repository
@RequiredArgsConstructor
public class OrderRepository {
    private final EntityManager em;

   //...

    public List<Order> findAllWithMemberDelivery(int offset, int limit) {
		    //ToOne 관계만 FETCH 조인
        return em.createQuery(
                        "select o from Order o" +
                                " join fetch o.member m" +
                                " join fetch o.delivery d", Order.class)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
    }
}
@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;
    
    //...

    /**
     * V3.1 엔티티를 조회해서 DTO로 변환 페이징 고려
     * - ToOne 관계만 우선 모두 페치 조인으로 최적화
     * - 컬렉션 관계는 hibernate.default_batch_fetch_size, @BatchSize로 최적화
     */
    @GetMapping("/api/v3.1/orders")
    public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
                                        @RequestParam(value = "limit", defaultValue = "100") int limit) {
        List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
        List<OrderDto> result = orders.stream()
                .map(o -> new OrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
}

 

application.properites

spring.jpa.properties.hibernate.default_batch_fetch_size=1000

 

  • 쿼리 호출 수가 1 + N → 1 + 1 로 최적화 된다.
    조인보다 DB 데이터 전송량이 최적화 된다.
    (Order와 OrderItem을 조인하면 Order가 OrderItem 만큼 중복해서 조회된다. 이 방법은 각각 조회하므로 전송해야할 중복 데이터가 없다.)
  • FETCH 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
  • 이 방법은 페이징이 가능하다.
default_batch_fetch_size 의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장한다.
이 전략을 SQL IN 절을 사용하는데, 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 한다. 1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB에 순간 부하가 증가할 수 있다.
하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다.
1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는 지로 결정하면 된다.

 

 

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


API를 만들 때 엔티티 자체를 리턴하는 방식을 절대로 사용하면 안된다.

기본적으로 엔티티를 DTO로 바꾸어서 리턴해야 한다.

 

또한 트래픽이 낮다면 굳이 성능 최적화가 필요 없겠지만, 사용자가 늘어난다면 성능 최적화를 고려해야한다.

 

성능 최적화 순서

  1. 엔티티를 DTO로 변환하는 방법을 선택
  2. 필요하면 FETCH 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용

 

엔티티를 DTO로 변환

@Data
static class SimpleOrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public SimpleOrderDto(Order order) {
        orderId = order.getId();
        name = order.getMember().getName(); //Lazy 초기화
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress(); //Lazy 초기화
    }
}

/**
 * V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X) * - 단점: 지연로딩으로 쿼리 N번 호출
 */
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    List<SimpleOrderDto> result = orders.stream()
            .map(o -> new SimpleOrderDto(o))
            .collect(toList());
    return result;
}

이 방식의 가장 큰 문제점은 N+1 문제가 발생한다.

즉, DB에 쿼리가 예상했던 것 보다 많이 날라가는 문제가 발생한다.

 

FETCH JOIN으로 성능 최적화

/**
 * V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
 * - fetch join으로 쿼리 1번 호출
 * 참고: fetch join에 대한 자세한 내용은 JPA 기본편 참고(정말 중요함)
 */
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
    List<Order> orders = orderRepository.findAllWithMemberDelivery();
    List<SimpleOrderDto> result = orders.stream()
            .map(o -> new SimpleOrderDto(o))
            .collect(toList());
    return result;
}
//orderRepository
public List<Order> findAllWithMemberDelivery() {
    return em.createQuery(
                    "select o from Order o" +
                            " join fetch o.member m" +
                            " join fetch o.delivery d", Order.class)
            .getResultList();
}

이 방식은 N+1 문제가 해결되지만 필요 없을 수도 있는 컬럼마저 가져온다는 단점이 있다.

 

 

DTO로 직접 조회하는 방법

이 방법은 JPQL을 통해서 원하는 컬럼 정보만 가져오는 쿼리를 작성하고, 작성한 쿼리에 맞는 DTO 객체를 반환하는 방법이다.

성능이 확실히 올라가긴 하지만, 특수한 경우가 아니라면 굳이 이렇게까지 안해도 된다.

/**
 * V4. JPA에서 DTO로 바로 조회
 * - 쿼리 1번 호출
 * - select 절에서 원하는 데이터만 선택해서 조회
 */
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
    return orderSimpleQueryRepository.findOrderDtos();
}
@Data
public class OrderSimpleQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}
@Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
    private final EntityManager em;
    public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                "select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
                        " from Order o" +
                        " join o.member m" +
                        " join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();
    } 
}
  • 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
  • new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
  • SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트워크 용량 최적화(생각보다 미비)
  • 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

'Java Category > JPA' 카테고리의 다른 글

[Spring Data JPA] Spring Data JPA  (0) 2024.08.12
[JPA] 컬렉션 조회 최적화(OneToMany)  (0) 2024.08.11
[JPA] 병합(Merge)과 변경 감지(Dirty Checking)  (0) 2024.08.05
[JPA] JPQL 고급  (0) 2024.08.03
[JPA] JPQL 기본 문법  (0) 2024.07.29

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;

public class sec05_04 {
    public static int solution(String str) {
        Stack<Integer> stack = new Stack<>();
        for (Character c : str.toCharArray()) {
            if(Character.isDigit(c)) stack.push(Character.getNumericValue(c));
            else
            {
                int right = stack.pop();
                int left = stack.pop();
                if(c == '+') stack.push(left + right);
                else if(c == '-') stack.push(left - right);
                else if(c == '*') stack.push(left * right);
                else if(c == '/') stack.push(left / right);
            }
        }
        return stack.pop();
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.println(solution(br.readLine()));
    }
}

 

설명

  • 문자열의 각 문자를 순차적으로 탐색한다.
  • 숫자인 경우: 해당 숫자를 스택에 푸시(push)한다.
  • 연산자인 경우: 스택에서 두 개의 숫자를 팝(pop)하여 꺼낸다.
    -첫 번째로 꺼낸 숫자를 right, 두 번째로 꺼낸 숫자를 left라고 한다.
    -꺼낸 두 숫자에 대해 연산자를 적용한 결과를 다시 스택에 푸시한다.
    예를 들어, c가 +라면 left + right를 스택에 푸시한다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;
import java.util.StringTokenizer;

public class sec05_03 {
    public static int solution(int[][] board, int[] moves)
    {
        int count = 0;
        Stack<Integer> stack = new Stack<>();
        for(int i : moves)
        {
            for(int j = 0; j < board.length; ++j) //y축
            {
                if(board[j][i - 1] != 0)
                {
                    if((!stack.isEmpty()) && (stack.peek() == (board[j][i - 1])))
                    {
                        stack.pop();
                        count += 2;
                    }
                    else stack.push(board[j][i - 1]);

                    board[j][i - 1] = 0;
                    break;
                }
            }
        }

        return count;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        int N = Integer.parseInt(br.readLine());
        int[][] board = new int [N][N];
        for(int i = 0; i < N; ++i)
        {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j < N; ++j) board[i][j] = Integer.parseInt(st.nextToken());
        }

        int M = Integer.parseInt(br.readLine());
        int[] moves = new int[M];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < M; ++i) moves[i] = Integer.parseInt(st.nextToken());

        System.out.println(solution(board, moves));
    }
}

 

설명

  • for(int i : moves) 반복문을 통해 크레인이 moves 배열에 따라 지정된 열에서 인형을 집는 동작을 반복한다.
  • for(int j = 0; j < board.length; ++j) 내부 반복문을 통해 해당 열의 각 행을 위에서부터 순차적으로 검사하여, 가장 위에 있는 인형(board[j][i - 1])을 찾는다.
  • 인형을 찾으면
    스택이 비어 있지 않고, 스택의 맨 위에 있는 인형과 같은 종류라면, 스택에서 인형을 제거하고, count를 2 증가시킨다.
  • 그렇지 않다면, 인형을 스택에 추가한다.
  • 인형을 집어 올린 위치는 board[j][i - 1] = 0;을 통해 0으로 만들어 인형이 없음을 표시한다.
  • 인형을 집었으면, 해당 열의 탐색을 중지하고 다음 moves로 넘어간다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

첫 번째 코드(스택 이용)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;

public class sec05_02 {
    public static String solution(String str)
    {
        StringBuilder sb = new StringBuilder();
        Stack<Character> stack = new Stack<>();
        for(char c : str.toCharArray())
        {
            if(c == ')') while(stack.pop() != '(');
            else stack.push(c);
        }

        for(int i = 0; i < stack.size(); ++i) sb.append(stack.get(i));
        return sb.toString();
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.println(solution(br.readLine()));
    }
}

 

두 번째 코드(스택 이용 X)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class sec05_02 {
    public static String solution(String str)
    {
        StringBuilder sb = new StringBuilder();
        int left = 0;
        for(char c : str.toCharArray())
        {
            if(c == '(') ++left;
            else if(c == ')') --left;
            else if(left == 0) sb.append(c);
        }
        return sb.toString();
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.println(solution(br.readLine()));
    }
}

 

설명

두 번째 코드에 대한 설명

solution 메서드는 문자열에서 괄호로 둘러싸인 부분을 제거하고, 남은 문자열을 반환하는 기능을 한다. 주어진 문자열에서 괄호 내부의 문자는 무시하고, 괄호 외부의 문자들만을 추출하여 결과 문자열을 만든다.

  • int left = 0; -> 여는 괄호의 수를 추적하기 위한 변수 left를 초기화한다.
  • for(char c : str.toCharArray()) ->문자열 str을 문자 배열로 변환하고, 각 문자를 순차적으로 확인한다.
  • if(c == ‘(’) ++left; -> 현재 문자가 여는 괄호 ’(’인 경우, left를 1 증가시킨다.
  • else if(c == ‘)’) –left; -> 현재 문자가 닫는 괄호 ’)’인 경우, left를 1 감소시킨다.
  • else if(left == 0) sb.append(c); -> 현재 문자가 여는 괄호도 닫는 괄호도 아닌 경우, 그리고 left가 0일 때만 sb에 현재 문자를 추가한다. left가 0인 경우는 현재 문자가 괄호 밖에 있다는 의미이다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class sec05_01 {
    public static String solution(String str) {
        int left = 0;
        for(int i = 0; i < str.length(); ++i)
        {
            if(str.charAt(i) == '(') ++left;
            else
            {
                --left;
                if(left < 0) return "NO";
            }
        }
        return (left == 0) ? "YES" : "NO";
    }


    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.println(solution(br.readLine()));
    }
}

 

설명

주어진 문자열 str의 각 문자를 순회하면서 열린 괄호 ’(’의 개수를 카운트하고, 닫힌 괄호 ’)’를 만나면 카운트를 감소시킨다. 이때, 닫힌 괄호가 열린 괄호보다 먼저 나오는 경우(즉, left < 0이 되는 경우) “NO”를 반환한다. 최종적으로, 열린 괄호와 닫힌 괄호의 개수가 일치하는지 (left == 0) 확인하여 일치하면 “YES”를 반환하고, 그렇지 않으면 “NO”를 반환한다.

  • 변수 left: 열린 괄호 ’(’의 수를 카운트하는 변수이다. 닫힌 괄호 ’)’를 만나면 이 값을 감소시킨다.
  • 문자 순회: 문자열의 각 문자를 순회하며, 열린 괄호를 만나면 left를 증가시키고, 닫힌 괄호를 만나면 left를 감소시킨다.
  • left < 0 체크: 닫힌 괄호가 열린 괄호보다 많아지면, “NO”를 반환한다. 이는 올바른 괄호 문자열이 아니기 때문이다.
  • 최종 결과: 모든 문자열을 순회한 후, left가 0이면 모든 괄호가 짝을 이루는 것이므로 “YES”를 반환하고, 그렇지 않으면 “NO”를 반환한다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


JPA(Java Persistence API)에서 변경 감지(Dirty Checking)와 병합(Merge)은 엔티티 상태 관리를 위한 중요한 개념이다. 이 두 가지는 데이터베이스와 애플리케이션 간의 동기화를 효율적으로 관리하는 데 사용된다.

 

결론부터 말하면 두 방법중에 변경 감지를 사용하는 것이 더 좋다.

 

병합

병합(Merge)은 JPA에서 준영속 상태의 엔티티를 영속성 컨텍스트에 포함시키고, 해당 엔티티의 상태를 데이터베이스에 반영하는 작업이다.

이 과정은 데이터베이스에 이미 저장된 데이터를 수정할 때 사용된다.

@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
    Book book = new Book();
    book.setId(form.getId());
    book.setName(form.getName());
    book.setPrice(form.getPrice());
    book.setStockQuantity(form.getStockQuantity());
    book.setAuthor(form.getAuthor());
    book.setIsbn(form.getIsbn());
    itemService.saveItem(book);
    return "redirect:/items";
}

Book 객체는 Item의 자식 객체임

 

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;
    private final EntityManager em;

    @Transactional
    public void saveItem(Item item) {
        itemRepository.save(item);
    }
}

 

@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;

public void save(Item item) {
    if (item.getId() == null) {
        em.persist(item);
    } else {
        //파리미터로 넘어온 객체는 준영속 상태의 엔티티
        em.merge(item); //병합
    }
}

마지막 코드의 save 메서드는 엔티티의 id 값을 기준으로 새로운 엔티티인지 기존 엔티티인지를 판단하여 각각 persist 또는 merge를 사용한다.

 

->item.getId() == null일 경우 (persist)

id가 null인 경우, 이는 새로 생성된 엔티티라는 의미이므로, JPA의 persist 메서드를 사용하여 새로운 엔티티를 영속성 컨텍스트에 추가하고 데이터베이스에 삽입한다.

이 때, persist는 영속성 컨텍스트에서 관리되지 않는 새로운 엔티티를 영속 상태로 만든다.

 

->item.getId() != null일 경우 (merge)

id가 null이 아닌 경우, 이는 이미 데이터베이스에 저장된 엔티티가 수정되었음을 의미한다.

즉, 기존에 id를 저장하고 있고 영속성 컨텍스트에서 관리되는 객체(A)가 존재했는데, 다른 새로운 객체가 동일한 id를 갖고 있는 객체(B)가 있는 것이다.

이때 B 객체는 수정된 엔티티이고, 영속성 컨텍스트에서 관리되지 않는 준영속 상태의 객체이다.

->기존에 A상태였는데 B 상태로 변경하고 싶은 상황인 것이다.

 

그러한 준영속 상태의 객체 item을 merge() 메서드를 이용하여 병합하면 기존에 영속 상태이던 객체(A)를 찾고, A에 저장하고 있던 값을 모두 NULL로 만든다. 이후 준영속 상태였던 객체(B)의 값을 A에 병합(값 채우기) 한다.

Merge 개념 중간 정리

-영속성 컨텍스트에서 관리되는 기존 객체(A)가 존재하는 상황에서, 동일한 ID를 가진 수정된 엔티티인 새로운 객체(B)가 있을 수 있다.
-이때 B 객체는 준영속 상태의 객체로, 영속성 컨텍스트가 관리하지 않는다.
-병합을 통해 B 객체의 상태를 영속성 컨텍스트에 복사하여, 기존의 영속 상태 객체(A)에 반영하고, 이를 통해 데이터베이스와 동기화한다. 병합된 후의 객체는 영속성 컨텍스트에서 관리되며, 이후 변경 사항이 자동으로 데이터베이스에 반영된다.

마지막으로 merge() 메서드로 반환된 객체는 영속상태이다. 왜냐하면 기존 영속 상태의 객체를 수정한 것이니까.(B는 여전히 준영속 상태)

 

이때 주의해야할 점이있다.

병합은 변경된 부분 외에 다른 부분도 데이터베이스에 반영한다.

 

기존에 영속 상태이던 객체(A)를 찾고, A에 저장하고 있던 값을 모두 NULL로 만들고 준영속상태였던 객체(B)의 값을 A에 병합(값 채우기)하기 때문에, 예를 들어 A에는 5개의 값이 있고, B에는 3개의 값이 있다면 2개의 값은 NULL로 변경된다.

 

자세한 예시를 살펴보자

아래의 코드는 위에 있던 코드이다.

@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
    Book book = new Book();
    book.setId(form.getId());
    book.setName(form.getName());
    book.setPrice(form.getPrice());
    book.setStockQuantity(form.getStockQuantity());
    book.setAuthor(form.getAuthor());
    book.setIsbn(form.getIsbn());
    itemService.saveItem(book);
    return "redirect:/items";
}

여기서 book의 set 메서드를 하나라도 누락한다면 그 필드의 값은 A에 병합되지 않는다.

즉, 병합은 변경된 부분 외에 다른 부분도 데이터베이스에 반영한다.

 

변경 감지

변경 감지는 JPA의 EntityManager가 관리하는 영속성 컨텍스트 내의 엔티티가 변경되었는지 자동으로 감지하는 기능이다.

영속성 컨텍스트에 속한 엔티티는 영속 상태에 있으며, 이 상태의 엔티티는 자동으로 변경 감지 대상이 된다.

@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {
    itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
    return "redirect:/items";
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;
    private final EntityManager em;
    
    //...

    @Transactional
    public void updateItem(Long id, String name, int price, int stockQuantity) {
        Item item = itemRepository.findOne(id);
        item.setName(name);
        item.setPrice(price);
        item.setStockQuantity(stockQuantity);
    }
}
@Repository
@RequiredArgsConstructor
public class ItemRepository {
    private final EntityManager em;

    //...

    public Item findOne(Long id) {
        return em.find(Item.class, id);
    }
}

ItemService의 updateItem 메서드

@Transactional로 지정되어 있어 메서드 내에서 트랜잭션이 관리된다.

  • itemRepository.findOne(id)
    주어진 id로 Item 엔티티를 데이터베이스에서 조회한다. 반환된 item 객체는 영속 상태에 있으며, 영속성 컨텍스트에서 관리된다.

  • 엔티티 필드 변경
    item.setName(name), item.setPrice(price), item.setStockQuantity(stockQuantity)를 호출하여 엔티티의 필드를 변경한다.
    이 변경은 영속성 컨텍스트에 의해 추적되며, 트랜잭션이 커밋될 때 변경 감지 메커니즘에 의해 자동으로 데이터베이스에 반영된다.
JPA의 변경 감지 메커니즘

JPA의 변경 감지 메커니즘은 영속 상태의 엔티티가 트랜잭션 범위 내에서 변경될 때, 변경된 부분을 자동으로 감지하고 데이터베이스에 반영하는 기능이다. 이는 주로 다음과 같은 단계를 통해 이루어진다.

1. 영속 상태의 엔티티가 조회될 때, 스냅샷이 생성되어 초기 상태가 저장된다.
2. 엔티티의 필드가 변경되면, 트랜잭션 커밋 시점에 JPA가 스냅샷과 현재 상태를 비교하여 변경된 부분을 감지한다.
3. 변경된 부분이 있다면, 해당 필드에 대해 SQL UPDATE 쿼리를 생성하고 실행하여 데이터베이스에 반영한다.

 

즉, 파라미터로 넘어온 객체의 id를 조회해서 영속 상태인 객체를 찾아내고, 영속 상태인 객체의 값을 변경해서 DB의 내용을 수정하는 방법이다.

 

병합과 달리 변경된 부분만 데이터베이스에 반영되므로 성능이 좋다.

반대로 말하면 병합은 변경된 부분 외에 다른 부분도 데이터베이스에 반영된다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

package inflearn_algorithm.chapter4;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class sec04_05 {
    public static int solution(int N, int K , Integer[] arr) {
        int count = 1;
        TreeSet<Integer> reverseTreeSet = new TreeSet<>(Comparator.reverseOrder());
        for(int i = 0 ; i < N - 2 ; ++i)
        {
            for(int j = i + 1 ; j < N - 1 ; ++j)
            {
                for(int k = j + 1 ; k < N ; ++k) reverseTreeSet.add(arr[i] + arr[j] + arr[k]);
            }
        }

        for(Integer i : reverseTreeSet) if(count++ == K) return i;
        return -1;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int K = Integer.parseInt(st.nextToken());
        Integer[] arr = new Integer[N];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < N; i++) arr[i] = Integer.parseInt(st.nextToken());
        System.out.println(solution(N, K, arr));
    }
}

 

 

 

설명

  • TreeSet<Integer> reverseTreeSet = new TreeSet<>(Comparator.reverseOrder());를 사용하여 내림차순으로 정렬된 트리셋을 생성한다. 트리셋은 중복된 값을 허용하지 않으며, 자동으로 정렬된 상태를 유지한다.

  • 중첩된 for 루프를 사용하여 배열에서 가능한 모든 세 숫자의 조합을 순회한다.
    arr[i] + arr[j] + arr[k]를 계산하여 그 합을 트리셋에 추가한다. 이는 i, j, k가 서로 다른 인덱스여야 한다. 트리셋에 추가되면서 자동으로 중복된 합이 제거되고, 내림차순으로 정렬된다.

  • 트리셋의 요소들을 순회하면서, count 변수를 증가시킨다. count가 K와 같아지는 순간 해당 값을 반환한다.
    만약 K번째로 큰 값을 찾지 못하면 -1을 반환한다.