ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [토비의 스프링] 1장 : 오브젝트와 의존관계
    책/토비의 스프링 3.1 2024. 2. 24. 23:12
    반응형

    1.1 초난감 DAO

    1.1.1 User

    public class User {
        String id;
        String name;
        String password;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    }

    1.1.2 UserDao

    public class UserDao {
        public void add(User user) throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection(
                    "jdbc:mysql://localhost/springbook", "spring", "book");
    
            PreparedStatement ps = c.prepareStatement(
                    "insert into users(id, name, password) values (?,?,?)"
            );
            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());
    
            ps.executeUpdate();
            ps.close();
            c.close();
        }
    
        public User get(String id) throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection(
                    "jdbc:mysql://localhost/springbook", "spring", "book");
    
            PreparedStatement ps = c.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"));
    
            rs.close();
            ps.close();
            c.close();
    
            return user;
        }
    }

    1.1.3 main()을 이용한 DAO 테스트 코드

    public static void main(String[] args) throws Exception {
            UserDao dao = new UserDao();
    
            User user = new User();
            user.setId("choijaewon");
            user.setName("최재원");
            user.setPassword("choijaewon");
    
            dao.add(user);
    
            System.out.println(user.getId() + " 등록 성공");
    
            User user2 = dao.get(user.getId());
            System.out.println(user2.getName());
        }
    • UserDao 코드는 문제점이 많다.

    1.2 DAO의 분리

    1.2.1 관심사의 분리

    • 객체지향에서는 객체의 설계와 코드가 항상 변함
    • 그래서 변화의 폭을 최소한으로 줄여줘야 함
    • 분리확장을 고려한 설계
    • 분리
      • 관심이 같은 것끼리는 모으고, 다른 것은 떨어져 있게 → 관심사의 분리

    1.2.2 커넥션 만들기의 추출

    • add() 메소드 안의 세가지 관심사항
      • DB와 연결을 위한 커넥션을 어떻게 가져올 지
      • SQL 문장을 담을 Statement를 만들고 실행하는 것
      • 작업 후 Statement와 Connection 닫기

    중복 코드의 메소드 추출

    • 중복된 DB 연결 코드를 getConnection() 으로 분리
    private Connection getConnection() throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection(
                    "jdbc:mysql://localhost/springbook", "spring", "book"
            );
    
            return c;
        }
    
        public void add(User user) throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = getConnection();
    
            PreparedStatement ps = c.prepareStatement(
                    "insert into users(id, name, password) values (?,?,?)"
            );
            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());
    
            ps.executeUpdate();
            ps.close();
            c.close();
        }
    • 나중에 DB 종류, URL 등이 바뀌어도 getConnection() 만 수정하면 됨

    변경사항에 대한 검증: 리팩토링과 테스트

    • 리팩토링 : 기능에는 변화를 주지 않고 코드의 구조를 바꾸는 것

    1.2.3 DB 커넥션 만들기의 독립

    • 만약 DB 커넥션을 가져오는데 있어 독자적으로 만든 방법을 적용하고 싶다면?
      • 그리고 컴파일된 클래스 바이너리 파일만 제공하고 싶다면?

    상속을 통한 확장

    • UserDao 클래스를 추상 클래스로 만든다.
    • getConnection() 을 추상 메소드로 만들고, 이를 사용하는 측에서 구현하도록 만든다.
    public void add(User user) throws ClassNotFoundException, SQLException {
            Connection c = getConnection();
            ...
        }
    
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
    
    
    class NUserDao extends UserDao {
        public Connection getConnection() throws ClassNotFoundException, SQLException {
    
        }
    }
    • 슈퍼클래스에 기본 로직 흐름을 만들고 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드로 만들고 서브클래스에서 알맞게 구현하는 방법 → 템플릿 메소드 패턴
    • 서브클래스에서 구체적인 객체 생성 방법을 결정하게 하는 것 → 팩토리 메소드 패턴
    • 단점
      • 자바에서는 다중 상속이 불가능 → 이미 UserDao가 상속을 사용하고 있다면?
      • 상속은 생각보다 밀접한 관련 → 슈퍼클래스 내부 변경 시 모든 서브클래스에 영향이 감

    1.3 DAO의 확장

    1.3.1 클래스의 분리

    • 이번에는 아예 독립적인 클래스로 만든다.
    public class UserDao {
    
        private SimpleConnectionMaker simpleConnectionMaker;
    
        public UserDao() {
            simpleConnectionMaker = new SimpleConnectionMaker();
        }
    
        public void add(User user) throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = simpleConnectionMaker.makeNewConnection();
            }
    }
    
    class SimpleConnectionMaker {
        public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection(
                    "jdbc:mysql://localhost/springbook", "spring", "book"
            );
    
            return c;
        }
    }
    • UserDao의 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되게 됨 → 코드 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없음

    1.3.2 인터페이스의 도입

    • 두 개의 클래스가 서로 긴밀하게 연결되지 않도록 중간에 추상적인 느슨한 연결고리 삽입
    • 인터페이스 → 자신을 구현한 클래스에 대한 구체적 정보를 감춤
      • 이를 통해 구현 클래스가 바뀌어도 신경쓸 일이 없음
    interface ConnectionMaker {
        Connection makeConnection() throws ClassNotFoundException, SQLException;
    }
    
    class DConnectionMaker implements ConnectionMaker {
        public Connection makeConnection() throws ClassNotFoundException, SQLException {
    
        }
    }
    public class UserDao {
    
        private ConnectionMaker connectionMaker;
    
        public UserDao() {
            connectionMaker = new DConnectionMaker();
        }
    
        public void add(User user) throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = connectionMaker.makeConnection();
            }
    }
    • N사와 D사가 DB 접속용 클래스를 다시 만든다 해도 UserDao의 코드를 고칠일은 없음
    • 그러나 여전히 생성자에서 DConnection 클래스의 생성자를 직접 호출한다.
    • 결국 UserDao를 수정해야만 가능한 경우가 되었다.

    1.3.3 관계설정 책임의 분리

    • 위의 문제는 UserDao가 어떤 ConnectionMaker 구현 클래스를 이용할지를 결정하게 만드는 관심 사항이 존재하고 있기 때문이다.
    • UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안되게 만들어야함
    • UserDao 오브젝트가 동작하려면 특정 클래스의 오브젝트와 관계를 맺어야 하긴 함
      • 위의 경우 코드에 특정 클래스 이름이 들어감 → 클래스 간의 관계가 형성된 것
      • 오브젝트간의 관계는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면 사용 가능 → 다형성
    public UserDao(ConnectionMaker connectionMaker) {
            this.connectionMaker = connectionMaker;
        }
    • UserDao의 클라이언트에게 구현 클래스를 선택하는 책임을 넘김

    1.3.4 원칙과 패턴

    개방 폐쇄 원칙

    • OCP (Open-Closed Principle) : 클래스, 모듈은 확장에는 열려있어야 하고 변경에는 닫혀 있어야 함

    높은 응집도와 낮은 결합도

    • 응집도가 높다 → 하나의 모듈, 클래스가 하나의 책임 또는 관심사에 집중
    • 결합도 → 하나의 객체가 변경될 때 관계를 맺고 있는 다른 객체에게 변화를 요구하는 정도

    전략 패턴

    • 자신의 기능 맥락(Context)에서 변경이 필요한 알고리즘을 인터페이스로 통째로 분리시키고, 구현 클래스를 필요에 따라 바꿔서 사용하는 디자인 패턴

    1.4 제어의 역전 (IoC)

    1.4.1 오브젝트 팩토리

    • UserDaoTest 클라이언트 코드는 얼떨결에 구현 클래스를 지정하는 역할까지 맡게 되었다.
    • 원래의 책임은 UserDao를 테스트하는 것이므로, 이 부분을 분리해줘야 한다.

    팩토리

    • 객체의 생성 방법을 결정하고 만든 객체를 리턴하는 클래스
    public class DaoFactory {
        public UserDao userDao() {
            ConnectionMaker connectionMaker = new DConnectionMaker();
            UserDao userDao = new UserDao(connectionMaker);
    
            return userDao;
        }
    }
    
    UserDao dao = new DaoFactory().userDao();
    • UserDaoTest에서는 이제 DaoFactory로부터 UserDao를 받아 실행 → UserDao를 어떻게 만들고 어떻게 초기화하는지 관심을 가지지 않아도 됨

    1.4.2 오브젝트 팩토리의 활용

    • 만약 다른 DAO가 만들어지는 경우 중복된 코드가 발생 가능
    • 거기서 또 분리
    public class DaoFactory {
        public UserDao userDao() {
            return new UserDao(connectionMaker());
        }
    
        public AccountDao accountDao() {
            return new AccountDao(connectionMaker());
        }
    
        public ConnectionMaker connectionMaker() {
            return new DConnectionMaker();
        }
    }

    1.4.3 제어권의 이전을 통한 제어관계 역전

    • 제어의 역전 : 객체가 자신이 사용할 객체를 스스로 선택하지 않고 생성하지도 않음
    • 라이브러리와 프레임워크의 차이 : 라이브러리를 사용하면 어플리케이션 코드 흐름을 직접 제어 but 프레임워크는 코드가 프레임워크에 의해 사용됨

    1.5 스프링의 IoC

    1.5.1 오브젝트 팩토리를 이용한 스프링 IoC

    • 빈(Bean) : 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 객체
    • 이러한 빈의 생성 및 제어를 담당하는 IoC 오브젝트 → 빈 팩토리(Bean Factory) 혹은 Application Context

    1.5.2 애플리케이션 컨텍스트의 동작방식

    • DaoFactory와 달리 ApplicationContext 에는 직접 객체를 생성하고 관계를 맺는 코드가 없고, 별도의 설정 (@Configuration) 정보를 통해 얻음
    • @Bean 이 붙은 메소드의 이름을 가져와 bean 목록을 만들어둠
    • ApplicationContext를 사용했을 때의 장점
      • 구체적인 팩토리 클래스를 알 필요 없음
      • 종합 IoC 서비스 제공
      • 빈을 검색하는 다양한 방법 제공

    1.6 싱글톤 레지스트리와 오브젝트 스코프

    • DaoFactory를 사용할 경우 dao를 계속 만들 때 계속 다른 객체가 나온다.
    • 그러나 ApplicationContext로 계속 만들면 똑같은 객체가 나온다

    1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트

    • 스프링에서는 별다른 설정 안하면 싱글톤으로 빈 객체를 생성

    서버 애플리케이션과 싱글톤

    • 서버 환경에서는 매번 요청이 올 때마다 객체를 새로 만들면 오버헤드가 너무 큼
    • 그래서 엔터프라이즈 분야에서 서블릿은 멀티스레드 환경에서 싱글톤으로 동작
      • 서블릿 클래스당 하나의 객체만 만들고, 여러 스레드에서 하나의 오브젝트를 공유해 사용

    싱글톤 패턴의 한계

    • 자바를 이용한 싱글톤의 한계
      • private 생성자 → 상속 불가능
      • 테스트가 힘듦
      • 서버환경에서는 싱글톤이 하나임을 보장하기 어려움 → 여러 JVM에 분산되어있는 경우 각각 독립적으로 싱글톤이 생성됨
      • 전역 상태를 만들 수 있어 바람직하지 않음

    싱글톤 레지스트리

    • 이러한 자바의 싱글톤 구현 방식 문제를 해결 → 싱글톤 레지스트리

    1.6.2 싱글톤과 오브젝트의 상태

    • 멀티스레드 환경에서 싱글톤은 상태정보를 가지고 있지 않은 무상태 방식으로 만들어야함
    • 이 때 각 요청에 대한 정보, DB, 서버의 리소스로부터 만든 정보는 어떻게?
      • 파라미터, 로컬 변수, 리턴 값 등을 이용
      • 이러한 값들은 스택에 저장 → 각각의 독립적인 공간에서 사용됨
    public class UserDao {
    
        private ConnectionMaker connectionMaker;
        private Connection c;
        private User user;
    
        public UserDao(ConnectionMaker connectionMaker) {
            this.connectionMaker = connectionMaker;
        }
    
        public User get(String id) throws ClassNotFoundException, SQLException {
            Class.forName("com.mysql.jdbc.Driver");
            c = connectionMaker.makeConnection();
    
            PreparedStatement ps = c.prepareStatement(
                    "select * from users where id = ?"
            );
            ps.setString(1, id);
    
            ResultSet rs = ps.executeQuery();
            rs.next();
    
            user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
    
            rs.close();
            ps.close();
            c.close();
    
            return user;
        }
    }
    • 위의 코드에서 Connection과 User 인스턴스 변수는 심각한 문제를 발생시킬 수 있다.
    • 이러한 개별적으로 바뀌는 정보는 모두 로컬 변수로 사용해야 한다.
    • connectionMaker는 왜 가능?
      • 읽기 전용의 정보이고, 스프링의 의존성 주입으로 받는 객체로써 스프링의 관리를 받기 때문
    • 읽기 전용의 인스턴스 변수의 경우 final을 붙여주자.

    1.6.3 스프링 빈의 스코프

    • 빈이 생성되고, 존재하고, 적용되는 범위 → 스코프 (Scope)
    • 싱글톤 : 컨테이너 내에 한 개의 객체만 만들어지고, 컨테이너가 존재하는 동안 계속 유지

    1.7 의존관계 주입 (DI)

    1.7.1 제어의 역전(IoC)과 의존관계 주입

    • 스프링 IoC 기능의 대표 동작 원리는 의존관계 주입 (DI)이다.

    1.7.2 런타임 의존관계 설정

    의존관계

    • UML 모델에서 A가 B에 의존할 경우 점선 화살표로 A → B라고 표현
    • 의존 - B가 변하면 A에 영향을 미침
    • 의존관계에는 방향성이 있음 → B는 A의 변화에 영향을 받지 않음

    UserDao의 의존관계

    • UserDao가 ConnectionMaker를 사용하므로, UserDao → ConnectionMaker라고 할 수 있음
    • 하지만 ConnectionMaker를 구현한 클래스가 바뀌는 것은 UserDao에 영향을 주지 못함
      • 인터페이스에 대해서만 의존 관계를 만들면 결합도를 낮출 수 있음
    • 런타임 의존관계도 존재 - 런타임 시에 의존관계를 맺는 실제 사용 대상인 객체를 의존 오브젝트라고 함
    • 의존 관계 주입 (DI) : 구체적인 의존 오브젝트, 그것을 사용할 주체를 런타임 시에 연결해주는 작업

    1.7.3 의존관계 검색과 주입

    • 스프링은 외부로부터 주입이 아니라 스스로 검색을 이용하게도 만들 수 있음 → 의존관계 검색 (Dependency Lookup)
    • ApplicationContext는 getBean() 메소드를 제공하여 의존관계 검색
    • 검색 vs 주입
      • 검색 방법의 경우 스프링 API가 나타남 → 스프링에 의존하는 코드를 만들게 됨
      • 그래서 주입이 나음
    • 검색의 경우 검색하는 객체가 스프링의 빈일 필요가 없음 → 어딘가에서 new로 생성해서 사용해도 됨
    • 그러나 주입은 자기 자신이 컨테이너가 관리하는 빈이어야함

    1.7.4 의존관계 주입의 응용

    기능 구현의 교환

    // 개발 시
    @Bean
    public ConnectionMaker connectionMaker() {
        return new LocalDBConnectionMaker();
    }
    
    // 배포 시    
    @Bean
    public ConnectionMaker connectionMaker() {
        return new ProductionDBConnectionMaker();
    }
    • Configuration 파일만 손보면 다른 코드는 손대지 않고 DB를 바꿀 수 있음

    부가기능 추가

    • 만약 DB 연결횟수를 카운팅하고 싶다면?
      • DAO와 DB 커넥션을 만드는 객체 사이에 연결횟수를 카운팅하는 객체를 하나 더 추가
    public class CountingConnectionMaker implements ConnectionMaker {
        int counter = 0;
        private ConnectionMaker realConnectionMaker;
    
        public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
            this.realConnectionMaker = realConnectionMaker;
        }
    
        @Override
        public Connection makeConnection() throws ClassNotFoundException, SQLException {
            this.counter++;
    
            return realConnectionMaker.makeConnection();
        }
    
        public int getCounter() {
            return counter;
        }
    }
    • UserDao → CountingConnectionMaker → DConnectionMaker 로 런타임 의존관계가 형성됨
    • @Configuration 파일을 수정함으로써 UserDao의 ConnectionMaker를 CountingConnectionMaker로 주입시킴
    • CountingConnectionMaker의 실제 사용은 클라이언트 코드에서 직접 getBean으로 호출하고, getCounter() 를 호출하면서 이루어진다.

    1.7.5 메소드를 이용한 의존관계 주입

    • setter를 이용해서 주입받거나, 일반 메소드를 이용해서 주입받을 수도 있다.

     

     

    https://product.kyobobook.co.kr/detail/S000000935360

    반응형
Designed by Tistory.