<< 학습 목표 >>

1. 정적 쿼리와 동적 쿼리의 특징을 설명할 수 있다.

2. trim 태그를 활용할 수 있다.

3. if 태그를 활용할 수 있다.

4. choose, when, otherwise 태그를 활용할 수 있다.

5. if 태그와 choose, when, otherwiser 태그를 사용할 때 주의할 점을 설명할 수 있다.

6. foreach 태그를 활용할 수 있다.


지금까지 배운 것들만 가지고서도 충분히 내가 만들고 싶은 쿼리를 만들 수 있음

이번에는 알고 있으면 유용한 동적 쿼리에 대해서 배우자

 

우리가 지금까지 작성한 쿼리는 정적 쿼리임

 

다시 한번 전 글 ( https://codingaja.tistory.com/118 ) 에서 작성한 쿼리를 보자

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

<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.StudygroupDao">
	<insert id="saveStudygroupInfo" parameterType="com.study.chapter04.StudygroupInfo" useGeneratedKeys="true" keyColumn="idx">
		INSERT INTO studygroupInfo(title, contents, regDate) VALUES(#{title}, #{contents}, CURRENT_TIMESTAMP());
	</insert>

	<insert id="joinStudygroupMember" parameterType="com.study.chapter04.StudygroupMember">
		INSERT INTO studygroupMember(studygroupIdx, memberIdx) VALUES(#{studygroupIdx}, #{memberIdx});
	</insert>
</mapper>

어느 상황에서든 INSERT 쿼리가 바뀔 일은 없음

INSERT 쿼리에 사용한 값은 바뀔 수 있지만 INSERT 쿼리의 구조 자체가 바뀔 순 없음

 

이렇게 언제 실행시키든 그 구조가 똑같은 쿼리를 정적 쿼리라고 함

 

 

이번에는 동적 쿼리의 예를 하나 들어보고 바로 동적 쿼리를 배워보자

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

<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.StudygroupDao">
	<insert id="saveStudygroupInfo" parameterType="com.study.chapter04.StudygroupInfo" useGeneratedKeys="true" keyColumn="idx">
		INSERT INTO studygroupInfo(title, contents, regDate) VALUES(#{title}, #{contents}, CURRENT_TIMESTAMP());
	</insert>

	<insert id="joinStudygroupMember" parameterType="com.study.chapter04.StudygroupMember">
		<if test="idx == 0">
			INSERT INTO studygroupMember(studygroupIdx, memberIdx) VALUES(#{studygroupIdx}, #{memberIdx});
		</if>
		<if test="idx != 0">
			INSERT INTO studygroupMember(idx, studygroupIdx, memberIdx) VALUES(#{idx}, #{studygroupIdx}, #{memberIdx});
		</if>
	</insert>
</mapper>

id가 joinStudygroupMember 인 insert 태그를 보면 if문이 적용되 동적 쿼리를 만들었음

1. <if test="idx == 0">  ==>  전달 받은 DTO의 idx 멤버 변수 값이 0 이라면 if문 안에 있는 INSERT 쿼리가 실행됨

2. <if test="idx != 0">  ==>  전달 받은 DTO의 idx 멤버 변수 값이 0 이 아니라면 if문 안에 있는 INSERT 쿼리가 실행됨

 

전달 받은 DTO의 idx 멤버 변수 값이 0 이냐 아니냐에 따라 서로 다른 구조의 INSERT 문이 실행됨

전달 받은 DTO의 idx 멤버 변수 값이 0 이라면 studygroupIdx, memberIdx 칼럼만 활용하는 INSERT 쿼리문이 실행되고 전달 받은 DTO의 idx 멤버 변수 값이 0 이 아니라면 idx, studygroupIdx, memberIdx 칼럼을 활용하는 INSERT 쿼리문이 실행됨

 

MyBatis는 이런식으로 쿼리문에 자바처럼 조건문, 반복문 등을 활용해 동적 쿼리를 구성할 수 있음


동적 쿼리는 DML문 ( SELECT, INSERT, UPDATE, DELETE ) 에 사용할 수 있음

여기서 동적 쿼리를 구성하는 방법만 잘 익혀두면 DML문 모두 사용하는 방법은 똑같음


동적쿼리에 사용하는 태그는 아래와 같음

태그명 설명
trim 접두사(prefix), 접두어(suffix) 를 붙여주거나 지우는 태그
if 자바의 if와 같은 태그로 조건식이 참(true)인 경우 안에 있는 구문을 추가해주는 태그
choose 자바의 if ~ else if ~ else와 같은 태그로 if ~ else if ~ else 의 영역을 나타내는 태그
when 자바의 if ~ else 와 같은 태그로 choose 안에 사용하며 조건식이 참(true)인 경우 안에 있는 구문을 추가해주는 태그
otherwise 자바의 else와 같은 태그로 choose 안에 사용하며 모든 when의 조건식이 거짓(false)인 경우 otherwise 안에 있는 구문을 추가해주는 태그
foreach 자바의 for 또는 향상된 for문과 같은 태그로 반복적으로 구문을 추가해주는 태그

태그명 trim
설명 접두사(prefix), 접두어(suffix) 를 붙여주거나 지우는 태그

! 주의 !
일반적으로 trim은 앞, 뒤 공백을 "지워주는" 함수로 많이 사용함
이때문에 MyBatis의 trim도 무언가를 지워준다고 오해할 수 있는데 MyBatis의 trim은 지워주기도 하지만 concat 함수처럼 덧붙여줄 수도 있음
속성 1. prefix : trim 태그 내에 있는 구문의 맨 앞에 덧붙일 문자열

2. suffix : trim 태그 내에 있는 구문의 맨 뒤에 덧붙일 문자열

3. prefixOverrides : trim 태그 내 구문의 맨 앞에 해당 문자열이 있다면 지움

4. suffixOverrides : trim 태그 내 구문의 맨 뒤에 해당 하는 문자열이 있다면 지움

 

- prefix, suffix 속성 사용 예시

<insert id="insertMember" parameterType="com.study.chapter04.InsertMemberDto">
	INSERT INTO member(id, pw, nickname, tel) VALUES(#{id}, #{pw}, #{nickname}, #{tel});
</insert>

위와 같은 쿼리가 있을 때 prefix, suffix 를 사용해 아래와 같이 바꿀 수도 있음

<insert id="insertMember" parameterType="com.study.chapter04.InsertMemberDto">
	INSERT INTO member(id, pw, nickname, tel) VALUES
	<trim prefix="(" suffix=");">
		#{id}, #{pw}, #{nickname}, #{tel}
	</trim>
</insert>

! 주의 사항 !

가끔 이런 예시를 들면 "왜 저렇게 사용하나요?" 라는 의문을 갖는 분들이 있는데 저렇게 할 수도 있다는거지 반드시 저렇게 해야한다는게 아님

저렇게 할 수 있다는걸 알고 있으면 나중에 여러분의 상황에 맞게 바꿔서 사용할 수 있을 것

 

 

- prefixOverrides, suffixOverrides 속성 사용 예시

<delete id="deleteMember" parameterType="com.study.chapter04.DeleteMemberDto">
	DELETE FROM member WHERE id = #{id} AND pw = #{pw}
</delete>

위와 같은 쿼리가 있을 때 prefixOverrides, suffixOverrides 를 사용해 아래와 같이 바꿔도 실행되는 쿼리는 동일함

<delete id="deleteMember" parameterType="com.study.chapter04.DeleteMemberDto">
	DELETE FROM member WHERE
		
	<trim prefixOverrides="AND" suffixOverrides="OR">
		AND id = #{id} AND pw = #{pw} OR
	</trim>
</delete>

 

prefixOverrides 는 맨 앞에 있는 해당 문자열을 지워주므로 prefixOverrides 속성으로 인해 [ id = #{id} AND pw = #{pw} OR ] 가 됨

suffixOverrides 는 맨 뒤에 있는 해당 문자열을 지워주므로 suffixOverrides 속성으로 인해 [ id = #{id} AND pw = #{pw} ] 가 됨

 

 

prefixOverrides, suffixOverrides 속성은 | ( or ) 연산자를 사용할 수 있음

아래와 같이 | 연산자를 사용해서 ( 맨 앞에 있는 AND 또는 OR ) 를 지우고 ( 맨 뒤에 있는 AND 또는 OR ) 를 지울 수 있음

<delete id="deleteMember" parameterType="com.study.chapter04.DeleteMemberDto">
	DELETE FROM member WHERE
	
	<trim prefixOverrides="AND | OR" suffixOverrides="AND | OR">
		AND id = #{id} AND pw = #{pw} OR
	</trim>
</delete>

아직 배우진 않았지만 trim 태그를 잘 사용하면 where, set 태그를 대신할 수 있음


태그명 if
설명 자바의 if와 같은 태그로 조건식이 참(true)인 경우 안에 있는 구문을 추가해주는 태그

사용할 수 있는 연산자는 논리 연산자, 비교 연산자를 사용할 수 있음

논리 연산자의 && 는 and 로 사용하고 || 는 or 로 사용함
같다는 == , 다르다는 != 로 자바의 비교 연산자 문법과 동일함

그외에 문자와 문자열을 비교하는 방법은 MyBatis 만의 별도 문법이 있음

속성 1. test : 조건식이 들어가는 속성으로 이 속성에 들어있는 조건식의 결과가 true일 경우 if문 안에 있는 구문이 추가됨

 

- if 태그 사용 예시

<select id="selectMember" parameterType="String" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member
		
	<if test="nickname != null">
		WHERE nickname = #{nickname}
	</if>
		
	;
</select>

<< 코드 설명 >>

if의 test 속성에 사용한 nickname은 파라미터로 전달 받은 DTO가 가지고 있는 nickname 멤버 변수의 값임

지금까지 INSERT, UPDATE, DELETE, SELECT 쿼리를 작성하며 파라미터로 전달 받은 DTO가 가지고 있는 멤버 변수에 접근할 때는 #{멤버변수} 를 사용했지만 test 속성에는 #{멤버변수} 가 아닌 멤버 변수명을 바로 쓴다는 점을 주의하자

 

- nickname 속성이 없을 경우 : SELECT * FROM member;

- nikcname 속성이 있을 경우 : SELECT * FROM WHERE nickname = "닉네임속성값";

 

으로 상황에 따라 서로 다른 구조의 쿼리가 만들어지므로 동적 쿼리 라고 함

 

 

그외에 몇 가지 예시를 더 보자

<select id="selectMember" parameterType="String" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member WHERE nickname = #{nickname}
		
	<if test="start >= 0">
		LIMIT #{start}
		
		<if test="end != 0">
			, #{end}
		</if>
	</if>
		
	;
</select>

이 상황에서는 만들어질 수 있는 쿼리의 경우가 3가지임

- start가 0 미만인 경우 : SELECT * FROM member WHERE nickname = #{nickname}

- start가 0 이상이고 end가 0인 경우 : SELECT * FROM member WHERE nickname = #{ncikname} LIMIT #{start}

- start가 0 이상이고 end가 0이 아닌 경우 : SELECT * FROM member WHERE nickname = #{nickname} LIMIT #{start}, #{end}

 

 

이번에는 문자열을 비교하는 방법을 알아보자

MyBatis는 문자와 문자열을 구분하지 않고 모두 문자열로 처리함

따라서 문자를 표현할 때는 문자열 ( " ) 로 표현해야함

 

<select id="selectMember" parameterType="String" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member WHERE nickname = #{nickname}
	
	<if test='orderby != null and orderby == "오름차순"'>
		ORDER BY idx ASC
	</if>
	<if test='orderby != null and orderby == "내림차순"'>
		ORDER BY idx DESC
	</if>
	
	<if test="start >= 0">
		LIMIT #{start}
	
		<if test="end != 0">
			, #{end}
		</if>
	</if>
	
	;
</select>

<< 코드 설명 >>

(1). 문자열 값을 표현할 때 " 를 사용해야하므로 test 속성의 값을 ' (홑따옴표) 로 감쌌음

  자바에서는 문자열 비교는 equals 메서드를 사용해야하지만 MyBatis는 == 연산자를 사용해도 같다, 다르다를 판단할 수 있음

 

문자열 비교할 때 아래와 같이 equals, equalsIsIgnoreCase 메서드를 호출할 수 있음

equals 메서드는 자바의 equals 메서드와 같음

equalsIsIgnoreCase 메서드는 대소문자를 무시하고 같은지를 판단할 수 있음

 


태그명 choose, when, otherwise
설명 choose - 자바의 if ~ else if ~ else와 같은 태그로 if ~ else if ~ else 의 영역을 나타내는 태그

when - 자바의 if ~ else 와 같은 태그로 choose 안에 사용하며 조건식이 참(true)인 경우 안에 있는 구문을 추가해주는 태그

otherwise - 자바의 else와 같은 태그로 choose 안에 사용하며 모든 when의 조건식이 거짓(false)인 경우 otherwise 안에 있는 구문을 추가해주는 태그
속성 choose 태그 - 속성 없음

when 태그 - test 속성 : if의 test 속성과 동일함

otherwise 태그 - 속성 없음

- choose, when, otherwise 태그 사용 예시

  if문에서 사용했던 예시를 choose, when, otherwise 의 상황에 맞게 바꾼 것

<select id="selectMember" parameterType="String" resultType="com.study.chapter04.SelectMemberDto">
	SELECT * FROM member WHERE nickname = #{nickname}
	
	<choose>
		<when test='orderby != null and orderby.equals("오름차순")'>
			ORDER BY idx ASC
		</when>
		<otherwise>
			ORDER BY idx DESC
		</otherwise>
	</choose>
    
	<if test="start >= 0">
		LIMIT #{start}
	
		<if test="end != 0">
			, #{end}
		</if>
	</if>
	
	;
</select>

<< 코드 설명 >>

(1). when과 otherwise는 choose 태그 안에만 사용할 수 있음

  마치 case 는 switch 안에만 사용할 수 있듯이...

  when은 if 내지는 else if의 역할을 하는 태그

  when의 test가 true라면 그 안에 있는 구문이 추가됨

  otherwise는 else 의 역할을 하는 태그

  모든 when의 test가 false라면 otherwise 안에 있는 구문이 추가됨

 

  if문에서도 else를 붙일 때 주의해야하는 것처럼 when, otherwise에서도 otherwise를 붙일 때 주의해야함

  위 코드는 문제가 생길 수 있는 코드임

  orderby 가 null이 아닌데 orderby 가 오름차순 이라면 ORDER BY idx ASC 구문이 붙지만 그외의 모든 상황에서는 ORDER BY idx DESC가 붙는 것임

  그외의 모든 상황이라는건 orderby가 null 이거나 orderby가 오름차순이 아닌 상황임

 

  if문에서 사용한 의도와는 전혀 다른 코드가 된 것

  틀렸다고 할 순 없지만 otherwise를 붙일 때는 조심하자

  if문에서 사용한의도와 완전히 같은 코드가 되려면 아래와 같이 바꿀 수 있음

 

choose, when, otherwise의 마지막으로 if문과 choose, when, otherwise를 비교해보자

(1). orderby가 null이 아닌데 orderby가 오름차순이라면 ORDER BY idx ASC 구문이 추가됨

  orderby가 null이 아닌데 orderby가 내림차순이라면 ORDER BY idx DESC 구문이 추가됨

(2). orderby가 null이 아닌데 orderby가 오름차순이라면 ORDER BY idx ASC 구문이 추가됨

  orderby가 null이 아닌데 orderby가 내림차순이라면 ORDER BY idx DESC 구문이 추가됨

 

위 if, choose, when, otherwise 를 해석해보면 이렇게 해석될 수 있을 것

그러나 여기서 if와 choose, when, otherwise는 큰 차이가 있음

 

if는 if 마다 마다가 개별적인 한 덩어리임

choose, when, otherwise는 coose 단위로 한 덩어리임

 

따라서 컴퓨터는 첫번째 if문의 test가 true이면 ORDER BY idx ASC 구문을 추가한 후 두 번째 if문 또한 체크함

지금 상황에서는 그럴리없지만 두 번째 if문의 test가 true라면 이어서 ORDER BY idx DESC 구문이 추가됨

이러면 문법적 오류가 발생함

if는 if 마다 개별적인 한 덩어리라는점에 주의하자

 

 

choose, when, otherwise 는 choose 안에 있는 한 when, 또는 otherwise 만 실행 되기 때문에 첫 번째 when의 test가 true이면 ORDER BY idx ASC 구문만 추가됨

첫 번째 when의 test가 false면 두 번째 when의 test를 체크하고 두 번째 when의 test가 true이면 ORDER BY idx DESC 구문만 추가됨

이렇게 choose, when, otherwise는 choose 단위로 한 덩어리라는 점에 주의하자


태그명 foreach
설명 자바의 for 또는 향상된 for문과 같은 태그로 반복적으로 구문을 추가해주는 태그
속성 - collection : 반복문에 사용할 반복 가능한 객체 / 배열, Map, List, Set 등과 같은 객체가 반복 가능한 객체임

- item : n번 째 반복에서 반복 가능한 객체 내 n번 째 요소(element) 를 전달 받을 변수명

- index : 반복 시 인덱스 번호가 필요할 때 지정하는 속성으로 인덱스 번호를 전달 받을 변수명

- open : 반복을 시작하기 전 추가할 구문

- close : 반복을 끝낸 후 추가할 구문

- separator : n번 째 반복이 끝난 후 추가할 구문

 

- foreach 태그 사용 예시

DAO가 쿼리 쪽으로 데이터들을 전달할 때 Map 보단 배열, List, Set 을 더 많이 활용하므로 Map인 경우는 설명하지 않음

또한 배열, List, Set은 모두 데이터들을 선형적으로 담고 있기 때문에 사용하는 방법 모두 동일함

 

<< SelectMemberDto >>

package com.study.chapter04;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class SelectMemberDto {
	private String id;
	private String pw;
	private String nickname;
	private String tel;
	private int idx;
	private LocalDateTime joinDateTime;
	private boolean isDel;
}

 

<< MemberDao >>

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao {
	// ...
    
	SelectMemberDto selectMember(List<SelectMemberDto> selectConditions);
    
	// ...
}

<< 코드 설명 >>

id, pw, nickname, tel, idx, joinDateTime, isDel 멤버 변수를 갖고 있는 DTO가 있음

 

 

 

 

 

 

 

 

DAO에서는 id가 selectMember인 쿼리로 SelectMemberDto들을 담은 List를 전달하고 있음

전달하는 List의 이름은 selectConditions 임

 

 

 

 

 

 

이제 쿼리를 만들자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao">
	// ...
    
	<select id="selectMember" parameterType="com.study.chapter04.SelectMemberDto" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname IN
		
		<foreach collection="selectConditions" item="condition" open="(" close=")" separator=", ">
			#{condition.nickname}
		</foreach>
		
		;
	</select>
    
	// ...
</mapper>

<< 코드 설명 >>

(1). DAO가 전달해주는 데이터들을 받기 위해서 데이터의 타입을 지정함

(2). 이 쿼리가 완성된 한 예를 보면 다음과 같음

  SELECT * FROM member WHERE nickname IN ("홍길동", "김철수", "고영희");

  IN 안에는 반복적으로 문자열이 들어가야하므로 foreach를 사용할 수 있음

  collection 속성은 DAO가 전달해주는 데이터들의 이름

  item 속성은 전달 받은 데이터들에서 첫 번째 데이터부터 마지막 데이터까지 차례대로 저장될 변수명

  open 속성은 반복문이 동작하기 전 추가될 문자열

  close 속성은 반복문이 동작한 후 추가될 문자열

  separator 속성은 n번째 반복이 끝난 후 추가될 문자열

 

만약 open, close 속성을 사용하지 않고 싶다면 다음과 같이 쿼리를 구성하면 됨

그러나 아래와 같이 구성하면 가독성이 많이 떨어진다는 걸 잘 알고 있을 것임

앞서 얘기했던 것처럼 꼭 이걸 써야된다 저걸 써야된다 는 없음

이런 저런 방법을 알고 있으면 내 상황에 맞는 코드를 작성할 수 있는 것

 

foreach의 특별한 점은 반복문이므로 특정 쿼리를 반복적으로 실행하도록 만들 수도 있음

INSERT 또는 UPDATE 또는 DELETE 를 굉장히 많이, 수천번 해야한다 면 아래와 같이 INSERT 쿼리문을 만들어 반복적으로 호출하면 될 것

 

<< 쿼리 >>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.StudygroupDao">
	// ...
	
	<insert id="joinStudygroupMember" parameterType="com.study.chapter04.StudygroupMember">
		INSERT INTO studygroupMember(studygroupIdx, memberIdx) VALUES(#{studygroupIdx}, #{memberIdx});
	</insert>
	
	// ...
</mapper>

 

<< DAO >>

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StudygroupDao {
	// ...
	
	void joinStudygroupMember(StudygroupMember studygroupMember);
	
	// ...
}

 

<< 컨트롤러 >>

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class StudygroupController {
	@Autowired
	private StudygroupDao studygroupDao;
	
	@PostMapping()
	public void joinStudygroupMember(List<StudygroupMember> joinMemberList) {
		for(StudygroupMember joinMember : joinMemberList) {
			studygroupDao.joinStudygroupMember(joinMember);
		}
	}
}

이와 같이 컨트롤러에서 반복적으로 쿼리를 호출해 특정 쿼리를 반복적으로 실행할 수 있지만 굉장히 많이, 수 천번 반복적으로 호출하게 되면 서버 전체에 부하가 굉장히 클 수 있음

 

이렇게 특정 쿼리를 굉장히 많이 실행해야 되는 상황을 벌크(Bulk) 라고함

INSERT 를 많이 실행해야되는 상황을 Bulk INSERT

UPDATE 를 많이 실행해야되는 상황을 Bulk UPDATE

DELETE 를 많이 실행해야되는 상황을 Bulk DELETE

 

이럴 때는 DAO가 쿼리쪽으로 대량의 데이터를 전달하고 쿼리에서는 foreach 를 사용해 실행할 쿼리들을 한번에 생성하는 방법이 있음

 

<< 컨트롤러 >>

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class StudygroupController {
	@Autowired
	private StudygroupDao studygroupDao;
	
	@PostMapping()
	public void joinStudygroupMember(List<StudygroupMember> joinMemberList) {
		studygroupDao.joinStudygroupMember(joinMemberList);
	}
}

<< 코드 설명 >>

전과 다르게 컨트롤러에서 DAO로 대량의 데이터들을 한번에 전달함

 

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StudygroupDao {
	// ...
	
	void joinStudygroupMember(List<StudygroupMember> studygroupMembers);
	
	// ...
}

<< 코드 설명 >>

DAO는 대량의 데이터를 받아 쿼리로 대량의 데이터를 전달함

이때 쿼리로 전달하는 대량의 데이터 이름은 studygroupMembers 임

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.StudygroupDao">
	// ...
	
	<insert id="joinStudygroupMember" parameterType="com.study.chapter04.StudygroupMember">
		INSERT INTO studygroupMember(studygroupIdx, memberIdx)
	
		<foreach collection="studygroupMembers" item="studygroupMember" open="VALUES " separator=", " close=";">
			(#{studygroupMember.studygroupIdx}, #{studygroupMember.memberIdx})
		</foreach>
	</insert>
	
	// ...
</mapper>

<< 코드 설명 >>

쿼리에서는 전달 받은 대량의 데이터를 foreach로 반복하며 INSERT 쿼리를 구성함

 

위 insert 태그가 동작하면 다음과 같은 INSERT 코드가 만들어짐

INSERT INTO studygroupMember(studygroupIdx, memberIdx) VALUES (#{1번째 스터디그룹 idx}, #{1번째 사용자 idx}), (#{2번째 스터디그룹 idx}, #{2번째 사용자 idx}), (#{3번째 스터디그룹 idx}, #{3번째 사용자 idx}), ... , (#{n번째 스터디그룹 idx}, #{n번째 사용자 idx});

 

UPDATE, DELETE 또한 마찬가지로 foreach 를 사용하면 호출 한번으로 대량의 UPDATE, DELETE 가 되도록 할 수 있음

그러나 ! 앞서 언급했듯 "Bulk INSERT, UPDATE, DELETE 할 때 반드시 이 방법을 사용해야된다" 가아님

이 방법도 있다 임


이외에도 다른 예시를 보고 싶거나 더 자세한 사항들을 알고 싶다면 MyBatis 공식 홈페이지 ( https://mybatis.org/mybatis-3/ ) 에서 찾아보자

728x90
LIST

<< 학습 목표 >>

1. INSERT 한 데이터의 PK 값을 돌려받을 수 있다.


프로젝트의 규모가 커지면 하나의 처리를 하기 위해서 쿼리를 여러번 실행해야하는 경우가 자주 발생함

우리는 지금까지 단편적인 기술만 배웠으므로 그런일이 없었지만 곧 지금까지 배운 것들을 종합적으로 활용해서 하나의 프로젝트를 만들어볼 예정임

 

이렇게 하나의 처리를 하기 위해서 쿼리를 여러 번 실행해야할 때 INSERT 후 INSERT 한 데이터의 PK 값을 사용해 그 다음 쿼리들이 동작해야하는 경우가 있음

 

좀 더 구체적인 예를 들어보자

[ 스터디 그룹 모집 사이트 개발 하기 ] 프로젝트를 하는 상황인데 스터디 그룹 모집 글을 작성하고 다른 사용자들이 이 모집 글에 그룹원 신청을 하는 기능이 있다고 상상하자

 

이를 위해서 DB를 다음과 같이 구성했음

StudygroupInfo 테이블 : 스터디 그룹 모집 글 정보가 저장되는 테이블

StudygroupMember 테이블 : 스터디 그룹원의 정보가 저장되는 테이블

 

한 회원이 스터디 그룹 모집 글을 작성하면 StudygroupInfo 테이블에 모집 글 정보가 저장됨

모집 글을 작성한 사람은 스터디 그룹원(StudygroupMember)이므로 모집 글을 StudygroupInfo 테이블에 INSERT 후 바로 이어서 INSERT 한 모집 글의 idx를 사용해 StudygroupMember 테이블에 글을 작성한 회원의 정보를 저장해야함

 

이 처리를 구현한 쿼리, DAO, 컨트롤러를 보자

코드를 입력할 필요는 없고 코드만 보면 됨

 

<< 쿼리 >>

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

<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.StudygroupDao">
	<insert id="saveStudygroupInfo" parameterType="com.study.chapter04.StudygroupInfo">
		INSERT INTO studygroupInfo(title, contents, regDate) VALUES(#{title}, #{contents}, CURRENT_TIMESTAMP());
	</insert>
	
	<select id="getStudygroupIdx" parameterType="com.study.chapter04.StudygroupInfo" resultType="_int">
		SELECT idx FROM studygroupInfo WHERE id = #{id} AND pw = #{pw};
	</select>
	
	<insert id="joinStudygroupMember" parameterType="com.study.chapter04.StudygroupMember">
		INSERT INTO studygroupMember(studygroupIdx, memberIdx) VALUES(#{studygroupIdx}, #{memberIdx});
	</insert>
</mapper>

 

<< DAO >>

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StudygroupDao {
	void saveStudygroupInfo(StudygroupInfo studygroupInfo);
	int getStudygroupIdx(StudygroupInfo studygroupInfo);
	void joinStudygroupMember(StudygroupMember studygroupMember);
}

 

<< 컨트롤러 >>

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

import jakarta.servlet.http.HttpSession;

@Controller
public class StudygroupController {
	@Autowired
	private StudygroupDao studygroupDao;
	
	@PostMapping()
	public void saveStudygroupInfo(StudygroupInfo newStudygroupInfo, HttpSession session) {
		// 스터디 그룹 모집 글 저장
		studygroupDao.saveStudygroupInfo(newStudygroupInfo);
		
		// 로그인한 사용자 번호
		int loginUserIdx = (int) session.getAttribute("loginUserIdx");
		// 스터디 그룹 번호
		int studygroupIdx = studygroupDao.getStudygroupIdx(newStudygroupInfo);
		
		// 모집글을 작성한 사용자는 작성한 모집글의 스터디 그룹원이므로
		// 작성한 모집글의 스터디 그룹원으로 등록하기 위한 정보 생성
		StudygroupMember studygroupMember = new StudygroupMember();
		studygroupMember.setMemberIdx(loginUserIdx);
		studygroupMember.setMemberIdx(studygroupIdx);
		
		// 모집글을 작성한 사용자를 작성한 모집글의 스터디 그룹원으로 등록
		studygroupDao.joinStudygroupMember(studygroupMember);
	}
	
}

 

앞서  언급했듯 [ 스터디 그룹 모집글 작성 ] 이라는 하나의 처리를 하기 위해서 INSERT, SELECT, INSERT 쿼리가 순차적으로 동작해야함

 

이때, INSERT한 스터디 그룹 번호를 찾기 위해 SELECT를 하는데 이런 경우 여러 가지 문제가 생길 수 있는데 대표적으로 우연찮게 똑같은 제목, 내용의 모집 글이 있을 경우 올바른 스터디 그룹 번호를 조회할 수 없음


이렇게 방금 INSERT 한 데이터의 PK 값이 필요할 때 얻는 방법을 알아보자

 

방금 INSERT 한 데이터의 PK 값이 필요할 때는 insert 태그에 useGeneratedKeys 속성과 keyColumn 속성을 사용하면됨

아래와 같이 useGeneratedKeys 속성 값은 true 로 주고 keyColumn 속성은 PK 칼럼명을 주면 됨

 

이렇게 insert 태그를 구성하면 INSERT 후에 INSERT 한 데이터의 PK 값을 INSERT 할 때 매개변수로 전달한 DTO에 담아줌

 

 

바뀐 컨트롤러, DAO, 쿼리와 흐름을 한번에 보자

 

특히, 컨트롤러에서 INSERT 한 스터디 그룹 번호를 가져오기 위해 select가 빠졌다는 점에 주목하자

728x90
LIST

<< 학습 목표 >>

1. 쿼리 실행 결과로 데이터를 받을 수 있다.

2. 쿼리 실행 결과로 정보를 받을 수 있다.

3. 쿼리 실행 결과로 데이터들을 받을 수 있다.

4. 쿼리 실행 결과로 정보들을 받을 수 있다.


지금까지 과정을 보면 먼저 MyBatis 를 사용해 쿼리를 실행하고 결과를 받아오는 방법에 대해서 간단하게 배웠음 ( https://codingaja.tistory.com/115 ) 그 후 쿼리에 사용할 값에 대해서 자세히 배웠음 ( https://codingaja.tistory.com/116 )

 

이번에는 쿼리를 실행한 후 결괏값을 받는 방법을 배워보자

 

1. SELECT 쿼리를 실행했을 때는 SELECT 쿼리에 맞는 결괏값을 얻을 수 있음

ex) SELECT COUNT(*) AS count FROM member; 쿼리를 실행했다면 결괏값으로 정수를 얻을 수 있음

SELECT * FROM member WHERE nickname = "홍길동"; 쿼리를 실행했다면 결괏값으로 닉네임이 홍길동인 회원의 정보를 얻을 수 있음

SELECT * FROM member; 쿼리를 실행했다면 결괏값으로 회원들의 정보를 얻을 수 있음

 

2. INSERT 쿼리를 실행했을 때는 결괏값으로 정수 1을 얻을 수 있음

3. UPDATE 쿼리를 실행했을 때는 UPDATE 쿼리로 인해 값이 바뀐 행의 개수(정수)를 얻을 수 있음

4. DELETE 쿼리를 실행했을 때는 DELETE 쿼리로 인해 삭제된 행의 개수(정수)를 얻을 수 있음


이제 SELECT 쿼리부터 시작해서 쿼리를 실행한 후 결괏값을 받는 방법을 알아보자

우선 전 글 ( https://codingaja.tistory.com/116 ) 에서 가장 마지막에 사용했던 Member5.xml, MemberDao5, MyBatisController5 파일들을 복사하자

복사한 파일의 이름은 Member6.xml, MemberDao6, MyBatisController6 으로 지정하자

 

 

1. SELECT 쿼리를 실행했을 때는 SELECT 쿼리에 맞는 결괏값을 얻을 수 있음

  1-1. 결괏값이 정수 하나 일 때

더보기

결괏값으로 정수 한 개만 반환하는 쿼리를 작성하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao6">
	<select id="getMemberCount" resultType="_int">
		SELECT COUNT(*) AS count FROM member;
	</select>
</mapper>

 << 코드 설명 >>

(1). 이 쿼리를 실행 하고 나면 결과로 가입된 전체 회원의 수(정수 한 개)가 반환됨
(2). resultType 속성은 이 쿼리를 실행 하고 나면 반환될 값의 타입을 쓰는 것
  (1)에서 설명했듯 이 쿼리를 실행 하고 나면 정수가 반환 될 것이기에 정수(int)의 별칭인 _int 로 명시했음
  주의 할 점은 resultType="int" 로 쓰면 안됨
  int는 Integer의 별칭이고 _int는 int의 별칭임
  만약 Integer와 int에 대해서 잘모른다면 자바의 래퍼(Wrapper) 클래스에 대해서 다시 봐야함

 

이번에는 쿼리와 매칭될 DAO에 메서드를 선언하자

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao6 {
	int getMemberCount();
}

<< 코드 설명 >>

MemberDao6의 getMemberCount 메서드와 매치될 쿼리가 정수를 반환하므로 이 메서드의 반환 타입도 정수여야함

 

 

이번에는 컨트롤러를 만들자

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MyBatisController6 {
	@Autowired
	private MemberDao6 memberDao;
	
	@GetMapping("/chapter04/mybatis/v6/member/count")
	public void getMemberCount(SelectMemberDto selectMemberDto) {
		int count = memberDao.getMemberCount();
		
		System.out.println("가입된 전체 회원의 수 => " + count);
	}

}

 

이와 같이 SELECT 쿼리의 실행 결과가 정수 하나라면 select 태그의 resultType을 _int 로 명시해야하고 해당 쿼리와 매칭될 DAO 메서드는 반환 타입이 int 여야함

 

또한 SELECT 쿼리의 실행 결과가 정수 하나 외에 실수 하나, 문자열 하나 등 데이터 하나라면 select 태그의 resultType 속성의 값으로 그 데이터에 맞는 타입 명을 명시해줘야함

그리고 DAO의 메서드는 반환 타입으로 데이터에 맞는 타입을 써줘야함


  1-2. 결괏값이 한 행(정보 하나) 일 때

더보기

결괏값으로 정보 한 개만 반환하는 쿼리를 작성하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao6">
	// ...
    
	<select id="getMemberByNickname" parameterType="string" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{nickname};
	</select>
    
	// ...
</mapper>

( 쿼리가 점점 길어지므로 지금 상황에 필요한 쿼리만 가져왔음 )

<< 코드 설명 >>

(1). 이 쿼리는 해당 닉네임을 사용하는 회원 정보를 조회하므로 이 쿼리의 결과는 회원 정보 하나(한 행)임
(2). 이 쿼리에 사용할 값이 닉네임(문자열)이므로 parameterType 속성 값은 String의 별칭인 string으로 명시
(3). 이 쿼리의 결과가 회원 정보이므로 회원 정보를 받을 수 있는 DTO를 resultType 속성 값으로 명시

 

 

이번에는 쿼리와 매칭될 DAO에 메서드를 선언하자

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao6 {
	// ...
    
	SelectMemberDto getMemberByNickname(String nickname);
    
	// ...
}

<< 코드 설명 >>

MemberDao6의 getMemberByNickname 메서드와 매치될 쿼리가 SelectMemberDto(회원 정보 하나)를 반환하므로 이 메서드의 반환 타입도 똑같이 SelectMemberDto여야함

 

 

이번에는 컨트롤러를 만들자

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MyBatisController6 {
	@Autowired
	private MemberDao6 memberDao;
	
	// ...
	
	@GetMapping("/chapter04/mybatis/v6/member")
	public void getMemberByNickname(String nickname) {
		SelectMemberDto member = memberDao.getMemberByNickname(nickname);
		
		System.out.println("<< 닉네임이 [ " + nickname + " ] 인 회원의 정보 >>");
		System.out.println(member);
	}

	// ...
}

 

 

이와 같이 SELECT 쿼리의 실행 결과가 정보 하나라면 select 태그의 resultType을 정보를 받을 수 있는 DTO 로 명시해야하고 해당 쿼리와 매칭될 DAO 메서드의 반환 타입은 동일한 DTO로 지정해주면됨


  1-3. 결괏값이 여러 행(데이터들) 일 때

더보기

결괏값으로 문자열들(여러 행 / 데이터들)을 반환하는 쿼리를 작성하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao6">
	// ...
	
	<select id="getAllMemberNickname" resultType="string">
		SELECT nickname FROM member;
	</select>
    
	// ...
</mapper>

<< 코드 설명 >>

(1). 이 쿼리를 실행하면 모든 회원들의 닉네임(문자열들)을 반환하므로 resultType은 string 으로 명시함

 

 

이번에는 쿼리와 매칭될 DAO에 메서드를 선언하자

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;


@Mapper
public interface MemberDao6 {
	// ...
    
	List<String> getAllMemberNickname();
 
	// ...
}

<< 코드 설명 >>

앞서 [ 결괏값이 정수 하나 일 때 ], [ 결괏값이 한 행(정보 하나) 일 때 ] 는 resultType과 DAO 메서드의 반환 타입을 맞춰줬는데 [ 결괏값이 여러 행(데이터들) 일 때 ] 는 resultType과 DAO 메서드의 반환 타입이 다름

 

결국 MyBatis는 SELECT nickname FROM member; 쿼리를 실행한 후 결과를 DAO 메서드의 반환 타입에 맞춰서 문자열들을 반환함

 

 

이번에는 컨트롤러를 만들자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MyBatisController6 {
	@Autowired
	private MemberDao6 memberDao;
	
	// ...

	@GetMapping("/chapter04/mybatis/v6/member/nicknames")
	public void getAllMemberNickname() {
		List<String> nicknameList = memberDao.getAllMemberNickname();
		
		for(String nickname : nicknameList) {
			System.out.println(nickname);
		}
	}
    
	// ...
}

( 코드 설명이 필요 없을 정도로 간단하므로 코드 설명은 생략함 )

 

 

이와 같이 SELECT 쿼리의 실행 결과가 데이터들이라면 select 태그의 resultType은 데이터 타입으로 명시해야하고 해당 쿼리와 매칭될 DAO 메서드는 반환 타입이 List<데이터타입> 여야함


  1-3. 결괏값이 여러 행(정보들) 일 때

더보기

결괏값으로 회원들의 정보(여러 행 / 정보들)을 반환하는 쿼리를 작성하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao6">
	// ...
	
	<select id="getAllMember" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member;
	</select>
    
	// ...
</mapper>

 << 코드 설명 >>

이 쿼리를 실행하면 회원들의 정보를 반환하므로 resultType 속성 값은 회원 정보를 담을 수 있게 DTO로 명시해야함

 

 

이번에는 쿼리와 매칭될 DAO에 메서드를 선언하자

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao6 {
	// ...
    
	List<SelectMemberDto> getAllMember();
    
	// ...
}

<< 코드 설명 >>

앞서 [ 결괏값이 여러 행(데이터들) 일 때 ] 와 마찬가지로 resultType과 DAO 메서드의 반환 타입이 다름

 

결국 MyBatis는 SELECT * FROM member; 쿼리를 실행한 후 결과를 DAO 메서드의 반환 타입에 맞춰서 회원 정보들을 반환함

 

 

이번에는 컨트롤러를 만들자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MyBatisController6 {
	@Autowired
	private MemberDao6 memberDao;
	
	// ...
	
	@GetMapping("/chapter04/mybatis/v6/members")
	public void getAllMember() {
		List<SelectMemberDto> memberList = memberDao.getAllMember();
		
		for(SelectMemberDto member : memberList) {
			System.out.println(member);
		}
	}
    
	// ...
}

( 코드 설명이 필요 없을 정도로 간단하므로 코드 설명은 생략함 )

 

 

이와 같이 SELECT 쿼리의 실행 결과가 정보들이라면 select 태그의 resultType은 정보를 담을 수 있는 DTO로 명시해야하고 해당 쿼리와 매칭될 DAO 메서드는 반환 타입이 List<DTO타입> 이여야함



2. INSERT 쿼리를 실행했을 때는 결괏값으로 정수 1을 얻을 수 있음

이번에는 INSERT 쿼리를 작성하고 결괏값을 받아 보자

 

아래와 같이 Member6.xml 파일에 INSERT 쿼리를 추가하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao6">
	// ...
	
	<insert id="joinMember" parameterType="com.study.chapter04.InsertMemberDto">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{id}, #{pw}, #{nickname}, #{tel});
	</insert>
    
	// ...
</mapper>

<< 코드 설명 >>

SELECT와 연계해서 생각해보면 특이한 점이 있음

SELECT에서는 쿼리 결과가 정수 하나 라면 resultType 속성 값을 _int 로 명시해야했음

INSERT 쿼리 결과는 정수 1 이라고 했으므로 insert 태그에 resultType 속성 값을 _int 로 명시해야함

그러나 insert 태그에는 resultType 속성을 쓸 수 없음

insert 태그에는 resultType을 쓰지 않아도 insert 쿼리의 결과로 정수 1을 반환함

 

 

이번에는 INSERT 쿼리와 매치될 DAO 메서드를 추가하자

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao6 {
	// ...
    
	int joinMember(InsertMemberDto newMember);
    
	// ...
}

<< 코드 설명 >>

알고 있듯이 이 메서드를 호출했을 때 실행되는 쿼리가 정수 1을 반환하므로 정수를 받을 수 있게 반환 타입을 int로 지정했음

만약, INSERT 쿼리가 반환하는 정수 1이 필요 없으면 메서드의 반환 타입을 void 로 해도됨

 

 

이번에는 컨트롤러를 추가하자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyBatisController6 {
	@Autowired
	private MemberDao6 memberDao;
	
	// ...
	
	@PostMapping("/chapter04/mybatis/v6/member")
	public void joinMember(InsertMemberDto newMember) {
		int result = memberDao.joinMember(newMember);
		
		if(result == 1) {
			System.out.println("회원 가입 성공");
		} else {
			System.out.println("회원 가입 실패");
		}
	}
    
	// ...
}

( 코드 설명이 필요 없을 정도로 간단하므로 설명은 생략함 )


3. UPDATE 쿼리를 실행했을 때는 UPDATE 쿼리로 인해 값이 바뀐 행의 개수(정수)를 얻을 수 있음

4. DELETE 쿼리를 실행했을 때는 DELETE 쿼리로 인해 삭제된 행의 개수(정수)를 얻을 수 있음

 

이번에 배울건 설명이 필요 없을 정도로 간단하니 코드만 첨부하겠음

 

<< 회원 정보 수정(UPDATE 쿼리), 삭제(DELETE 쿼리) 쿼리 >>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao6">
	// ...
	
	<update id="updateMember" parameterType="com.study.chapter04.InsertMemberDto">
		UPDATE member SET pw = #{pw}, nickname = #{nickname}, tel = #{tel} WHERE id = #{id}
	</update>
	
	<delete id="deleteMember" parameterType="com.study.chapter04.DeleteMemberDto">
		DELETE FROM member WHERE id = #{id} AND pw = #{pw}
	</delete>
    
	// ...
</mapper>

 

<< DAO >>

package com.study.chapter04;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao6 {
	// ...
    
	int updateMember(InsertMemberDto newMember);
	int deleteMember(DeleteMemberDto targetMember);
    
	// ...
}

 

<< 컨트롤러 >>

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class MyBatisController6 {
	@Autowired
	private MemberDao6 memberDao;
	
	// ...
	
	@PutMapping("/chapter04/mybatis/v6/member")
	public void updateMember(InsertMemberDto newMember) {
		int result = memberDao.updateMember(newMember);
		
		if(result == 1) {
			System.out.println("회원 정보 수정 성공");
		} else {
			System.out.println("회원 정보 수정 실패");
		}
	}
	
	@DeleteMapping("/chapter04/mybatis/v6/member")
	public void deleteMember(DeleteMemberDto targetMember) {
		int result = memberDao.deleteMember(targetMember);
		
		if(result == 1) {
			System.out.println("회원 정보 삭제 성공");
		} else {
			System.out.println("회원 정보 삭제 실패");
		}
	}
    
	// ...
}

여기서 주의할 점은 INSERT 쿼리는 무조건 정수 1만 반환하지만 UPDATE, DELETE는 쿼리로 영향을 받은 행의 수를 반환함

INSERT 쿼리와 공통점은 UPDATE, DELETE 쿼리로 영향 받은 행의 수를 사용하지 않는다면 DAO 메서드의 반환 타입을 void 로 지정하면 됨

728x90
LIST

<< 학습 목표 >>

1. 다양한 형태의 값을 쿼리에 사용할 수 있다.


전 글 ( https://codingaja.tistory.com/115 ) 에서 쿼리에 필요한 값들을 DAO의 매개변수로 넘겨 사용했었음

 

이번에는 간단하지만 파라미터를 사용하는 다섯 가지 방법을 배워보자

1. #{param1}, #{param2}, #{param3} ... 처럼 #{param숫자} 로 파라미터를 사용할 수 있음

2. #{0}, #{1}, #{2} ... 처럼 #{숫자} 로 파라미터를 사용할 수 있음

3. @Param 어노테이션을 사용해 #{ } 안에 사용할 파라미터 명을 직접 지정할 수 있음

4. 해시맵의 키를 #{ } 안에 사용할 수 있음

5. DTO의 멤버 변수명을 #{ } 안에 사용할 수 있음


여기서 사용할 테이블, 쿼리, DAO, 컨트롤러는 모두 전 글까지 사용했던 것들을 사용할 것임

테이블 : Member 테이블

쿼리 : Member.xml 파일 내 INSERT, SELECT

컨트롤러 : MyBatisController

 

전 글에서는 파라미터를 사용하는 가장 나은 방법인 DTO를 사용했지만 이번에는 다양한 방법을 배우는 것이므로 1 ~ 4번 방법을 배울 때는 DTO가 빠짐


방법1. #{param1}, #{param2}, #{param3} ... 처럼 #{param숫자}

 

1. Member.xml 파일을 복사하고 파일명은 Member1 로 지정하자

 

Member1.xml 파일 내 코드를 아래와 같이 바꾸자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao1">
	<insert id="insertMember">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{param1}, #{param2}, #{param3}, #{param4});
	</insert>

	<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{param1};
	</select>
</mapper>

<< 코드 설명 >>

Member1.xml 파일 내 쿼리는 INSERT 쿼리(2)와 SELECT 쿼리(3)임

파라미터를 사용하는 방법이므로 SELECT 쿼리는 파라미터가 있는 닉네임으로 회원 정보 조회 SELECT 쿼리임

 

여기서 주의할 점은 이 쿼리 파일과 연결될 DAO는 com.study.chapter04 패키지 내 MemberDao1 임(1)

 

 

2. 쿼리 파일과 연결될 DAO를 추가하자

  DAO도 간단하게 복사하자

  com.study.chapter04 패키지 내 MemberDao 를 복사하고 인터페이스명은 MemberDao1 로 지정하자

 

MemberDao1 내 코드를 아래와 같이 바꾸자

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao1 {
	void insertMember(String id, String pw, String nickname, String tel);
	SelectMemberDto selectMember(String nickname);
}

<< 코드 설명 >>

- insertMember 메서드 : Member1.xml 파일 내 id 속성이 insertMember인 쿼리로 넘겨주는 파라미터는 총 4개임

  Member1.xml 파일 내 id 속성이 insertMember인 쿼리에서는 DAO가 전달하는 id 매개변수를 #{param1} 로 접근할 수 있고 pw 매개변수는 #{param2} 로 접근할 수 있음

  나머지 nickname, tel도 이와 같은 방식으로 접근할 수 있음

 

 - selectMember 메서드 : Member1.xml 파일 내 id 속성이 selectMember인 쿼리로 넘겨주는 파라미터는 총 1개임

  Member1.xml 파일 내 id 속성이 selectMember인 쿼리에서는 DAO가 전달하는 nickname 매개변수를 #{param1} 로 접근할 수 있음 ( 그림은 생략 )

 

 

3. DAO를 통해 DB와 통신할 컨트롤러를 추가하자

  컨트롤러 역시 간단하게 복사하자

  com.study.chapter04 패키지 내 MyBatisController를 복사하고 컨트롤러명은 MyBatisController1 로 지정하자

 

MyBatisController1 내 코드를 아래와 같이 바꾸자

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyBatisController1 {
	@Autowired
	private MemberDao1 memberDao;
	
	@GetMapping("/chapter04/mybatis/v1/member")
	public void getMemberByNickname(String nickname) {
		System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
		
		// 닉네임으로 회원 정보 조회
		SelectMemberDto member = memberDao.selectMember(nickname);
		
		// 조회한 회원 정보 출력
		System.out.println(member);
		
		System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
	}
	
	@PostMapping("/chapter04/mybatis/v1/member")
	public void insertMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 가입 시작 >>");
		
		String id = newMember.getId();
		String pw = newMember.getPw();
		String nickname = newMember.getNickname();
		String tel = newMember.getTel();

		// 회원 가입
		memberDao.insertMember(id, pw, nickname, tel);
		
		System.out.println("<< 회원 가입 성공 >>");
	}
}

<< 코드 설명 >>

컨트롤러에서 큰 변화는 없으니 주의만 하면 됨

(1). 사용할 DAO 인터페이스명을 MemberDao1 으로 바꾸자

(2). 컨트롤러간 URL이 충돌하지 않도록 URL의 중간에 v1 경로를 추가하자

(3). 이제 회원 가입을 할 때 DTO로 가입할 회원의 정보를 넘기는 방식이 아니므로 지금의 방식에 맞게 가입할 회원의 정보를 넘겨주기 위해서 id ~ tel까지 모두 꺼냄

(4). 가입할 회원의 정보를 DAO로 전달, DAO는 쿼리로 전달, 쿼리는 전달 받은 가입할 회원의 정보를 #{param1} ~ #{param4} 로 접근

이제 서버를 시작하고 회원 가입, 회원 정보 조회를 해보며 #{param1}, #{param2}, #{param3} ... 처럼 #{param숫자} 로 파라미터를 사용하는 방식이 제대로 동작하는지 확인해보자


방법2. #{0}, #{1}, #{2} ... 처럼 #{숫자}

 

1. Member1.xml 쿼리 파일을 복사하고 파일명을 Member2.xml 로 지정하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao2">
	<insert id="insertMember">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{0}, #{1}, #{2}, #{3});
	</insert>

	<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{0};
	</select>
</mapper>

 

2. MemberDao1 DAO를 복사하고 인터페이스명을 Member2 로 지정하자

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao2 {
	void insertMember(String id, String pw, String nickname, String tel);
	SelectMemberDto selectMember(String nickname);
}


3. MyBatisController1 컨트롤러를 복사하고 컨트롤러명을 MyBatisController2 로 지정하자

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyBatisController2 {
	@Autowired
	private MemberDao1 memberDao;
	
	@GetMapping("/chapter04/mybatis/v2/member")
	public void getMemberByNickname(String nickname) {
		System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
		
		// 닉네임으로 회원 정보 조회
		SelectMemberDto member = memberDao.selectMember(nickname);
		
		// 조회한 회원 정보 출력
		System.out.println(member);
		
		System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
	}
	
	@PostMapping("/chapter04/mybatis/v2/member")
	public void insertMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 가입 시작 >>");
		
		String id = newMember.getId();
		String pw = newMember.getPw();
		String nickname = newMember.getNickname();
		String tel = newMember.getTel();

		// 회원 가입
		memberDao.insertMember(id, pw, nickname, tel);
		
		System.out.println("<< 회원 가입 성공 >>");
	}
}

 

쿼리 파일, DAO, 컨트롤러 모두 설명이 필요 없을 정도로 간단하므로 여러분이 직접 방법1과 방법2를 비교해 공통점과 차이점을 찾아 익혀보자


방법3. @Param 어노테이션

 

1. Member1.xml 쿼리 파일을 복사하고 파일명을 Member3.xml 로 지정하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao3">
	<insert id="insertMember">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{newMemberId}, #{newMemberPw}, #{_nickname}, #{TEL});
	</insert>

	<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{nickname};
	</select>
</mapper>

 

2. MemberDao1 DAO를 복사하고 인터페이스명을 Member3 로 지정하자

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface MemberDao3 {
	void insertMember(@Param("newMemberId") String id, @Param("newMemberPw")String pw, @Param("_nickname") String nickname, @Param("TEL") String tel);
	SelectMemberDto selectMember(@Param("nickname") String nickname);
}


3. MyBatisController1 컨트롤러를 복사하고 컨트롤러명을 MyBatisController3 로 지정하자

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyBatisController3 {
	@Autowired
	private MemberDao3 memberDao;
	
	@GetMapping("/chapter04/mybatis/v3/member")
	public void getMemberByNickname(String nickname) {
		System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
		
		// 닉네임으로 회원 정보 조회
		SelectMemberDto member = memberDao.selectMember(nickname);
		
		// 조회한 회원 정보 출력
		System.out.println(member);
		
		System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
	}
	
	@PostMapping("/chapter04/mybatis/v3/member")
	public void insertMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 가입 시작 >>");
		
		String id = newMember.getId();
		String pw = newMember.getPw();
		String nickname = newMember.getNickname();
		String tel = newMember.getTel();

		// 회원 가입
		memberDao.insertMember(id, pw, nickname, tel);
		
		System.out.println("<< 회원 가입 성공 >>");
	}
}

방법4. 해시맵

 

1. Member1.xml 쿼리 파일을 복사하고 파일명을 Member4.xml 로 지정하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao4">
	<insert id="insertMember">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{item1}, #{item2}, #{nickname}, #{tel});
	</insert>

	<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{key};
	</select>
</mapper>

 

2. MemberDao1 DAO를 복사하고 인터페이스명을 Member4 로 지정하자

package com.study.chapter04;

import java.util.Map;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao4 {
	void insertMember(Map<String, String> newMember);
	SelectMemberDto selectMember(Map<String, String> filter);
}


3. MyBatisController1 컨트롤러를 복사하고 컨트롤러명을 MyBatisController4 로 지정하자

package com.study.chapter04;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyBatisController4 {
	@Autowired
	private MemberDao4 memberDao;
	
	@GetMapping("/chapter04/mybatis/v4/member")
	public void getMemberByNickname(String nickname) {
		System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
		
		Map<String, String> filter = new HashMap<>();
		filter.put("key", nickname);
		
		// 닉네임으로 회원 정보 조회
		SelectMemberDto member = memberDao.selectMember(filter);
		
		// 조회한 회원 정보 출력
		System.out.println(member);
		
		System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
	}
	
	@PostMapping("/chapter04/mybatis/v4/member")
	public void insertMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 가입 시작 >>");
		
		Map<String, String> filter = new HashMap<>();
		filter.put("item1", newMember.getId());
		filter.put("item2", newMember.getPw());
		filter.put("nickname", newMember.getNickname());
		filter.put("tel", newMember.getTel());

		// 회원 가입
		memberDao.insertMember(filter);
		
		System.out.println("<< 회원 가입 성공 >>");
	}
}

방법5. DTO

 

1. Member1.xml 쿼리 파일을 복사하고 파일명을 Membe5.xml 로 지정하자

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao5">
	<insert id="insertMember">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{id}, #{pw}, #{nickname}, #{tel});
	</insert>

	<select id="selectMember" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{nickname};
	</select>
</mapper>

 

2. MemberDao1 DAO를 복사하고 인터페이스명을 Member5 로 지정하자

package com.study.chapter04;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberDao5 {
	void insertMember(InsertMemberDto newMemberDto);
	SelectMemberDto selectMember(SelectMemberDto memberDto);
}


3. MyBatisController1 컨트롤러를 복사하고 컨트롤러명을 MyBatisController5 로 지정하자

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MyBatisController5 {
	@Autowired
	private MemberDao5 memberDao;
	
	@GetMapping("/chapter04/mybatis/v5/member")
	public void getMemberByNickname(SelectMemberDto selectMemberDto) {
		System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
		
		// 닉네임으로 회원 정보 조회
		SelectMemberDto member = memberDao.selectMember(selectMemberDto);
		
		// 조회한 회원 정보 출력
		System.out.println(member);
		
		System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
	}
	
	@PostMapping("/chapter04/mybatis/v5/member")
	public void insertMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 가입 시작 >>");
		
		// 회원 가입
		memberDao.insertMember(newMember);
		
		System.out.println("<< 회원 가입 성공 >>");
	}
}

여기까지 쿼리가 사용할 값으로 다양한 형태의 값을 보내고 사용하는 방법을 알아봤음

 

쿼리가 값을 사용할 때 쿼리를 감싸고 있는 태그에 parameterType을 명시해줘야함

그러나 우리는 parameterType을 명시하지 않았음

parameterType을 명시하지 않으면 MyBatis가 쿼리와 연결될 DAO의 메서드의 매개변수 타입을 보고 알아서 채워넣음

이는 좋지 못함

 

이 글의 마지막으로 상황별 적절한 parameterType을 지정하는 방법을 알아보자

 

방법1. 쿼리에서 #{param1}, #{param2}, #{param3} ... 처럼 #{param숫자} 로 값을 사용할 때는 쿼리를 감싸고 있는 태그에 parameterType을 명시할 수 없음

 

방법2. 쿼리에서 방법2. #{0}, #{1}, #{2} ... 처럼 #{숫자} 로 값을 사용할 때 역시 쿼리를 감싸고 있는 태그에 parameterType을 명시할 수 없음

 

방법3. 쿼리에서 @Param 어노테이션 으로 값을 사용할 때 역시 쿼리를 감싸고 있는 태그에 paramterType을 명시할 수 없음

 

방법1 ~ 방법3까지는 쿼리에서 값을 사용할 때 parameterType을 명시할 수 없음

방법1 ~ 방법3까지는 실제 개발에서 아예 라고 할 수 있을 정도로 거의 사용하지 않는 방법임

우리는 파라미터를 사용하는 방법을 배우고 있기 때문에 쿼리가 단순해서 방법1 ~ 방법3 을 사용해 쿼리를 만든다고 해도 가독성이 떨어지지 않지만 실무에서는 방법1 ~ 방법3 을 사용해 쿼리를 만들면 가독성이 심하게 떨어지기 때문...

 

 

실무에서는 방법4 또는 방법5를 많이 사용하며 특히, 방법5가 일반적임

 

방법4. 쿼리에서 해시맵 으로 값을 사용할 때는 parameterType을 hashmap 으로 명시해야함

Member4.xml 파일에 들어있는 쿼리들이 해시맵을 사용하므로 Member4.xml 내 insert, select 태그를 정확하게 사용하려면 다음과 같이 사용해야함

<mapper namespace="com.study.chapter04.MemberDao4">
	<insert id="insertMember" parameterType="HashMap">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{item1}, #{item2}, #{nickname}, #{tel});
	</insert>

	<select id="selectMember" parameterType="hashmap" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{key};
	</select>
</mapper>

프로그래밍은 대소문자를 엄격히 구분하기 때문에 parameterType을 쓸 때 hashmap 은 대소문자를 정확히 맞춰 입력해야함

 

 

방법5. 쿼리에서 DTO 로 값을 사용할 때는 parameterType을 DTO의 전체 경로인 패키지명.클래스명 으로 명시해야함

Member5.xml 파일에 들어있는 쿼리들이 DTO를 사용하므로 Member5.xml 내 insert, select 태그를 정확하게 사용하려면 다음과 같이 사용해야함

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.chapter04.MemberDao5">
	<insert id="insertMember" parameterType="com.study.chapter04.InsertMemberDto">
		INSERT INTO member(id, pw, nickname, tel) VALUES(#{id}, #{pw}, #{nickname}, #{tel});
	</insert>

	<select id="selectMember" parameterType="com.study.chapter04.SelectMemberDto" resultType="com.study.chapter04.SelectMemberDto">
		SELECT * FROM member WHERE nickname = #{nickname};
	</select>
</mapper>

 

이외에도 쿼리에서 다양한 타입의 값을 사용할 수 있음

쿼리에 사용할 수 있는 타입명은 아래 표를 참고하자

데이터 타입명
쿼리에서 사용할 값이 [ boolean ] 이라면 paramterType은 [ _boolean ] 으로 명시
쿼리에서 사용할 값이 [ byte ] 라면 paramterType[ _byte ] 명시
쿼리에서 사용할 값이 [ short ] 이라면 paramterType[ _short ]  명시
쿼리에서 사용할 값이 [ int ] 라면 paramterType [ _int ]  명시
쿼리에서 사용할 값이 [ long ] 이라면 paramterType [ _long ] 으로 명시
쿼리에서 사용할 값이 [ float ] 이라면 paramterType [ _float ] 으로 명시
쿼리에서 사용할 값이 [ double ] 이라면 paramterType [ _double ] 으로 명시
쿼리에서 사용할 값이 [ Boolean ] 이라면 paramterType [ boolean ] 으로 명시
쿼리에서 사용할 값이 [ Byte ] 라면 paramterType [ byte ]  명시
쿼리에서 사용할 값이 [ Short ] 이라면 paramterType [ short ]  명시
쿼리에서 사용할 값이 [ Integer ] 라면 paramterType [ int ]  명시
쿼리에서 사용할 값이 [ Long ] 이라면 paramterType [ long ] 으로 명시
쿼리에서 사용할 값이 [ Float ] 이라면 paramterType [ float ] 으로 명시
쿼리에서 사용할 값이 [ Double ] 이라면 paramterType [ double ] 으로 명시
쿼리에서 사용할 값이 [ Object ] 라면 paramterType [ object]  명시
쿼리에서 사용할 값이 [ String ] 이라면 paramterType [ string ] 으로 명시
쿼리에서 사용할 값이 [ Date ] 라면 paramterType [ date ]  명시
쿼리에서 사용할 값이 [ Map ] 이라면 paramterType [ map ] 으로 명시
쿼리에서 사용할 값이 [ HashMap ] 이라면 paramterType [ hashmap ] 으로 명시
쿼리에서 사용할 값이 [ List ] 라면 paramterType [ list ]  명시
쿼리에서 사용할 값이 [ ArrayList ] 라면 paramterType [ arraylist ]  명시
쿼리에서 사용할 값이 [ Collection ] 이라면 paramterType [ collection ] 으로 명시
쿼리에서 사용할 값이 [ Iterator ] 라면 paramterType [ iterator ]  명시
쿼리에서 사용할 값이 [ ResultSet ] 이라면 paramterType [ ResultSet ] 으 명시

이 표는 외울 필요는 없음

그리고 어떻게 활용하는지에 대해서는 곧 배우게 될 것

728x90
LIST

<< 학습 목표 >>

1. DB에 접속하기 위한 라이브러리를 추가할 수 있다

2. DB에 접속하기 위한 설정 값을 추가할 수 있다.

3. 원하는 쿼리를 실행하고 결과를 받아올 수 있다.


이번 챕터를 시작하기 전에 프로젝트 -> com.study.chapter04 패키지를 추가하고 시작하자

 

또한 practice DB를 생성(1)한 후 그 안에 아래와 같이 member 테이블을 추가(2)하자

 

위 프로그램은 HeidiSQL로 Mysql Workbench 를 사용한다면 DB가 아니라 Schema 라고 부를 것

따라서 Mysql Workbench 를 사용한다면 [ Create Schema ] 를 통해 practice Schema 를 생성한 후 그 안에 member 테이블을 추가하자


이번 챕터에서는 DB와 관련된 것을 배울 것

먼저, 자주 사용하지는 않지만 DB에 쿼리를 보내고 결과를 받아오는 방법 중 하나인 JdbcTemplate을 사용해보고 그 다음에는 자주 사용하는 방법은 Mybatis를 사용해보자


서블릿에서는 DB에 쿼리를 보내고 결과를 받아오기 위해 JDBC를 직접 사용했었음

 

간단한 SELECT 문 조차 아래와 같이 깨알 같은 코드를 입력해야함

여기서 SELECT 쿼리와 직접 연관된 코드는 단 몇 줄이지만 SELECT 쿼리를 실행하고 결과를 받아오기 위해 부가적인 코드가 수 십 줄이 붙고 그 수 십 줄의 코드 중 하나라도 오타가 있어도 안되고 빠져도 안됐음 

( Spring Framework는 JSP, Servlet이 필수 선행되야하므로 위 코드는 이미 잘 알고 있을 것, 따라서 자세하게 설명하진 않음 )

 

이번에는 Spring Framework 프로젝트에서 위와 똑같이 SELECT 쿼리를 실행하고 결과를 받아오는 코드를 보자

 

서블릿에서 DB와 통신하기 위한 코드랑 SpringFramework에서 DB와 통신하기 위한 코드를 비교해보면 비교도 안되게 짧아진 걸 알 수 있음

 

특히 인상적인 부분은 서블릿은 DB와 통신하기 위한 자원들( Connection, PreparedStatement, ResultSet 등 ) 을 반드시 해제 해줘야했지만 SpringFramework는 그런 부분이 전혀 없음

 

이런 점들 때문에 서블릿 대신 SpringFramework를 사용하는 이유임


이제 본격적으로 SpringFramework에서 DB와 통신을 해보자

 

SpringFramework에서 DB와 통신하기 위해서는 두 가지 라이브러리를 추가해야함

1. spring-boot-starter-jdbc

2. mariadb connector/j

 

이 라이브러리는 서블릿에서 추가 했던 라이브러리와 같음

서블릿에서도 DB와 통신하기 위해서 JDBC 또는 DBCP를 추가해야했고 mariadb connector/j 를 추가해야했음

spring-boot-starter-jdbc 는 HikariCP 라고하는 DBCP를 사용하는 JDBC임

 

maven repository에서 spring-boot-starter-jdbc 를 추가하자

인터넷에서 maven repository 검색(1) 후 해당 사이트로 접속(2) 후 spring-boot-starter-jdbc 검색(3) 후 Spring Boot Starter JDBC 라이브러리 페이지로 들어감(4)

 

라이브러리 URL에서 버전값은 지울 것이기 때문에 어떤 버전을 사용해도 상관 없음

아무 버전 중 하나를 선택(1)해 들어가 라이브러리 URL(2) 복사

 

이제 sts로 돌아가 프로젝트 -> pom.xml의 dependencies 에 복사한 URL 붙여넣기

그 후 version 태그는 삭제하자

 

 

이번에는 mariadb connector/j 라이브러리를 추가하자

maven repository 에서 mariadb 로 검색(1) 후 MariaDB Java Client 라이브러리 페이지(2)로 들어가자

mariadb connector/j 라이브러리 역시 라이브러리 URL에서 버전값은 지울 것이기 때문에 어떤 버전을 사용해도 상관 없음

아무 버전 중 하나를 선택(1)해 들어가 라이브러리 URL(2) 복사

이제 sts로 돌아가 프로젝트 -> pom.xml의 dependencies 에 복사한 URL 붙여넣기

그 후 version 태그는 삭제하자


여기까지 DB와 통신하기 위한 라이브러리들을 추가했으니 이제 본격적으로 DB와 통신할 코드를 작성하자

! 한가지 주의할 점 ! SpringFramework에서 JDBC Template 을 사용해 DB와 통신하는 경우는 거의 없음

주로 JPA라는 ORM Framework 또는 MyBatis라는 SQL Mapper를 사용하므로 이번에 배울 JDBC Template은 간단하게만 보고 넘어갈 것

 

SpringFramework에서 DB와 통신하려면 DB에 접속하기 위한 설정값이 필요한데 이는 프로젝트 -> src/main/resources -> application.properties에 적어둠

(1). JDBC 드라이버 명

(2). 접속할 DB 경로

(3). DB 사용자명

(4). DB 사용자 비밀번호

 

이 설정값들 또한 JSP/Servlet에서 DB와 통신하기 위한 설정값과 똑같으니 상세하게 설명하진 않겠음


이번에는 클라이언트의 요청을 받고 요청을 처리할 서비스 메서드를 호출하기 위한 컨트롤러를 추가하자

프로젝트 -> com.study.chapter04 -> JdbcTemplateController 클래스 추가 후 아래 코드를 추가하자

package com.study.chapter04;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class JdbcTemplateController {

	@GetMapping("/chapter04/members")
	public void getAllMembers() {
		
	}
	
	@GetMapping("/chapter04/member")
	public void getMemberByNickname() {
		
	}
	
	@PostMapping("/chapter04/member")
	public void insertMember() {
		
	}
	
	@PutMapping("/chapter04/member")
	public void updateMember() {
		
	}
	
	@DeleteMapping("/chapter04/member")
	public void deleteMember() {
		
	}
	
}

<< 코드 설명 >>

이 컨트롤러는 아직 완성된 컨트롤러가 아님

어떤 컨트롤러를 만들려고 하는지 전체적인 뼈대를 보여주는 코드

 

1. 클라이언트가 모든 회원의 정보를 조회하고 싶을 때 접근하는 컨트롤러

 

2. 클라이언트가 닉네임으로 특정 회원의 정보를 조회하고 싶을 때 접근하는 컨트롤러

 

3. 클라이언트가 회원 정보를 저장하고 싶을 때 접근하는 컨트롤러

 

4. 클라이언트가 회원 정보를 수정하고 싶을 때 접근하는 컨트롤러

 

5. 클라이언트가 회원 정보를 삭제하고 싶을 때 접근하는 컨트롤러

 

 

 

 

 

 

 

 

 

 

 

 

 

 

가장 먼저 3번 회원 정보 저장 ( 회원 가입 ) 컨트롤러를 구현하자

더보기

member 테이블을 보면 회원 가입을 하려면 클라이언트는 id, pw, nickname, tel 값을 보내야함

( idx, joinDateTime 칼럼의 값은 기본값이 들어있고 isDel 칼럼은 NULL 허용이고 기본값이 NULL이므로 )

 

 

클라이언트가 보낸 값을 서버에서 받으려면 DTO가 필요하니 DTO를 추가하자

프로젝트 -> com.study.chapter04 -> InsertMemberDto 클래스 를 추가하고 아래 코드를 추가하자

package com.study.chapter04;

import lombok.Data;

@Data
public class InsertMemberDto {
	private String id;
	private String pw;
	private String nickname;
	private String tel;
}

 

이제 insertMember 컨트롤러에 InsertMemberDto를 매개변수로 넣자

 ( 전체 컨트롤러의 코드가 길어 해당 컨트롤러 코드만 썼음 )

@PostMapping("/chapter04/member")
public void insertMember(InsertMemberDto newMember) {
	System.out.println(newMember);
}

 

우선 Sysout으로 클라이언트가 보낸 값들을 잘 받았는지 확인해보자

이제 [ Boot Dashboard ] 를 통해 서버를 실행시키고 Postman으로 해당 컨트롤러로 id, pw, nickname, tel 값을 보내자

 

이제 컨트롤러의 코드를 완전하게 채워 회원 가입 ( 클라이언트가 보낸 회원 정보를 DB에 저장 ) 하자

컨트롤러의 전체 코드가 길어 필요한 코드만 첨부함

package com.study.chapter04;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class JdbcTemplateController {
	@Autowired
	JdbcTemplate jt;
    
    // ...
    
	@PostMapping("/chapter04/member")
	public void insertMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 가입 시작 >>");
		
		String id = newMember.getId();
		String pw = newMember.getPw();
		String nickname = newMember.getNickname();
		String tel = newMember.getTel();
		
		String query = "INSERT INTO member(id, pw, nickname, tel) VALUES(?, ?, ?, ?)";
		
		jt.update(query, id, pw, nickname, tel);
		
		System.out.println("<< 회원 가입 성공 >>");
	}
    
    // ...
    
}

 << 코드 설명 >>

(1). INSERT 쿼리를 실행시킬 때는 update 메서드를 사용함
      이때 INSERT 쿼리의 ? 는 update 메서드의 두 번째 매개변수부터 차례대로 할당됨

Servlet을 배울 때 사용했던 PreparedStatement를 떠올려보면 PreparedStatement 보다 JdbcTemplate이 훨씬 사용하기 편하다는걸 느낄 수 있을 것

 

 

서버를 재시작하고 Postman 을 사용해 서버로 다시 데이터를 보내 회원 가입을 해보자

아직 회원 정보 조회 컨트롤러를 구현하지 않았으므로 직접 테이블 내 데이터를 조회해 회원 가입이 정상적으로 이뤄졌는지 확인하자

 

회원 가입 컨트롤러를 통해 아래 회원 정보도 추가하자

id = id2, pw = pw2, nickname = 홍길동, tel = 010-2222-2222

id = id3, pw = pw3, nickname = 고영희, tel = 010-3333-3333



 

이번에는 1번 전체 회원 조회 컨트롤러를 구현하자

더보기

전체 회원 정보를 조회할 때 클라이언트가 보낼 값은 없음

대신 SELECT 쿼리를 사용해 회원 정보를 조회한 후 JDBC Template이 조회한 회원들의 정보를 DTO에 담아서 반환해야하므로 조회한 회원들의 정보를 담을 DTO인 SelectMemberDto 를 추가하자

 

프로젝트 -> com.study.chapter04 -> SelectMemberDto 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter04;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class SelectMemberDto {
	private String id;
	private String pw;
	private String nickname;
	private String tel;
	private int idx;
	private LocalDateTime joinDateTime;
	private boolean isDel;
	
	public boolean getIsDel() {
		return isDel;
	}
	
	public void setIsDel(boolean isDel) {
		this.isDel = isDel;
	}
}

! 잠시 ! SelectMemberDto에 대해서 자세히 알아보고 넘어가자

isDel 멤버 변수는 getter, setter를 직접 선언했는데 Lombok은 is 로 시작하는 멤버 변수의 경우 getter 메서드명을 멤버 변수명 그대로 isDel 로 만들어줌

또한 setter 메서드명은 is를 뺀 setDel 로 만들어줌

그렇게 되면 isDel 칼럼 값을 isDel 멤버 변수에 저장할 수 없음

따라서 is 로 시작하는 멤버 변수의 경우 위와 같이 getIsDel, setIsDel 처럼 직접 getter, setter 메서드를 선언해줘야함

 

다시 본론으로 돌아와서 전체 회원 정보를 조회하는 컨트롤러를 완성시켜 전체 회원 정보를 조회하자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class JdbcTemplateController {
	@Autowired
	JdbcTemplate jt;
	
	@GetMapping("/chapter04/members")
	public void getAllMembers() {
		System.out.println("<< 회원 정보 조회 시작 >>");
		
		String query = "SELECT * FROM member";
		
		List<SelectMemberDto> memberList = jt.query(query, new BeanPropertyRowMapper<SelectMemberDto>(SelectMemberDto.class));
		for(SelectMemberDto member : memberList) {
			System.out.println(member);
		}
		
		System.out.println("<< 회원 정보 조회 종료 >>");
	}
    
	// ...
    
}

<< 코드 설명 >>

(1). SELECT 쿼리를 실행할 때는 query 메서드 또는 queryForObject 메서드 중 한 메서드를 호출함
       query 메서드는 조회 결과가 여러 개 일 때 사용하는 메서드
       queryForObject 메서드는 조회 결과가 하나일 때 사용하는 메서드
      전체 회원 정보를 조회할 것이므로 query 메서드를 호출했음
      전체 회원 정보를 조회한 후 조회한 회원 정보를 담기 위해 query 메서드의 두 번째 매개변수로 BeanPropertyRowMapper 클래스의 인스턴스를 넣었음
      BeanPropertyRowMapper 클래스는 제네릭스가 적용된 클래스로 인스턴스를 생성할 때 제네릭 타입을 지정해야함
      조회 결과를 담을 DTO를 BeanPropertyRowMapper 클래스의 제네릭 타입으로 지정하면 됨

 

 

서버를 재시작 한 후 Postman으로 이 컨트롤러를 호출해 전체 회원 정보가 조회되는지 확인해보자



이번에는 2번 닉네임으로 특정 회원의 정보를 조회 컨트롤러를 구현하자

더보기

클라이언트가 보낸 닉네임으로 특정 회원의 정보를 조회해야하므로 컨트롤러에서 클라이언트가 보낸 닉네임을 받아야함

클라이언트가 정보(데이터들)를 보낸다면 DTO로 받아야함

그래서 3번 회원 정보 저장 컨트롤러에서는 클라이언트가 보낸 정보를 DTO로 받았음

 

이번에는 클라이언트가 데이터(닉네임)을 보낼것이기 때문에 컨트롤러에 String 타입 매개변수 하나만 있으면 됨

 

컨트롤러에 매개변수가 필요하다는걸 인지하면서 아래와 같이 컨트롤러를 완성하자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class JdbcTemplateController {
	@Autowired
	JdbcTemplate jt;
    
	@GetMapping("/chapter04/member")
	public void getMemberByNickname(String nickname) {
		System.out.println("<< 닉네임으로 회원 정보 조회 시작 >>");
		
		String query = "SELECT * FROM member WHERE nickname = '" + nickname + "'";
		
		SelectMemberDto member = jt.queryForObject(query, new BeanPropertyRowMapper<SelectMemberDto>(SelectMemberDto.class));
		
		System.out.println(member);
		
		System.out.println("<< 닉네임으로 회원 정보 조회 종료 >>");
	}
    
	// ...
}

<< 코드 설명 >>

(1). 이번에는 조회 결과가 하나이므로 SELECT 쿼리를 실행할 때 queryForObject 메서드를 사용했음
      이 메서드 역시 조회 결과를 담을 DTO를 두 번째 매개변수로 넣어줌
      특히, 이 메서드의 경우 조회 결과가 없으면 예외가 발생함
      예외가 발생하지 않도록 할 수도 있고 예외가 발생했을 다면 try ~ chatch 로 처리할 수도 있지만 이 글의 맨 처음에 언급했듯 JDBCTemplate은 거의 사용하지 않으므로 "그렇구나" 정도로만 생각하고 넘어가면 됨

 

 

서버를 재시작한 후 닉네임으로 회원 정보를 조회해보자



 

이번에는 4번 회원 정보 수정 컨트롤러를 구현하자

더보기

회원 정보 수정에서는 아이디는 변경할 수 없고 비밀번호, 닉네임, 연락처는 변경할 수 있게 구현하자

클라이언트는 정보를 수정할 회원의 아이디, 변경할 비밀번호, 변경할 닉네임, 변경할 연락처를 보내야함
비밀번호, 닉네임, 연락처 중 변경하지 않는것이 있다면 원래 데이터를 그대로 담아 보내야한다고 상황을 설정하자

 

이와 같이 회원 정보 수정 컨트롤러를 구현하려면 클라이언트가 수정할 회원의 정보를 보내므로 컨트롤러에서는 클라이언트가 보낸 값을 DTO로 받으면 됨

DTO는 회원 정보 때 구현한 InsertMemberDto를 그대로 활용하자

 

이제 아래와 같이 컨트롤러를 완성하자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class JdbcTemplateController {
	@Autowired
	JdbcTemplate jt;
    
	@PutMapping("/chapter04/member")
	public void updateMember(InsertMemberDto newMember) {
		System.out.println("<< 회원 정보 수정 시작 >>");
		
		String id = newMember.getId();
		String pw = newMember.getPw();
		String nickname = newMember.getNickname();
		String tel = newMember.getTel();
		
		String query = "UPDATE member SET pw = ?, nickname = ?, tel = ? WHERE id = ?";
		
		jt.update(query, pw, nickname, tel, id);
		
		System.out.println("<< 회원 정보 수정 성공 >>");
	}
    
	// ...
}

 << 코드 설명 >>

(1). UPDATE 쿼리 역시 INSERT 쿼리와 같이 update 메서드를 사용해 실행함
     또한 ? 역시 INSERT 쿼리와 같이 두 번째 매개변수부터 순서대로 할당됨

 

 

서버를 재시작한 후 회원 정보 수정 컨트롤러가 정상적으로 동작하는지 확인해보자



 

이번에는 5번 회원 정보 삭제 컨트롤러를 구현하자

더보기

회원 정보를 삭제할 때는 클라이언트가 삭제할 회원의 아이디와 비밀번호를 보내야 한다고 하자

 

클라이언트가 삭제할 회원의 정보(아이디, 비밀번호) 를 보내므로 컨트롤러에서는 클라이언트가 보낸 정보를 DTO에 담아야하지만 클라이언트가 보내는 데이터가 2개(아이디, 비밀번호) 밖에 없으므로 매개 변수를 두 개 선언해도 됨

 

최대한 일관성을 유지하기 위해 DTO로 받도록 프로젝트 -> com.study.chapter04 -> DeleteMemberDto 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter04;

import lombok.Data;

@Data
public class DeleteMemberDto {
	private String id;
	private String pw;
}

 

이제 컨트롤러를 구현하자

package com.study.chapter04;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

@Controller
public class JdbcTemplateController {
	@Autowired
	JdbcTemplate jt;
    
	@DeleteMapping("/chapter04/member")
	public void deleteMember(DeleteMemberDto deleteMember) {
		System.out.println("<< 회원 정보 삭제 시작 >>");
		
		String id = deleteMember.getId();
		String pw = deleteMember.getPw();
		
		String query = "DELETE FROM member WHERE id = ? AND pw = ?";
		
		jt.update(query, id, pw);
		
		System.out.println("<< 회원 정보 삭제 성공 >>");
	}
    
	// ...
	
}

<< 코드 설명 >>

INSERT, UPDATE와 동일하므로 별도의 설명은 생략함

 

서버를 재시작한 후 회원 정보 삭제가 정상적으로 이뤄지는지 확인해보자



여기까지 거의 사용하진 않지만 SpringFramework가 제공하는 JDBCTemplate을 사용해 쿼리를 실행하고 결과를 받아오는 방법을 봤음

 

JSP/Servlet을 배우며 쿼리를 실행하고 결과를 받아오는 코드와 비교해보면 굉장히 간단해졌다는 걸 알 수 있음

지금까지 SpringFramework를 사용하는 이유를 찾지 못했다면 여기서는 확 느꼈을 것

 

꼭 ! "그렇다니까 그런가보다' 하고 넘어가지 말고 JSP/Servlet을 배우며 쿼리를 실행하고 결과를 받아오는 코드와 JDBCTemplate을 사용해 쿼리를 실행하고 결과를 받아오는 코드를 비교해보자

728x90
LIST

<< 학습 목표 >>

1. XX

2. XX


이번에는 코드를 더 발전시켜보자

프로젝트 -> com.study.chapter03 -> Ex05 소스 파일을 추가하고 아래 코드를 추가하자

package com.study.chapter03;

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

public class Ex05 {
	
	public static void main(String[] args) {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
		
		GalaxyS23 phone = (GalaxyS23) ctx.getBean("galaxyS23");
		
		System.out.println("<< phone >>");
		phone.printBatteryInfo();
	}

}

 

그리고 실행시켜보면 문구가 이상하다는걸 알 수 있음

스마트폰의 배터리를 다 썼다고해서 교체하는 사람은 없을 것

스마트폰의 배터리를 다 쓰면 충전하는게 일반적

 

우리가 만든 스마트폰은 베터리가 건전지였을 때는 베터리의 정보를 출력하면 "교체하세요." 문구가 적절했지만 이제 리튬 베터리로 바뀌어서 "교체하세요." 대신 "충전하세요." 로 문구를 바꿔야함

 

스마트폰 베터리는 나중에라도 더 효율적인 베터리로 업그레이드가 될 가능성이 있으므로 가장 적은 노력으로 문구를 바꾸는 방법은 자바에서 배운 클래스, 인터페이스, 상속 등을 활용하는 것

다형성, 클래스, 인터페이스에 대한건 기초인 자바에서 배우는 것들이므로 여기서는 설명하진 않겠음

 

프로젝트 -> com.study.chapter03 -> BatteryType 인터페이스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public interface BatteryType {
	void printBatteryInfo();
}

 

프로젝트 -> com.study.chapter03 -> NormalBattery 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public class NormalBattery implements BatteryType {

	@Override
	public void printBatteryInfo() {
		System.out.println("배터리는 건전지입니다.");
		System.out.println("배터리를 모두 사용했다면 건전지를 교체하세요.");
	}

}

 

프로젝트 -> com.study.chapter03 -> LithiumBattery 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public class LithiumBattery implements BatteryType {

	@Override
	public void printBatteryInfo() {
		System.out.println("배터리는 리튬 배터리입니다.");
		System.out.println("배터리를 모두 사용했다면 배터리를 충전하세요.");
	}

}

 

BatteryType 인터페이스는 베터리가 되기 위한 클래스는 반드시 구현해야하는 인터페이스로 정의했음

이 인터페이스를 통해 배터리에 다형성을 적용할 수 있게 됐음

그 후 건전지를 표현한 NormalBattery 클래스와 리튬 배터리를 표현한 LithiumBattery 클래스를 선언했음

NormalBattery 클래스는 건전지에 맞는 배터리 정보를 출력하도록 printBatteryInfo 메서드를 구현했음

LithiumBattery 클래스는 리튬 배터리에 맞는 배터리 정보를 출력하도록 printBatteryInfo 메서드를 구현했음

 

 

이제 각 스마트폰에서는 문자열 대신 인터페이스를 활용해 다형성이 적용된 베터리 타입을 갖고(1) 베터리 유형에 맞는 정보가 출력(2)되도록 했음

 

스마트폰을 생성하고 의존 주입을 해주는 Config 컨테이너의 코드도 상황에 맞게 바꾸자

여기서는 우선 스마트폰의 베터리를 건전지로 지정했음

package com.study.chapter03;

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

@Configuration
public class Config {
	private BatteryType batteryType = new NormalBattery();
	
	@Bean
	public GalaxyS23 galaxyS23() {
		return new GalaxyS23(batteryType);
	}
	
	@Bean
	public GalaxyFlip4 galaxyFlip4() {
		return new GalaxyFlip4(batteryType);
	}
	
	@Bean
	public IPhone14 iPhone14() {
		return new IPhone14(batteryType);
	}
}

<< 코드 설명 >>

(1). 스마트폰이 의존 하고 있는 의존 객체 생성 / 이때 의존 객체로 건전지 정보를 갖고 있는 배터리를 생성했음

(2), (3), (4). 건전지 정보가 주입 된 빈을 컨테이너에 등록

 

 

다시 Ex05 소스 파일로 돌아가 프로그램을 실행시켜보자

당연히 "건전지를 교체하세요" 문구가 뜸

package com.study.chapter03;

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

public class Ex05 {

	public static void main(String[] args) {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
		
		GalaxyS23 phone = (GalaxyS23) ctx.getBean("galaxyS23");
		
		System.out.println("<< phone >>");
		phone.printBatteryInfo();
	}

}

 

 

스마트폰의 배터리를 리튬 배터리로 교체해야한다면 잘 알고 있듯 이와 같이 배터리를 교체하면 됨

 

지금까지 @Configuration 애너테이션과 @Bean 애너테이션을 사용해 Spring Framework가 제공하는 DI, IoC를 사용했음

@Configuration 애너테이션을 사용해 컨테이너를 생성하고 @Bean 애너테이션을 사용해 프로젝트에 필요한 빈 객체를 등록했음

 

특히, 컨테이너에 의존 객체를 생성하도록 직접 코드를 썼고 의존 주입이 된 빈을 등록하도록 직접 코드를 썼음 


Spring Framework의 장점 중 하나는 직접 입력해야 할 코드를 애너테이션으로 대체할 수 있다는 점

 

현재 우리는 세 종류의 스마트폰(GalaxyS23, GalaxyFlip4, IPhone14)를 컨테이너에 빈으로 등록하고 있는데 이렇게 컨테이너에 빈으로 등록하고 싶은 클래스가 있다면 클래스 명 위에 @Component 애너테이션(1)을 붙이면 더 간편하게 빈으로 등록할 수 있음

package com.study.chapter03;

import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@Data
public class GalaxyS23 {
	private BatteryType batteryType;
	
	public GalaxyS23(BatteryType batteryType) {
		this.batteryType = batteryType;
	}

	public void printBatteryInfo() {
		batteryType.printBatteryInfo();
	}
}

<< 코드 설명 >>

클래스 명 위에 @Component 애너테이션을 붙이면 컨테이너에 빈 객체를 등록할 필요가 없어짐

@Component 애너테이션은 클래스 명 위에 붙이는 것으로 클래스명 위에 만 있으면 되므로 @Data 애너테이션 위이든 아래이든 상관 없음

 

GalaxyFlip4와 IPhone14 클래스에도 @Component 애너테이션을 붙이자

 

@Component 애너테이션을 붙이면 컨테이너에 빈이 등록됨

이때 빈의 이름은 클래스 이름을 따라감

 

 

여기서 드는 의문!

"빈 객체들(GalaxyS23, GalaxyFlip4, IPhone14)은 BatteryType에 의존하고 있는데 의존 주입은 어떻게 되는거지?"

그렇다. 빈 객체에 의존 객체가 있다면 의존 객체의 클래스명 위에도 @Component 애너테이션을 붙여줘야함

 

 

이번에는 스마트폰이 의존하고 있는 의존 객체에도 @Component 애너테이션을 붙이자

 

의존 객체에 @Component 애너테이션을 사용하면 의존 주입을 하기 위해 의존 객체를 생성할 필요가 없어짐

여기서 각 스마트폰에 사용한 @Component 애너테이션과 각 베터리에 사용한 @Component 애너테이션이 다름

(1). 각 스마트폰에 사용한 @Component 애너테이션은 ( ) 가 없었음

(2). 각 베터리에 사용한 @Component 애너테이션은 ( ) 안에 문자열이 들어있음

 

 

@Component 애너테이션에 ( ) 가 없으면 해당 빈을 컨테이너에 등록할 때 클래스 이름이 빈의 이름이 됨

@Component 애너테이션의 ( ) 안에 문자열이 있으면 해당 빈을 컨테이너에 등록할 때 지정한 문자열이 빈의 이름이 됨

 

(1)의 빈 색깔과 (2)의 빈 색깔이 다른 이유는 컨테이너 입장에서는 모두 빈이지만 개발자 입장에서는 GalaxyS23, GalaxyFlip4, IPhone14는 필요한 곳에서 꺼내서 쓰기 위한 빈이고 nb, lb 빈은 GalaxyS23, GalaxyFlip4, IPhone14이 의존하고 있는 의존 객체이기 때문에 색깔을 다르게 지정했음

 

 

우선 여기까지 빈 객체들(GalaxyS23, GalaxyFlip4, IPhone14)이 주입 받아야할 의존 객체를 생성했음

빈 객체들에 의존 주입을 받으려면 빈 객체들이 갖고 있는 의존 객체에 @Autowired 애너테이션을 붙어야함

package com.study.chapter03;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@Data
public class GalaxyS23 {
	@Autowired
	@Qualifier("nb")
	private BatteryType batteryType;
	
	public void printBatteryInfo() {
		batteryType.printBatteryInfo();
	}
}

 

잠깐! 이 코드에는 굉장히 중요한 점이 있음!

지금까지 빈 객체들(GalaxyS23, GalaxyFlip4, IPhone14)이 생성자를 갖고 있었던 이유는 우리(개발자)가 직접 의존 주입을 하기 위해서였음

이제는 우리가 직접 의존 주입을 하지 않고 애너테이션을 통해서 Spring Framework가 알아서 의존 주입을 하도록 할 것이므로 생성자 방식 또는 setter 방식의 의존 주입 코드 는 지워야함

 

@Qualifier 애너테이션은 의존 주입 받을 빈의 이름임

따라서 GalaxyS23은 배터리가 건전지임

 

나머지 빈 객체는 리튬 배터리로 의존 주입을 하자

 

여기까지 컨테이너에 코드 한 줄 안 쓰고 @Component, @Autowired, @Qualifer 애너테이션으로 빈 객체를 등록하고 의존 주입을 했음

 

 

현재 상황에서 빈 객체를 갖고 있는 컨테이너는 누굴까?

그 컨테이너는 바로 우리가 프로젝트를 만들었을 때 자동으로 들어있던 코드 중 하나임

 

지금은 시간이 꽤 흘러 기억이 안나겠지만 우리가 처음 프로젝트를 만들었을 때부터 있던 코드가 있음

그 코드는 com.study 패키지 안에 들어있음

 

이 중에서 (프로젝트명)Application.java 인 StudyProjectApplication.java 소스 파일을 보자

 

이 소스 파일에는 굉장히 중요한 @SpringBootApplication 애너테이션이 있음

이 애너테이션은 여러 애너테이션이 하나로 합쳐진 애너테이션임

 

애너테이션 위에 마우스를 올려두면 어떤 애너테이션들이 하나로 합쳐졌는지 알 수 있음

 

이 애너테이션들은 또 다시 여러 애너테이션이 하나로 합쳐진 애너테이션임

즉, @SpringBootApplication 애너테이션은 굉장히 많은 애너테이션이 하나로 합쳐진 애너테이션임

합쳐진 애너테이션 중 @Configuration, @EnableAutoConfiguration, @ComponentScan 애너테이션이 있는데 이 애너테이션이 컨테이너를 생성하고 컨테이너에 의존 주입된 빈 객체를 등록하는 역할을 함

 

하나 하나 자세히 설명하면 또 다른 얘기로 많이 넘어가야하므로 Spring Boot 난이도에 맞춰서 대략적으로만 이해하고 있자

( 하나 하나 자세히 이해하고 싶다면 Spring Framework 를 공부해야함 )

@Configuration - Confing 컨테이너에 붙였던 애너테이션으로 컨테이너를 생성할 때 사용하는 애너테이션

@ComponentScan - 컨테이너에 등록할 빈 객체를 자동으로 찾아주는 애너테이션으로 @Component 애너테이션이 붙은 클래스를 컨테이너에 빈 객체로 등록함

@EnabledAutoConfiguration - 생성된 Config 컨테이너를 불러와 ctx에 저장해 사용했는데 이 애너테이션을 사용하면 컨테이너를 불러 올 필요 없이 컨테이너 내 빈 객체를 꺼낼 수 있음

 

 

@SpringBootApplication 애너테이션 덕분에 @Component, @Autowired, @Qualifer 애너테이션으로 빈 객체를 등록하고 의존 주입을 했다면 컨테이너를 만들 필요 없이 빈 객체를 꺼낼 수 있음

 

빈 객체를 꺼내보기 전에 빈 객체의 의존 주입 방식이 완전히 바뀌었기도 하고 Config 컨테이너는 필요 없으므로 Config 클래스는 삭제하자

(1). 컨테이너를 생성할 때 사용할 설정 클래스인 Config 클래스를 삭제했으므로 더 이상 컨테이너를 직접 생성할 수 없음

       이 코드도 삭제 해야함

(2). 빈 객체 생성과 의존 주입을 우리가 직접 한게 아니라 애너테이션을 사용해서 Spring Framework가 대신 해주도록 했으므로 더 이상 main 메서드 안에서는 빈 객체를 사용할 수 없음

main 메서드 이기 때문이 아니라 main 메서드가 static 메서드이기 때문임

 

이제 main 의 역할을 대신할 컨트롤러를 만들어야함

프로젝트 -> com.study.chapter03 -> Ex06 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Ex06 {
	@Autowired
	private GalaxyS23 phone1;
	@Autowired
	private GalaxyFlip4 phone2;
	@Autowired
	private IPhone14 phone3;
	
	@GetMapping("/chapter03/diNioc/phone1")
	public void diAndIocTest1() {
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
	}
	
	@GetMapping("/chapter03/diNioc/phone2")
	public void diAndIocTest2() {
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
	}
	
	@GetMapping("/chapter03/diNioc/phone3")
	public void diAndIocTest3() {
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
	}
}

<< 코드 설명 >>

(1). @Controller 애너테이션이 붙은 클래스는 SpringFramework가 컨테이너로 만들어줌

즉, 컨테이너도 우리가 직접 생성하는게 아니라 애너테이션으로 생성한 것

앞에서는 우리가 직접 설정 클래스를 사용해서 ctx 라는 컨테이너를 생성했지만 이제는 @Controller 애너테이션 한 줄을 통해서 SpringFramework가 대신 컨테이너를 생성한 것

(2). 컨테이너에 빈 객체 등록

컨테이너에 빈 객체를 등록할 때도 @Autowired 애너테이션을 붙임

(3). GET 방식으로 http://서버주소:포트번호/chapter03/diNioc/phone1 경로에 접근했을 때는 diAndIocTest1 메서드가 호출되도록 diAndIocTest1 메서드에 GetMapping 애너테이션을 지정했음

diAndIocTest1 메서드가 호출되면 sts 내 콘솔창에는

 

<< phone1 >>

배터리는 건전지입니다.

배터리를 모두 사용했다면 건전지를 교체하세요.

 

가 출력될 것

 

이제 sts의 [ Boot Dashboard ] 에서 서버를 실행 시키고 컨트롤러를 호출해보자

 

여기서 URL에 사용한 diNioc는 DI와 IoC임


이렇게 Spring Framework는 컨트롤러가 컨테이너의 역할을 하며 사용자의 요청을 받고 사용자의 요청에 맞는 서비스 메서드를 호출함

 

서비스 메서드에는 DAO에 의존해 DB와 통신할 때 DAO를 통해 통신함

 

여기까지 Spring Framework에서 굉장히 중요한 DI, IoC 에 대해서 배웠음

DI, IoC는 굉장히 중요한 요소이므로 여러번 복습해서 꼭 내것으로 만들어야함

728x90
LIST

<< 학습 목표 >>

1. 컨테이너를 생성하고 빈 객체를 등록할 수 있다.

2. 의존 주입을 한 빈 객체를 컨테이너에 등록할 수 있다.

3. 컨테이너의 생성 과정을 설명할 수 있다.


전 글 ( https://codingaja.tistory.com/111 ) 에서 의존, 의존 주입을 배웠고 의존 주입을 대신 해주는 요소에 대해 배웠음

그러면서 우리가 직접 자바만 사용해 의존 주입을 대신 해주는 요소(SmartPhoneFactory)를 만들었음


Spring Framework의 경우 xml을 이용한 의존 주입, 직접 의존 주입, 애너테이션을 이용한 의존 주입 / 이렇게 3가지 의존 주입 기법을 제공함

 

특히, 우리처럼 Spring Boot 로 생성한 Spring Framework 프로젝트의 경우 일반적으로는 애너테이션을 이용한 의존 주입을 하도록 권장하고 있음


이번에는 Spring Framework가 제공하는 의존 주입 기법 중 직접 의존 주입에 대해서 알아보자

SPring Framework가 권장하는 방식인 애너테이션을 이용한 의존 주입 방법은 다음 글 ( ) 에서 알아 볼 예정


<< 직접 의존 주입 하는 방법 >>

전 글의 SmartPhoneFactory 역할을 할 Configuration 클래스가 필요함

프로젝트 -> com.study.chapter03 -> Config 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

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

@Configuration
public class Config {
	private String batteryType = "리튬 폴리머 배터리";
	
	@Bean
	public GalaxyS23 galaxyS23() {
		return new GalaxyS23(batteryType);
	}
	
	@Bean
	public GalaxyFlip4 galaxyFlip4() {
		return new GalaxyFlip4(batteryType);
	}
	
	@Bean
	public IPhone14 iPhone14() {
		return new IPhone14(batteryType);
	}
}

<< 코드 설명 >>

왼쪽에 SmartPhoneFactory 는 자바만 사용해 IoC, DI 를 적용한 클래스이고 왼쪽에 Config 는 Spring Framework가 제공하는 IoC, DI 를 적용한 클래스임

 

같은 역할을 하는 두 클래스의 코드를 비교 해보자

(1). 의존 주입을 하기 위한 공통 분모 생성

(2), (3), (4). 객체를 생성하며 의존 주입 후 반환

 

여기서 사용된 @Configuration 애너테이션과 @Bean 애너테이션을 분석해보자

 

@Configuration 애너테이션은 빈(Bean) 들을 생성해 가지고 있고 이 Bean들이 필요한 곳에 제공을 해주는 클래스에 애너테이션을 붙임

@Configuration 애너테이션이 붙은 클래스를 컨테이너(Container) 라고 부름

실행활에서 컨테이너를 직접 보지못했어도 어떤 역할을 하는지 충분히 알고 있을 것

컨테이너는 물건들을 옮길 때 사용하는 것으로 컨테이너에 물건들을 넣고 뺄 수 있음

 

Spring Framework의 컨테이너( @Configuration 애너테이션이 붙은 클래스 ) 도 이와 같은 역할을 함

Spring Framework의 컨테이너에 넣고 빼는 물건을 빈(Bean) 이라고 부름

 

빈(Bean)은 자바에서 객체, 인스턴스로 부르는 것으로 SmartPhoneFactory 클래스에서 각 if문이 return 해주는 인스턴스를 SpringFramework의 용어로 빈(Bean)이라고 부름

 

이 빈들을 Spring Framework의 컨테이너에 넣어두면 빈이 필요한 곳에서 컨테이너에 들어있는 빈을 필요할 때 꺼낼 수 있음

Spring Framework의 컨테이너에 빈을 넣어두려면 @Bean 애너테이션이 붙은 메서드가 필요한대 아래 (1), (2), (3) 메서드가 컨테이너에 빈을 넣어두는 @Bean 애너테이션이 붙은 메서드임

 

Config 클래스에 @Configuration 애너테이션이 붙어있으므로 Config 클래스는 컨테이너가 됨

그리고 Config 컨테이너에 @Bean 애너테이션이 붙은 메서드를 선언했으므로 메서드가 return 해주는 빈들이 Config 컨테이너에 등록되는데 이때 빈의 이름은 메서드이름이 됨

 

이제 빈이 필요한 곳에서 Config 컨테이너를 불러온 다음 빈의 이름을 사용해 각 빈을 꺼낼 수 있음

 

 

프로젝트 -> com.study.chapter03 -> Ex02 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

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

public class Ex02 {
	public static void main(String[] args) {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
		
		GalaxyS23 phone1 = (GalaxyS23) ctx.getBean("galaxyS23");
		GalaxyS23 phone2 = (GalaxyS23) ctx.getBean("galaxyS23");
		GalaxyFlip4 phone3 = (GalaxyFlip4) ctx.getBean("galaxyFlip4");
		IPhone14 phone4 = (IPhone14) ctx.getBean("iPhone14");
		IPhone14 phone5 = (IPhone14) ctx.getBean("iPhone14");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}
}

<< 코드 설명 >>

(1). Config 컨테이너를 불러와 ctx 객체에 저장

(2). Config 컨테이너에 있는 빈을 꺼내 각 객체에 저장

  이때 빈의 이름은 메서드 이름이라는 점을 기억하자

  또한 getBean 메서드로 어떤 빈을 꺼낼지 모르기 때문에 getBean 메서드는 반환 타입이 Object 라는점

  그래서 getBean으로 빈을 꺼낸 다음 적절한 형태로 형변환해야함


우리가 직접 만든 컨테이너인 SmartPhoneFactory 컨테이너와 Spring Framework가 제공하는 컨테이너의 사용 방식을 보면 다를게 없음

SmartPhoneFactory는 컨테이너를 흉내낸 우리가 직접 만든 컨테이너로 실무에서 활용할 수 있는 수준의 컨테이너가 되려면 굉장히 많은 기반 지식과 개발 경험이 있어야함

SmartPhoneFactory 처럼 이렇게 개발에 필요한 부분을 처음부터 끝까지 다 만들려면 상당한 개발 지식, 경험이 필요하고  개발자가 관리해야할 코드가 많아지고 개발 기간이 상당히 길어짐

또한 그것을 직접 만든 개발자가 퇴사를 하거나 어떤 일로 자리를 비우면 프로젝트에 마비가 올 것

 

프레임워크를 사용하면 개발에 필요한 상당 부분을 프레임워크의 도움을 받을 수 있고 개발 기간이 단축될 수 있음

또한 특정한 개발자가 직접 만든게 아니기 때문에 프레임워크가 제공하는 것들을 사용해 개발했다면 개발자가 퇴사를 하거나 어떤 일로 자리를 비우더라도 프레임워크를 알고 있는 다른 개발자가 대신 할 수 있으므로 프로젝트에 마비가 오지 않음


이제 Spring Framework가 제공하는 컨테이너에 대해서 좀 더 깊게 들어가보자

 

다시 한번 우리가 직접 만든 컨테이너와 Spring Framework가 제공하는 컨테이너를 비교해보자

아래와 같이 두 방식 모두 두 대의 핸드폰을 생성했고 각 핸드폰의 배터리 타입을 바꿨음

위와 같이 프로젝트 -> com.study.chapter03 -> Ex03 소스 파일을 추가하고 위에서 왼쪽에 있는 코드를 입력하자

또한 프로젝트 -> com.study.chapter03 -> Ex04 소스 파일을 추가하고 위에서 오른쪽에 있는 코드를 입력하자

 

 

우리가 직접 만든 컨테이너를 사용한 코드에서는 우리가 알고 있는대로 각 객체의 배터리 타입을 바꿨으므로 phone1, phone2의 배터리 타입이 다르게 출력됨

 

 

그러나 Spring Framework가 제공하는 컨테이너를 사용한 코드에서는 우리가 알고 있는 것과 다르게 두 핸드폰의 배터리 타입이 모두 리튬 폴리머 배터리로 바꼈음

 

왜이럴까??

이는 Spring Framework 컨테이너의 생성 과정과 동작 과정을 보면 어렵지 않게 이해할 수 있음

 

Spring Framework 프로젝트를 실행시키면 @Configuration 애너테이션이 붙은 컨테이너를 생성함

컨테이너의 이름은 @Configuration 애너테이션이 붙은 클래스의 이름을 따라감

 

 

바로 이어서 빈들(galaxyS23, galaxyFlip4, iPhone14)이 의존하고 있는 의존 객체인 batteryType을 생성함

 

 

그 후 @Bean 애너테이션이 붙은 메서드들이 차례대로 호출됨

 

메서드들이 호출되면서 컨테이너에 빈 객체가 등록되는데 메서드의 이름이 빈 객체의 이름이 됨

 

빈은 프로젝트에 필요한 인스턴스를 갖고 있는데 그 인스턴스는 메서드가 반환해주는 인스턴스임

메서드가 인스턴스를 생성할 때 우리가 지정한 방식인 생성자를 통해 의존 주입을 하고 반환함

따라서 빈은 의존 주입이 된 프로젝트에 필요한 인스턴스를 갖고 있음

 

나머지 빈 객체들도 마찬가지로 의존 주입이 된 인스턴스를 갖고 있음

 

이렇게 의존 주입이 된 빈 객체를 갖고 있는 컨테이너가 생성됨

 

Ex04에서 사용할 컨테이너가 생성되는 과정을 다시 한번 알아봤음

다시 Ex04 코드를 보자

 

(1). 이렇게 생성된 Config 컨테이너를 불러와 ctx에 저장함

 

(2). ctx를 통해 Config 컨테이너에 저장되어있는 galaxyS23 빈을 가져와 phone1 객체에 저장함

 

여기서 주의할 점은 빈을 가져온다는건 빈을 꺼낸다와는 다름

 

실실행활에서 컨테이너에서 물건을 가져온다는건 꺼낸다는 것이고 컨테이너에서 물건을 가져오면 그 컨테이너에는 가져온 물건이 빠짐

Spring Framework의 컨테이너에서 빈을 가져온다는건 빈을 꺼낸다가 아님

galaxyS23 빈은 GalaxyS23 클래스의 인스턴스가 저장된 주소를 갖고 있는 것

( 이는 자바의 기초적인 내용이니 빈이 왜 인스턴스가 저장된 주소를 갖고 있는지에 대한 설명은 하지 않겠음, 만약 왜? 라는 생각이 들면 자바의 클래스를 다시 처음부터 공부하고 오자 )

 

그래서 getBean 메서드로 빈을 가져오면 빈이 가지고 있는 의존 주입된 GalaxyS23 클래스의 인스턴스 주소를 반환함

phone1 객체는 이 인스턴스의 주소를 갖고 있게되는 것

 

결국 phone1 객체는 아래와 같이 GalaxyS23 클래스의 인스턴스를 참조하게 됨

 

 

(3). 이번에도 (2)와 마찬가지로 ctx를 통해 Config 컨테이너에 저장되어있는 빈을 가져옴

따라서 phone2 객체도 phone1 객체와 마찬가지로 GalaxyS23 클래스의 인스턴스를 참조함

 

(4). 이런 상태에서 phone1, phone2 객체의 setter 를 사용해 의존 객체를 수정하면 같은 인스턴스의 의존 객체를 수정하는 것이므로 GalaxyS23 클래스의 인스턴스의 의존 객체는 "리튬 폴리머 배터리"가 됨

 

여기까지 의존 주입이 적용된 컨테이너를 생성해봤고 IoC를 활용해 빈이 필요한곳에서 빈을 꺼내봤음

 

DI, IoC는 Spring Framework에서 굉장히 중요한 부분이므로 이 글에서 설명하는 것들을 이해할 수 있을 때까지 여러번 반복해서 읽어보고 여러번 반복해서 읽었는데도 이해가 안되면 다른 블로그를 더 찾아보고서라도 반드시 이해해야함

728x90
LIST

<< 학습 목표 >>

1. 의존에 대해서 설명할 수 있다.

2. 의존 객체에 대해서 설명할 수 있다.

3. 의존 주입에 대해서 설명할 수 있다.


우선 프로젝트 -> com.example.de.chapter03 패키지를 추가하자

 

 

Spring Framework에서 중요한 개념인 DI (Dependency Injection) 에 대해서 알아보자

DI는 우리말로 의존 주입이라고 함

DI는 Spring Framework에서 사용되는 용어가 아닌 개발에서 일반적으로 사용하는 말이고 일반적으로 사용되는 기법임

 

먼저 의존에 대해서 알아보자

우리 말에서 "A가 B에게 의존한다" 라면 "A는 B에게 전적으로 도움을 받고 있다" 로 해석할 수 있음

B가 없으면 A가 스스로 무언가를 할 수 없을 것

 

좀 더 현실의 예를 들어보자

대부분이 가전제품은 전기에 의존하고 있음

컴퓨터는 전기에 의존하고 있기 때문에 전기가 없다면 컴퓨터를 켤 수 없음

스마트폰은 배터리에 의존하고 있기 때문에 배터리가 없다면 스마트폰을 켤 수 없음

시계는 건전지에 의존하고 있기 때문에 건전지가 없다면 시계는 동작하지 않음

 

이런것처럼 프로그래밍에서도 의존 현상이 발생하고 이는 중요한 개발 기법 중 하나임

Spring Framework으로 의존 기법을 설명하면 좋겠지만 난이도가 굉장히 어려워지니 자바로 의존 기법을 이해하자

Spring Framework는 자바 언어를 사용하는 프레임워크이므로 자바로 의존 기법을 이해했다면 Spring Framework의 의존, 의존 주입 등을 이해할 수 있음


우선 캡슐화부터 시작해서 의존, 의존 주입(DI), 제어 역전(IoC)까지 천천히 그리고 확실하게 알아보자

상속까지 들어가야 할 수도 있지만 상속까지 끼게되면 이해하는데 더 복잡해지므로 상속은 제외하고 보자

 

위에서 언급한대로 잠깐 여기서는 Spring Framework는 접어두고 자바로만 알아보자

 

아래와 같이 스마트폰을 구현한 클래스들이 있음

이 클래스들을 프로젝트 -> com.study.chatper03 패키지에 추가하자

 

이번에는 프로젝트 -> com.study.chapter03 -> Ex01 클래스를 아래와 같이 추가하자

Ex01 클래스는 스마트폰들을 생성하고 스마트폰들의 정보를 출력하는 역할임

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		GalaxyS23 phone1 = new GalaxyS23();
		phone1.setBatteryType("건전지");
		
		GalaxyS23 phone2 = new GalaxyS23();
		phone2.setBatteryType("건전지");
		
		GalaxyFlip4 phone3 = new GalaxyFlip4();
		phone3.setBatteryType("건전지");
		
		IPhone14 phone4 = new IPhone14();
		phone4.setBatteryType("건전지");
		
		IPhone14 phone5 = new IPhone14();
		phone5.setBatteryType("건전지");
		
		System.out.println("<< phone1 >>");
		System.out.println("배터리는 " + phone1.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone1.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone2 >>");
		System.out.println("배터리는 " + phone2.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone2.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone3 >>");
		System.out.println("배터리는 " + phone3.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone3.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone4 >>");
		System.out.println("배터리는 " + phone4.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone4.getBatteryType() + " 를 교체하세요.");
		
		System.out.println("<< phone5 >>");
		System.out.println("배터리는 " + phone5.getBatteryType() + " 입니다.");
		System.out.println("배터리를 모두 사용했다면 " + phone5.getBatteryType() + " 를 교체하세요.");
	}

}

 

현재 모든 스마트폰의 배터리가 건전지인 상태라고 하자

그래서 스마트폰 5대를 생성했고 모든 스마트폰의 배터리를 건전지로 설정한 상태임

그리고 스마트폰의 배터리 정보를 출력하고 있음

 

이때 배터리 정보를 출력하는 문구가 조금이라도 바뀌면 모든 Sysout에서 코드를 바꿔야하므로 메서드를 활용하면 스마트폰의 배터리 정보를 좀 더 쉽게 출력할 수 있고 좀 더 쉽게 수정할 수 있음

각 스마트폰 클래스에 다음과 같이 배터리 정보를 출력하는 메서드를 추가하자

 

이제 바뀐 방식대로 배터리 정보를 출력하도록 바꿔보자

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		GalaxyS23 phone1 = new GalaxyS23();
		phone1.setBatteryType("건전지");
		
		GalaxyS23 phone2 = new GalaxyS23();
		phone2.setBatteryType("건전지");
		
		GalaxyFlip4 phone3 = new GalaxyFlip4();
		phone3.setBatteryType("건전지");
		
		IPhone14 phone4 = new IPhone14();
		phone4.setBatteryType("건전지");
		
		IPhone14 phone5 = new IPhone14();
		phone5.setBatteryType("건전지");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}

}

 

여기서 스마트폰은 배터리(batteryType 멤버 변수)에 의존하고 있음

따라서 배터리는 스마트폰의 의존 객체임

그리고 main이 스마트폰의 의존 객체를 setter 통해서 넣어주고 있음

 

스마트폰이 의존 하고 있는 batteryType 멤버 변수에 건전지 문자열을 직접 넣는게 아니라 main이 setter 통해 넣어주고 있는데 이를 의존 주입이라고 함

다시 설명하면 본인이 아닌 제 3자가 의존 객체를 넣어주는걸 의존 주입(Dependency Injection) 이라고 함

 

의존 주입을 하는 방법은 두 가지가 있음

- 생성자를 통한 의존 주입

- setter를 통한 의존 주입

 

지금 우리는 setter를 통한 의존 주입이므로 다른 방식의 의존 주입도 해보자

아래와 같이 생성자를 통한 의존 주입으로 바꿔보자

 

변경된 의존 주입 방식을 적용해 스마트폰에 배터리를 지정하자

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		GalaxyS23 phone1 = new GalaxyS23("건전지");
		GalaxyS23 phone2 = new GalaxyS23("건전지");
		GalaxyFlip4 phone3 = new GalaxyFlip4("건전지");
		IPhone14 phone4 = new IPhone14("건전지");
		IPhone14 phone5 = new IPhone14("건전지");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}

}

 

의존 주입에 좋은 방식, 더 나은 방식은 없음

그저 두 가지 방식이 있을 뿐이고 상황에 따라서 필요한 또는 하고 싶은 방식으로 의존 주입을 하면 됨

 

여기까지 의존이 무엇이고 의존 주입이 무엇인지 설명을 했음


지금 의존 주입을 하나 하나 일일히 해주고 있음

이렇게 했을 때 생길 수 있는 문제점은 의존 객체가 반드시 바뀌어야할 때 개발자가 직접 바꿔줘야하는 의존 객체들이 너무 많기 때문에 개발자의 실수로 의존 객체가 바뀌지 않을 수 있다는 것

 

개발자의 실수로 의존 객체가 바뀌지 않는 상황을 알아보자

 

 

이제 시대가 발전해서 스마트폰이 건전지 대신 리튬이온 배터리로 바꼈음

그렇다면 main에서 스마트폰에 건전지를 주입해주는 대신 리튬이온 배터리를 주입해주도록 해야함

 

중간에 바로 보이지만 의존 객체가 반드시 바뀌어야하는데 의존 객체를 바꿔줘야 할 스마트폰들이 너무 많기 때문에 실수로 어떤 객체의 의존 객체를 바꾸지 못할 수도 있다는 것

 

이 문제를 해결할 수 있는 첫 번째 방법은 스마트폰이 의존 객체를 직접 주입하는 것

아래와 같이 스마트폰의 생성자를 통해서 의존 주입을 받는게 아닌 스마트폰이 직접 필요한 의존 객체(batteryType 멤버 변수)를 주입하면 됨

아래와 같이 따라 입력할 필요는 없고 보기만 하면 됨

 

바뀐 의존 주입 방법대로 스마트폰들을 생성해보자

 

이렇게 하면 개발자의 실수를 없앨 수 있는것처럼 보임

기존에는 생성된 스마트폰 마다 의존 객체를 바꿔줘야했으므로 5대의 스마트폰이 있다면 5번 의존 객체를 바꿔줘야하고 100대의 스마트폰이 있다면 100번 의존 객체를 바꿔줘야했지만 스마트폰이 의존 객체를 직접 생성하도록 바꿈으로써 스마트폰의 대수에 상관 없이 세 종류의 스마트폰의 생성자를 수정하면 되므로...

 

그러나 바로 위에서 언급한것처럼 없앨 수 있는 것처럼 보이는거지 없앤건 아님

스마트폰의 종류가 늘어나면 어떻게 될까?

결국 100대의 스마트폰이 있는 상황과 100종류의 스마트폰이 있는 상황은 똑같은 상황이 됨

 

스마트폰이 직접 의존 객체를 생성하는 방식을 강한 결합 이라고 부르고 스마트폰이 의존 객체를 주입 받는걸 약한 결합이라고 부름

강한 결합은 좋지 못한 코드를 만들 확률이 높아지게 만들고 약한 결합은 좋은 코드를 만들 확률이 높아지게 만듬

 

다시 원래대로 생성자 또는 setter 를 사용해 의존 주입을 받는 약한 결합으로 되돌아가자

어차피 되돌아갈 것이므로 따라 입력하지는 말라고 했던 것


다시 원점으로 돌아와서 의존 객체를 주입하는 약한 결합이 좋은데 약한 결합을 하면 개발자의 실수로 의존 객체를 반드시 바꿔야하는데 바꾸지 못하는 상황이 생김

 

이를 해결할 수 있는 좋은 방안은 없을까?

 

당연히 있음

 

이를 해결할 수 있는 좋은 방안은 main에서 스마트폰을 직접 생성하지 말고 스마트폰을 생성해주는 공장을 만드는 것

프로젝트 -> com.study.chapter03 -> SmartPhoneFactory 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter03;

public class SmartPhoneFactory {
	public Object createSmartPhone(String smartPhoneType) {
		String batteryType = "리튬이온 배터리";
		
		if(smartPhoneType.equals("GalaxyS23")) {
			return new GalaxyS23(batteryType);
		} else if(smartPhoneType.equals("GalaxyFlip4")) {
			return new GalaxyFlip4(batteryType);
		} else if(smartPhoneType.equals("iPhone14")) {
			return new IPhone14(batteryType);
		} else {
			System.out.println("생산할 수 없는 스마트폰입니다.");
			return null;
		}
	}
}

위와 같이 스마트폰을 만들고 필요한 의존 주입까지된 스마트폰을 반환해주는 공장이 있다면 개발자의 실수를 상당히 줄 일 수 있음

 

<< 코드 설명 >>

(1). 반환 타입이 Object 인건 스마트폰 공장에서 여러 종류의 스마트폰을 만들 수 있으므로 모든 종류의 스마트폰을 반환 할 수 있도록 하기 위함

혹시 반환 타입이 Object인 메서드 또는 매개변수 타입이 Object인 메서드에 대해서 잘 모른다면 자바 쪽이 약한것이므로 [ 반환 타입이 Object인 메서드 ], [ 매개변수 타입이 Object인 메서드 ] 로 검색해서 찾아보자

 

(2). 스마트폰에 필요한 의존 객체 생성

(3). 필요한 스마트폰을 생성해 의존 주입 후 반환

(4). 오타나 어떤 실수로 잘못된 스마트폰 타입을 전달했다면 이를 알려주는 else 문

 

 

이제 main에서는 스마트폰을 직접 생산하는게 아니라 SmartPhoneFactory 를 사용해서 필요한 스마트폰을 생산하면 됨

package com.study.chapter03;

public class Ex01 {

	public static void main(String[] args) {
		SmartPhoneFactory sf = new SmartPhoneFactory();
		
		GalaxyS23 phone1 = (GalaxyS23) sf.createSmartPhone("GalaxyS23");
		GalaxyS23 phone2 = (GalaxyS23) sf.createSmartPhone("GalaxyS23");
		GalaxyFlip4 phone3 = (GalaxyFlip4) sf.createSmartPhone("GalaxyFlip4");
		IPhone14 phone4 = (IPhone14) sf.createSmartPhone("iPhone14");
		IPhone14 phone5 = (IPhone14) sf.createSmartPhone("iPhone14");
		
		System.out.println("<< phone1 >>");
		phone1.printBatteryInfo();
		
		System.out.println("<< phone2 >>");
		phone2.printBatteryInfo();
		
		System.out.println("<< phone3 >>");
		phone3.printBatteryInfo();
		
		System.out.println("<< phone4 >>");
		phone4.printBatteryInfo();
		
		System.out.println("<< phone5 >>");
		phone5.printBatteryInfo();
	}

}

<< 코드 설명 >>

(1). 스마트폰을 만들기 위한 공장 생성

(2). 공장을 통해 필요한 스마트폰을 생성하는데 공장에서는 Object 타입으로 반환하므로 필요한 적절한 형태로 형변환

 

특히, 이제 main이 직접 스마트폰을 생성하는 방식에서 SmartPhoneFactory가 대신 스마트폰을 생성해주므로 이를 제어의 역전 ( IoC / Inversion Of Controll ) 이라고 부름


이제 코드가 상당히 완전해졌음

시대가 더 발전해 모든 스마트폰의 배터리를 리튬이온 배터리에서 리튬 폴리머 배터리로 바꿔야한다면 간단하게 스마트폰 생산 공장에서 배터리 타입만 바꿔주면 됨

 

 

main을 보면 처음(왼쪽)에는 스마트폰을 직접 생산하고 직접 의존 주입을 해줬지만 이제(오른쪽)는 스마트폰 공장에서 의존 주입까지 완료된 스마트폰을 받는 방식으로 바꼈음

이를 제어 역전(IoC / Inversion Of Control) 이라고 함

 

Spring Framework 를 배우다 갑자기 의존, 의존 주입(Dependency Injection / DI) 를 하는 이유는 Spring Framework의 핵심 기능 중 하나가 ( IoC, DI )임

Spring Framework에서 의존, 의존 주입, 제어 역전은 굉장히 중요한 키워드이니 반드시 내것으로 만들자

 

프로젝트를 개발하다 보면 컨트롤러와 서비스, 서비스와 DAO 등 많은 요소들이 서로 의존하며 동작하게됨

이때 마다 개발자가 직접 의존 주입을 하는게 아닌 SmartPhoneFactory 처럼 Spring Framework의 DI 를 사용해 의존 주입 된 요소를 받아서 사용하면 개발자의 실수도 줄어들고 개발도 편리해짐

 

이 글에서 만든 SmartFactory 를 컨테이너라고 부름

컨테이너는 프로젝트에 필요한 인스턴스를 생성해 반환해주는 역할을 함

또한 생성할 인스턴스에 의존 주입이 필요하면 의존 주입까지 해줌

 

이 글에서는 컨테이너를 우리가 직접 만들었지만 다음 글 ( https://codingaja.tistory.com/112 ) 에서는 Spring Framework의 DI를 사용해보자


의존, 의존 주입과 관련되서 다른 시각으로 이해하고 싶다면 아래 블로그를 더 보자

 

의존관계 주입(Dependency Injection) 쉽게 이해하기

이번 글에서는 DI(의존성 주입, 의존관계 주입)의 개념을 설명한다.

tecoble.techcourse.co.kr

 

 

의존성 주입이란 무엇이며 왜 필요한가?

목표 의존성 주입이 무엇인지 이해한다. 의존성 주입이 왜 필요한지 이해한다. 의존성 주입이란? 의존성 주입이란 클래스간 의존성을 클래스 외부에서 주입하는 것을 뜻한다. 더 자세하게는 의

kotlinworld.com

 

728x90
LIST

<< 학습 목표 >>

1. InitBinder 를 사용해 Validator 를 생성할 수 있다.

2. InitBinder 가 생성한 Validator 로 커맨드 객체를 검증할 수 있다.

3. 애너테이션을 사용해 커맨드 객체 검증 코드를 최대한 줄일 수 있다.


앞서 우리는 Validator 를 컨트롤러 안에서 직접 생성했음

 

type1 컨트롤러는 커맨드 객체를 검증하기 위해 Validator 가 필요한데 이를 "type1 컨트롤러는 Validator에 의존(Dependency)한다" 라고 표현함

type1 컨트롤러와 Validator 사이에서 Validator 는 의존 객체라고 부름

 

A가 B에 의존할 때 A가 B를 직접 생성하면 이를 강한 결합이라 부르고 이는 좋지 못한 코드가 됨

왜 그런지는 Chapter03 에서 배울 것

A와 B 사이에서 B는 의존 객체라고 부름

 

A가 B에 의존할 때 A 안에서 B를 직접 생성하는 방식이 아닌 다른 객체의 도움으로 A가 B를 생성하도록 해야하는데 이를 Spring Framework가 담당함

 

type1 컨트롤러가 Validator에 의존하고 있는데 type1 컨트롤러 안에서 Validator를 직접 생성하는게 아니라 Spring Framework의 도움으로 type1 컨트롤러가 Validator 를 생성하도록 해야함

이렇게  A가 누군가의 도움으로 B(의존 객체)를 생성하면 이를 약한 결합이라 부르고 이는 좋은 코드가 됨

왜 그런지는 Chapter03에서 배울 것


컨트롤러가 Spring Framework의 도움으로 Validator를 생성하려면 Spring Framework가 제공하는 initBinder를 사용해야함

따라서 구체적으로 말하면 컨트롤라가 initBinder의 도움으로 Validator를 생성하도록 해보자

 

컨트롤러가 initBinder의 도움을 받으려면 pom.xml에 spring-boot-starter-validation 라이브러리를 추가해야함

spring-boot-starter-validation 라이브러리의 URL을 알려면 역시나 Maven Repository에서 라이브러리 명으로 검색하면 됨

 

Maven Repository 에서 다음과 같이 spring-boot-starter-validation 라이브러리를 검색(1) -> 첫 번째 검색 결과(2)로 이동 -> 가장 최상위 버전(3) 을 선택하자

그 후 라이브러리 URL을 복사 한 뒤 프로젝트 내 pom.xml 의  닫는 dependency ( </dependency> ) 의 바로 위에 붙여 넣자

그 후 version 태그는 삭제하자

 

spring-boot-starter 로 시작하는 라이브러리의 경우 version 태그가 이미 프로젝트 설정에 지정되어있으므로 우리가 지정하지 않는게 좋음

우리가 지정하면 프로젝트에 설정된 version 태그와 우리가 지정한 version 태그가 충돌이 발생해 문제가 생길 수 있음


initBinder 를 사용하기 위해 라이브러리를 추가했다면 이제 컨트롤러가 initBinder의 도움을 받아 Validator(의존 객체)를 생성하도록 만들어보자

 

ValidationController 클래스에 다음과 같이 type2 컨트롤러와 initBinder 메서드를 추가하자

package com.study.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;

import jakarta.validation.Valid;

@Controller
public class ValidationController {
	@PostMapping("/chapter02/validation/type2")
	public void type2(@Valid Member member, BindingResult errors) {
		if(errors.hasErrors()) {
			// 커맨드 객체 검증에 실패했을 때
			// 필요한 처리
		}
		
		if(errors.hasFieldErrors("id")) {
			// 커맨드 객체의 특정 값의 검증에 실패했을 때
			// 필요한 처리
		}
		
		// ...
	}
	
	@InitBinder
	protected void initBinder(WebDataBinder binder) {
		binder.setValidator(new MemberValidator());
	}
	
	@PostMapping("/chapter02/validation/type1")
	public void type1(Member member, BindingResult errors) {
		Validator validator = new MemberValidator();
		
		validator.validate(member, errors);
		
		if(errors.hasErrors()) {
			// 커맨드 객체 검증에 실패했을 때
			// 필요한 처리
		}
		
		if(errors.hasFieldErrors("id")) {
			// 커맨드 객체의 특정 값의 검증에 실패했을 때
			// 필요한 처리
		}
		
		// ...
	}
}

<< 코드 설명 >>

 

initBinder의 도움 없이 의존 객체를 직접 생성하는 강한 결합(왼쪽)과 initBinder의 도움으로 의존 객체를 생성하는 약한 결합(오른쪽)을 비교해보자

(1). 의존 객체를 직접 생성함

(2). 의존 객체를 직접 생성했으므로 커맨드 객체를 검증하기 위해 validate 메서드를 직접 호출함

(3). 의존 객체를 initBinder가 대신 생성함

  이때 initBinder 메서드 안에 binder.setValidator 메서드의 인자를 보면 type2 컨트롤러에서 필요한 Validator를 생성해 인자로 넣었음

  우리 눈에는 안보이지만 initBinder를 통해 의존 객체를 생성 한 후 type2 컨트롤러에게 전달한 것

(4). 컨트롤러가 의존 객체를 직접 생성하지 않았으므로 커맨드 객체 검증을 할 때도 우리가 직접 하는게 아님

  검증 하고 싶은 커맨드 객체에 @Valid 애너테이션을 달아주면 MemberValidator에 선언한 validate메서드가 적절하게 알아서 호출됨

 

Spring Framework의 장점은 애너테이션을 통해 개발자가 입력해야되는 코드의 양을 상당 부분 줄일 수 있다는 것

그 대신 각 애너테이션이 어떤 역할인지 애너테이션 간에 서로 어떤 관계가 있는지를 확실히 알아야함

initBinder의 도움을 받는것도 마찬가지

왼쪽에 있는 type1 컨트롤러처럼 initBinder의 도움 없이 커맨드 객체를 검증할 수 있지만 좋지 못한 코드가 될 가능성이 높기 때문에 오른쪽에 있는 type2 컨트롤러처럼 initBinder의 도움으로 커맨드 객체를 검증하는 것

type2 컨트롤러처럼 의존 객체와 약하게 결합 되어있다면 좋은 코드가 될 가능성이 높음

그 대신 @InitBinder 애너테이션과 @Valid 애너테이션에 대해서 확실히 알고 있어야함


전 글 ( https://codingaja.tistory.com/109 ) 부터 여기까지 배운 커맨드 객체를 검증하는 과정을 순서대로 나열해보자

1. 검증하고 싶은 커맨드 객체를 검증할 Validator 클래스를 생성함

  이때 Validator 클래스의 이름은 보통 (커맨드객체명)Validator 로 지어줌

  또한 Validator 클래스는 반드시 Validator 인터페이스를 구현해야함

2. Validator 인터페이스를 구현할 때는 supports, validate 메서드를 오버라이딩 해야함

  supports 메서드는 이 Validator가 어떤 커맨드 객체를 검증할 지 Spring Framework에게 알려주는 메서드

  단순히 이름이 (커맨드객체명)Validator 라고 해서 Spring Framework에서 어떤 커맨드 객체를 검증할 Validator 인지 알 수 없음

  validate 메서드는 이 Validator가 커맨드 객체를 검증하는 메서드로 이 메서드에 검증 하는 코드를 구현하면 됨

3. Validator 클래스가 준비됐다면 컨트롤러에서 Validator를 사용해 커맨드 객체를 검증해야하는데 이때 InitBinder의 도움으로 컨트롤러가 Validator 를 생성하도록 해야함

4. 컨트롤러가 갖고 있는 커맨드 객체에 @Valid 애너테이션을 붙여서 Validator로 커맨드 객체를 검증하도록 함


이제 프로젝트를 실행시키고 postman 으로 type2 컨트롤러에게 다양한 값을 보내 커맨드 객체가 제대로 검증 되는지 확인해보자


이제 마지막으로 커맨드 객체 검증 코드를 더 더 줄여보자

이 방법은 애너테이션을 사용해 Validator의 코드를 더 줄이는 방법으로 만약 지금까지 커맨드 객체 검증 방법이 헤깔리거나 어렵다고 느껴진다면 이 아랫부분은 안보고 여기서 마무리 지어도 됨

 

@Valid 애너테이션이 붙은 커맨드 객체의 멤버 변수에 @NotNull, @NotEmpty, @Size 등과 같은 애너테이션을 사용할 수 있고 이 애너테이션들을 사용하면 커맨드 객체를 검증할 Validator의 코드가 더 줄어듬

 

프로젝트 -> com.study.chapter02 -> Member 클래스 내 다음과 같이 애너테이션들을 추가하자

package com.study.chapter02;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class Member {
	@NotNull(message="empty")
	@NotEmpty(message="empty")
	@Size(min=3, max=20, message="too short or long")
	private String id;
	
	@NotNull(message="empty")
	@NotEmpty(message="empty")
	@Size(min=6, max=20, message="too short or long")
	private String pw;
	
	// 커맨드 객체를 검증하기 위한 애너테이션들
	private String nickname;
	
	// 커맨드 객체를 검증하기 위한 애너테이션들
	private String[] hobby;
}

그리고 프로젝트 -> com.study.chapter02 -> MemberValidator 클래스 내 코드를 다음과 같이 대폭 줄이자

package com.study.chapter02;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class MemberValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
		return Member.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Member member = (Member) target;
		
		// 커맨드 객체 검증 코드1
		// 커맨드 객체 검증 코드2
		// 커맨드 객체 검증 코드3
		// ...
		// 커맨드 객체 검증 코드n
	}
}

<< 코드 설명 >>

먼저 MemberValidator 클래스부터 설명하면 MemberValidator 클래스 내 validate 메서드의 코드가 싹 사라졌음

이는 이를 대신할 애너테이션을 Member 클래스에 사용했기 때문임

 

MemberValidator와 Member 를 서로 비교해보자

(1), (2). 커맨드 객체의 멤버 변수가 null이거나 빈 문자열일 경우 커맨드 객체 검증 실패로 처리했는데 이 역할을 @NotNull, @NotEmpty 애너테이션이 대신하도록 한 것

  @NotNull은 애너테이션명 그대로 해당 멤버 변수의 값이 null 이라면 커맨드 객체 검증에 실패하고 사유로 "empty"를 남김

  @NotEmpty는 해당 멤버 변수의 값이 빈문자열("") 이거나 공백문자만 들어있는 문자열이라면 커맨드 객체 검증에 실패하고 실패 사유로 "empty"를 남김

  검증 실패 사유를 바꾸고 싶다면 message 속성의 값을 바꾸면 됨

 

 

(1), (2). 커맨드 객체의 멤버 변수 값이 너무 짧거나 너무 길다면 커맨드 객체 검증 실패로 처리했는데 이 역할을 @Size 애너테이션이 대신하도록 한 것

  @Size는 애너테이션명 그대로 해당 멤버 변수의 값이 일정 길이 미만, 초과라면 커맨드 객체 검증에 실패하고 사유로 "too short or long" 을 남기는 것

  해당 멤버 변수 값의 길이가 min, max를 포함해 min부터 max 사이라면 커맨드 객체 검증에 성공하고 min 미만 또는 max 를 초과했다면 커맨드 객체 검증에 실패함

 

이렇게 @Valid 애너테이션이 붙은 커맨드 객체는 애너테이션을 추가적으로 더 사용해 개발자가 입력해야할 코드를 많이 줄일 수 있음

 

여기서 한가지 단점은 @Valid 애너테이션을 사용했고 커맨드 객체에서 @NotNull, @NotEmpty 등의 애너테이션을 사용해 커맨드 객체를 검증한다면 initBinder가 동작하지 않도록 주석 처리해야함

 

커맨드 객체 안에서 애너테이션을 사용해서 커맨드 객체를 검증할 것이냐 initBinder를 통해서 생성한 Validator로 검증을 할 것이냐 선택해야한다는 것

나중에 포트폴리오용 프로젝트 등을 만들면 이는 큰 걸림돌이 될텐데 Spring-Boot 에서는 이정도만 배우자

Spring Framwork 포스팅을 올릴 때가 온다면 거기서 커맨드 객체 검증(Validation)에 대해서 자세히 올릴 예정임

 

커맨드 객체를 검증할 때 initBinder 를 통해서 Validator 를 생성하고 검증하는 방법과 커맨드 객체 안에서 애너테이션으로 검증하는 방법이 있으니 적절히 필요한 것들을 골라서 사용하면 됨

728x90
LIST

<< 학습 목표 >>

1. Validator 인터페이스를 구현해 커맨드 객체를 검증할 수 있다.

2. ValidationUtils 클래스의 메서드를 사용해 커맨드 객체를 검증할 수 있다.


클라이언트가 보낸 값을 사용하기 전에 반드시 검증해야함

검증은 다음과 같은 것을 해야함

1. 서버에 반드시 필요한 값이라면 클라이언트가 그 값을 보냈는지 확인

2. 클라이언트가 보낸 값이 서버에서 활용할 수 있는 형태인지 확인

 

검증하지 않고 사용하면 컨트롤러에 오류가 발생할 수도 있고 잘못된 데이터가 저장될 수 있음

 

studyProject 프로젝트 -> com.study.chapter02 -> ValidationController 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ValidationController {
	@PostMapping("/chapter02/validation/type1")
	public void type1(Member member) {
		System.out.println(member);
	}
}

일단 type1 컨트롤러는 별도의 커맨드 객체 검증을 하지 않고 있음

롬복을 사용한 커맨드 객체를 테스트한다는 생각으로 프로젝트를 실행시켜 값을 보내보자

 

여기서 이제 프로젝트가 2개가 됐음

이때는 내가 실행시키고 싶은 프로젝트를 정확히 확인한 후 실행시켜야함

[ Boot Dashboard ] (1)에서 studyProject 를 실행시키자


앞서 클라이언트가 파라미터를 반드시 전달해야하면 @RequestParam 애너테이션에 required 속성을 true로 설정하면 된다고 했는데 커맨드 객체를 사용했을 때는 @RequestParam 애너테이션을 붙일 수 없음

 

그럼 DTO 안에 @RequestParam 애너테이션을 붙일 수 있을까? 없음

@RequestParam 애너테이션은 컨트롤러의 매개변수에만 붙일 수 있음

그래서 DTO를 적용했다면 클라이언트가 파라미터를 보내지 않을 수 있음

그래서~! 여기서 커맨드 객체 검증을 배우는 것


서블릿 컨트롤러의 방식대로 직접 커맨드 객체를 검증해도 되지만 굉장히 비효율적임


Spring Framework는 커맨드 객체를 검증하는대 사용할 Validator 인터페이스를 제공하고 있음

Spring Framework 프로젝트는 이 인터페이스를 사용해 커맨드 객체를 검증할 Validator 클래스를 만들어야함

 

이제 커맨드 객체를 검증할 MemberValidator 를 만들자

studyProject 프로젝트 -> com.study.chapter02 -> MemberValidator 클래스를 추가하고 아래 코드를 추가하자

package com.study.chapter02;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class MemberValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
		return Member.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Member member = (Member) target;
		
		String id = member.getId();
		String pw = member.getPw();
		
		if(id == null || id.trim().isEmpty()) {
			errors.rejectValue("id", "empty");
		} else if(id.length() < 3 || id.length() > 20) {
			errors.rejectValue("id", "too short or long");
		} 
		
		if(pw == null || pw.trim().isEmpty()) {
			errors.rejectValue("pw", "empty");
		} else if(pw.length() < 6 || pw.length() > 20) { 
			errors.rejectValue("pw", "too short or long");
		}
	}
}

<< 코드 설명 (1) >>

위 코드와 설명하기 위해 캡쳐한 코드가 살짝 다른데 위 코드에 더해서 더 다양하게 커맨드 객체를 검증할 수 있음을 표현하기 위함

(1). 커맨드 객체를 검증할 Validator 클래스는 반드시 Validator 인터페이스를 구현해야함

 

(2). 커맨드 객체가 어떤 커맨드 객체를 검증할 수 있는지 명시하는 부분

return (커맨드객체명).class.isAssignableForm(claszz)

으로 형태가 정해져있음

이 커맨드 객체(MemberValidator)는 Member 커맨드 객체만 검증할 수 있는 Validator임

 

(3). 커맨드 객체를 검증하는 메서드

이 메서드에는 매개변수가 2개 있음

첫번째 매개변수 - 검증할 커맨드 객체를 전달 받을 매개변수

두번째 매개변수 - 커맨드 객체 검증에 실패했을 때 어떤 사유로 실패했는지 실패 사유를 적어두는 매개변수

 

<< 코드 설명 (2) >>

(1). 검증하기 위해 첫번째 매개변수로 전달 받은 커맨드 객체를 원래의 형태로 형변환

 

(2). 커맨드 객체 내 검증할 값들을 꺼냄

 

(3), (4). 커맨드 객체의 특정 값을 확인하는 부분으로 이와 같이 if문으로 서버에 필요한 값이 있는지 없는지 확인함

 

(3). 커맨드 객체의 특정 값이 없거나 비어있으면 errors에 이름은 id, 사유는 empty 로  "커맨드 객체 검증 실패" 로 기록

 

(4). 커맨드 객체의 특정 값이 너무 짧거나 길다면 errors에 이름은 pw, 사유는 too short or long 으로 "커맨드 객체 검증 실패"로 기록

 

그 이후에 있는 else if ( ... ) { } 은 커맨드 객체 검증에 필요한 코드를 더 쓰면 된다는 의도로 붙여둠

 

 

 

 

 

여기서 검증 성공과 검증 실패란 말이 나오는데 검증 성공이라 함은 커맨드 객체가 서버에 필요한 모든 값을 다 갖고 있다는 것

검증 실패라 함은 커맨드 객체 내 특정 값 하나라도 서버에 필요한 값을 갖고 있지 않을 경우임


Member 커맨드 객체를 검증할 MemberValidator 클래스를 만들었으니 이를 사용해 컨트롤러에서 커맨드 객체를 검증해보자

package com.study.chapter02;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ValidationController {
	@PostMapping("/chapter02/validation/type1")
	public void type1(Member member, BindingResult errors) {
		Validator validator = new MemberValidator();
		
		validator.validate(member, errors);
		
		if(errors.hasErrors()) {
			// 커맨드 객체 검증에 실패했을 때
			// 필요한 처리
		}
		
		if(errors.hasFieldErrors("id")) {
			// 커맨드 객체의 특정 값의 검증에 실패했을 때
			// 필요한 처리
		}
		
		// ...
	}
}

<< 코드 설명 >>

(1). 커맨드 객체를 검증하면서 검증에 실패했을 때 사유를 기록할 매개변수를 컨트롤러의 두 번째 매개변수로 선언해야함

그래야 컨트롤러가 동작하면서 사유를 기록할 객체가 만들어짐

(2). 커맨드 객체를 검증하기 위한 Validator 생성

(3). 커맨드 객체(member) 검증

(4), (5). 검증 결과를 확인 하고 검증에 실패했다면 필요한 처리를 함

 

프로젝트를 실행시키기 전에 커맨드 객체를 검증하는 방법을 다시 한번 자세히 알아보자


(1). 컨트롤러에 errors 매개변수를 선언했는데 이 매개변수는 커맨드 객체 검증 결과를 저장할 객체임

 

errors 매개변수는 따로 우리가 생성해야하는게 아님

클라이언트가 http://localhost:8080/chapter02/validation/type1 경로로 접근하면 이 컨트롤러(type1) 가 호출됨

 

이 컨트롤러가 호출되면 Spring Framework는 클라이언트가 보낸 파라미터는 첫 번째 매개변수인 member 에 저장됨

그 다음 Spring Framework가 알아서 errors 매개변수를 만들어줌

 

(2). errors 객체를 Validator 로 넘겨서 Validator 에서 커맨드 객체 검증에 실패했을 경우 errors에 실패 사유를 기록하는 것

 

커맨드 객체를 검증하기 위해서 validate 메서드를 호출하는데 validate 메서드로 검증 할 커맨드 객체를 첫번째 매개변수로, 검증 결과를 기록할 객체를 두번째 매개변수로 전달하고 있음

 

의외로 많은 사람들이 잘 모르는데 메서드를 호출했을 때 메서드의 끝을 만나면 다시 호출했던 제자리로 돌아와(2) 밑으로 내려감

 

 

이번에는 validate 메서드 내 코드를 다시 살펴보자

(1). 검증할 커맨드 객체를 원래의 형태로 형변환

(2). 커맨드 객체가 가지고 있는 검증할 값들을 꺼냄

(3). 검증할 값이 서버에 필요한 값을 가지고 있지 않을 경우

(4). errors 객체에 이름은 id로 사유는 empty 로 기록

  이 코드가 동작한다면 컨트롤러가 전달해준 errors 에 이와 같이 기록됨

 

당연하지만 값이 비어있지는 않았지만 길이가 짧거나 길이가 너무 길어 else if 가 동작했다면 다음과 같이 errors에 이름은 id로 사유는 too short or long 으로 기록됨

 

역시나 당연하지만 값이 비어있지 않았고 길이가 적절했다면 if, else if 모두 건너 뛰고 errors 에는 아무것도 기록되지 않음

값이 비어있어 id, empty 가 기록되어있는 상태라고 하자

 

아직 validate 메서드의 끝을 만나지 않았으므로 여기서 끝이 아니라 그 다음에 있는 다른 값을 검증하는 if문이 더 동작함

 

그 다음 if문이 동작해 비밀번호가 비어있지는 않았지만 짧거나 길긴 상황이라 else if 가 동작해 errors 에 다음과 같이 기록된 상황이라고 하자

 

 

이제 메서드의 끝을 만났으므로 제 자리(컨트롤러)로 돌아감

 

그 후 밑으로 내려가 if문들(1, 2)이 동작해 커맨드 객체 검증이 성공했는지 실패했는지 여부를 판단함

(1). hasErrors 메서드는 errors에 사유가 하나라도 기록되어있다면 true를 반환하고 사유가 아무것도 없다면 false를 반환함

(2). hasFieldErrors 메서드는 인자로 넣은 문자열로 기록된 사유가 있다면 true를 반환하고 인자로 넣은 문자열로 기록된 사유가 없다면 false를 반환함

 

커맨드 객체가 하나라도 비정상적인 값을 갖고 있다면 으로 커맨드 객체를 검증하고 싶다면 hasErrors(1) 메서드를 사용하고 커맨드 객체가 갖고 있는 값 중 일부가 비정상적인 값을 갖고 있다면 으로 커맨드 객체를 검증하고 싶다면 hasFieldErrors(2) 메서드를 사용하면 됨


 

커맨드 객체 검증 시 특정 값이 비어있는지 여부는 거의 모든 검증에 사용됨

특정 값이 비어있는지 여부를 체크하는 걸 직접 해도 되지만 Spring Framework 에서는 ValidationUtils 클래스로 제공함

 

MemberValidator 클래스를 아래와 같이 바꾸자

package com.study.chapter02;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class MemberValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
		return Member.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		Member member = (Member) target;
		
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "empty");
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pw", "empty");
		
		String id = member.getId();
		String pw = member.getPw();
		
		if(id.length() < 3 || id.length() > 20) {
			errors.rejectValue("id", "too short or long");
		} 
		
		if(pw.length() < 6 || pw.length() > 20) { 
			errors.rejectValue("pw", "too short or long");
		}
	}
}

<< 코드 설명 >>

코드가 살짝 바뀌었는데 이전 코드와 비교해보자

해당 부분을 ValidationUtils 클래스의 rejectIfEmptyOrWhitespace 메서드로 대체한 것

 

 

rejectIfEmptyOrWhitespace 메서드의 첫 번째 매개변수는 검증 결과를 저장할 errors, 두 번째 매개변수는 검증할 멤버 변수 이름, 세 번째 매개변수는 검증에 실패했을 때 실패 사유임

 

아마 여기서 의문이 드는 분도 있을 것

그렇다면 정확히 본 것

rejectIfEmptyOrWhitespace 메서드로 커맨드 객체를 전달해주지 않았는데 어떻게 커맨드 객체를 검증한다는걸까?

MemberValidator 클래스에 supports 메서드를 오버라이딩 했는데 이 메서드의 역할은 이 Validator 클래스가 어떤 커맨드 객체를 검증할 Validator인지 지정하는 것이라 했음

rejectIfEmptyOrWhitespace 메서드로 커맨드 객체를 전달해주지 않아도 supports 덕분에 어떤 커맨드 객체를 검증해야하는지 알 수 있는 것

그래서 커맨드 객체를 메서드로 넘겨주지 않아도 커맨드 객체를 검증할 수 있음


여기까지 실제 코드가 길진 않았지만 설명이 길었던 커맨드 객체 검증하기를 알아봤음

커맨드 객체 검증이 여기가 끝은 아니고 더 있으니 다음 글을 통해 커맨드 객체 검증을 더 깊게 들어가보자

728x90
LIST