221019 Spring

서이·2022년 10월 24일

수업정리

목록 보기
13/17

DAO

DAO란?

DAO(Data Access Object)는 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 말한다.

클래스의 분리

  • 이전의 코드에서는 메소드, 상속으로 Connection을 분리해왔다. 이번에는 상속관계도 아닌 완전히 독립적인 클래스로 만들었다.
  • 그 이유는? Connection의 연결은 1번, 그 외에 add(),get() 메서드의 사용은 여러번이다. 이때문에 필요 없는 Connection 인스턴스를 계속 생성하기 보단 SimpleConnectionMaker 오브젝트를 만들어서 저장해두고 이를 계속 사용하는 편이 낫기 때문이다.
  • 코드
    //SimpleConnectionMaker
    public class SimpleConnectionMaker {
        public Connection makeNewConnection() throws SQLException {
            Map<String, String> env = System.getenv();
            String dbHost = env.get("DB_HOST");
            String dbUser = env.get("DB_USER");
            String dbPassword = env.get("DB_PASSWORD");
    
            Connection conn = DriverManager.getConnection(dbHost, dbUser, dbPassword);
            return conn;
        }
    
    }
    • DB커넥션 생성 기능을 다른 클래스로 완전히 독립시켰다.

    • 더 이상 상속을 이용한 확장 방식을 사용할 필요가 없으니 추상 클래스로 만들 필요가 없다.

      //UserDAO
      public class UerDAO {
          private SimpleConnectionMaker simpleConnectionMaker;
      
          public UerDAO() {
              **simpleConnectionMaker = new SimpleConnectionMaker();**
      				//상태를 관리하는게 아닌 한 번만 만들어 인스턴스 변수에 저장해두고
      				//메소드에서 사용하게 한다.
          }
      
          public void add(User user) throws SQLException {
              Connection conn = simpleConnectionMaker.makeNewConnection();
              PreparedStatement ps = conn.prepareStatement("INSERT INTO user(id, name, password) VALUES (?, ?, ?)");
      
              ps.setString(1, user.getId());
              ps.setString(2, user.getName());
              ps.setString(3, user.getPassword());
      
              ps.executeUpdate();
              ps.close();
              conn.close();
          }
      
          public User get(String id) throws SQLException {
              Connection conn = simpleConnectionMaker.makeNewConnection();
              PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
              ps.setString(1, id);
      
              ResultSet rs = ps.executeQuery();
      
              rs.next();
              User user = new User();
              user.setId(rs.getString("id"));
              user.setName(rs.getString("name"));
              user.setPassword(rs.getString("password"));
      
              ps.close();
              conn.close();
              rs.close();
      
              return user;
          }
      }

문제점

  • 상속을 사용해서 DB 커넥션 기능을 확장해서 사용하게 했던 것이 불가능해짐
  • 다른 방식으로 DB커넥션을 제공하는 클래스를 사용하기 위해서는 코드 수정이 불가피함

인터페이스의 도입

  • 위와같은 문제점을 해결하기 위해서는 두 개의 클래스가 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어 주도록 추상화를 진행한다.
  • 추상화란? 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이며 자바에서는 인터페이스를 통해 구현할 수 있다.
  • UserDao는 자신이 사용할 클래스가 어떤 것인지 몰라도 된다. 단지 인터페이스를 통해 원하는 기능을 사용하기만 하면 된다.
  • 코드
    //ConnectionMaker **(커넥션 인터페이스)**
    public interface ConnectionMaker {
        public Connection makeConnection() throws SQLException, ClassNotFoundException;
    }
    **//인터페이스 구현체**
    //우리가 사용할 awsConnectionMaker
    public class AWSConnectionMaker implements ConnectionMaker{
        @Override
        public Connection makeConnection() throws SQLException {
            Map<String, String> env = System.getenv();
            String dbHost = env.get("DB_HOST");
            String dbUser = env.get("DB_USER");
            String dbPassword = env.get("DB_PASSWORD");
    
            Connection conn = DriverManager.getConnection(dbHost, dbUser, dbPassword);
            return conn;
        }
    }
    
    //예시로 구현한 LocalConnectionMaker
    public class LocalConnectionMaker implements ConnectionMaker {
        @Override
        public Connection makeConnection() throws SQLException, ClassNotFoundException {
            return null;
        }
    }
    //UserDao
    public class UserDao {
        private ConnectionMaker connectionMaker;
    		//인터페이스를 통해 오브젝트에 접근하므로 구체적인 클래스 정보를 알 필요가 없다.
    
        public UserDao(ConnectionMaker connectionMaker) {
            this.connectionMaker = new AWSConnectionMaker;
    				
        }
    
        public void add(User user) throws SQLException, ClassNotFoundException {
            **Connection conn = connectionMaker.makeConnection();
    				//인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀐다해도 메소드
    				// 이름이 변경될 걱정이 없다**
            PreparedStatement ps = conn.prepareStatement("INSERT INTO user(id, name, password) VALUES (?, ?, ?)");
    
            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());
    
            ps.executeUpdate();
            ps.close();
            conn.close();
        }
    
        public User get(String id) throws SQLException, ClassNotFoundException {
            Connection conn = connectionMaker.makeConnection();
            PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
            ps.setString(1, id);
    
            ResultSet rs = ps.executeQuery();
    
            rs.next();
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
    
            ps.close();
            conn.close();
            rs.close();
    
            return user;
        }
    
        public void deleteAll() throws SQLException, ClassNotFoundException {
            Connection conn = connectionMaker.makeConnection();
            PreparedStatement ps = conn.prepareStatement("DELETE FROM users");
            ps.executeUpdate();
    
            conn.close();
            ps.close();
        }
    
        public void getCount() throws SQLException, ClassNotFoundException {
            Connection conn = connectionMaker.makeConnection();
            PreparedStatement ps = conn.prepareStatement("SELECT COUNT(*) FROM users");
    
            ResultSet rs = ps.executeQuery();
            rs.next();
            int count = rs.getInt(1);
    
            rs.close();
            ps.close();
            conn.close();
        }
    }

