[Spring] 동적 SQL & 트랜잭션 & Java Config

young-gue Park·2023년 10월 18일
0

Spring

목록 보기
8/14
post-thumbnail

⚡ 동적 SQL & 트랜잭션 & Java Config


📌 동적 SQL

🔷 Runtime 시점에서 사용자의 입력 값에 따라 동적으로 SQL을 생성하여 실행하는 방식

  • JDBC나 다른 Framework 사용 시 어려움을 느낄 수 있는데 MyBatis는 이를 편리하게 사용할 수 있게 도움을 준다.
  • JSTL이나 XML 기반의 텍스트 프로세서를 사용해본 사람에게는 친숙할 것이다.

⭐ MyBatis 동적 SQL 종류

🔷 if

  • 흔히 아는 if문의 기능이다.
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

🔷 choose (when, otherwise)

  • 흔히 아는 switch문의 기능이다.
  • 동적 쿼리 if에 else if와 else가 없기 때문에 이를 사용한다.
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

🔷 trim (where, set)

  • WHERESET을 엘리먼트로 사용할 수 있다.
  • 반환되는 컨텐츠가 없을 때 쿼리문 뒤에 붙을 필요가 없는 키워드를 붙지 않게 한다.
  • trim으로 사용할 때는 prefix 속성의 값으로 WHERESET을 지정할 수 있고, prefixOverridessuffixOverrides속성으로 AND 등의 값을 지정하면 반환되는 컨텐츠가 없을 때 해당 접미사나 접두사가 쿼리문에 붙지 않는다.

WHERESET를 굳이 엘리먼트로 사용하는 이유는 ifchoose 조건에서 아무 값도 반환되지 않았을 때 WHERESET, AND 등의 키워드만 덜렁 마지막에 적혀 오류를 초래할 수 있다. 물론 이런 오류를 초래할 여지가 없다면 이를 사용하지 않고 쿼리에 적어도 작동엔 지장이 없다.

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

<!-- 위와 같다 -->
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="state != null">
           state = #{state}
      </if>
      <if test="title != null">
          AND title like #{title}
      </if>
      <if test="author != null and author.name != null">
          AND author_name like #{author.name}
      </if>
  </trim>
</select>
<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

<!-- 위와 같다 -->
<update id="updateAuthorIfNecessary">
  update Author
    <trim prefix="SET" suffixOverrides=",">
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </trim>
  where id=#{id}
</update>

🔷 foreach

  • collection에 대해 반복처리를 한다.
  • for문과 비슷하다.
<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  <where>
    <foreach item="item" index="index" collection="list"
        open="ID in (" separator="," close=")" nullable="true">
          #{item}
    </foreach>
  </where>
</select>

💡 컬렉션 파라미터로 Map이나 배열객체와 더불어 List, Set등과 같은 반복가능한 객체를 전달할 수 있다. 반복가능하거나 배열을 사용할때 index값은 현재 몇번째 반복인지를 나타내고 value항목은 반복과정에서 가져오는 요소를 나타낸다. Map을 사용할때 index는 key객체가 되고 항목은 value객체가 된다.

마이바티스3 | 동적 쿼리

⭐ 동적 쿼리 활용

🔷 동적 쿼리 활용의 대표적인 예로 검색 조건 변경이 있는데 이를 지난번 board 실습에 넣어보자.

  1. 검색 조건을 담을 DTO를 제작한다. (SearchCondition)

🖥 SearchCondition

package com.bzeromo.board.model.dto;

public class SearchCondition {
	private String key;
	private String word;
	private String orderBy;
	private String orderByDir;
	
