///////
Search

알고리즘,토비의스프링_이연재

1. 알고리즘

1.1 문제

1.2 배열로 풀기

1.2.1 배열로 풀기 전 알아야 할 사항

위 문제를 배열로 풀기 전 다음 세가지 사항을 알고 있어야 풀 수 있다.
1) 배열을 자르는 방법을 아는가? 
- 0을 기준으로 Arrays.copyOfRange(원본 배열, 자를 배열의 시작인덱스, 자를 배열의 끝인덱스+1)를 사용한다. 
- ex) arr = {1,5,2,6,3,7,4}라는 배열이 있을 때 Arrays.copyOfRange(arr, 2, 5);를 하면 {2,6,3}이 된다.
2) 정렬을 할 줄 아는가?  
 - Arrays.sort(정렬할 배열)을 사용한다.
3) 특정 인덱스의 값을 뽑을 수 있는가?   
배열의 이름[i]를 사용한다.
4) 이차원 배열의 행과 열을 구할 수 있는가?  
- 행: 배열의 이름.length   
- 열: 배열의 이름[행 인덱스].length

1.2.2 코드

public int[] solution(int[] array, int[][] commands) { int[] answer = new int[commands.length]; for(int i = 0; i<commands.length; i++){ int[] temp = Arrays.copyOfRange(array, commands[i][0]-1, commands[i][1]); Arrays.sort(temp); answer[i] = temp[commands[i][2]-1]; } return answer; }
Java
복사
1) solution메소드는 프로그래머스의 답 메소드를 그대로 가져왔다.
2) answer 배열은 commands의 행의 개수만큼 선언한다.
3) for문도 commands의 행의 개수만큼 돌아가게 한다.
4) temp 배열은 문제에서 배열의 i~j번째 숫자까지 잘랐을 때 저장하는 배열이다.
5) 잘린 배열을 temp에 저장한 후 Arrays.sort를 통해 정렬한다.
6) 그 후 answer에 temp의 k번째 수를 구한다.

1.3 우선순위 큐(Priority Queue)로 풀기

1.3.1 우선순위 큐란?

일반적인 큐의 구조 FIFO(First In First Out)를 가지면서, 데이터가 들어온 순서대로 나가는 것이 아닌 우선순위를 먼저 결정하고 그 우선순위가 높은 데이터가 먼저 나가는 자료구조이다.
우선순위 큐의 특징
0) 우선순위 큐에 넣으면 정렬이 된다.
1) 높은 우선순위의 요소를 먼저 꺼내서 처리하는 구조이다. 
- 우선순위 큐에 들어가는 원소는 비교가 가능한 기준이 있어야 한다.
2) 내부 요소는 힙으로 구성되어 이진 트리 구조로 이루어져 있다.
3) 내부 구조가 힙으로 구성되어 있기에 시간 복잡도는 O(NLogN)이다.
4) 우선순위를 중요시해야 하는 상황에서 주로 쓰인다.
우선순위 큐 사용 방법

1.3.2 사용 이유

배열을 잘라서 새로 만들면 메모리를 추가로 쓰게 되는데 배열 만드는 것을 최소화 하면 메모리를 덜 쓰지 않을까 라는 생각으로 우선순위 큐를 사용

1.3.3 코드

public int[] solution2(int[] arr, int[][] commands) { int[] answer = new int[commands.length]; for (int i = 0; i < commands.length; i++) { PriorityQueue<Integer> pq = new PriorityQueue<>(); // 우선순위 큐에 from, to까지 넣기 for (int j = commands[i][0]-1; j < commands[i][1]; j++) { pq.add(arr[j]); } // 정렬이 되었기 때문에 뽑기만 한다. for (int j = 0; j < commands[i][2]; j++) { answer[i] = pq.poll(); //stack의 pop과 비슷 } } return answer; }
Java
복사
1) answer 선언과 for문 사용은 1.2 배열로 풀기 사용과 똑같다.
2) pq라는 우선순위 큐를 선언한다.
3) 우선순위 큐에 문제의 i~j번째까지 원소를 넣는다. 원소가 들어가면서 자동으로 정렬되어 따로 정렬할 필요가 없어진다.
4) 그 후 answer에 pq의 k번째 원소를 뽑아 넣는다.

2. 토비의 스프링

2.1 지난 시간 코드

지난 시간에 토비의 스프링 228p AddStatement까지 진행했었다.
지난 시간까지 진행했던 코드는 다음과 같다.
AWSConnectionMaker.java
AddStrategy.java
ConnectionMaker.java
DeleteAllStrategy.java
LocalConnectionMaker.java
StatementStrategy.java
UserDao.java
UserDaoFactory.java
User.java
UserDaoTest.java

