1장 오브젝트와 의존관계

Jiwon An·2021년 10월 18일
0

백엔드/스프링

목록 보기
1/6

들어가며

스프링은 자바 엔터프라이즈 애플리케이션 개발에 사용되는 애플리케이션 프레임워크이다. 애플리케이션 프레임워크는 애플리케이션 개발을 빠르고 효율적으로 할 수 있도록 애플리케이션의 바탕이 되는 틀과 공통 프로그래밍 모델, 기술 API 등을 제공해준다.

애플리케이션의 기본 틀 - 스프링 컨테이너(애플리케이션 컨텍스트)

  • 스프링 런타임 엔진
  • 설정정보를 참고해 애플리케이션을 구성하는 오브젝트를 생성하고 관리
  • 독립적으로 동작할 수도 있지만 보통 웹 모듈에서 동작하는 서비스나 서블릿으로 등록해서 사용

공통 프로그래밍 모델 - IoC/DI, 서비스 추상화, AOP

  • 프로그래밍 모델 : 애플리케이션을 구성하는 오브젝트가 생성되고 동작하는 방식에 대한 틀
  • IoC/DI : 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델
    • 유연하고 확장성이 뛰어난 코드를 만들 수 있게 도와주는 객체 지향 설계 원칙과 디자인 패턴의 핵심 원리를 담고 있음
    • 스프링의 가장 중요한 기술
  • 서비스 추상화
    • 환경, 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있게 해주는 기술
    • 구체적인 기술과 환경에 종속되지 않도록 유연한 추상 계층을 두는 방법
  • AOP
    • 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델
    • AOP를 이용해 다양한 엔터프라이즈 서비스를 적용하고도 깔끔한 코드를 유지할 수 있게 해줌

기술 API

  • 스프링은 엔터프라이즈 애플리케이션을 개발의 다양한 영역에 바로 활용할 수 있는 방대한 양의 기술 API를 제공
  • 스프링의 모든 기술은 표준 자바 엔터프라이즈 플랫폼(Java EE)에 기반을 두고 있음
  • UI 작성, 웹 프레젠테이션 계층, 비즈니스 서비스 계층, 기반 서비스 계층, 도메인 계층, 데이터 액세스 계층 등에서 필요한 주요 기술을 스프링에서 일관된 방식으로 사용할 수 있도록 지원해주는 기능과 전략 클래스 등을 제공
  • 스프링이 제공하는 API와 지원 기술은 몯 스프링의 프로그래밍 모델에 따라 작성되었기 때문에, 이를 가져다 쓰는 것만으로도 스프링의 프로그래밍 모델을 코드에 자연스럽게 적용할 수 있음

스프링을 사용한다는 것은 바로 이 세 가지 요소를 적극적으로 활용해서 애플리케이션을 개발한다는 뜻
클래스는 스프링 컨테이너 위에서 오브젝트로 만들어져 동작하게 만들고, 코드는 스프링의 프로그래밍 모델을 따라서 작성하고, 엔터프라이즈 기술을 사용할 때는 스프링이 제공하는 기술 API와 서비스를 활용하도록 해주면 된다.

스프링의 가치

  • 단순함
    • 스프링은 가장 단순한 객체지향적인 개발 모델인 POJO 프로그래밍을 주장한다.
  • 유연성
    • 스프링은 프레임워크를 위한 프레임워크 또는 여러 프레임워크를 함께 사용하게 해주는 접착 프레임워크라고도 불림
    • 스프링 개발 철학 : "항상 프레임워크 기반의 접근 방법을 사용하라"

스프링 학습 방법

  • 스프링의 핵심 가치와 원리에 대한 이해
    • 스프링의 핵심 가치, 그것을 가능하도록 도와주는 세 가지 핵심 기술, 프로그래밍 모델을 먼저 자세히 공부하고 일관된 방식으로 이해할 수 있는 눈 갖기
  • 스프링의 기술에 대한 지식과 선택 기준 정립
    • 스프링이 제공하는 기술의 종류와 접근 방법을 충분히 살펴보고, 선택의 기준을 마련해 상황에 맞는 최선의 기술과 접근 방법을 선택할 수 있어야 한다.
    • 스프링은 어떤 분야와 기술환경에도 적용 가능하도록 기술 영역별로 매우 폭넓은 접근 방법을 제공하기 때문에 그중에서 어떤 것을 선택할지는 개발자의 몫
  • 스프링의 적용과 확장
    • 스프링의 다양한 기술을 어떻게 실제 애플리케이션 개발에 어떤 식으로 적용해야 하는지를 공부
    • 스프링에 제공하는 기능을 그대로 사용하는 것 외에도 그것을 확장하거나 추상화해서 사용하는 방법을 알아야 함
    • 스프링을 효과적으로 사용하는 기업과 개발팀에선 스프링을 기반으로 프레임워크를 만들어서 사용함

1. 초난감 DAO

  • 스프링의 핵심 철학 : 자바 엔터프라이즈 기술의 혼란 속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로부터 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자
  • 오브젝트 : 스프링이 가장 관심을 많이 두는 대상
  • 오브젝트에 대한 관심은 오브젝트의 기술적인 특징과 사용 방법을 넘어서 오브젝트의 설계로 발전된다.
  • 스프링은 오브젝트를 어떻게 효과적으로 설계, 구현, 사용하고, 이를 개선해나갈 것인가에 대한 명쾌한 기준을 마련해준다.

들어가기 전에

  • 사용자 정보를 JDBC API를 통해 DB에 저장하고 조회할 수 있는 간단한 DAO를 하나 만들어보자.
    • DAO(Data Access Object)
      DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트

1.1 User

  • 사용자 정보를 저장할 때는 자바빈 규약을 따르는 오브젝트를 이용하면 편리하다.

    자바빈(JavaBean) 규약
    원래 비주얼 툴에서 조작 가능한 컴포넌트 즉, 비주얼 컴포넌트라고 불렸지만 이젠 다음 두 자기 관례를 따라 만들어진 오브젝트를 가리킨다. 간단히 이라고 하기도 한다.

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

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;
    }
}

이제 User 오브젝트에 담긴 정보가 실제로 보관될 DB의 테이블을 만들어보자.

1.2 UserDao

사용자 정보를 DB에 넣고 관리할 수 있는 DAO 클래스를 만들어보자.
사용자 정보 등록, 수정, 삭제와 같은 각종 조회 기능을 만들어야겠지만,
일단 사용자 생성(add), 아이디 가지고 사용자 정보를 읽어오는(get) 두 개의 메서드를 생성하자.

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

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

일단 예외는 모두 메서드 밖으로 던져버리는 편이 간단하다.

package com.springbook.user.dao;

...

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("insert into users(id, name, password) values (?, ?, ?)");
        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;
    }
}

DAO를 테스트하는 방법 중 가장 단순한 방법은 DAO의 기능을 사용하는 웹 애플리케이션을 만들어 서버에 배치하고, 웹 브라우저를 통해 DAO 기능을 사용해보는 것이지만 간단한 UserDao 코드가 동작함을 확인하기 위한 작업치고는 너무 부담이다.

1.3 main()을 이요한 DAO 테스트 코드

만들어진 코드의 기능을 검증하고자 할 때 사용할 수 있는 가장 간단한 방법은 오브젝트 스스로 자신을 검증하도록 만들어주는 것이다. 모든 클래스에는 자신을 엔트리 포인트로 설정해 직접 실행 가능하게 해주는 static 메서드 main() 있다.

  • 그전에 gradle에 mysql-connector-java 를 추가해주자.
public static void main(String[] args) throws ClassNotFoundException, SQLException{
        UserDao dao = new UserDao();

        User user = new User();
        user.setId("whiteship");
        user.setName("백기선");
        user.setPassword("married");

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

        SpringApplication.run(SpringbookApplication.class, args);
    }

2. DAO의 분리

2.1 관심사의 분리

오브젝트에 대한 설계와 이를 구현한 코드는 끊임없이 변한다.
애플리케이션이 기반을 두고 있는 기술도 시간이 지남에 따라 변하고 운영하는 환경도 변화한다.
그래서 개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.
그러면 어떻게 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있었을까? 그것은 분리와 확장을 고려한 설계가 있었기 때문이다.

객체지향 설계와 프로그래밍이 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다. 객체지향 기술은 흔히 실세계를 최대한 가깝게 모델링해낼 수 있는데 의미가 있다. 하지만 그보다는 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는 데 더 큰 의미가 있다.

먼저, 분리에 대해 생각해보자.
모든 변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어난다.
문제는, 변화는 대체로 집중된 한 가지 관심에 대해 일어나지만 그에 따른 작업은 한 곳에 집중되지 않는 경우가 많다는 점이다.
변화가 한 번에 한 가지 관심에 집중돼서 일어난다면, 우리가 준비해야 할 일은 한 가지 관심이 한 군데에 집중되게 하는 것이다. 즉, 관심이 같은 것끼리는 모으고, 관심이 다른 것은 따로 떨어져 있게 하는 것이다.

관심사의 분리

프로그래밍의 기초 개념 중에 관심사의 분리라는 게 있다. 이를 객체지향에 적용해보면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게 하고, 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리하는 것이라 생각할 수 있다.

2.2 커넥션 만들기의 추출

2.2.1 UserDao의 관심사항

UserDaoadd() 메서드 하나에서만 적어도 세 가지 관심사항을 발견할 수 있다.

  • DB와 연결을 위한 커넥션을 어떻게 가져올까
    더 세분화하여 어떤 DB를 쓰고, 어떤 드라이버를 사용할 것이고, 어떤 로그인 정보를 쓰는데, 그 커넥션을 생성하는 방법은 또 어떤 것이다라는 것까지 구분해서 볼 수도 있다. 세부관심까지 분류하면 너무 복잡해지므로 일단은 DB 연결과 관련된 관심히 하나라고 보자.
  • 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
    여기서의 관심은 파라미터로 넘어온 사용자 정보를 Statement에 바인딩시키고, Statement에 담긴 SQL을 DB를 통해 실행시키는 방법이다. 파라미터를 바인딩하는 것과 어떤 SQL을 사용할지를 다른 관심사로 분리할 수도 있지만, 우선은 이것도 하나로 묶어 생각하자.
  • 작업이 끝나면 사용한 리소스인 StatementConnection오브젝트를 닫아줘서 공유 리소스를 시스템에 돌려주는 것

2.2.2 중복 코드의 메서드 추출

DB커넥션을 가져오는 코드는 다른 관심사와 섞여서 같은 add() 메서드에 담겨있다. 더 큰 문제는 add() 메서드에 있는 DB 커넥션을 가져오는 코드와 동일한 코드가 get()메서드에도 중복되어 있다는 점이다.

