UserDaoTest 다시보기

테스트의 유용성

우리가 현재 만들었던 UserDaoTestadd()get() 메서드를 이용해서 UserDao 에서의 add()get() 을 호출합니다.

이러한 테스트 코드는 내가 예상하고 의도했던 때로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업입니다.

이를 통해서 코드나 설계의 결함을 확인할 수 있습니다.


UserDaoTest의 특징

우리가 이전에 만들었던 main() 메서드로 작성된 테스트코드를 다시 살펴봅시다.

public static void main(String[] args) throws ClassNotFoundException, SQLException{
    UserDao dao = new UserDao();

    User user = new User();
    user.setId("haecahn");
    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() + "제대로 나왔습니다");

}

이러한 테스트는 main() 메서드를 사용해서 쉽게 테스트 수행을 가능하게 했다는 점과 테스트할 대상인 UserDao를 직접 호출해서 사용한다는 점이 특징입니다.


웹을 통한 DAO 테스트 방법의 문제점

우리가 보통 DAO를 테스트 하는 방법은 DAO를 만들고 난 뒤에 바로 테스트를 하는 것이 아닌 서비스 계층과 MVC 패턴등을 모두 대충이라도 만든 후에 직접적으로 웹에서 기능들을 실행해보면서 확인을 합니다.

하지만 이러한 방법은 모든 서비스 클래스, 컨트롤러 JSP등을 모두 만들고 나서야 테스트가 가능하는게 큰 문제입니다.

그래서 문제가 발생을 한다면 어디서 발생했는지 못찾는다는 단점이 존재합니다.


작은 단위의 테스트

우리는 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직합니다.
모든 것들을 한꺼번에 몰아서 한다면 테스트 수행과정이 복잡해지고 정확한 원인을 찾기가 힘들어집니다. 이를 통해서 관심사의 분리라는 원리가 적용됩니다.

UserDaoTest는 한 가지 관심에 집중할 수 있게 작은 단위로 만들어진 테스트입니다.

그래서 서비스 오브젝트나 MVC같은 것들이 필요가 없습니다.

이렇게 작은 단위로 테스트 하는 것을 단위 테스트(Unit test) 라고 불립니다.

여기에서 말하는 단위는 크기와 범위가 딱 정해진것이 아니라 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위라고 보면 됩니다.

일반적으로는 단위는 작을수록 좋습니다. 또한, 단위를 넘어서는 다른 코드들은 신경 쓰지 않고, 참여하지도 않고 테스트가 동작할 수 있으면 좋습니다.


그렇다면 UserDaoTest는 테스트 중에 DB까지 사용이 되는데 이를 단위 테스트라고 봐도 좋은것일까요?

하지만 UserDaoTest는 이를 수행할 때 User 테이블의 내용을 비우고 테스트를 진행 했습니다.

이는 결론적으로 DB의 상태를 Test가 관장하고 있기 때문에 우리는 단위 테스트라고 부를수 있습니다.

단위 테스트가 필요한 이유는 무엇인지 살펴봅시다

우리가 단위 테스트를 하는 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지 개발자 스스로 빨리 확인 받기 위해서 우리는 단위 테스트를 사용합니다.

만약 개발자가 스스로 테스트를 받지 않느다면 나중에 클라이언트에게 주고 테스트를 받을 떄는 코드가 엄청커져서 오류를 찾기 힘들 것입니다.

이러한 단위 테스트는 우리가 만든 혹은 개선한 코드가 처음 설계하고 의도한 대로 바르게 동작했는지를 확인하기 위해서 개발자 입장에서 만든 것이므로 이를 개발자 테스트라고도 부를 수 있습니다.


자동수행 테스트 코드

UserDaoTest의 한 가지 특징은 테스트할 데이터가 코드를 통해 제공되고 테스트 작업도 코드를 통해 자동으로 실행합니다.

그래서 웹 화면에 폼을 띄우고 버튼을 누르는 일이 없습니다.

현재 우리가 만든 UserDaoTestmain() 메서드를 실행하는 가장 간단한 방법만으로 테스트의 전 과정이 자동으로 진행이 됩니다. 이러한 코드는 우리가 웹 화면에 폼을 띄우고 테스트를 직접하는 방법보다 훨씬 빨리 진행이 될 수 있습니다.

이러한 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요합니다.

또한, 테스트 코드는 어플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보단 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫습니다.

그래서 우리는 UserDaoTest 라는 별개의 클래스로 분리해서 그 안에 테스트 코드를 넣도록 했습니다.

이러한 자동으로 수행되는 코드의 장점은 자주 반복할 수 있다는 것 입니다. 이를 통해서 빠른 테스트를 할 수 있습니다.


지속적인 개선과 점진적인 개발을 위한 테스트

우리는 지금까지의 과정을 통해서 DAO코드를 → 스프링을 이용한 코드로 만들 때 테스트가 중요한 역할을 했습니다.

우리는 그래서 DAO코드를 작성하고 바로 DAO를 테스트하는 코드를 만들어 주었기 때문에 코드를 개선하는 과정에서 실수를 테스트를 통해 바로 확인할 수 있었습니다.

만약에 그것이 아니라 처음부터 스프링을 적용한 뒤 XML로 설정을 만들고 모든 코드를 검증하려고 했다면 우리는 오류가 난다면 막막할 것입니다.

우리는 이런식으로 기능을 만들면 테스트도 함께 추가하는 방식으로 점진적인 개발이 가능해집니다.

이러한 테스트를 사용한다면 새로운 기능도 기대한 대로 동작하는지 확인을 할 수 있습니다.

또한, 새로운 기능을 추가ㅎ느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지 확인이 가능합니다.


UserDaoTest의 문제점

물론 웹을 통한 Test보다 훌륭하지만 이는 문제점이 존재합니다.

수동 확인 작업의 번거로움

우리는 add()를 통해서 User 정보를 DB에 등록을하고 이 때 넣었던 User의 정보가 get()으로 가져왔을때 같은지는 눈으로 직접확인을 해야합니다. 이를 Test코드는 확인을 해주지 않습니다.

현재는 필드가 별로 많지 않아서 직접 확인해도 많이 번거롭지는 않지만 만약 필드값이 늘어나거나 복잡하다면 이는 물론 복잡한 일입니다.


실행 작업의 번거로움

우리는 DAO가 많아지고 이를 테스트하는 main() 메서드도 많아진다면 전체 기능을 테스트해보기 위해서 main() 메서드를 엄청 많이 실행하는 수고가 필요합니다.

또한, 테스트 결과를 정리하는 것도 큰 작업이 됩니다.


UserDaoTest 개선

테스트 검증의 자동화

우선 첫 번째 문제점인 수동 확인 작업의 번거로움을 해결해 봅시다.

우리는 테스트에 대해서 성공과 실패의 두 가지 결과를 가질 수 있습니다. 또한, 실패에 대해서는 2가지로 나누어집니다.

  • 테스트 진행중 오류가 발생하는 경우
  • 테스트 작업중 에러는 발생하지 않았지만 결과값이 기댓값과 다를 경우

이러한 두 가지로 나뉘어집니다.

테스트에 에러가 발생하는 것은 콘솔에 에러 메시지와 호출 스택 정보를 통해서 확인이 가능합니다.

하지만 테스트가 실패를 한다는 것은 우리는 별도의 확인 작업이 필요합니다. get() 을 통해서 가져온 결과가 add()를 한 값과 다를 때는 “테스트 실패”, 아닌 경우는 “조회 성공”이라는 값을 나타 내도록 수정하면 다음과 같습니다.

//변경 전
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + "조회 성공");

//변경 후
if (!user.getName().equals(user2.getName())){
    System.out.println("테스트 실패 (name)");
}
else if(!user.getPassword().equals(user2.getPassword())){
    System.out.println("테스트 실패 (password)");
}
else{
    System.out.println("테스트 성공");
}

이렇게 가져온 오브젝트가 같은지 equals 라는 메서드를 통해서 확인해서 검증을 할 수 있습니다.

이러한 테스트는 UserDao의 두 가지 기능이 정상적으로 동작하는지를 언제든지 손쉽게 확인할 수 있게 해줍니다.

그렇기 떄문에 이 코드의 동작에 영향을 미칠 수 있는 어떤 변화라도 생기면 언제든 다시 실행해볼 수 있습니다.

만약 스프링이 아닌 다른 프레임워크나 기술로 변화해도 우리가 만든 테스트 한 번이면 충분하게 됩니다.

우리는 만들어진 코드의 기능을 모두 점검할 수 있는 포괄적인 테스트를 만들어서 기존에 수동 테스트로는 당장 수정한 기능의 가장 간단한 케이스를 확인하기도 힘든 상황을 극복할 수 있게 되었습니다.


테스트의 효율적인 수행과 관리

이제 main() 메서드로 만든 테스트는 테스트로서 필요한 기능은 모두 갖추었습니다. 하지만 이러한 main() 메서드로는 한계가 있습니다. 이를 해결하기 위해서 우리는 Junit을 사용할 것 입니다.

Junit 테스트로 전환

Junit은 자바로 단위 테스트를 만들때 사용하는 프레임워크입니다.

프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어합니다.

이러한 프레임워크에서 동작하는 코드는 main() 메서드도 필요하지 않고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없습니다.


테스트 메서드 전환

우리가 현재 main() 메서드로 테스트가 만들어졌다는 것은 제어권을 직접 갖는다는 의미이므로 이를 일반 메서드로 옮기는 과정을 진행할 것입니다.

이때 우리는 JUnit 프레임워크가 요구하는 조건 2가지를 따라야합니다.

  • 메서드는 public 으로 선언돼야 합니다.
  • 메서드에 @Test 라는 에너테이션을 붙여야합니다.
@Test
public void addAndGet() throws SQLException{
    ApplicationContext context = new
        ClassPathXmlApplicationContext("applicationContext.xml");

        UserDao dao = context.getBena("userDao", UserDao.class); 

        /...
}

검증 코드 전환

우리는 테스트의 결과를 검증하는 if/else를 JUnit 이 제공하는 방법을 이용해서 전환해보려 합니다.

우리는 현재 equals 를 통해서 반환된 객체가 같은지를 확인합니다.

이러한 것을 assertThat 이라는 스태틱 메서드를 이용해서 비교를 하는 것으로 변환을 해줍니다.

assertThat() 은 첫 번째 파라미터의 값을 뒤에 나오는 매처 라고 불리는 조건으로 비교해서 일치하면 넘어가고 아니면 테스트가 실패하도록 합니다. 그래서 이는 “테스트 성공”과 같은 메시지를 굳이 출력할 필요는 없습니다.

@Test
public void addAndGet() throws SQLException{
    ApplicationContext context = new
        ClassPathXmlApplicationContext("applicationContext.xml");

        UserDao dao = context.getBena("userDao", UserDao.class); 

        User user = new User();
        user.setId("haechan");
        user.setName("해찬");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        // 검증
        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
}

JUnit 테스트 실행

우리를 테스트 메서드를 시작하기 위해서는 JUnit 프레임 워크를 시작시켜야 합니다.

그러기 위해서는 어디에서든 main() 메서드를 하나 추가하고 그 안에 JUnitCore 클래스의 main() 메서드를 호출해주는 간단한 코드를 넣으면 됩니다.

또한, 메서드 파라미터에는 @Test 테스트 메서드를 가진 클래스의 이름을 넣어줍니다. 예시는 다음과 같습니다.

import org.junit.runner.JUnitCore;

public static void main(String[] args) {

        JUnitCore.main("springbook.user.dao.UserDaoTest");
}

이런식으로 설정을 하면 결과가 나오게 됩니다.

만약 테스트가 실패한다면 총 수행한 테스트중에서 몇 개의 테스트가 실패했는지를 보여줍니다.

우리는 assertThat() 에서 검증한 결과값이 아니면 AssertionError 를 던져서 예외를 발생합니다. 그 뒤에 테스트는 중단이 됩니다.


개발자를 위한 테스팅 프레임워크 JUnit

JUnit은 자바의 표준 테스팅 프레임워크라고 불릴만큼 폭넓게 사용되고 있습니다.

스프링 프레임워크 자체도 JUnit 프레임워크를 이용해서 테스트를 만들어가면서 개발되었습니다. 그래서 스프링도 JUnit을 이용합니다. 그래서 이러한 기능을 배워보도록 하겠습니다.

테스트 결과의 일관성

우리는 현재 JUnit을 적용해 깔끔한 테스트 코드를 만들었습니다. 하지만 불편한 점도 존재합니다.

우리는 테스트를 실행할 때 DB의 USER 테이블의 데이터를 모두 사젝해줘야 합니다. 만약 이전 테스트의 정보가 있으면 에러가 발생할 것입니다.

이를 살펴본다면 외부의 (DB 에) 상태에 따라서 성공하기도 실패하기도 한다는 점입니다.

이러한 코드는 외부요인에 따라서 변하기 때문에 좋은 테스트라고 할 수 없습니다.

만약 DB의 USER 테이블에 문제가 있다면 이를 테스트를 실행하고 난 뒤 마다 테스트를 수행하기 이전 상태로 만들어 주는 것입니다.

이렇게 한다면 테스트를 수행하기 이전 상태로 만들어줄 수 있습니다.


