ํ’€์Šคํƒ ์›น๐ŸŒ ๊ฐœ๋ฐœ์ž ์ง€๋ง์ƒ ๐Ÿง‘๐Ÿฝโ€๐Ÿ’ป
โž• ์ธ๊ณต์ง€๋Šฅ ๊ด€์‹ฌ ๐Ÿค–


Categories


Recent views

  • 1
  • 2
  • 3
  • 4
  • 5

Spring5 ์ž…๋ฌธ-DB ์—ฐ๋™

  1. ์˜์กด ๋ชจ๋“ˆ ์ถ”๊ฐ€ ๋ฐ DB ์„ค์ •
    • DataSource ์„ค์ •
    • JdbcTemplate
    • ์Šคํ”„๋ง ์ต์…‰์…˜ ๋ณ€ํ™˜ ์ฒ˜๋ฆฌ
      • ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ : @Transactional
      • ๋กœ๊น… ์ฒ˜๋ฆฌ

        DB ์—ฐ๋™

        ๐Ÿ—ฃ๏ธ ์ถœ์ฒ˜

        _ ์ดˆ๋ณด ์›น ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์Šคํ”„๋ง 5 ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ž…๋ฌธ _์™€ ์Šคํ”„๋ง ์ธ ์•ก์…˜ ์˜ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ •๋ฆฌํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.

        ์˜์กด ๋ชจ๋“ˆ ์ถ”๊ฐ€ ๋ฐ DB ์„ค์ •

        1. Mysql๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ง„ํ–‰๋˜๋ฉฐ Maven์ด๋‚˜ Gradle์„ ์ด์šฉํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜์กด ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•˜์ž.
          • spring-jdbc : JdbcTemplate, ์Šคํ”„๋ง ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋Šฅ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณต
            • spring boot์˜ ๊ฒฝ์šฐ: spring-boot-starter-jdbc
          • tomcat-jdbc : DB ์ปค๋„ฅ์…˜ ํ’€ ๊ธฐ๋Šฅ์„ ์ œ๊ณต
          • mysql-connector-java : MySQL ์—ฐ๊ฒฐ์— ํ•„์š”ํ•œ JDBC ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ œ๊ณต
        2. Mysql์€ ๊ณต์‹ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ์ด์šฉํ•ด ์„ค์น˜ํ•˜๊ฑฐ๋‚˜ Docker๋ฅผ ์ด์šฉํ•ด ์ƒ์„ฑํ•˜์ž.
        3. ์ดํ›„ DB ์„œ๋ฒ„์— ์ ‘์†ํ•ด ์ง์ ‘ SQL ๊ตฌ๋ฌธ์„ ์ด์šฉํ•ด DB, ์‚ฌ์šฉ์ž, ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋ฐ ๊ถŒํ•œ ์„ค์ •์„ ํ•œ๋‹ค.
        ๐Ÿงพ๏ธ sql๋ฌธ ์˜ˆ์‹œ

        ๋งŒ์•ฝ ์Šคํ”„๋ง ๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, src/main/resources/schema.sql, data.sql ํŒŒ์ผ์˜ SQL ๊ตฌ๋ฌธ์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰์‹œ ์ž๋™์œผ๋กœ ์‹คํ–‰ํ•œ๋‹ค.

        create user 'spring5'@'localhost' identified by 'spring5';
        create database spring5fs character set=utf8;
        grant all privileges on spring5fs.* to 'spring5'@'localhost';
        create table spring5fs.MEMBER (
        	ID int auto_increment primary key,
        	EMAIL varchar(255),
        	PASSWORD varchar(100),
        	NAME varchar(100),
        	REGDATE datetime,
        	unique key (EMAIL)
        ) engine=InnoDB character set = utf8;
        
        1. DB์™€ ํ†ต์‹ ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋ชจ์•„๋†“์€ ๊ฐ์ฒด์ธ DAO(Data Access Object) ๋˜ํ•œ ์ •์˜ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
        ๐Ÿงพ๏ธ /src/main/java/spring/MemberDao.java

        ์ถ”๊ฐ€๋กœ ๊ตฌ์„ฑ ํด๋ž˜์Šค๋กœ ํ•˜๋‹จ์˜ memberDao๋ฅผ ๋นˆ์œผ๋กœ ์ถ”๊ฐ€ํ•˜์ž.

        package spring;
        import java.util.Collection;
        public class MemberDao {
        	public Member selectByEmail(String email) {
        		return null;
        	}
        	
        	public void insert(Member member) {}
        
        	public void update(Member member) {}
        
        	public Collection<Member> selectAll() {
        		return null;
        	}
        }
        

        DataSource ์„ค์ •

        JDBC API๋Š” DriverManager ๊ฐ์ฒด ํ˜น์€ DataSource ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์ปค๋„ฅ์…˜ ํ’€์—์„œ ์—ฐ๊ฒฐ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

        โ„น๏ธ ์ปค๋„ฅ์…˜ ํ’€(Connection pool)์ด๋ž€?

        DBMS ์—ฐ๊ฒฐ์„ ์ƒ์„ฑํ•˜๋Š” ์‹œ๊ฐ„์„ ์ค„์ด๊ณ , ๋ฐœ์ƒํ•˜๋Š” ๋ถ€ํ•˜๋ฅผ ์ œํ•œํ•˜๊ธฐ ์œ„ํ•ด ์ผ์ • ๊ฐœ์ˆ˜์˜ DB ์ปค๋„ฅ์…˜์„ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•ด๋‘๊ณ  ์ด๋ฅผ ์‚ฌ์šฉ์‹œ ๋งˆ๋‹ค ๋Œ€์—ฌ, ๋ฐ˜๋‚ฉํ•˜๋Š” ๋ฐฉ์‹

        ๊ฐ ์ปค๋„ฅ์…˜์€ ์‚ฌ์šฉ ์ค‘์ธ ํ™œ์„ฑ ์ƒํƒœ, ๋Œ€๊ธฐ ์ค‘์ธ ์œ ํœด ์ƒํƒœ๊ฐ€ ์กด์žฌํ•œ๋‹ค.

        Tomcat JDBC Datasource ํด๋ž˜์Šค

        ๋ชจ๋“  Datasource ํด๋ž˜์Šค๋Š” javax.sql.Datasource๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋ฉฐ, ์šฐ๋ฆฌ๋Š” Tomcat JDBC ๋ชจ๋“ˆ์˜ Datasource ํด๋ž˜์Šค ์ด์šฉํ•  ๊ฒƒ์ด๋‹ค.

        ๐Ÿงพ๏ธ /src/main/java/config/AppCtx.java

        ๋จผ์ € DataSource๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜๊ณ  ์ฃผ์ž…์„ ๋ฐ›๋„๋ก ํ•œ๋‹ค.
        ์ด์™ธ์˜ DB ์„ค์ •์€ ๋‹ค์Œ์ด ์žˆ๋‹ค.

        • ์ปค๋„ฅ์…˜ ํ• ๋‹น ์‹œ ๊ฒ€์‚ฌ(setTestOnBorrow)
        • ์ปค๋„ฅ์…˜ ์œ ํšจ ์ฟผ๋ฆฌ ์ง€์ •(setValidationQuery("select 1"))
        • ์ตœ์†Œ ์ปค๋„ฅ์…˜ ๊ฐฏ์ˆ˜(setMinIdle)
        • ์ตœ๋Œ€ ์—ฐ๊ฒฐ ํ• ๋‹น ๋Œ€๊ธฐ ์‹œ๊ฐ„(setMaxWait(default=30์ดˆ))
        • ์œ ํœด ์—ฐ๊ฒฐ ์ œ๊ฑฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„(setMinEvictableIdleTimeMillis(default=60์ดˆ))
        package config;
        
        import org.apache.tomcat.jdbc.pool.DataSource;
        //...
        
        @Configuration
        public class DbConfig {
        	@Bean(destroyMethod="close") // DataSource ๊ฐ์ฒด ์†Œ๋ฉธ ์‹œ close ๋ฉ”์„œ๋“œ(์ปค๋„ฅ์…˜ ํ’€ ๋น„์šฐ๊ธฐ)๋ฅผ ์‹คํ–‰
        	public DataSource dataSource() {
        		DataSource ds = new DataSource(); // ๊ฐ์ฒด ์ƒ์„ฑ
        		ds.setDriverClassName("com.mysql.jdbc.Driver"); // JDBC Mysql ๋“œ๋ผ์ด๋ฒ„ ์‚ฌ์šฉ
        		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8"); // URL ์ง€์ •
        		ds.setUsername("spring5"); // ์œ ์ €๋ช… ์„ค์ •
        		ds.setPassword("spring5"); // ํŒจ์Šค์›Œ๋“œ ์„ค์ •
        		ds.setInitialSize(2); // ์ดˆ๊ธฐ ์ปค๋„ฅ์…˜ ๊ฐœ์ˆ˜ ์ง€์ •
        		ds.setMaxActive(10); // ์ตœ๋Œ€ ์ปค๋„ฅ์…˜ ๊ฐœ์ˆ˜ ์ง€์ •
        		ds.setTestWhileIdle(true); // ์œ ํœด ์ƒํƒœ ์—ฐ๊ฒฐ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฒ€์‚ฌ
        		ds.setMinEvictableIdleTimeMillis(1000*60*3); // ์ตœ์†Œ ์œ ํœด ์‹œ๊ฐ„ 3๋ถ„
        		ds.setTimeBetweenEvictionRunsMillis(10*1000); // 10์ดˆ ์ฃผ๊ธฐ ๊ฒ€์‚ฌ
        		return ds;
        	}
        }
        

        ์œ„์™€ ๊ฐ™์ด ์„ค์ •ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์—ฐ๊ฒฐ์„ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        ๐Ÿงพ๏ธ /src/main/java/dbquery/DbQuery.java
        package dbquery;
        import java.sql.Connection;
        import java.sql.ResultSet;
        import java.sql.SQLException;
        import java.sql.Statement;
        import javax.sql.DataSource;
        public class DbQuery {
            private DataSource dataSource;
            public DbQuery(DataSource dataSource) {
                this.dataSource = dataSource;
            }
            public int count() {
                Connection conn = null;
                try {
                    conn = dataSource.getConnection(); // ์—ฐ๊ฒฐ ๊ฐ€์ ธ์˜ค๊ธฐ
                    try (Statement stmt = conn.createStatement(); // sql๋ฌธ ์ƒ์„ฑ ์ค€๋น„๋ฅผ ์œ„ํ•œ Statement
                            ResultSet rs = stmt.executeQuery("select count(*) from MEMBER")) {
                        rs.next(); 
                        return rs.getInt(1); // ๊ฒฐ๊ณผ ๊ฐ’์ธ ResultSEt 
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                } finally {
                    if (conn != null)
                        try {
                            conn.close();
                        } catch (SQLException e) {
                        }
                }
            }
        }
        

        JdbcTemplate

        Spring์€ JDBC, JPA, MyBatis ๋“ฑ ์—ฌ๋Ÿฌ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ด๋ฒˆ์—๋Š” JdbcTemplate๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ณด์ž.

        JdbcTemplate ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜๋ฉด ์ผํ”Œ๋ฆฟ ๋ฉ”์„œ๋“œ ํŒจํ„ด๊ณผ ์ „๋žต ํŒจํ„ด์„ ์ด์šฉํ•˜์—ฌ ๊ธฐ์กด JDB API์˜ ๊ตฌ์กฐ์  ๋ฐ˜๋ณต์„ ์ค„์ด๊ณ  ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋Š˜๋ฆด ์ˆ˜ ์žˆ๋‹ค.

        ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

        ๐Ÿงพ๏ธ JDBC API ์—ฐ๋™ ์ฝ”๋“œ
        Member member;
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
        	conn = DriverManager.getConnection("jdbc:mysql://localhost/spring5fs", "spring5", "spring5");
        	// ------โ†‘โ†‘-----๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ------โ†‘โ†‘-----
        	pstmt = conn.prepareStatement("select * from MEMBER where EMAIL = ?");
        	pstmt.setString(1, email);
        	rs = pstmt.executeQuery();
        	if (rs.next()) {
        		member = new Member(rs.getString("EMAIL"), 
        			rs.getString("PASSWORD"),
        			rs.getString("NAME"),
        			rs.getTimestamp("REGDATE"));
        		member.setId(rs.getLong("ID"));
        		return member;
        	} else {
        		return null;
        	}
        	// ------โ†“โ†“-----๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ------โ†“โ†“-----
        } catch (SQLException e) {
        	e.printStackTrace();
        	throw e;
        } finally {
        	if (rs != null)
        		try { rs.close(); } catch (SQLException e2) {}
        	if (pstmt != null)
        		try { pstmt.close(); } catch (SQLException e1) {}
        	if (conn != null)
        		try { conn.close(); } catch (SQLException e) {}
        }
        
        ๐Ÿงพ๏ธ Jdbc Template ์ฝ”๋“œ
        List<Member> results = jdbcTemplate.query(
        	"select * from MEMBER where EMAIL = ?",
        	new RowMapper<Member>(){
        		@Override
        		public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
        			Member member = new Member(rs.getString("EMAIL"),
        				rs.getString("PASSWORD"),
        				rs.getString("NAME"),
        				rs.getTimestamp("REGDATE"));
        			member.setId(rs.getLong("ID"));
        			return member;
        		}
        	},
        	email);
        return results.isEmpty() ? null : results.get(0);
        

        ๊ธฐ์กด์˜ JDBC ์ฝ”๋“œ์— ๋น„ํ•ด ํ›จ์”ฌ ์ค„์–ด๋“ ๋‹ค๋Š” ์ ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

        JdbcTemplate๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์งง์€ ์ฝ”๋“œ๋กœ ์†์‰ฝ๊ฒŒ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

        JdbcTemplate ์ƒ์„ฑ

        jdbc.core.JdbcTemplate ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  dataSource ๊ฐ์ฒด๋ฅผ ํ• ๋‹นํ•ด ์ค€ ๋’ค, ํ•ด๋‹น DAO๋ฅผ ๋นˆ ๊ฐ์ฒด๋กœ ๋“ฑ๋กํ•ด์ฃผ์ž.

        ๐Ÿงพ๏ธ MemberDao ์ „๋ฌธ
        package spring;
        import java.sql.Connection;
        import java.sql.PreparedStatement;
        import java.sql.ResultSet;
        import java.sql.SQLException;
        import java.util.List; 
        import java.sql.Timestamp;
        
        import javax.sql.DataSource;
        
        import org.springframework.jdbc.core.JdbcTemplate; // JdbcTemplate ์ž„ํฌํŠธ
        import org.springframework.jdbc.core.PreparedStatementCreator;
        import org.springframework.jdbc.core.RowMapper;
        import org.springframework.jdbc.support.GeneratedKeyHolder;
        import org.springframework.jdbc.support.KeyHolder;
        
        public class MemberDao {
            private JdbcTemplate jdbcTemplate;
        
            public MemberDao(DataSource dataSource) { // ์ƒ์„ฑ์ž๋กœ JdbcTemplate์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
                this.jdbcTemplate = new JdbcTemplate(dataSource); // ๊ตฌ์„ฑ ํด๋ž˜์Šค ์ถ”๊ฐ€ ์‹œ datasource๋ฅผ ๊ฑด๋„ค์ค˜์•ผ ํ•จ
            }
        
            public Member selectByEmail(String email) {
                List<Member> results = jdbcTemplate.query( // query ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ์กฐํšŒ
                        "select * from MEMBER where EMAIL = ?",
                        new RowMapper<Member>() {
                            @Override
                            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
                                Member member = new Member(
                                        rs.getString("EMAIL"),
                                        rs.getString("PASSWORD"),
                                        rs.getString("NAME"),
                                        rs.getTimestamp("REGDATE").toLocalDateTime());
                                member.setId(rs.getLong("ID"));
                                return member;
                            }
                        }, email);
        
                return results.isEmpty() ? null : results.get(0);
            }
        
        	public void insert(Member member) {
                KeyHolder keyHolder = new GeneratedKeyHolder();
                jdbcTemplate.update(new PreparedStatementCreator() { // update ๋ฉ”์„œ๋“œ๋กœ insert๋„ ์ง„ํ–‰
                    @Override
                    public PreparedStatement createPreparedStatement(Connection con)
                            throws SQLException {
                        // ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌ๋ฐ›์€ Connection์„ ์ด์šฉํ•ด์„œ PreparedStatement ์ƒ์„ฑ
                        PreparedStatement pstmt = con.prepareStatement(
                                "insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
                                "values (?, ?, ?, ?)",
                                new String[] { "ID" });
        
        				// ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’ ์„ค์ •
                        pstmt.setString(1, member.getEmail());
                        pstmt.setString(2, member.getPassword());
                        pstmt.setString(3, member.getName());
                        pstmt.setTimestamp(4,
                                Timestamp.valueOf(member.getRegisterDateTime()));
        
        				// ์ƒ์„ฑํ•œ PreparedStatement ๊ฐ์ฒด ๋ฆฌํ„ด
                        return pstmt;
                    }
                }, keyHolder);
        
        		Number keyValue = keyHolder.getKey();
                member.setId(keyValue.longValue());
            }
        
        	public void update(Member member) {
                jdbcTemplate.update(
                        "update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
                        member.getName(), member.getPassword(), member.getEmail());
            }
        
            public List<Member> selectAll() {
                List<Member> results = jdbcTemplate.query("select * from MEMBER",
                        (ResultSet rs, int rowNum) -> { // ๋žŒ๋‹ค์‹์„ ์ด์šฉํ•œ ๋”์šฑ ์งง์€ ๊ตฌํ˜„, ์•ž์„  ์กฐํšŒ ์ฟผ๋ฆฌ์— ์‚ฌ์šฉํ•œ RowMapper์™€ ๊ฐ™์€ ๋‚ด์šฉ์ด๋ฏ€๋กœ ์žฌ์‚ฌ์šฉ์œผ๋กœ ์ƒ๋žต๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. 
                            Member member = new Member(
                                    rs.getString("EMAIL"),
                                    rs.getString("PASSWORD"),
                                    rs.getString("NAME"),
                                    rs.getTimestamp("REGDATE").toLocalDateTime());
                            member.setId(rs.getLong("ID"));
                            return member;
                        });
                return results;
            }
        
            public int count() {
                Integer count = jdbcTemplate.queryForObject( // queryforObject๋ฅผ ์ด์šฉํ•œ ์กฐํšŒ, ์ฃผ๋กœ ๊ฒฐ๊ณผ ๊ฐ’์ด ํ•˜๋‚˜์ผ ๊ฒฝ์šฐ์— ์‚ฌ์šฉ
                        "select count(*) from MEMBER", Integer.class);
                return count;
            }
        }
        

        DAO๋ฅผ ๋นˆ ๊ฐ์ฒด๋กœ ๋“ฑ๋กํ•˜๊ธฐ

        @Configuration
        @EnableTransactionManagement
        public class AppCtx {
            @Bean
            public MemberDao memberDao() {
                return new MemberDao(dataSource());
            }
        }
        
        โž• DAO vs@Repository

        ๋งŒ์•ฝ, ์Šคํ”„๋ง MVC๋ฅผ ์ด์šฉํ•œ๋‹ค๋ฉด, DAO ๋Œ€์‹  @Repository ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฑฐ๋‚˜ ์„ž์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        DAO(Data Access Object)

        • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง(service?)๊ณผ ํผ์‹œ์Šคํ„ด์Šค ๋กœ์ง(DB ์ ‘๊ทผ ๋กœ์ง)์„ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด
        • ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋จผ์ € ์ถ”์ƒํ™”ํ•˜๊ณ , DAOImpl ๊ฐ์ฒด๋กœ ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ์‹์ด ์žˆ์ง€๋งŒ ์œ„ ์˜ˆ์‹œ์ฒ˜๋Ÿผ ๊ฐ„๋‹จํ•˜๋‹ค๋ฉด ๊ณง๋ฐ”๋กœ ๊ตฌํ˜„ํ•ด๋„ ๋œ๋‹ค.
        • DB ํ…Œ์ด๋ธ”, ์ฟผ๋ฆฌ์™€ ๋ฐ€์ ‘ํžˆ ์—ฐ๊ด€ ๋จ

        Repository ํŒจํ„ด

        • ๋งŒ์•ฝ, ๋„๋ฉ”์ธ์ด ๋ณต์žกํ•ด์ง„๋‹ค๋ฉด, ์—ฌ๋Ÿฌ DAO๋‚˜ ๋‹ค๋ฅธ API ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
        • ์ด๋•Œ Repository ๊ฐ์ฒด๋ฅผ ์ƒ์œ„๋กœ ์ถ”๊ฐ€ํ•ด ์„œ๋กœ ๋‹ค๋ฅธ ์—ฌ๋Ÿฌ DAO๋‚˜ ๋‹ค๋ฅธ API๋กœ DB์™€ ๊ฐ„์ ‘ ํ†ต์‹ ํ•œ๋‹ค.
        • ์ฆ‰ DAO๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์œ„ ๊ณ„์ธต ๊ฐ์ฒด์ด๋‹ค.
        • DB๋ณด๋‹ค๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋”์šฑ ๊ฐ€๊นŒ์šด ์ผ์„ ํ•œ๋‹ค.
        • ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Repository ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋จผ์ € ์ถ”์ƒํ™”ํ•˜๊ณ  RepositoryImpl๋กœ ๊ตฌํ˜„ํ•ด๋„ ๋œ๋‹ค.

        ์ฆ‰, ๋ณต์žกํ•œ ๋„๋ฉ”์ธ ๊ตฌ์กฐ๋ผ๋ฉด Repository ํŒจํ„ด์„ ์“ฐ๋ฉด ์ข‹๋‹ค.

        @Repository๋ฅผ ์ด์šฉํ•˜๋ฉด @Autowwired ๊ฐ™์€ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด ์ž๋™์œผ๋กœ jdbc๋‚˜ datasource ๋นˆ ๊ฐ์ฒด๋ฅผ ํ• ๋‹น๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

        JdbcTemplate ์กฐํšŒ ์ฟผ๋ฆฌ ์ƒ์„ฑ ์‹คํ–‰

        RowMapper ์ธํ„ฐํŽ˜์ด์Šค

        ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋กœ, ์ด์šฉ ์‹œ mapRow() ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

        ๐Ÿงพ๏ธ RowMapper ์ธํ„ฐํŽ˜์ด์Šค
        package org.springframework.jdbc.core.RowMapper;
        
        public interface RowMapper<T> {
        	T mapRow(ResultSet rs, int rowNum) throws SQLException;
        }
        

        SQL ์‹คํ–‰ ๊ฒฐ๊ณผ ResultSet์—์„œ ํ•œ ํ–‰ ์”ฉ, ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์™€ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. (์ฆ‰, ๋‚˜์˜ค๋Š” ๊ฒฐ๊ณผ๊ฐ€ ๋ฐฐ์—ด ํ˜•ํƒœ์—ฌ์•ผ ํ•œ๋‹ค.)

        ๐Ÿงพ๏ธ select sql์„ ์œ„ํ•œ RowMapper ๊ตฌํ˜„ ์˜ˆ์‹œ
        List<Member> results = jdbcTemplate.query( // query ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•œ ์กฐํšŒ 
        	"select * from MEMBER where EMAIL = ?", 
        	new RowMapper<Member>() { //์ž„์˜ ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•œ ์ฆ‰์„ ์˜ค๋ฒ„๋ผ์ด๋”ฉ
        		@Override 
        		public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
        			Member member = new Member( 
        				rs.getString("EMAIL"), 
        				rs.getString("PASSWORD"),
        				rs.getString("NAME"), 
        				rs.getTimestamp("REGDATE").toLocalDateTime());
        			member.setId(rs.getLong("ID"));
        			return member;
        		}
        	}, email);
        

        ๊ตณ์ด RowMapper์ธํ„ฐํŽ˜์ด์Šค์˜ mapRow ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ  ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ์ผ์น˜ํ•˜๊ฒŒ ๋žŒ๋‹ค์‹์œผ๋กœ ๋”์šฑ ์งง๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

        List<Member> results = jdbcTemplate.query(
        	"select * from MEMBER where EMAIL = ?", 
        	(ResultSet rs, int rowNum) -> {
        		Member member = new Member( 
        			rs.getString("EMAIL"), 
        			rs.getString("PASSWORD"),
        			rs.getString("NAME"), 
        			rs.getTimestamp("REGDATE").toLocalDateTime());
        		member.setId(rs.getLong("ID"));
        		return member;
        	}, email);
        
        ๐Ÿงพ๏ธ RowMapper ํ•จ์ˆ˜

        ๋งŒ์•ฝ ๊ฐ™์€ mapRow ๋กœ์ง์„ ๊ฐ€์ง„๋‹ค๋ฉด, ๊ตฌํ˜„ํ•œ RowMapper๋ฅผ ์•„๋ž˜ ๊ฐ™์ด ํ•จ์ˆ˜ํ™”ํ•œ ๋’ค ์žฌํ™œ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

        private Member mapRowToMember(ResultSet rs, int rowNum) {
        	Member member = new Member( 
        		rs.getString("EMAIL"), 
        		rs.getString("PASSWORD"),
        		rs.getString("NAME"), 
        		rs.getTimestamp("REGDATE").toLocalDateTime());
        	member.setId(rs.getLong("ID"));
        	return member;
        }
        

        query() ๋ฉ”์„œ๋“œ

        query() ๋ฉ”์„œ๋“œ๋Š” sql ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌ๋ฐ›์€ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์•ž์„œ ๋ฐฐ์šด RowMapper๋ฅผ ์ด์šฉํ•ด ResultSet์˜ ๊ฒฐ๊ณผ๋ฅผ ์ž๋ฐ” ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.

        โœ query ๋ฉ”์„œ๋“œ์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜
        • List<T> query(String sql, RowMapper<T> rowMapper)
        • List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)
        • List<T> query(String sql, RowMapper<T> rowMapper, Object... args)
        • List<T> query(PreparedStatementCreator psc, RowMapper<T> rowMapper) : PreparedStatementCreator๋Š” ์•„๋ž˜ ์ฐธ์กฐ

        ๋’ค์˜ ์ธ์ž args์˜ ๊ฐฏ์ˆ˜๋Š” String sql์˜ ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ(?)์˜ ๊ฐฏ์ˆ˜์— ๋‹ฌ๋ ค์žˆ์œผ๋ฉฐ, ์ˆœ์„œ๋Œ€๋กœ args์˜ ๊ฐ’๊ณผ sql ๋‚ด๋ถ€์˜ ? ์œ„์น˜์— ํฌ๋งทํŒ…๋œ๋‹ค.

        ๐Ÿงพ๏ธ query ๋ฉ”์„œ๋“œ์™€ ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ด์šฉ ์˜ˆ์‹œ
        String email = "asdf@asdf.com";
        String name = "asdf";
        List<Member> results = jdbcTemplate.query(
        	"select * from MEMBER where EMAIL = ? and NAME = ?",
        	new MemberRowMapper(), // RowMapper ์žฌํ™œ์šฉ
        	email, name);
        // s0ql ๊ฒฐ๊ณผ๋Š” select * from MEMBER where EMAIL = "asdf@asdf.com" and NAME = "asdf"
        

        ๋งŒ์•ฝ ๊ฒฐ๊ณผ๊ฐ€ ์—†๋‹ค๋ฉด ๊ธธ์ด๊ฐ€ 0์ธ List๋ฅผ ๋ฆฌํ„ดํ•˜๋ฉฐ, ๊ฒฐ๊ณผ๊ฐ€ ์˜ค์ง ํ•˜๋‚˜๋ผ๋ฉด ๊ธธ์ด๊ฐ€ 1์ธ List๋ฅผ ๋ฆฌํ„ดํ•œ๋‹ค.

        ๋”ฐ๋ผ์„œ ๋งŒ์•ฝ, ๋‹จ ํ•˜๋‚˜์˜ ๊ฒฐ๊ณผ๋งŒ ๊ธฐ๋Œ€ํ•˜๋Š” ์ฟผ๋ฆฌ์˜ ๊ฒฝ์šฐ,
        return results.isEmpty() ? null : results.get(0); ์ฒ˜๋Ÿผ ๋ฆฌ์ŠคํŠธ ๊ธธ์ด๊ฐ€ 0์ด๋ฉด ๊ฒฐ๊ณผ ์—†์Œ, ๋˜๋Š” ์ฒซ๋ฒˆ์งธ ํ•ญ๋ชฉ์„ ๊ฐ€์ ธ์™€์•ผ ํ•˜๊ฑฐ๋‚˜, ๋‹ค์Œ์— ์†Œ๊ฐœ๋  queryForObject() ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ๋œ๋‹ค.

        queryForObject() ๋ฉ”์„œ๋“œ

        ๐Ÿงพ๏ธ queryForObject() ๋ฉ”์„œ๋“œ ์˜ˆ์‹œ

        query() ๋ฉ”์„œ๋“œ์™€ ๊ฐ™์ด ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        double avg = queryForObject(
        	"select avg(height) from FURNITURE where TYPE=? and STATUS=?",
        	Double.class, // Row mapper ๋Œ€์‹  ๊ฒฐ๊ณผ๊ฐ’์˜ ํƒ€์ž…๋งŒ ์ง€์ •ํ•ด์ฃผ๋ฉด OK
        	100, "S");
        

        ๋งŒ์•ฝ ์›ํ•˜๋Š” ์ฟผ๋ฆฌ์˜ ๊ฒฐ๊ณผ๊ฐ€ ํ•˜๋‚˜๋ฟ์ผ ๊ฒฝ์šฐ -> ๊ฒฐ๊ณผ ๊ฐ’์˜ ํƒ€์ž…์„ ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ , ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ queryForObject() ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”์ฒœ

        ๋งŒ์•ฝ, ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ€ 2๊ฐœ ์ด์ƒ์ด๊ฑฐ๋‚˜ 0๊ฐœ์ด๋ฉด ์˜ค๋ฅ˜๋ฅผ ์ผ์œผํ‚จ๋‹ค.

        ๐Ÿงพ๏ธ queroyForObject()์˜ ๋ฉ”์„œ๋“œ
        • T queryForObject(String sql, Class<T> requiredType)
        • T queryForObject(String sql, Class<T> requiredType, Object... args)
        • T queryForObject(String sql, RowMapper<T> rowMapper)
        • T queryForObject(String sql, RowMapper<T> rowMapper, Object... args)

        query() ๋ฉ”์„œ๋“œ์ฒ˜๋Ÿผ rowMapper๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋•Œ๋Š” ๋ฆฌ์ŠคํŠธ๊ฐ€ ์•„๋‹Œ rowMapper์˜ ๋ฆฌํ„ด ํƒ€์ž…์œผ๋กœ ๋˜๋Œ๋ ค ์ค€๋‹ค.

        JdbcTemplate ๋ณ€๊ฒฝ ์ฟผ๋ฆฌ ์ƒ์„ฑ ์‹คํ–‰

        SQL์˜ INSERT, UPDATE, DELETE ์ฟผ๋ฆฌ ๋“ฑ์€ update() ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•œ๋‹ค.

        ๐Ÿงพ๏ธ update() ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ
        • int update(String sql)
        • int update(String sql, Object... args)
        • int update(PreparedStatementCreator psc)

        ์—ญ์‹œ๋‚˜ ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ(?)๋ฅผ ์ง€์›ํ•œ๋‹ค.

        • int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)

        ์ถ”๊ฐ€๋กœ ์•„๋ž˜์— ์†Œ๊ฐœ๋  PreparedStatement์™€ KeyHolder๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        ๐Ÿงพ๏ธ update() ๋ฉ”์„œ๋“œ ์˜ˆ์‹œ
        jdbcTemplate.update(
        	"udpate MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
        	member.getName(), member.getPassword(), member.getEmail());
        

        PreparedStatement ์ธํ„ฐํŽ˜์ด์Šค ์ด์šฉ

        ์• ๋งค๋ชจํ˜ธํ•œ ? ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ ๋Œ€์‹ , PreparedStatement๋ฅผ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

        ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ PrepareStatementCreator ์ธํ„ฐํŽ˜์ด์Šค์˜ createPreparedSatement๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๋‹ค์Œ์ด ์˜ˆ์‹œ์ด๋‹ค.

        ๐Ÿงพ๏ธ ์ž„์˜ ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•œ PreparedStatement ์ด์šฉ ์˜ˆ์‹œ
        import java.sql.PreparedStatement;
        
        jdbcTemplate.update(new PreparedStatementCreator() {
        	@Override
        	public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
        		// ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌ๋ฐ›์€ Connection์„ ์ด์šฉํ•ด์„œ PreparedStatement ์ƒ์„ฑ
        		PreparedStatement pstmt = con.prepareStatement(
        			"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) values (?, ?, ?, ?)");
        		// ์ธ๋ฑ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๊ฐ’ ์„ค์ •
        		pstmt.setString(1, member.getEmail());
        		pstmt.setString(2, member.getPassword());
        		pstmt.setString(3, member.getName());
        		pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
        		// ์ƒ์„ฑํ•œ PreparedStatement ๊ฐ์ฒด ๋ฆฌํ„ด
        		return pstmt;
        	}
        });
        

        ์ด ๋ฐฉ์‹์€ ์•ž์„œ ๋ฐฐ์› ๋˜ query() ๋ฉ”์„œ๋“œ์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

        KeyHolder๋ฅผ ์ด์šฉํ•œ ์ž๋™ ์ƒ์„ฑ ํ‚ค๊ฐ’ ๊ตฌํ•˜๊ธฐ

        ๋งŽ์€ ํ…Œ์ด๋ธ”์—์„œ ํ–‰ ์ถ”๊ฐ€ ์‹œ, ์ฃผ์š” ํ‚ค์ธ ID๋ฅผ ์ž๋™์œผ๋กœ ์ฆ๊ฐ€์‹œํ‚ค๋Š” ๋ฐฉ์‹์œผ๋กœ ์ด์šฉํ•˜๋ฏ€๋กœ, ์ฝ”๋“œ ์ƒ์—์„œ ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ํ–‰์— ๋Œ€ํ•œ ID๋ฅผ ์•Œ ์ˆ˜ ์—†๋‹ค.

        • update() ๋ฉ”์„œ๋“œ์˜ ๊ฒฐ๊ณผ ๊ฐ’์€ ๋ณ€๊ฒฝ๋œ ํ–‰์˜ ๊ฐฏ์ˆ˜๋งŒ ๋ฆฌํ„ดํ•œ๋‹ค.

        ๋”ฐ๋ผ์„œ ์ฟผ๋ฆฌ ํ›„ ์ƒ์„ฑ๋œ ๊ฐ’์— ๋Œ€ํ•œ ํ‚ค ๊ฐ’์„ ๊ตฌํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ keyHolder๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        ๐Ÿงพ๏ธ Keyholder ๊ตฌํ˜„ ์˜ˆ์‹œ
        import org.springframework.jdbc.support.GeneratedKeyHolder;
        import org.springframework.jdbc.support.KeyHolder;
        
        public void insert(Member member) {
        	KeyHolder keyHolder = new GeneratedKeyHolder(); //์ž๋™ ์ƒ์„ฑ๋œ ํ‚ค ๊ฐ’ ๊ตฌํ•ด์ฃผ๋Š” KeyHolder ๊ตฌํ˜„ ํด๋ž˜์Šค
            jdbcTemplate.update(new PreparedStatementCreator() {
                @Override
                public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                    PreparedStatement pstmt = con.prepareStatement(
                            "insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
                            "values (?, ?, ?, ?)",
                            new String[] { "ID" }); // preparedStatement์˜ ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ๊ตฌํ•  ํ‚ค ๊ฐ’์˜ ์ด๋ฆ„ ๋ช…์‹œ 
                            
                    pstmt.setString(1, member.getEmail());
                    pstmt.setString(2, member.getPassword());
                    pstmt.setString(3, member.getName());
                    pstmt.setTimestamp(4,
                            Timestamp.valueOf(member.getRegisterDateTime()));
        
        			// ์ƒ์„ฑํ•œ PreparedStatement ๊ฐ์ฒด ๋ฆฌํ„ด
                    return pstmt;
                }
            }, keyHolder);
        
        	Number keyValue = keyHolder.getKey(); // ๊ตฌํ•œ ํ‚ค๊ฐ’์„ ์•Œ๋ ค์ฃผ๋Š” ๋ฉ”์„œ๋“œ
            member.setId(keyValue.longValue()); // ์ ์ ˆํ•œ ํƒ€์ž…์œผ๋กœ ์บ์ŠคํŒ…
        }
        

        SimpleJdbcInsert ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•œ ๋” ์‰ฌ์šด ์‚ฝ์ž…

        JdbcTemplate ๊ฐ์ฒด์˜ wrapper ๊ฐ์ฒด์ธ SimpleJdbcInsert ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•˜๋ฉด ๋” ๊ฐ„๋‹จํ•จ

        ๐Ÿงพ๏ธ SimpleJdbcInsert ์‚ฌ์šฉ๋ก€

        jdbcTemplate ๊ฐ์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  SimpleJdbcInsert ๊ฐ์ฒด๋กœ ๊ฐ์‹ธ ์‚ฌ์šฉํ•œ๋‹ค.

        package tacos.data;
        
        import java.util.Date;
        import java.util.HashMap;
        import java.util.List;
        import java.util.Map;
        
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.jdbc.core.JdbcTemplate;
        import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
        import org.springframework.stereotype.Repository;
        import com.fasterxml.jackson.databind.ObjectMapper;
        
        import tacos.Taco;
        import tacos.Order;
        
        @Repository
        public class JdbcOrderRepository implements OrderRepository {
            private SimpleJdbcInsert orderInserter;
            private SimpleJdbcInsert orderTacoInserter;
            private ObjectMapper objectMapper;
        
            @Autowired
            public JdbcOrderRepository(JdbcTemplate jdbc) {
                this.orderInserter = new SimpleJdbcInsert(jdbc)
                        .withTableName("Taco_Order") // ๋Œ€์ƒ ํ…Œ์ด๋ธ” ๋ช…
                        .usingGeneratedKeyColumns("id"); // DB ์ž๋™์ƒ์„ฑ ํ‚ค ์‚ฌ์šฉ
                        
                this.orderTacoInserter = new SimpleJdbcInsert(jdbc)
                        .withTableName("Taco_Order_Tacos");
                        
                this.objectMapper = new ObjectMapper();
                // ๋‚˜์ค‘์— ์ปค๋งจ๋“œ ๊ฐ์ฒด๋ฅผ Map ๊ฐ์ฒด๋กœ ๋ฐ”๊พธ๋Š”๋ฐ ์‚ฌ์šฉ
            }
        
            @Override
            public Order save(Order order) {
                order.setPlacedAt(new Date());
                long orderId = saveOrderDetails(order);
                order.setId(orderId);
                List<Taco> tacos = order.getTacos();
                for (Taco taco : tacos) {
                    saveTacoToOrder(taco, orderId);
                }
                return order;
            }
        
            private long saveOrderDetails(Order order) {
                @SuppressWarnings("unchecked")
                Map<String, Object> values = 
                objectMapper.convertValue(order, Map.class); // Map ๊ฐ์ฒด๋กœ ๋ฐ”๊พธ๊ธฐ 
                values.put("placedAt", order.getPlacedAt());
                long orderId =
                        orderInserter
                        .executeAndReturnKey(values) // ์‹คํ–‰ ๋’ค id ๋Œ๋ ค๋ฐ›๊ธฐ
                        .longValue(); // long์œผ๋กœ ๋ณ€ํ˜•
                return orderId;
            }
        
            private void saveTacoToOrder(Taco taco, long orderId) {
                Map<String, Object> values = new HashMap<>(); // execute๋Š” Map<String, Object> ํƒ€์ž…์„ ์ธ์ž๋กœ ๋ฐ›๋Š”๋‹ค.
                values.put("tacoOrder", orderId); 
                values.put("taco", taco.getId()); // column ์„ค์ •
                orderTacoInserter.execute(values); // ์‚ฝ์ž… ์‹คํ–‰
            }
        }
        

        ๋‹จ์ˆœํžˆ ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ๋˜๋ฏ€๋กœ ๋”์šฑ ๋น ๋ฅด๋‹ค.

        ์Šคํ”„๋ง ์ต์…‰์…˜ ๋ณ€ํ™˜ ์ฒ˜๋ฆฌ

        DB๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์€ ๋ฏผ๊ฐํ•˜๊ณ  ์ค‘์š”ํ•˜๋ฉฐ ๋ณต์žกํ•œ ์‚ฌ์•ˆ์ด๋ฏ€๋กœ ์ƒ๋‹นํžˆ ๋งŽ์€ ์˜ค๋ฅ˜๋ฅผ ๋ณด๊ฒŒ๋  ๊ฒƒ์ด๋‹ค.

        • ์ธ์ฆ/์ธ๊ฐ€ ๊ด€๋ จ ์˜ค๋ฅ˜ (ex) CannotGetJdbcConnectionException)
        • SQL ๊ตฌ๋ฌธ ์—๋Ÿฌ (ex) BadSqlGrammarException)
        • db ์„ค์ • ๊ด€๋ จ ์—๋Ÿฌ (ex) CannotGetJdbcConnectionException)
          ์ด๋ฅผ ์ฐจ๋ถ„ํžˆ ์ฝ๊ณ  ๋ฌธ์ œ ๋ฐœ์ƒ ์›์ธ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.

        ์Šคํ”„๋ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ต์…‰์…˜์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

        1. JDBC, Hibernate, JPA ๋“ฑ ์—ฌ๋Ÿฌ ์—ฐ๋™ ๊ธฐ์ˆ ๋“ค์˜ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ์ต์…‰์…˜ ๋ฐœ์ƒ
        2. -> ์Šคํ”„๋ง์€ ๊ฐ๊ฐ์˜ ์ต์…‰์…˜์„ ๋™์ผํ•œ DataAccessException์œผ๋กœ ๋ณ€ํ™˜
          • ์ด๋ฅผ ํ†ตํ•ด ์—ฐ๋™ ๊ธฐ์ˆ ์— ๊ด€๊ณ„์—†์ด ๋™์ผํ•˜๊ฒŒ ์ต์…‰์…˜ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
        3. -> DataAccessException๋ฅผ ์ƒ์†๋ฐ›์€ ๊ตฌ์ฒด์ ์ธ ํ•˜์œ„ ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ (ex) DuplicateKeyExcetion, QueryTimeoutException)
          • ์ด๋ฅผ ํ†ตํ•ด ์ด๋ฆ„๋งŒ์œผ๋กœ ๋ฌธ์ œ ์›์ธ ์œ ์ถ” ๊ฐ€๋Šฅ

        DataAccessException์€ RuntimeException์„ ์ƒ์†๋ฐ›๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ์Šคํ”„๋ง์—์„œ ์ž๋™์œผ๋กœ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค€๋‹ค.

        ๋”ฐ๋ผ์„œ ๊ธฐ์กด์˜ JDBC์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ try~catch๋ฅผ ์ด์šฉํ•œ ์ต์…‰์…˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•ด์•ผ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์Šคํ”„๋ง์—์„œ๋Š” ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ต์…‰์…˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

        ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ : @Transactional

        JDBC์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜์„ ํ•˜๋‚˜์˜ ์ž‘์—…๋‹จ์œ„๋กœ ์ปค๋ฐ‹ํ•˜๊ฑฐ๋‚˜ ๋กค๋ฐฑํ•˜๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ง์ ‘ commit๊ณผ rollback ํ•˜์—ฌ ์ง„ํ–‰ํ–ˆ๋‹ค.

        • ์ฝ”๋“œ ๋ˆ„๋ฝ, ๋ฐ˜๋ณต, ๊ด€๋ฆฌ ํž˜๋“ฌ ๋“ฑ์˜ ๋ฌธ์ œ๊ฐ€ ์žˆ์Œ
        ๐Ÿงพ๏ธ ๊ธฐ์กด์˜ JDBC ํŠธ๋žœ์žญ์…˜ ๊ตฌํ˜„
        Connection conn = null;
        try {
        	conn = DriverManager.getConnection(jdbcUrl, user, pw);
        	conn.setAutoCommit(false); // ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ์‹œ์ž‘
        	//... ๊ธฐํƒ€ ์ฟผ๋ฆฌ
        	conn.commit(); // ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ์ข…๋ฃŒ ๋ฐ ์ปค๋ฐ‹
        } catch(SQLException ex) {
        	if (conn != null)
        		// ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ์ข…๋ฃŒ : ๋กค๋ฐฑ
        		try { conn.rollabck(); } catch (SQLException e) {}
        } finally {
        	if (conn != null)
        		try { conn.close(); } catch (SQLException e) {}
        }
        

        ์Šคํ”„๋ง์˜ @Transactional ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•˜๋ฉด ์†์‰ฝ๊ฒŒ ์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

        ๐Ÿงพ๏ธ ์Šคํ”„๋ง์˜ @Transactional์„ ์ด์šฉํ•œ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ
        import org.springframework.transaction.annotation.Transactional;
        
        @Transactional
        public void changePassword(String email, String oldPwd, String newPwd) {
        	Member member = memberDao.selectByEmail(eamil);
        	if (member == null)
        		throw new MemberNotFoundException();
        	member.changePassword(oldPwd, newPwd);
        	memberDao.update(member);
        }
        

        @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ๋Š” ๋™์ผํ•œ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ๋‚ด์— ์‹คํ–‰๋˜๋ฉฐ, ์‹คํŒจ์‹œ ์ „๋ถ€ ๋กค๋ฐฑ ๋œ๋‹ค.
        @Transactional ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์˜ ๋‹ค๋ฅธ ์ผ๋ฐ˜ ๋ฉ”์„œ๋“œ ๊ณผ์ • ๋‚ด์˜ ์˜ค๋ฅ˜์—๋„ ๋กค๋ฐฑ๋˜๋ฉฐ, ์ผ๋ฐ˜ ๋ฉ”์„œ๋“œ ๋‚ด์˜ DB ๋ณ€ํ™”๋„ ๋กค๋ฐฑ๋œ๋‹ค.

        @Transactional์™€ ์Šคํ”„๋ง ์„ค์ •๋ฒ•

        @Transactional ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‘ ๊ฐœ์˜ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.

        1. ๊ตฌ์„ฑ ํด๋ž˜์Šค์— ํ”Œ๋žซํผ ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €(PlatformTransactionManager) ๋นˆ ์„ค์ •
        2. @Transactional ์–ด๋…ธํ…Œ์ด์…˜ ํ™œ์„ฑํ™” ์„ค์ •

        ํ”Œ๋žซํผ ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €(PlatformTransactionManager) ๋นˆ ์„ค์ •
        ์Šคํ”„๋ง ๊ตฌ์„ฑ ํด๋ž˜์Šค์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

        ๐Ÿงพ๏ธ ์Šคํ”„๋ง ๊ตฌ์„ฑ ํด๋ž˜์Šค ์˜ˆ์‹œ(AppCtx)
        • @EnableTransactionManagement: @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ๋ฅผ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„์—์„œ ์‹คํ–‰, ๋นˆ์ด ์ง์ ‘ ํŠธ๋žœ์žญ์…˜ ์ ์šฉ
          • ํ”„๋ก์‹œ ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ AOP์—์„œ ๋ฐฐ์› ๋˜ order, proxyTargetClass ์†์„ฑ์„ ์„ค์ • ๊ฐ€๋Šฅํ•˜๋‹ค.
        • PlatformTransactionManager: ๊ตฌํ˜„ ๊ธฐ์ˆ ์— ๊ด€๊ณ„์—†์ด ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค, ์ด์šฉํ•  datasource๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ.
        import org.springframework.jdbc.datasource.DataSourceTransactionManager;
        import org.springframework.transaction.PlatformTransactionManager;
        import org.springframework.transaction.annotation.EnableTransactionManagement;
        
        @Configuration
        @EnableTransactionManagement // ํ”Œ๋žซํผ ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € ์‚ฌ์šฉํ•จ์„ ์˜๋ฏธ
        public class AppCtx {
        
            @Bean
            public PlatformTransactionManager transactionManager() { // ํŠธ๋žœ์žญ์…˜๋งค๋‹ˆ์ € ๋“ฑ๋ก
                DataSourceTransactionManager tm = new DataSourceTransactionManager();
                tm.setDataSource(dataSource());
                return tm;
            }
            
            @Bean 
            public ChangePasswordService changePwdSvc() { // @Transactional์ด ์ ์šฉ๋œ ํŠธ๋žœ์žญ์…˜ ๋ฉ”์„œ๋“œ๊ฐ€ ์กด์žฌํ•˜๋Š” ์„œ๋น„์Šค ๊ฐ์ฒด ๋“ฑ๋ก
                ChangePasswordService pwdSvc = new ChangePasswordService();
                pwdSvc.setMemberDao(memberDao());
                return pwdSvc;
            }
        }
        

        @Transactional ์–ด๋…ธํ…Œ์ด์…˜ ํ™œ์„ฑํ™” ์„ค์ •

        ๐Ÿงพ๏ธ ChangePasswordService ๊ฐ์ฒด ์˜ˆ์‹œ
        package spring;
        import org.springframework.transaction.annotation.Transactional;
        
        public class ChangePasswordService {
            private MemberDao memberDao;
            
            @Transactional // ์ด์ œ changePassword ๋ฉ”์„œ๋“œ๋Š” ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ๋‚ด์—์„œ ์‹คํ–‰๋จ
            public void changePassword(String email, String oldPwd, String newPwd) {
                Member member = memberDao.selectByEmail(email);
                if (member == null)
                    throw new MemberNotFoundException();
                member.changePassword(oldPwd, newPwd);
                memberDao.update(member);
            }
        
            public void setMemberDao(MemberDao memberDao) {
                this.memberDao = memberDao;
            }
        }
        

        ์ด์ œ ์›ํ•  ๋•Œ, ์œ„์˜ ์„œ๋น„์Šค๋ฅผ ๋ถˆ๋Ÿฌ์™€ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        ํ”„๋ก์‹œ๋ฅผ ์ด์šฉํ•œ ์ปค๋ฐ‹, ๋กค๋ฐฑ ์ฒ˜๋ฆฌ

        ์Šคํ”„๋ง์˜ @Transactional ์–ด๋…ธํ…Œ์ด์…˜์€ ์•ž์„œ ๋ฐฐ์› ๋˜ AOP ๋ฅผ ์ด์šฉํ•ด ํŠธ๋žœ์žญ์…˜์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์ฆ‰ ํ”„๋ก์‹œ๋ฅผ ์ด์šฉํ•œ๋‹ค.

        1. ์Šคํ”„๋ง์ด ๊ตฌ์„ฑ ํด๋ž˜์Šค์—์„œ @EnableTransactionManagement ํƒœ๊ทธ๋ฅผ ๊ฐ์ง€
        2. @Transactional ์–ด๋…ธํ…Œ์ด์…˜์ด ์ ์šฉ๋œ ๋นˆ ๊ฐ์ฒด๋ฅผ ์ฐพ์Œ
        3. ์ ์ ˆํ•œ ํ”„๋ก์‹œ ๊ฐ์ฒด ์ƒ์„ฑ ๋’ค ๋ฉ”์„œ๋“œ ์ด์šฉ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜ ์ ์šฉ
        ๐Ÿงพ๏ธ ํ”„๋ก์‹œ ํŠธ๋žœ์žญ์…˜ ์˜ˆ์‹œ
        • ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต ์‹œ ํ”„๋ก์‹œ ์ปค๋ฐ‹ ๋™์ž‘

        • ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ ์‹œ ํ”„๋ก์‹œ ๋กค๋ฐฑ ๋™์ž‘

        ๋กค๋ฐฑ์€ RuntimeException์ด ์ผ์–ด๋‚ ๋•Œ๋งŒ ์ƒ์„ฑ๋˜๋ฏ€๋กœ SQLException ๊ฐ™์ด ๋‹ค๋ฅธ ํƒ€์ž…์˜ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

        ๐Ÿงพ๏ธ @Transactional์˜ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ ์˜ˆ์‹œ
        • ๋ฐ˜๋Œ€๋กœ noRollbackFor ์†์„ฑ์„ ์ง€์ •ํ•ด์ฃผ๋ฉด ํ•ด๋‹นํ•˜๋Š” ์ต์…‰์…˜์— ๋กค๋ฐฑํ•˜์ง€ ์•Š๋Š”๋‹ค.
        @Transactional(rollbackFor = {SQLException.class, IOException.class})
        public void someMethod(){
        	//...
        }
        

        @Transactional์˜ ์ฃผ์š” ์†์„ฑ๊ณผ ์ „ํŒŒ

        • value: ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌํ•  PlatformTransactionManager ๋นˆ์˜ ์ด๋ฆ„ ์ง€์ •, ์—†์œผ๋ฉด ํ•ด๋‹น ํƒ€์ž…์˜ ๋นˆ์„ ์ž๋™์œผ๋กœ ์ฐพ์Œ
        • isolation: ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ ๋ ˆ๋ฒจ, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE ๋“ฑ์ด ์กด์žฌ, ๊ธฐ๋ณธ๊ฐ’์€ Isolation.DEFAULT
        • timeout: ํŠธ๋žœ์žญ์…˜ ์ œํ•œ ์‹œ๊ฐ„ ์ง€์ •, ์ดˆ๋‹จ์œ„์ด๋ฉฐ ๊ธฐ๋ณธ๊ฐ’ -1์ธ ๊ฒฝ์šฐ DB์˜ ์„ค์ •์„ ๋”ฐ๋ฆ„
        • propagation: ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ํƒ€์ž… ์ง€์ •, ๊ธฐ๋ณธ๊ฐ’์€ Propagation.REQUIRED
          • ๊ธฐ๋ณธ ๊ฐ’์ด๋ฉด ๋ฉ”์„œ๋“œ ์ˆ˜ํ–‰ ์‹œ ์ด๋ฏธ ์ง„ํ–‰์ค‘์ธ ํŠธ๋žœ์žญ์…˜์ด ์กด์žฌํ•˜๋ฉด ํ•ด๋‹น ํŠธ๋žœ์žญ์…˜์— ๋ณ‘ํ•ฉ
        โž• ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ๋ž€?

        @Transactional์ด ์ ์šฉ๋œ ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์— ๋˜ ๋‹ค๋ฅธ @Transactional ๋ฉ”์„œ๋“œ๊ฐ€ ์กด์žฌํ•˜๋ฉด, ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ(transaction propagation) ์†์„ฑ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋™์ž‘์„ ํ•œ๋‹ค.

        ์˜ˆ๋ฅผ ๋“ค์–ด Propagation.MANDATORY๋Š” ์ด๋ฏธ ์ง„ํ–‰์ค‘์ธ ํŠธ๋žœ์žญ์…˜์ด ์žˆ๋‹ค๋ฉด ์—๋Ÿฌ๋ฅผ ์ผ์œผํ‚ค๋ฉฐ, Propagation.REQUIRES_NEW์€ ํ•ญ์ƒ ์ƒˆ๋กœ์šด ํŠธ๋ž˜์žญ์…˜์œผ๋กœ ๋ฎ์–ด์”Œ์šด๋‹ค.

        • ์ด์™ธ์—๋„ SUPPORTS, NOT_SUPPORTED, NEVER, NESTED ๋“ฑ์ด ์กด์žฌ

        ๋กœ๊น… ์ฒ˜๋ฆฌ

        ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ, ์˜ค๋ฅ˜, ๊ด€๋ จ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๊ธฐ ์œ„ํ•ด์„œ ์ผ์ผ์ด ํ”„๋ฆฐํŠธํ•˜๊ธฐ ๋ณด๋‹จ Log4j2๋‚˜ Logback ๋“ฑ์˜ ์™ธ๋ถ€ ํŒจํ‚ค์ง€๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

        • ์—„๋ฐ€ํžˆ ๋งํ•˜๋ฉด ์Šคํ”„๋ง ์ž์ฒด ๋กœ๊น… ๋ชจ๋“ˆ์ธ spring-jcl์ด ์œ„์˜ ๋กœ๊น… ๋ชจ๋“ˆ์„ ์ธ์‹ํ•˜์—ฌ ์‚ฌ์šฉํ•œ๋‹ค.

        ๋ฉ”์ด๋ธ์ด๋‚˜ ๊ทธ๋ž˜๋“ค์„ ์ด์šฉํ•ด Logback์„ ์˜์กด ๋ชจ๋“ˆ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ , ํด๋ž˜์Šค ํŒจ์Šค์— ์„ค์ •ํŒŒ์ผ์„ ์œ„์น˜์‹œํ‚ค๊ธฐ ์œ„ํ•ด src/main/resources ํด๋”๋ฅผ ๋งŒ๋“ค์–ด ์„ค์ • xml์„ ์ถ”๊ฐ€ํ•˜๊ณ , ๋ฉ”์ด๋ธ, ๊ทธ๋ž˜๋“ค๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ๋œ๋‹ค.

        ๐Ÿงพ๏ธ Logback ์„ค์ • xml ์˜ˆ์‹œ (main/resources/logback.xml)
        <?xml version="1.0" encoding="UTF-8"?>
        <configuration>
            <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
                <encoder>
                    <pattern>%d %5p %c{2} - %m%n</pattern>
                </encoder>
            </appender>
            <root level="INFO">
                <appender-ref ref="stdout" />
            </root>
            <logger name="org.springframework.jdbc" level="DEBUG" /> <!--๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ์ƒ์„ธ ๋ณด๊ธฐ ์„ค์ •-->
        </configuration>
        

        ์ดํ›„ ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ์˜ˆ์‹œ ๊ฐ™์€ ํ•ต์‹ฌ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

        ๐Ÿงพ๏ธ Logback ๋กœ๊ทธ ์˜ˆ์‹œ
        ๋‚ ์งœ ์‹œ๊ฐ„ DEBUG o.s.j.d DataSourceTransactionManager - Initiating transaction commit
        ๋‚ ์งœ ์‹œ๊ฐ„ DEBUG o.s.j.d DataSourceTransactionManager - Committing JDBC transaction on Connection[ProxyConnection...]
        ๋‚ ์งœ ์‹œ๊ฐ„ DEBUG o.s.j.d DataSourceTransactionManager - Initiating transaction rollback
        ๋‚ ์งœ ์‹œ๊ฐ„ DEBUG o.s.j.d DataSourceTransactionManager - Rolling back JDBC transaction on Connection[ProxyConnection...]