본문 바로가기
GD's IT Lectures : 기초부터 시리즈/마이바티스(MyBatis) 기초부터 ~

[마이바티스(MyBatis)] 마무리 (주의사항 및 성능 최적화)

by GDNGY 2023. 5. 16.

Chapter 10. 마무리 (주의사항 및 성능 최적화)

마이바티스의 기본적인 개념부터 실제 활용 방법까지 다양한 내용을 알아보았으며. 이 장에서는 마이바티스를 사용하며 주의해야 할 사항과 성능 최적화 방법에 대해 알아보겠습니다.

 


 

반응형

 


Chapter 10. 마무리 (주의사항 및 성능 최적화)

 

10.1. 마이바티스 사용 시 주의사항

10.1.1. N+1 문제

10.1.1.1. N+1 문제의 정의

10.1.1.2. N+1 문제 해결 방법

10.1.2. 커넥션 누수

10.1.2.1. 커넥션 누수의 원인

10.1.2.2. 커넥션 누수 방지 방법

 

10.2. 성능 최적화

10.2.1. 캐싱 방법

10.2.1.1. 캐싱의 중요성

10.2.1.2. 마이바티스에서의 캐싱 방법

10.2.2. 쿼리 최적화

10.2.2.1. 쿼리 최적화의 필요성

10.2.2.2. 쿼리 최적화 방법

 


10.1. 마이바티스 사용 시 주의사항

마이바티스를 사용하면서 알아두어야 할 몇 가지 주의사항에 대해 알아봅시다. 이 중에서도 특히 N+1 문제와 커넥션 누수는 개발자들이 직면하는 일반적인 문제입니다.

 


10.1.1. N+1 문제

N+1 문제는 ORM(Object-Relational Mapping)에서 흔히 발생하는 문제입니다. 이를 이해하려면 먼저 N+1 문제의 정의를 알아야 합니다.

 

10.1.1.1. N+1 문제의 정의

N+1이란 이름에서 알 수 있듯이, 데이터를 조회하는 데 필요한 쿼리의 수가 기대보다 훨씬 많아지는 문제를 말합니다. 이 문제가 발생하면, 한 번의 쿼리로 충분한 데이터를 가져오는 대신, 불필요하게 많은 수의 쿼리가 데이터베이스에 전송되므로 성능이 저하됩니다.

 

이 문제는 다음과 같은 상황에서 흔히 발생합니다. 먼저, 한 번의 쿼리로 여러 개의 행을 가져옵니다. 이렇게 가져온 각 행에 대해, 추가 정보를 가져오기 위해 또 다른 쿼리를 실행합니다. 따라서 총 쿼리 수는 1(초기 쿼리) + N(추가 쿼리)가 되어 N+1 문제가 발생합니다.

 

예를 들어, 여러 사용자를 선택하고 각 사용자의 주문을 선택하는 상황을 생각해 보세요. 여기서 N+1 문제가 발생할 수 있습니다.

 

[예제]

// 사용자를 가져옵니다.
List<User> users = userMapper.selectAllUsers();

for (User user : users) {
    // 사용자의 모든 주문을 가져옵니다.
    List<Order> orders = orderMapper.selectOrdersByUserId(user.getId());
    user.setOrders(orders);
}

 

이 코드는 사용자 수만큼 주문을 가져오는 쿼리를 추가로 실행하므로 N+1 문제를 일으킵니다.

 

10.1.1.2. N+1 문제 해결 방법

이 문제를 해결하기 위한 가장 일반적인 방법 중 하나는 "조인(JOIN)"을 사용하여 필요한 모든 데이터를 한 번에 가져오는 것입니다. 아래는 조인을 사용하여 사용자와 주문을 한 번에 가져오는 MyBatis의 매퍼 인터페이스입니다.

 

[예제]