deleteAll()의 getCount() 추가

일관성있는 결과를 만들기 위해서 다음 코드를 추가합니다.

// deleteAll() 메서드
public void deleteAll() thorws SQLException{
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("delete from users");
    ps.executeUpdate();

    ps.close();
    c.close();
}

// getCount() 메서드
public int getCount() throws SQLException{
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("select count(*) from users");

    ResultSet rs = ps.executeQuery();
    rs.next();
    int count = rs.getInt(1);

    rs.close();
    ps.close();
    c.close();

    return count;
}

deleteAll()과 getCount()의 테스트

우리는 새로운 기능을 추가했기 때문에 추가된 기능에 대한 테스트를 만들어 봅시다.

하지만 새로 만든 deleteAll()getCount()는 독립적으로 자동 실행되는 테스트를 만들기가 애매합니다.

만약, 굳이 테스트를 한다면 수동으로 데이터를 넣은 뒤 deleteAll() 메서드로 삭제를 하고 테이블에 남은 게 있는지 확인해야 하면 됩니다. 하지만 이런 방법은 자동회되서 반복적으로 실행 가능한 테스트 방법은 아닙니다.

그래서 우리는 addAndGet() 테스트를 확정하는 방법을 사용해서 이를 검증합니다.

이에 대한 테스트의 불편한 점은 수동으로 USER 테이블의 내용을 모두 삭제해줘야합니다.

이를 사용하면 매번 USER 테이블의 모든 내용을 삭제할 수 있어 테이블을 수동으로 삭제하지 않아도 됩니다.

하지만 이는 부족합니다. 우선 deleteAll() 의 검증이 되지 않았는데 addAndGet() 의 검증을 진행할 수 없습니다. 그래서 getCount() 를 사용해서 검증을 합니다. 만약 모두 삭제가 되었으면 count = 0 이 될 것입니다.

일단 우리는 getCount()를 검증을 해봅시다.

앞에서 add()는 검증이 완료되었기 때문에 add()를 통해서 만약, 데이터를 넣으면 count가 바뀌는 것을 확인하면 검증이 가능합니다. 이를 통해서 deleteAll()을 하고 count가 0이 되는것을 보고 난 뒤에 이도 검증을 끝마칠수 있습니다.

// addAndGet() 
@Test
public void addAndGet() throws SQLException{

        .../

        dao.deleteAll();
        assertThat(dao.getCount(), is(0));    

        User user = new User();
        user.setId("haechan");
        user.setName("해찬");
        user.setPassword("1234");

        dao.add(user);

        User user2 = dao.get(user.getId());

        // 검증
        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
}

동일한 결과를 보장하는 테스트

이제 테스트를 실행해본다면 몇번을 실행하도 계속 성공할 것입니다. 이러한 addAndGet(0) 을 통해서 수동으로 데이터를 삭제할 일이 사라졌습니다.

우리는 이러한 방식말고도 다른 방식이 존재합니다. addAndGet() 테스트를 마치기 전에 테스트가 변경하거나 추가한 데이터를 모두 원래 상태로 만들어 주는 것도 한가지 방법입니다.

이것도 나쁜방법은 아니지만 addAndGet() 테스트 실행 이전에 다른 이유로 USER 테이블에 데이터가 들어가 있으면 테스트가 실패할수도 있습니다. 그래서 테스트하기 전에 테스트 실행에 문제가 되지 않는 상태를 만들어주는 편이 좋습니다.


포괄적인 테스트

우리는 getCount(0) 를 사용해서 메서드에 테스트를 적용했지만 우리는 테이블이 비어있는 경우와 한번호출하는 경우만 사용이 가능합니다

이러한 우리가 생각하지도 못하는 문제들이 숨어있기 때문에 성의없게 테스트 코드를 작성하면 안됩니다.

getCount() 테스트

우리는 이러한 getCount() 테스트를 좀더 꼼꼼하게 만드려고 합니다.

이번에는 여러 개의 User를 등록해가면서 getCount() 의 결과를 매번확인하려고 합니다.

이를 위해서 addAndGet 메서드를 추가하는 것은 좋은 생각이 아닙니다. 왜냐하면 테스트 메서드는 한 번에 한 가지 검증 목적에만 충실해야 하기 때문입니다.

JUnit은 하나의 클래스 안에 여러 개의 테스트 메서드가 들어가는 것을 허용합니다. 그렇기 때문에 이를 메서드를 여러개 만들어서 사용을합니다.

우선 테스트를 만들기 전에 먼저 User 클래스에 한 번에 모든 정보를 넣을 수 있도록 초기화가 가능한 생성자를 추가합니다.

public User(String id, String name, String password){
    this.id = id;
    this.name = name;
    this.password = password;
}

public User(){ // 기본생성자 만들기

}

이렇게 변경하면 addAndGet() 도 다음과 같이 수정이 가능합니다.

UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User("gyumee", "박성철", "springno1");

이제 getCount() 에 대한 테스트 메서드를 작성해보겠습니다.

@Test
public void count() throws SQLException{
    ApplicationContext context = new GenricXmlApplicationContext("applicationContex.xml");

    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("gyumee", "박성철", "springno1");
    User user2 = new User("hachan", "유해찬", "springno2");
    User user3 = new User("chayoo", "찬해유", "springno3");

    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.add(user1);
    assertThat(dao.getCount(), is(1));

    dao.add(user2);
    assertThat(dao.getCount(), is(2));

    dao.add(user3);
    assertThat(dao.getCount(), is(3));
}

이렇게 작성을 하면 별 문제 없이 실행이 됩니다.

하지만 이는 특정한 테스트 메서드의 실행 순서를 보장해주지는 안씁니다.

만약, 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것입니다.

그래서 우리는 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야합니다.


addAndGet() 테스트 보완

이제 addAndGet() 을 보완해봅시다. 이는 우리는 현재 add() 는 여러번 하면서 검증을 완료하였지만 get() 에 대해서는 id를 조건으로만 찾았기 때문에 이는 부족한 감이 있습니다. 그래서 검증이 부족한 감이 있습니다.

이를 User의 id를 파라미터로 전달하여 get()을 실행하게 할 것입니다.

그래서 get()에 대한 테스트 기느이 보완된 addAndGet() 테스트코드를 살펴봅시다.

