[토비의 스프링 3.1] 1장. 오브젝트와 의존관계

    Source : yes24

    1. 해당 포스팅은 책읽기 스터디의 활동을 통해 작성된 포스팅입니다.
    2. 공부하면서 블로그를 참고하였는데, 책 내용을 그대로 정리하는데 그치는 글들이 절반이었습니다. 스스로 새롭게 알게 된 내용이거나 책의 설명이 너무 불친절한 경우 부가설명을 작성하거나 또는 새롭게 쓰고자 노력했습니다. 공부하다 생기는 의문들은 레포지토리 이슈에서 질의응답을 주고 받았으니 학습하다 궁금한 점이 생기면 검색 해보시기를 권장드립니다.
    3. 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다.
    4. 코드나 책 내용 캡쳐 내용들은 다른 블로그의 캡쳐본이나 텍스트를 가져와 작성하였습니다. 대부분 출처를 표기하였으나 누락된 경우 원하시는 조치 내용을 댓글로 남겨주시면 시정하겠습니다.

    스프링의 핵심 철학

    스프링이 자바에서 가장 중요하게 생각하는 가치는 객체지향이다. 객체지향 프로그래밍의 혜택을 누릴 수 있도록 오브젝트에 많은 관심을 둔다. 어플리케이션에서 오브젝트의 생성, 관계, 사용, 소멸의 전 과정을 이해하고, 오브젝트의 설계와 사용되는 과정과 가치에 대해서도 고민해봐야 한다.

    스프링은 설계와 구현에 특정한 방법이나 모델을 따르도록 강요하지 않지만, 오브젝트의 효과적 설계와 구현, 사용에 대한 명쾌한 기준을 제공하여 평범한 개발자도 자연스럽고 손쉽게 사용하도록 프레임워크 형태로 제공된다.

    1.1 초난감 DAO

    DAO(Data Access Object)는 DB에 접근하여 데이터를 조회/조작하는 기능을 전담하는 오브젝트를 말한다.

    사용자 정보를 JDBC API를 통해 저장하고 조회하는 간단한 DAO를 작성한다.

    CODE #1 : 사용자 정보를 저장할 User 클래스(Data Transfer Object)

    public class User {
        private String id;
        private String name;
        private 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;
        }
    }

    Java Beans : 아래 2가지 관례를 따라 만들어진 오브젝트를 가르킨다.

    • Default Constructor(기본 생성자 : 파라미터가 0개인 생성자)를 가지고 있어야 한다.
    • 이름을 가진 속성을 Property라고 하며, get+(property name), set+(property name) 이름을 가진 setter와 getter 메서드를 통해 오브젝트의 값을 조회/수정할 수 있다. 예를 들면 getName(), setAge(16) 등의 형태로 사용한다.

    다수의 파라미터를 받는 Constructor로 객체 생성을 하는 방식은 만약 파라미터의 순서나 갯수가 바뀌면 유지보수에 어려움을 겪을 수 있으며 파라미터의 갯수가 많을 경우 개발자가 파라미터의 순서를 착각하는 등의 실수를 하기 쉽고 몇 번째 파라미터가 어떤 의미를 갖는 지 한 눈에 알기가 어려우며, 모든 파라미터가 필수가 아닌 경우에는 가능한 모든 경우의 파라미터 조합을 모두 구성하여야 하기 때문에 코드의 길이가 길어진다.

    이에 비해 Java Beans 패턴은 좀 더 변화에 쉽게 대응할 수 있으며, 가독성이 좋아진다.

    CODE #2 : User 오브젝트를 DB에 넣고 관리하는 DAO 클래스

    public class UserDao {
        public void add(User user) throws ClassNotFoundException, SQLException {
            Class.forName("org.h2.Driver");
            Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/springbook", "sa", "");
    
            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("org.h2.Driver");
            // Connection을 사용하기 위해선 해당 DB의 드라이버가 클래스패스에 있어야 한다.
            // MySQL의 경우 mysql-connector-java-{version}-bin.jar가 될 것이다.
            Connection c = DriverManager.getConnection("jdbc:h2:tcp://localhost/~/springbook", "sa", "");
    
            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;
        }
    }

    JDBC는 자바에서 DB에 접근할 수 있도록 제공하는 API이다.

    • DB 연결을 위해 Connection을 가져온다.
    • SQL을 담은 Statement or PreparedStatement를 만든다.
    • Statement를 실행한다.
    • SQL 쿼리의 실행결과를 ResultSet으로 받아서 DTO에 옮겨준다. (여기서는 User 오브젝트)
    • 작업 중에 생성된 리소스들은 close() 메서드로 닫아주어야 한다.
    • JDBC API가 만드는 예외를 처리하거나 상위 메서드에 throws로 던지도록 한다.

    위와 같이 코드를 작성했다면, 제대로 동작하는지 확인하는 방법은 무엇이 있을까? 간단한 웹 어플리케이션을 만들어 직접 부라우저로 DAO 동작여부를 확인할 수 있다. 그러나 테스트 과정이 너무 험난하다. DAO를 이용하는 웹앱 작성과 프론트 작성하고, 웹 브라우저로 매번 DAO 기능을 사용하면서 정보를 일일이 타이핑하는 방법은 효율적이지 못하다.

    main()을 이용한 DAO 테스트 코드 작성하기

    main 메서드를 만들어 DAO의 add(), get() 메서드가 잘 동작하는지 확인해보자. add() 메서드로 DB에 유저 정보를 등록한 다음, get() 메서드로 DB에 저장된 정보를 가져오도록 해보자.

    Code #3 : DAO 테스트 코드

    public class UserDaoTest {
        public static void main(String[] args) throws ClassNotFoundException, SQLException {
            UserDao dao = new UserDao();
    
            User user = new User();
            user.setId("daebalprime");
            user.setName("김대발");
            user.setPassword("1a2w3e4r!");
    
            dao.add(user);
    
            System.out.println(user.getId() + " 등록 성공");
    
            User user2 = dao.get(user.getId());
            System.out.println(user2.getName());
            System.out.println(user2.getPassword());
            System.out.println(user2.getId() + " 조회 성공");
        }
    }

    DAO를 작성하고, 테스트 코드를 통해 문제없이 작동하는 것도 확인했다. 그러나 문제가 많은 DAO 코드라고 할 수 있다. 동작이 잘 되는데 왜 문제가 많은 코드냐고 반발심이 들 수도 있을 것이다. 작동이 잘 되는데 개선해야 하는 이유는 무엇일까? 장점은 무엇일까? 그 장점이 가져올 당장의, 미래의 이점은 무엇일까? 스프링을 공부한다는 것은 이 질문에 답을 찾아나가는 과정이다.

    1.2 DAO의 분리

    객체지향에선 모든 것이 변할 수 있다. 현실에 기반한 요구사항으로 코드를 작성한다. 현실은 항상 바뀌고 요구사항은 발전한다. 어플리케이션이 사용하는 기술도, 어플리케이션이 구동되는 환경도 바뀐다. 그래서 미래의 변화를 염두에 두고 코드를 작성해야한다. 객체지향 패러다임이 성공적으로 정착한 이유는, 절차지향 패러다임에 비해 실세계를 추상화하여 모델링하기 좋다는 장점도 있지만 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는데 장점이 있으며 Java와 Spring이 객체지향 패러다임을 따르고 효과적으로 이용하기에 우리는 이에 따라야 한다.

    요구사항의 변화로 기능 변경을 적용해야 한다고 생각해보자. 미래지향적으로 코드를 작성했다면 변경사항을 적용하고 검증하는데 긴 시간이 걸리지 않지만, 만약 현재 구현에 급급해 코드를 작성했다면 변경에 오랜 시간이 걸릴 수도 있고, 변경사항이 다른 잘 작동하는 컴포넌트에 문제를 일으키지 않으리란 보장도 쉽게 할 수 없다.

    그렇다면 어떻게 변화에 잘 대비할 수 있을까? 분리와 확장을 고려한 설계가 그 답이다.

    분리

    변경은 여러 영역에 걸쳐 폭넓게 일어나지 않는다. 'DB를 바꾸면서 프론트의 레이아웃을 바꾸며 Ajax를 적용하고 트랜잭션이 일어날 때 특정조건 하에서 정보가 웹 서비스로 전송되는 동시에 ...'와 같이 일어나지 않는다는 말이다. 모든 변경은 한 번에 한 가지 관심사항에 집중해서 일어난다. 'DB 비밀번호가 바뀌었으니 접속이 원활하도록 수정하라'와 같은 방식으로 말이다.

    그러나 하나의 관심사항에 대한 변경점 때문에 코드의 수백군데를 수정하는 일이 일어나선 안된다. 변화가 한 번에 하나의 관심사에 집중된다면, 관심이 같은 것 끼리는 모으고, 다른 것은 분리하여야 한다. 관심사의 분리(Separation of Concerns)의 개념이 등장한다.

    위의 DAO 코드의 관심사항을 분리해보자.

    1. DB와 연결을 위한 Connection을 가져오는 것을 관심사로 본다. 어떤 DB를 쓰고, ID와 PW는 무엇이고, 커넥션을 생성하는 방법까지 세부적으로 나눌 수 있지만, 'DB와 연결한다'로 관심사를 정하자.
    2. DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것을 관심사로 본다. 물론 Statement에 파라미터로 넘어온 User를 바인딩하고 Statement에 있는 SQL을 실행시키는 것으로 나눌 수 있지만 일단은 SQL 문장을 실행하는 것을 하나의 관심사로 본다.
    3. 사용이 끝난 리소스를 닫는 것을 하나의 관심사로 본다.

    리팩토링 : 메서드 추출

    get()과 add() 메서드에 Connection 코드가 중복되어 있다. 지금은 메서드의 갯수가 적어 괜찮을지 모르나, 나중에 수십 수백 개의 메서드를 가지는 상황에서 반복적인 코드를 작성한다면, 접속 비밀번호를 바꾸는 사소한 변경점 하나도 쉽지 않은 작업이 될 것이다. 중복되는 코드를 하나의 메서드로 추출한다. 기능의 변화는 없지만 중복되어 등장하는 관심사항에 대한 코드를 하나의 메서드로 추출하여 재구성해 변화에 대응이 쉬워졌다. 이런 과정을 리팩토링이라고 한다.

    Code #4 : 메서드 추출로 중복되는 코드 제거하기

        public void add(User user) throws ClassNotFoundException, SQLException {
            Connection c = getConnection();
            //...
        }
    
        public User get(String id) throws ClassNotFoundException, SQLException {
            Connection c = getConnection();
            //...
        }
    
        private Connection getConnection() throws ClassNotFoundException, SQLException {
            Class.forName("org.h2.Driver");
            return DriverManager.getConnection("jdbc:h2:tcp://localhost/~/springbook", "sa", "");
        }

    리팩토링 : 템플릿 메서드 패턴

    시나리오

    만약 UserDao를 기똥차게 만들어서 이 코드를 구매하겠다고 나서는 회사들이 있다고 가정해보자. 각 회사는 다른 DB 종류를 사용하고 있고, 독자적으로 구현한 방법으로 커넥션을 만들고 싶어한다. 그러나 우리의 UserDao는 너무너무 잘 만들고 팔아야 하기 때문에 소스코드를 내 주는 대신, 클래스 바이너리 형태로 전달하고자 한다. 이 때 우리의 코드를 사가는 고객들이 소스코드 없이 DB 커넥션을 고유한 방법으로 적용시킬 수 있는 방법이 무엇이 있을까?

    UserDao에서 getConnection() 구현부를 없애고, 추상 메서드(abstract method)로 선언한다. UserDao 또한 추상 클래스가 되기 때문에 직접 인스턴스를 만들수는 없지만, 각 고객들이 UserDao를 상속하여 getConnection을 override 함으로써 커넥션을 가져오는 방법을 구현하도록 하고, get()이나 add() 메서드는 그냥 getConnection()을 호출하도록 하면 된다.

    Code #5 : 상속을 통한 UserDao 확장하기

        public User get(String id) throws ClassNotFoundException, SQLException {
            Connection c = getConnection();
            //getConnection()은 여전히 남아있을 수 있다.
        }
    
        // abstract method로 getConnection을 선언하고, 상속받은 하위 클래스에서 이를 구현한다.
        public abstract Connection getConnection() throws ClassNotFoundException, SQLException;

    이렇게 구현을 정리하여 UserDao를 상속하여 getConnection()을 구현하기만 하면 손쉽게 확장이 가능하다.

    이렇게 슈퍼 클래스(UserDao)에 기본적이고 필수적인 로직의 흐름을 구현하고, 그 중 변화가 잦은 부분은 추상 메서드나 protected 메서드로(합쳐서 hook 메서드라 부른다) 만들어 오버라이딩이 가능하도록 만들어 필요에 맞게 구현할 수 있도록 하는 방법을 템플릿 메서드 패턴(template method pattern)이라고 한다.

    UserDao가 getConnection()을 바라보는 관심사는, '안에서 무슨 일이 벌어지는지 관심은 없고 이 메서드를 호출하면 내가 필요로 하는 DB 커넥션을 받을 것'이다. 그리고 UserDao를 상속한 Subclass에서 getConnection()을 바라보는 관심사는 어떻게 DB커넥션을 만들어 반환하는 지가 될 것이다.

    템플릿 메서드 패턴과 유사하게 팩터리 메서드 패턴(factory method pattern)도 있다. 팩터리 메서드는 오브젝트를 만들어서 반환하는 메서드인데, 이 팩터리 메서드를 마찬가지로 abstract/protected 메서드로 정의하여 subclass에 구체적 구현을 위임하고 Superclass로부터 구현을 독립시킨 패턴이다.

    그러나 상속을 이용한 디자인 패턴들은 여전히 문제가 있다. Java에서는 다중 상속을 허용하지 않기에, 만약 UserDao가 getConnection을 분리하기 위한 목적이 아닌 다른 목적으로 이미 상속을 이용하고 있다면? 그리고 Subclass가 Superclass의 구현에 의존할 수 있다면? 여전히 두 가지 다른 관심사가 긴밀하게 엮일 수 있을 가능성이 있으며, Subclass가 Superclass에 긴밀하게 의존할 경우, Superclass가 변경된다면 Subclass도 영향을 받을 수 있으며 이를 방지하기 위해 Superclass의 불변을 보장해야 하지만 여러 개발자가 이를 지키도록 강제하는건 쉬운 일도 아니며 보장할 수도 없다.

    1.3 DAO의 확장

    모든 오브젝트는 변한다. 그러나 모두가 동일한 방식으로 변화하지는 않는다. 관심사에 따라 분리한 오브젝트는 변화의 양상이 다르다. 변화의 양상이 다르다는건 변화의 이유, 시기, 주기 등이 다르다는 의미이다. 지금까지는 관심사에 따라 공통적으로 쓰이는 로직은 abstract class에, 변화가 잦은 것들은 sub class에 구현하도록 두었다. 그러나 상속은 여러 단점이 많으며, 다른 관심사를 상속 관계에 두는 대신, 관심사 별로 별도의 클래스로 분리한다. DB 커넥션을 만드는 기능들을 별도의 클래스에 담고, UserDao가 이를 사용하도록 하면 끝이다.

    그러나 UserDao는 SimpleConnectionMaker라는 구체적인 객체에 종속되어 있기 때문에 UserDao의 코드 수정 없이는 커넥션을 얻어오는 방법을 변경할 방법이 없다. 두 개의 클래스가 강력하게 결속되어 있기에 생기는 문제이며, 느슨한 추상적 연결고리를 중간에 끼워넣을 수 있다. 자바의 인터페이스를 이용하면, 인터페이스를 구현한 구체적인 클래스 정보는 몰라도 되며 인터페이스에 있는 추상 메서드를 호출하면 어떤 결과물을 받을 것인지만 알면 된다. 실제로 인터페이스를 구현한 클래스가 내부적으로 어떻게 돌아가는지는 관심갖지 않는다.

    Code #6 : 인터페이스를 이용한 리팩토링

    public interface ConnectionMaker {
        public Connection makeConnection() throws ClassNotFoundException, SQLException;
    }
    
    public class DConnectionMaker implements ConnectionMaker {
        public Connection makeConnection() throws ClassNotFoundException, SQLException {
                //...
        }
    }
    
    public class UserDao {
        private ConnectionMaker connectionMaker; 
            // 인터페이스를 이용했기 때문에 ConnectionMaker를 Implements한 클래스는 모두 가능
    
        public UserDao(ConnectionMaker connectionMaker){ 
                    // Constructor 파라미터로 구체 클래스를 전달함
            this.connectionMaker = connectionMaker;   
        }
            //...
    }
    
    public class {Any Client...}{
            public static void main(String[] args) throws ClassNotFoundException, SQLException {
            ConnectionMaker connectionMaker = new DConnectionMaker();
            UserDao dao = new UserDao(connectionMaker);
                    // 어떤 ConnectionMaker를 사용할지 직접 관계를 지정하는 것은 클라이언트의 역할이 될 것이다.
            }
    }

    원칙과 패턴

    나이브한 UserDao를 클린하게 만드는 과정에는 여러 객체지향의 이론이 담겨있다.

    개방 폐쇄 원칙 (Open-Closed Principle)

    정의는 '클래스나 모듈은 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다'.

    앞선 코드에서 DB 커넥션 기능을 확장하는 데는 열려있다고 할 수 있으며, DB 커넥션 기능을 확장한다고 다른 부분을 수정할 필요는 없기 때문에 변경에는 닫혀있다고 할 수 있다. 잘 설계된 API는 이 원칙을 따르고 있으며, 인터페이스를 통해 확장 기능을 정의함으로써 원칙을 지킬 수 있다.

    개방 폐쇄 원칙은 객체지향의 설계 원칙인 SOLID 원칙 중 하나이다.

    얕게 SOLID 원칙 둘러보기 : 객체지향 SOLID 원칙 by daebalprime

    높은 응집도와 낮은 결합도

    위의 개방 폐쇄 원칙은 고전적인 높은 응집도와 낮은 결합도라는 원리로도 설명 가능하다.

    어떤 클래스에 변경이 일어날 때, 모듈의 많은 부분이 함께 바뀌어야 한다면 응집도가 높다고 이야기한다. 분명 지금까지 변화에 쉽게 대응하기 위해 리팩토링 해왔는데, 변화에 대응하기 위해 많은 부분을 수정하는건 이상하게 들릴 수도 있다. 그러나 하나의 클래스가 잘 설계되었다고 한다면, 클래스는 하나의 역할만을 하도록 구성되어야 한다. (SRP, 단일 책임 원칙) 그래서 하나의 클래스가 하나의 책임을 맡는다면, 그 책임에 변경점이 있을 때 클래스의 많은 부분이 수정되어야 하는 것이 맞다. 만약 하나의 클래스가 여러 책임을 지고 있다면(초창기의 커넥션도 생성하고 DB에 쿼리를 날리는 묵직한 UserDao를 생각해보자) 하나의 변경점에도 코드의 일부분만 수정되어야 하며, 이 코드의 변경이 UserDao의 다른 역할에 영향을 미치지 않는지 검증하고 디버깅하는 일은 쉽지 않은 일이 될 것이다.

    낮은 결합도는 책임과 관심사가 다른 오브젝트와 느슨하게 연결됨을 의미한다. 다르게 이야기하면 '하나의 오브젝트가 변경이 일어날 때 다른 오브젝트가 얼마나 변경되는지'로 알 수 있다. 기존의 구체 클래스에 커넥션 생성을 의존한 UserDao 코드는 DB 연결 방식의 변경이 있을 때 UserDao까지 수정해야 하는 불편함이 있었지만, 인터페이스를 도입함으로써 결합도를 낮출 수 있음은 물론이며 변경에 따른 버그 발생 가능성도 낮출 수 있었다.

    전략 패턴

    UserDao를 디자인 패턴 시각에서 보면 전략 패턴(Strategy Pattern)으로 볼 수 있다. Context와 Strategy로 나뉜다. UserDao는 DB에 접근해 쿼리를 넘기는 context로 볼 수 있고, 어떤 DB 연결 커넥션을 사용할 지 변경이 자주 일어나는 부분을 인터페이스로 대체해 외부로 분리했기 때문에 Strategy라고 할 수 있다. UserDao를 사용하는 클라이언트는 전략(어떤 DB를 선택할 것인가)을 선택해 컨텍스트를 이용한다고 할 수 있다.

    전략 패턴의 이해를 돕기 위한 Comparator로 예시 들기

    Comparator<>는 배열이나 컬렉션을 정렬하는 메서드에서 정렬의 규칙을 넘기기 위한 인터페이스이며 Java로 알고리즘을 많이 풀어봤다면 꽤 익숙할 것이다. 여기서 Comparator는 정렬이라는 주요한 기능을 유지하는 인터페이스인 Context이고,

    1.4 제어의 역전 IoC

    팩토리

    팩토리란 객체의 생성 방법을 결정하고 만들어진 오브젝트를 돌려주는 것인데, 메서드를 호출하여 객체를 가져올 수 있는 객체다. 클라이언트는 디테일한 내부구현을 알 필요없이 어떤 방식으로 객체를 만들지 이미 결정되어 있는 메서드 중 하나를 골라 호출하기만 하면 된다.

    Code #7 : DaoFactory

    public class DaoFactory{
      public UserDao userDao(){
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
      }
    }
    
    public class UserDaoTest{
      public static void main(String[] args) throws ClassNotFoundException, SQLException{
        UserDao dao = new DaoFactory.userDao();
            // 테스트는 테스트 본연의 임무에 충실할 뿐, Dao 객체를 어떻게 생성하는지는 팩토리에 위임한다.
        //...
      }
    }

    만약 DaoFactory가 여러 Dao를 만들어야 한다고 가정해보자. UserDao, MessageDao 등... 여러 Dao를 반환하는 메서드를 작성하다보면 ConnectionMaker를 여러 번 반복적으로 작성하여야 한다는 문제점이 일어날 수 있다. 다른 커넥션을 이용하고 싶을 경우, 반복되는 코드를 여러 번 수정하여야 한다. 중복 문제는 분리가 답이며 ConnectionMaker 구현 클래스를 결정하는 코드를 별도의 메서드로 분리한다. getConnection 메서드 분리와 유사하게 구현하면 된다.

    Code #8 : 커넥션 생성 코드 분리하기

    public class DaoFactory{
      public UserDao userDao(){
        return new UserDao(connectionMaker());
      }
    
        public AnotherDao anotherDao(){
        return new AnotherDao(connectionMaker());
      }
    
      public ConnectionMaker connectionMaker(){
        return new DConnectionMaker();
      }
    }

    제어의 역전

    일반적인 프로그램은 main() 메서드와 같이 프로그램의 시작점에서 다음에 사용할 오브젝트를 직접 결정하고, 생성하고, 오브젝트 메서드를 호출하는 등의 행위가 이루어진다. 해당 프로그램의 코드에서 능동적으로 어떤 객체를 생성하는지를 결정하는 것이다. 지금까지의 예제에서 특정 커넥션을 코드에서 직접 생성하였을 때를 결정하면 쉽다. 그러나 코드를 점점 수정하면서 어떤 Connection을 사용할 지는 해당 프로그램이 직접 결정하는 것이 아닌, connectionMaker()가 주는대로 받아 쓸 뿐이며, 프로그램은 그 내부가 어떻게 작성되어 있는지도 관심이 없다. 객체 결정과 생성을 외부의 코드에 위임하는 것이다. 제어의 역전이라고 부를 수 있다.

    라이브러리와 프레임워크의 차이도 제어권의 시점에서 볼 수 있다. 라이브러리를 사용하는 어플리케이션 코드는 직접 흐름을 제어하는 와중에 라이브러리를 이용한다. 그러나 프레임워크는 스스로 흐름을 제어하며 그 중간에 개발자가 작성한 코드를 프레임워크가 이용한다.

    지금까지 UserDao가 ConnectionMaker의 구현 클래스를 직접 코드에 박아 놓음으로써 제어권을 가지고 있었다. 그러나 지금까지 수정해온 코드는 UserDao도 DaoFactory에 의해 생성되고, 커넥션 오브젝트도 connectionMaker 메서드에 위임하였다. 관심사를 분리하고 책임을 나누는 과정이 IoC를 이용하기 위함이었다.

    지금까지 책에서 소개한 코드만 하더라도 IoC를 적용한 코드라 할 수 있지만, 컴포넌트 생성과 관계설정, 생명주기 관리, 사용 등을 총괄하는 프레임워크를 사용하는 편이 훨씬 좋으며 그렇기에 스프링을 배우려고 하는 것이다. IoC는 스프링의 존재 이유이자 아이덴티티라고 할 수 있다.

    1.5 스프링의 IoC

    Spring Bean은 직접 제어권을 가지고 객체를 생성하여 관계를 부여하는 오브젝트를 말한다. 이 Bean을 관리하는 IoC 오브젝트를 Bean Factory라 부르며, 스프링에서는 Bean Factory를 확장한 Application Context를 더 많이 사용한다. 책에서는 Bean Factory라고 이야기할 경우에는 Bean을 생성하고 관계를 설정하는 IoC적인 측면에 초점을 맞춘 단어라면, Application Context는 앱 전반의 컴포넌트 제어를 담당하는 IoC 엔진적인 측면이 더욱 부각된다고 생각하면 된다.

    Application Context

    기존의 DaoFactory 코드에는 어떤 클래스의 오브젝트를 생성하고 어디에 연결해줄 것인가를 지정하는 정보가 자바 코드에 담겨있지만, Application Context는 직접 이런 정보를 가지지는 않고 설정정보를 참고한다. 이 설정정보를 만드는 방법은 여러가지가 있지만, 여기선 애너테이션 방식을 제일 먼저 살펴본다.

    Code #9 : 스프링 빈 팩토리가 인식할 수 있도록 설정정보를 담는 DaoFactory 클래스

    @Configuration 
    public class DaoFactory{
    
      @Bean 
        // 오브젝트를 새엉을 담당하는 IoC용 메소드라는 표시
      public UserDao userDao(){
        return new UserDao(connectionMaker());
      }
    
      @Bean
      public ConnectionMaker connectionMaker(){
        return new DConnectionMaker();
      }
    }

    @Configuration이라는 애너테이션을 클래스 위에 추가하여 Bean Factory나 Application Context가 필요로 하는 정보임을 스프링 컨테이너에게 알려주며, @Bean은 오브젝트 생성을 담당하는 IoC 메서드임을 표시한다.

    Code #10 : Application Context를 적용한 UserDaoTest

    public class UserDaoTest{
      public static void main(String[] args) throws ClassNotFoundException, SQLException{
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao dao = context.getBean("userDao",UserDao.class);
        //...
      }
    }

    ApplicationContext 타입의 오브젝트를 생성하는데, AnnotationConfigApplicationContext를 이용하면 @Configuration 애너테이션이 붙은 자바코드를 설정정보로 이용할 때 사용한다.

    getBean() 메서드로 객체를 가져오는데, 첫 번째 파라미터는 @Bean이 붙은 메서드의 이름을, 두 번째 파라미터엔 해당 Bean의 타입을 명시해준다. 굳이 타입을 명시하는 이유는, 내부적으로 Bean은 Object 타입으로 관리 되는데 이를 코드에서 직접 타입 캐스팅을 하는 대신, 반환할 때 UserDao 타입으로 반환하기 위함이다.

    진짜 놀랍게도 애너테이션을 잘 붙인다면 스프링에서 알아서 설정정보를 관리한다.

    ApplicationContext를 사용했을때 장점

    1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
    2. 필요한 오브젝트를 생성할 때 어떤 팩토리 클래스를 사용해야 할 지 알아야 하는 대신, 그냥 Bean 이름으로 일관성 있게 가져올 수 있다.
    3. 애플리케이션 컨텍스트는 종합 IoC서비스를 제공해준다.
    4. 생성과 관계설정 외에도 오브젝트가 만들어지는 방식, 시점, 전략 등 다양한 설정과 인터셉트, 오브젝트 후처리, AOP 등 여러가지 기능등을 이용할 수 있다.
    5. 애플리케이션 컨텍스트는 빈을 검색하는 다양한방법을 제공한다.
    6. Bean의 이름 뿐 아니라 타입으로도 빈을 가져올 수 있다.

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

    오브젝트의 동일성(identical)과 동등성(equivalent)

    동일성(Identical)

    String s1 = new String("고길동");
    String s2 = new String("고길동");
    
    s1 == s2; // false; 두 스트링 객체는 담고있는 값이 같을 뿐 동일한 객체는 아니다.

    두 객체가 완전히 동일한 객체라면 동일한(identical) 객체라고 할 수 있다. s1==s1은 참이지만, s1==s2는 거짓이다.

    동등성

    String s1 = new String("고길동");
    String s2 = new String("고길동");
    
    s1.equals(s2); // true; 
    // Java String 객체의 동등성 비교는 각 스트링 객체가 실질적으로 담고있는
    // 문자열이 같다면 동등하다고 하며 우리의 직관에도 배치되지 않는다.

    두 객체의 동등성 조건 - 각 객체에 구현된 equals메서드에는 무엇이 같아야 두 객체가 동등한지를 구현해놓았다. 두 객체는 동일하지 않더라도 동등할 수 있으며, 두 객체가 동일하면 동등하다.

    자세히 살펴보기 : Effective Java ITEM 10. equals는 일반 규약을 지켜 재정의 하라

    싱글톤(Singleton) 패턴

    싱글톤 패턴은 어떤 클래스를 어플리케이션 내에서 제한된 갯수의 인스턴스 갯수를 갖도록 하는 패턴이며, 이름에서 유추할 수 있듯이 일반적으로 단 하나의 오브젝트만 존재한다.

    전역에서 하나의 인스턴스를 공유해야 하는 경우에 이 패턴을 사용한다.

    싱글턴 패턴을 Java에서 구현하려면

    • 생성자를 private로 지정하여 외부에서 새 인스턴스를 만들지 못하도록 한다.
    • static 필드에 인스턴스를 담을 수 있는 변수를 하나 정의한다.
    • static factory method인 getInstance()를 만들고 메서드가 최초로 호출되는 시점에 오브젝트를 만들어 static 필드의 변수에 저장한다.
    • getInstance() 메서드는 static 필드에 저장된 인스턴스를 반환한다. 호출될 때 마다 static 필드에 저장된 단 하나의 인스턴스를 반환하므로 싱글톤 패턴이다.

    그러나 싱글턴 패턴은

    • private 생성자는 상속을 제한한다.
    • 테스트하기 힘들다.
      직접 인스턴스를 만들 수 없기 때문에 테스트가 힘들다.
    • 서버환경에서는 싱글톤이 단 하나임을 보장하지 못한다.
      서버에서 클래스 로더를 어떻게 구성하냐에 따라 static 변수는 단 하나임이 보장되지 않는다. 예를 들면 다수의 JVM에서 분산되어 돌아가는 경우가 있다.
    • 전역 상태의 특성은 바람직하지 않다.
      객체지향 프로그래밍에서 전역 상태는 권장되지 않는 모델이다.

    스프링에서의 싱글톤

    지금까지 작성한 DaoFactory 코드는 userDao() 메서드를 호출할 때 마다 새로운 객체를 반환한다.

    그러나 스프링 Application Context에 설정정보를 등록하고 getBean() 메서드로 가져오는 userDao() 메서드는 항상 동일한 인스턴스를 반환한다. 그래서 Application Context에 이르러 싱글톤 레지스트리(Singleton Registry)라고 하기도 한다.

    스프링은 주로 서버환경에서 사용된다. 서버에는 다양한 계층의 오브젝트가 있다. 비즈니스 로직, 서비스 로직, DAO 등등... 그러나 이를 담당하는 객체들을 매 요청마다 생성한다면? 한 번의 요청에 10개의 객체를 생성한다면, 500번의 요청에는 5000개의 객체가 생성될 것인데 GC가 바쁘게 돌아가고 성능 저하는 피할 수 없을 것이다. 그래서 자바 엔터프라이즈에서는 서비스 오브젝트라는 개념이 일찍부터 고안되었다. 엔터프라이즈의 핵심이자 기반인 서블릿에서는 싱글톤으로 멀티쓰레드 환경에서 동작한다.

    싱글톤 레지스트리

    위에서 언급했던 것 처럼 Java의 싱글톤 구현의 정석은 여러 문제가 있기 때문에, 스프링에서 싱글톤 오브젝트를 생성하고 관리하는 기능을 제공하는 요소를 싱글톤 레지스트리라고 한다. 싱글톤 레지스트리는 평범한 자바 객체도 싱글톤으로 활용할 수 있도록 해준다. 어노테이션을 붙여 스프링 컨테이너에게 제어권을 넘기면 알아서 다 해준다.

    싱글톤과 오브젝트의 상태

    싱글톤은 멀티쓰레드 환경에서 여러 쓰레드가 접근할 수 있으므로, stateless 방식으로 만들어져야 한다. 만약 인스턴스 필드를 두고 값을 유지하며 동작에 관여하도록 stateful 하게 만들면 race condition에 의해 서버가 망가질 것이다.

    Code #10 : stateful한 UserDao

    public class UserDao {
        private ConnectionMaker connectionMaker; 
        // 생성자가 한 번 설정하면 후에 바뀔 일이 없는 읽기전용 인스턴스 변수.
      // connectionMaker는 스프링이 관리하는 빈이고, 싱글톤이기 때문에
        // 다른 ConnectionMaker 타입의 인스턴스가 할당될 일이 없다.
      // 물론 final 키워드로 지정해 주는 편이 좋을 것이다.
      // 요약 : 싱글톤 빈을 저장하는 인스턴스 변수는 문제가 되지 않는다.
    
        private Connection c;
        private User user;
        // 각 요청마다 고유한 객체를 담고 있어야 할 필드가 인스턴스 변수로 선언이되면
      // race condition이 발생한다.
        public User get(String id) throws ClassNotFoundException, SQLException {
            this.c = connectionMaker.makeConnection();
    
            this.user = new User(); 
            this.user.setId(rs.getString("id")); 
            /*
             * this.user 변수로 접근하여 조작하는 와중에 
             * 다른 쓰레드가 this.user를 덮어 씌우게 된다면?
             */
    
            //...
    
            return this.user;
        }
    }

    stateless하게 만들면 정보는 어떻게 다뤄야 할까? 파라미터와 리턴 값, 로컬 변수 등을 이용하면 thread-safe함을 보장할 수 있다.

    스프링 빈의 스코프

    스프링 빈이 생성되고 존재하고 적용되는 범위를 스코프라고 한다. 스프링 빈의 default 스코프는 싱글톤이며, 컨테이너 내에 한 개의 오브젝트만 만들어진다. 만약 Bean을 요청할 때 마다 새로은 인스턴스를 만들도록 하고 싶다면 prototype 스코프를 고려할 수 있다. 그 외에도 매 요청마다 생성되는 request 스코프, 웹의 세션에 대응하는 session 스코프도 있다.

    1.7 의존관계 주입(DI)

    IoC는 객체지향 패러다임에서 중요하게 여겨지는 개념으로, DaoFactory처럼 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화한 것이 스프링의 IoC 컨테이너다.

    스프링을 IoC 컨테이너로 부르기에는 단순한 템플릿 메서드 패턴을 이용해 만들어진 프레임워크,인지, 서블릿처럼 동작하는 서비스 컨테이너 뜻인지 이해하기 어렵기 때문에 스프링의 IoC 방식을 의존관계 주입(Dependency Injection)이라 부른다. 런타임에 의존관계가 정해진다는 의도를 담고 있다.

    의존관계

    두 개의 클래스가 있다. A가 B를 의존한다면 B의 변화가 A에 영향을 미친다는 의미이다. A가 B의 메서드를 사용하는 경우가 ㄷ대표적이다. 사용에 대한 의존관계가 있다고 말할 수 있다. B의 메서드 시그니쳐가 바뀌거나 내부 동작이 바뀌면 A의 코드를 수정해야하거나 결과에 영향이 미칠 수 있다. UML 모델에선 점선으로 표기하며, 아래 그림을 참조하면 된다.

    UserDao의 경우에는, ConnectionMaker 인터페이스에 의존하고 있다. 그러나 인터페이스를 구현한 클래스가 다른 것으로 바뀌거나 인터페이스에 정의된 메서드가 아닌 내부에서 사용하는 메서드에 변화가 생겨도 UserDao는 동작할 것이다. 인터페이스에 의존하면 그 구현 클래스와의 관계는 느슨해지며 변화에 영향을 많이 받지 않는다. UserDao 입장에선 구현 클래스의 존재를 알지 못한다.

    의존관계 주입

    UML에서 의존관계는 설계 모델의 관점에서 이야기하는 것이다. 모델이나 코드에서 드러나는 의존관계 이외에도 런타임에 오브젝트 사이에서 만들어ㅏ지는 의존 관계를 런타임 의존관계라 부르며, 위에서 다뤘던 설계 모델의 관점에서 의존 관계가 구체화된다고 볼 수 있다. 런타임에 의존관계를 맺는 오브젝트를 의존 오브젝트라고 한다.

    구체적인 의존 오브젝트와 클라이언트 오브젝트를 연결해주는 작업을 의존관계 주입이라고 부른다. UserDao가 DConnectionMaker 인스턴스를 인터페이스를 통해 받아들이는 경우이다.

    의존관계 주입은 다음 세 가지 조건을 따른다.

    • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 즉 어떤 구현 클래스를 쓰는지 UserDao 코드에선 알 수 없으며 인터페이스에만 의존하고 있는 상태이다.
    • 런타임 시점의 의존관계는 컨테이너나 팩토리와 같은 제3의 존재가 결정한다.
    • 이 외부 주체가 실질적으로 사용할 오브젝트를 주입/제공하는 시점에 의존관계가 만들어진다.

    이 제 3의 존재는 Application Context, Bean Factory, IoC 컨테이너 등이다.

    Code #11 : 의존관계 주입을 적용한 생성자

    public UserDao(ConnectionMaker connectionMaker){
            this.connectionMaker = connectionMaker; 
            // 자신이 사용할 오브젝트는 생성자 파라미터를 통해서 전달받는다.
    }

    주의해야 할 점은 외부에서 오브젝트를 파라미터로 전달해주었다고 해서 의존관계 주입은 아니다. 반드시 인터페이스 타입으로 전달해주어야 한다.

    의존관계 검색

    외부로 주입을 받지 않고 스스로 검색하여 의존관계를 맺는 의존관계 검색(Dependency Lookup)도 있다. 자신이 사용할 구체적인 클래스를 지목하지는 않지만, 컨테이너에게 요청한다.

    Code #12 : 의존관계 검색을 이용하는 생성자

    public UserDao(){
            AnnotationConfigApplicationContext context = 
                    new AnnotationConfigApplicationContext(DaoFactory.class);
            this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
    
    }

    ApplicationContext에 "connectionMaker" 이름을 가진 Bean을 요청한다. 해당 이름을 가진 Bean을 직접 생성하지는 않기 때문에 여전히 IoC 원칙을 준수한다.

    의존관계 주입 vs 검색

    일반적으로는 의존관계 주입을 사용하는 것이 깔끔하다. 의존관계 검색은 스프링 API나 팩토리 클래스에 의존하게 된다.

    그러나 의존관계 검색을 사용해야 할 때가 있다. 스프링 IoC와 DI 컨테이너가 적용되어 있지만 한 번은 반드시 의존관계 검색을 수행해야 한다. 엔트리 포인트인 main()에서는 DI로 주입받을 방법이 없기 때문이다. 또한 의존관계 검색을 구현한 클래스는 자기 자신이 스프링의 Bean일 필요가 전혀 없다. ConnectionMaker는 스프링의 Bean이여야 하지만, UserDao는 그렇지 않아도 된다는 소리이다. 그 반대로 의존관계 주입에서는 UserDao도 반드시 Spring이 관리하는 Bean이 되어야 한다.

    DI의 장점은 무엇일까?

    앞에서 설명한 원칙과 객체지향 설계를 따랐을 때 얻는 이점들을 그대로 얻을 수 있다. 코드에는 의존하는 클래스가 구체적으로 나타나지 않아 결합도가 낮기 때문에 변경을 통한 확장에 자유롭다.

    시나리오 1. 데이터베이스 교체하기

    배포 시에는 실제 데이터베이스 서버를 이용해야겠지만, 개발할 때는 개발자 로컬의 데이터베이스를 이용하는 것이 일반적이다. DI를 적용한 코드에서는 이 변경이 빠르고 자유롭다. 예를 들면, ConnectionMaker.connectionMaker()에서 반환하는 오브젝트만 수정하면 끝이다.

    시나리오 2. 부가기능 추가

    DAO가 DB에 얼마나 자주 연결하는지 파악하고 싶다고 해보자. 개발하다보면 DAO의 종류가 많아질 텐데 그 모든 코드에 커넥션을 호출할 때 마다 카운트를 증가시키는 로직을 추가한다면 손이 많이 가겠지만, 실제 ConnectionMaker 구체 클래스와 인터페이스 사이에 하나의 오브젝트를 더 끼워넣고, 카운트하는 로직을 추가하는게 다일 것이다.

    Code #13 : 연결 횟수를 Counting하는 ConnectionMaker 구체 클래스

    import java.sql.Connection;
    import java.sql.SQLException;
    
    public class CountingConnectionMaker implements ConnectionMaker {
    
        int counter = 0;
        private ConnectionMaker realConnectionMaker;
    
        public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
            this.realConnectionMaker = realConnectionMaker; 
            // 실제 사용될 ConnectionMaker 구체클래스를 받는다.
        }
    
        @Override
        public Connection makeConnection() throws ClassNotFoundException, SQLException {
            this.counter++; 
            return realConnectionMaker.makeConnection();
            // 실제 오브젝트를 반환하기 전에 counter를 한 번 증가시켜준다.
        }
    
        public int getCounter() {
            return this.counter;
        }
    }

    스프링에서 사용하는 메서드 이용 의존관계 주입

    지금까지 예제에서는 의존관계 주입에 생성자 파라미터를 이용했지만, 일반적으로 스프링에선 setter 메서드를 통한 DI를 사용했다. 자바 빈 규격을 따르기 때문에 나중에 사용하기 편하다.

    메서드 이름은 의미있고 단순한 이름이 제일 좋지만 특별한 아이디어가 없다면 메서드를 통해 주입받을 오브젝트 타입이름을 붙이는 게 제일 좋다. setConnectionMaker()가 그 예다.

    1.8 XML을 이용한 설정

    스프링 DI 의존관계 설정하는 방법은 XML과 Annotation 크게 2가지가 있다. 그 중 XML은

    1. 단순한 텍스트 파일로 수정이 용이
    2. 쉽게 이해 가능
    3. 변경사항 발생 시 재컴파일하지 않아도 됨
    4. 빠르게 변경사항 반영 가능
    5. 스키마나 DTD를 이용해 정해진 포맷을 따라 작성되었는지 검사 가능 (문법 검사)

    과 같은 장점을 가지고 있다.

    XML 설정하기

    스프링의 Application Context는 XML에 담긴 DI정보를 활용할 수 있다. 이 XML을 작성할 때 루트 엘리먼트를 로 두고 작성해 나가면 된다.

    어노테이션으로 작성한 하나의 @Bean 메소드에서 얻을 수 있는 빈의 DI 정보는

    • 빈의 이름 : @Bean 메소드 이름으로 지정이 되며, getBean()에서 사용하는 이름
    • 빈의 클래스 : 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의
    • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메소드를 통해 의존 오브젝트를 주입하는데, 주입하는 오브젝트 또한 빈일 것이고 이름으로 가져온다. 여러 개에 의존할 수도 있음

    클래스 - XML 설정 1:1 대응

    기존 클래스에 작성된 정보를 XML로 1:1 대응하면 된다. 주의할 점은, 빈의 클래스 항목에 작성하는 내용은 반환하고자 하는 실제 구체 클래스이며, 인터페이스를 작성하지 않도록 하자. 또한 패키지 경로를 모두 작성하여야 한다.

    Code #14 : 메서드 → XML 전환 예시

    @Bean //----------------------------------->  <bean
    public ConnectionMaker{
        connectionMaker(){ //------------------> id="connectionMaker
            return new DConnectionMaker(); // -> class="springbook...DConnectionMaker" />
        }
    }
    
    <bean id="connectionMaker class="springbook...DConnectionMaker"/>

    의존 관계를 주입받는 Bean 설정하기

    앞에서 작성한 userDao는 connectionMaker 의존을 주입받는다. 스프링은 자바빈을 관례를 따라서 setter 메서드를 프로퍼티로 사용한다. 즉 setConnectionMaker()라는 메서드를 정의해 놓으면 set을 제외한 ConnectionMaker라는 프로퍼티 이름으로 사용한다.

    XML에서는 태그를 이용해 의존 관계를 정의한다. name과 ref라는 두 개의 애트리뷰트를 갖는다. name은 프로퍼티의 이름이고, ref는 주입해줄 오브젝트 빈의 이름이다.

    Code #15 : 메서드 → XML 의존주입 전환

    userDao.setConnectionMaker(connectionMaker());
            ___|______________ _______________
                     /     \                    \
     _______/       \______________      \_______________
    <property name="connectionMaker" ref="connectionMaker" />

    주의해야 할 점은, setter 메서드는 Java의 컨벤션인 Camel Case를 따라 C가 대문자이지만, xml에 작성할 때도 set을 제외한 부분에서 첫 글자는 소문자로 작성해 주어야 한다.

    Code #16 : userDao 빈 설정 마치기

    <bean id ="userDao" class="{classpath....}.UserDao">
        <property name="connectionMaker" ref="connectionMaker" />
    </bean>

    Code #17 : XML 완성

    <beans>
        <bean id="connectionMaker class="springbook...DConnectionMaker"/>
        <bean id ="userDao" class="{classpath....}.UserDao">    
            <property name="connectionMaker" ref="connectionMaker" />
        </bean>
    </beans>

    ApplicationContext 설정하기

    XML 빈의 의존관계 정보를 이용하는 DI에는 GenericXmlApplicationContext를 사용한다. ApplicationContext가 사용하는 파일은 applicationContext.xml로 만드는게 컨벤션이다.

    Code #18 : applicationContext.xml 예시

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="connectionMaker class="springbook...DConnectionMaker"/>
        <bean id ="userDao" class="{classpath....}.UserDao">    
            <property name="connectionMaker" ref="connectionMaker" />
        </bean>
    </beans>

    Code #19 : ApplicationContext 불러오기

    ApplicationContext context = new GenericXmlApplicationContext("{path}/applicationContext.xml");
    ApplicationContext context = new ClassPathApplicationContext("daoContext.xml", UserDao.class);
    //UserDao 패키지가 위치한 클래스패스를 가져와 상대경로를 지정할 수 있다.

    DataSource 인터페이스 적용

    ConnectionMaker는 DB 커넥션 생성 딱 하나만 당당하는 인터페이스다. Java에서는 DB 커넥션을 가져오는 오브젝트의 기능을 담은 DataSource라는 인터페이스가 존재한다. 이에 맞춰 구현된 클래스들을 사용하는 것으로 충분하다.

    jdbc 라이브러리를 추가하자.

    Code #20 : DataSource

    package javax.sql
    
    public interface DataSource extends CommonDataSource, Wrapper{
        Connection getConnction() throws SQLException;
            // 커넥션을 가져오는 메서드인 getConnection() 사용
            //...
    }
    
    import javax.sql.DataSource;
    
    public class UserDao{
        private DataSource dataSource;
    
        public void setDataSource(DataSource dataSource){
            this.dataSource = dataSource;
        }
    
        public void add(User user) throws SQLException{
            Connection c = dataSource.getConnction();
            //...
        }
        //...
    }

    Code #21 : 자바 코드로 url, id, pw 설정하기

    @Bean 
    public DataSource dataSource(){
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
    
        dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
        dataSource.setUrl("jdbc:mysql://localhost/springbook");
        dataSource.setUsername("myid");
        dataSource.setPassword("mypw");
    
        return dataSource;
    }
    
    @Bean 
    public UserDao userDao(){
        UserDao userDao = new UserDao();
        userDao.setDataSource(dataSource()); // DataSource를 주입받는다.
        return userDao;
    }

    Code #21 : XML로 로그인 정보, url 설정하기

    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />
        <property name="driverClass" value="com.mysql.jdbc.Driver" />
        <!--
            driver class는 java.lang.Class를 받도록 되어있지만 value는 String 값이다.
            스프링이 파라미터 타입을 참고해서 적당히 추론하여 변환해준다.
            내부적으론 아래와 같은 작업을 해준다고 생각하면 된다.
            Class driverClass = Class.forName("com.mysql.jdbc.Driver");
            dataSource.setDriverClass(driverClass);
        -->
        <property name="url" value="jdbc://localhost/springbook" />
        <property name="username" value="myid" />
        <property name="password" value="mypw" />
    </bean>

    Reference

    토비의 스프링 예제코드 by icednut

    토비의 스프링 - 1장 오브젝트와 의존관계 by wan-blog (다이어그램 스캔본 출처)

    댓글