알고리즘
프로그래머스 - 괄호 문제 풀기
•
열린 괄호와 닫힌 괄호가 반드시 짝지어서 문자로 닫히는 것을 구현해야 하는 문제이다.
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()를 호출해줌으로써 사용할 수 있습니다.