@Test
public void addAndGet() throws SQLExeption{
    /...
    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("haechan", "유해찬", "spring1");
    User user2 = new User("chanyoo", "찬해유", "spring2");

    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    dao.add(user1);
    dao.add(user2);
    assertThat(dao.getCount(), is(2));

    User userget1 = dao.get(user1.getId());
    assertThat(userget1.getName(), is(user1.getName());
    assertThat(userget1.getPassword(), is(user1.getPassword());

    User userget2 = dao.get(user2.getId());
    assertThat(userget2.getName(), is(user2.getName());
    assertThat(userget2.getPassword(), is(user2.getPassword());

}

이를 통해서 get() 메서드에 대해서 더욱 정확히 확인할 수 있습니다.


get() 예외조건에 대한 테스트

get() 메서드에 전달된 id 값에 해당하는 사용자 정보가 없으면 어떻게 될지 생각해봅시다.

우리는 이를 2가지 방법으로 처리할 수 있습니다.

  • null과 같은 값을 리턴하는 방식
  • id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 방식입니다.

우리는 후자를 사용할 것입니다.

스프링에서는 데이터 액세스 예외 클래스가 있습니다. 우리는 그 예외인 EmptyResultDataAccessException 예외를 사용할 것입니다.

만약, get()을 실행 했을 때 결과에 아무것도 없으면 예외를 던지게 하면됩니다. 만약 예외가 던져지게 되면 테스트 메서드의 실행은 중단되고 실패합니다.

하지만 우리는 현재 EmptyResultDsataAccessException 이라는 예외가 던져져야 우리는 테스트가 성공을 했구나? 라는 것을 알 수 있습니다. 이런 특별한 상황에는 assertThat() 을 사용할 수 없습니다.

그래서 이런경우에는 JUnit의 기능을 사용할 수 있습니다. 확인해 봅시다.

@Test(excepted=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException{
    ApplicationContext context = new GenericXmlApplicationContext("appplicationContext.xml");

    UserDao dao = context.getBean("userDao", UserDao.class);
    dao.deleteAll();
    assertThat(dao.getCount(),is(0));

    dao.get("unknown_id");
}

@Test 에서 excepted 엘리먼트를 사용해서 우리는 기대하는 예외클래스를 넣어주면 확이니 가능합니다.

만약, excepted 라는 엘리먼트를 추가하면 예외가 던져지면 성공이고 던져지지 않으면 실패입니다.


테스트를 성공시키기 위한 코드의 수정

이제부터 우리는 get() 메서드 코드를 수정해서 이 테스트가 성공하도록 할 것입니다.

다음과 같이 작성을 하면 만들 수 잇습니다.

// 데이터를 찾지 못하면 예외를 발생시키도록 수정한 get() 메서드
public User get(String id) throws SQLException{
    /...
    ResultSet rs = ps.excuteQuery();

    User user = null;
    if(rs.next()){
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

    rs.close();
    ps.close();
    c.close();

    if(user == null) throw new EmptyResultDataAccessException(1);

    return user;
    }
}

이런식으로 get() 메서드에서 데이터가 없을 때 예외를 발생시키면 테스트는 성공할 것입니다.


포괄적인 테스트

사실 우리가 작성한 코드들은 간단해서 테스트가 없어도 문제를 살펴볼 수 잇습니다.

하지만 이런식으로 DAO의 메서드에 대해서 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용합니다.

또한, 여러가지 이점이 존재합니다.

개발자들은 이상한 습성이 있습니다. 바로 성공하는 테스트만 골라서 만드는 것입니다. 하지만 이러한 것은 큰 문제점입니다. 스프링의 창시자도 ”항상 네거티브 테스트를 먼저 만들라” 라는 조언을 하고 잇습니다. 그렇기 때문에 부정적인 케이스 부터 먼저 만드는 습관을 들이는 것이 좋습니다.


테스트가 이끄는 개발

우리는 get() 메서드의 예외 테스트를 만들면서 UserDao 코드를 수정을 계속해서 반복하였습니다. 이는 테스트를 통해서 우리는 개발을 진행하였습니다.

물론 테스트할 코드도 없이 테스트 코드를 만드는 것은 이상하지만 이러한 순서를 따라서 개발하는 TDD 방식의 개발도 존재합니다, 우리는 이런 개발 방법을 적극적으로 권장합니다.=


기능설계를 위한 테스트

우리는 가장 먼저 존재하지 않는 id로 get() 메서드를 실행한다면 특정한 예외가 던져져야한다. 라는 방식으로 만들어야 할 기능을 결정하였습니다.

하지만 테스트할 코드도 없는데 어떻게 테스트할까요? 이는 추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 가능합니다.

우리는 이를 조건, 행위, 결과로 나타낼 수 있습니다.

getUserFailure()

타입 단계 내용 코드
조건 어떤 조건을 가지고 가져올 사용자 정보가 존재하지 않는 경우에 dao.deleteAll()
assertThat(dao.getCount()m is(0));
행위 무엇을 할 때 존재하지 않는 id로 get()을 실행하면 get("unknown_id");
단계 어떤 결과가 나온다 특별한 예외가 던져진다. @Test(expected=EmptyResultDataAccessEception.class)

이러한 테스트 코드는 마치 잘 작성된 하나의 기능정의서처럼 보입니다.

테스트가 실패한다면 설계한 대로 코드가 만들어지지 않는다는 것이기 때문에 이를 통해서 코드를 계속 다듬어갑니다.


테스트 주도 개발

우리는 이러한 방식을 테스트 주도 개발(TDD, Test Driven Development)라고 합니다.

또는 테스트 우선 개발 이라고도 부릅니다.

테스트 주도 개발의 중요한 점은 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다. 라는 것입니다. 이에 대한 장점은 다음과 같습니다.

  • TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어 낼 수 있습니다.
  • 또한, 코드를 만들고 테스트를 수행할 때까지 걸리는 시간은 0입니다.
  • 매번 테스트가 성공하는 것을 보면서 작성한 코드에 대한 확신을 가질 수 있습ㄴ디ㅏ.

이러한 TDD를 할 때는 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 짧게 가져가도록 권장합니다.

우리는 머리속에서 예를들어 ‘이런 조건의 값이 들어오면 코드의 흐름과 조건을 따라서 이렇게 진행되서 이런결과의 값이 나오겠다.’ 라는 식의 시뮬레이션을 계속합니다.

현재 우리는 머리속으로만 이를 시뮬레이션을 하는데 그러면 제약도 심하고 오류도많고 다시 반복하기가 힘듭니다.

이러한 아이디어들을 바로 코드로 끄집에 내는 것이 TDD 입니다.


테스트 코드 개선

우리는 3개의 테스트 메서드를 만들었습니다. 이제 테스트 코드에 대해서 리펙토링 해봅시다.

일단 UserDao 를 가져오는 부분이 계속 반복이 됩니다. 이를 메서드를 통해서 분리하는 것 보다는

JUnit 이 제공하는 기능을활용해서 변경해봅시다.

@Before

우리는 @Before 어노테이션을 사용해서 setUp() 이라는 메서드를 만들어서 테스트 메서드에서 반복되는 ㄱ코드를 넣고 이를 로컬로 private 하게 변수를 선언해서 변수에 저장하여 언제든지 쓸수 있게 해봅시다.

import org.junit.Before;

public class UserDaoTest{
    private UserDao dao;

    @Before
    public void setUp(){
        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        this.dao = context.getBean("userDao", UserDao.class);
    }

    @Test
    public void addAndGet() throws SQLException{
        /...
    }

    /...
}

이런식으로 작성하게 되면 테스트 코드를 실행 전에 이를 얻게 됩니다.

왜 이렇게 작동하는지에 대해서는 JUnit 이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식을 살펴봅시다.

  1. 테스트 클래스에서 @Test 가 붙은 public 이고 void 형이며 파라미터가 없는 테스트 메서드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다.
  3. @Before 가 붙은 메서드가 있으면 실행한다.
  4. @Test 가 붙은 메서드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After 가 붙은 메서드가 있으면 실행합니다.
  6. 나머지 테스트 메서드에 대해 2~5번을 반복합니다.
  7. 모든 테스트의 결과를 종합해서 돌려줍니다.

세부적으로는 더 복잡하지만 크게보면 다음과 같습ㄴ디ㅏ.

그렇기 때문에 @Before 이나 @After 메서드를 자동으로 실행합니다.

하지만, @Before@After 메서드를 테스트 메서드에서 직접 호출하지 않기 때문에 이를 인스턴스 변수를 이용해서 저장하여 테스트 메서드에서 사용하게 만들면 됩니다.

이러한 테스트 메서드를 실행할때마다 새로운 오브젝트를 만들고 있습니다. JUnit은 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해서 매번 새로운 오브젝트들을 만들게 한것이 그 이유입니다

그래서 우리는 인스턴스 변수를 부담없이 사용을 할 수 있습니다. 다음 텍스트 메서드가 실행 될 때는 새로운 오브젝트가 만들어져서 다 초기화 될 것이기 때문입니다.

만약, 테스트 메서드의 일부에서만 공통적으로 사용되는 코드가 있다면 어떻게 해야할까요?

이때는 @Before 보다는 일반적으로 우리가 사용하는 메서드 분리 방식을 이용하는 것이 바람직 합니다.


픽스처

픽스처(fixture)는 테스트를 수행하는 데 필요한 정보나 오브젝트를 말합니다.

이는 여러 테스트에서 반복되어 사용되기 때문에 @Before 를 통해서 메서드를 생성해두면 편합니다.

우리가 짠 코드를 본다면 현재 getUserFailure() 라는 테스트를 제외하고는 User 에 대한 오브젝트가 사용이 되고 있습니다. 우리는 일부에서만 공통적으로 사용하기 때문에 메서드 분리 방식을 이용한다고 생각하지만 다른 테스트들은 거의다 User 에 대해서 사용할 것이기 때문에 @Before 를 통해서 빼주도록 합니다.

그래서 다음과 같은 코드를 생성할 수 있습니다.

public class UserDaoTest{
    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp(){
        /...
        this.user1 = new User("haechan", "유해찬", "spring1");
        this.user2 = new User("chanyoo", "찬유해", "spring2");
        this.user3 = new User("yoohae", "해유찬", "spring3");
    }
} 

의존관계 주입(DI)

의존관계 주입(DI)

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

우리가 배우고 있는 IoC는 너무 폭넓게 사용하는 언어입니다. 그래서 스프링을 IoC 컨테이너 라고만 말하면 스프링이 제공하는 기능의 특징을 명확하게 설명할 수 없습니다.

이렇게 명확하지 않고 너무 폭넓게 사용하기 때문에 스프링이 제공하는 IoC방식의 핵심을 짚어주는 의존관계 주입(Dependency Injection) 이라는 단어를 사용해서 부르기로 약속하였습니다.

스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불립니다.


런타임 의존관계 설정

의존관계

우선 의존관계 라는 것은 방향성이라는 것이 있습니다. 의존을 하는 것과 의존을 받는 것 2가지로 나뉘어집니다.

만약 의존한다 라는 것은 의존 대상이 변하면 의존하는 것에 영향을 미친다는 것입니다.

다음과 같이 의존이라는 것은 점선으로 된 화살표로 나타냅니다.

현재는 A가 B에 의존하고 있다는 것을 나타냅니다.

만약, B가 변하면 A에 영향이 그대로 전달됩니다.

의존관계에는 방향성이 있습니다. 서로 의존관계라면 예를들어서 B가 현재 새로운 메서드가 추가되거나 기존 메서드의 형식이 변한다면 A에도 영향을 미쳐서 A도 변화를 시켜야합니다.


UserDao의 의존관계

UserDao 는 현재 ConnectionMaker 라는 인터페이스에 의존하고 있습니다.

그래서 만약, ConnectionMaker 의 메서드가 변하던가 이러한 일들이 일어난다면 UserDao에 영향을 직접적으로 받게 됩니다.

하지만 UserDaoConnectionMaker 를 구현한 클래스인 다른 회사의 Maker 예를들어서 AconnectionMaker 등이 변화할 때는 영향이 없습니다.

원래는 UserDaoConnectionMaker 라는 인터페이스가 없었을 경우는 다른 회사의 ConnectionMaker 로 변화했을 경우에 의존관계 때문에 UserDao 의 코드가 직접적으로 변경이 되어야 했지만 이제는 그렇지 않습니다.

이런식으로 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에서 자유로워질 수 있습니다.

우리가 이런식으로 ConnecitonMaker 라는 인터페이스를 통해서 새로 만들어서 UserDao 는 어떤 회사의 ConnectionMaker 를 사용하는지 알지도 못한채로 사용을 하고있습니다.

그래서 결론적으로 UserDaoConnectionMaker 라는 인터페이스를 사용하고 있기 때문에 UserDao 의 오브젝트가 런타임 시에 사용할 오브젝트(의존 오브젝트 / Dependent Object)가 어떤 클래스로 만든 것인지 미리 알 수가 없습니다.

결론적으로 의존관계 주입이란 구체적인 의존 오브젝트와 그것을 사용할 사용할 주체인 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말합니다.

이러한 의존관계 주입은 다음과 같은 세가지 조건을 충족하는 작업을 말합니다.

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

우리는 현재로 DaoFactory 를 사용해서 DI에서 말하는 제 3의 존재를 설정하였습니다.


UserDao의 의존관계 주입

우리는 UserDao에 적용된 의존관계 주입 기술을 다시 살펴볼 것입니다.

우리가 만든 예전의 UserDao 클래스의 생성자는 다음과 같습니다.

public UserDao(){
    connectionMaker = new AConnectionMaker();
}

이러한 UserDao 의 예전 코드는 저런식으로 AConnectionMaker() 라는 구체적인 클래스를 알고있습니다.

이러한 것은 런타임 시의 의존관계가 코드 속에 다 미리 결정되어 있다는 점이 문제점입니다.

이를 우리는 IoC 방식을 사용해서 UserDaoDaoFactory 라는 제3의 존재에 런타임 의존관계 결정 권환을 위임을 시켰습니다.

이런방식으로 우리는 앞서말한 3가지 조건을 충족시켜서 의존관계 주입을 이용했습니다. 이렇게 만들어진 DaoFactory는 다음과 같은 기능을 합니다.

  • 두 오브젝트 사이의 런터임 의존관계를 설정해주는 의존관계 주입 작업을 주도하는 존재입니다.
  • IoC방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너입니다.
  • 또한, 이를 의존관계 주입을 담당하는 컨테이너라고 볼 수 있고 DI 컨테이너라고 불러도 됩니다.

그래서 결론적으로 DaoFactoryDI 컨테이너라고 부릅니다.

우리는 DI 컨테이너인 DaoFactory 를 통해서 UserDao를 만드는 시점에서 생성자의 파라미터로 이미 만들어진 AConnectionMaker의 오브젝트를 전달합니다.

이런식으로 파라미터 전달을 생성자를 통해서 해줍니다. 그래서 우리는 결론적으로 다음과 같은 코드로 런타임 의존관계를 두 오브젝트간에 만들었습니다.

public class UserDao{
    private ConnectionMaker connectionMaker;

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

이렇게 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 래퍼런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가 UserDao에게 주입해주는 것과 같다. 라고 보이기 때문에 이를 의존관계 주입(DI)라고 합니다.


의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아닙니다.

의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용해서 하는 방식도 있습니다.

이러한 것을 의존관계 검색(dependency lookup)이라고 불리는 것도 있습니다. 한번 살펴봅시다.

다음과 같은 UserDao의 생성자 리스트를 만들었다고 합니다.

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

이렇게 만든 생성자는 자신이 어떤 기업의 ConnectionMaker 오브젝트를 사용할지 미리 알지 못합니다.

또한, 의존대상은 ConnectionMaker 인터페이스 뿐입니다.

코드를 보면 DaoFactory 가 만들어서 돌려주는 오브젝트와 다이내믹하게 런타임 의존관계를 맺고있습니다.

그래서 IoC 개념을 잘 따르고 있지만 적용 방법이 생성자에서 DaoFactroy 에게 스스로 요청을 하고 있습니다.

지금은 자바코드지만 스프링에서 Application Context를 사용했다고 생각하면 다음과 같이 코드가 바뀔것 입니다.

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

getBean() 이라는 메서드를 사용해서 의존관계 검색을 합니다.

이런식으로 ApplicationContext를 사용해서 미리 정해놓은 이름을 전달해서 그 이름에 해당하는 오브젝트를 찾게 하기 때문에 우리는 의존관계 검색이라고 합니다.

이렇게 만들어진 의존관계 검색은 기존 의존관계 주입의 거의 모든 장점을 가집니다. 또한, IoC 원칙에도 잘 들어맞습니다. 하지만 방법만 다릅니다.

결론적으로 어떤것이 더 낫냐라고 본다면 의존관계 주입이 더 깔끔합니다. 또한, 다른 단점들도 존재합니다.

  • 의존관계 검색을 사용하면 코드 안에 오브젝트 팩토리 클래스나 스프링 API가 나타나서 성격이 다른 오브젝트에 의존하게 되어 바람직하지 않습니다.
  • 사용자에 대한 DB 정보를 어떻게 가져올건가에 집중하는 UserDao 에서 스프링이나 오브젝트 팩토리를 만들고 API를 이용하는 코드가 섞여있으면 어색합니다.

그래서 의존관계 주입 방법을 사용하지만 의존관계 검색 방식을 사용해야할 떄도 있습니다.

앞서서 만들었던 UserDaoTest를 다시 봅시다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{

                ApplicationContext context = 
                    new AnnotationCongifApplicationContext(DaoFactory.class);

                UserDao dao = context.getBean("userDao", UserDao.class);
        }
}

확인해보면 의존관계 검색 방식을 사용한 것을 볼 수 있습니다. 이러한 경우는 스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 합니다.

왜냐하면 main() 메서드는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문에 우리는 서블릿이라는 것을 사용하여 스프링 컨테이너에 담긴 오브젝트를 사용하기 위해서 의존관계 검색 방식을 사용해 오브젝트를 가져와야한다.

우리가 사용하는 의존관계 검색의존관계 주입은 차이점이 있습니다.

의존관계 검색 방식은 검색하는 오브젝트는 자신이 스프링의 빈일 필요는 없습니다.

위에 코드를 살펴보면 UserDao 는 굳이 getBean()이 아닌 new UserDao() 해서 만들어도 사용해도 됩니다.

의존관계 주입에서는 UserDaoConnectionMaker 사이에 DI가 적용되려면 UserDao 도 반드시 컨테이너가 만드는 빈 오브젝트여야 합니다.

DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈이 돼야 한다는 사실을 잊지 말아야합니다.


의존관계 주입의 응용

런타임 시에 사용 의존관계를 맺을 오브젝트를 주입해준다는 DI 기술의 장점은 무엇인지 확인해봅시다.

우리가 앞에서 오브젝트 팩토리로 구현을 하였는데 이는 DI 방식을 구현한 것입니다. 이러한 DI의 장점을 그대로 얻을 수 있습니다.

  • 코드에는 런타임 클래스에 대한 의존관계가 나타나지 않습니다.
  • 인터페이스를 통해 결합도가 낮은 코드를 만들어 다른 책임을 가진 사용 의존관계에 있는 대상이 바뀌거나 변경되더라도 자신은 영향을 받지 않습니다.
  • 변경을 통한 다양한 확장 방법에는 자유롭습니다.

우리는 이러한 DI 방식을 스프링을 사용하면 99% 혜택을 볼 수 있습니다. 이러한 DI 방식의 여러가지 응용사례를 살펴봅시다.


기능 구현의 교환

실제 운영에 사용할 데이터베이스는 매우 중요한 자원입니다. 이러한 데이터 베이스는 항상 부하가 많이 가해집니다. 그래서 개발 중에는 절대로 사용하지 말아야합니다.

그래서 이제 개발자의 로컬 DB로 사용한다고 생각해봅시다. 우리는 로컬 DB에 대한 기능이 있는 LocalDBConnectionMaker 라는 클래스를 만들고 제일 처음에 만들었던 초난감 DAO와 같이 사용하고 있다고 생각해봅시다.

이렇게 Local에서 사용을 하고나서 다시 서버에 배포할 때는 다시 서버가 제공하는 특별한 DB 연결 클래스를 사용해야한다고 생각해봅시다.

우리는 일단 초난감 DAO방식을 사용하고 있기 때문에 모든 DAO들에는 new LocalDBConnectionMaker() 라는 코드를 사용해서 LocalDBConnectionMaker에 의존을 하고 있을 것입니다.

만약 이제 배포를 해야한다고 생각해봅시다. 그러면 LocalDBConnectionMaker 에서 ProductionDBConnectionMaker 라는 클래스로 변경해줘야 한다고 생각해봅시다. 만약 DAO가 1000개라면 1000개의 코드를 모두 수정해야합니다. 이러한 방식은 매우 끔찍합니다.

만약, 초난감 DAO가 아닌 DI 방식을 적용해서 만들었다고 생각해봅시다.

그러면 DAO는 생성 시점에서 ConnectionMaker 타입의 오브젝트를 컨테이너로부터 제공을 받습니다.

컨테이너에는 구체적으로 사용할 클래스 이름이 들어있습니다.

그래서 @Configuration이 붙은 DaoFactory를 사용한다고 한다면 개발자 PC에서 DaoFactory 코드를 이전에 만든 방식처럼 만들면 됩니다.

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

이런 방식처럼 만든다면 DAO 클래스에서는 어떤 코드도 변경을 안해도 됩니다. 단지 다음과 같이 변경만 해주면 됩니다.

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

이런식으로 DB가 개발용 배포용이 나눠져있거나 다른 DB들을 여러개 사용할 경우 이런식으로 DI 컨테이너를 만들어서 모든 DAO가 사용할 수 있도록 DI 해줄 수 있습니다.


부가기능 추가

만약에 우리가 DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다고 생각해봅시다.

첫 번째 방법으로는 그냥 단순하게 DAO의 makeConnection() 메서드를 호출하는 부분에 카운터를 증가시키는 코드를 넣을 수 있습니다. 하지만 이런 방식은 너무 비효율적 입니다.

두 번째 방법으로는 DI 컨테이너에서 사용할 수 있는 방법입니다. DI에서는 매우 간단하게 하결할 수 있습니다. 그냥 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하면 됩니다.

DI 개념을 쉽게 응용한다면 기존코드를 수정하지 않고 그냥 컨테이너가 사용하는 설정정보만 수정해서 런타임 의존관계만 새롭게 정의하면 가능합니다. 우리는 CountingConnectionMaker라는 클래스를 리스트 1-30과 같이 만듭니다. 또한, 이 클래스는 ConnectionMaker 인터페이스를 구현해서 만든다고 합니다.

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

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

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

    public int getCounter() {
        return this.counter;
    }
}

CountingConnectionMaker 클래스는 현재 ConnectionMaker 를 인터페이스를 implement해서 구현을 하였지만 직접 DB커넥션을 만들지는 않았습니다.

대신 DAO가 DB를 가져오는 makeConnection()에서 DB 연결횟수 카운터를 증가시킵니다.

CountingConnectionMaker 는 DB 연결횟수 카운팅이 관심사여서 이를 끝낸 뒤에는 DB 커넥션이 정의되어 있는 realConnectionMaker 에 저장된 ConnectionMaker 타입 오브젝트의 makeConnection()을 호출해서 그 결과를 DAO에게 돌려줍니다.

이러한 것은 CountingConnectionMaker도 DI를 받고 이에 대한 오브젝트가 DI를 받을 오브젝트도 ConnectionMaker 인터페이스를 구현한 오브젝트입니다. 결론적으로 우리가 사용하는 AConnectionMaker 같은 실제 DB 커넥션을 돌려주는 클래스의 오브젝트를 받습ㄴ디ㅏ.

이는 CountingConnectionMakerConnectionMakerUserDao 로 가는 것을 볼 수 있습니다.

AConnectionMaker 가 구현하고 있는 ConnectionMaker 라는 인터페이스에만 의존하고 있기 때문에 만약 ConnectionMaker 를 implement 하고 있는 어떤 클래스라도 바꿔치기가 가능합니다. 그래서 CountingConnectionMaker 가 사용이 가능한 것입니다.

우리는 이것만 사용하면 안됩니다. CountingConnectionMaker가 다시 실제 사용할 DB 커넥션을 제공해주는 AConnectionMaker 를 호출하도록 만들어야 합니다. 이럴 때도 DI를 사용하면 됩니다.

다음과 같이 CountingDaoFactory 라는 컨테이너를 만들어서 사용할 수 있게 만들 수 있습니다.

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

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

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

이제 커넥션 카운팅을 위한 실행코드를 만들어 봅시다. DL(의존관계 검색)을 사용해서 CountingConnectionMaker 빈을 가져와서 작성해봅시다.

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

        // DL 사용
        CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
        System.out.println("Connection counter의 수는 : " + ccm.getCounter());
    }

}

이런식으로 DAO가 여러개여도 관심사의 분리를 통해서 얻어지는 높은 응집도에서 DI의 장점이 나옵니다.

이러한 DI는 정말 중요하고 좋은 도구입니다. 그래서 우리는 DI를 어떻게 활용해야 할지를 공부해야합니다.


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

지금까지는 UserDao의 의존관계 주입을 위해서 생성자를 사용하였습니다.

그래서 생성자에 파라미터를 만들어서 DI 컨테이너가 의존할 오브젝트 레퍼런스를 넘겨주었습니다.

현재까지는 생성자에만 사용했지만 이는 일반 메서드를 통해서 의존 오브젝트와의 관계를 주입해줄 수도 있습니다.


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

수정자는 파라미터로 전달된 값을 내부의 인스턴스 변수에 저장하는 것 입니다. 이러한 수정자는 DI 방식에서 활용하기에 적당합니다.


일반 메서드를 이용한 주입

수정자는 정해진 형태가 있고 파라미터는 한개받게 못받습니다. 만약 이런게 싫다면 일반 메서드를 이용할 수도 있습니다.

하지만 파라미터의 개수가 많아져 비슷한 타입이 여러 개라면 실수하기가 쉽습니다. 하지만, 여러개의 파라미터를 받아서 여러 개의 초기화 메서드도 만들 수 있기 때문에 이런것에 대해서 장점이 존재합니다.

스프링은 보통 메서드를 이용한 DI 방식중에서는 수정자 메서드를 가장 많이 사용했습니다.

수정자 메서드 DI를 사용할 때에는 메서드의 이름을 잘 결정하는 것이 좋습니다.

한번 만들어 봅시다. 우리는 기존 생성자를 제거하고 setConnectionMaker() 라는 메서드를 하나 추가한 뒤 파라미터로 ConnectionMaker 타입의 오브젝트를 받도록 선언합시다.

public class UserDao{
    private ConnectionMaker connectionMaker;

    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

이런식으로 만들어 주었다면 DaoFactory의 코드도 함꼐 수정해야합니다.

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

이러면 의존관계를 주입하는 시점과 방법만 달라지고 결과는 동일합니다. 이뿐만이 아니라 다양한 의존관계 주입 방법도 지원합니다.


XML을 이용한 설정

지금까지 DI 컨테이너인 스프링을 도입하면서 애너테이션을 추가해서 DI를 설정해주었습니다.

우리는 DaoFactory와 같은 자바 클래스로만 이용하여 DI 의존관계 설정정보를 만들었지만 다른 방법도 존재합니다.

우리는 그중에서 XML을 이용하는 방법을 살펴 봅시다.

XML의 장점은 다음과 같습니다.

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

이제 한번 XML을 통해서 만들어 봅시다.


XML 설정

스프링 Application Context는 XML에 담긴 DI 정보를 활용할 수 있습니다.

우리는 <beans> 를 루트 엘리먼트로 사용해서 사용할 수 있습니다. 이러한 <beans> 안에는 여러개의 <bean> 가 들어갈 수 있습니다.

본래대로면 @Bean 메서드에서는 알 수 있는 빈의 DI 정보는 3가지가 존재합니다.

  • 빈의 이름 : 메서드의 이름입니다.
  • 빈의 클래스 : 빈 오브젝트를 어떤 클래스에 이용해서 만들지 결정합니다.
  • 빈의 의존 오브젝트 : 빈의 생성자나 수정자 메서드 등을 통해서 의존 오브젝트를 넣어줍니다. 이는 하나 이상일 수도 있습니다.

XML에서도 마찬가지로 이러한 3가지 정보를 정의할 수 있습니다.


connectionMaker() 전환

목록 자바 코드 설정정보 XML 설정정보
빈 설정파일 @Configuration <beans>
빈의 이름 @Bean 메서드이름() <bean id=”메서드이름”
빈의 클래스 return new BeanClass(); class=”a.b.c...BeanClass”>

다음처럼 1:1교환으로 만들 수 있습니다. 여기서 <bean> 에 들어간느 class 속성에 지정하는 것은 자바 메서드에서 오브젝트를 만들 때 사용하는 클래스 이름입니다.

그렇기 때문에 메서드의 리턴 타입을 class의 속성에 넣으면 안됩니다.

이를 변경하면 다음과 같습니다.

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

XML은 다음과 같습니다.

<bean id="connectionMaker" class="springbook... AConnectionMaker"/>

DI 컨테이너는 이러한 XML의 <bean> 태그의 정보를 읽어서 우리가 작성했던 connectionMaker() 메서드 와 같은 작업을 진행합니다.


userDao()전환

이번에는 userDao 를 XML로 변환해봅시다. 보통 수정자 메서드를 사용하는게 왜 스프링개발자들이 선호를 했었냐면 XML로 의존관계 정보를 만들 때 편리하기 때문입니다.

만약 수정자 메서드를 사용하면 setConnectionMaker() 가 있다면 set 을 제외한 connectionMaker 라는 프로퍼티를 가집니다.

XML에서는 <property> 태그를 사용해서 의존 오브젝트와의 관계를 정의합니다. 이는 2개의 속성을 가집니다.

  • name : 프로퍼티의 이름 → 이를 통해서 수정자 메서드를 알 수 있습니다.
  • ref : 수정자 메서드를 통해서 주입해줄 오브젝트의 빈 이름입니다. / DI할 오브젝트도 빈입니다. 그 빈의 이름을 지정해주면 됩니다.

그래서 만약에 다음과 같은 방식으로 UserDaoTest 에서 사용을 한다면 하나씩 확인을 해봅시다.

userDao.setConnectionMaker(connectionMaker());

여기서 connectionMaker()userDao 빈의 connectionMaker() 라는 프로퍼티를 이용해서 의존관계 정보를 주입한다는 것입니다. 그래서 ref의 값이 됩니다.

이를 XML로 고치면 다음과 같습니다.

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

XML의 의존관계 주입정보

이를 모두 전환을 해서 적으면 다음과 같습니다.

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

현재는 property 태그의 nameref 가 같지만 이름이 같더라도 어떤 차이가 있는지 구별할 수 있어야 합니다.

  • name 속성은 DI에 사용할 수정자 메서드의 프로퍼티 이름입니다.
  • ref 속성은 주입할 오브젝트를 정의한 빈의 ID 입니다.

보통 둘이 같은 경우가 많습니다. 이러한 프로퍼티의 이름은 보통 주입할 빈 오브젝트의 인터페이스를 따르는 경우가 많습니다.

만약 빈의 이름이 중복되거나 의미를 좀 더 잘 드러낼 수 있는 이름이 있다면 변경을 할때 property 태그의 ref 속성의 값도 함께 변경을 해줘야 합니다.

만약 connectionMaker 에서 myConnectionMaker 로 변경했다고 하면 어떻게 되는지 봅시다.

<beans>
    <bean id="**myConnectionMaker**" class="springbook.user.dao.AConnectionMaker" />
    <bean id-="userDao" class="springbook.user.dao.UserDao">
        <property name="connectionMaker" ref="**myConnectionMaker**"/>
    </bean>
</beans>

만약, 인터페이스를 구현한 의존 오브젝트를 여러개 만들어 놓고 골라서 사용하는 경우도 있습니다. 아까전에 로컬 DB를 사용하는것 혹은 배포할 때DB를 사용하는것 이러한 기능들이 있는 경우도 있습니다. 그럴때는 다음과 같이 작성을 합니다.

<bean>
    <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="connectionMaker" ref="**localDBConnectionMaker**"/>
    </bean>
</bean>

이런식으로 사용하고 ref 만 내가 사용할 것을 놓으면 됩니다. 바뀌면 이것도 바꾸면 됩니다.


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

이제 Application Context가 DaoFactory 대신 XML 설정정보를 활용하도록 만들어 봅시다.

XML을 사용하면 IoC/DI 작업에서는 GenericXmlApplicationContext 를 사용해서 합니다. 이에대한 생성자 파라미터로 XML 파일의 클래스패스를 정하면 됩니다. 클래스패스를 정해야하기 때문에 보통은 클래스패스 최상단에 둡니다.

이러한 설정파일을 만들 떄는 보통 applicationContext.xml 로 만들어 저장합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframwork.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframwork.org/schema/beans
            http://www.springframwork.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="connectionMaker" class="springbook.user.dao.AConnectionMaker"/>

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

이제 UserDaoTest를 수정해봅시다. 이제 AnnotationConfigApplicationContext 대신에 GenericXmlApplicationContext를 사용합니다.

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

이런식으로 바꿔서 사용하면 됩니다.

또한, GenericXmlApplicationContext 가 아닌 ClassPathXmlApplicationContext 도 존재합니다 이는 XML 파일을 클래스패스에서 가져올 때 사용할 수 있는 편리한 기능이 추가된 것입니다.

보통 XML 파일의 경로를 가져와야하는데 귀찮으면 자동으로 클래스 오브젝트를 사용해서 다음과 같이 변환을 할 수 있습니다.

new GenericXmlApplicationContexct("springbook/user/dao/daoContext.xml");

new ClassPathXmlApplicationContext("daoContext.xml", UserDao.class);

DataSource 인터페이스로 변환

DataSource 인터페이스 적용

우리는 ConnectionMaker 를 만들어 DB 커넥션을 생성해주는 기능 하나만을 정의한 매우 단순한 인터페이스를 만들었습니다.

하지만 사실 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진 Datasource 라는 인터페이스가 이미 존재하고 있습니다.

그래서 우리는 ConnectionMaker 와 같은 인터페이스를 만들어서 사용할 일은 없습니다.

Datasource 에서 getConnection() 메서드는 우리가 만든 makeConnection() 과 동일한 기능을 하는 메서드입니다. 이를 통해서 DB 커넥션을 가져올 수 있습니다.

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 구현 클래스가 필요합니다. 우리는 스프링에서 제공하는 테스트환경에서 간단히 사용할 수 있는 SimpleDriverDataSource 라는 것을 사용해서 이 클래스를 사용하도록 DI를 재구성하려고합니다.


자바 코드 설정 방식

먼저 DaoFactory의 설정방식을 이용해봅시다. 기존의 connectionMaker() 메서드를 dataSource()로 변경하고 SimpleDriverDataSource의 오브젝트를 리턴하게 합니다.

// DaoFactory
@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;
}

이제 DaoFactoryuserDao() 메서드도 수정해봅시다.

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

이런식으로 connectionMaker()dataSource() 로 변경하였습니다.


XML 설정 방식

먼저 id가 connectionMaker<bean> 을 없앤 뒤 dataSource 라는 이름의 <bean>을 등록합니다. 그리고 SimpleDriverDataSource 로 변경합니다.

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

이런식으로 SimpleDriverDataSource의 오브젝트를 만드는 것까지 가능하지만 datasource() 메서드에서 SimpleDriverDataSource 의 오브젝트의 수정자로 넣어준 DB접속정보는 없습니다.

UserDao 처럼 다른 빈에 의존하면 그냥 property 태그랑 ref 속성을 사용해서 의존할 빈의 이름을 넣어주면 되었는데 datasource() 인경우는 어떤식으로 해결을 해야할까요?


프로퍼티 값의 주입

값 주입

DaoFactorydatasource() 메서드에서 본것 처럼 수정자 메서드에는 다른 빈이나 오브젝트 뿐만 아니라 스트링 같은 단순 값을 넣어줄 수도 있습니다.

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

이중 setDriverClass() 메서드는 Class 타입의 오브젝트를 넣었지만 다른 빈 오브젝트를 DI 방식으로 가져와서 넣는 것은 아닙니다.

이렇게 사용하는 수정자 또한, <property>를 사용해서 값을 주입할 수 있습니다. 성격은 다르지만 일종의 DI라고도 볼 수 있습니다.

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.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/springbook"/>
<property name="username" value="spring"/>
<property name="password" value="book"/>

다음과 같은 ref 대신 value 를 써서 나타낼 수 있습니다.


value 값의 자동 변환

그런데 우리는 이상한 점을 볼 수 잇습니다. url, username, password 들은 모두 스트링 타입이기 때문에 텍스트로 정의되는 value 속성 값을 사용하는 것은 문제가 되지 않습니다.

하지만 현재 driverClass 를 보면 java.lang.Class 타입인것을 볼 수 있습니다.

텍스트로 정의되어 있다고 해쓴ㄴ데 별다른 타입정보가 없이 그냥 클래스이름이 텍스트형태로 현재 value에 들어가 있습니다.

직관적으로만 보면 현재 다음과 같은 상황입니다.

Class driverClass = "com.mysql.jdbc.Driver";

이러면 본래 스트링 값을 Class에 넣는것 이기 때문에 컴파일조차 안되기 마련입니다.

하지만 우리는 컴파일을 하면 아무런 문제 없이 성공하는 것을 볼 수 있습니다. 이러한 이유는

스프링이 프로퍼티의 값을 수정자 메서드의 파라미터 타입을 참고로 해서 적절한 형태로 변환해줍니다.

"com.mysql.jdbc.Driver" 라는 텍스트 값을 오브젝트로 자동 변환을 시켜줍니다. 결론적으로는 다음과 같은 코드로 스프링이 자동적으로 변환작업을 해주는 것을 알 수 있습니다.

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

이런식으로 스프링은 value에 지정한 텍스트 값을 적절한 자바 타입으로 변환해주기도 합니다.

결론적으로 우리는 모두 적용하면 다음과 같은 코드를 얻을 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframwork.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframwork.org/schema/beans
            http://www.springframwork.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dataSource"
            class="org.springframwork.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.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>

제어의 역전(IOC)

오브젝트 팩토리

현재까지 우리는 DAO를 깜끔한 구조로 리팩토링 하였습니다.

하지만 UserDaoTest는 그냥 넘겼습니다. 현재는 UserDaoTest에서 Connection을 할 때 어떤 클래스를 사용하는지 정의를 하면서 UserDao가 담당하던 기능을 분리하였습니다.

그러나 원래 UserDaoTest를 만든 목적은 UserDao의 기능이 잘 동작하는지를 테스트하기 위해서 만든 것입니다. 그렇긱 때문에 이러한 관심사가 겹치는 것을 분리해보도록 하겠습니다.


팩토리 (Config)

분리시킬 기능을 당담하는 클래스를 만듭니다.

이 클래스는 객체의 생성방법을 결정하고 그렇게 만들어진 Object를 돌려주는 역할을 합니다.

이런것을 우리는 팩토리라고 부릅니다.

이제 DaoFactory라는 클랙스를 생성하고 UserDaoConnectionMaker 관련 생성작업을 옮기도록 합시다.

public class DaoFactory{
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new AConnectionMaker(); // A회사것을 쓰겠다.
        UserDao userDao = new UserDao(connectionMaker);
        return UserDao;
    }
}

이제 DaoFactory 클래스의 userDao() 라는 메서드를 호출한다면 A회사의 AConnectionMaker를 사용해 DB 커넥션을 가져오도록 설계된 UserDao를 반환하게 됩니다.

이제 그러면 UserDaoTest 에서 객체를 설정해주면서 생성해줄 필요가없기 때문에 다음과 같이 고칩니다.

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

설계도로서의 팩토리

결론적으로 이렇게 분리를하게 된다면 DaoFactory 라는 클래스에서 Client가 요청을 한다면

알아서 내가 어떤 것을 Connection으로 사용할지 생성을하고 UserDao에서 그 클래스를 사용하도록 해주게 된다.

이제 회사에 UserDao를 납품할 때 DaoFactory도 함께 납품을 해서 클래스 변경이 일어나면 DaoFactory의 코드만 수정하면 됩니다.


오브젝트 팩토리의 활용

만약, DaoFactory 에서 UserDao 라는 Dao 클래스가 아닌 다른 클래스를 넣는다면 어떻게 될까? 이런식으로 DAO가 늘어나게된다면 Parameter로 넘길 때 ConnectionMaker 의 인스턴스를 생성해서 넘기는 코드가 중복되게 됩니다.

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

    public AccountDao AccountDao(){
            return new AccountDao(new AConnectionMaker());
    }

    public MessageDao messageDao(){
            return new MessageDao(new AConnectionMaker());
    }
}

