SpringBoot + MyBatis + Oracle(with Docker) CRUD 구현해보기

지난 포스팅에서 스프링부트로 만든 API로 오라클RDBMS에 데이터를 요청하는 간단한 실습을 구현해보았다. 당시 오라클 DB의 테이블을 단순 조회하는 것까지 해보았는데, 이번엔 간단한 CRUD를 구현한 API를 만들어보려고 한다.

마찬가지로 도커를 이용하여 오라클 DB(Oracle 11g)를 띄워두었고, 스프링부트로 웹서버를 개발하고, MyBatis를 이용하여 DB를 매핑하였다.

조회(Read)하는건 했으니 이번 포스팅에선 C(Create), U(update), D(delete)를 구현하면 될 것 같다.

2달만에 다시 하려고 하니 테이블을 다시 만들어야 하는 과정에서 필드 하나를 누락했다. 만약 첫번째 포스팅을 보고 이어서 실습을 하는 분이 계시다면 아래의 테이블 변경부터 하고 튜토리얼을 진행하겠다.

원래 Members 라는 테이블에 loc 라는 필드가 있었는데, 이를 삭제했다.

1
2
ALTER TABLE MEMBERS
DROP COLUMN loc;
  • VO 약간의 리팩토링하기
  • 컨트롤러 리팩토링 및 추가하기
  • 서비스 생성하기
  • DAO 생성하기
  • Mapper 쿼리 작성하기
  • 포스트맨으로 API 조회하여 테스트

VO 리팩토링

지난 포스팅때 VO를 만들면서 Lombok 어노테이션의 @Data 를 사용했는데, 이렇게 하면 어떤 어노테이션을 사용중인지 직관적이지 않다는 생각이 들어서 이를 @Getter, @Setter 어노테이션으로 분리했다.

그리고 @JsonProperty 어노테이션에 value값을 넣어서 JSON 객체를 주고받을 때, 필드명을 좀 더 분명히 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.devandy.web.vo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class MemberVO {
@JsonProperty(value="id")
private int id;

@JsonProperty(value="name")
private String name;

@JsonProperty(value="job")
private String job;
}

컨트롤러 리팩토링 및 추가하기

지난 포스팅때는 @Controller 를 생성해서 각 컨트롤러마다 @ResponseBody 어노테이션을 사용했는데, 화면과 통신하는게 아니라 그저 JSON만 주고받는 컨트롤러이기 때문에 리팩토링을 해보았다.

@RestController 로 생성해서 @ResponseBody 어노테이션을 쓰지않아도 되도록 리팩토링 하였다.

회원 등록 컨트롤러

1
2
3
4
5
@PostMapping("/member/new")
public List<MemberVO> insertMember(@RequestBody MemberVO member){
memberService.insertMember(member);
return memberService.selectAllMembers();
}

HTTP body로 들어오는 JSON을 가져오기 위해서 컨트롤러의 파라미터 영역에서 @RequestBody 어노테이션을 사용했다.

서비스단에서 비즈니스 로직을 구현할 예정이므로 아직 생성하지 않은 서비스의 insertMember() 를 호출했다.

마지막으로 포스트맨에서 API 요청을 하면, 실제로 회원이 성공적으로 추가되었는지를 확인하기 위해 전체 회원을 반환할수있도록 List<> 타입으로 반환타입을 지정하였다.

List<MemberVO> 는 리스트인데, 이 리스트에 들어올 수 있는 타입으로 MemberVO 클래스로만 고정하는 제너릭 타입이다.

회원 수정 컨트롤러

어떤 멤버를 수정할지를 URL에 id값을 받아서 해당 회원정보를 HTTP body에 들어온 JSON 데이터로 변경하는 컨트롤러를 생성했다.

1
2
3
4
5
@PutMapping("/member/{id}")
public List<MemberVO> updateMember(@PathVariable int id, @RequestBody MemberVO member){
memberService.updateMember(id, member);
return memberService.selectAllMembers();
}

RESTful한 API를 위해 회원 수정 API는 HTTP PUT 메서드를 받도록(@PutMapping) 개발했다.

쿼리스트링이 아닌 REST 방식으로 URL을 설계하였으며, 이를 위해 컨트롤러의 파라미터에 @PathVariable 어노테이션을 사용하여 URL로 들어오는 id 값을 매핑처리하였다.

서비스에 id값과 MemberVO 를 인자로 받는 updateMember()를 호출하도록 하였다. 향후 이 서비스에서 id값으로 회원을 찾아서 해당 회원에 대한 정보를 인자로 들어온 member 변수로 변경할 것이다.

회원 삭제 컨트롤러

마찬가지로 RESTful한 API 설계를 위해 HTTP DELETE 메서드를 받도록 처리하였다. (@DeleteMapping())

회원 삭제 컨트롤러도 회원 수정 컨트롤러와 마찬가지로 URL로 들어오는 id값을 인자로 가져와서 해당 멤버를 조회후 삭제하는 로직을 태웠다.

1
2
3
4
5
@DeleteMapping("/member/{id}")
public List<MemberVO> deleteMember(@PathVariable int id){
memberService.deleteMember(id);
return memberService.selectAllMembers();
}

Github에 소스코드를 공개해두었다.

MemberController.java


서비스 생성하기

인터페이스를 먼저 생성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.devandy.web.service;

import java.util.List;
import com.devandy.web.vo.MemberVO;

public interface MemberService {

List<MemberVO> selectAllMembers();
void insertMember(MemberVO member);
void updateMember(int id, MemberVO member);
void deleteMember(int id);

}

이제 인터페이스의 구현체를 생성해보자.

