Home > Today I Learned > 2026_04_20~2026_04_27 > 2026-04-21 정리본

2026-04-21 정리본
jdbc mybatis java database

MyBatis DynamicQuery 구현


MyBatis if문 구현하기

 <select id="search" resultType="domain.Product">
        SELECT
        id,
        name,
        price,
        stock
        FROM
        product
        WHERE 1=1
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test = "minPrice != null and minPrice > 0">
            AND price &gt;= #{minPrice}} 
        </if>
        <if test = "maxPrice != null and maxPrice > 0">
            AND price &lt;= #{maxPrice}}
        </if>

여기서 부등호 >는 앵글브라켓일 수도 있고 부등호가 될 수가 있는데 컴퓨터는 이를 비교해주기가 힘들다.
그래서 크고 작고를 사용할 것이면 대체 식을 사용해야 한다,
escaping : 위혐이 될만한 문자열을 다른 문자열로 대체하는 것

왼쪽이 오른쪽보다 큰,작은 부등호 표시 &gt, &lt
그럼 && 와 같은 and 연산자와 헷갈릴 수 있기 때문에 이에 관한 escaping 문자가 필요하다
&amp는 & 부등호와 같이 and 연산자를 표시해주는 기능을 수행한다.

MyBatis for문 구현하기

<select id="findByIds" resultType="domain.Product">
    SELECT
        <include refid="productColumns"/>
    FROM
    product
    <where>
        <choose>
            <when test="ids != null and ids.size() > 0">
                id IN
                <foreach collection="ids" item="id" open="(" separator="," close=")">
                    #{id}
                </foreach>
            </when>

            <otherwise>
                1 = 0
            </otherwise>
        </choose>
    </where>
</select>

여기서 은 MyBatis의 동적 기능을 수행하는 것으로 조건있을 때 AND, OR 정리를 하는 기능이다. 그리고 는 java의 swtich - case와 유사하다고 보면 될 것 같다. 다음과 같은 정렬 기능을 구사하는 경우 enum을 활용해서 하면 좋을 것 같다.

<select id="findByIds" resultType="domain.Product">
        SELECT
            <include refid="productColumns"/>
        FROM
        product
        <where>
            <choose>
                <when test="ids != null and ids.size() > 0">
                    id IN
                    <foreach collection="ids" item="id" open="(" separator="," close=")">
                        #{id}
                    </foreach>
                </when>

                <otherwise>
                    1 = 0
                </otherwise>
            </choose>
        </where>
    </select>


구현 팁 : 그냥 어노테이션을 활용해 java파일에서 구현하고 구현하기 까다로운 코드를 xml파일에 작성

 @Select("""
    SELECT 
        id,
        name,
        stock
    FROM
        product
    where 
        id = #{id}
    """)
    Optional<Product> findById(@Param("id") long id);

간단한 select구문을 사용하는 경우 그냥 xml파일이 아닌 java파일에 어노테이션을 통해 정의를 해도 상관없다.

Logback과 Log이야기


Logback이란?

java에서 자주 사용하는 로깅 구현체 중 하나이다. 비동기 로깅 지원, 내부 최적화, garbage collector 부담이 적어 대용량 트래픽 서버에서도 사용하기도 한다.

자주쓰이는 Logback.xml 설정파일

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="mapper" level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>

    <logger name="java.sql.ResultSet" level="DEBUG" />

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>

</configuration>

Log에 관해서


LOG에 관련된 정보를 절대절대 노출되지 않게 할 것

실제 개발에 있어서 배포를 할 때 로그 레벨을 debug로 낮추어서 배포를 하는 것은 안된다., Info로 급을 올려서 보여지도록 해야 한다

Logback에서의 로그의 분류


  1. ERROR
  2. WARN
  3. INFO
  4. Debug
  5. TRACE

INFO가 기본으로 설정으로 되어있는 것이고 debug나 trace는 임의로 낮춰야지 볼 수가 있다.

WARN은 문제가 될 수 있으나 넘어가도 되는 경우가 있다. 다만 심각한 문제가 될 수가 있으니 살펴보는 것은 좋다. → 논리적으로는 오류이지만 실제로는 오류가 아닌 경우도 있다.

ERROR : 오류 발생, spring이 종료 될 수가 있다.

FATAL: Error보다 심각한 문제로 실행이 되지 않는다.

참고사항


페이징