public interface UserMapper {
    @Select("SELECT u.id as userId, u.name as userName, o.id as orderId, o.total as orderTotal "
        + "FROM User u LEFT JOIN Order o ON u.id = o.user_id")
    @Results({
        @Result(property = "id", column = "userId"),
        @Result(property = "name", column = "userName"),
        @Result(property = "orders.id", column = "orderId"),
        @Result(property = "orders.total", column = "orderTotal")
    })
    List<User> selectAllUsersWithOrders();
}


이 코드는 User 테이블과 Order 테이블을 조인하여 모든 사용자와 그들의 주문을 한 번에 가져옵니다. @Results 어노테이션을 사용하여 결과 칼럼과 매핑할 도메인 모델의 프로퍼티를 지정합니다. 이렇게 하면 MyBatis가 한 번의 쿼리로 모든 데이터를 가져오고, 각 사용자와 해당 사용자의 주문을 올바르게 매핑할 수 있습니다. 이 방법으로 N+1 문제를 해결하고 애플리케이션의 성능을 향상할 수 있습니다.

 

XML 매퍼를 사용하여 N+1 문제를 해결하는 방법도 있습니다. MyBatis는 collection 요소를 제공하여 조인 결과를 쉽게 매핑할 수 있게 해줍니다. collection 요소를 사용하면, 한 테이블의 행과 다른 테이블의 여러 행을 쉽게 연관시킬 수 있습니다.

 

이러한 접근 방식을 사용하면, 사용자와 그들의 주문을 한 번의 쿼리로 가져올 수 있습니다. 먼저, 사용자와 주문을 조인하는 SQL 쿼리를 작성합니다. 그런 다음, collection 요소를 사용하여 주문 결과를 사용자 객체에 매핑합니다.

 

다음은 이를 달성하는 방법의 예입니다.

 

[예제]

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <resultMap id="UserOrderMap" type="User">
        <id property="id" column="user_id"/>
        <result property="name" column="user_name"/>
        <collection property="orders" ofType="Order">
            <id property="id" column="order_id"/>
            <result property="total" column="order_total"/>
        </collection>
    </resultMap>

    <select id="selectAllUsersWithOrders" resultMap="UserOrderMap">
        SELECT u.id as user_id, u.name as user_name,
               o.id as order_id, o.total as order_total
        FROM User u
        LEFT JOIN Order o ON u.id = o.user_id
    </select>
</mapper>

 

이 XML 매퍼 파일에서는 resultMap 요소를 사용하여 결과 매핑을 정의합니다. collection 요소는 사용자와 관련된 주문 객체를 만드는 데 사용되며, 이는 MyBatis에게 주문 정보를 사용자 객체의 orders 프로퍼티에 매핑하도록 지시합니다.

 

이 방법으로, N+1 문제 없이 사용자와 그들의 주문 정보를 효율적으로 조회할 수 있습니다.

 

10.1.2. 커넥션 누수

데이터베이스 커넥션은 소중한 자원입니다. 따라서 사용 후에는 반드시 닫아주어야 합니다. 그렇지 않으면 커넥션 누수가 발생하게 되며, 이는 시스템 전체의 성능 저하를 초래합니다.

 

10.1.2.1. 커넥션 누수의 원인

데이터베이스 커넥션은 사용 후 반드시 닫아야 하는데, 이를 잘못 관리하면 커넥션이 제대로 닫히지 않아 커넥션 풀에 반환되지 않는 상황이 발생합니다. 이렇게 되면 커넥션 풀이 고갈되어 애플리케이션은 더 이상 새로운 데이터베이스 커넥션을 생성할 수 없게 됩니다.

 

자바에서 SQLException이 발생하면, 커넥션이 닫히지 않고 그대로 남을 수 있습니다. 또한, 커넥션을 닫는 코드가 잘못된 위치에 있거나, 아예 없을 경우에도 커넥션 누수가 발생합니다.

 

10.1.2.2. 커넥션 누수 방지 방법