이런식으로 Parameter에 사용한 ConnectionMaker 를 생성해주는 코드가 중복이 됩니다.

이를 해결하는 제일 좋은 방법은 또 부리하는 것 입니다.

ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메소드로 뽑아내는 것이 좋은 결정입니다.

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

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

제어의 역전(IOC)가 우리가 배우고자 하는 목표입니다.

제어의 역전은 간단하게 프로그램의 제어 흐름 구조가 뒤바뀌는 것을 말합니다.

보통으로는 main() 메서드에서 다음과 같은 흐름을 가집니다.

  1. 시작되는 지점에서 다음에 사용할 오브젝트를 결정한다.
  2. 결정한 오브젝트를 생성한다.
  3. 만들어진 오브젝트에 있는 메서드를 호출한다.
  4. 그 오브젝트 메서드 안에서 다음에 사용할 것을 결정한다.
  5. 이 과정을 반복한다.

그래서 우리가 초기에 만든 UserDao의 흐름을 살펴보면 다음과 같습니다.

  1. UserDao 클래스의 오브젝트를 직접 생성합니다.
  2. 만들어진 오브젝트의 메서드를 사용합니다.
  3. UserDao는 자신이 사용할 ConnectionMaker의 구현클래스를 자기가 결정합니다.
  4. 그 오브젝트를 필요한 시점에 생성합니다.
  5. 각 메서드가 이를 사용합니다.