가장 먼저 할 일은 커넥션을 가져오는 중복된 코드를 분리하는 것이다. 중복된 DB 연결 코드를 getConnection()이라는 이름의 독립적인 메서드로 만들어둔다.

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("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }

DB 연결과 관련된 부분에 변경이 일어났을 경우, 앞으로는 getConnection() 이 한 메서드의 코드만 수정하면 된다.
이렇게 관심의 종류에 따라 코드를 구분해놓았기 때문에 한 가지 관심에 대한 변경이 일어날 경우 그 관심이 집중되는 부분의 코드만 수정하면 된다. 관심이 다른 코드가 있는 메서드에는 영향을 주지도 않을뿐더러, 관심 내용이 독립적으로 존재하므로 수정도 간단해졌다.

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

방금 한 작업은 UserDao의 기능에는 아무런 변화를 주지 않았다. 여러 메서드에 중복돼서 등장하는 특정 관심사항이 담긴 코드를 별도의 메서드로 분리해낸 것이다. 기능이 추가되거나 바뀐 것은 없지만 UserDao는 이전보다 훨씬 깔끔해졌고 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드가 됐다. 이런 작업을 리팩토링이라고 한다. 또한 위에서 사용한 getConnection()이라고 하는 공통의 기능을 담당하는 메서드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메서드 추출 기법이라고 부른다.

리팩토링
기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술이다. 리팩토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다.

현재 앞에서 만들어뒀던 main() 메서드 테스트는 두 번째부터는 무조건 예외가 발생한다. 테이블의 기본키인 id값이 중복되기 때문이다. 따라서 main() 메서드 테스트를 다시 실행하기 전에 User 테이블의 사용자 정보를 모두 삭제해줘야 한다.

2.3 DB 커넥션 만들기의 독립

만약 UserDao가 인기를 끌더니 N 사와 D 사에서 사용자 관리를 위해 이 UserDao를 구매하겠다는 주문이 들어왔다고 상상해보자. 문제는 N 사와 D 사가 각기 다른 종류의 DB를 사용하고 있고, DB 커넥션을 가져오는 데 있어 독자적으로 만든 방법을 적용하고 싶어한다는 점이다. 더욱 큰 문제는 UserDao를 구매한 이후에도 DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다는 점이다.

게다가, UserDao가 비밀기술이라 고객에게는 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶다. 과연 이런 경우에 UserDao 소스코드를 N 사와 D 사에 제공해주지 않고도 고객 스스로 원하는 DB 커넥션 생성 방식을 적용해가면서 UserDao를 사용하게 할 수 있을까?

2.3.1 상속을 통한 확장

이럴 땐 기존 UserDao 코드를 한 단계 더 분리하면 된다. 일단 UserDao에서 메서드의 구현 코드를 제거하고 getConnection()을 추상 메서드로 만들어 놓는다. 이러면 add(), get() 메서드에서 getConnection()을 호출하는 코드는 그대로 유지할 수 있다. 이제 이 추상 클래스인 UserDao를 N 사와 D 사에 판매한다.

포탈사들은 UserDao 클래스를 상속해 NUserDao 같은 서브클래스를 만들고 그 클래스에서 getConnection() 메서드를 원하는 방식대로 구현할 수 있다. 이렇게 하면 UserDao의 소스코드를 제공하지 않고 getConnection() 메서드를 원하는 방식으로 확장한 후에 UserDao의 기능과 함께 사용할 수 있다.

기존에는 같은 클래스에 다른 메서드로 분리됐던 DB 커넥션 연결이라는 관심을 이번엔 상속을 통해 서브클래스로 분리해버리는 것이다.

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

public abstract class UserDao{
	...
	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 {
        // N 사 DB Connection 생성코드
    }
}

수정한 코드를 보면 DAO의 핵심 기능인 어떻게 데이터를 등록하고 가져올 것인가(SQL 작성, 파라미터 바인딩, 쿼리 실행, 검색정보 전달)라는 관심을 담당하는 UserDao와, DB 연결 방법은 어떻게 할 것인가라는 관심을 담고 있는 NUserDao, DUserDao가 클래스 레벨로 구분되고 있다.
클래스 계층구조를 통해 두 개의 관심이 독립적으로 분리되면서 변경 작업은 한층 용이해졌다. 새로운 DB 연결 방법을 적용해야 할 때는 UserDao의 코드의 수정 없이 상속을 통해 확장해주기만 하면 된다.

이렇게 슈퍼클래스에 기본적인 로직의 흐름(커넥션 가져오기, SQL 생성, 실행, 반환)을 만들고, 그 기능의 일부를 추상 메서드나 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤 서브클래스에서 이런 메서드를 필요에 맞게 구현해서 사용하도록 하는 방법을 템플릿 메서드 패턴이라고 한다. 즉, UserDao는 어떤 기능을 사용한다는 데에, NUserDao는 어떤 식으로 기능을 제공하는 지에 관심이 있다.

또한, UserDao이 getConnection() 메서드는 Connection 타입 오브젝트를 생성한다는 기능을 정의해놓은 추상 메서드다. 그리고 UserDao의 서브클래스의 getConnection() 메서드는 어떤 Connection 클래스의 오브젝트를 어떻게 생성할 것인지 결정하는 방법이라고도 볼 수 있다. 이렇게 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메서드 패턴이라고 부르기도 한다. 즉,UserDao는 Connection 인터페이스 타입의 오브젝트라는 것만, NUserDao는 어떤 방법으로 Connection 오브젝트를 만들어내는지에 관심이 있다. NUserDao와 DUserDao가 모두 같은 종류의 Connection 구현 클래스의 오브젝트를 리턴할 수도 있다. 그래도 오브젝트를 생성하는 방식이 다르다면, 이는 팩토리 메서드 패턴으로 이해할 수 있다.

템플릿 메서드 패턴
상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. 슈퍼 클래스에서는 미리 추상 메서드 또는 오버라이드 가능한 메서드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메서드를 만든다. 슈퍼클래스에서 디폴트 기능을 정의해두고나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메서드를 훅(hook)메서드라고 한다. 서브클래스에선 추상 메서드를 구현하거나, 훅 메서드를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.

팩토리 메서드 패턴
팩토리 메서드 패턴도 템플릿 메서드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 그래서 구조도 비슷하다. 슈퍼클래스 코드에서는 서브클래스에서 구현할 메서드를 호출해서 필요한 타입의 오브젝트를 가져와 사용한다. 서브 클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에선 알지 못한다. 서브클래스는 다양한 방법으로 오브젝트를 생성하는 베서드를 재정의할 수 있다. 이렇게 서브클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메서드를 팩토리 메서드라고 하고, 이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법을 팩토리 메서드 패턴이라고 한다.

이렇게 템플릿 메서드 패턴 또는 팩토리 메서드 패턴으로 관심사항이 다른 코드를 분리해내고, 서로 독립적으로 변경 또는 확장할 수 있도록 만드는 것은 간단하면서도 매우 효과적인 방법이다.

하지만 이 방법은 상속을 사용했다는 단점이 있다.

  • 자바는 다중상속을 허용하지 않는데, UserDao가 단지 커넥션 객체를 가져오는 방법을 분리하기 위해 상속구조로 만들어버리면, 후에 다른 목적으로 USerDao에 상속을 적용하기 힘들다.
  • 상속관계는 두 가지 다른 관심사에 대해 긴밀한 결합을 허용한다.
    • 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다.
  • 확장된 기능인 DB 커넥션을 생성하는 코드를 다른 DAO 클래스에 적용할 수 없다.
    • 만약 UserDao를 상속한 DAO 클래스들이 계속 만들어진다면 getConnection()의 구현 코드가 매 DAO 클래스마다 중복돼서 나타나는 심각한 문제가 발생할 것이다.

3. DAO의 확장

지금까지 데이터 액세스 로직을 어떻게 만들 것인가와 DB 연결을 어떤 방법으로 할 것인가라는 두 개의 관심을 상하위 클래스로 분리시켰다. 이 두 개의 관심은 변화의 성격 즉, 변화의 이유와 시기, 주기 등이 다르다.
추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꿔서 쓸 수 있게 만든 이유는 바로 이렇게 변화의 성격이 다른 것을 분리해서, 서로 영향을 주지 않은 채로 각각 필요한 시점에 독립적으로 변경할 수 있게 하기 위해서다. 그러나 상속은 단점이 많다.

3.1 클래스의 분리

이번에는 아예 상속관계도 아닌 완전히 독립적인 클래스로 만들어보겠다. DB 커넥션과 관련된 부분을 서브클래스가 아닌 아예 별도의 클래스로 담고, 이를 UserDao가 이용하게 하면 된다.

SimpleConnectionMaker라는 새로운 클래스를 만들고 DB 생성 기능을 그 안에 넣는다. 그리고 UserDao는 new 키워드를 사용해 SimpleConnectionMaker 클래스의 오브젝트를 만들어두고, 이를 add(), get() 메서드에서 사용하면 된다.

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

public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }
}

독립시킨 DB 연결 기능인 SimpleConnectionMaker

public class UserDao {
    private SimpleConnectionMaker scm = new SimpleConnectionMaker();
    
    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = scm.makeNewConnection();
        ...
    }

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

이번엔 N 사와 D 사에 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던 게 다시 불가능해졌다. 왜냐하면 UserDao의 코드가 SimpleConnectionMaker라는 특정 클래스에 종속되어 있기 때문에 상속을 사용했을 때 처럼 UserDao 코드의 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없다.

이렇게 클래스를 분리한 경우에도 상속때와 마찬가지로 자유롭게 확장하려면 두 가지 문제를 해결해야 한다.

  • SimpleConnectionMaker의 메서드 이름이 N 사의 메서드와 다르면 일일히 변경해야 한다.
  • DB 커넥션을 제공하는 클래스가 UserDao가 구체적으로 알고 있어야 한다는 점이다.

이런 문제의 근본적인 원인은 UserDao가 바뀔 수 있는 정보, 즉 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문이다. 어떤 클래스가 쓰일지, 그 클래스에서 커넥션을 가져오는 메서드는 이름이 뭔지까지 일일이 알고 있어야 한다.

3.2 인터페이스의 도입

이에 대한 가장 좋은 해결책은 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다. 추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.

인터페이스는 어떤 일을 하겠다는 기능만 정의해놓은 것이지 구현 방법은 나타나 있지 않다.
UserDao가 인터페이스를 사용하게 된다면 인터페이스의 메서드를 통해 알 수 있는 기능에만 관심을 가지면 되지, 그 기능을 어떻게 구현했는지에는 관심을 둘 필요가 없다.

ConnectionMaker 인터페이스

public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

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

public class UserDao {
    private ConnectionMaker cm;

