알고리즘
Stack이란?
•
입구가 하나인 LIFO(Last In First Out) 후입선출 형식의 자료 구조
◦
가장 최근에 스택에 추가한 항목이 가장 먼저 제거될 항목(후입선출)
스택의 4가지 기능
•
pop() : 스택에서 가장 위에 있는 항목을 제거한다.
•
push(item) : item 하나를 스택의 가장 윗 부분에 추가한다.
•
peek() : 스택의 최상단 원소를 확인한다. (제거 하지 않음)
•
isEmpty() : 스택이 비어있는지 확인해준다. 만약 비어 있으면 True를 반환하는 Boolean Type이다.
◦
비어 있는 스택에서 원소를 추출하면, stack UnderFlow가 발생하기 때문에, 원소를 하나 꺼낼 경우에 isEmpty() 메소드를 호출하여 스택이 비어 있는지 검증해주어야 한다.
스택의 활용 예시
•
역순 문자열 만들기 : 가장 나중에 입력된 문자 부터 출력
•
실행 취소 : 가장 나중에 실행된 것부터 실행을 취소한다.
•
후위 표기법 계산
•
수식의 괄호 검사
Stack 구현을 위한 뼈대 코드를 만들어보자.
코드
Stack의 push() 구현
코드
Stack의 pop() 구현
코드
2. 토비의 스프링
2.1 인터페이스의 유연성
•
어제 제 개인 회고를 보시면 아시다시피, 공통 관심사에 대한 부분을 인터페이스에 선언해주고, 구현체 클래스에서 이를 각자 상황에 맞게 기능을 확장하여 사용하도록 구현해 주었습니다.
•
클래스 다이어그램을 보면 이해가 좀 더 잘 될 것입니다.
•
이제 UserDao 클래스는 ConnectionMaker에 의존함으로써 추상에 의존할 수 있게 됩니다.
•
만약 DB 커넥션을 변경해야하는 상황이 발생한다면, UserDao는 코드의 변경이 일어나지 않고, 변경하고 싶은 Connection 구현체 클래스를 외부에서 주입해주어 변경해서 사용하면 됩니다.
•
UserDao는 인터페이스에 의존하고 있기 때문에, ConnectionMaker를 구현한 구현체를 모두 사용할 수 있습니다.
2.2 Factory 적용
•
관계 설정 책임의 분리
•
팩토리는 객체를 생성하고 생성된 객체의 관계를 결정해줍니다.
•
선풍기를 예시로 들어 봅시다.
◦
선풍기의 역할은 “시원한 바람을 분다”. 입니다.
◦
그러나, 선풍기의 모델에 따라 날개가 4개가 달려 있을 수도, 5개 혹은 2개 까지 다양한 모양의 날개를 갖습니다.
•
이 말을 하는 요지는, 선풍기가 만들어지고(객체 생성) 선풍기의 날개는 몇개인지(구현체를 결정, 객체간이 관계를 결정)관리해주는 것이 필요하다는 것입니다.
•
프로그래밍에서는 이를 객체를 생성하고, 객체 간의 관계를 결정해준다고 표현해줄 수 있습니다.
◦
이는, 팩토리 클래스에서 담당할 수 있습니다.
UserDao에 Factory를 적용한 코드
2.3 Spring Boot 적용하기
•
그동안의 프로젝트는 단순히 자바 코드로만 진행했습니다.
•
이제 스프링 부트의 여러 기능을 활용하도록 외부 라이브러리를 추가해 봅시다.
spring boot jdbc / spring boot test 라이브러리 추가 in build.gradle
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-jdbc', version: '2.7.4'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.7.4'
Plain Text
복사
UserDaoTest 클래스의 테스트 메소드를 실행하여 스프링 부트가 잘 연결됬는지 확인해보자
package org.example.dao;
import org.example.domain.User;
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.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = UserDaoFactory.class)
class UserDaoTest {
@Autowired
ApplicationContext ac;
@Test
@DisplayName("add와 select 테스트")
void addAndSelect(){
//given
UserDao userDao = ac.getBean("awsUserDao", UserDao.class);
User user = new User("9", "정준하", "1231231313");
//when
userDao.add(user);
User savedUser = userDao.findById(user.getId());
//then
assertEquals(user.getName(), savedUser.getName());
}
}
Java
복사
•
위와 같이 ac.getBean()을 통해 스프링의 다양한 빈이 출력되면 스프링 부트가 제대로 적용됬음을 알 수 있다.
스프링의 어노테이션
@Configuration
•
설정 파일을 만들기 위한 애노테이션 or Bean을 등록하기 위한 어노테이션
@ContextConfiguration
•
자동으로 만들어줄 ApplicationContext의 설정 파일 위치를 지정한 것이다.
•
우리는 테스트 코드에서 @ContextConfiguration(classes = UserDaoFactory.class)의 형식으로 작성
•
이로써, ApplicationContext 즉, 스프링 컨테이너의 설정 파일의 위치를 지정해준 것이다.
•
JUnit의 어노테이션
@Extendwith
•
확장을 선언적으로 등록할 때 사용함
•
우리는 테스트 코드에서 @ExtendWith(SpringExtension.class)의 형태로 작성
2.4 IoC(Inversion of Control) - 제어의 역전
•
프로그래머가 직접 객체의 생성, 관계같은 제어를 수행하는 것이 아니라, 여러 프레임워크(우리는 Spring FrameWork), 컨테이너(ApplicationContext)에서 객체 제어를 수행하는 것입니다.
•
확장 가능하고 모듈화된 프로그램을 구성하는 느슨한 결합을 달성하기 위해 다양한 종류의 제어를 역전해줍니다. (객체 관리를 여러 프레임워크 혹은 컨테이너가 해준다)
•
이로써, 클래스 간의 결합을 느슨하게 설계하여 테스트가 가능하고 유지 보수가 용이하게 만들 수 있습니다.
•
코드로 이해해봅시다.
[UserDao.java]
package org.example.dao;
import org.example.connection.ConnectionMaker;
import org.example.domain.User;
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){
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;
}
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() throws SQLException, ClassNotFoundException {
Connection connection = connectionMaker.makeConnection();
PreparedStatement pstmt = connection.prepareStatement("SELECT count(users) as cnt from users");
int result = pstmt.executeUpdate();
return result;
}
public void deleteAll(){
}
}
Java
복사
•
위의 코드는 UserDao 클래스 입니다.
•
기존의 ConnectionMaker의 구현 클래스를 결정하고, 오브젝트를 만드는 제어권은 UserDao에게 있었습니다.
◦
위의 말을 코드로 표현하면 아래와 같습니다.
public class UserDao(){
ConnectionMaker connectionMaker = new AwsConnectionMaker();
}
Java
복사
•
그런데 이제, ConnectionMaker 구현 클래스를 결정하고, 오브젝트를 만드는 제어권은 DaoFactory에게 있습니다.
[UserDaoFactory.java]
package org.example.dao;
import org.example.connection.AwsConnectionMaker;
import org.example.connection.LocalDbConnectionMaker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserDaoFactory {
@Bean
public UserDao awsUserDao(){
UserDao userDao = new UserDao(new AwsConnectionMaker());
return userDao;
}
@Bean
public UserDao localUserDao(){
UserDao userDao = new UserDao(new LocalDbConnectionMaker());
return userDao;
}
}
Java
복사
•
위와 같이 UserDaoFactory에서 UserDao 오브젝트를 생성하고, 구현 클래스를 결정해줍니다.
•
따라서, UserDao는 어떤 ConnectionMaker 구현 클래스를 만들고 사용할지를 결정할 권한을 Factory에게 넘기게 되었고, 수동적인 존재가 되어버렸습니다.
•
더욱이, UserDao는 자신도 팩토리에 의해 수동적으로 만들어지게 됩니다.
•
UserDao는 수동적인 존재가 되어버리고, UserDaoFactory가 객체의 생성과 관계를 관리해주는 것, 이것이 바로 제어의 역전입니다.
•
DaoFactory를 도입하는 과정은 바로 IoC를 적용하는 작업이었다고 볼 수 있다.
애플리케이션 컨텍스트의 동작 방식
•
userDao는 UserDaoFactory에서 @Bean 표시를 해주었으므로, ApplicationContext에 빈으로 등록된다.
◦
Bean 이란 ? 스프링 컨테이너에서 관리해주는 객체라고 생각하면 된다.
•
위의 그림은 우리가 작성한 테스트 코드를 애플리케이션 컨텍스트의 동작 방식의 관점에서 나타낸 그림이다.
package org.example.dao;
import org.example.domain.User;
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.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = UserDaoFactory.class)
class UserDaoTest {
@Autowired
ApplicationContext ac;
@Test
@DisplayName("add와 select 테스트")
void addAndSelect(){
//given
UserDao userDao = ac.getBean("awsUserDao", UserDao.class);
User user = new User("13", "정준하", "1231231313");
//when
userDao.add(user);
User savedUser = userDao.findById(user.getId());
//then
assertEquals(user.getName(), savedUser.getName());
}
}
Java
복사
1.
DaoFactory는 userDao 오브젝트의 생성 및 구현체 결정을 하고 ApplicationContext에 Bean으로 등록한다.
a.
ContextConfiguration 어노테이션으로 우리의 구현체 설정 클래스를 지정해준다.
2.
테스트 코드에서 applicationContext.getBean()을 통해, ApplicationContext에 등록된 스프링 빈을 조회한다.
3.
클라이언트는 applicationContext가 반환해준 userDao를 사용한다.
2.5 싱글톤 패턴
•
객체의 인스턴스가 오직 1개만 생성되는 패턴
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
//외부에서 객체가 생성되지 않도록 생성자를 private으로 지정해준다.
}
public static Singleton getInstance() {
return instance;
}
public void say() {
System.out.println("안녕");
}
}
Java
복사
싱글톤 패턴을 사용하는 이유
•
최초 한번의 new 연산자를 통해 고정된 메모리 영역을 사용하므로, 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있다.
•
또한, 이미 생성된 인스턴스를 활용하므로 속도 측면에서도 이점이 있다.
•
다른 클래스간의 데이터 공유가 쉽다.
◦
싱글톤 인스턴스는 전역으로 사용되는 인스턴스 이기 때문에, 다른 클래스의 인스턴스들이 접근하여 사용할 수 있다.
◦
하지만, 여러 클래스의 인스턴스에서 싱글톤 인스턴스에 동시 접근하게 되면 “동시성 문제”가 발생하게 된다.
싱글톤 패턴의 문제점
•
멀티 스레드 환경에서 두개 이상의 스레드가 getInstance()를 하게 될 경우 두개의 인스턴스가 생성되는 문제가 생길 수 있다.(동시성 문제, thread safte 하지 못함)
•
테스트 하기 어렵다.
◦
싱글톤 인스턴스는 자원을 공유 → 테스트가 결정적으로 격리된 환경에서 수행되려면 매번 인스턴스의 상태를 초기화시켜주어야 한다.
◦
그렇지 않으면, 애플리케이션 전역에서 상태를 공유 → 테스트가 온전하게 수행되지 못한다.
•
의존 관계상 클라이언트가 구체 클래스에 의존하게 된다.
◦
new 키워드를 직접 사용하여 클래스 안에서 객체를 직접 생성하므로, 이는 SOLID 원칙 중 DIP를 위반하게 된다.
싱글톤 패턴의 멀티 스레드 환경에서의 문제점 해결 방법
1.
Thread Safe lazy initialization 방법 - synchronized
•
synchronized 키워드를 사용해 getinstance() 메소드를 동기화시킴으로써 thread-safe하게 만든다.
•
하지만 큰 성능 저하가 발생한다.
ublic class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public synchronized static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
Java
복사
2.
Holder initialization 방법 - Lazy Holder
•
가장 많이 사용되고 있는 해결 방법으로, getInstance() 메소드에서 LazyHolder.INSTANCE를 호출하는 순간 Class가 로딩되며 초기화가 진행되고, 이 시점에 thread-safe를 보장하도록 한다.
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
public static final Singleton INSTANCE = new Singleton();
}
}
Java
복사
출처
2.6 스프링 컨테이너
•
그런데 스프링 컨테이너는 왜 싱글톤 컨테이너일까?
•
가비지 컬렉션을 줄이기 위해 싱글톤 컨테이너를 스프링 부트는 기본적으로 사용합니다.
2.7 테스트란?
•
내가 예상하고 의도했던 코드가 정확히 동작하는지 확인해서, 코드에 대한 확신을 얻기 위한 작업이다.
•
테스트의 결과가 원하는 대로 나오지 않으면, 코드나 설계에 결함이 있음을 알 수 있다.
•
이를 통해, 코드의 결함을 제거하는 작업, 즉 디버깅을 거치게 된다.
•
이를 통해, 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.
자동화된 테스트의 중요성
•
애플리케이션의 규모는 점점 복잡해지기 때문에, 위의 이점을 가지는 테스트 코드를 꼭 작성해야 합니다.