본문 바로가기
Back-End/토비의 스프링

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

by Havi 2016. 12. 29.

스프링은 자바를 기반으로 한 기술이고 자바는 객체지향 프로그래밍을 가장 중요한 가치로 두고 잇다. 스프링의 핵심철학은 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가는 것이다. 그래서 스프링이 가장 관심을 많이 두는 대상은 오브젝트이다.

  • 오브젝트가 생성되고 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정에 대한 고찰이 필요
  • 더 나아가 오브젝트가 어떻게 설계되는지
  • 어떤 단위로 만들어지며 어떤 과정을 통해 자신의 존재를 드러내고 등장해야 하는지

이러한 오브젝트의 관심은 설계와 구현에 관한 여러가지 응용 기술의 관심으로 발전하게 된다.

  • 객체지향 설계의 기초와 원칙
  • 다양한 목적을 위해 재활용 가능한 설계 방법인 디자인 패턴
  • 좀 더 깔끔한 구조가 되도록 지속적으로 개선해나가는 작업인 리팩토링
  • 오브젝트가 기대한 대로 동작하고 있는지를 효과적으로 검증하는 데 쓰이는 단위 테스트

1.1 초난감 DAO

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

1)평범한 User domain class를 만듭니다.

자바빈: 원래 비주얼 툴에서 조작 가능한 컴포넌트를 말한다. 이제는 비주얼 컴포넌트라기 보다는 다음 두가지 관례를 따라 만들어진 오브젝트를 가리킨다.

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

cf.리플렉션: 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법을 말한다. 투영, 반사 라는 사전적인 의미를 지니고 있다

2)UserDao

  • 사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만듭니다.
  • JDBC의 일반적인 작업 순서
    • DB연결 위한 Connections call
    • SQL을 담은 Statement(or PreparedStatement)를 만들고 실행
    • 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트(User)에 옮김
    • 작업 후 Connection, Statement, ResultSet은 close() 호출
    • JDBC API 예외는 직접 처리나 throws를 통해 처리

3)DAO 테스트 코드

  • 자신의 코드에 대한 검증은 필수적
  • 결과값을 DB콘솔로 확인하기 보다는 조회된 결과값 ID를 사용하여 확인하는게 편리하다.
  • 테스트 코드를 사용하여 DB 설정과 Connection 정보, users 테이블 등록 여부를 확인
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDaoTest {
    UserDao data = new UserDao();

    User user = new User();
    user.setId("havi");
    user.setName("영");
    user.setPassword("pass");

    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의 분리

  • 개발자가 객체를 설계할 때 가장 중요시 해야 할 점은 미래의 변화를 어떻게 대비할 것인가이다.
  • 지혜로운 개발자는 미래를 위해 설계하고 개발한다.
  • 흔히 객체지향은 실세계를 최대한 가깝게 모델링 해낼 수 있기 때문에 의미가 있다고 여겨진다.
  • 하지만 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장할 수 있는데 더 큰 의미가 있다.

관심사의 분리

  • 관심이 같은 것끼리는 하나의 객체 안으로 모이게 하고
  • 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리

UserDao의 관심사항

  • 어떤 드라이버, 어떤 로그인 정보를 쓰는지 등 DB 연결에 대한 관심사항
  • SQL 문장을 담을 Statement를 만들고 실행하는 것, 다른 관심사 끼리 분리가 가능하다.
  • close()를 실행하는 부분

리펙토링: 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업(기술)

상속을 통한 확장

public abstract class UserDao {
    public void add(User user) throws Exception {
        Connection c = getConnection();
        ...
    }   
    public void get(String id) throws Exception {
        Connection c = getConnection();
        ...
    }
    public abstract Connection getConnection() throws Exception {
        SQLException;
    }
}
public class NUserDao extends UserDao {
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        //N사 DB connection 생성 코드
    }
}

public class EUserDao extends UserDao {
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        //E사 DB connection 생성 코드
    }
}
  • 기능의 일부를 추상 메소나, 오버라이딩 가능한 protected 메소드 등으로 만든 뒤 이런 메소드들을 필요에 맞게 구현해서 사용하는 디자인 패턴을 템플릿 메소드 패턴이라 한다.
    • 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되거나 확장되는 기능은 서브클래스에 만들도록 한다.
  • getConnection() 메소드는 어떤 Connection 클래스의 오브젝트를 어떻게 사용할 것인지를 결정하게 하는데 이를 팩토리 메소드 패턴이라 한다.
    • 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해두는 방법
    • 즉, 슈퍼클래스의 기본 코드에서 독립시키는 방법
  • 팩토리 메소드 패턴은 어떤 클래스의 인스턴스를 만드는 결정을 서브클래스에서 하게끔 한다.

팩토리 메소드 패턴 vs 추상 팩토리 패턴: 템플릿 메소드 패턴을 사용했다는 점은 같지만 팩토리 메소드 패턴은 클래스를 이용하여 객체를 만들고, 추상 팩토리 패턴은 객체 구성을 통해 객체를 만든다.