2.2 DataSource 인터페이스 적용(138p)

앞에서 만든 ConnectionMaker는 단순히 DB 커넥션을 생성해주는 기능 하나만을 정의한 인터페이스이다.
하지만 자바에는 이미 DB 커넥션을 가져오는 오브젝트의 기능을 추상화 해서 사용할 수 있게 만들어진 DataSource라는 인터페이스가 이미 존재한다. 따라서 ConnectionMaker같은 인터페이스를 만들지 않아도 된다.
아래 코드들은 각각DataSource()를 적용한 UserDaoFactory.java와 UserDao.java이다.
[UserDaoFactory.java]
import org.springframework.jdbc.datasource.SimpleDriverDataSource; import javax.sql.DataSource; import java.util.Map; @Configuration public class UserDaoFactory { @Bean UserDao awsUserDao() { return new UserDao(awsDataSource()); } @Bean UserDao localUserDao() { return new UserDao(localDataSource()); } @Bean DataSource awsDataSource() { Map<String, String> env = System.getenv(); SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class); dataSource.setUrl(env.get("DB_HOST")); dataSource.setUsername(env.get("DB_USER")); dataSource.setPassword(env.get("DB_PASSWORD")); return dataSource; } @Bean DataSource localDataSource() { Map<String, String> env = System.getenv(); SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class); dataSource.setUrl("localhost"); dataSource.setUsername("root"); dataSource.setPassword("12345678"); return dataSource; }}
Java
복사
[UserDao.java]
import javax.sql.DataSource; public class UserDao { private DataSource dataSource; // DataSource를 의존하게 변경 public UserDao(DataSource dataSource) { this.dataSource = dataSource; // 생성자도 변경 }public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { Connection c = null; PreparedStatement pstmt = null; try { c = dataSource.getConnection(); // datasource를 사용하게 변경 }
Java
복사

2.3 익명 클래스 도입(231p)

익명 내부 클래스란?
익명 내부 클래스를 도입한 이유: StatementStrategy Interface의 구현체인 DeleteAllStrategy()를 쓰는 곳이 deleteAll()한군데 뿐이기 때문에 굳이 class를 새로 만들 필요가 없다. 메소드도 한 개 뿐이라서 더욱 그렇다.
AddStatement도 마찬가지로 익명 내부 클래스로 만들 수 있다.
아래 코드를 보면 jdbcContextWithStatementStrategy(new StatementStrategy() {}를 이용해 익명클래스를 구현했다.
[UserDao.java]
public void deleteAll() throws SQLException { // "delete from users" jdbcContextWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makeStatement(Connection conn) throws SQLException { return conn.prepa reStatement("delete from users"); } }); } public void add(final User user) throws SQLException { // DB접속 (ex sql workbeanch실행) jdbcContextWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makeStatement(Connection conn) throws SQLException { PreparedStatement pstmt = null; pstmt = conn.prepareStatement("INSERT INTO users(id, name, password) VALUES(?,?,?);"); pstmt.setString(1, user.getId()); pstmt.setString(2, user.getName()); pstmt.setString(3, user.getPassword()); return pstmt; } }); }
Java
복사

2.4 JdbcContext 분리(234p)

2.4.1 jdbcContextWithStatementStrategy의 분리

분리하는 이유: jdbcContextWithStatementStrategy는 어디 하나에 종속되지 않고 다른 Dao에서도 사용이 가능하기 때문에 UserDao에서 분리한다.(ex. UserDao뿐만아니라 HospitalDao등에서도 사용하기 위해)
jdbcContextWithStatementStrategy를 분리한 클래스를 JdbcContext라고 하고 기존에 UserDao에 있던 jdbcContextWithStatementStrategy를 JdbcContext클래스의 workWithStatementStrategy라는 이름으로 바꿔서 넣어준다.
[JdbcContext.java]
public class JdbcContext { private DataSource dataSource; public JdbcContext(DataSource dataSource) { this.dataSource = dataSource; } public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException { Connection c = null; PreparedStatement pstmt = null; try { c = dataSource.getConnection(); pstmt = stmt.makeStatement(c); // Query문 실행 pstmt.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (pstmt != null) { try { pstmt.close(); } catch (SQLException e) { } } if (c != null) { try { c.close(); } catch (SQLException e) { } } } } }
Java
복사

2.4.2 UserDao가 JdbcContext를 의존하게 변경

위에서 JdbcContext 클래스를 생성했으므로 기존에 jdbcContextWithStatementStrategy를 사용하던 UserDao도 JdbcContext를 의존하도록 바꿔준다.
책에서는 set을 썼는데 이 방식은 xml설정 방식에서 set을 쓰기 때문에 set을 사용하게 써놓았다. 지금은 xml설정 방식을 잘 쓰지 않는다.
[UserDao.java]
public class UserDao { private final DataSource dataSource; private final JdbcContext jdbcContext; public UserDao(DataSource dataSource) { this.dataSource = dataSource; this.jdbcContext = new JdbcContext(dataSource); } public void add(final User user) throws SQLException { // DB접속 (ex sql workbeanch실행) jdbcContext.workWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makeStatement(Connection conn) throws SQLException { PreparedStatement pstmt = null; pstmt = conn.prepareStatement("INSERT INTO users(id, name, password) VALUES(?,?,?);"); pstmt.setString(1, user.getId()); pstmt.setString(2, user.getName()); pstmt.setString(3, user.getPassword()); return pstmt; } }); }
Java
복사
JdbcContext와 DataSource의 관계 설정을 Constructor에서 한 이유

2.5 TemplateCallback적용(247p)

TemplateCallback을 적용하는 이유: 앞에서 사용했던 익명 클래스는 중복되는 부분이 있고 UserDao외에도 사용할 가능성이 있다. 따라서 이렇게 재사용 가능한 콜백을 담고 있는 메소드는 Dao가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다.
익명 클래스를 JdbcContext 클래스에 콜백 생성과 템플릿 호출이 담긴 executeSql()메소드를 생성해서 옮긴다.
[JdbcContext.java]
public void executeSql(String sql) throws SQLException { this.workWithStatementStrategy(new StatementStrategy() { @Override public PreparedStatement makePreparedStatement(Connection connection) throws SQLException { return connection.prepareStatement(sql); } }); }
Java
복사
[UserDao.java]
public void deleteAll() throws SQLException { this.jdbcContext.executeSql("delete from users"); }
Java
복사

2.6 스프링의 JdbcTemplate적용(261p)

스프링은 JDBC를 이용하는 Dao에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.
따라서 앞에서 만들었던 JdbcContext대신 스프링이 제공하는 JdbcTemplate을 사용한다.
queryForInt가 아닌 queryForObject를 사용하는 이유
getCount() 메소드를 보면 책에선 queryForInt을 사용하지만 오래전에 사라졌으므로 queryForObject를 사용한다.
queryForObject는 반환형으로 데이터만 가능하다.
간단하게 쿼리와, 리턴받을 Class를 넘기면 된다. count(*)쿼리이기 때문에 Integer.class를 넘기면 된다.
[UserDao.java]
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; public class UserDao { private final DataSource dataSource; private JdbcTemplate jdbcTemplate; public UserDao(DataSource dataSource) { this.dataSource = dataSource; // 생성자도 변경 this.jdbcTemplate = new JdbcTemplate(dataSource); } public void add(final User user) throws SQLException { //jdbcTemplate.update()의 경우 두번째부터 파라메터 개수 만큼 ?자리에 값을 넘길 수 있다. this.jdbcTemplate.update("insert into users(id, name, password) values (?, ?, ?);", user.getID(), user.getName(), user.getPassword()); } public User get(String id) throws ClassNotFoundException, SQLException { String sql = "select * from users where id = ?"; RowMapper<User> rowMapper = new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password")); return user; } }; return this.jdbcTemplate.queryForObject(sql, rowMapper, id); } public void deleteAll() throws SQLException { this.jdbcTemplate.update("delete from users"); } public int getCount() throws SQLException, ClassNotFoundException { //queryForObject에 두번째 파라메터로 Integer.class를 넘겨줌으로써 int형의 데이터를 받아온다. return this.jdbcTemplate.queryForObject("select count(*) from users;", Integer.class); } }
Java
복사
여기서 get()메소드는 RowMapper를 이용한다.
RowMapper란?
위에서 queryForObject는 반환형으로 데이터형만 가능하다고 했는데 만약 SELECT * FROM USER 구문으로 User 객체 자체를 반환받고 싶다면? -> RowMapper를 이용한다.
RowMapper를 사용하면, 원하는 형태의 결과값을 반환할 수 있다.
RowMapper의 mapRow 메소드는 ResultSet을 사용한다. 사용법은 다음과 같다.??? mapRow(ResultSet rs, int count);
ResultSet에 값을 담아와서 User 객체에 저장하고 그것을 count만큼 반복한다는 뜻이다.

2.7 getAll()과 테스트 구현(270p)

jdbcTemplate.query() 를 사용한다. 그리고 List<User> getAll() List<User> 를 리턴하게 해놓으면 모든 User를 List에 담아서 리턴 해준다.
[UserDao.java]
public List<User> getAll() { String sql = "select * from users order by id"; RowMapper<User> rowMapper = new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password")); return user; } }; return this.jdbcTemplate.query(sql, rowMapper); }
Java
복사
getAll()을 테스트하는 메소드도 테스트 클래스에 만들어 준다.
[UserDaoTest.java]
void getAll() throws SQLException { userDao.deleteAll(); List<User> users = userDao.getAll(); assertEquals(0,users.size()); userDao.add(user1); userDao.add(user2); userDao.add(user3); users = userDao.getAll(); assertEquals(3,users.size()); }
Java
복사

2.8 rowMapper의 중복 제거(274p)

get()메소드와 getAll()메소드에 rowMapper가 중복되기도 하고 앞으로도 UserDao에 기능이 더 추가 될것을 생각해 따로 메소드로 빼서 중복을 제거해 준다.
지금까지 작성했던 UserDao의 전체 코드는 다음과 같다. 처음 작성했던 것에 비하면 매우 간단해졌다.
[UserDao.java]
package org.example; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import user.User; import javax.sql.DataSource; import java.sql.*; import java.util.List; import java.util.Map; public class UserDao { private final DataSource dataSource; private JdbcTemplate jdbcTemplate; public UserDao(DataSource dataSource) { this.dataSource = dataSource; // 생성자도 변경 this.jdbcTemplate = new JdbcTemplate(dataSource); } RowMapper<User> rowMapper = new RowMapper<User>() { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password")); return user; } }; public void add(final User user) throws SQLException { this.jdbcTemplate.update("insert into users(id, name, password) values (?, ?, ?);", user.getID(), user.getName(), user.getPassword()); } public User get(String id) throws ClassNotFoundException, SQLException { String sql = "select * from users where id = ?"; return this.jdbcTemplate.queryForObject(sql, rowMapper, id); } public void deleteAll() throws SQLException { this.jdbcTemplate.update("delete from users"); } public int getCount() throws SQLException, ClassNotFoundException { return this.jdbcTemplate.queryForObject("select count(*) from users;", Integer.class); } public List<User> getAll() { String sql = "select * from users order by id"; return this.jdbcTemplate.query(sql, rowMapper); } }
Java
복사

2.9 생각해볼 사항들

2.9.1 docker가 자꾸 내려간다면?

AWS가 켜져있는데도 docker가 자꾸 내려간다면 비밀번호 유출에 의한 해킹 가능성이 있다.
특정 컨테이너가 종료된 경우 로그 보는 방법
docker container ls -a | grep mysql
Plain Text
복사
아래와 같은 로그가 떴는데 SHUTDOWN을 한적이 없다면 해킹 가능성이 있다.
2022-10-21T02:22:01.006922Z 10 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.30).
Plain Text
복사

2.9.2 DI할 때 final을 쓰는 이유?

final이란 한번 초기화 되면 바꿀 수 없는 불변하는 성질을 가진다.
final을 사용했을때 이점
1) 신뢰성 - 불변이기 때문에 변화를 고려하지 않아도 된다.
2) Memory를 적게 쓴다. - 바뀔 여지가 없기 때문에 바뀌는데 필요한 메모리 할당이 필요 없다.
3) DI하고 나서 DataSource가 바뀌는 경우 - 무슨일이 일어날지 예측이 안된다.
final을 사용하는 이유
1) Spring에서 DI되었다면 이미 Factory에서 조립이 끝난 상태이므로 변화하지 않는게 좋다.
2) 변화하지 않는게 좋으므로 final로 쓰는게 좋다. 왜냐하면 메모리 사용에 유리하고 신뢰성 있기 때문이다.
3) 이후 SpringBoot에서 @Autowired하는 부분이 final로 대체하는 것을 권장하게 바뀌었다.

2.9.3 Bean은 언제 사용할까?

Spring의 ApplicationContext에 등록하는 빈을 만들 때 ex) AwsUserDao 등 재료가 되는 @Bean 도 붙여주는 경우가 있다. 조립의 재료로만 쓴다면 ApplicationContext에 등록을 안해줘도 된다.

3. Reference

queryForObject의 사용 출처: https://krksap.tistory.com/1995