	public SearchCondition() {}
	
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
	public String getWord() {
		return word;
	}
	public void setWord(String word) {
		this.word = word;
	}
	public String getOrderBy() {
		return orderBy;
	}
	public void setOrderBy(String orderBy) {
		this.orderBy = orderBy;
	}
	public String getOrderByDir() {
		return orderByDir;
	}
	public void setOrderByDir(String orderByDir) {
		this.orderByDir = orderByDir;
	}
	
}
  1. BoardServiceBoardServiceImplSearchCondition을 매개변수로 받아 List<Board> 타입으로 반환할 search 메서드를 추가한다.

🖥 BoardService 중

//검색 버튼을 눌렀을 때 처리할 메서드
List<Board> search(SearchCondition condition);

🖥 BoardServiceImpl 중

@Override
public List<Board> search(SearchCondition condition) {
	return boardDao.search(condition);
}
  1. BoardController에 검색 기능을 추가한다.

🖥 BoardController 중

// 검색기능 추가
@GetMapping("search")
public String search(Model model, SearchCondition condition) {
		
	model.addAttribute("list", boardService.search(condition));
		
	return "/board/list";
}
  1. boardMapper.xml에서 동적 쿼리를 이용한 검색기능 쿼리문을 등록한다. 검색 조건(key)에 따라 해당 키 컬럼에 포함된 값들 중에 word가 포함된 값을 찾을 것이다. 또한 어떤 기준으로 정렬할 것이냐(키, 오름차순&내림차순)에 따라 출력값을 정렬할 것이다.

🖥 boardMapper.xml 중