    public UserDao() {
        this.cm = new DConnection();
    }

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

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

고객에게 납품할 때는 UserDao 클래스와 함께 ConnectionMaker 인터페이스도 전달한다. 납품받은 포털사의 개발자는 ConnectionMaker 인터페이스를 구현한 클래스를 만들고, 자신들의 DB 연결 기술을 이용해 DB 커넥션을 가져오도록 메서드를 작성해주면 된다.

하지만 UserDao 코드를 자세히 살펴보면 DConnection이라는 클래스 이름이 보인다. UserDao의 다른 모든 곳에서는 인터페이스를 이용하게 만들어서 DB 커넥션을 제공하는 클래스에 대한 구체적인 정보는 모두 제거 가능했지만, 초기에 한 번 어떤 클래스의 오브젝트를 사용할지를 결정하는 생성자의 코드는 제거되지 않고 남아 있다.

3.3 관계설정 책임의 분리

이 때문에 인터페이스를 이용한 분리에도 불구하고 여전히 UserDao 변경 없이는 DB 커넥션 기능의 확장이 자유롭지 못한데, 그 이유는 UserDao 안에 분리되지 않은, 또 다른 관심사항이 존재하고 있기 때문이다.

UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 new DConnectionMaker()라는 코드가 있다. 이 코드는 간단히 말해 UserDaoUserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다.
이 관심사를 담은 코드를 UserDao에서 분리하지 않으면 UserDao는 결코 독립적으로 확장가능한 클래스가 될 수 없다.

UserDao의 클라이언트 오브젝트가 바로 제3의 관심사항인 UserDao와 ConnectionMaker 구현 클래스의 관계를 결정해주는 기능을 분리해서 두기에 적절한 곳이다.

여기서의 클라이언트는 UserDao를 사용하는 오브젝트이고 UserDao는 서비스를 제공하는 것이다.

UserDao의 클라이언트에서 UserDao를 사용하기 전에, 먼저 UserDao 오브젝트와 특정 클래스로부터 만들어진 ConnectionMaker 오브젝트 사이에 관계를 설정해줘보자.

클래스 사이를 설정해주는 것이 아닌 오브젝트 사이의 관계를 설정해주는 것이다. 클래스 사이에 관계가 만들어진다는 것은 한 클래스가 인터페이스 없이 다른 클래스를 직접 사용한다는 뜻이기 때문이다.

오브젝트 사이의 관계는 런타임 시에 한쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 만들어진다. 오브젝트 사이의 관계가 만들어지려면 (1) 직접 생성자를 호출해 직접 오브젝트를 만들거나 (2) 외부에서 만들어준 것을 가져오는 방법이 있다.
직접 오브젝트를 만들 필요는 없고, 외부에서 만든 오브젝트를 전달받으려면 메서드 파라미터나 생성자 파라미터를 이용하면 된다. 이때 파라미터의 타입을 전달받을 오브젝트의 인터페이스로 선언해뒀다고 한 경우, 해당 인터페이스 타입의 오브젝트라면 파라미터로 전달 가능하고, 파라미터로 제공받은 오브젝트는 인터페이스에 정의된 메서드만 이용한다면 그 오브젝트가 어떤 클래스로부터 만들어졌는지 신경 쓰지 않아도 된다.

또한, UserDao의 모든 코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가져서는 안되게 해야 한다. 그래야만 UserDao의 수정 없이 DB 커넥션 구현 클래스를 변경할 수 있다.

물론 UserDao 오브젝트가 동작하려면 특정 클래스의 오브젝트와 관계를 맺어야 하긴 하지만, 클래스 사이에 관계가 만들어진 것이 아닌 단지 오브젝트 사이에 다이내믹한 관계가 만들어지는 것이다.
UserDao 오브젝트가 DConnectionManager 오브젝트를 사용하게 하려면 두 클래스의 오브젝트 사이에 런타임 사용관계 또는 링크, 또는 의존관계라고 불리는 관계를 맺어주면 된다. 이러한 런타임 오브젝트 관계를 갖는 구조로 만들어주는 게 바로 클라이언트의 책임이다.

클라이언트는 자기가 UserDao를 사용해야 할 입장이기 때문에 UserDao의 세부 전략이라고도 볼 수 있는 ConnectionMaker의 구현 클래스를 선택하고, 선택한 클래스의 오브젝트를 생성해서 UserDao와 연결해줄 수 있다.

수정한 생성자

public UserDao(ConnectionMaker connectionMaker) {
    this.connectionMaker = connectionMaker;
}

DConnectionMaker를 생성하는 코드는 UserDao와 특정 ConnectionMaker 구현 클래스의 오브젝트 간 관계를 맺는 책임을 담당하는 코드였는데, 그것을 UserDao의 클라이언트에게 넘겨버렸다.

관계설정 책임이 추가된 UserDao 클라이언트인 main() 메서드

public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao dao = new UserDao(connectionMaker);
        ...
    }
}

기존의 UserDao 클래스의 main() 메서드가 UserDao의 클라이언트였는데 좀 더 깔끔하게 구분하기 위해 UserDaoTest라는 클래스를 생성해서 분리했다.

UserDaoTestUserDaoConnectionMaker 구현 클래스와의 런타임 오브젝트 의존관계를 설정하는 책임을 담당해야 한다. 그래서 특정 ConnectionMaker 구현 클래스의 오브젝트를 만들고, UserDao 생성자 파라미터에 넣어 두 개의 오브젝트를 연결해준다.

이제는 UserDao의 변경 없이도 자유롭게 포털사들이 자신들을 위한 DB 접속 클래스를 만들어서 UserDao가 사용하게 할 수 있다. 즉, UserDao는 자신의 관심사이자 책임인 사용자 데이터 액세스 작업을 위해 SQL을 생성하고, 이를 실행하는 데만 집중할 수 있게 됐다.

3.4 원칙과 패턴

3.4.1 개방 폐쇄 원칙(OCP)

  • 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
  • UserDao는 DB 연결 방법이라는 기능을 확장하는 데는 열려 있다. UserDao에 전혀 영향주지 않고도 얼마든지 기능을 확장할 수 있게 되어 있는 동시에, UserDao 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향받지 않고 유지할 수 있으므로 변경에는 닫혀 있다.
  • 인터페이스를 이용하는 클래스는 확장에는 개방되어 있지만, 자신의 변화는 불필요하게 일어나지 않도록 굳게 폐쇄되어 있다.

3.4.2 높은 응집도와 낮은 결합도

  • 높은 응집도
    • 응집도가 높다는 것은 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다는 것이다.
      • 모듈의 일부분에만 변경이 일어나도 된다면, 모듈 전체를 파악해서 변경이 필요한 부분을 체크하고 변경되지 않는 부분에 미칠 영향 또한 확인해야 하는 이중 부담이 생긴다.
    • ConnectionMaker 인터페이스를 이용해 DB 연결 기능을 독립시킨 경우, ConnectionMaker 구현 클래스를 새로 만들기만 하면 된다.
      • 만약, 처음 만든 DAO에서 DB 커넥션 만드는 기능을 변경한다면 여러 관심사와 책임이 얽혀 있는 복잡한 코드에서 변경이 필요한 부분과 아닌 부분에 미칠 영향도 일일이 확인해야 한다.
  • 낮은 결합도
    • 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결된 형태를 유지해야 한다.
    • 느슨한 연결은 관계를 유지하는 데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없게 만들어주는 것이다.
      • 여기서 결합도란 '하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도'라고 설명할 수 있다.
    • 결합도가 낮아지면 변화에 대응하는 속도가 높아지고, 구성이 깔끔해진다. 또한 확장하기에도 매우 편리하다.
      • 반면 결합도가 높아지면 변경에 따르는 작업량이 많아지고, 변경으로 인해 버그 발생 가능성이 높아진다.
    • ConnectionMaker의 클래스를 결정하는 책임을 DAO의 클라이언트로 분리한 덕분에 사용할 ConnectionMaker 구현 클래스가 바뀌어도, DAO 클래스의 코드를 수정할 필요가 없게 됐다.

정리하자면, UserDao 클래스는 그 자체로 자신의 책임에 대한 응집도가 높다. ConnectionMaker 또한 자신의 기능에 충실하도록 독립돼서 순수한 자신의 책임을 담당하는 데만 충실할 수 있다.
동시에 UserDaoConnectionMaker의 관계는 인터페이스를 통해 매우 느슨하게 연결되어 있다. UserDao는 구체적인 ConnectionMaker 구현 클래스를 알 필요도 없고, 구현 방법이나 전략 등에 대해 신경 쓰지 않아도 된다. 꼭 필요한 관계만 ConnectionMaker라는 인터페이스를 통해 낮은 결합도로 최소한으로 연결되어 있다.

3.4.3 전략 패턴

  • 자신의 기능 맥락(context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴
    • 여기서 알고리즘이란 독립적인 책임으로 분리가 가능한 기능을 뜻한다.
  • 개선한 UserDaoTest-UserDao-ConnectionMaker 구조는 전략 패턴에 해당한다.
    • UserDao는 전략 패턴의 컨텍스트에 해당한다. 컨텍스트는 자신의 기능을 수행하는 데 필요한 기능 중에서 변경 가능한, DB 연결 방식이라는 알고리즘을 ConnectionMaker라는 인터페이스로 정의하고, 이를 구현한 클래스, 즉 전략을 바꿔가면서 사용할 수 있게 분리했다.
    • 전략 패턴의 적용 방법을 보면 컨텍스트(UserDao)를 사용하는 클라이언트(UserDaoTest)는 컨텍스트가 사용할 전략(ConnectionMaker를 구현한 클래스)을 컨텍스트의 생성자 등을 통해 제공해주는 게 일반적이다.

4. 제어의 확장(IoC)

4.1 오브젝트 팩토리

원래 UserDaoTestUserDao의 기능이 잘 동작하는지 테스트하려고 만든 것이다. 그런데 지금은 또 다른 책임까지 떠맡고 있으니 성격이 다른 책임이나 관심사는 분리하자. 이렇게 분리될 기능은 (1) UserDaoConnectionMaker 구현 클래스의 오브젝트를 만드는 것과, (2) 그렇게 만들어진 두 개의 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺어주는 것이다.

4.1.1 팩토리

분리시킬 기능을 담당할 클래스의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것인데, 이런 일을 하는 오브젝트를 흔히 팩토리(factory)라고 부른다.
단지 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하려는 목적으로 사용하는 것이다.

UserDao의 생성 책임을 맡은 팩토리 클래스 DaoFactory

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

팩토리를 사용하도록 수정한 UserDaoTest

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

UserDaoTest는 이제 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경 쓰지 않고 팩토리로부터 UserDao 오브젝트를 받아다가, 자신의 관심사인 테스트를 위해 활용하기만 하면 된다.

4.1.2 설계도로서의 팩토리

분리된 오브젝트들의 역할과 관계를 분석해보자. UserDaoConnectionMaker는 각각 애플리케이션의 핵심적인 데이터 로직과 기술 로직을 담당하고 있고, DaoFactory는 이런 애플리케이션의 오브젝트들을 구성하고 그 관계를 정의하는 책임을 맡고 있다. 전자가 실질적인 로직을 담당하는 컴포넌트라면, 후자는 애플리케이션을 구성하는 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할을 한다고 볼 수 있다.

설계도란 간단히 어떤 오브젝트가 어떤 오브젝트를 사용하는 지를 정의해놓은 코드라고 생각하면 된다.

이제 UserDao를 공급할 때 UserDao, ConnectionMaker, DaoFactory를 제공한다. 여기서 DaoFactory는 소스를 제공한다. 새로운 ConnectionMaker 구현 클래스로 변경이 필요하면 DaoFactory를 수정해서 변경된 클래스를 생성해 설정해주도록 코드를 수정해주면 된다. 여전히 우리의 핵심 기술이 담긴 UserDao는 변경이 필요 없으므로 안전하게 소스코드를 보존할 수 있다. 동시에 DB 연결 방식은 자유로운 확장이 가능하다.

DaoFactory를 분리했을 때 얻을 수 있는 장점 중 애플리케이션의 컴포넌트 역할을 하는 오브젝트와 애플리케이션의 구조를 결정하는 오브젝트를 분리했다는 데 가장 의미가 있다.

4.2 오브젝트 팩토리의 활용

DaoFactory에서 다른 DAO(AccountDao, MessageDao)의 생성 기능을 넣었다고 하자. 이 경우 UserDao를 새성하는 userDao() 메서드를 복사해서 만들면 어떤 ConnectionMaker 구현 클래스를 사용할 지 결정하는 기능이 중복돼서 나타난다.

이 역시 ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메서드로 뽑아내자. 처음 DAO코드에서 getConnection 메서드를 따로 분리해낸 것과 동일한 리팩토링 방법이다.

생성 오브젝트 코드 수정

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    public AccountDao accountDao() {
        return new AccountDao(connectionMaker());
    }

    public MessageDao messageDao() {
        return new MessageDao(connectionMaker());
    }
    
    public ConnectionMaker connectionMaker() {
        return new DConnectionMaker();
    }
}

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

