///////
Search

Stack, 토비의 스프링_곽철민

1.

알고리즘

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() 메소드를 호출해야 한다.