우리는 이러한 제어 흐름의 개념을 거꾸로 뒤집어서 사용할 것 입니다.

이를 사용하면 이제 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않습니다.

왜냐하면 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문입니다.

현재 우리는 DaoFactory를 만들어서 제어의 역전(IOC)를 도입하였습니다.

원래는 ConnectionMaker의 구현클래스의 결정은 UserDao에서 하였지만 현재는 이런 구현클래스의 결정에 대해서 DaoFactory에 넘겼습니다.

넘김으로써 UserDao는 능동적이 아닌 DaoFactory가 주는 것을 사용하는 수동적인 존재가 되었습니다.

우리는 현재 IOC프레임워크를 사용하지 않고 DaoFactory를 이용해서 IoC를 구현한 셈입니다.

물론 DaoFactory 같이 Ioc를 만들어도 되지만 프레임워크의 도움을 받는 편이 유리하기 때문에 프레임워크를 사용할 예정입니다.


스프링의 IoC

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

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

우리는 DaoFacotory를 스프링에서 사용이 가능하도록 변신시킬 것 입니다.

일단 들어가기전에 빈(Bean) 이라는 중요한 개념이 존재합니다.

  • (Bean)
    • 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 말합니다.
    • 자바 빈 또는 엔터프라이즈 자바빈에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말합니다.
    • 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말입니다.

또한, 우리는 이러한 빈(Bean)의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 합니다.

하지만 이러한 빈 팩토리보다 Application context를 자주 사용하게 됩니다. 이것은 IoC 방식을 따라 만들어진 일종의 빈 팩토리 입니다.


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

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

우리는 @Configuration 이라는 애노테이션을 사용해서 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식 시킵니다.

그리고 @Bean 이라는 애노테이션을 오브젝트를 만들어주는 메서드를 붙여줍니다.

@Configuration // 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
public class DaoFactory{

    @Bean // 오브젝트 생성을 담당하는 IoC용 메서드라는 표시
    public UserDao userDao(){
        return new UserDao(connecitonMaker());
    }

    @Bean
    public ConnectionMakerr connectionMaker(){
        return new AConnectionMaker();
    }
}

이제 설정을 완료했으니 DaoFactory 를 설정정보로 사용하는 Application Context를 만들어 볼 것입니다.

@Configuration 이 붙은 자바 코드를 설정정보로 사용하기 위해서는 AnnocationConfigApplicationContext 를 이용하면 됩니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
            SQLException{

                ApplicationContext context = 
                    new AnnotationCongifApplicationContext(DaoFactory.class);

                UserDao dao = context.getBean("userDao", UserDao.class);
        }
}

우리는 ApplicationContextgetBean() 이라는 메서드를 통해서 UserDao의 오브젝트를 가져올 수 있습니다.

이런식으로 @Bean 이라는 애노테이션을 붙인 매서드가 바로 빈의 이름이 됩니다.

그래서 이름에 따라서 getBean("메서드이름", 사용클래스.class)로 가져올 수 있습니다.


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

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

스프링에서는 ApplicationContext라는 인터페이스를 통해서 애플리케이션 컨텍스트를 구현합니다.

이러한 ApplicationContextBeanFactory 인터페이스를 상속합니다. 그래서 다음과 같이 불립니다.

  • IoC 컨테이너
  • 스프링 컨테이너
  • 빈 팩토리
  • 스프링

이전에 구현한 DaoFactoryuserDao 를 비롯한 DAO를 생성하고 DB 생성 오브젝트와 관계를 맺어주는 제한적인 역할을 합니다.

하지만 ApplicationContext를 사용하면 DaoFacotory와 달리 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 존재하지 않습니다. 이러한 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻습니다.

그래서 다음과 같은 순서로 작동하게 됩니다.

  1. Client가 UserDao 를 요청합니다.
  2. ApplicationContext 에서 getBean() 메서드를 통해서 생성을 요청합니다.
  3. @Configuration 이 붙은 DaoFactory 에서 @Bean 으로 등록된 UserDao() 메서드를 호출해서 객체를 생성합니다.
  4. 생성한 객체를 Client에게 넘겨줍니다.