일반적으로 프로그램 흐름은 main() 메서드와 같은 프로그램 시작 지점에서 다음 사용할 오브젝트 결정 -> 결정한 오브젝트 생성 -> 만들어진 오브젝트에 있는 메서드 호출 -> 그 오브젝트 메서드 안에서 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복된다. 이런 프로그램 구조에서 각 오브젝트는 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장한다. 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조다.

제어의 역전에서는 이런 제어 흐름의 개념을 거꾸로 뒤집어, 오브젝트가 자신이 사용할 오브젝트를 생성하지도, 자신이 사용할 오브젝트를 스스로 선택하지도 않는다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하여 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.

제어의 역전 개념이 적용된 예는 서블릿, 템플릿 메서드 패턴, 프레임워크 등이 있다.
그 중 프레임워크에 대해 설명하자면, 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다. 반면 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 보통 프레임워크 위에 개발한 클래스를 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식이다. 애플리케이션 코드가 프레임워크가 짜놓은 틀에서 수동적으로 동작해야 프레임워크라고 부를 수 있다.

우리가 만든 UserDaoDaoFactory에도 제어의 역전이 적용되어 있다.

  • ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 권한이 DaoFactory에 넘겨서 UserDao는 수동적인 존재가 되었다.
  • UserDao 자신도 팩토리에 의해 수동적으로 만들어지고 자신이 사용할 오브젝트도 DaoFactory가 공급해주는 것을 수동적으로 사용해야 할 입장이 되었다.
  • UserDaoTestDaoFactory가 만들고 초기화해서 자신에게 사용하도록 공급해주는 ConnectionMaker를 사용할 수밖에 없다.
  • 더욱이 UserDaoConnectionMaker의 구현체를 생성하는 책임도 DaoFactory가 맡고 있다.

이렇게 관심을 분리하고 책임을 나누고 유연하게 확장 가능한 구조로 만들기 위해 DaoFactory를 도입했던 과정이 바로 IoC를 적용하는 작업이었다. 제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다. DaoFactory는 오브젝트 수준의 가장 단순한 IoC 컨테이너 내지는 IoC 프레임워크라고 볼 수 있다.

5. 스프링의 IoC

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

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

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

  • 빈(bean) : 스프링에서 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트
    • 자바빈 또는 엔터프라이즈 자바빈(EJB)에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트이다.
    • 동시에 스프링 빈은 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리킨다.
  • 빈 팩토리(bean factory) : 스프링에서 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트
    • 보통 빈 팩토리보다는 이를 좀 더 확장한 애플리케이션 컨텍스트(application context)를 주로 사용한다.
    • 애플리케이션 컨텍스트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리이다.

빈 팩토리와 애플리케이션 컨텍스트라는 용어는 동일하다고 보면 된다.
앞으로 빈 팩토리라고 말할 때는 빈을 생성하고 관계를 설정하는 IoC의 기본 기능에 초점을 맞춘 것이고, 애플리케이션 컨텍스트라고 말할 때는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진이라는 의미가 좀 더 부각된다고 보면 된다.

애플리케이션 컨텍스트는 어떤 클래스의 오브젝트를 생성하고 어디에서 사용하도록 연결해줄 것인가 등에 관한 정보를 직접 담고 있진 않고, 대신 별도로 설정정보를 참고해서 빈(오브젝트)의 생성, 관계설정 등의 제어 작업을 총괄하는 범용적인 IoC 엔진 같은 것이라 볼 수 있다.

앞에서 DaoFactory 자체가 설정정보까지 담고 있는 IoC 엔진이다.

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

DaoFactory를 스프링의 빈 팩토리가 사용할 수 있는 본격적인 설정정보로 만들어보자.

  • 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration을 추가한다.
  • 오브젝트를 만들어주는 메서드에는 @Bean을 붙여준다.
    • userDao() : UserDao 타입 오브젝트를 생성하고 초기화해서 돌려주는 메서드
    • connectionMaker() : ConnectionMaker 타입의 오브젝트를 생성하는 메서드

스프링 빈 팩토리가 사용할 설정정보를 담은 DaoFactory 클래스

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

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

이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어보자.

  • 애플리케이션 컨텍스트를 만들 때 생성자 파라미터로 DaoFactory 클래스를 넣어준다.
  • ApplicationContext의 getBean() 메서드를 이용해 UserDao의 오브젝트를 가져온다.

애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트다. ApplicationConetxt를 구현한 클래스 중 DaoFactory처럼 @Configuration이 붙은 자바 코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다.

애플리케이션 컨텍스트를 적용한 UserDaoTest

public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        ApplicationContext ac = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao dao = ac.getBean("userDao", UserDao.class);
    }
}
  • getBean() : ApplicationContext가 관리하는 오브젝트를 요청하는 메서드
    • ApplicationContext에 등록된 빈의 이름을 같이 넘겨준다.
    • @Bean을 붙인 메서드의 이름이 바로 빈의 이름이 된다.
    • ex) "userDao" : userDao라는 이름의 빈을 가져온다는 것은 DaoFactory의 userDao() 메서드를 호출한 결과를 가져온다는 것이다.

왜 이름을 사용할까?
UserDao를 생성하는 방식이나 구성을 다르게 가져가는 메서드를 추가할 수 있기 때문이다.
ex) specialUserDao()라는 메서드라면, getBean("specialUserDao", UserDao.class)로 가져오면 된다.

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

기존에 오브젝트 팩토리를 이용했던 방식과 스프링의 애플리케이션 컨텍스트를 사용한 방식을 비교해보자.

오브젝트 팩토리에 대응되는 것이 스프링의 애플리케이션 컨텍스트다. 스프링에서는 이 애플리케이션 컨텍스트를 IoC 컨테이너라 하기도 하고, 간단히 스프링 컨테이너라고 부르기도 한다. 또는 빈 팩토리라고 부를 수도 있다. 애플리케이션 컨텍스트는 BeanFactory 인터페이스를 상속받은 ApplicationContext 인터페이스를 구현하므로 일종의 빈 팩토리이다.

애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다. 애플리케이션 컨텍스트는 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없고, 그런 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻는다. 때로는 외부의 오브젝트 팩토리에 그 작업을 위임하고 그 결과를 가져다가 사용하기도 한다.

@Configuration이 붙은 클래스는 이 애플리케이션 컨텍스트가 활용하는 IoC 설정정보다. 내부적으로는 애플리케이션 컨텍스트가 @Bean이 붙은 메서드를 호출해서 오브젝트를 가져온 것을 클라이언트가 getBean()으로 요청할 때 전달해준다.

정확히는, 애플리케이션 컨텍스트는 @Configuration이 붙은 클래스를 설정정보로 등록해두고, @Bean이 붙은 메서드의 이름을 가져와 빈 목록을 만들어둔다. 클라이언트가 이 애플리케이션 컨텍스트의 getBean() 메서드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메서드를 호출해서 오브젝트를 생성시킨 후 클라이언트에 돌려준다.

애플리케이션 컨텍스트를 사용했을 때의 장점

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.

    • DaoFactory처럼 IoC를 적용한 오브젝트가 추가될 때, 클라이언트가 이전처럼 어떤 팩토리 클래스를 사용해야 할지 알아야 하고, 필요할 때마다 팩토리 오브젝트를 생성해야 하는 번거로움이 없어진다.
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.

    • 애플리케이션 컨텍스트의 역할은 단지 오브젝트 생성과 다른 오브젝트와의 관계설정만이 전부가 아니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고, 이에 부가적으로 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정 방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다. 또, 빈이 사용할 수 있는 기반기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 제공해주기도 한다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

    • getBean() 메서드가 빈의 이름 외에도 타입만으로도 빈을 검색하거나 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수도 있다.

5.3 스프링 IoC의 용어 정리

빈(bean)

빈 또는 빈 오브젝트는 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 스프링이 사용하는 애플리케이션에서 만들어지는 모든 오브젝트 중 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.

빈 팩토리(bean factory)

스프링의 IoC를 담당하는 핵심 컨테이너를 가리킨다. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외에 부가적인 빈을 관리하는 기능을 담당한다. 보통은 빈 팩토리를 확장한 애플리케이션 컨텍스트를 이용한다. 빈 팩토리가 구현하고 있는 가장 기본적인 인터페이스는 BeanFactory이고 여기에 getBean()과 같은 메서드가 정의되어 있다.

애플리케이션 컨텍스트(application context)

빈 팩토리를 확장한 IoC 컨테이너다. 빈 팩토리의 빈을 등록하고 관리하는 기본적인 기능에서 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 빈 팩토리라고 부를 때는 주로 빈의 생성과 제어의 관점에서 이야기하는 것이고, 애플리케이션 컨텍스트라고 할 때는 스프링이 제공하는 애플리케이션 지원 기능을 모두 포함해서 이야기하는 것이라고 보면 된다. 애플리케이션 컨텍스트가 구현해야 하는 기본 인터페이스는 ApplicationContext이고 이것은 BeanFactory를 상속한다.

설정정보/설정 메타정보(configuration metadata)

애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 스프링의 설정정보는 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, 그보다 IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용된다. 애플리케이션 형상정보라고 부르기도 하고, 청사진이라고도 한다.

컨테이너(container) 또는 IoC 컨테이너

IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 한다. 후자는 주로 빈 팩토리의 관점에서 이야기하는 것이고, 그냥 컨테이너 또는 스프링 컨테이너라고 할 때는 애플리케이션 컨텍스트를 가리키는 것이라고 보면 된다. 컨테이너라는 말 자체가 IoC 개념을 담고 있기 때문에 애플리케이션 컨텍스트를 스프링 컨테이너라고 부르기도 한다. 때로는 컨테이너라는 말을 떼고 스프링이라고 부를 때도, 바로 이 스프링 컨테이너를 가리키는 것일 수 있다.

스프링 프레임워크

스프링 프레임워크는 IoC 컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 주로 사용한다. 그냥 스프링이라고 줄여서 말하기도 한다.

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

스프링의 애플리케이션 컨텍스트는 기존에 직접 만들었던 오브젝트 팩토리와는 중요한 차이점이 있다.
먼저, DaoFactory의 userDao()를 여러 번 호출했을 때 동일한 오브젝트가 돌아오는가를 알아보자. 오브젝트를 직접 콘솔에 출력하면 오브젝트별로 할당되는 고유한 값이 출력되는데, 이 값이 같으면 동일한 오브젝트라는 뜻이다.

