dev-course day27

2rlokr·2025년 4월 9일

dev-course

목록 보기
27/43
post-thumbnail

오늘 배운 것

실습

case 1 : UUID

String targetPwd = UUID.randomUUID().toString();
// 우주에 있는 유일한 값
  • UUID.randomUUID() 로 무작위 UUID를 생성한다.
    • UUID : 네트워크 상에서 고유성이 보장되는 id를 만들기 위한 표준 규약

case 2 : remove

@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를 제거했다.

Soft Delete vs Hard Delete

Soft Delete (논리적 삭제)

데이터를 실제로 데이터베이스나 파일 시스템에서 제거하지 않는 방식이다. 대신, 데이터가 삭제되었다는 표시를 남겨 데이터를 더 이상 사용하지 않도록 한다.

Hard Delete (물리적 삭제)

실제 데이터베이스에서 데이터를 제거하는 방식이다.

case 3 : datasource에서 auto-commit 끄기

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 파라미터를 넣는 방식과 동일하다.

  • 범위 : DataSource를 통해 생성되는 모든 커넥션에 적용된다.
  • 설정 시점 : 커넥션이 생성되기 이전에 적용된다.
  • 의미 : 커넥션 풀에서 커넥션 객체를 생성할 때부터 autoCommit=false 상태로 만들어달라는 요청이다.
  • 주의할 점 : 이 방식은 커넥션 풀 구현체에 따라 무시될 수도 있어 신뢰성이 떨어질 수 있다.

case 4 : Connection에서 auto-commit 끄기

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() 으로 커넥션을 얻은 이후에 호출된다.
  • 의미 : 그 커넥션은 더 이상 자동 커밋을 하지 않겠다는 의미이다.

case 5 : SimpleJdbcService

하지만, 위와 같은 방법으로 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을 해줬기 때문에 롤백해줄 방법이 없다!! => ✅ 트랜잭션으로 로직을 관리해야 한다

case 6 : 트랜잭션 관리 (Transaction) 🤝

SimpleJdbcService.java

@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는 현재 트랜잭션의 상태와 정보를 담고 있는 핸들이다.
    • commitrollback을 할 때 이 객체를 넣는다.

❓ TransactionManager이란?

TransactionManager 은 트랜잭션을 시작하고, 끝날 때 커밋 또는 롤백을 결정하는 "트랜잭션 조율자"이다.

  1. 트랜잭션 시작 : 커넥션을 얻고 autoCommitfalse로 설정한다.
  2. 트랜잭션 바인딩 : 현재 스레드 (ThreadLocal)에 커넥션을 저장해둔다.
  3. 트랜잭션 종료 : commit() 또는 rollback() 을 호출하고 커넥션을 정리한다.

SimpleJdbcCrudTransactionRepository.java

@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()을 통해 커넥션을 닫거나 커넥션을 풀에 반납해준다.

SimpleJdbcServiceTests.java

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);
}
  • Repository와 Service에 넣어줄 dataSource를 만들어준다.
  • Service에 넣어줄 TransactionManager도 만들어줘야 하는데, TransactionManager은 인터페이스이기 때문에 구현체인 DataSourceTransactionManager을 생성하고, 인자로 dataSource를 넣어줘야 한다.

MyBatis 실습

MyBatis는 SQL Mapper 파일 (XML) 혹은 어노테이션 기반 Mapper 인터페이스를 작성하여 SQL과 Java 객체 간의 매핑을 설정한다.

ItemMapper.java (Inteface)

@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 인터페이스를 만들어주었다.
  • 이 인터페이스에서는 CRUD 기능을 실행할 함수를 선언해두었다.

