
String targetPwd = UUID.randomUUID().toString();
// 우주에 있는 유일한 값
UUID.randomUUID() 로 무작위 UUID를 생성한다.@Test
@DisplayName("remove test")
void remove_test() throws Exception {
int targetId = 3;
repository.remove(targetId);
Optional<Member> memberOptional = repository.findById(targetId);
boolean result = memberOptional.isPresent();
assertThat(result).isFalse();
assertThatThrownBy(
() -> {
memberOptional.get();
}
).isInstanceOf(NoSuchElementException.class);
}
memberOptional.get()으로 없는 걸 꺼내려고 할 때 NoSuchElementException이 발생한다.assertThatThrownBy로 해당 에러가 발생하는지 확인하고, 예외가 발생하면 테스트가 통과한다.hard delete 방식으로 row를 제거했다.데이터를 실제로 데이터베이스나 파일 시스템에서 제거하지 않는 방식이다. 대신, 데이터가 삭제되었다는 표시를 남겨 데이터를 더 이상 사용하지 않도록 한다.
실제 데이터베이스에서 데이터를 제거하는 방식이다.
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(ConnectionUtil.MysqlDbConnectionConstant.URL);
dataSource.setUsername(ConnectionUtil.MysqlDbConnectionConstant.USERNAME);
dataSource.setPassword(ConnectionUtil.MysqlDbConnectionConstant.PASSWORD);
dataSource.setAutoCommit(false);
// jdbc:mysql://localhost:3306/grepp_jdbc?autoCommit=false&charset=UTF-8&timeZone=Asia/Seoul
JDBC URL에 autoCommit=false 파라미터를 넣는 방식과 동일하다.
autoCommit=false 상태로 만들어달라는 요청이다.try{
conn = dataSource.getConnection();
conn.setAutoCommit(false);
Member saveReq = new Member(0, "_test", "_test");
String sql = "insert into member (username, password) values (?,?)";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, saveReq.getUsername());
pstmt.setString(2, saveReq.getPassword());
pstmt.executeUpdate();
conn.commit();
} catch (SQLException e) {
throw e;
}
dataSource.getConnection() 으로 커넥션을 얻은 이후에 호출된다.하지만, 위와 같은 방법으로 Repository 안에서 save(), update()등의 메서드에서 auto-commit=false 로 두고, conn.commit() 으로 커밋을 해줘도 문제가 생길 수 있다.
// SimpleJdbcService.java
public void logic1() {
Member newMember = new Member(0, "member1", "member1");
repository.save(newMember);
Member findMember = repository.findById(newMember.getMemberId());
findMember.setPassword("asdf");
// 여기서 예외 발생 !!
repository.update(findMember);
서비스에서 비즈니스 로직을 구현하기 위해 logic1()을 저렇게 구현했다고 가정해보자. 해당 메서드에서는 save(), update() 둘 다를 수행하는 게 서비스 로직으로 정해져있는 것이다.
⚠️ 하지만, 위와 같이 save()만 해주고, update() 전에 예외가 발생했다면..? => 사용자가 원한 기능 (하나의 비즈니스 로직)을 수행하지 못한 것이다. 그래서 되돌리려면 저 로직 전체를 되돌려줘야 한다 !
BUT! 이미 save()는 내부에서 commit을 해줬기 때문에 롤백해줄 방법이 없다!! => ✅ 트랜잭션으로 로직을 관리해야 한다
@RequiredArgsConstructor
public class SimpleJdbcService {
private final SimpleCrudRepository repository;
private final PlatformTransactionManager transactionManager;
public void logic1(Member saveReq, boolean isRollback) {
TransactionStatus transaction = transactionManager.getTransaction(
new DefaultTransactionDefinition()
);
try {
Member saved = repository.save(saveReq);
Optional<Member> memberOptional = repository.findById(saved.getMemberId());
Member findMember = memberOptional.orElseThrow();
log.info("findMember.getUsername() = {}", findMember.getUsername());
if(isRollback) {
transactionManager.rollback(transaction);
return;
}
transactionManager.commit(transaction);
} catch (Exception e) {
transactionManager.rollback(transaction);
}
}
}
transactionManager 을 받는다..getTransaction()을 호출하면 트랜잭션이 시작되고, DataSource에서 커넥션을 가져온다. DataSourceUtils.getConnection()으로 같은 커넥션을 공유할 수 있다.TransactionStatus는 현재 트랜잭션의 상태와 정보를 담고 있는 핸들이다.commit과 rollback을 할 때 이 객체를 넣는다.❓ TransactionManager이란?
TransactionManager 은 트랜잭션을 시작하고, 끝날 때 커밋 또는 롤백을 결정하는 "트랜잭션 조율자"이다.
autoCommit을 false로 설정한다.commit() 또는 rollback() 을 호출하고 커넥션을 정리한다.@RequiredArgsConstructor
public class SimpleJdbcCrudTransactionRepository implements SimpleCrudRepository {
private final DataSource dataSource;
private Connection getConnection() throws SQLException {
return DataSourceUtils.getConnection(dataSource);
}
private void closeConnection(Connection connection, Statement statement, ResultSet resultSet) {
JdbcUtils.closeStatement(statement);
JdbcUtils.closeResultSet(resultSet);
DataSourceUtils.releaseConnection(connection, dataSource);
}
❓ DataSourceUtils란?
DataSourceUtils 는 Spring 트랜잭션 매니저와 연동해서 커넥션을 안전하게 얻고, 반납하는 유틸 클래스이다.
즉, 트랜잭션이 열려 있으면 이미 바인딩된 커넥션을 재사용하고, 없으면 새 커낵션을 생성한다.
DataSourceUtils.getConnection(dataSource) 로 현재 트랜잭션이 시작됐다면, 해당 트랜잭션이 사용하는 커넥션을 반환하고, 시작된 트랜잭션이 없다면 dataSource.getConnection()을 호출해서 새 커넥션을 만든다.
DataSourceUtils.releaseConnection(connection, dataSource) 로 커넥션을 처리해줄 수 있다.
conn.close()을 통해 커넥션을 닫거나 커넥션을 풀에 반납해준다.SimpleCrudRepository repository;
SimpleJdbcService simpleJdbcService;
@BeforeEach
void init() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(
ConnectionUtil.MysqlDbConnectionConstant.URL,
ConnectionUtil.MysqlDbConnectionConstant.USERNAME,
ConnectionUtil.MysqlDbConnectionConstant.PASSWORD
);
repository = new SimpleJdbcCrudTransactionRepository(dataSource);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
simpleJdbcService = new SimpleJdbcService(repository, transactionManager);
}
dataSource를 만들어준다.DataSourceTransactionManager을 생성하고, 인자로 dataSource를 넣어줘야 한다.MyBatis는 SQL Mapper 파일 (XML) 혹은 어노테이션 기반 Mapper 인터페이스를 작성하여 SQL과 Java 객체 간의 매핑을 설정한다.
@Mapper
public interface ItemMapper {
void save(Items items);
void update(@Param("id") Long id, @Param("price") Integer price);
Items findByItemCode(@Param("itemCode") String itemCode);
void remove(Items items);
}
@Mapper 어노테이션을 가진 ItemMapper 인터페이스를 만들어주었다. <?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>
</mapper>
<mapper namespace="io.silver.dao.dao.mybatis.ItemMapper">
namespace에 해당 XML 파일이 매핑할 인터페이스를 명시해두어야 한다.<insert id="save" useGeneratedKeys="true" keyProperty="id">
INSERT INTO items
(name, code, price)
VALUES
(#{name}, #{itemCode}, #{price})
</insert>
<mapper> 태그 안에는 쿼리문을 작성해줘야 한다.<select>, <update>, <delete>, <insert> 태그가 있다. id가 필요한데, 이 id는 인터페이스의 메서드 이름과 매핑된다. useGeneratedKeys 는 GeneratedKeys를 이용해 DB에 자동 생성된 PK를 가져오도록 하는 옵션이다.keyProperty는 가져온 PK 값을 어떤 필드에 넣을지 지정하는 속성이다. (DB에는 item_id로 저장되어있고, 객체에서는 id로 필드가 정의되어있다.)<update id="update" >
UPDATE
items
SET
price = #{price}
WHERE
item_id = #{id}
</update>
// void update(@Param("id") Long id, @Param("price") Integer price);
#{}는 MyBatis에서 파라미터를 바인딩할 때 사용한다.update 함수에서 @Param("파라미터명")을 반드시 붙여야 XML에서 사용가능하다. @Param에 있는 파라미터랑 #{}에 있는 게 매핑되는 것이다. (이 파라미터를 XML에서는 이렇게 불러달라)<select id="findByItemCode" resultType="Items">
SELECT
i.item_id as id,
i.name,
i.code as itemCode,
i.price,
i.created_at as createdAt
FROM
items i
WHERE
i.code = #{itemCode}
</select>
// Items findByItemCode(@Param("itemCode") String itemCode);
@Param("itemCode")으로 여기서도 #{itemCode} 로 사용한다.resultType 속성으로 해당 쿼리의 결과를 어떤 Java 객체로 매핑할지 알려주는 설정이다. namespace처럼 전체 경로를 써줘야 하지만, application.yml 설정 파일에서 설정해주면 저렇게 객체명만 나타낼 수 있다.<delete id="remove">
DELETE FROM items i
WHERE
i.item_id = #{id}
</delete>
// void remove(Items items);
@Param 을 활용하지 않았지만, 객체를 바로 넘긴다. items.getId()를 자동으로 찾아서 #{id} 자리에 바인딩해준다.spring:
datasource:
url: jdbc:mysql://localhost:3306/{데이터베이스이름}
username: {사용자이름}
password: {비밀번호}
mybatis:
type-aliases-package: io.silver.dao.global.entity
mapper-locations: classpath:mappers/*.xml
configuration:
map-underscore-to-camel-case: true
spring: datasource: 로 dataSource 연결설정을 해줘야 한다. (url, username, password)type-aliases-packagemapper-locationsclasspath: 기준이기 때문에, resources 폴더 하위 구조로 인식한다. 또, *를 통해 xml파일을 하나하나 다 적지 않아도 된다.map-underscore-to-camel-casepublic class User {
private String name;
private Integer age;
private String email;
private String address;
public static Builder builder() {
return new Builder();
}
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
this.address = builder.address;
}
public static class Builder {
private String name;
private Integer age;
private String email;
private String address;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(Integer age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
User 생성자는 Builer 내부 클래스의 값을 가져와서 초기화한다. User의 기본 생성자를 private로 두었기 때문에, 외부에서는 직접 생성자를 호출할 수 없고, builder().build()로만 User 객체를 생성할 수 있다.builder() , Builder 를 static 으로 두어서 클래스에서 직접 호출이 가능하다. User user = User.builder()
.name(name)
.age(age)
.email(email)
.build();
그래서 위와 같이 User 에서 builder()를 호출하고 new Builder() 객체를 만들고 여기서 메서드 체이닝으로 각 필드 값을 넣어줄 수 있다.
와우.. 오늘은 엄청 집중이 잘된 날이다 ! :) 기분이 좋군. 되게 재밌었던 건 .. 이 때까지 일일이 Connection 연결했다가.. 끊었다가.. 이것저것 고려해줬다가.. 이랬는데 MyBatis는 알아서 다 해준다니까 너무 신기했다 ! 뭔가 MyBatis부터 배웠다면 이 감사함(?)을 몰랐을 텐데 이런 순서로 배우니까 원래는 어떻게 이루어지는 건지, Spring이 뭘 추상화한 건지 이해할 수 있어서 좋은 것 같다. 항시.. 수업은 재미있지만 요즘은 더 재미있는 것 같다 !
평일의 반 ! 수요일도 끝 ! 목, 금.. 파이팅해보쟈 :)