오브젝트의 동일성과 동등성

  • 동일성
    • 두 개의 오브젝트가 완전히 같은 오브젝트라는 뜻으로, == 연산자를 이용해 비교한다.
    • 사실 하나의 오브젝트만 존재하는 것이고 두 개의 오브젝트 레퍼런스 변수를 갖고 있는 것이다.
  • 동등성
    • 두 개의 오브젝트가 동일한 정보를 담고 있다는 뜻으로, equals() 메서드를 이용해 비교한다.
    • 두 개의 각기 다른 오브젝트가 메모리상에 존재하는 것이다.

직접 생성한 DaoFactory 오브젝트 출력 코드

DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

System.out.println(dao1);
System.out.println(dao2);

실행 결과

com.springbook.user.dao.UserDao@1324409e
com.springbook.user.dao.UserDao@2c6a3f77

출력 결과에서 알 수 있듯이, 두 개는 각기 다른 값을 가진 동일하지 않은 오브젝트다. 즉, 오브젝트가 두 개가 생겼다는 사실을 알 수 있다.

스프링 컨텍스트로부터 가져온 오브젝트 출력 코드

ApplicationContext ac = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao dao3 = ac.getBean("userDao", UserDao.class);
UserDao dao4 = ac.getBean("userDao", UserDao.class);

System.out.println(dao3);
System.out.println(dao4);

실행 결과

com.springbook.user.dao.UserDao@5b38c1ec
com.springbook.user.dao.UserDao@5b38c1ec

두 오브젝트의 출력 값이 값으므로, getBean()을 두 번 호출해서 가져온 오브젝트가 동일하다는 사실을 알 수 있다.

우리가 만들었던 오브젝트 팩토리와 스프링의 애플리케이션 컨텍스트의 동작방식에 무엇인가 차이점이 있다. 스프링은 여러 번에 걸쳐 빈을 요청하더라도 매번 동일한 오브젝트를 돌려준다는 것인데, 왜 그럴까?

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

애플리케이션 컨텍스트는 우리가 만들었던 오브젝트 팩토리와 비슷한 방식으로 동작하는 IoC 컨테이너면서 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(singleton registry)이기도 하다.

스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다. 여기서 싱글톤이라는 것은 디자인 패턴에서 나오는 싱글톤 패턴과 비슷한 개념이지만 그 구현 방법은 확연히 다르다.

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

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

왜 스프링은 시싱글톤으로 빈을 만드는 것일까? 이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.

스프링이 처음 설계됐던 대규모의 엔터프라이즈 서버환경은 서버 하나당 최대로 초당 수십에서 수백 번씩 브라우저나 여타 시스템으로부터의 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경이었다. 그런데 매번 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다고 생각해보자. 아무리 자바의 오브젝트 생성과 가비지 컬렉션의 성능이 좋아졌다고 한들 이렇게 부하가 걸리면 서버가 감당하기 힘들다.

그래서 엔터프라이즈 분야에선 서비스 오브젝트라는 개념을 일찍부터 사용해왔고, 서블릿은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다. 스펙에서 강제하진 않지만, 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.

이렇게 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것이 싱글톤 패턴의 원리다. 따라서 서버환경에서는 서비스 싱글톤의 사용이 권장된다. 하지만 디자인 패턴에 소개된 싱글톤 패턴은 사용하기가 까다롭고 여러 가지 문제점이 있다.

싱글톤 패턴(Singleton Pattern)
싱글톤 패턴은 어떤 클래스를 애플리케이션 내에서 제한된 인스턴스 개수, 이름처럼 주로 하나만 존재하도록 강제하는 패턴이다. 이렇게 하나만 만들어지는 클래스의 오브젝트는 애플리케이션 내에서 전역적으로 접근이 가능하다. 단일 오브젝트만 존재해야 하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용한다.

싱글톤 패턴의 한계

자바에서 싱글톤을 구현하는 방법은 보통 이렇다.

  • 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
  • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의한다.
  • 스태틱 팩토리 메서드인 getInstance()를 만들고 이 메서드가 최초로 호출되는 시점에서 한번만 오브젝트가 만들어지게 한다. 생성된 오브젝트는 스태틱 필드에 저장된다. 또는 스태틱 필드의 초기값으로 오브젝트를 미리 만들어둘 수도 있다.
  • 한번 오브젝트(싱글톤)가 만들어지고 난 후에는 getInstance() 메서드를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.

싱글톤 패턴을 적용한 UserDao

public class UserDao {
    private static UserDao INSTANCE;
    private static ConnectionMaker connectionMaker;

    private UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
    
    public static synchronized UserDao getInstance() {
        if (INSTANCE == null) INSTANCE = new UserDao(???);
        return INSTANCE;
    }
    ...
}

UserDao에 싱글톤 패턴을 도입함으로서 생기는 문제

  • private로 바뀐 생성자는 외부에서 호출할 수가 없기 때문에 DaoFactory에서 UserDao를 생성하며 ConnectionMaker 오브젝트를 넣어주는 게 불가능해졌다.

일반적으로 싱글톤 패턴 구현 방식에 있는 문제

  • private 생성자를 갖고 있기 때문에 상속할 수 없다.
    싱글톤 패턴은 생성자를 private로 제한한다. 오직 싱글톤 클래스 자신만이 자기 오브젝트를 만들도록 제한하는 것이다. 문제는 private 생성자를 가진 클래스는 다른 생성자가 없다면 상속이 불가능하다는 점이다. 객체지향의 장점인 상속과 이를 이용한 다형성을 적용할 수 없다. 또한 상속과 다형성 같은 객체지향의 특징이 적용되지 않는 스태틱 필드와 메서드를 사용하는 것도 역시 동일한 문제를 발생시킨다.

  • 싱글톤은 테스트하기가 힘들다.
    싱글톤은 테스트하기가 어렵거나 테스트 방법에 따라 아예 테스트가 불가능하다. 싱글톤은 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 때 목 오브젝트 등으로 대체하기가 힘들다. 싱글톤은 초기화 과정에서 생성자 등을 통해 사용할 오브젝트를 다이내믹하게 주입하기도 힘들기 때문에 필요한 오브젝트는 직접 오브젝트를 만들어 사용할 수밖에 없다. 이런 경우 테스트용 오브젝트로 대체하기가 힘들다.

  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라서 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다. 여러 개의 JVM에 분산돼서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.

  • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
    싱글톤은 사용하는 클라이언트가 정해져 있지 않다. 싱글톤이 스태틱 메서드를 이용해 언제든지 싱글톤에 쉽게 접근할 수 있기 때문에 애플리케이션 어디서든지 사용될 수 있고, 그러다 보면 자연스럽게 정역 상태(global state)로 사용되기 쉽다. 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태를 갖는 것은 객체지향 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다.

싱글톤 레지스트리

스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 이것이 바로 싱글톤 레지스트리(singleton registry)다. 스프링 컨테이너는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너이기도 하다. 싱글톤 레지스트리의 장점은 스태틱 메서드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 점이다. 평범한 자바 클래스라도 IoC 방식의 컨테이너를 사용해서 생성과 관계설정, 사용 등에 대한 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있다. 오브젝트 생성에 관한 모든 권한은 IoC 기능을 제공하는 애플리케이션 컨텍스트에게 있기 때문이다.

스프링의 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다. 따라서 테스트 환경에서 자유롭게 오브젝트를 만들 수 있고, 테스트를 위한 목 오브젝트로 대체하는 것도 간단하다. 생성자 파라미터를 이용해서 사용할 오브젝트를 넣어주게 할 수도 있다.

가장 중요한 것은 싱글톤 패턴과 달리 스프링이 지지하는 객체지향적인 설계 방식과 원칙, 디자인 패턴(싱글톤 패턴은 제외) 등을 적용하는 데 아무런 제약이 없다는 점이다. 스프링은 IoC 컨테이너일 뿐만 아니라, 고전적인 싱글톤 패턴을 대신해서 싱글톤을 만들고 관리해주는 싱글톤 레지스트리라는 점을 기억해두자. 스프링이 빈을 싱글톤으로 만드는 것은 결국 오브젝트의 생성 방법을 제어하는 IoC 컨테이너로서의 역할이다.

싱글톤과 오브젝트의 상태

싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다. 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는, 상태정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야 한다. 다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변구를 수정하는 것은 매우 위험하다. 물론 읽기전용의 값이라면 초기화 시점에서 인스턴스 변수에 저장해두고 공유하는 것은 아무 문제가 없다.

상태가 없는 방식으로 클래스를 만드는 경우에는 각 요청에 대한 정보나, DB나 서버의 리소스로부터 생성한 정보는 파라미터와 로컬 변수, 리턴 값등을 이용해서 다뤄야 한다. 메서드 안에서 생성되는 로컬 변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어쓸 일은 없다.

인스턴스 변수를 사용하도록 수정한 UserDao

public class UserDao {
    private static ConnectionMaker connectionMaker;
    private Connection c;
    private User user;

    public User get(String id) throws ClassNotFoundException, SQLException {
        this.c = connectionMaker.makeConnection();
        ...
        this.user = new User();
        this.user.setId(rs.getString("id"));
        this.user.setName(rs.getString("name"));
        this.user.setPassword(rs.getString("password"));
        ...
        return this.user;
    }
}

스프링의 싱글톤 빈으로 사용되는 클래스를 만들 때 인스턴스 필드로 선언해도 되는 것

  • (X) 개별적으로 바뀌는 정보 - Connection, User
    • 싱글톤으로 만들어져서 멀티스레드 환경에서 사용하면 심각한 문제가 발생한다.
    • 이런 정보는 로컬 변수로 정의하거나, 파라미터로 주고받으면서 사용하게 해야한다.
  • (O) 읽기전용의 정보 - connectionMaker
    • 스프링이 한 번 초기화해주고 나면 이후에는 수정되지 않기 때문에 멀티스레드 환경에서 사용해도 아무런 문제가 없다.
    • 이 connectionMaker도 DaoFactory에 @Bean을 붙여서 만들었으니 스프링이 관리하는 빈이 될 것이고, 별다른 설정이 없다면 기본적으로 오브젝트 한 개만 만들어져서 UserDao의 connectionMaker 인스턴스 필드에 저장된다.
    • 즉, 이 변수에는 ConnectionMaker 타입의 싱글톤 오브젝트가 들어 있다.

동일하게 읽기전용의 속성을 가진 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋다. 물론 단순한 읽기전용 값이라면 static final이나 final로 선언하는 편이 낫다.

6.3 스프링 빈의 스코프

스프링에서 빈의 스코프(scope)란 스프링이 관리하는 오브젝트, 즉 빈이 생성되고, 존재하고, 적용되는 범위라는 의미이다.

  • 기본 스코프 : 싱글톤 스코프
    싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서, 갖에로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다.
  • 그외 스코프 : 프로토타입 스코프, 요청 스코프, 세션 스코프
    • 프로토타입(prototype) 스코프 : 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트가 생성된다.
    • 요청(request) 스코프 : 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성된다.
    • 세션(session) 스코프 : 웹의 세션과 스코프가 유사하다.

7. 의존관계 주입(DI)

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

스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입(Dependency Injection)이라고 불린다. 물론 스프링이 컨테이너이고 프레임워크이니 기본적인 동작원리가 모두 IoC 방식이라고 할 수 있지만, 스프링이 여타 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 용어를 사용할 때 분명하게 드러난다. 그래서 지금은 스프링이 DI 컨테이너라고 더 많이 불리고 있다.