이렇게 구현을한다면 다음과 같은 장점이 생깁니다.

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
    • 애플리케이션 컨텍스트를 사용한다면 오브젝트 팩토리가 아무리 많아져도 이를 알아야하거나 직접 사용할 필요가 없습니다. 또한, XML을 사용해서 컨텍스트가 사용할 IoC 설정정보도 만들 수 있습니다.
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줍니다.
    • 애플리케이션 컨텍스트가 단지 오브젝트 생성과 다른 오브젝와의 관계설정을 해준다고 생각할 수도 있다.
    • 오브젝트가 만들어지는 방식 오브젝트에 대한 후처리 등등 다양한 기능을 제공하기도 합니다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공합니다.
    • 애플리케이션의 getBean() 메서드를 보면 빈의 이름을 이용해서 빈을 찾아 줄 수 있습니다. 또한, 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수도 있습니다.

스프링 IoC의 용어 정리

빈(Bean)

빈은 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻입니다.

이를 관리 오브젝트(Managed object) 라고도 부릅니다. 하지만 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 아닌 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고합니다.


빈 팩토리(Bean Factory)

빈 팩토리는 스프링의 IoC를 담당하는 핵심 컨테이너를 가리킵니다

빈 팩토리는 다음과 같은 기능을 합니다.

  • 빈을 등록한다.
  • 빈을 생성한다.
  • 빈을 조회후 반환한다.

보통은 빈 팩토리를 바로 사용하지 않고 상속받은 Application Context를 이용합니다.


애플리케이션 컨텍스트(Application Context)

애플리케이션 컨텍스트는 빈 팩토리를 확장한 IoC컨테이너 입니다.

빈 팩토리와 기본적인 기능은 비슷하지만 추가적인 기능들이 확장되어 있습니다.


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

스프링의 설정정보는 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해서 사용하는 메타정보를 말합니다.

IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용됩니다.

이를 Blueprints(청사진)이라고도 합니다.


컨테이너 또는 IoC 컨테이너

애플리케이션 컨텍스트나 빈 팩토리를 말합니다.

이는 애플리케이션 컨텍스트보다는 추상적인 표현입니다.


스프링 프레임워크

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


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

우리는 오브젝트 팩토리를 만든것과 애플리케이션 컨텍스트를 사용한것 2가지를 만들었습니다.

이 둘은 중요한 차이점이 존재합니다.

만약, DaoFactoryuserDao() 메서드를 2번 호출하면 2개의 오브젝트는 같을까? 라는것 입니다.

다음과 같은 코드로 오브젝트를 확인해봅시다.

// 오브젝트 팩토리 방식
DaoFactory factory = new DaoFactory();
UserDao dao1 = factory.userDao();
UserDao dao2 = factory.userDao();

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

확인해보면 출력값이 서로 다른것을 확인할 수 있습니다.

이를 통해서 userDao() 를 매번 호출하면 계속해서 새로운 오브젝트가 생성된다는 것을 알 수 있습니다.

하지만 만약, ApplicationContext 를 사용한 것에서는 어떤지 확인해봅시다.

// ApplicationContext 사용
ApplicationContext = context = new AnnotationConfigApplication(DaoFactory.class)

UserDao dao3 = context.getBean("userDao", UserDao.class);
UserDao dao4 = context.getBean("userDao", UserDao.class);

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

만약 ApplicationContext 를 실행하면 출력값이 서로 같은 것을 볼 수 있습니다.

결론적으로 여러개의 오브젝트를 생성하려고해도 매번 동일한 오브젝트를 돌려준다는 것 입니다.


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

애플리케이션 컨텍스트는 IoC 컨테이너로 역할을 하지만 동시에 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(Singleton Registry)입니다.

스프링에서는 별다른 설정을 하지 않는 이상 내부에서 생성하는 빈 오브텍트를 싱글톤으로 만듭니다.


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

만약에 우리가 사용하는 UserDao 가 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트가 새로만들어진다고 가정한다면

우리는 계속해서 요청에 따라서 수만개의 오브젝트를 생성하고 지우고를 반복해야합니다.

이러면 서버에 많은 부하가 걸리게 됩니다. 그래서 우리는 싱글톤을 사용하게 됩니다.

싱글톤 패턴은 애플리케이션 안에 제한된 수, 대개 한 개의 오브젝트만 만들어서 사용하는 것 입니다.

이러한 싱글톤 패턴은 장점만 있을것 같지만 안티패턴으로 피해야하는 패턴으로 불리기도 합니다.


싱글톤 패턴의 한계

싱글톤을 구현하기 위해서는 다음과 같은 방식으로 구현을 합니다.

  • 클래스 밖에서는 오브젝트를 생성하지 못하게 생성자를 private로 만듭니다.
  • 생성도니 싱글톤 오브젝트를 저장하기 위해서 자기와 같은 타입의 static 필드를 정의합니다.
  • 스태틱 팩토리의 메서드인 getInstance()를 만들고 메서드가 오출되는 시점에서 한번의 오브젝트가 만들어지게 합니다.
  • 한번의 오브젝트가 만들어지고 난 뒤는 getInstance()를 통해서 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨줍니다.

이렇게 만든 UserDao의 예제를 살펴봅시다.

public class UserDao{
    private static UserDao INSTANCE;

    public static synchronized UserDao getInstacne(){
        if(INSTANCE == null) INSTANCE = new UserDao(???);
        return INSTANCE;
    }

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

이런식으로 만들어진 싱글톤 패턴은 코드가 상당히 지저분해지고 private로 바귄 생성자는 외부에서 호출을 할 수 없기 때문에 DaoFactory 에서 UserDao 를 생성하며 ConnectionMaker 오브젝트를 넣어지는것이 더이상 안된다.

이러한 싱글톤 패턴구현 방식에는 다음과 같은 문제가 존재합니다.

  • private 생성자를 가져 상속할 수 없다.
    • private 를 사용해서 생성자를 제한한다면 객체지향의 장점인 상속과 이를 이용한 다형성을 사용할 수없다. static과 메서드를 사용하는 것도 객체지향의 장점을 사용할 수 없게 한다.
  • 싱글톤은 테스트하기가 힘들다
    • 싱글톤은은 만들어지는 방식이 제한적이여서 테스트에서 사용될 때 목 오브젝트 등으로 대체하기가 힘들어서 테스트하기가 힘듭니다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    • 서버에서 클래스 로더를 어떻게 구성하고 있냐에 따라서 싱글톤 클래스여도 하나이상의 오브젝트가 만들어질 수 있습니다.
  • 싱글톤 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
    • 싱글톤은 static 메서드를 이용해서 애플리케이션 어디서든지 사용될 수 있습니다. 이러한 방식은 객체지향 프로그래밍에서 권장되지 않는 프로그램 모델입니다.

싱글톤 레지스트리

자바의 기본적인 싱글톤 패턴 구현 방식은 여러가지 단점이 존재합니다.

그래서 스프링은 싱글톤 레지스트리(Singleton Registry)를 사용해서 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공합니다.

이러한 싱글톤 레지스트리는 싱글톤을 생성하고, 관리하고, 공급하는 싱글톤 관리 컨테이너라고도 볼 수 있습니다.

기존에 사용했던 privatestatic을 사용해서 구현한 클래스가 아닌 기본 클래스에서 싱글톤으로 관리를 해줘서 단점을 보완합니다.

그래서 스프링이 지지하는 객체지향적인 설계 방식과 디자인 패턴등을 적용하는데 아무런 제약이 사라집니다.

이제 주의할점들을 살펴봅시다.


싱글톤 오브젝트와 상태

싱글톤은 멀티스레드 환경에서 여러 스레드가 동시에 접근해서 사용할 수 있습니다.

그렇기 때문에 더욱 상태 관리에 주의를 기울여야합니다.

싱글톤은 우선 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되면 상태정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야합니다.

이런것들을 파라미터나 로컬 변수, 리턴 값등을 이용해서 상태정보를 다룹니다.

//인스턴스 변수를 사용하도록한 UserDao
public class UserDao{
    private ConnetcionMaker 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;
    }
}

이 코드는 기존가 다른점으로는 원래는 ConnectionUser는 로컬 변수로 각 메서드에서 선언해서 사용했는데 이를 클래스의 인스턴스 필드로 선언했다는 것 입니다.

이런식으로 로컬이 아닌 인스턴스 변수로 정의해서 사용한다면 심각한 문제가 발생합니다.

A가 접근을 할때 B도 동시에 접근을한다면 A의 정보를 추가해야하는데 B의 정보를 잘 못 추가할 수도 있기 때문입니다.

하지만 현재 기존코드에서도 connectionMaker는 인스턴스 변수를 사용을 했습니다.

하지만, 이것은 문제를 일은키지 않습니다. 왜냐하면 읽기전용 정보이기 때문입니다.

또한, 이 변수는 @Bean을 붙여서 싱글톤으로 관리되어 있습니다.

이런식으로 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도일 경우는 인스턴스 변수를 사용해도 무관합니다.

또한, 읽기전용 속성을 가진 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋습니다.

그렇지만 단순한 읽기전용이라면 static final 이나 final 을 사용하는 것이 더욱 효율적 입니다.


스프링 빈의 스코프

스프링이 관리하는 오브젝트인, 빈이 생성되고, 적용되는 범위는 10장에서 배우게 됩니다.

보통은 스프링 빈의 기본 스코프는 싱글톤입니다.

이러한 싱글톤 스코프는 컨테이너 내에 한 개의 오브젝트만 만들어져서 강제로 제거하지 않는 이상 스프링 컨테이너가 존재하는 동안 계속 유지됩니다.

오브젝트와 의존관계

DAO

DAO란 (Data Access Object)라고 하고 이것은 데이터를 조회하거나 조작하는 기능을 전담하게 하는 객체를 말합니다.

보통 DAO를 사용하기 위해서는 DTO라는 (Data Transfer Object)를 만들게 됩니다.

정말 간단하게 DTO는 사용자가 어떤 데이터를 받는지 객체에 저장하는 것을 말합니다.

이들은 Getter/Setter로 묶여있어 데이터를 접근하거나 반환받을 수 있습니다.

우선 User라는 DTO를 만들도록 하겠습니다.

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

}

이러한 DTO는 그냥 현재 id, name, password로 구현이 된 상태입니다.

이런식으로 Getter/Setter만 있는 것이 DTO입니다

이러한 DTO에 데이터 베이스에서 꺼낸 정보가 들어가게 됩니다.

이러한 DTO에 꺼내고 저장하는 역할을 하는것이 바로 DAO입니다.

일단 데이터 베이스를 구축하기 위해서 테이블을 하나 만듭니다

필드와 정보는 다음과 같습니다

User 테이블

다음처럼 테이블을 구성한다. 이때 처음 배운다는 가정하에 Primary Key를 모를 수도 있다고 생각합니다.

이러한 Primary Key는 고유의 값이라고 생각하면 편합니다.

Primary Key라고 설정을 하면 아무도 중복의 값을 가질 수 없습니다

이러한 테이블을 생성하기 위한 SQL 문법은 다음과 같습니다

create table users(
    id varchar(10) primary key,
    name varchar(20) not null,
    password varchar(20) not null
)

이렇게 입력을하고 실행을 하면 DB가 생성이 됩니다.

이제 UserDao를 생성해봅시다!!

이름은 당연히 UserDao라는 이름으로 클래스를 생성할 것 입니다.

또한, 우선적으로 다음과 같은 메서드를 생성할 것 입니다

  • 새로운 사용자를 생성하는 기능 (add 라는 메서드)
  • 아이디를 가지고 사용자의 정보를 가져오는 기능 (get 이라는 메서드)

이러한 기능을 JDBC를 이용해서 진행할 예정입니다

JDBC의 작업순서

JDBC를 작업하기 위해서는 다음과 같은 순서를 통해서 작업을 할 수 있습니다.

우리는 스프링을 배우고 있고 JDBC도 물론 배우겠지만 중점적으로 할 것은 아니기 때문에 일단 이런과정을 통해서 사용이 되는구나 라고 알아두면 좋을 것 같습니다

  • DB 연결을 하기 위해서 Connection이라는 객체를 만들어 연결한다
  • SQL을 담은 Statement를 만든다
  • 만들 Statement를 실행한다
  • 조회해서 SQL 쿼리의 실행결과들을 ResultSet으로 받아서 User라는 미리만든 DTO에 넣어준다.
  • 작업이 끝난 후는 이 때 생성한 Statement, ResultSet, Connection 들을 작업을 마친 후에 반드시 닫아주어야 합니다
  • JDBC API가 만들어내는 예외는 직접 처리하거나 메소드에 throws를 넘겨서 메소드 밖으로 던지게 합니다.

그래서 생성하면 다음과 같은 코드가 나옵니다

import java.sql.*;
//...//
public class UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
                "jdbc:mysql:// 내 쿼리주소","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 void get(String id) throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
                "jdbc:mysql:// 내 쿼리주소","spring","book"
        );

        PreparedStatement ps = c.prepareStatement(
                "select * from users where id = ?"
        );
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}