디자인 패턴: 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용하는 재사용 가능한 솔루션이다. 객제치향적 설계는 대부분 비슷한데, 그 이유는 클래스 상속이나 오브젝트 합성으로 이루어지기 때문이다. 패턴의 핵심은 적용할 상황, 해결해야 할 문제, 솔루션의 구조와 가장 중요한 핵심이 담긴 목적과 의도가 무엇인지를 아는게 중요하다.

1.3 DAO의 확장

상속을 사용한 방법은 다른 클래스의 상속을 허용치 않기 때문에(다중상속 X) 한계가 있다. 지금까지 데이터 액세스 로직 구현과 DB 연결의 관점에서 클래스를 분리시켰는데 이를 더 확장하는 방법에 대해 알아보자.
DB 컨넥션과 관련된 부분은 아에 별도의 클래스로 담는다.

public abstract class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws Exception {
        Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }   
    public void get(String id) throws Exception {
        Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }
}

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        ...
    }
}
  • 문제점
    • 이는 연결에 대한 분리는 되지만 만약 E사가 다른 이름의 메소드와 연결방법을 사용하게 되다면 코드의 양은 너무 커진다.(ex. simpleConnectionMaker.openEConnection)
    • DB 컨넥션을 제공하는 클래스가 어떤 것인지 UserDao가 구체적으로 알고 있어야 한다.
  • 따라서 UserDao는 DB 컨넥션을 담당하는 부분에 종속되어 버린다.
  • 해결방법으로는 인터페이스가 있다.

인터페이스의 도입

public interface ConnectionMaker {
    Connection makeNewConnection() throws Exception;
}

public class EConnectionMaker implements ConnectionMaker {
    ...
    public Connection makeConnection() throws Exception {
        //E사의 코드
    }
}

public abstract class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao() {
        connectionMaker = new EConnectionMaker(); //하지만 여기에 이름이 나온다...ㅠㅠ
    }

    public void add(User user) throws Exception {
        Connection c = connectionMaker.makeNewConnection();
        ...
    }   
    public void get(String id) throws Exception {
        Connection c = connectionMaker.makeNewConnection();
        ...
    }
}

관계설정 책임의 분리

  • 인터페이스를 사용하여 역할을 분리하였지만 여전히 ConnectionMaker는 완전한 분리가 되지 못했다.
  • UserDao의 생성자 메소드에서 EConnectionMaker를 생성하기 때문이다.
  • 이를 클라이언트로 분리해 내는게 지금의 숙제라 할 수 있다.(클라이언트는 사용하는 오브젝트를 가리키며 여기서는 main 메소드로 생각하면 된다.)
public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        ConnectionMaker connectionMaker = new EConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
        ...
    }
}
  • 위의 코드를 보면 main 메소드가 클라이언트가 되어 UserDaon의 생성자에서 생성했던 ConnectionMaker 부분을 위임받았다.
  • 생성된 다양한 타입의 connectionMaker를 UserDao의 파라미터로 전송할 수 있다.
  • 이렇게 함으로서 각각의 역할과 관심에 따라 완변한 분리가 가능해 졌다!

원칙과 패턴

  • 개방 폐쇄 원칙(OCP): 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다. 즉, UserDao는 DB 연결 방법에는 얼마든지 열려있지만 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않는다.
  • 높은 응집도: 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것. 즉, 변경이 일어날 때 많은 부분이 함께 바뀐다는 것은 응집도가 높다고 말할 수 있다.
  • 낮은 결합도: 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도. 즉, 느슨하게 연결된 형태를 유지하는 것이 중요하다.
    • ConnectionMaker 인터페이스의 도입으로 DB 연결 기능이 바뀌더라도 DAO의 코드는 변경될 필요가 없어졌다.
    • 결합도가 낮아진 것.

전략 패턴

  • 개선한 UserDaoTest - UserDao - ConnectionMaker 구조를 말한다.
  • UserDao는 전략 패턴의 컨텍스트에 해당한다.
  • UserDaoTest는 클라이언트의 필요성에 대해서 역할을 분명히 하고 있다.

1.4 제어의 역전(IOC)

지금까지 UserDaoTest를 통해 UserDao가 직접 담당하던 기능, 즉 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능을 떠맡았다. 그런데 원래의 UserDaoTest는 기능이 잘 동작하는지 테스트하도록 만든것이 아닌가? 이제 UserDao와 ConnectionMaker 구현 클래스의 오브젝트를 만드는 것을 분리하고 그렇게 만들어진 두 개의 오브젝트가 연결되서 사용될 수 있도록 관계를 맺어주자.