<!-- 검색기능  -->
<select id="search" resultType="Board" parameterType="SearchCondition">
  SELECT id, content, writer, title, view_cnt as viewCnt, date_format(reg_date, '%Y-%M-%d') as regDate
  FROM board
  <!-- 검색 조건 (동적쿼리)-->
  <if test="key != 'none'">
    WHERE ${key} LIKE concat('%', #{word}, '%')
  </if>
  <!-- 어떤 기준으로 어떤 방향으로 정렬 -->
  <if test="orderBy != 'none'">
    ORDER BY ${orderBy} ${orderByDir}
  </if>
</select>

💡 $#의 차이
#은 '' 내에 문자열 형식으로, $는 parameter의 값으로 그냥 들어온다.
$은 sql injection의 위험이 커 보안 측면에서 좋지 않다.

  1. list.jsp에 검색 form을 추가한다. (부트스트랩 적용)
  • form내부의 select마다 있는 option의 value들이 검색 버튼을 누르면 /search 를 향해 GET 방식으로 날린다.

🖥 list.jsp 중

<!-- action 경로를 작성할 떄는 헷갈리지 않게 풀로 쓰는게 좋을듯 하지만 직접 작성보다는 변수를 활용 -->
<form action="search" method="GET" class="row">
  <div class="col-2">
    <label>검색기준</label>
    <select class="form-select" name="key">
      <option value="none">없음</option>
      <option value="writer">작성자</option>
      <option value="title">제목</option>
      <option value="content">내용</option>
    </select>
  </div>
  <div class="col-5">
    <label>검색 내용</label>
    <input type="text" name="word" class="form-control">
  </div>
  <div class="col-2">
    <label>정렬기준</label>
    <select class="form-select" name="orderBy">
      <option value="none">없음</option>
      <option value="writer">작성자</option>
      <option value="title">제목</option>
      <option value="view_cnt">조회수</option>
    </select>
  </div>
  <div class="col-2">
    <label>정렬방향</label>
    <select class="form-select" name="orderByDir">
      <option value="asc">오름차순</option>
      <option value="desc">내림차순</option>
    </select>
  </div>
  <div class="col-1">
    <input type="submit" value="검색" class="btn btn-primary">
  </div>
</form>

🖨 결과

제목을 기준으로 작성자명에 "맨"이 들어간 글을 출력

내용으로 "힘내세요"가 들어가있는 글을 출력


📌 Spring TX (트랜잭션)

🔷 데이터 무결성을 위해서 사용한다.

  • 스프링에서 제공하는 트랜잭션 기능을 활용할 수 있음.
  • jar 혹은 pom.xml 을 이용하여 등록한다.
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-tx</artifactId>
  <version>${org.springframework-version}</version>
</dependency
  • root-context.xml에 트랜잭션 관리자와 어노테이션 기반 트랜잭션을 설정해야한다.
<!-- 트랜잭션 매니저를 등록 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <constructor-arg ref="dataSource"/>
</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>
  • 설정이 끝나면 메소드나 클래스에 @Transactional이 선언되어 있으면, AOP를 통해 트랜잭션 처리를 한다.

🖥 BoardServiceImpl의 writeBoard() 메서드

@Transactional
@Override
public void writeBoard(Board board) {
	System.out.println("게시글을 작성합니다.");
	//강제로 ID를 99번으로 결정하겠다.
	//board.setId(99);
	boardDao.insertBoard(board);
	//boardDao.insertBoard(board);
}

🤔 주석 표시 제거 후 트랜잭션 처리 없이 글을 두 번 게시할 때 강제로 ID를 99번으로 만들면 어떻게 될까?

이렇게 터지는데...

하나는 걸렀지만 다른 게시가 들어오고 말았다.
그렇다면 트랜잭션 처리를 했을 때 같은 작업을 하면 어떻게 될까?

마찬가지의 오류가 발생하고....

이번엔 새로 추가되지 않았다!
이를 통해 트랜잭션 처리되어 작업이 ROLLBACK되었음을 알 수 있다.


📌 Java Config

  • 이전에 Bean에 의존성을 주입하기 위해 두 가지 방법을 배웠었다.
  1. XML을 이용한 방식
  2. Annotation을 이용한 방식

이번엔 Java를 이용한 방식을 다룬다.
(굳이 지금 다루는 이유는 곧 사용할 SpringBoot에서 쓰지만 Spring Legacy 버전에서는 안쓰니까...)

👍 이전에 사용했던 방식들과 코드들(Spring DI 게시글) (재활용할 것이다.)

  1. ApplicationConfig.java에 직접 등록하기
  • 이 경우에는 모든 Bean에 @component를 달지 않아도 된다.
  • 마찬가지로 스코프 지정을 통해 인스턴스 생성을 한번만 할지 여러번 할지 정할 수 있다.

🖥 com.bzeromo.di.ApplicationConfig

package com.bzeromo.di;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class ApplicationConfig {
	
	@Bean
//	@Scope("prototype")
	public Desktop desktop() {
		return new Desktop();
	}
	
	@Bean
	public Laptop laptop() {
		return new Laptop();
	}
	
	
	@Bean
	public Programmer programmer() {
//		Programmer pr = new Programmer();
//		pr.setComputer(desktop()); //설정자 주입
		
		Programmer pr = new Programmer(desktop()); //생성자 주입
		
		
		return pr;
	}
	
}
  1. @ComponentScan을 통해 ApplicationConfig.java에서 Bean을 찾아 주입하기
  • 이 경우에는 의존성을 주입할 Bean에 @Component 표시를 해야한다.

🖥 com.bzeromo.di2.ApplicationConfig

package com.bzeromo.di2;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {"com.bzeromo.di2"})
public class ApplicationConfig {
}

🖥 Test

  • context 변수명으로 저장할 녀석은 이제 AnnotationConfigApplicationContext(ApplicationConfig.class) 이다.
package com.bzeromo.di2;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test {
	public static void main(String[] args) {

		ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
		
		Desktop d1 = context.getBean("desktop", Desktop.class);
		Desktop d2 = context.getBean("desktop", Desktop.class);
		
		System.out.println(d1 == d2);
		
		Programmer p = (Programmer)context.getBean("programmer");
		p.coding();
		
	}
}

싱글턴(default)이기 때문에 같은 인스턴스가 반환되는 모습


스프링부트가 다가온다...

profile
Hodie mihi, Cras tibi

0개의 댓글