여기서 Connection과 Class를 건너뛰고 설명하자면

  • PreparedStatement는 내가 입력하고자하는 SQL문이 들어가는 곳이다.
  • ps.setString(1,id)이런식의 메서드 활용이 많은데 이 뜻은 SQL문의 첫 번째 ?의 값을 id라고 설정하는 것이다.
  • ps.executeQuery()를 사용해서 쿼리를 실행해서 ResultSet에 저장한다.

그래서 이러한 코드를 짜게 되었습니다. 이제 테스트코드를 사용해서 테스트를 한번 해봅시다😊

테스트 코드는 main()메서드에 생성을해서 add()get()메서드를 검증 해보도록 합니다

public static void main(String[] args) throws ClassNotFoundException, SQLException{
    UserDao dao = new UserDao();

    User user = new User();
    user.setId("haecahn");
    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() + "제대로 나왔습니다");

}

이런식으로 확인하면 제대로 값이 나오는 것을 볼 수 있다.

하지만 이러한 코드는 스프링의 개발을 한 원칙으로 살펴보면 무척이나 무성의한 코드로 볼 수 있다.

이러한 것을 Spring 스타일로 개선하는 작업을 이 책에서는 진행하려고 합니다.

DAO의 분리

관심사의 분리

객체지향에서는 모든것이 변합니다. 비지니스에서 A를 개발하라고 했다가 다시 B로 돌아와라 라고 할 수도 있고 이러한 오브젝트에 대한 설계와 구현한 코드는 계속 게속 바뀝니다.

그래서 우리가 중점적으로 생각해야할 것은 미래를 위해서 다시 잘 사용하고 유지보수가 쉽게 설계하는 것을 목표로 하는 것이 개발자들이 가장 염두해야할 사항입니다.

이런 것이 너무 미래라고 생각할지도 모르지만 당장 코드를 짜고나서 한 시간 뒤에도 이런 변화는 찾아올 수 있기 때문에 객체지향 기술을 모두 잘 활용해서 이를 자유롭고 편리하게 변경, 발전, 확장시킬수 있어야합니다

이러한 것을 보다 효율적으로 하기 위해서 관심사의 분리라는 프로그래밍의 기초 개념을 이용합니다.

이러한 관심사의 분리라는 것은 관심사가 같은 것 끼리는 하나의 객체 안으로

하지만, 관심사가 다르다면 서로 영향을 주지 않기 위해서 분리하는 것 입니다.

물론 코드 작성은 그냥 관심사를 상관없이 대충 비슷한것 끼리 짜는 것이 편하지만

이를 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어주는 것이 좋습니다.

UserDao의 관심사항

우리가 아까전에 작성한 UserDao를 다시 돌아보면서 관심사항에 대해서 정리를 해봅시다.

총 3가지로 나뉠 수 있습니다

  1. DB와의 연결을 위해서 우리는 Connection을 연결하고 가져왔습니다. 이러한 것을 DB연결에 대한 관심이라고 하나로 정의할 수 있습니다.
  2. DB에 보낼 SQL Satement를 만들고 실행하는 것으로 볼 수 있습니다.
  3. 마지막으로 사용한 Statement, ReusltSet, Connection들을 닫아주는 일을 하는 것이 있습니다.

첫 번째로 DB연결에 대한 관심사를 살펴보겠습니다.

현재는 각 메소드마다 동일한 Connetion정보인데 이를 여러번 사용하는 것을 볼 수 있습니다.

우리는 앞으로 수천 개의 DAO코드를 만들수도 있는데 이러한 것이 계속해서 중복으로 있다면 충분히 성능에 영향을 끼칠 것이라고 생각합니다.

만약 진짜 Connection에 대한 정보가 바뀐다면 모든 DAO에 있는 수천개의 Connection의 코드를 바꿔주어야합니다. 이러한 코드를 방지하기 위해서 메소드로 추출할 것 입니다.

중복 코드의 메소드 추출

이러한 중복된 DB 연결 코드를 getConnection이라는 메서드를 만들어서 수정할 것입니다.

동일하게 getConnection이라는 메서드에는 연결하는 코드가 존재합니다.

이제 연결을 할때는 그저 하나의 메서드만 사용하면 됩니다.

private Connection getConnection() throws ClassNotFoundException, SQLException{
        Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection(
            "jdbc:mysql:// 내 쿼리주소","spring","book"
    );

        return c;
} 

이제 DAO에 대한 메서드가 엄청 많아진다고 해도 Connection을 할 때 접속 Address가 바뀌거나 하는 일이 생기면 getConnection 코드만 고치면 됩니다.

이러한 코드는 다른 코드에 영향을 주지도 않고 관심 내용이 독립적이기 때문에 수정도 간단해집니다.

변경 사항에 대한 검증

이러한 코드가 당연하게도 이전 코드를 똑같이 가져다 썼기 때문에 괜찮다 라고 생각할 수 있지만

항상 검증은 필요하다. 어떠한 문제점이 발견될 수 도 있기 때문이다.

DB 커넥션 만들기의 독립

현재 메서드를 분리해서 Connection을 할 수 있도록 만들었습니다.

하지만 이렇게 만든 기술을 A회사와 B회사가 구매를 주문해서 사용한다고 합니다.

그런데 A회사랑 B회사가 각기 다른종류의 DB를 사용하고있고 DB 커넥션을 가져오는데 독자적인 방법을 사용한다고 합니다

또한, DB커넥션을 UserDao라는 우리가 만들 프로그램을 구입하고 나서도 계속 변경될 가능성이 있다고 합니다.

이러한 상황에서는 우리가 UserDao에 대한 소스코드를 공개하고나서 getConnection을 수정해서 사용하라고 하면 편하지만

이러한 UserDao는 우리만의 독자적인 기술이고 이러한 코드를 공개하고 싶지 않다 라고 했을 경우는 어떻게 해야할까요?

정말 난감한 상황입니다. 공개를 하고 싶다고 해서 소스코드를 줘버리면 다른 회사가 우리의 기술을 모두 익혀버려서 주문이 안들어올 수 도 있고 이러한 과정에서는 어떠한 방법을 사용해야할까요?

상속을 통한 확장

이런경우는 우리가 사용하고 있는 UserDao 의 코드를 한 단계 더 분리하면 해결할 수 있습니다.

getConnection을 추상 메서들로 만들어 버리면 A라는 회사와 B라는 회사에서 알아서 필요한 부분을 구현할 수 있기 때문에 나머지 UserDao라는 클래스의 코드는 그냥 getConneciton 메서드를 따로 수정할 필요도 없고 코드를 수정할 필요도 없어집니다.

우리가 생성한 UserDao라는 추상 클래스를 통해서 A회사와 B회사에 판매를 하면 이제 각각 회사에서 자식클래스를 만들어서 getConnection을 원하는 형태로 만들 수 있습니다.

그렇게 된다면 자유롭게 회사 입맛대로 getConnection이라는 메서드를 자유롭게 구현할 수 있습니다.

또한, 우리 회사는 UserDao에 대한 구현 기능을 넘기지 않아도 가능합니다.

// UserDao에서 추상 메서드로 만든 getConnection
public abstract Connection getConnection() throws ClassNotFoundException, SQLException;

// A회사의 새로만든 클래스에서 구현한 getConnection()
public class AUserDao extends UserDao { // UserDao를 상속받음
    public Connection getConnection() throws ClassNotFoundException,
                SQLException{
                // A회사의 코드
            }
}

// B회사의 새로만든 클래스에서 구현한 getConnection()
public class BUserDao extends UserDao { // UserDao를 상속받음
    public Connection getConnection() throws ClassNotFoundException,
                SQLException{
                // B회사의 코드
            }
}

잘 살펴보면 상속을 받아서 추상 메서드인 getConnection을 구현함으로서 클레스 레벨로 AUserDaoBUserDao가 독립적으로 분리되는 것을 볼 수 있습니다.

이제 UserDao의 코드를 건들일 필요 없이 각 회사의 클래스를 만들어서 거기서 getConnection이라는 추성 메서드만 생성하면 됩니다.

이런식으로 UserDao처럼 슈퍼 클래스에 기본적인 로직의 흐름을 만들고

그 기능의 일부를 추상 메서드, 오버라이딩이 가능한 protected메서드로 만들어서

서브 클래스에서 메서드를 필요에 맞게 구현하는 방법을 템플릿 메서드 패턴이라고 합니다.

이러한 템플릿 메서드 패턴이라는 디자인 패턴은 스프링에서 자주사용하는 패턴입니다

다른 관점에서 보면 UserDao에서의 getConnection이라는 메서드는 Connection타입의 객체를 생성한다는 기능을 정의 해놓은 추상 메서드라고 할 수 있습니다.

그래서 UserDao의 서브 클래스의 getConnection 메서드는 어떠한 Connection 클래스의 객체를 어떻게 생성할 것인지 결정하는 방법이라고도 볼 수 있습니다.

이러한 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을

팩토리 메서드 패턴이라고도 부릅니다.

이렇게 변화한 코드도 장점만 있는 것은 아닙니다. 이제 슈퍼클래스를 변화한다면 서브클래스를 수정하거나 다시 개발해야 할 수도 있습니다.

또한, 확장된 기능인 DB Connection을 생성하는 코드는 오직 UserDao에서 만 사용 가능하고 다른 DAO 클래스에서는 사용할 수 없다는 단점이 생깁니다.

DAO의 확장

모든 오브젝트는 변합니다. 하지만 반드시 오브젝트가 다 동일한 방식으로 변하는 것은 아닙니다.

변화의 성격은 여러가지 특징에 따라 달라집니다. ex) JDBC API를 사용하는가?, JPA를 사용하는가?

현재까지는 추상 클래스를 만들고 이를 상속한 서브클래스에서 변화가 필요한 부분을 바꾸어 쓸수 있도록 변화시켰습니다.

하지만, 여러가지 단점도 많은 것을 알게 되었습니다. 상속이라는 방법은 좋았지만 그럼에도 불구하고 단점이 존재합니다.

클래스의 분리

그래서 다른 방식으로 클래스를 분리하면서 두 개으 관심사를 독립시키고 동시에 손쉽게 확장할 수 있는 방법을 알아 봅시다.

현재까지는 다음과 같이 분리를 진행하였습니다.

  1. 독립된 메소드를 사용해서 분리했습니다. → getConnection을 만들기
  2. 상 하위 클래스로 만들어서 분리 했습니다. → 추상 메서드 사용

이제는 상속관계도 아닌 완전히 독립적인 클래스로 만들어보려고 합니다.

DB 커넥션과 관련된 부분을 서브클래스로 하는 것이 아닌 새로운 클래스를 만들어서

UserDao가 이것을 이용하게 만들면 됩니다.

우리는 SimpleConnectionMaker라는 클래스를 만들고 getConnection 기능을 그안에 넣을 것 입니다.

그러면 UserDao 에서는 SimpleConnectionMakernew를 통해서 객체를 만들어서 각각의 메서드에서 사용할 수 있게 합니다.

이 말은 꼭 add()get()에서 SimpleConnectionMakernew를 통해 객체를 만들라는 소리가 아니라

한번만 SimpleConnectionMaker를 만ㄷ르어서 저장해두고 사용할 것 입니다.

public class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker; // 한 번만 생성자를 통해서 만든다.

    public UserDao(){
        simpleConnectionMaker = new SimpleConnectionMaker(); // 객체 생성 -> 생성자라 한번만 생성
    }
}

이렇게 생성자를 통해서 하나만 만들면 다른 메서드에서 어떻게 사용하는지 확인해보자.

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

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

그리고 이제 DB 커넥션 생성 기능을 독립시킨 SimpleConnectionMaker는 다음과 같이 만들 수 있다.

makeNewConnection()이라는 메서드를 통해서 Connect를 해준다. 이제 상속을 사용하지 않으니 더이상 getConnection() 기능을 하는 makeNewConnection()abstract가 아니여도 된다.

public class SimpleConnectionMaker{
        public Connection makeNewConnection() throws ClassNotFoundException,
        SQLException{
                Class.forName("com.mysql.jdbc.Driver");
                Connection c = DriverManager.getConnection(
                        "SQL 주소"
                )
                return c;
        }
}

이를 통해서 Connection을 반환해준다.

이렇게 해서 분리는 하였지만 다시 문제가 발생하였습니다

이제 UserDao의 코드는 SimpleConnectionMaker라는 특정 클래스에 종속이 되기 때문입니다.

그렇기 때문에 상속을 사용했을 때 처럼 UserDao의 코드 수정없이 A회사랑 B회사가 Connection을 생성하는 기능을 변경할 방법이 사라졌습니다.