스프링 프레임워크에서는 @Transactional 어노테이션을 사용하여 트랜잭션을 관리합니다. @Transactional 어노테이션은 선언적 트랜잭션을 사용하여 개발자가 코드로 직접 트랜잭션을 관리하는 것이 아니라, 스프링 프레임워크에게 트랜잭션의 시작과 종료, 그리고 예외 처리를 맡깁니다. 이렇게 하면, 개발자는 비즈니스 로직에만 집중할 수 있습니다.

 

@Transactional 어노테이션은 메서드 레벨과 클래스 레벨에서 모두 사용할 수 있습니다. 메서드 레벨에서 사용하면 해당 메서드 내에서 발생하는 모든 데이터베이스 작업이 하나의 트랜잭션으로 처리됩니다. 클래스 레벨에서 사용하면 해당 클래스의 모든 public 메서드가 각각 하나의 트랜잭션으로 처리됩니다.

[예제]

@Service
public class UserService {

  @Autowired
  private UserMapper userMapper;

  @Transactional
  public void updateUserName(int userId, String newName) {
    User user = userMapper.selectUser(userId);
    user.setName(newName);
    userMapper.updateUser(user);
  }
}

 

이 예제에서는 updateUserName 메서드가 @Transactional 어노테이션이 붙은 메서드입니다. 따라서 이 메서드에서 발생하는 모든 데이터베이스 작업은 하나의 트랜잭션으로 처리되며, 메서드가 성공적으로 종료하면 트랜잭션은 자동으로 커밋됩니다. 만약 메서드 실행 중에 예외가 발생하면, 트랜잭션은 자동으로 롤백됩니다.

 

@Transactional 어노테이션을 사용하면, 스프링 프레임워크가 커넥션을 효율적으로 관리하고 커넥션 누수를 방지해줍니다.


10.2. 성능 최적화

성능 최적화는 어떤 시스템에서든 중요한 고려사항입니다. 이번 섹션에서는 캐싱과 쿼리 최적화 두 가지 방법에 대해 알아보겠습니다.

 


10.2.1. 캐싱 방법

캐싱은 데이터를 빠르게 접근할 수 있도록 일시적으로 저장하는 기술입니다. 캐싱을 사용하면 데이터베이스에 직접 접근하는 비용을 줄일 수 있습니다.

 

10.2.1.1 캐싱의 중요성

데이터베이스 캐싱은 데이터베이스 쿼리의 결과를 메모리에 저장하는 것입니다. 이는 반복적인 데이터베이스 쿼리에 의한 성능 저하를 방지하기 위해 사용됩니다. 예를 들어, 같은 쿼리가 반복적으로 수행되면, 첫 번째 쿼리의 결과가 캐시에 저장되고, 동일한 쿼리가 나중에 다시 요청되면 데이터베이스에 다시 접근하지 않고 캐시 된 결과를 반환합니다. 이로 인해 데이터베이스에 대한 부하를 줄이고 애플리케이션의 전반적인 성능을 향상합니다.

 

10.2.1.2 마이바티스에서의 캐싱 방법

마이바티스는 기본적으로 세션 수준의 캐시인 1차 캐시를 제공합니다. 1차 캐시는 SQL 세션마다 별도로 생성되며, 같은 세션 내에서 동일한 쿼리를 다시 실행하면 캐시에서 결과를 가져와 성능을 향상시킵니다. 이는 자동으로 활성화되므로 별도로 설정할 필요가 없습니다.

 

하지만 이러한 1차 캐시는 세션마다 독립적이기 때문에, 여러 세션에서 동일한 쿼리를 수행하더라도 각각 쿼리를 수행하게 됩니다. 이런 상황에서는 2차 캐시가 필요합니다. 2차 캐시는 여러 세션 간에 공유되는 캐시로, 이를 설정하면 세션 간에 쿼리 결과를 공유하여 성능을 더욱 향상할 수 있습니다.

 

마이바티스에서 2차 캐시를 사용하려면 mapper XML에 <cache /> 요소를 추가하면 됩니다.