7.2 런타임 의존관계 설정

7.2.1 의존관계

A가 B를 의존한다고 가정해보자. 의존한다는 건 의존대상, 즉 B가 변하면 그것이 A에 영향을 미친다는 뜻이다. B의 기능이 추가되거나 변경되거나, 형식이 바뀌거나 하면 그 영향이 A로 전달된다는 뜻이다.

대표적인 예는 A가 B를 사용하는 경우, 예를 들어 A에서 B에 정의된 메서드를 호출해서 사용하는 경우다. 이럴 땐 '사용에 대한 의존관계'가 있다고 말할 수 있다. 만약 B에 새로운 메서드가 추가되거나 기능이 변경되면 A의 기능이 수행되는 데 영향을 끼칠 수 있다.

7.2.2 UserDao의 의존관계

UserDao는 ConnectionMaker 인터페이스에만 의존하고 있다. 따라서 ConnectionMaker 인터페이스가 변한다면 그 영향을 UserDao가 직접적으로 받게 된다. 하지만 ConnectionMaker 인터페이스를 구현한 클래스는 변화가 생겨도 UserDao에 영향을 주지 않는다. 이렇게 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태인 결합도가 낮은 상태가 된다.

UML에서 말하는 의존관계란 이렇게 설계 모델의 관점에서 이야기 하는 것이다. 그런데 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 런타임 의존관계 또는 오브젝트 의존관계인데, 설계 시점의 의존관계가 실체화된 것이라고 볼 수 있다.

인터페이스를 통해 설계 시점에 느슨한 의존관계를 갖는 경우에는 UserDao의 오브젝트가 런타임 시에 사용할 오브젝트가 어떤 클래스로 만든 것인지 미리 알 수가 없다. 프로그램이 시작되고 UserDao 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트(dependent object)라고 말한다.

의존관계 주입은 이렇게 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

정리하면 의존관계 주입이란 다음과 같은 세 가지 조건을 충족하는 작업을 말한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다. DI에서 말하는 제3의 존재는 바로 관계설정 책임을 가진 코드를 분리해서 만들어진 오브젝트라고 볼 수 있다.

전략 패턴에 등장하는 클라이언트나 앞에서 만들었던 DaoFactory, 또 DaoFactory와 같은 작업을 일반화해서 만들어졌다는 스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제3의 존재라고 볼 수 있다.

7.2.3 UserDao의 의존관계 주입

UserDao에 적용된 의존관계 주입 기술을 다시 살펴보자.

관계설정 책임을 분리하기 전의 생성자

public UserDao() {
	this.connectionMaker = new DConnectionMaker();
}
  • 문제 : 인터페이스를 사이에 두고 의존관계를 느슨하게 만들긴 했지만, UserDao가 설계 시점에서 사용할 구체적인 클래스를 알고 있다.

    • 모델링 때의 의존관계, 즉 ConnectionMaker 인터페이스의 관계뿐 아니라 런타임 의존관계인 DConnectioMaker 오브젝트를 사용하겠다는 것까지 UserDao가 결정하고 관리하고 있는 셈이다.
  • 해결 : IoC 방식을 써서 UserDao로부터 제3의 존재에 런타임 의존관계 결정 권한을 위임한다. 그래서 만들어진 것이 DaoFactory였다.

DaoFactory

여기서 두 오브젝트 사이의 런타임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재이자 IoC 방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너다. 즉, 의존관계 주입을 담당하는 컨테이너로 DI 컨테이너이다.

DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고 이 생성자의 파라미터로 오브젝트의 레퍼런스를 전달해준다. 이렇게 생성자 파라미터를 통해 전달받은 런타임 의존관계를 갖는 오브젝트는 인스턴스 변수에 저장해둔다.

의존관계 주입을 위한 코드

private static ConnectionMaker connectionMaker;

  public UserDao(ConnectionMaker connectionMaker) {
      this.connectionMaker = connectionMaker;
  }

이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레페런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다고 해서 이를 의존관계 주입이라고 부른다. DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다.

7.3 의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다. 의존관계를 맺는 방법이 외부로부터의 주입이 아닌 스스로 검색을 이용하기 때문에 의존관계 검색(Dependency Lookup)이라고 불리는 것도 있다. 의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메서드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

DaoFactory를 이용하는 생성자

public UserDao() {
	DaoFactory daoFactory = new DaoFactory();
	this.connectionMaker = daoFactory().connectionMaker();
}

이렇게 해도 UserDao는 여전히 자신이 어떤 ConnectionMaker 오브젝트를 사용할 지 미리 알지 못한다. 또한 의존관계 주입이 아닌 검색은 스스로 IoC 컨테이너인 DaoFactory에게 요청하는 것이다. DaoFactory의 경우엔 단순 메서드 호출로 보이겠지만 이런 작업을 일반화한 스프링의 애플리케이션 컨텍스트라면 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 된다. 그 대상이 런타임 의존관계를 가질 오브젝트이므로 의존관계 검색이라고 부르는 것이다. 스프링의 IoC 컨테이너인 애플리케이션 컨텍스트의 getBean() 메서드가 의존관계 검색에 사용된다.

의존관계 검색을 이용해는 UserDao 생성자

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

의존관계 검색은 방법만 조금 다를 뿐 기존 의존관계 주입의 거의 모든 장점을 갖고 있다.
하지만 위의 예에서는 사용자에 대한 DB 정보를 어떻게 가져올 것인가에 집중해야 하는 UserDao에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여 있는 것은 어색하다. 따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다.

의존관계 검색은 UserDaoTest같은 테스트 코드에서 사용하기 좋다. 또한 애플리케이션 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. 스태틱 메서드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다.

의존관계 검색과 의존관계 주입을 적용할 때는 중요한 차이점이 하나 있다. 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다. UserDao에 getBean()을 사용한 의존관계 검색 방법을 적용했다고 해보자. 이 경우 ConnectionMaker만 스프링의 빈이면 되지, UserDao는 어딘가에서 new UserDao() 해서 만들어서 사용해도 된다. 반면에 의존관계 주입에선 DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다. 즉, UserDao와 ConnectionMaker 사이에 DI가 적용되려면 UserDao도 반드시 컨테이너가 만드는 빈 오브젝트여야 한다.

DI 받는다
단지 외부에서 파라미터로 주입해줬다고 해서 다 DI가 아니고, 주입받는 메서드 파라미터가 이미 특정 클래스 타입으로 고정되어 있다면 DI가 일어날 수 없다. DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야 한다.

7.4 의존관계 주입의 응용

7.4.1 기능 구현의 교환

실제 운영에 사용할 DB는 매우 중요하기 때문에, 개발 중에는 절대 사용하지 말아야 한다. 대신 개발자 PC에 설치한 로컬 DB로 사용해야 한다고 해보자. 그리고 어느 시점이 되면 지금까지 개발한 것을 그대로 운영서버로 배치해서 사용할 것이다. 그런데 이때 만약 DI 방식을 적용하지 않았다고 해보자. LocalDBConnectionMaker을 사용하다가 이를 서버에 배치하는 시점에서 ProductionDBConnectionMaker라는 클래스로 변경해줘야 한다. DI를 안 했으니 모든 DAO에는 new LocalDBCOnnectionMaker() 라는 코드가 들어 있을 것이다. DAO가 100개라면 최소한 100군데의 코드를 new ProductionDBConnectionMaker() 로 수정해줘야 한다.

반면에 DI 방식을 적용하면, 모든 DAO는 생성 시점에 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공받는다.

개발용 ConnectionMaker 생성 코드

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

@Configuration이 붙은 DaoFactory를 사용한다고 하면 위처럼 만들어서 사용하면 된다. 이를 서버에 배포할 때는 어떤 DAO 클래스와 코드도 수정할 필요 없이, 단지 딱 한 줄만 변경하면 된다.

운영용 ConnectionMaker 생성 코드

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

개발환경과 운영환경에서 DI의 설정정보에 해당하는 DaoFactory만 다르게 만들어두면 나머지 코드에는 전혀 손대지 않고 개발 시와 운영 시에 각각 다른 런타임 오브젝트에 의존관계를 갖게 해줘서 문제를 해결할 수 있다.

7.4.2 부가기능 추가

DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 해보자. 모든 DAO의 makeConnection() 메서드를 호출하는 부분에 카운터를 증가시키는 코드를 넣는 것은 엄청난 노가다다. 또한 DB 연결횟수를 세는 일은 DAO의 관심사항이 아니다.

DI 컨테이너에서라면 아주 간단히 해결할 수 있다. DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하는 것이다. 기존 코드 수정 없이 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의해주면 된다.

먼저 ConnectionMaker 인터페이스를 구현해서 CountingConnectionMaker를 만든다. DAO가 의존할 대상이 될 것이다.

연결횟수 카운팅 기능이 있는 클래스

public class CountingConnectionMaker implements ConnectionMaker {
    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        this.counter++;
        return realConnectionMaker.makeConnection();
    }

    public int getCounter() {
        return this.counter;
    }
}
  • CountingConnectionMaker 클래스는 내부에서 직접 DB 커넥션을 만들지 않고, 대신 DAO가 DB 커넥션을 가져올 때마다 호출하는 makeConnection()에서 DB 연결횟수 카운터를 증가시킨다.
  • CountingConnectionMaker는 자신의 관심사인 DB 연결횟수 카운팅 작업을 마치면 실제 DB 커넥션을 만들어주는 realConnectionMaker에 저장된 ConnectionMaker 타입 오브젝트의 makeConnection()을 호출해서 그 결과를 DAO에게 돌려준다.
  • UserDao는 ConnectionMaker의 인터페이스에만 의존하고 있기 때문에, ConnectionMaker 인터페이스를 구현하고 있다면 어떤 것이든 DI가 가능하다.

새로운 의존관계를 컨테이너가 사용할 설정정보를 이용해 만들어보자.

CountingConnectionMaker 의존관계가 추가된 DI 설정용 클래스

@Configuration
public class CountingDaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

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

    @Bean
    public ConnectionMaker realConnectionMaker() {
        return new DConnectionMaker();
    }
}
  • CountingDaoFactory라는 이름의 설정용 클래스를 만든다.
  • 기존과 달리 connectionMaker() 메서드에서 CountingConnectionMaker 타입 오브젝트를 생성하도록 만든다. 그리고 realConnectionMaker() 메서드가 만들어주는 오브젝트는 connectionMaker() 에서 만드는 오브젝트의 생성자를 통해 DI 해준다.
  • 기존 DAO 설정 부분은 수정할 필요 없이, 계속해서 connectionMaker() 메서드를 통해 생성되는 오브젝트를 사용하게 한다.

이제 커넥션 카운팅을 위한 실행 코드를 만든다.

CountingConnectionMaker에 대한 테스트 클래스

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

        //
        // DAO 사용 코드
        // 

        CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
        System.out.println("Connection counter : " + ccm.getCounter());
    }
}
  • 기본적으로 UserDaoTest와 같지만 설정용 클래스를 CountingDaoFactory로 변경해줘야 한다.
  • DAO를 DL 방식으로 가져와 어떤 작업이든 여러 번 실행시킨 후 CountingConnectionMaker 빈을 가져와서 카운터 값을 출력해보고 DAO 사용횟수와 일치하는지 확인해보자.