오브젝트 팩토리

  • 팩토리: 분리시킬 기능을 담당할 클래스.(추상 팩토리 패턴 or 팩토리 메소드 패턴과는 다름)
  • UserDaoTest에 담겨있는 UserDao, ConnectionMaker 관련 생성 작업을 DaoFactory라는 클래스를 만들어 옮기자.
public class DaoFactory {
    ConnectionMaker connectionMaker = new EConnectionMaker();
    UserDao userDao = new UserDao(connectionMaker);
    return userDao;
}

public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundExceptioin, SQLException {
        UserDao dao = new DaoFactory().userDao();
        ...
    }
}
  • 이제 DaoFactory에서 각각 모듈의 핵심적인 데이터 로직과 기술 로직의 관계를 정의하는 책임을 떠맡았다
  • 이러한 팩토리의 가장 큰 장점은 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다는 데 가장 의미가 있다

오브젝트 팩토리의 활용

DaoFactory에 UserDao가 아닌 다른 DAO의 생성 기능을 넣으면 어떻게 될까? 예를 들어 AccountDao, MessageDao 등을 만들었다고 해보자.

public class DaoFactory {
    public UserDao UserDao() {
        return new UserDao(connectioinMaker());
    }

    public UserDao accountDao() {
        return new UserDao(connectioinMaker());
    }

    public UserDao messageDao() {
        return new UserDao(connectioinMaker());
    }

    //각각의 생성자를 만들필요없도록 중복코드를 제거하였다
    public ConnectioinMaker connectioinMaker() {
        return new EConnectionMaker();
    }
}

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

일반적으로 프로그램의 흐름은 main() 메소드가 시작지점으로서 사용할 오브젝트를 결정하고, 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 호출하고, 그 메소드 안에서 다음에 사용할 것을 결정하는 식의 작업을 반복한다. 제어의 역전이란 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다.

  • 서블릿을 예로 들면 일반적인 프로그래밍 실행 순서와는 다르게 그 실행을 개발자가 직접 제어할 수 있는 방법이 없다. main() 메소드가 있어서 직접 실행할 수도 없다.
  • 대신 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에서 서블릿 클래스의 오브젝트를 만들고 그 안의 메소드를 호출한다.
  • JSP, EJB처럼 컨테이너 안에서 동작하는 구조는 간단하지만 제어의 역전 개념이 적용되어 있다고 볼 수 있다.
  • 프레임워크도 제어의 역전 개념이 적용된 기술이다.
    • 프레임워크는 애플리케이션 코드가 프레임워크에 의해 사용된다.
    • 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 하는 방식이다.
  • 우리가 만든 UserDao와 DaoFactory에도 IOC가 적용되어 있다.
    • 자신이 어떤 ConnectionMaker를 만들고 결정할 지를 DaoFactory에 넘겼으니 UserDao는 이제 수동적인 존재가 되었다.
    • 관심을 분리하고 책임을 나누고 유연하게 확장 가능한 구조로 만들기 위해 DaoFactory를 도입했던 과정이 바로 IOC를 적용하는 작업이다.

1.5 스프링의 IOC

스프링의 핵심을 담당하는 건 빈 팩토리 또는 애플리케이션 컨텍스트라고 불리는 것이다. 이 두 가지는 DaoFactory가 하는 일을 좀 더 일반화한 것이라고 설명할 수 있다.

애플리케이션 컨텍스트와 설정정보

  • 빈(Bean)
    • 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트
    • 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전(IOC)이 적용된 오브젝트를 말한다.
  • 이러한 빈의 생성과 관계설정 같은 제어를 담당하는 IOC 오브젝트를 빈 팩토리라 부른다. 보통은 빈 팩토리보다 이를 더 확장한 애플리케이션 컨텍스트를 주로 사용한다.(빈 팩토리 == 애플리케이션 컨텍스트)
  • 앞에서의 DaoFactory가 애플리케이션 컨텍스트와 그 설정정보라 생각하면 된다. 그 자체로는 애플리케이션 로직을 담당하지는 않지만 IOC방식을 이용해 컴포넌트를 생성하고 사용할 관계를 맺어주는 책임을 담당하고 있다.

DaoFactory를 사용하는 애플리케이션 컨텍스트

  • 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라 인식하도록 @Configuration을 추가한다.
  • userDao()는 오브젝트를 생성하고 초기화해서 돌려주는 것이니 @Bean이 붙는다.(connectionMaker()도 동일하다.)
@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }
}

이제 이를 사용하는 애플리케이션 컨텍스트를 만들어보자. 애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트이다. ApplicationContext를 구현한 클래스는 여러가지가 있지만 @Configuration이 붙은 자바코드를 설정정보로 사용하는 AnnotationConfigApplicationContext를 이용하면 된다.

  • getBean() 메소드는 ApplicationContext를 관리하는 오브젝트를 요청하는 메소드이다.
  • userDao라는 이름의 메소드가 붙였는데 이 메소드 이름이 바로 빈의 이름이다.
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);
        ...
    }
}


댓글0