1장-오브젝트와 의존관계

8 분 소요

1장 오브젝트와 의존관계

스프링은 자바를 기반으로 한 기술이다. 스프링이 가장 관심을 많이 두는 대상은 오브젝트다.

애플리케이션에서 오브젝트가 생성되고 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 진지하게 생각해볼 필요가 있다.

추가로, 오브젝트는 어떻게 설계돼야 하는지, 어떤 단위로 만들어지며 어떤 과정을 통해 자신의 존재를 드러내고 등장해야 하는지에 대해서도 살펴봐야한다.

스프링은 오브젝트를 어떻게 효과적으로 설계하고 구현하고, 사용하고, 이를 개선해나갈 것인가에 대한 명쾌한 기준을 마련해준다.

1장에서는 스프링이 관심을 갖는 대상오브젝트의 설계와 구현, 동작원리에 더 집중하기를 바란다. 그러다보면 자연스럽게 스프링이 무엇인지도 이해될 것이다.

1.1 초난감 DAO

DAO란 ?

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

자바빈이란?

  • 자바빈(JavaBean)은 다음 두가지 관례를 따라 만들어진 오브젝트를 가리킨다.
    • 디폴트 생성자 : 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
    • 프로퍼티 : 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 set으로 시작하는 수정자 메소드(setter)get으로 시작하는 접근자 메소드(getter)를 이용해 수정 또는 조회할 수 있다.

1.1.1 User

사용자 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO를 하나 만들어본다.

사용자 정보를 저장할 User 클래스를 만든다.


@Setter
@Getter
public class User {
    private String id;
    private String name;
    private String password;
}

1.1.2 UserDao

사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어본다.

  1. 새로운 사용자를 생성하고(add) 메소드 만들기
  2. 아이디를 가지고 사용자 정보를 읽어오는(get) 메소드 만들기

JDBC를 이용하는 작업의 일반적인 순서

  1. DB 연결을 위한 Connection을 가져온다.
  2. SQL을 담은 Statement(또는 PreparedStatement)를 만든다.
  3. 만들어진 Statement를 실행한다.
  4. 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트(여기서는 User)에 옮겨준다.
  5. 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
  6. JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.

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:3306/user-db", "root", "0000");

        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:3306/user-db", "root", "0000");

        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 class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        UserDao dao = new UserDao();

        User user = new User();
        user.setId("diamond");
        user.setName("장종욱");
        user.setPassword("1234");

        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() + "조회 성공");
    }
}

1.2 DAO의 분리

1.2.1 관심사의 분리

  • 개발자가 객체를 설계할 때 가장 염두에 둬야할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다.
  • 객체지향 설계와 프로그래밍이 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다.

  • 어떻게 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있을까?
  • 그것은 분리와 확장을 고려한 설계가 있었기 때문이다.

  • 문제는, 변화는 대체로 집중된 한가지 관심에 대해 일어나지만 그에 따른 작은은 한곳에 집중되지 않는 경우가 많다는 점이다.
  • 프로그래밍의 기초 개념 중에 관심사의 분리 라는게 있다.
  • 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이다.
  • 관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어주는 것이다.

1.2.2 커넥션 만들기의 추출

UserDAO의 관심사항

  1. DB와 연결을 위한 커넥션을 어떻게 가져올까라는 관심
  2. 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
  3. 작업이 끝나면 사용한 리소스인 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것
  • 그러나, 가장 문제가 되는 것은 첫째 관심사인 DB연결을 위한 Connection 오브젝트를 가져오는 부분이다.
  • 여기저기 흩어져 있어서 다른 관심의 대상과 얽혀 있으면, 변경이 일어날 때 엄청난 고통을 일으키는 원인이 된다.

중복 코드의 메소드 호출

  • 가장 먼저 할일은 커넥션을 가져오는 중복된 코드를 분리하는 것이다.
  • 각 DAO 메서드에서는 이렇게 분리한 getConnection() 메서드를 호출해서 DB 커넥션을 가져오게 만든다.

** getConnection() 메서드를 추출해서 중복을 제거한 UserDao

public void add(User user) throws ClassNotFoundException, SQLException {
    Connection c = getConnection();
    ...
}

public User get(String id) throws ClassNotFoundException, SQLException {
    Connection c = getConnection();
    // DB 연결 기능이 필요하면 getConnection() 메서드를 이용하게 한다.
    ...
}
// 중복된 코드를 독립적인 메서드로 만들어서 중복을 제거했다.
private Connection getConnection() throws ClassNotFoundException, SQLException {
    Clss.forName("com.mysql.jdbc.Driver");
    Connnection c = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/schema", "spring", "book");
    return c;
}
  • 관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다.
  • 예를 들어, 위와 같이 DB 종류와 접속 방법이 바뀌어서 드라이버 클래스와 URL이 바뀌었다거나, 로그인 정보가 변경돼도 앞으로는 getConnection()이라는 한 메소드의 코드만 수정하면 된다.

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

  • 코드 수정한 후에 기능에 문제가 없다는게 보장되지 않기에 검증이 필요하다.
  • main() 메소드를 이용한 테스트를 실행해본다.

리팩토링

  • 리팩토링은 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다.
  • 리팩토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다.
  • 생산성은 올가고, 코드의 품질은 높아지며, 유지보수하기 용이해지고, 견고하면서도 유연한 제품을 개발할 수 있다.

    메소드추출기법

  • 위에서 사용한 getConnection()이라고 하는 공통의 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메소드 추출 기법이라고 부른다.

1.2.3 DB 커넥션 만들기의 독립

