///////
Search

[PG]올바른 괄호, 토비의 스프링 3장, 곽철민

1.

알고리즘

프로그래머스 - 괄호 문제 풀기
열린 괄호와 닫힌 괄호가 반드시 짝지어서 문자로 닫히는 것을 구현해야 하는 문제이다.

Code

스택을 사용하지 않은 로직
스택을 활용한 코드

2. 토비의 스프링

2.1 deleteAll(), getCount() 추가 - 166pg Review

deleteAll()
public void deleteAll() { Connection c = null; PreparedStatement pstmt = null; try { c = connectionMaker.makeConnection(); //ConectionMaker 인터페이스에게 커넥션 생성 요청 위임 pstmt = c.prepareStatement("delete from users"); //쿼리문 생성 pstmt.executeUpdate(); //쿼리문 실행 } catch (SQLException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { //finally : error가 발생해도 실행되는 블록 -> 에러가 나도 자원은 close 해주어야 한다. if (pstmt != null) { try { pstmt.close(); //ps.close()에서도 SQL Exception이 발생할 수도 있다. } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } }
Java
복사
getCount()
public int getCount() { Connection c = null; PreparedStatement pstmt = null; ResultSet rs = null; try { c = connectionMaker.makeConnection(); //connectionMaker 인터페이스에게 커넥션 생성 위임 pstmt = c.prepareStatement("SELECT count(*) from users"); //쿼리문 생성 rs = pstmt.executeQuery(); //쿼리문 실행 rs.next(); //결과값 하나 읽어오기 return rs.getInt(1); } catch (SQLException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { //예외가 발생해도 한정된 자원을 풀에 할당하고 돌려서 쓰는 리소스들이 고갈되지 않도록 //자원들은 반환해주어야 한다. if (rs != null) { try { rs.close(); } catch (SQLException e) { } } if (pstmt != null) { try { pstmt.close(); } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } }
Java
복사

2.2 deleteAll(), getCount() 테스트 코드 추가 Review

deleteAll()과 getCount() 테스트 코드 추가
package org.example.dao; import org.example.domain.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.sql.SQLException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = UserDaoFactory.class) class UserDaoTest { @Autowired ApplicationContext ac; private UserDao userDao; private User user1; private User user2; private User user3; @BeforeEach //각 테스트가 실행되기 전에 작동 void setUp(){ userDao = ac.getBean("awsUserDao", UserDao.class); //이름으로 등록된 빈을 불러옴 //User Object 3개 생성 user1 = new User("50", "홍길동", "12345"); user2 = new User("51", "홍길자", "123456"); user3 = new User("52", "홍길수", "1234567"); } ' @Test @DisplayName("count Test") void count(){ userDao.deleteAll(); //deleteAll() 기능 검증 userDao.add(user1); assertEquals(1, userDao.getCount()); userDao.add(user2); assertEquals(2, userDao.getCount()); userDao.add(user3); assertEquals(3, userDao.getCount()); }
Java
복사

2.3 한 두줄 빼고 나머지 코드는 같은 경우

deleteAll() 메소드나 getCount() 메소드는 sql문을 생성하는 Connection.prepareStatement() 부분을 제외하고는 거의 모든 로직이 동일합니다.

2.4 인터페이스 도입

위에서 언급했듯이, deleteAll(), getCount(), add() 등 UserDao 클래스에 정의한 메소드들은 sql을 생성하는 부분 외에는 커넥션 생성, 예외 처리등 동일한 로직으로 구성되어 있습니다.
메소드에 따라 PreparedStatement 부분만 바뀌기 때문에 PreparedStatement를 리턴하는 StatementStrategy 인터페이스를 도입합니다.

2.5 전략 패턴

객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여,
객체의 행위를 동적으로 바꾸고 싶은 경우, 직접 행위를 수정하지 않고 전략을 바꿔주기만 하믕로써 행위를 유연하게 확장하는 방법을 말합니다.
한줄로 요약해서 말하자면, 인터페이스 즉, 역할을 만들어두고 상황에 맞게 구현 클래스를 갈아끼워줌으로써 동적으로 행위를 수정하는 것이 가능하도록 만든 패턴 입니다.
앞서 언급했듯이, UserDao 클래스의 메소드들은 PreparedStatement를 사용하는 부분이 각 메소드에 따라 달라지고 있습니다.
따라서, preparedStatement를 생성의 역할을 인터페이스에 정의해두고, 이를 각 메소드에 따라 동적으로 바꿔 사용할 수 있도록 구현할 수 있습니다.

2.6 DeleteAllStrategy 구현

package org.example.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class DeleteAllStrategy implements StatementStrategy{ @Override public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement("DELETE FROM users"); } }
Java
복사
구현한 DeleteAllStrategy 구현체 클래스 사용하기 in UserDao.java
package org.example.dao; import org.example.connection.ConnectionMaker; import org.example.domain.User; import org.springframework.dao.EmptyResultDataAccessException; import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { private ConnectionMaker connectionMaker; private StatementStrategy statementStrategy; public UserDao(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; } public void deleteAll() throws SQLException { StatementStrategy st = new DeleteAllStrategy(); //업캐스팅 jdbcConnectWithStatementStrategy(st); //JDBC 연결하고 sql문 실행을 담당하는 메소드 호출 } //공통 로직 분리,여러 메소드에서 중복된 코드를 하나의 메소드로 분리한 것, 아래에서 설명 public void jdbcConnectWithStatementStrategy(StatementStrategy stmt) throws SQLException{ Connection c = null; PreparedStatement ps = null; try{ c = connectionMaker.makeConnection(); ps = stmt.makePreparedStatement(c); ps.executeUpdate(); } catch(SQLException e){ throw e; } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { if (ps != null){ try{ ps.close(); }catch(SQLException e){} } if (c != null){ try{ c.close(); }catch(SQLException e){} } } } }
Java
복사
위의 그림에서 클라이언트의 전략 선택, 생성 부분은 deleteAll() 메소드의 아래 코드와 같습니다.
public void deleteAll() throws SQLException { StatementStrategy st = new DeleteAllStrategy(); //DeleteAllStrategy 전략 선택 }
Java
복사
앞서 구현했던 JDBC 로드 및 커넥션 생성을 담당하는 ConnectionMaker 또한, 위 그림에서의 전략(Strategy) 부분에 해당합니다.
위 표는 우리가 지금까지 강의를 통해 구현했었던 전략 패턴 구조를 가진 코드들 입니다.
우리는 하나의 역할에 대해 Parser, ConnectionMaker, StatementStrategy라는 인터페이스에 선언해주고, 이 인터페이스에 대한 여러 구현 클래스들을 작성했었습니다.
이렇게 작성된 구조는 모두 전략 패턴이 적용된 코드임을 알 수 있습니다.

2.7 AddStrategy 도입

public void add(User user) throws SQLException { StatementStrategy st = new AddStatementStrategy(user); //user 오브젝트를 sql 문에서 활용해주어야 함. jdbcConnectWithStatementStrategy(st); //공통 로직의 분리, 2.8에서 설명 }
Java
복사
add() 메소드는 User 오브젝트를 DB에 삽입해주는 역할을 담당하기 때문에, sql문 실행 시, 디비에 저장할 User 오브젝트를 외부에서 전달해주어야 합니다.
package org.example.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class SelectStrategy implements StatementStrategy{ @Override public AddStatementStrategy makePreparedStatement(Connection c) throws SQLException { private User user; // 생성자에서 db에 삽입해 줄 User 타입의 오브젝트 초기화 public AddStatementStrategy(User user){ this.user = user; } PreparedStatement ps = c.prepareStatement("INSERT INTO users values (?,?,?)"); //생성자에서 초기화 한 User 오브젝트의 id,Name,password 값을 SQL문의 변수로 설정 ps.setString(1, user.getId()); ps.setString(2, user.getName()); ps.setString(3, user.getPassword()); } }
Java
복사

2.8 공통 로직 jdbcContextWithStatementStrategy() 메소드로 분리

코드를 먼저 봅시다. 우리가 살펴볼 코드는 UserDao 클래스 입니다.
package org.example.dao; import org.example.connection.ConnectionMaker; import org.example.domain.User; import org.springframework.dao.EmptyResultDataAccessException; import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { private ConnectionMaker connectionMaker; public UserDao(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; } public void add(User user) { try { //jdbc 로드 Connection c = connectionMaker.makeConnection(); //2. 쿼리 작성 PreparedStatement ps = c.prepareStatement("INSERT INTO users values(?,?,?)"); ps.setString(1, user.getId()); ps.setString(2, user.getName()); ps.setString(3, user.getPassword()); //3. 쿼리 실행 ps.executeUpdate(); //4. 자원 반납 ps.close(); c.close(); } catch (Exception e) { throw new RuntimeException(e); } } public User findById(String id) throws SQLException, ClassNotFoundException { User user = null; Connection c = connectionMaker.makeConnection(); PreparedStatement pstmt = c.prepareStatement("select * from users where id = ?"); pstmt.setString(1, id); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password")); } if (user == null) { throw new EmptyResultDataAccessException(1); } rs.close(); pstmt.close(); c.close(); return user; } public List<User> findAll() { List<User> userList = new ArrayList<>(); try { Connection c = connectionMaker.makeConnection(); Statement statement = c.createStatement(); ResultSet rs = statement.executeQuery("select * from users"); while (rs.next()) { User user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password")); userList.add(user); } rs.close(); statement.close(); c.close(); } catch (Exception e) { throw new RuntimeException(e); } return userList; } public int getCount() { Connection c = null; PreparedStatement pstmt = null; ResultSet rs = null; try { c = connectionMaker.makeConnection(); pstmt = c.prepareStatement("SELECT count(*) from users"); rs = pstmt.executeQuery(); rs.next(); return rs.getInt(1); } catch (SQLException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { if (rs != null) { try { rs.close(); } catch (SQLException e) { } } if (pstmt != null) { try { pstmt.close(); } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } } public void deleteAll() { Connection c = null; PreparedStatement pstmt = null; try { c = connectionMaker.makeConnection(); pstmt = c.prepareStatement("delete from users"); pstmt.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { //error가 발생해도 실행되는 블록 -> 에러가 나도 자원은 close 해주어야 한다. if (pstmt != null) { try { pstmt.close(); //ps.close()에서도 SQL Exception이 발생할 수도 있다. } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } } }
Java
복사
위의 코드의 메소드들을 보면, 공통 로직들이 많이 보입니다. SQL문을 실행하는 부분을 제외하고는 거의 같은 작업 (자원 반납, 커넥션 생성)을 위한 로직임을 알 수 있습니다.
중복되는 코드는 메소드로 분리할 수 있습니다.
이제 코드를 수정해보겠습니다.
수정된 UserDao
package org.example.dao; import org.example.connection.ConnectionMaker; import org.example.domain.User; import org.springframework.dao.EmptyResultDataAccessException; import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { private ConnectionMaker connectionMaker; private StatementStrategy statementStrategy; public UserDao(ConnectionMaker connectionMaker) { this.connectionMaker = connectionMaker; } public void add(User user) throws SQLException { //익명 클래스를 통해 구현체를 설정, 구현체 클래스를 내부에서 바로 만드는 것 jdbcConnectWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makePreparedStatement(Connection c) throws SQLException { PreparedStatement ps = c.prepareStatement("INSERT INTO users values(?,?,?)"); ps.setString(1, user.getId()); ps.setString(2, user.getName()); ps.setString(3, user.getPassword()); return ps; } }); } public void deleteAll() throws SQLException { //익명 클래스를 통해 구현체를 설정, 구현체 클래스를 내부에서 바로만드는 것 jdbcConnectWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement("DELETE from users"); } }); } public void jdbcConnectWithStatementStrategy(StatementStrategy stmt) throws SQLException{ Connection c = null; PreparedStatement ps = null; try{ c = connectionMaker.makeConnection(); ps = stmt.makePreparedStatement(c); ps.executeUpdate(); } catch(SQLException e){ throw e; } catch (ClassNotFoundException e) { throw new RuntimeException(e); } finally { if (ps != null){ try{ ps.close(); }catch(SQLException e){} } if (c != null){ try{ c.close(); }catch(SQLException e){} } } } }
Java
복사
UserDao 클래스 중 강의 시간에 일부 리팩토링 한 add() 메소드와 deleteAll() 메소드 입니다.
Connection을 생성하고 자원을 반납해주는 역할을 하는 jdbcConnectWithStatementStrategy() 메소드를 생성해줍니다.
각 메소드에 중복되는 로직들을 하나의 메소드로 생성해주었습니다.
이제 add(), deleteAll() 등의 메소드에서 정의한 jdbcConnectWithStatementStrategy() 메소드를 호출해줌으로써, jdbc과 관련된 작업을 수행할 수 있습니다.
위의 로직을 통해, jdbcContext는 executeUpdate()를 사용하는 모든 곳에서 jdbcConnectWithStatementStrategy()를 호출해줌으로써 사용할 수 있습니다.