문제점

  • 여전히 AWSConnectionMaker 클래스의 생성자를 호출해서 오브젝트를 생성하는 코드과 남아있다.

관계설정 책임의 분리

  • 위와 같이 여전히 UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 코드가 남아 있다.
  • UserDAO와 UserDAO가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주어야 한다.
  • UserDao 클라이언트에서 UserDao.를 사용하기 전에, 먼저 UserDao가 어떤 클래스를 사용할지를 결정하도록 만든다. 이는 다형성을 통해 구현할 수 있다.
  • 여기에서 다형성이란? 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있다.
  • 이러한 오브젝트의 관계를 조립해주는 게 바로 클라이언트의 책임이다.
  • 코드
    @Test
    void 관계설정 책임의 분리() throws SQLException, ClassNotFoundException {
        ConnectionMaker connectionMaker = new AWSConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
    위의 예제는 test코드로 클라이언트를 대체하였다.

적용한 방법

  • 개방 패쇄 원칙(OCP)
    • 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
    • EX) DB 커넥션을 교체하는 등 UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있고, 인터페이스를 이용하여 불필요한 변화는 일어나지 않도록 막아놓았다.
  • 높은 응집도
    • 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다.
    • EX) AWSConnectionMaker를 일부 수정하더라도 어디에서 무엇을 변경할지 명확하며 다른 코드를 변경하지 않아도 된다.
  • 낮은 결합도
    • 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도
    • 느슨하게 연결된 형태를 유지하는 것이 바람직하다.
    • EX) AWSConnectionMaker이 아닌 LocalConnectionMaker로 변경한다 하더라도 클라이언트의 파라미터 값만 변경해주면 된다.

제어의 역전(loC)

  • Test모듈을 보게 되면 기능적인 테스트 외에도 오브젝트의 연결을 하는 다른 책임까지 떠맡고 있다.
  • 이를 해결하기 위해서 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺어주는 팩토리를 만든다.
  • 팩토리란? 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것이다.
  • UserDao는 실제적인 로직 코드라면, DaoFactory는 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할을 한다고 생각하면 된다. 또는 만들어진 컴포넌트들을 조립한다고 생각하면 된다.
  • 코드
    public class UserDaoFactory {
    
        public UserDao awsUserDao(){
            AWSConnectionMaker awsConnectionMaker = new AWSConnectionMaker();
            UserDao userDao = new UserDao(awsConnectionMaker);
            return userDao;
        }
    
        public UserDao localUserDao(){
            UserDao userDao = new UserDao(new LocalConnectionMaker());
            return userDao;
        }
    }

제어의 역전

  • 제어의 역전이란? 프로그램의 제어 흐름 구조가 뒤바뀌는 것
  • 원래는 클라이언트가 가져야 하는 제어 권한을 자신이 아닌 다른 대상에게 위임했다.
  • 제어권을 상위 템플릿 메소드에 넘기고 자신은 필요할 때 호출되어 사용되도록 한다.
  • ex) Test에서 어떤 오브젝트를 만들지, 언제 사용할지 관장했던 구조가 현재는 UserDaoFactory에 의해 제어당하고 있다
  • ex) 템플릿 메소드 패턴, 프레임워크
💡 **프레임워크 vs 라이브러리 차이** 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크다. 반면에, 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 라이브러리이다.