DI의 장점은 관심사의 분리(SoC)를 통해 얻어지는 높은 응집도에서 나온다. 모든 DAO가 직접 의존해서 사용할 ConnectionMaker 타입 오브젝트는 connectionMaker() 메서드에서 만든다. 따라서 CountingConnectionMaker의 의존관계를 추가하려면 이 메서드만 수정하면 된다. 또한 설정 클래스도 DaoFactory 설정 클래스를 CountingDaoFactory로 변경하기만 하면 된다.

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

의존관계 주입 시 생성자가 아닌 일반 메서드를 사용하는 방법이 더 자주 사용된다.
일반 메서드를 이용해 의존 오브젝트와의 관계를 주입해주는 방법은 크게 두 가지가 있다.

수정자(setter) 메서드를 이용한 주입

  • 수정자 메서드의 핵심기능은 파라미터로 전달된 값을 보통 내부의 인스턴스 변수에 저장하는 것이다. 부가적으로, 입력 값에 대한 검증이나 그 밖의 작업을 수행할 수도 있다.
  • 외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메서드에서 사용하게 하는 DI 방식에서 활용하기 좋다.

일반 메서드를 이용한 주입

  • 수정자 메서드처럼 set으로 시작해야 하고 한 번에 한 개의 파라미터만 가질 수 있다는 제약이 싫다면 사용한다.
  • 파라미터의 개수가 많아지고 비슷한 타입이 여러 개라면 실수하기 쉽다.
  • 임의의 초기화 메서드를 이용하는 DI는 적절한 개수의 파라미터를 가진 여러 개의 초기화 메서드를 만들 수도 있기 때문에 한 번에 모든 필요한 파라미터를 다 받아야 하는 생성자보다 낫다.

스프링은 전통적으로 메서드를 이용한 DI 방법 중 수정자 메서드를 가장 많이 사용해왔다.
수정자 메서드 DI를 사용할 때는 메서드의 이름은 메서드를 통해 DI 받을 오브젝트의 타입 이름을 따르는 것이 가장 무난하고 이 관례를 따르도록 하자.

수정자 메서드 DI 방식을 사용한 UserDao

public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}
  • 생성자 대신 setConnectionMaker() 메서드를 사용해 파라미터로 ConnectionMaker 타입의 오브젝트를 받도록 선언한다.
  • 파라미터로 받은 오브젝트는 인스턴스 변수에 저장해두도록 만든다.
  • UserDao의 DI를 적용하는 DaoFactory의 코드도 함께 수정해줘야 한다.

수정자 메서드 DI를 사용하는 팩토리 메서드

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setConnectionMaker(connectionMaker());
    return userDao;
}

8. XML을 이용한 설정

스프링은 DaoFactory와 같은 자바 클래스를 이용하는 것 외에도, 다양한 방법을 통해 DI 의존관계 설정정보를 만들 수 있다. 가장 대표적인 것이 바로 XML이다.

XML의 장점

  • 단순한 텍스트 파일이기 때문에 다루기 쉽다.
  • 쉽게 이해할 수 있으며 컴파일과 같은 별도의 빌드 작업이 없다.
  • 환경이 달라져서 오브젝트의 관계가 바뀌는 경우에도 빠르게 변경사항을 반영할 수 있다.
  • 스키마나 DTD를 이용해서 정해진 포맷을 따라 작성됐는지 손쉽게 확인할 수 있다.

이제 DaoFactory 자바 코드에 담겨 있던, DI를 위한 오브젝트 의존관계 정보를 XML을 이용해 만들어보자.

8.1 XML 설정

스프링의 애플리케이션 컨텍스트는 XML에 담긴 DI 정보를 활용할 수 있다. DI 정보가 담긴 XML 파일은 <beans>를 루트 앨리먼트로 사용한다. <beans> 안에는 여러 가지 <bean>을 정의할 수 있다. XML 설정은 @Configuration@Bean이 붙은 자바 클래스로 만든 설정과 내용이 동일하다.

@Configuration<beans>, @Bean<bean>에 대응해서 생각하면 이해하기 쉬울 것이다.

하나의 @Bean 메서드를 통해 얻을 수 있는 빈의 DI 정보

  • 빈의 이름 : @Bean 메서드 이름이 빈의 이름이다. 이 이름은 getBean()에서 사용된다.
  • 빈의 클래스 : 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의한다.
  • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메서드를 통해 의존 오브젝트를 넣어준다. 의존 오브젝트도 하나의 빈이므로 이름이 있을 것이고, 그 이름에 해당하는 메서드를 호출해서 의존 오브젝트를 가져온다. 의존 오브젝트는 하나 이상일 수도 있고, 의존하고 있는 오브젝트가 없는 경우에는 생략할 수 있다.

XML에서 <bean>을 사용해도 이 세 가지 정보를 정의할 수 있다. XML은 자바 코드처럼 유연하게 정의될 수 있는 것이 아니므로, 핵심 요소를 잘 짚어서 그에 해당하는 태그와 애트리뷰트가 무엇인지 알아야 한다.

8.1.1 connectionMaker() 전환

먼저 DaoFactory의 connectionMaker() 메서드에 해당하는 빈을 XML로 정의해보자.
connectionMaker()로 정의되는 빈은 의존하는 다른 오브젝트는 없으니 DI 정보 세 가지 중 두 가지만 있으면 된다.

클래스 설정과 XML 설정의 대응항목

  • DI 정의 세 가지 중 빈의 이름과 빈 클래스(이름) 두 가지를 <bean> 태그의 id와 class 애트리뷰트를 이용해 정의할 수 있다.
  • <bean> 태그의 class 애트리뷰트에 쩡하는 것은 자바 메서드에서 오브젝트를 만들 때 사용하는 클래스 이름이라는 것을 주의하자. (메서드의 리턴 타입X)
  • class 애트리뷰트에 넣을 클래스 이름은 패키지까지 모두 포함해야 한다.

connectionMaker() 메서드의 <bean> 태그 전환

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

8.1.2 userDao() 전환

이번에는 userDao 메서드를 XML로 변환해보자.
userDao() 에는 DI 정보의 세 가지 요소가 모두 들어있다. 여기서 관심을 가질 것은 수정자 메서드를 사용해 의존관계를 주입해주는 부분이다. 자바빈의 관례를 따라서 수정자 메서드는 프로퍼티가 된다. 프로퍼티 이름은 메서드 이름에서 set을 제외한 나머지 부분을 사용한다.

XML에서는 <property> 태그를 사용해 의존 오브젝트와의 관계를 정의한다.

  • <property> 태그는 name과 ref라는 두 개의 애트리뷰트를 갖는다.
  • name은 DI에 사용할 수정자 메서드의 프로퍼티 이름이다.
  • ref는 주입해줄 오브젝트의 빈 이름이다.
    • 보통 프로퍼티 이름과 DI 되는 빈의 이름이 같은 경우가 많다.
      • 프로퍼티 이름은 주입할 빈 오브젝트의 인터페이스를 따르는 경우가 많고, 빈 이름도 역시 인터페이스 이름을 사용하는 경우가 많기 때문이다.
      • 바뀔 수 있는 클래스 이름보단 대표적인 인터페이스 이름을 따르는 편이 자연스럽다.
      • 하지만, 프로퍼티 이름이나 빈의 이름을 인터페이스 이름과 다르게 정해도 상관없다.
        (의미를 좀 더 잘 드러낼 수 있는 이름이 있거나 이름이 중복되는 상황이라면)

예를 들어, @Bean 메서드에서 다음과 같이 다른 @Bean 메서드를 호출해서 주입할 오브젝트를 가져온다면

userDao.setConnectionMaker(connectionMaker());
  • userDao.setConnectionMaker() 은 userDao 빈의 connectionMaker 프로퍼티를 이용해 의존관계 정보를 주입한다는 뜻이다.
  • connectionMaker() 은 connectionMaker() 메서드를 호출해서 리턴하는 오브젝트를 주입하라는 뜻이다.
  • 각 정보를 <property> 에 대응하면
    • set -> <property>
    • ConnectionMaker() -> name="connectionMaker"
    • connectionMaker() -> ref="connectionMaker"

8.1.3 XML의 의존관계 주입 정보

완성된 XML 설정정보

<beans>
    <bean id="connectionMaker" class="springbook.user.dao.DConnectionMaker" />
    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="connetionMaker" ref="connectionMaker" />
    </bean>
</beans>

같은 인터페이스를 구현한 의존 오브젝트를 여러 개 정의할 경우
같은 인터페이스를 구현한 의존 오브젝트를 여러 개 정의해두고 그 중에서 원하는 걸 골라서 DI 하는 경우도 있다. 이 때는 각 빈의 이름을 독립적으로 만들어두고 ref 애트리뷰트를 이용해 DI 받을 빈을 지정해주면 된다.

<beans>
    <bean id="localDBConnectionMaker" class="..localDBConnectionMaker" />
    <bean id="testDBConnectionMaker" class="..testDBConnectionMaker" />
    <bean id="productionDBConnectionMaker" class="..productionDBConnectionMaker" />
 
    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="connetionMaker" ref="localDBConnectionMaker" />
    </bean>
</beans>

DTD와 스키마
XML 문서는 미리 정해진 구조를 따라서 작성됐는지 검사할 수 있다. XML 문서의 구조를 정의하는 방법에는 DTD와 스키마(schema)가 있다. 스프링의 XML 설정파일은 이 두 가지 방식을 모두 지원한다. 특별한 이유가 없다면 스키마를 사용하는 편이 바람직하다.

  • DTD를 사용할 경우
    <beans> 엘리먼트 앞에 다음과 같은 DTD 선언을 넣어준다.
<!DOCTYPE brans PUBLIC "-//SPRING//DTD BRAN 2.0//EN"
    "http://www.springframework.org/dtd/spring-beans-2.0.dtd">  
  • 스키마를 사용할 경우
    스프링의 DI를 위한 태그들은 각각 별개의 스키마 파일에 정의되어 있고 독립적인 네임스페이스를 사용해야만 한다. 따라서 이런 태그를 사용하려면 DTD 대신 네임스페이스가 지원되는 스키마를 사용해야 한다. <beans> 태그를 기본 네임스페이스로 하는 스키마 선언은 다음과 같다.
<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-3.0.xsd">

8.2 XML을 이용하는 애플리케이션 컨텍스트

이제 애플리케이션 컨텍스트가 DaoFactory 대신 XML 설정정보를 활용하도록 만들어보자.

  • XML에서 빈의 의존관계 정보를 이용하는 IoC/DI 작업에는 GenericXmlApplicationContext를 사용한다.
    • GenericXmlApplicationContext의 생성자 파라미터로 XML 파일의 클래스패스를 지정한다.
    • XML 설정파일은 클래스패스 최상단에 두면 편하다.
  • 애플리케이션 컨텍스트가 사용하는 XML 설정파일의 이름은 관례를 따라 applicationContext.xml이라고 만든다.

XML 설정정보를 담은 applicationContext.xml

<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-3.0.xsd">
    <bean id="connectionMaker" class="springbook.user.dao.DConnectionMaker" />

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="connetionMaker" ref="connectionMaker" />
    </bean>