myBatis에서 사용하는 페이징은 많은 데이터 중에 일부만 잘라서 조회하는 것을 의미한다.

    try (SqlSession session = connector.openSession()) {

                MemberMapper mapper = session.getMapper(MemberMapper.class);

                List<MemberDto> members = mapper.findAll();
                MemberDto findMember = members.getLast();
                String memberName = findMember.getMemberName();
                System.out.println("memberName = " + memberName);

커서기반 페이징이라는 것이 있는데 이는 OFFSET대신 마지막 값을 기준으로 조회를 하는 것이다.

.class 활용


try (SqlSession session = connector.openSession()) {
            MemberMapper memberMapper = session.getMapper(MemberMapper.class);

mybatis가 .class를 보고 MapperProxy로 생성 및 사용하여 어떤 mapper임을 알 수가 있게 하고 메서드의 정보를 분석하는 것이 가능하다.
다른 말로 하면 .class는 이 인터페이스를 기반으로 객체를 만들어줘라는 신호라고 볼 수가 있다.

myBatis에서 map 자료구조형을 사용하는 이유


map은 유연한 동적처리를 위해 사용한다. 먼저 테이블의 column이 50개가 넘어가는 데이터의 경우 사용하는 메서드의 파라미터 개수가 너무 많아지고 순서가 헷갈리고 유지보수가 어려워지는 단점이 존재한다.
하지만 mapKey를 변수처럼 활용하면 동적 SQL과 잘맞고 바뀌는 검색 조건에도 유연하게 대응도 가능하다.

<select id="search" parameterType="map" resultType="domain.Product">
        SELECT
            id,
            name,
            price,
            stock
        FROM
            product
        WHERE 1=1
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>

DTO (Data Transfer Object) 데이터 전송 객체


데이터를 전송하기 위한 객체를 의미한다.
회원정보의 경우 비밀번호와 같이 개인정보와 직결되는 정보를 보호하고 필요한 데이터 전송을 위해 사용한다. 그리고 데이터 정보를 변경 시 생기는 유지보수 과정을 줄여줌에도 이점을 가지고 있다.
데이터의 Entity가 바뀌어도 그것을 사용하는 API는 깨지지 않는다.
controller와 service 사이에서 구현되고 myBatis에서는 DB에서 DTO로 바로 매핑하는 것이 가능하다. 이때 map자료구조형을 활용하여

참고사항


정보를 넘겨줄 때 한 데이터에 관한 컬럼을 전부 넘기는 것보다 목적에 맞는 한개씩만을 넘겨주려고 하는 것이 좋다. 그리고 만약 고객사에서 개인정보를 보내주는 프로그램을 요청할 경우에는 선별을 할 필요가 있다. 그리고 왠만하면 안하는게 좋다.

사용하면 좋은 단축키


원하는 static 메서드를 블록하고 f6번을 누르면 그것을 추출해서 다른 클래스에 생성하는 것이 가능하다.

n+1이 나타나는 이유


위 1번 코드를 통해 나오는 결과는 1개이지만 실제 터미널에서 행해진 쿼리를 봤을 때 1 + n번이 발생한 것으로 볼 수가 있다. member을 null로 두지 말고 가져오라고 시켰기 때문에 발생하게 된다. order과 연관된 member을 가져오는데 join 연산을 하지 않아 발생하는 문제라고 볼 수가 있다.

지금은 약 100개의 row에 대해 실행해서 문제가 없었지만 실무에서는 이것보다 훨씬 데이터가 커져 문제가 크게 될 수가 있다.

이러한 n+1문제와 관련되서 lazy loading이 있는데 lazy loading은 중요도가 떨어지거나 당장 화면에 보이지 않는 요소들의 로딩을 우선적으로 시행하지 않음으로서 웹페이지 로딩 기술을 향상시키는 방법이다. 이러한 lazy loading은 프록시 객체를 사용하여 프록시 객체가 실제 값에 접근시 실체화를 하는데 이는 프록시 객체를 여러번 접근할 수 있기 때문에 n+1 문제를 해결하는데 좋은 방법이 아니다.

다른 해결방안으로는 fetch join이 있는데 fetch join은 연관된 엔티티를 join을 해서 한 번의 쿼리로 함께 가져오는 방법이다.

예를 들어

    select o from Order o join fetch o.member

위 쿼리처럼 order가 참조하는 member도 같이 채워서 가져오는 방법이다. 이게 왜 n+1문제를 해결하는 방법이 되냐면 fetch join은 연관 객체를 처음부터 같이 조회를 하여 추가 조회를 하지 않아도 된다.

그렇다면 lazy는 안좋은 방법인가?

여전히 lazy는 최적화를 위해 필요한 방법이다. 따라서 기본 설계는 Lazy로 두고 필요한 조회에서 fetch join으로 최적화를 하여 n+1문제를 피하는 방향으로 설계를 두는 편이 좋을 것 같다. 다만 fetch join은 연관객체를 항상 같이 들고오므로 들고오는 데이터 양이 많아질 수가 있다. 그래서 최적화가 필요하고 정확도를 중시하는 분야의 몇몇 기업들은 sql mapper을 사용하여 쿼리 튜닝을 해서 n+1문제 발생을 방지한다.