dependency 추가

  • build.gradle > Alt + insert
  • spring-boot-starter-jdbc, spring-boot-starter-test 추가

스프링 loC의 용어 정리

  • 빈(bean): IoC방식으로 관리하는 오브젝트 라는 뜻이다. 스프링이 직접 그 생성과 제어를 담당하는 오브젝트를 빈이라고 부른다.
  • 빈 팩토리(bean factory): 스프링의 IoC를 담당하는 핵심 컨테이너. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외에 부가적인 빈을 관리하는 기능을 담당한다.
  • 애플리케이션 컨텍스트(application context): 빈 팩토리를 확장한 IoC컨테이너이다. 빈을 등록화고 관리하는 기본적인 기능은 빈 팩토리와 동일하다.
  • 설정 정보(Configuration): 스프링의 설정정보란 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다.
  • 코드
    public class UserDaoFactory {
    
        @Bean
        public UserDao awsUserDao(){
            AWSConnectionMaker awsConnectionMaker = new AWSConnectionMaker();
            UserDao userDao = new UserDao(awsConnectionMaker);
            return userDao;
        }
    
        @Bean
        public UserDao localUserDao(){
            UserDao userDao = new UserDao(new LocalConnectionMaker());
            return userDao;
        }
    }

@Configuration : Bean Factory를 위한 Object 설정을 담당하는 Class를 인식시켜줌. 이 annotation이 붙은 Class를 스캔해서 ApplicationContext에 Bean을 등록.

@Bean : Object를 만들어 주는 Method에 붙임.

getBean : ApplicationContext에 등록된 Bean중 해당되는 것이 있다면, Bean 생성 Method를 호출하여 Object를 생성시켜 가져옴. "awsUserDao" : 찾고자 하는 Bean의 이름, UserDao.class : 원하는 리턴 타입. (기본적으로 Object 타입으로 리턴)

다만, getBean을 두번 이상 사용 했을 때, Object가 새로 생성 되는 것이 아니라, 최초에 생성된 Object를 리턴한다. 이를 Singleton pattern이라 한다.

Bean애너테이션을 통해 스프링에 등록했다.

```java
@ExtendWith(SpringExtension.class)
// Junit에서 Spring ApplicationContext를 쓸 수 있게 해주는 기능 
@ContextConfiguration(classes = UserDaoFactory.class)
// Junit5 Test코드를 실행할 때 ApplicationContext에 들어갈 설정 정보(관계 설정)를 불러오게 해주는 기능
class UerDAOTest {

    @Autowired
    ApplicationContext context;

    @Test
    void 테스트() throws SQLException, ClassNotFoundException {
        UserDao dao = context.getBean("awsUserDao", UserDao.class);
        String id = "23";

        dao.add(new User(id,"Suhwan","789456123"));

        User selectedUser = dao.get(id);
        Assertions.assertEquals("Suhwan",selectedUser.getName());

    }

}
```

ExtendWith(SpringExtension.class) : Junit에서 Spring ApplicationContext를 쓸 수 있게 해주는 기능

ContextConfiguration : Junit5 Test코드를 실행 할 때 ApplicationContext에 들어갈 설정 정보(관계 설정)를 불러오게 해주는 기능

싱글톤 패턴

  • DaoFactory를 직접 사용하는 것과 @Configuration 애너테이션을 추가해서 스프링의 애플리케이션 컨텍스트를 통해 사용하는 것은 결과만 보자면 동일하지만 중요한 차이점이 있다.

  • 직접 생성한 DaoFactory 오브젝트 → 호출될 때마다 달라지는 값

  • 스프링 컨텍스트에서 호출한 오브젝트 → 항상 같은 값

  • 즉, 스프링은 기본적으로 싱글톤 개념을 사용하기 때문이다.

  • 싱글톤이란? 오브젝트를 한번 생성 해놓고 계속 사용하는 것을 말한다.

  • 사용이유는? 계속 인스턴스를 생성하게 되면 서버에 부하가 온다. 서버의 부하를 줄이기 위해 사용한다.

  • 싱글톤 패턴의 특징

    • 클래스 밖에서는 오브젝트를 생성하지 못하도록 private 로 만듦
    • 메소드가 최초로 호출되는 시점에서 한번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 스테틱 필드에 저장된다.
    • 한번 오브젝트가 만들어지고 난 후에는 getInstance() 메소드를 통해 이미 만들어져 스테틱 필드에 저장해둔 오브젝트를 넘겨준다.
  • 싱글톤 패턴의 단점

    • private 생성자를 갖고 있기 때문에 상속할 수 없다.
    • 싱글톤은 테스트하기 힘들다.
    • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
profile
작성자 개인이 잊을 때마다 보라고 정리한 글

0개의 댓글