상속을 통한 확장

  • 기존 UserDao 코드를 한 단계 더 분리하면 된다.
  • 즉, UserDao에서 메소드의 구현 코드를 제거하고 getConnection()을 추상 메소드로 만들어놓는다.

템플릿 메소드 패턴

  1. 슈퍼클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환)을 만들고,
  2. 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 디자인 패턴
  3. 즉, 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법
  4. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다.
  5. 훅(hook) 메서드란 슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드

팩토리 메소드 패턴

  1. UserDao의 getConnection() 메소드는 Connection 타입 오브젝트를 생성한다는 기능을 정의해놓은 추상 메소드다.
  2. UserDao의 서브클래스의 getConnection() 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지를 결정하는 방법이다.
  3. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 부른다.
  4. 즉, 서브클래스는 다양한 방법으로 오브젝트를 생성하는 메소드를 재정의할 수 있다.
  5. 이렇게 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드

상속을 통한 확장 방법이 제공되는 UserDao

(참고) 자바는 클래스의 다중상속을 허용하지 않는다.


public abstract class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        ...
    }
    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        ...
    }
    // 구현 코드는 제거되고 추상 메소드로 바뀌었다.
    // 메소드의 구현은 서브클래스가 담당한다.
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

public class NUserDao extends UserDao {
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // N사 DB connection 생성 코드
    }
}

public class DUserDao extends UserDao {
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // D 사 DB connection 생성 코드
    }
}

디자인 패턴

  • 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션

  • 디자인 패턴은 주로 객체지향 설계에 관한 것이고, 대부분 객체 지향적 설계 원칙을 이용해 문제를 해결한다.
  • 객체지향적인 설계로부터 문제를 해결하기 위해 적용할 수 있는 확장성 추구 방법이 대부분 2가지 구조로 정리된다.
  • 하나는 클래스 상속이고 다른 하나는 오브젝트 합성이다.
  • 패턴에서 가장 중요한 것은 각 패턴의 핵심이 담긴 목적 또는 의도다.
  • 디자인 패턴은 객체지향 언어인 자바를 사용하는 개발자라면 반드시 공부해야할 주제다.
  • 디자인 패턴을 최초로 집대성한 책인 GoF의 디자인 패턴(에릭 감마 외) 또는 Head First Design Patterns(에릭 프리먼)을 추천한다.

1.3 DAO의 확장

  • 변화의 성격은 변화의 이유와 시기, 주기 등이 다르다는 것이다.
  • 이를 위해, 추상클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 바로 이렇게 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서이다.
  • 여러가지 단점이 많은, 상속이라는 방법을 사용했다는 사실이 불편하다.

1.3.1 클래스의 분리

  • DB 커넥션과 관련된 부분을 서브클래스가 아니라, 아예 별도의 클래스에 담는다.

  • 각 메소드에서 매번 SimpleConnectionMaker의 오브젝트를 만들수도 있지만, 그보다는 한번만 SimpleConnectionMaker 오브젝트를 만들어서 저장해두고 이를 계속 사용하는 편이 낫다.
  • UserDao는 상속을 통한 방법을 쓰지 않으니 더이상 abstract일 필요는 없다.
  • 생성자에서 SimpleConnectionMaker의 오브젝트를 만들어두고, add/get 메소드에서 이를 이용해 DB 커넥션을 가져오면 된다.

독립된 SimpleConnectionMaker를 사용하게 만든 UserDao

public class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {

        simpleConnectionMaker = new SimpleConnectionMaker();
        // 상태를 관리하는 것도 아니니 한번만 만들어 인스턴스 변수에 저장해두고 메소드에서 사용하게 한다.
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
    }

}

독립시킨 DB 연결 기능인 SimpleConnectionMaker

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdcbc.Driver");
        Connection c = DriverManager.getConnection(
                        "jdbc:mysql://localhost:3306/schema", "root", "0000");
        return c;
    }
}

1.3.2 인터페이스의 도입

추상화

  • 추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.
  • 자바가 추상화를 위해 제공하는 가장 유용한 도구는 바로 인터페이스이다.

  • 인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이다.
  • 따라서 인터페이스에는 어떻게 하겠다는 구현 방법은 나타나 있지 않다.

ConnectionMaker 인터페이스

ConnectionMaker 인터페이스 정의

  • DB 커넥션을 가져오는 메소드 이름은 makeConnection()이라고 정했다.
  • makeConnectoin() 메소드를 호출하기만 하면 Connection 타입의 오브젝트를 만들어서 돌려줄것이라고 기대할 수 있다.
public interface ConnnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
  • D 사의 개발자라면 ConnectionMaker 인터페이스를 구현한 클래스를 만들고, 자신들의 DB 연결 기술을 이용해 DB 커넥션을 가져오도록 메소드를 작성해준다.

ConnectionMaker 구현 클래스

public class DConnectionMaker implements ConnectionMaker {
    ...
    public Connection makeConnection() throws ClassNotFoundException, SQLException{
        // D 사의 독자적인 방법으로 Connection을 생성하는 코드
    }
}
  • 특정 클래스 대신 인터페이스를 사용해서 DB 커텍션을 가져와 사용하도록 수정한 UserDao 코드

ConnectionMaker 인터페이스를 사용하도록 개선한 UserDao


public class UserDao {
    private ConnectionMaker connectionMaker;
    // 인터페이스를 통해 오브젝트 접근하므로 구체적인 클래스 정보 를 알 필요가 없다.

    public UserDao() {
        connectionMaker = new DConnectionMaker();
        // 앗 그런데 여기에는 클래스 이름이 나오네!!
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        // 인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀐다고 해도 메소드 이름이 변경될 걱정은 없다.
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c= connectionMaker.makeConnection();
        ...
    }

}

카테고리:

업데이트:

댓글남기기