알고리즘
1.1 TDD 방식으로 push() 기능 개발하기
•
push() 메소드가 아직 구현되지 않았기 때문에, 빨간색으로 표시되고 있다.
•
이제 테스트 코드를 통과하는 실제 push 로직을 구현해줘야 한다.
•
alt+enter를 클릭해 Stack02 클래스에 push() 메소드를 생성해준다.
•
이제 테스트 코드에서 이 기능이 통과되도록 push 메소드를 구현해준다.
•
테스트 코드에서 스택 배열의 값을 활용하여 기댓값과 실제 스택의 반환값을 getArr() 메소드도 Stack 구현 클래스에서 작성해주어야 한다.
•
위의 3가지를 모두 작성한 후, 테스트 코드를 다시 확인하면, 빨간줄이 아래와 같이 사라지게 된다.
•
위와 같이 push() 기능이 정상 작동함을 테스트 코드를 통해 검증할 수 있다.
1.2 pop() 기능 테스트
코드
@BeforeEach
•
위의 pop() 메소드를 테스트 하는 코드에서, @BeforeEach 어노테이션을 볼 수 있습니다.
•
이 어노테이션이 붙은 메소드는, 여러 각각의 테스트를 실행하기 전에 실행됩니다.
•
db에서 데이터를 넣거나, 지우는 코드들을 @BeforeEach를 활용해 구현할 수 있습니다.
•
즉, 테스트를 수행하기 전에 필요한 초기 작업을 수행해줄 수 있습니다.
1.3 스택의 isEmpty() 구현
•
스택이 비어있는지 아닌지 검증하는 메소드
◦
스택이 비어있는데 값을 가져오려고 하면, 이치에 맞지 않기 때문에 이에 대한 검증을 해주는 로직이 필요합니다.
•
top 포인터 변수를 활용하여 이 값이 -1이면 스택이 비었으므로 True를 반환하도록 구현할 수 있습니다.
코드
isEmpty() 기능의 테스트 코드
1.4 pop() 리팩토링
•
우리는 스택이 비어있는지 아닌지 검증하는 isEmpty() 메소드를 위에서 구현했습니다.
•
따라서, 스택의 값을 반환하고 삭제해주는 pop() 메소드에서 이를 활용할 수 있습니다.
코드
테스트 코드 작성 시 원칙
•
negative Test 부터 작성해라
◦
작동하지 않을 것 같은 기능부터 테스트 코드를 작성해라
•
언제 실행해도 동일한 결과가 나오게끔 테스트를 구성해라
1.5 peek() 메소드 구현
•
peek() 메소드는 스택의 최상단 원소가 무엇인지 확인하는 기능이다.
◦
최상단 원소의 값을 단순히 가져와주는 것일 뿐이지, 최상단 원소를 삭제하는 것이 아니다.
public Integer peek(){
if(isEmpty()){
throw new EmptyStackException();
}
return this.stack[top];
}
Java
복사
2. 토비의 스프링
2.1 Factory 적용하기
•
어제 개인 회고를 통해 UserDao 클래스와 관련된 UserDaoFactory 클래스를 하나 만들었습니다.
•
팩토리 클래스에 @Configuration 어노테이션을 달아줌으로써, 이 클래스는 객체를 생성하고 생성된 객체의 관계를 결정해주는 클래스임을 명시해줬습니다.
•
코드를 통해 이해해봅시다.
package org.example.dao;
import org.example.connection.AwsConnectionMaker;
import org.example.connection.LocalConnectionMaker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserDaoFactory {
@Bean //스프링 빈 등록
public UserDao awsUserDao(){ //AwsConnectionMaker 라는 구현체 클래스와 관계를 맺음
UserDao userDao = new UserDao(new AwsConnectionMaker());
return userDao;
}
@Bean //스프링 빈 등록
public UserDao localUserDao(){
//객체 생성 및 구현체 클래스와의 관계 설정
UserDao userDao = new UserDao(new LocalConnectionMaker());
return userDao;
}
}
Java
복사
•
위의 코드를 통해, 우리는 두 개의 빈이 등록됬음을 알 수 있습니다.
•
@Bean을 통해 UserDaoFactory 라는 설정 클래스는 ApplicationContext에 awsUserDao와 localuserDao라는 이름의 빈을 등록해줍니다.
2.2 deleteAll() 구현하기
public void deleteAll() throws SQLException{
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeQuery();
ps.close();
c.close();
}
Java
복사
•
deleteAll() 메소드는 users 테이블의 모든 레코드를 삭제하도록 합니다.
•
executeQuery()를 통해 작성한 SQL문을 실행하도록 합니다.
2.3 getCount() 구현하기
public int getCount() throws SQLException, ClassNotFoundException {
Connection connection = connectionMaker.makeConnection();
PreparedStatement pstmt = connection.prepareStatement("SELECT count(*) as cnt from users");
ResultSet rs = pstmt.executeQuery();
rs.next();
return rs.getInt(1);
}
Java
복사
•
getCount()는 select count(users) from users 라는 쿼리문을 실행합니다.
◦
이는 users 테이블에 존재하는 모든 레코드의 수를 가져와줍니다.
•
executeQuery()는 ResultSet 타입의 오브젝트를 반환합니다.
◦
주로 조회할 때 쓰입니다.
◦
값의 수정,생성, 삭제는 executeUpdate()를 활용합니다.
•
반환한 결과를 하나 읽어와서 int 타입의 값을 반환해줍니다.
2.4 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);
user1 = new User("30", "정인지", "12345");
user2 = new User("31", "박준영", "123456");
user3 = new User("32", "송용호", "1234567");
}
@Test
@DisplayName("count Test")
void count(){
userDao.deleteAll();
userDao.add(user1);
assertEquals(1, userDao.getCount());
userDao.add(user2);
assertEquals(2, userDao.getCount());
userDao.add(user3);
assertEquals(3, userDao.getCount());
}
Java
복사
•
위의 코드를 통해 deleteAll()과 getCount()의 기능을 테스트해주고 있는 것을 알 수 있다.
•
테스트가 실행되기 전에 setUp() 메소드가 호출됨으로써, awsuserDao라는 이름의 빈이 userDao에 할당되게 된다.
◦
이를 통해 우리는, userDao 오브젝트가 Connection을 생성하는데 AWSConnectionMaker가 구현해주는 방식을 사용한다는 것을 알 수 있다.
•
또한, 세 개의 유저 인스턴스를 생성한다.
•
다시 count() 메소드 내부를 살펴보자.
◦
가장 먼저 deleteAll()을 호출하여 users 테이블에 있는 모든 User 타입의 데이터들을 삭제해준다.
◦
이 후, 생성했던 3명의 유저들을 각각 add()를 호출함으로써 users 테이블에 추가해준다.
◦
이 과정에서, getCount() 메소드, 즉 현재 users 테이블의 레코드의 수를 반환해주는 메소드를 호출해줌으로써, count 기능이 잘 동작하는지 검증할 수 있다.
데이터가 없을 때 Resultset이 빈 경우
•
위와 같이 SQLException이 발생하게 된다.
2.5 결과값이 없을 때 exception 처리
•
id값을 통해 유저가 테이블에 존재하는지에 대한 여부를 알려주는 findById() 메소드를 살펴보자.
public User findById(String id){
User user;
try{
Connection c = connectionMaker.makeConnection();
PreparedStatement pstmt = c.prepareStatement("select * from users where id = ?");
pstmt.setString(1, id);
ResultSet rs = pstmt.executeQuery();
rs.next(); //하나 읽어오기
user = new User(rs.getString("id"), rs.getString("name"), rs.getString("password"));
rs.close();
pstmt.close();
c.close();
}catch (Exception e){
throw new RuntimeException(e);
}
return user;
}
Java
복사
•
위 코드에서 rs.next()로 쿼리문의 실행 결과를 읽었을 때, users 테이블에 id가 xx인 유저가 존재하지 않는다면, user에는 null값이 들어가게 됩니다.
•
따라서, 우리는 이에 대한 예외처리를 설정해줄 필요가 있습니다.
◦
쿼리문의 실행 결과가 null이 아닐 경우에만 User 오브젝트를 생성하도록 수정합니다.
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;
}
Java
복사
2.6 JUnit의 작동 방식
1.
테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
•
public은 생략해도 된다.(Junit 5)
2.
테스트 클래스의 오브젝트를 하나 만든다,
3.
@BeforeEach가 붙은 메소드 실행
4.
@Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둠
5.
@AfterEach가 붙은 메소드 실행
6.
나머지 남은 테스트에 대해 2~5번을 반복
7.
모든 테스트 결과를 종합해서 돌려줌
•
이 때 나는 2번에 대해 의문이 생겼다.
◦
테스트가 하나 실행되고, 다른 테스트를 실행하기 위해 테스트 클래스의 인스턴스를 다시 생성해서 또 다른 테스트를 실행한다고?
Junit5에서 테스트 실행할 때 마다 오브젝트를 생성하는 이유
•
이는, 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만든다고 한다.
•
테스트 클래스는 인스턴스 변수를 매번 새로 생성해주기 때문에, 독립적인 실행을 확실하게 보장한다.
•
따라서, 각 테스트 메소드끼리는 의존이 전혀 없으므로, 테스트 메소드 실행에 대한 순서가 보장되지 않는다.
2.7 ApplicationContext Singleton
•
@Autowired를 이용해 Container에서 가져오게 하여 new를 한번만 하도록 한다.
•
ApplicationContext는 스프링 컨테이너라고 생각하면 편하다고 어제 개인 회고에 작성했다.
◦
근데 ApplicationContext가 스프링 컨테이너인데, ApplicationContext를 스프링 컨테이너에서 가져온다고?
▪
스프링 컨테이너에서 스프링 컨테이너를 가져온다고? 라고 개인적으로 생각했다.
◦
이는, AppplicationContext도 스프링 빈으로 등록되기 때문에 컨테이너에서 가져와줄 수 있다.
•
@Autowired
◦
필요한 의존 객체의 “타입"에 해당하는 빈을 ApplicationContext 에서 찾아 주입한다.
◦
필드 주입, 생성자 주입, 설정자 주입의 3가지로 DI가 가능하다.
◦
우리가 작성한 방식은 필드 주입 방식이다.
◦
즉, 이 어노테이션을 통해 AppplicationContext라는 빈 객체를 스프링 컨테이너가 알아서 자동으로 주입해주는 것이다.
ApplicationContext에서 Bean을 가지고 올 때 Spring이 검색하는 방법
@BeforeEach
void setUp(){
userDao = ac.getBean("awsUserDao", UserDao.class);
user1 = new User("30", "정인지", "12345");
user2 = new User("31", "박준영", "123456");
user3 = new User("32", "송용호", "1234567");
}
Java
복사
•
getBean()을 통해 스프링에 등록된 빈을 조회할 수 있다.
•
빈의 이름은 설정 클래스에서 @Bean 어노테이션을 통해 빈을 생성하는 각 메소드의 이름으로 지정된다.
2.8 토비의 스프링 3장 : 예외 처리
•
예외 처리가 없을 때 문제점
public void deleteAll() throws SQLException{
Connection c = connectionMaker.makeConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
//Truncate users
ps.executeQuery();
ps.close();
c.close();
}
Java
복사
•
위 코드는 로컬에서는 전혀 문제가 발생하지 않는다.
•
그러나, 서버 환경에서는 서버가 일찍 다운될 수 있다는 문제점이 있다.
•
커넥션과 PreparedStatement는 보통 풀 방식으로 운영됨
◦
미리 정해진 풀 안에 제한된 수의 리소스를 만들어 두고, 필요할 때 이를 할당하고 반환하면 다시 풀에 넣는 방식으로 운영
◦
요청이 매우 많은 서버 환경에서는 매번 새로운 리소스를 생성하는 대신 풀에 미리 만들어둔 리소스를 돌려가며 사용하는 편이 훨씬 유리하다.
◦
대신 사용한 리소스를 반환하지 않으면 돌려쓰는 리소스가 고갈되고, 결국 문제가 발생한다.
◦
따라서 close() 메소드를 호출하여 사용한 리소스를 풀로 다시 돌려줘야 한다.
2.9 Connection, preparedStatement 할 때 에러가 나도, ps.close(), c.close() 하기 위한 처리
•
아래 코드는 UserDao 클래스의 getCount() 메소드와, deleteAll() 메소드 이다.
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
복사
•
코드를 보면 매우 많은 부분이 달라진 것을 볼 수 있다.
◦
Connection과 PreparedStatement 타입의 변수를 null로 초기화 한다.
•
에러가 발생하든 발생하지 않든, 커넥션과 PreparedStatement, ResultSet과 같은 클래스는 풀에 있는 위에서 언급한 것과 같이 정해진 풀 안에 제한된 리소스를 만들어두고, 필요할 때 할당하고 반환하면 다시 풀에 넣는 방식으로 사용한다.
•
만약 makeConnection()에서 Db 커넥션을 가져오다가 일시적인 db 서버 문제나, 네트워크 문제 때문에 예외가 발생했다면, PreparedStatement와 커넥션 오브젝트는 아직 모두 null 상태이다.
◦
null 상태의 변수에 close() 메소드를 호출하면 NPE가 발생할 테니 이럴 땐 close() 메소드를 호출하면 안된다.
•
또 다른 상황으로, PreparedStatement를 생성하다가 예외가 발생했다면, 그 때는 c 변수가 커넥션 객체를 갖고 있는 상태이므로 c는 close() 호출이 가능한 반면 pstmt는 아니다.
•
이 말의 요지는 어느 시점에서 예외가 발생했는지에 따라서 close()를 사용할 수 있는 변수가 달라질 수 있기 때문에 finally에서는 반드시 c와 pstmt가 null이 아닌지 먼저 확인한 후에 close() 메소드를 호출해야 한다.