04.25 학습

한강섭·2025년 4월 25일
0

학습 & 숙제

목록 보기
75/103
post-thumbnail

🔄 MyBatis 동적 SQL과 서비스 계층 트랜잭션 처리


🔹 MyBatis 동적 SQL


#{} VS ${}

  • #{}: PreparedStatement의 ? 를 대체 → 값에 대한 대체로 SQL문장을 구성하지는 않음
  • ${}: 문장 자체에 대한 재구성 가능 → Statement 사용
public List<Address> queryStructureChange(Map<String, Object> param);

<select id="queryStructureChange" resultMap="addressMap">
    select * from address where ${key}=#{word}
</select>
@Test
public void queryStructureChangeTest() {
    List<Address> address = aDao.queryStructureChange(Map.of("key", "title", "word", "7|="));
    Assertions.assertEquals(1, address.size());

    address = aDao.queryStructureChange(Map.of("key", "mno", "word", "100"));
    Assertions.assertEquals(0, address.size());

    address = aDao.queryStructureChange(Map.of("key", "1=1 or mno", "word", "100"));
    Assertions.assertEquals(2, address.size());
}

⚠️ 테스트 결과 1=1 or (SQL Injection) 공격에 취약!

🧩 동적 SQL

상황에 따라 분기 처리를 통해 SQL을 동적으로 만드는 것

  • JAVA였다면 기본 문법(조건문, 반복문)에 따라 다르겠지만 XML에서는 별도의 표기법 필요
  • OGNL (Object Graph Navigation Language) 사용
  • 파라미터로 들어온 객체 또는 Map의 속성을 이용해서 값 비교

🔍 if

가장 많이 사용되는 태그로 주로 조건절에 따라 where 문장을 구성할 때 사용

<select id="dynamicQueryIf" resultMap= "addressMap">
    select * from address
    <if test="title != null">
        where title = #{title}
    </if>
    <if test="mno != null">
        where mno=#{mno}
    </if>
</select>

where가 두개? 어떻게 해결할까?

<select id="dynamicQueryIf" resultMap="addressMap">
    select * from address where 1=1
    <if test="title != null">
        and title = #{title}
    </if>
    <if test="mno != null">
        and mno=#{mno}
    </if>
</select>

오! 해결되긴 했는데 뭔가 찜찜.. (1=1도 연산인데..)

🔄 choose, when, otherwise

<select id="dynamicQueryChoose" resultMap="addressMap">
    select * from address where 1=1
    <choose>
        <when test="title != null">
            and title = #{title}
        </when>
        <when test="mno != null">
            and mno = #{mno}
        </when>
        <otherwise></otherwise>
    </choose>
</select>

🔍 where

  • where: 하위 엘리먼트에서 생성한 내용이 있을 경우 앞에 where를 붙여주고 없으면 무시
  • where 뒤에 오는 문장이 and, or 키워드로 시작하는 경우 키워드 삭제
<select id="dynamicQueryWhere" resultMap="addressMap">
    select * from address
    <where>
        <if test="title != null">
            and title = #{title}
        </if>
        <if test="mno != null">
            and mno=#{mno}
        </if>
    </where>
</select>

✂️ trim

좀 더 다양한 상황에서 사용 가능

  • prefix/suffix 처리 후 엘리먼트에 내용이 있으면 가장 앞/뒤에 붙임
<select id="dynamicQueryTrim" resultMap="addressMap">
    select * from address
    <trim prefix="where" prefixOverrides="and|or">
        <if test="title != null">
            and title = #{title}
        </if>
        <if test="mno != null">
            and mno=#{mno}
        </if>
    </trim>
</select>

🔄 foreach

주로 in 절을 추가할 경우 사용

  • collection: 값 목록을 가진 객체
  • item: collection 내의 item
  • open: 해당 블럭을 시작할 때 사용할 기호
  • close: 해당 블럭을 종료할 때 사용할 기호
  • separator: 각 item을 구분할 분리자 기호
<select id="selectUseIn" resultMap="addressMap">
    select * from address
    <where>
        <foreach collection="titles" item="title" open="title in (" separator=", " close=")">
            #{title}
        </foreach>
    </where>
</select>

⚙️ set

update에서 set 절을 생성하며 맨 마지막 column 표기에서 쉼표 제거

<update id="dynamicUpdate">
    update address
    <set>
        <if test="mno != null">mno=#{mno},</if>
        <if test="title != null">title=#{title},</if>
        <if test="address != null">address=#{address},</if>
        <if test="detailAddress != null">detail_address=#{detailAddress},</if>
        <if test="x != null">x=#{x},</if>
        <if test="y != null">y=#{y},</if>
    </set>
    where ano=#{ano}
</update>

⚠️ 주의사항

<, >, <=, >= 조건 절에서 부등호를 사용시 XML이라서 태그로 인식할 수도 있다!