ItemMapper.xml (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>
  
</mapper>
  • 이 XML 파일은 MyBatis Mapper용이라는 것을 명시하는 코드이다.
  • MyBatis XML Mapper 파일에서의 약속이기 때문에 꼭 적어줘야 한다.
<mapper namespace="io.silver.dao.dao.mybatis.ItemMapper">
  • mapper의 속성 namespace에 해당 XML 파일이 매핑할 인터페이스를 명시해두어야 한다.
  • 전체 경로를 작성해야 한다.
<insert id="save" useGeneratedKeys="true" keyProperty="id">
	INSERT INTO items
        (name, code, price)
    VALUES
        (#{name}, #{itemCode}, #{price})
</insert>
  • <mapper> 태그 안에는 쿼리문을 작성해줘야 한다.
  • SQL에서와 똑같이 <select>, <update>, <delete>, <insert> 태그가 있다.
  • 각 SQL 태그에는 id가 필요한데, 이 id는 인터페이스의 메서드 이름과 매핑된다.
  • useGeneratedKeysGeneratedKeys를 이용해 DB에 자동 생성된 PK를 가져오도록 하는 옵션이다.
    • 마지막 추가된 row의 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);
  • XML에서 #{}는 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);
  • DB에 저장된 컬럼에서 얻어와 객체에 매핑해주기 위해서는 alias를 이용해 객체 이름과 동일하게 맞춰줘야 한다.
  • @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 을 활용하지 않았지만, 객체를 바로 넘긴다.
  • 그럴 땐 MyBatis가 알아서 items.getId()를 자동으로 찾아서 #{id} 자리에 바인딩해준다.
    즉, 객체의 필드명을 변수명으로 인식해서 쓸 수 있다.

applicatoin.yml (설정 파일)

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)
    • 일일이 Connection을 만들지 않아줘도 알아서 MyBatis에서 해준다..!
  • type-aliases-package
    : 해당 경로에 있는 클래스들을 타입 별칭으로 등록해준다. XML에서 클래스명을 적을 때 풀 패키지명을 생략할 수 있게 해준다.
  • mapper-locations
    : MyBatis에게 Mapper XML 파일들이 어디에 있는지 알려주는 설정이다. classpath: 기준이기 때문에, resources 폴더 하위 구조로 인식한다. 또, *를 통해 xml파일을 하나하나 다 적지 않아도 된다.
  • map-underscore-to-camel-case
    : DB 컬럼명이 snake_case인데, 자바 필드는 camelCase일 때 자동으로 매핑해준다. 이 설정으로 XML에서 alias를 안 써도 되게 된다.

Builder 패턴

  • 생성자를 실수로 만드는 경우를 방지하기 위해 클래스에 빌더 패턴을 적용한 예시이다.
  • 생성자에 파라미터가 많을 때는 순서를 정확히 맞춰야 한다. 실수할 경우 의도한대로 파라미터가 들어가지 않아 잘못된 객체가 만들어질 수 있다.
  • 하지만, 빌더 패턴으로 만들 경우, 어떤 필드에 무슨 값이 들어가는지 명확해지고, 순서 상관없이 선택적으로 설정할 수 있다.
public 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() , Builderstatic 으로 두어서 클래스에서 직접 호출이 가능하다.
 User user = User.builder()
                .name(name)
                .age(age)
                .email(email)
                .build();

그래서 위와 같이 User 에서 builder()를 호출하고 new Builder() 객체를 만들고 여기서 메서드 체이닝으로 각 필드 값을 넣어줄 수 있다.

느낀 점

와우.. 오늘은 엄청 집중이 잘된 날이다 ! :) 기분이 좋군. 되게 재밌었던 건 .. 이 때까지 일일이 Connection 연결했다가.. 끊었다가.. 이것저것 고려해줬다가.. 이랬는데 MyBatis는 알아서 다 해준다니까 너무 신기했다 ! 뭔가 MyBatis부터 배웠다면 이 감사함(?)을 몰랐을 텐데 이런 순서로 배우니까 원래는 어떻게 이루어지는 건지, Spring이 뭘 추상화한 건지 이해할 수 있어서 좋은 것 같다. 항시.. 수업은 재미있지만 요즘은 더 재미있는 것 같다 !

평일의 반 ! 수요일도 끝 ! 목, 금.. 파이팅해보쟈 :)

0개의 댓글