<mapper namespace="com.example.UserMapper">
    <cache />
    ...
</mapper>

 

이렇게 설정하면 해당 매퍼의 쿼리 결과가 2차 캐시에 저장되고, 동일한 쿼리가 다른 세션에서 실행되더라도 캐시 된 결과를 사용하게 됩니다.

 

10.2.2. 쿼리 최적화

데이터베이스 쿼리는 웹 애플리케이션의 성능을 크게 좌우합니다. 복잡하고 비효율적인 쿼리는 애플리케이션 성능을 저하시키고 사용자 경험을 떨어뜨릴 수 있습니다. 이러한 문제를 해결하기 위해 쿼리 최적화가 필요합니다.

 

10.2.2.1. 쿼리 최적화의 필요성

데이터베이스 쿼리의 성능은 애플리케이션의 전반적인 성능에 큰 영향을 미칩니다. 비효율적인 쿼리는 시스템의 전체적인 성능을 저하시킬 수 있으며, 이는 사용자 경험을 저하시키고, 비용 증가와 같은 문제를 일으킬 수 있습니다. 따라서 데이터베이스 쿼리를 최적화하는 것은 매우 중요하며, 이를 위해 마이바티스와 같은 툴을 사용하여 쿼리를 효율적으로 만들고 관리하는 것이 중요합니다.

 

10.2.2.2. 쿼리 최적화 방법

쿼리 최적화는 복잡한 주제로, 많은 방법이 있습니다. 하지만 몇 가지 기본적인 접근법을 소개하겠습니다.

  • 인덱싱: 인덱스는 데이터베이스 테이블의 열에 대한 포인터를 제공합니다. 인덱스를 사용하면 데이터베이스 엔진이 테이블의 모든 행을 검색하지 않고도 데이터를 찾을 수 있습니다. 따라서 데이터베이스에서 데이터를 빠르게 검색하는 데 도움이 됩니다.
  • Join 최적화: Join 연산은 두 개 이상의 테이블을 결합하는 데 사용됩니다. 이 작업은 계산 비용이 많이 들 수 있으므로 효율적으로 수행되어야 합니다. 가능한 한 Join의 수를 줄이고, 필요한 경우 인덱스를 사용하는 것이 좋습니다.
  • 서브쿼리 최적화: 서브쿼리는 한 쿼리 내부에서 다른 쿼리를 실행하는 것입니다. 서브쿼리는 때때로 불필요하게 복잡한 쿼리를 만들 수 있습니다. 가능하면 Join을 사용하여 서브쿼리를 피하는 것이 좋습니다.

여기서는 SQL 작성 방법에 초점을 맞추어 설명하겠습니다.

 

마이바티스에서는 동적 SQL을 지원하여 쿼리를 더욱 효율적으로 작성할 수 있습니다. 동적 SQL을 이용하면 조건에 따라 다른 SQL을 실행할 수 있으므로, 불필요한 쿼리 실행을 피하고 성능을 향상할 수 있습니다.

 

예를 들어, 특정 조건에 따라 다른 WHERE 절을 포함해야 하는 경우, 동적 SQL을 사용하여 이를 구현할 수 있습니다. 아래는 마이바티스의 <if /> 태그를 사용하여 동적 SQL을 작성하는 예입니다.

 

[예제]

<select id="selectUsers" resultType="User">
    SELECT * FROM users
    <where>
        <if test="name != null">
            name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

 

위 예제에서는 name과 age 파라미터가 null이 아닌 경우에만 해당 조건절이 SQL에 포함됩니다. 이를 통해 불필요한 쿼리 조건을 피하고, 쿼리의 효율성을 향상할 수 있습니다.

 

마지막으로, 성능을 최적화하는 것은 중요하지만, 먼저 애플리케이션의 기능과 정확성을 확보해야 합니다. 그다음에 성능 최적화를 진행하는 것이 좋습니다.

반응형

댓글