해결 방법 2가지
1. &lt; &gt; &le; &ge;
2. <![CDATA[ 태그를 열어서 기존 코드대로 작성 (XML 파서 작동X)

📦 include

SQL의 모듈화와 재사용

<select id="selectUseIn" resultMap="addressMap">
    <!-- select * from address -->
    <include refid="select_address_all"></include>
    <where>
        <foreach collection="titles" item="title" open="title in (" separator=", " close=")">
            #{title}
        </foreach>
    </where>
</select>

🏗️ Service Layer와 트랜잭션


🔸 Service Layer가 없을 때

요구사항: 회원 정보 삭제 시 먼저 주소 정보 삭제 필요

  1. 주소 정보를 삭제하고 commit
  2. 회원 정보를 삭제하는데 예외 발생!
  3. 그러면 삭제가 절반만 되어버리게 되어버렸다..

➡️ Connection이 묶여야 하는데?

ServiceLayer를 이용한 Connection

Service Layer Connection

🔄 스프링의 트랜잭션 처리

PlatformTransactionManager

  • 트랜잭션 관리를 위한 최상위 모듈
  • DB 접근 기술에 따라 다양한 구현체 제공

트랜잭션 처리

💡 AOP에 의한 선언적 트랜잭션 처리

  • @Transactional로 트랜잭션 처리 선언
  • Target 객체에 대한 proxy 생성 → Around Advice 적용

Proxy 객체의 동작

  1. Target 메서드 호출 이전에 트랜잭션 시작
  2. Target 메서드 정상 종료 후 commit 실행
  3. RuntimeException 발생 시 rollback 실행

🔄 Spring - MyBatis 연동

@Controller

Service에서 SQLException이 전파되지 않음 → DataAccessException으로 대체!

private String registMember(@ModelAttribute Member member, Model model, HttpSession session) {
    try {
        // 처리 로직
    } catch (DataAccessException e) {
        e.printStackTrace();
        model.addAttribute("error", e.getMessage());
        return "member/member-regist-form";
    }
}

🔍 @Transactional 깊은 이해


🔸 주요 설정 속성

@Target({ElementType.TYPE, ElementType.METHOD}) // 클래스나 메서드에 적용 가능
public @interface Transactional {
    // 지정된 예외 발생 시 롤백 수행
    Class<? extends Throwable>[] rollbackFor() default {};
    // 지정된 예외 발생해도 롤백하지 않음
    Class<? extends Throwable>[] noRollbackFor() default {};
    // 트랜잭션 전파 방식 정의 (기본값: 이미 있으면 참여, 없으면 생성)
    Propagation propagation() default Propagation.REQUIRED;
    // 트랜잭션 격리 수준 정의 (기본값: 데이터베이스 기본 설정 사용)
    Isolation isolation() default Isolation.DEFAULT;
    // 트랜잭션 제한 시간(초) 설정 (기본값: -1, 제한 없음)
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    // 읽기 전용 트랜잭션 여부 (true면 데이터 변경 불가)
    boolean readOnly() default false;
}

🔄 Rollback 규칙

RuntimeException 계열의 예외 발생 시 Rollback

기본 동작 2가지
1. noRollbackFor: RuntimeException 중 commit 대상 예외
2. rollbackFor: Checked Exception(business exception) 중 rollback 대상이 되는 예외 설정

🔄 전파 속성

@Transaction에서 다른 @Transaction을 호출하는 경우 기존 트랜잭션의 전파 속성

  • propagation 값을 비교 (기본값: REQUIRED)
  • ⚠️ @Transactional이 붙은 두 Bean에서 서로를 호출했다면...? 일단 안된다! 왜? proxy를 생성해서 거쳐야 해서!

BeanA.method()의 @Transactional 전파 속성

  • PROPAGATION.REQUIRED

    • methodA에서 rollback 발생 시 전체 트랜잭션 rollback
    • methodB에 UnExpectedRollbackException 전파
  • PROPAGATION.REQUIRED_NEW

    • 별개의 트랜잭션 구성: 트랜잭션A에서 rollback 발생 시 트랜잭션B에 영향을 주지 않음
  • PROPAGATION.NESTED

    • 기존의 트랜잭션 내에 SavePoint로 트랜잭션B 구성
    • 부모가 롤백되면 전부 다 롤백!

🔒 독립성 수준

독립성 수준

일반적으로 ISOLATION_DEFAULT 사용: 개별 Programmer의 영역이 아닌 DBA의 영역

  • Oracle의 default: ISOLATION_READ_COMMITTED
  • MySQL의 default: ISOLATION_REPEATABLE_READ

⚠️ Spring AOP의 한계

  • Spring Bean에 대해서만 적용 가능
  • Proxy를 기반으로 동작함!

➡️ 그래서 명시적 트랜잭션 처리가 필요하다!

@Autowired
private PlatformTransactionManager txManager; // TransactionManager 획득

@Override
public int joinProgrammingTX(Member member) {
    DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // transaction definition 생성
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    TransactionStatus status = txManager.getTransaction(def); // txManager를 통해 TransactionStatus 획득

    int result = -1;
    try {
        // insert 작업
        // update 작업
        txManager.commit(status); // 성공 - commit
    } catch (RuntimeException e) {
        txManager.rollback(status); // 실패 - rollback
    }

    return result;
}
profile
기록하고 공유하는 개발자

0개의 댓글