회원 등록 서비스

1
2
3
4
@Override
public void insertMember(MemberVO member) {
memberDao.insert(member);
}

DB에 회원 생성 쿼리를 날려야 하므로 DAO의 메서드에 member 객체를 파라미터로 담아서 호출만 하면 된다.

회원 수정 서비스

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void updateMember(int id, MemberVO updateMember) {
MemberVO member = memberDao.selectById(id);

if(member!=null){
member.setName(updateMember.getName());
member.setJob(updateMember.getJob());
memberDao.update(member);
} else {
throw new IllegalStateException("회원이 존재하지 않습니다.");
}
}

회원 수정을 하기 위해서 파라미터로 받은게 두가지가 있다. 하나는 수정할 대상 회원의 id와 변경할 부분의 JSON 값.

먼저 수정 대상인 회원이 존재하는지 여부를 확인(memberDao.selectById(id)!=null)하고, 그렇지 않으면 IllegalStateException() 에 메세지를 작성하여 콘솔에 출력하도록 하였다. 이 예외처리가 발생하면 클라이언트는 500 HTTP 상태 코드를 전달 받을 것이다.

DAO에서 id값으로 회원을 조회한 결과가 null이 아니라면, 회원이 존재하므로 회원 객체에 JSON으로 넘어온 값에서 namejob 을 set해주고, 수정 쿼리를 날려주는 DAO의 update(member) 를 호출한다.

회원 삭제 서비스

1
2
3
4
5
6
7
8
@Override
public void deleteMember(int id) {
if(memberDao.selectById(id)!=null){
memberDao.delete(id);
} else {
throw new IllegalStateException("회원이 존재하지 않습니다.");
}
}

회원 삭제 서비스도 수정과 마찬가지로 회원이 존재하는지를 확인하고, 없으면 500 상태코드와 함께 콘솔에 메세지를 출력하고, DAO의 삭제 쿼리를 호출한다.

Github에 소스코드를 공개해두었다.

MemberServiceImpl.java


DAO 생성하기

DAO는 인터페이스이므로 DB 제어가 필요한 메서드만 만들어두었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.devandy.web.dao;

import com.devandy.web.vo.MemberVO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface MemberDao {
List<MemberVO> selectAll();
MemberVO selectById(int id);
void insert(MemberVO member);
void update(MemberVO member);
void delete(int id);
}

Service와 DAO 차이

간단한 실습 튜토리얼이다 보니 서비스와 DAO가 비슷해보이는 측면이 있다. 그러나 서비스와 DAO는 엄연히 다른 개념이다.

서비스가 DAO를 포함한 비즈니스 로직을 의미한다면, DAO는 실제로 쿼리를 이용하여 DB와 통신이 필요한 로직에만 해당한다.

함께 읽으면 좋을 글


Mapper 쿼리 작성하기

DAO에 의하여 실제로 DB와 통신할 쿼리가 작성될 곳이다.

Mapper 쿼리에서 사용하는 id값은 DAO에서 작성한 메서드명과 매핑된다.

ID값으로 회원조회하는 Mapper 쿼리

1
2
3
4
5
<select id="selectById" parameterType="int" resultType="MemberVO">
SELECT *
FROM MEMBERS
WHERE id = #{id}
</select>

조회하는 쿼리이므로 <select> 태그를 사용하였으며, 파라미터 타입으로 id의 타입인 int, 반환 타입은 SELECT 결과에 따른 멤버 객체를 지정하였다.

Mapper 쿼리에서 사용하는 id값은 DAO에서 작성한 메서드명과 매핑된다.

회원 등록 Mapper 쿼리

1
2
3
4
<insert id="insert" parameterType="MemberVO">
INSERT INTO MEMBERS(id, name, job)
VALUES(id_seq.nextval, #{name}, #{job})
</insert>

JSON으로 가져온 멤버 객체를 파라미터로 받아서 각 필드에 INSERT 했다. 여기서 id는 시퀀스로 생성했기 때문에 임의로 입력하는 대신 오라클 시퀀스에 의해 자동으로 값을 생성해서 INSERT하도록 하였다.

회원 수정 Mapper 쿼리

1
2
3
4
5
6
<update id="update" parameterType="MemberVO">
UPDATE MEMBERS
SET name = #{name},
job = #{job}
WHERE id = #{id}
</update>

회원 삭제 Mapper 쿼리

1
2
3
4
<delete id="delete" parameterType="int">
DELETE MEMBERS
WHERE id = #{id}
</delete>

Github에 소스코드를 공개해두었다.

MemberDaoImpl.xml


포스트맨으로 API 조회하여 테스트

회원 등록 API 테스트

1
POST  /member/new

포스트맨에서 요청할 때 Body에 JSON 객체를 담아야 하므로 아래의 이미지처럼 JSON 형태로 작성해서 API를 요청했다.

Body에 담은 JSON 객체가 성공적으로 회원 목록에 포함된걸 확인했다. id가 6이 아니라 8로 입력된 이유는 이 스크린샷을 찍기 전에 테스트를 하느라 시퀀스를 소비(?)했기 때문이다.

회원 수정 API 테스트

1
PUT  /member/{id}

이번엔 아까 등록해둔 Zidane이라는 회원의 job을 변경하는 수정 API를 요청해보았다.

반환받은 회원목록에서 Zidane 회원의 job이 Coach에서 Legend로 성공적으로 바뀐것을 확인하였다.

회원 삭제 API 테스트

1
DELETE  /member/{id}

이번엔 Zidane 회원을 삭제하는 API를 요청해보았다.

성공했다!