</beans>

다음은 UserDaoTest의 애플리케이션 컨텍스트 생성 부분을 수정한다.
클래스패스를 시작하는 /는 넣을 수도 있고 생략할 수도 없다. /가 없는 경우에도 항상 루트에서부터 시작하는 클래스패스라는 점을 기억해두자.

ApplicationContext ac = new GenericXmlApplicationContext(
                "applicationContext.xml");

참고
GenericXmlApplicationContext 외에도 ClassPathXmlApplicationContext를 이용해 XML로부터 설정정보를 가져오는 애플리케이션 컨텍스트를 만들 수 있다.
GenericXmlApplicationContext는 클래스패스뿐 아니라 다양한 소스로부터 설정파일을 읽어올 수 있다. ClassPathXmlApplicationContext는 XML 파일을 클래스패스에서 가져올 때 사용할 수 있는 편리한 기능이 추가된 것이다.

8.3 DataSource 인터페이스로 변환

8.3.1 DataSource 인터페이스 적용

IoC와 DI의 개념을 설명하기 위해 직접 DB 커넥션을 생성해주는 connectioMaker 인터페이스를 정의하고 사용했지만, 사실 자바에서는 DB 커넥션을 가져오는 오브젝트의 기능을 추강화해서 비슷한 용도로 사용할 수 있게 만들어진 DataSource라는 인터페이스가 이미 존재한다. 또한 이미 다양한 방법으로 DB 연결과 풀링(pooling) 기능을 갖춘 많은 DataSource 구현 클래스가 존재한다.

DataSource 인터페이스와 다양한 DataSource 구현 클래스를 사용할 수 있도록 UserDao를 리팩토링해보자.

DataSource를 사용하는 UserDao

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.getConnection();
        ...
    }
    ...
}

다음은 DataSource 구현 클래스가 필요하다. 스프링이 제공해주는 DataSource 구현 클래스 중 테스트 환경에서 간단히 사용할 수 있는 SimpleDriverDataSource라는 클래스를 사용하자.

SimpleDriverDataSource는 DB 연결에 필요한 필수 정보를 제공받을 수 있도록 여러개의 수정자 메서드를 갖고 있다. 예를 들어 JDBC 드라이버 클래스, JDBC URL, 아이디, 비밀번호 등이다.

8.3.2 자바 코드 설정 방식

먼저 DaoFactory 설정 방식을 이용해보자.

  • connectionMaker() 수정
    • 기존의 connectionMaker() 메서드를 dataSource()로 변경하고 SimpleDriverDataSource의 오브젝트를 리턴하게 한다.
    • 이 오브젝트를 넘기기 전에 DB 연결과 관련된 정보를 수정자 메서드를 이용해 지정해줘야 한다.
  • userDao() 수정
    • UserDao는 이제 DataSource 타입의 dataSource()를 DI 받는다.

DataSource 타입의 dataSource 빈 정의 메서드

@Bean
public DataSource dataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        
    dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
    dataSource.setUrl("jdbc://mysql://localhost/springbook");
    dataSource.setUsername("spring");
    dataSource.setPassword("book");
        
    return dataSource;
}

DataSource 타입의 빈을 DI 받는 userDao() 빈 정의 메서드

@Bean
public UserDao userDao() {
    UserDao userDao = new UserDao();
    userDao.setDataSource(dataSource());
    return userDao;
}

이렇게 해서 UserDaoDataSource 인터페이스를 적용하고 SimpleDriverDataSource의 오브젝트를 DI로 주입해서 사용할 수 있는 준비가 끝났다.

8.3.3 XML 설정 방식

  • idconnectionMaker<bean>dataSource라는 이름으로 변경한다.
  • classSimpleDriverDataSource의로 변경한다.

dataSource 빈

<bean id="dataSource"
    class="org.springframework.jdbc.datasource.SimpleDriverDataSource" />

여기서 문제는 <bean> 설정으로 SimpleDriverDataSource의 오브젝트를 만드는 것까지는 가능하지만, dataSource() 메서드에서 SimpleDriverDataSource 오브젝트의 수정자로 넣어준 DB 접속정보는 나타나 있지 않다는 점이다. 그렇다면 XML에서는 어떻게 해서 dataSource() 메서드에서처럼 DB 연결정보를 넣도록 설정을 만들 수 있을까?

8.4 프로퍼티 값의 주입

8.4.1 값 주입

다른 빈 오브젝트의 레퍼런스가 아닌 단순 정보도 오브젝트를 초기화하는 과정에서 수정자 메서드에 넣을 수 있다. 이때는 DI에서처럼 오브젝트의 구현 클래스를 다이내믹하게 바꿀 수 있게 해주는 것이 목적은 아니다. 대신 클래스 외부에서 DB 연결정보와 같이 변경 가능한 정보를 설정해줄 수 있도록 만들기 위해서다. 예를 들어 DB 접속 아이디가 바뀌었더라도 클래스 코드는 수정해줄 필요가 없게 해주는 것이다.

텍스트나 단순 오브젝트 등을 수정자 메서드에 넣어주는 것을 스프링에서는 '값을 주입한다'고 말한다. 이것도 성격은 다르지만 일종의 DI라고 볼 수 있다. 사용할 오브젝트 자체를 바꾸지는 않지만 오브젝트의 특성은 외부에서 변경할 수 있기 때문이다.

스프링의 빈으로 등록될 클래스에 수정자 메서드가 정의되어 있다면 <property>를 사용해 정보를 주입한다. 하지만 다른 빈 오브젝트의 레퍼런스(ref)가 아니라 단순 값(value)을 주입해주는 것이기 때문에 value 애트리뷰트를 사용한다.

코드를 통한 DB 연결정보 주입

dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
dataSource.setUrl("jdbc://mysql://localhost/springbook");
dataSource.setUsername("spring");
dataSource.setPassword("book");

XML을 이용한 DB 연결정보 설정

<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/springbook"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>

8.4.2 value 값의 자동 변환

url, username, password는 모두 스트링 타입이니 원래 텍스트로 정의되는 value 애트리뷰트의 값을 사용하는 것은 문제없다. 그런데 driverClassjava.lang.Class 타입이다. 어떻게 이 "com.mysql.cj.jdbc.Driver"라는 스트링 값이 Class 타입의 파라미터를 갖는 수정자 메서드에 사용될 수 있는 것일까?

이런 설정이 가능한 이유는 스프링이 프로퍼티의 값을, 수정자 메서드의 파라미터 타입을 참고로 해서 적절한 형태로 변환해주기 때문이다. setDriverClass() 메서드의 파라미터 타입이 Class임을 확인하고 "com.mysql.cj.jdbc.Driver"라는 텍스트 값을 com.mysql.cj.jdbc.Driver.class 오브젝트로 자동 변경해주는 것이다. 내부적으로 다음과 같이 변환 작업이 일어난다고 생각하면 된다.

Class driverClass = Class.forName("com.mysql.cj.jdbc.Driver");
dataSource.setDriverClass(driverClass);

스프링은 value에 지정한 텍스트 값을 적절한 자바 타입으로 변환해준다. Integer 같은 기본 타입은 물론 Class, URL 같은 오브젝트로 변환할 수도 있다. 또한 값이 여러 개라면 List 같은 것이나 배열 타입으로도 값의 주입이 가능하다.

DataSource를 적용 완료한 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-3.0.xsd">
    
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost/springbook"/>
        <property name="username" value="spring"/>
        <property name="password" value="book"/>
    </bean>

    <bean id="userDao" class="springbook.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>

9. 정리

1장에서는 사용자 정보를 DB에 등록하거나 아이디로 조회하는 기능을 가진 간단한 DAO 코드를 만들고, 그 코드의 문제점을 살펴본 뒤, 이를 다양한 방법과 패턴, 원칙, IoC/DI 프레임워크까지 적용해서 개선해왔다.

  • 관심사의 분리, 리팩토링
    먼저 책임이 다른 코드를 분리해서 두 개의 클래스로 만들었다.
  • 전략 패턴
    그 중에서 바뀔 수 있는 쪽의 클래스는 인터페이스를 구현하도록 하고, 다른 클래스에서 인터페이스를 통해서만 접근하도록 만들었다. 이렇게 해서 인터페이스를 정의한 쪽의 구현 방법이 달라져 클래스가 바뀌더라도, 그 기능을 사용하는 클래스의 코드는 같이 수정할 필요가 업도록 만들었다.
  • 개방 폐쇄 원칙
    이를 통해 자신의 책임 자체가 변경되는 경우 외에는 불필요한 변화가 발생하지 않도록 막아주고, 자신이 사용하는 외부 오브젝트의 기능은 자유롭게 확장하거나 변경할 수 있게 만들었다.
  • 낮은 결합도, 높은 응집도
    결국 한쪽의 기능 변화가 다른 쪽의 변경을 요구하지 않아도 되게 했고(낮은 결합도), 자신의 책임과 관심사에만 순수하게 집중하는(높은 응집도) 깔끔한 코드를 만들 수 있었다.
  • 제어의 역전/IoC
    오브젝트가 생성되고 여타 오브젝트와 관계를 맺는 작업의 제어권을 별도의 오브젝트 팩토리를 만들어 넘겼다. 또는 오브젝트 팩토리의 기능을 일반화한 IoC 컨테이너로 넘겨서 오브젝트가 자신이 사용할 대상의 생성이나 선택에 관한 책임으로부터 자유롭게 만들어줬다.
  • 싱글톤 레지스트리
    전통적인 싱글톤 패턴 구현 방식의 단점을 살펴보고, 서버에서 사용되는 서비스 오브젝트로서의 장점을 살릴 수 있는 싱글톤을 사용하면서도 싱글톤 패턴의 단점을 극복할 수 있도록 설계된 컨테이너를 활용하는 방법에 대해 알아봤다.
  • 의존과계 주입/DI
    설계 시점과 코드에는 클래스와 인터페이스 사이의 느슨한 의존관계만 만들어놓고, 런타임 시에 실제 사용할 구체적인 의존 오브젝트를 제3자(DI 컨테이너)의 도움으로 주입받아서 다이내믹한 의존관계를 갖게 해주는 IoC의 특별한 케이스를 알아봤다.
  • 생성자 주입과 수정자 주입
    의존 오브젝트를 주입할 때 생성자를 이용하는 방법과 수정자 메서드를 이용하는 방법을 알아봤다.
  • XML 설정
    마지막으로, XML을 이용해 DI 설정정보를 만드는 방법과 이존 오브젝트가 아닌 일반 값을 외부에서 설정해서 런타임 시에 주입하는 방법을 알아봤다.

스프링이란 '어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크'라는 사실을 꼭 기억해두자. 스프링의 관심은 오브젝트와 그 관계다. 하지만 오브젝트를 어떻게 설계하고, 분리하고, 개선하고, 어떤 의존관계를 가질지 결정하는 일은 스프링이 아니라 개발자의 역할이며 책임이다. 스프링은 단지 원칙을 잘 따르는 설계를 적용하려고 할 때 필연적으로 등장하는 번거로운 작업을 편하게 할 수 있도록 도와주는 도구일 뿐임을 잊지 말자.

profile
🚀 백엔드 2년차 개발자입니다 🚀 성장의 즐거움으로 아자자자!🤣

0개의 댓글