우리가 해결하야하는 문제는 그래서 다음과 같습니다.

  1. SimpleConnectionMaker 의 메서드의 문제
  2. 현재 makeNewConnection()으로 DB 커넥션을 가져오게 만들었습니다 이것으로 각 메서드에 Connection을 할당해줍니다. 만약, 변경이 있으면 add(), get() 뿐만아니라 많은 메서드가 생성이 되었으면 이를 일일히 변경하는 것은 무척이나 비효율적인 문제입니다.
  3. DB 커넥션을 제공하는 클래스가 어떤 것인지 UserDao가 구체적으로 알고 있어야한다는 것입니다.왜냐하면 SimpleConnetionMaker라는 클래스 타입의 인스턴스 변수까지 정의를 해놨기 때문에 UserDao자체를 다시 수정해야합니다.그래서 다른회사가 현재 우리의 UserDao 기술을 구매하려고하면 자유롭게 확장하기 때문에 상속을 안쓴만 못합니다.
  4. 현재 근본적인 원인으로는 UserDao가 DB 커넥션을 가져오는 클래스에 대해서 너무 많이 알고 있기 때문에 커넥션을 가져올 때마다 우리는 메서드가 이름이 뭐고 어떤 클래스가 사용되는지 모든것이 구체적인 방법에 종속됩니다.
  5. 만약 다른회사에서 다른 클래스를 구현하려고 한다면 불가피하게 UserDao를 수정을 해야 합니다.

인터페이스의 도입

이를 해결하기 위해서는 현재 UserDaoSimpleConnectionMaker가 너무 긴밀하게 연결되어 있기 때문에 그렇습니다.

이러한 둘이 긴밀하게 연결을 끊기 위해서 중간에 추상적인 느슨한 연결고리를 만들어주는 것이 좋습니다.

이러한 것을 인터페이스를 사용해서 공통적인 성격을 뽑아 내서 이를 따로 분리해내면 해결할 수 있을 것 같습니다.

우리가 인터페이스를 이용해서 접근하게 한다면 만약 SimpleConnecitonMaker가 아닌 HaechanConnectionMaker를 바꿔서 사용해도 신경쓸일이 없습니다.

왜냐하면 인터페이스는 변하지 않기 때문입니다.

이제 우리는 다음과 같이 구현할 것입니다.

UserDao에서는 ConnectionMaker라는 인터페이스를 사용합니다

ConnectionMakermakeConnection()이라는 메서드를 가지고

이러한 makeConnection()이라는 메서드는 각 회사의 사정에 맞춰서 구현된 클래스에 의해서 직접 구현하면 됩니다.

이제 ConnectionMaker 인터페이스를 정의해 봅시다. 이때 DB 커넥션을 가져오는 메서드 이름은 makeConnection()이라고 정의 할 것입니다.

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

이제 납품을 할때 다음과 같이 Interface를 사용해서 이제 UserDao와 함께 ConnectionMaker라는 인터페이스를 전달하면 각 회사에 맞게 인터페이스를 구현한 클래스를 만들고 makeConnection()메서드를 작성하면 됩니다.

그러면 인터페이스를 사용한 UserDao는 어떻게 변했냐면 다음과 같습니다.

public class UserDao{
    private ConnecitonMaker connectionMaker;

    public UserDao(){
        connectionMaker = new AConnectionMaker();
    }

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

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

변하긴 했는데 여기서도 connectionMaker = new AConnectionMaker()에 대해서는

아직도 AConnectionMaker()의 클래스의 생성자를 호출해서 객체를 생성하는 코드가 남아있다.

다른 곳에서는 이러한 정보들을 모두 지웠는데 결론적으로는 AconnectionMaker()라는 클래스 이름을 넣어서 객체를 만들지 않으면 사용을 할 수가 없다.

결론적으로 모두 지우지 못해서 원점으로 돌아오게 되었다.

관계설정 책임의 분리

현재까지의 진행상황은 UserDaoConnectionMaker라는 두 개의 관심을 인터페이스를 쓰면서 분리를 완료했습니다

하지만, 어찌해도 UserDao는 어떤 Connection을 사용할지 생성자에 명시가 되어있는 것을 볼 수 있습니다.

결론적으로는 Connection을 다른 회사에서 사용하면 UserDao는 아직까지도 무조건 변경을 해야합니다.

이렇게 된 이유는 UserDao안에 우리가 아직 분리하지 않은, 또 다른 관심사항이 존재하기 때문입니다.

현재 UserDao안에는 어떤 ConnectionMaker구현 클래스를 사용할지 결정하는 new AConnectionMaker()라는 코드가 존재합니다.

이 새로운 객체를 생성하는 코드는 매우 짧고 간단하지만 그 자체로 충분히 독립적인 관심사를 담고 있습니다.

왜냐면 이러 코드는 현재 UserDao의 관심사인 JDBC API와 User 객체를 이용해서 DB에 정보를 넣고 빼는 것을 하는 것도 아니고

ConnectionMaker인터페이스로 대표되는 DB 커넥션을 어떻게 가져올 것인가라는 관심사도 아니기 때문입니다.

결론적으로는 UserDaoUserDao 가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 대한 관심을 가지고 있다

UserDao가 지닌 관심사와 new AconnectionMaker()라는 객체를 생성하는 관심사가 충돌하고 있기 때문에

결코 독립적으로 확장 가능한 클래스가 될 수 없습니다.

UserDao를 현재는 사용하는 클라이언트는 적어도 하나 존재합니다.

이러한 UserDao의 오브젝트의 기능을 사용하는 클라이언트가 UserDao를 사용하기 전에 ConnectionMaker의 구현 클래스를 사용할지 결정을 하도록 만들어 봅시다.

결론적으로 지금 우리가 하는일은 UserDao 오브제긑와 특정 클래스로 만들어진 ConnectionMaker 오브젝트 사이에 관계를 설정해주려고 하는 것 입니다.

현재 우리가 사용하고 있는 코드인 connectionMaker = new AConnectionMaker()

AconnectionMaker의 오브젝트의 Reference를 UserDao가 가지고 있는 connectionMaker라는 변수에 넣어서 사용하게 함으로써 사용이라는 서로의 관계를 맺어줍니다.

이런식으로 객체 사이의 관계가 만들어지기 위해서는 만들어진 객체가 있어야합니다.

지금까지 우리는 생성자를 통해서 객체를 만들어 왔습니다.

하지만 꼭 생성자를 사용하지 않고 외부에서 만들어준 것을 가져오는 방법도 존재합니다.

우리는 현재까지 생성자를 통해서 UserDao의 코드 내에서 객체를 생성해주었습니다.

이제 외부에서 만든 객체를 Parameter를 통해 가져온다면 사용을 할 수 있습니다.

이런 방식으로 한번 다시 리팩토링을 해봅시다.

현재는 connectionMaker = new AConnectionMaker()를 사용하고 있기 때문에 AConnectionMaker

UserDao가 불필요하게 알고 있습니다. 이러한 UserDao코드는 ConnectionMaker 인터페이스 외에는 어떤 클래스와도 관계를 가지고 있으면 안됩니다!!

클라이언트는 현재 UserDao를 자기가 사용해야 할 입장이기 때문에 UserDao의 세부 전략인

ConnectionMaker의 구현 클래스를 선택하고, 선택한 클래스의 객체를 생성하여 UserDao와 연결할 수 있습니다.

이러한 관심을 분리해서 클라이언트에게 떠넘기는 방식으로 Parameter를 통해 사용해 봅시다.

UserDao 객체가 사용할 ConnectionMaker를 파라미터를 통해 전달받을 수 있는 형식으로 바꾸면 될 것 같습니다.

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

이런식으로 하면 클라이언트에게 책임을 넘겼기 때문에 AConnectionMaker가 사라진 것을 볼 수 있습니다.

이를 이제 넘겨주는 UserTest라는 클래스를 하나 만들어서 이러한 UserTest에서 원래 실행했던 new AConnectionMaker()를 생성하도록 합니다.

그리고 UserTest안에서 UserDao객체를 파라미터를 넘겨 생성하도록 합니다.

public class UserDaoTest{
    public static void main(String[] args) throws ClassNotFoundException,
        SQLException{
            ConnectionMaker connectionMaker = new AConnectionMaker(); // 객체 생성

            //Userdao 객체를 생성해서 내가 사용할 ConnectionMaker 객체를 넘겨준다.
            UserDao dao = new UserDao(connectionMaker); 
    }
}

이제 이렇게 생성된 UserDaoTestUserDaoConnectionMkaer 구현 클래스와의 런타임 객체 의존 관계를 설정하는 책임을 담당해아합니다.

이런식으로 까지 변환이 완료되었다면 테스트를 해봅시다!!

이제 UserDao에 있으면 안 되는 다른 관심사항을 클라이억트로 책임일 떠넘기는 작업을 완료했습니다.

UserDaoTest 덕분에 A와 B회사는 자신들이 원하는 DB 접속 클래스를 만들어서 UserDao가 사용할 수 있도록 할 수 있습니다.

그저 각자의 회사에서 만든 객체를 넘겨주기만하면 됩니다.

이렇게 만든 코드에서 DB 커넥션을 가져오는 방법을 어떻게든 변경해도 이제 UserDao에 영향을 주지는 않습니다.

이제 DB 접속 방법에 대한 관심은 한 군데에 집중되어 DAO가 많아져도 DB접속 방법이 변경되었을 때 한 곳만 고쳐주어서 유지보수하기 편하게 되었습니다.

원칙과 패턴

지금까지 DAO 코드를 수정하면서 객체지향 기술의 여러가지 이론을 사용했습니다.

이제 다시한번 우리가 사용한 객체지향 기술에 대해서 어떤 이론을 사용했는지 보도록 합시다.

개방 폐쇄 원칙(OCP)

개방 폐쇄 원칙(Open-Closed Priciple)을 이용한다면 지금까지 해온 리팩토링 작업의 특징과 최종적으로 개선된 설계와 코드의 장점이 무엇인지 효과적으로 설명이 가능합니다.

이 원칙은 간단히 설명하면 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야한다.라는 것을 의미합니다.

우리가 작성한 UserDao는 DB 연결 방법이라는 기능을 확장하는데는 매우 열려있습니다.

또한, UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있게 되어 있습니다.

그리고 UserDao 는 자신의 핵심 기능을 구현한 코드는 변화에 영향을 받지 않고 유지가 가능하기 때문에 변경에는 닫혀 있다고 말할 수 있습니다.

제일 처음 작성했던 코드를 다시 살펴보자. 우리는 DB Connection에 대해서 변경을 하려고 한다면

DAO 내부의 모든 메서드에 대해서 수정을 해야 했었습니다.

결론적으로 DB Connection을 확장하려면, DAO 내부도 변경해야 하기 때문에 개방 폐쇄 원칙을 잘 따르지 못한 설계라고 할 수 있습니다.

높은 응집도와 낮은 결합도

이러한 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도 라는 소프트웨어 개발의 원리로 설명이 가능합니다.

  • 높은 응집도
    • 응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻입니다.
  • 낮은 결합도
    • 하나의 변경이 발생할 때 어려가지 모듈과 객체에 대해 변경 요구가 전파되지 않는 상태를 말합니다.

높은 응집도는 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명이 가능합니다.

우리가 제일 처음 작성한 DAO에 대해서는 DB 커넥션을 바꾸면 어떤 일을 해야하냐면

  1. 변경이 필요한 부분을 모든 코드를 뒤져서 찾는다.
  2. 변경한 것이 DAO의 다른 기능에 영향을 주어서 오류를 발생시키지 않는지 확인해야한다.

하지만 우리가 만약 인터페이스로 DB연결 기능을 독립 시켰다면

  1. CoonectionMaker 구현 클래스를 새로만든다.

이게 끝이다. 시간이 대폭 단축될 수 있습니다. 인터페이스로 구현을 한다면 DAO에 대해서 변화하는게 없기 때문에 모든 DAO를 일일이 테스트를 하지 않아도 됩니다.

낮은 결합도는 높은 응집도 보다 더 민감한 원칙입니다.

책임과 관심사가 다른 객체 또는 모듈과는 느슨하게 연결된 형태를 유지하는 것이 바람직 하다는 것입니다.

현재 우리가 작성한 코드는 인터페이스를 통해서 느슨하게 연결되게 하였습니다.

이렇게 변하면 독립적이기 때문에 결합도가 낮아지면 변화에 대응하는 속도가 높아지고 구성이 깔끔해집니다.

또한, 확장하기에도 매우 편리합니다.

전략 패턴

우리가 최종적으로 만든 UserDaoTest → UserDao → ConnectionMaker 구조는 디자인 패턴의 시각으로 보면 전략 패턴에 해당한다고 볼 수 있습니다.

전략 패턴이란 무엇이냐면

  • 자신의 기능 맥락(Context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스로 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴을 말합니다

우리가 만든 DAO를 보면 UserDao 는 전략 패턴의 맥락(Context)에 해당됩니다.

UserDao가 기능을 수행하는 데 필요한 기능 중에서 변경 가능한 DB Connection이라는 알고리즘은

ConnectionMaker라는 인터페이스에 정의를 했습니다.

우리는 전략이 바뀔 때 즉, DB Connection을 하는 방식이 바뀔 때 ConnectionMaker를 변경만 하면 됩니다. 이래서 전략 패턴이라고 불립니다.

이러한 전략 패턴UserDaoTest와 같은 클라이언트의 필요성에 대해서도 잘 설명하고 있습니다.

전략 패턴은 Context(UserDao)를 사용하는 클라이언트(UserDaoTest)가 사용하는 전략(ConnectionMaker를 구현하는 클래스)를 Context의 생성자를 통해서 제공해주는 것이 일반적입니다.

Reference

  • 토비의 스프링 3.1 Vol. 1: 스프링의 이해와 원리

+ Recent posts